aislop 0.1.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.
@@ -0,0 +1,30 @@
1
+ import { r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DBG3uXLc.js";
2
+
3
+ //#region src/output/json.ts
4
+ const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
5
+ const allDiagnostics = results.flatMap((r) => r.diagnostics);
6
+ const engines = {};
7
+ for (const result of results) engines[result.engine] = {
8
+ issues: result.diagnostics.length,
9
+ skipped: result.skipped,
10
+ elapsed: result.elapsed
11
+ };
12
+ return {
13
+ version: APP_VERSION,
14
+ score: scoreResult.score,
15
+ label: scoreResult.label,
16
+ engines,
17
+ engineDefinitions: ENGINE_INFO,
18
+ diagnostics: allDiagnostics,
19
+ summary: {
20
+ errors: allDiagnostics.filter((d) => d.severity === "error").length,
21
+ warnings: allDiagnostics.filter((d) => d.severity === "warning").length,
22
+ fixable: allDiagnostics.filter((d) => d.fixable).length,
23
+ files: fileCount,
24
+ elapsed: elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`
25
+ }
26
+ };
27
+ };
28
+
29
+ //#endregion
30
+ export { buildJsonOutput };
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { n as ENGINE_INFO, t as APP_VERSION } from "./cli.js";
3
+
4
+ //#region src/output/json.ts
5
+ const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
6
+ const allDiagnostics = results.flatMap((r) => r.diagnostics);
7
+ const engines = {};
8
+ for (const result of results) engines[result.engine] = {
9
+ issues: result.diagnostics.length,
10
+ skipped: result.skipped,
11
+ elapsed: result.elapsed
12
+ };
13
+ return {
14
+ version: APP_VERSION,
15
+ score: scoreResult.score,
16
+ label: scoreResult.label,
17
+ engines,
18
+ engineDefinitions: ENGINE_INFO,
19
+ diagnostics: allDiagnostics,
20
+ summary: {
21
+ errors: allDiagnostics.filter((d) => d.severity === "error").length,
22
+ warnings: allDiagnostics.filter((d) => d.severity === "warning").length,
23
+ fixable: allDiagnostics.filter((d) => d.fixable).length,
24
+ files: fileCount,
25
+ elapsed: elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`
26
+ }
27
+ };
28
+ };
29
+
30
+ //#endregion
31
+ export { buildJsonOutput };
@@ -0,0 +1,59 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ //#region src/utils/subprocess.ts
4
+ const runSubprocess = (command, args, options = {}) => {
5
+ return new Promise((resolve, reject) => {
6
+ const child = spawn(command, args, {
7
+ cwd: options.cwd,
8
+ env: {
9
+ ...process.env,
10
+ ...options.env
11
+ },
12
+ stdio: [
13
+ "ignore",
14
+ "pipe",
15
+ "pipe"
16
+ ],
17
+ windowsHide: true
18
+ });
19
+ const stdoutBuffers = [];
20
+ const stderrBuffers = [];
21
+ child.stdout?.on("data", (buffer) => stdoutBuffers.push(buffer));
22
+ child.stderr?.on("data", (buffer) => stderrBuffers.push(buffer));
23
+ let settled = false;
24
+ let timer;
25
+ const finalize = (callback) => {
26
+ if (settled) return;
27
+ settled = true;
28
+ if (timer) clearTimeout(timer);
29
+ callback();
30
+ };
31
+ if (options.timeout && options.timeout > 0) {
32
+ timer = setTimeout(() => {
33
+ child.kill("SIGTERM");
34
+ setTimeout(() => child.kill("SIGKILL"), 1e3).unref();
35
+ finalize(() => reject(/* @__PURE__ */ new Error(`Command timed out after ${options.timeout}ms: ${command}`)));
36
+ }, options.timeout);
37
+ timer.unref();
38
+ }
39
+ child.once("error", (error) => finalize(() => reject(/* @__PURE__ */ new Error(`Failed to run ${command}: ${error.message}`))));
40
+ child.once("close", (code) => {
41
+ finalize(() => resolve({
42
+ stdout: Buffer.concat(stdoutBuffers).toString("utf-8").trim(),
43
+ stderr: Buffer.concat(stderrBuffers).toString("utf-8").trim(),
44
+ exitCode: code
45
+ }));
46
+ });
47
+ });
48
+ };
49
+ const isToolInstalled = async (tool) => {
50
+ try {
51
+ const result = await runSubprocess("which", [tool]);
52
+ return result.exitCode === 0 && result.stdout.length > 0;
53
+ } catch {
54
+ return false;
55
+ }
56
+ };
57
+
58
+ //#endregion
59
+ export { runSubprocess as n, isToolInstalled as t };
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "aislop",
3
+ "version": "0.1.0",
4
+ "description": "Stop AI slop from shipping. A unified code quality CLI that catches the lazy patterns AI coding tools leave behind.",
5
+ "type": "module",
6
+ "bin": {
7
+ "aislop": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "scripts"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "dev": "tsdown --watch",
21
+ "build": "rm -rf dist && NODE_ENV=production tsdown",
22
+ "postinstall": "node scripts/postinstall-tools.mjs",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "pnpm build && vitest run",
25
+ "scan": "pnpm build && node dist/cli.js scan .",
26
+ "scan:json": "pnpm build && node dist/cli.js scan . --json",
27
+ "quality": "pnpm typecheck && pnpm test && node dist/cli.js scan . --json"
28
+ },
29
+ "keywords": [
30
+ "aislop",
31
+ "ai-slop",
32
+ "code-quality",
33
+ "linter",
34
+ "formatter",
35
+ "cli",
36
+ "ai",
37
+ "copilot",
38
+ "code-review",
39
+ "static-analysis",
40
+ "typescript",
41
+ "javascript",
42
+ "python",
43
+ "go",
44
+ "rust",
45
+ "ruby",
46
+ "php"
47
+ ],
48
+ "author": "heavykenny",
49
+ "license": "MIT",
50
+ "homepage": "https://github.com/heavykenny/aislop#readme",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/heavykenny/aislop.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/heavykenny/aislop/issues"
57
+ },
58
+ "engines": {
59
+ "node": ">=20"
60
+ },
61
+ "packageManager": "pnpm@10.28.0",
62
+ "dependencies": {
63
+ "@biomejs/biome": "^2.4.5",
64
+ "adm-zip": "^0.5.16",
65
+ "commander": "^14.0.3",
66
+ "expo-doctor": "^1.18.10",
67
+ "knip": "^5.85.0",
68
+ "ora": "^9.3.0",
69
+ "oxlint": "^1.51.0",
70
+ "picocolors": "^1.1.1",
71
+ "tar": "^7.5.11",
72
+ "yaml": "^2.8.2",
73
+ "zod": "^4.3.6"
74
+ },
75
+ "devDependencies": {
76
+ "@types/node": "^25.3.3",
77
+ "tsdown": "^0.20.3",
78
+ "typescript": "^5.9.3",
79
+ "vitest": "^4.0.18"
80
+ }
81
+ }
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { Readable } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { fileURLToPath } from "node:url";
9
+ import AdmZip from "adm-zip";
10
+ import * as tar from "tar";
11
+
12
+ const THIS_FILE = fileURLToPath(import.meta.url);
13
+ const PACKAGE_ROOT = path.resolve(path.dirname(THIS_FILE), "..");
14
+ const TOOLS_BIN_DIR = path.join(PACKAGE_ROOT, "tools", "bin");
15
+ const USER_AGENT = "aislop-installer";
16
+
17
+ const PLATFORM_KEY = `${process.platform}-${process.arch}`;
18
+
19
+ const TOOL_DEFINITIONS = [
20
+ {
21
+ name: "ruff",
22
+ repo: "astral-sh/ruff",
23
+ version: "0.15.4",
24
+ tag: "0.15.4",
25
+ binaryName: "ruff",
26
+ assets: {
27
+ "darwin-arm64": ["ruff-aarch64-apple-darwin.tar.gz"],
28
+ "darwin-x64": ["ruff-x86_64-apple-darwin.tar.gz"],
29
+ "linux-arm64": ["ruff-aarch64-unknown-linux-gnu.tar.gz"],
30
+ "linux-x64": ["ruff-x86_64-unknown-linux-gnu.tar.gz"],
31
+ "win32-arm64": ["ruff-aarch64-pc-windows-msvc.zip"],
32
+ "win32-x64": ["ruff-x86_64-pc-windows-msvc.zip"],
33
+ },
34
+ },
35
+ {
36
+ name: "golangci-lint",
37
+ repo: "golangci/golangci-lint",
38
+ version: "2.10.1",
39
+ tag: "v2.10.1",
40
+ binaryName: "golangci-lint",
41
+ assets: {
42
+ "darwin-arm64": [
43
+ "golangci-lint-2.10.1-darwin-arm64.tar.gz",
44
+ "golangci-lint-v2.10.1-darwin-arm64.tar.gz",
45
+ ],
46
+ "darwin-x64": [
47
+ "golangci-lint-2.10.1-darwin-amd64.tar.gz",
48
+ "golangci-lint-v2.10.1-darwin-amd64.tar.gz",
49
+ ],
50
+ "linux-arm64": [
51
+ "golangci-lint-2.10.1-linux-arm64.tar.gz",
52
+ "golangci-lint-v2.10.1-linux-arm64.tar.gz",
53
+ ],
54
+ "linux-x64": [
55
+ "golangci-lint-2.10.1-linux-amd64.tar.gz",
56
+ "golangci-lint-v2.10.1-linux-amd64.tar.gz",
57
+ ],
58
+ "win32-arm64": [
59
+ "golangci-lint-2.10.1-windows-arm64.zip",
60
+ "golangci-lint-v2.10.1-windows-arm64.zip",
61
+ ],
62
+ "win32-x64": [
63
+ "golangci-lint-2.10.1-windows-amd64.zip",
64
+ "golangci-lint-v2.10.1-windows-amd64.zip",
65
+ ],
66
+ },
67
+ },
68
+ ];
69
+
70
+ const isWindows = process.platform === "win32";
71
+ const withExecutableExtension = (name) => (isWindows ? `${name}.exe` : name);
72
+
73
+ const info = (message) => console.log(`[aislop] ${message}`);
74
+ const warn = (message) => console.warn(`[aislop] ${message}`);
75
+
76
+ const downloadFile = async (url, destination) => {
77
+ const response = await fetch(url, {
78
+ headers: { "User-Agent": USER_AGENT },
79
+ });
80
+ if (!response.ok || !response.body) {
81
+ throw new Error(`Failed to download ${url} (${response.status})`);
82
+ }
83
+ await pipeline(
84
+ Readable.fromWeb(response.body),
85
+ fs.createWriteStream(destination),
86
+ );
87
+ };
88
+
89
+ const extractArchive = async (archivePath, extractDir) => {
90
+ if (archivePath.endsWith(".tar.gz")) {
91
+ await tar.x({ file: archivePath, cwd: extractDir });
92
+ return;
93
+ }
94
+ if (archivePath.endsWith(".zip")) {
95
+ const zip = new AdmZip(archivePath);
96
+ zip.extractAllTo(extractDir, true);
97
+ return;
98
+ }
99
+ throw new Error(`Unsupported archive format for ${archivePath}`);
100
+ };
101
+
102
+ const getTagCandidates = (tag) => {
103
+ if (tag.startsWith("v")) {
104
+ return [tag, tag.slice(1)];
105
+ }
106
+ return [tag, `v${tag}`];
107
+ };
108
+
109
+ const getAssetUrls = (tool, assetName) =>
110
+ getTagCandidates(tool.tag).map(
111
+ (tag) =>
112
+ `https://github.com/${tool.repo}/releases/download/${tag}/${assetName}`,
113
+ );
114
+
115
+ const downloadFromCandidates = async (urls, archivePath) => {
116
+ const failures = [];
117
+ for (const url of urls) {
118
+ try {
119
+ await downloadFile(url, archivePath);
120
+ return url;
121
+ } catch (error) {
122
+ failures.push(error instanceof Error ? error.message : String(error));
123
+ }
124
+ }
125
+ throw new Error(
126
+ `Could not download from candidate URLs: ${failures.join(" | ")}`,
127
+ );
128
+ };
129
+
130
+ const findBinary = (rootDir, binaryName) => {
131
+ const queue = [rootDir];
132
+ while (queue.length > 0) {
133
+ const current = queue.shift();
134
+ if (!current) continue;
135
+ const entries = fs.readdirSync(current, { withFileTypes: true });
136
+ for (const entry of entries) {
137
+ const fullPath = path.join(current, entry.name);
138
+ if (entry.isDirectory()) {
139
+ queue.push(fullPath);
140
+ continue;
141
+ }
142
+ if (entry.name === binaryName) return fullPath;
143
+ }
144
+ }
145
+ return null;
146
+ };
147
+
148
+ const installTool = async (tool) => {
149
+ const assetNames = tool.assets[PLATFORM_KEY];
150
+ if (!assetNames || assetNames.length === 0) {
151
+ warn(`No bundled ${tool.name} build for ${PLATFORM_KEY}; skipping.`);
152
+ return false;
153
+ }
154
+
155
+ const destinationBinary = path.join(
156
+ TOOLS_BIN_DIR,
157
+ withExecutableExtension(tool.binaryName),
158
+ );
159
+ if (fs.existsSync(destinationBinary)) {
160
+ info(`${tool.name} already present.`);
161
+ return true;
162
+ }
163
+
164
+ const tempDir = fs.mkdtempSync(
165
+ path.join(os.tmpdir(), `aislop-${tool.name}-`),
166
+ );
167
+ const archivePath = path.join(tempDir, assetNames[0]);
168
+ const extractDir = path.join(tempDir, "extract");
169
+ fs.mkdirSync(extractDir, { recursive: true });
170
+
171
+ try {
172
+ const candidateUrls = assetNames.flatMap((assetName) =>
173
+ getAssetUrls(tool, assetName),
174
+ );
175
+ info(`Downloading ${tool.name} ${tool.version}...`);
176
+ await downloadFromCandidates(candidateUrls, archivePath);
177
+ await extractArchive(archivePath, extractDir);
178
+
179
+ const extractedBinary = findBinary(
180
+ extractDir,
181
+ withExecutableExtension(tool.binaryName),
182
+ );
183
+ if (!extractedBinary) {
184
+ throw new Error(
185
+ `Unable to locate ${tool.binaryName} in extracted archive`,
186
+ );
187
+ }
188
+
189
+ fs.mkdirSync(TOOLS_BIN_DIR, { recursive: true });
190
+ fs.copyFileSync(extractedBinary, destinationBinary);
191
+ if (!isWindows) fs.chmodSync(destinationBinary, 0o755);
192
+
193
+ info(`Installed bundled ${tool.name} at ${destinationBinary}`);
194
+ return true;
195
+ } finally {
196
+ fs.rmSync(tempDir, { recursive: true, force: true });
197
+ }
198
+ };
199
+
200
+ const main = async () => {
201
+ if (process.env.AISLOP_SKIP_TOOL_DOWNLOAD === "1") {
202
+ info("Skipping bundled tool download (AISLOP_SKIP_TOOL_DOWNLOAD=1).");
203
+ return;
204
+ }
205
+
206
+ const failures = [];
207
+ for (const tool of TOOL_DEFINITIONS) {
208
+ try {
209
+ const installed = await installTool(tool);
210
+ if (!installed) {
211
+ failures.push(`${tool.name}: unsupported platform`);
212
+ }
213
+ } catch (error) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ failures.push(`${tool.name}: ${message}`);
216
+ warn(`Failed to install ${tool.name}: ${message}`);
217
+ }
218
+ }
219
+
220
+ if (failures.length > 0) {
221
+ warn("Some bundled tools could not be installed:");
222
+ for (const failure of failures) {
223
+ warn(` - ${failure}`);
224
+ }
225
+ warn(
226
+ "aislop will still run, but coverage for those tools may be reduced until installation succeeds.",
227
+ );
228
+ }
229
+ };
230
+
231
+ main().catch((error) => {
232
+ warn(
233
+ `postinstall failed: ${error instanceof Error ? error.message : String(error)}`,
234
+ );
235
+ });