aislop 0.6.2 → 0.7.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
@@ -5,6 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/aislop.svg)](https://www.npmjs.com/package/aislop)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/aislop.svg)](https://www.npmjs.com/package/aislop)
7
7
  [![CI](https://github.com/scanaislop/aislop/actions/workflows/ci.yml/badge.svg)](https://github.com/scanaislop/aislop/actions/workflows/ci.yml)
8
+ [![aislop score](https://badges.scanaislop.com/score/scanaislop/aislop.svg)](https://scanaislop.com/scanaislop/aislop)
8
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
10
  [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
10
11
 
@@ -34,7 +35,7 @@ npx aislop fix
34
35
  # CI mode (JSON output + quality gate)
35
36
  npx aislop ci
36
37
 
37
- # wire aislop into your agent so it runs on every edit (new in 0.6.0)
38
+ # wire aislop into your agent so it runs on every edit
38
39
  npx aislop hook install --claude
39
40
  ```
40
41
 
@@ -46,7 +47,7 @@ Sample output:
46
47
  [!] Code Quality: done (2 warnings, 812ms)
47
48
  [!] AI Slop: done (4 warnings, 455ms)
48
49
  [ok] Security: done (0 issues, 1.3s)
49
- aislop 0.6.2 · the quality gate for agentic coding
50
+ aislop 0.7.0 · the quality gate for agentic coding
50
51
 
51
52
  scan · my-app · typescript · 142 files
52
53
 
@@ -78,7 +79,7 @@ AI coding tools generate code that compiles and passes tests but ships with patt
78
79
 
79
80
  - **One score, one gate**: a 0-100 number you can enforce in CI with `aislop ci`. Weighted so sloppy patterns (dead code, `as any`, swallowed errors) hit harder than style noise.
80
81
  - **Auto-fix first, agent second**: `aislop fix` clears what's mechanically safe (formatters, unused imports, trivial comments, dead patterns). For the rest, one flag hands off to Claude Code, Codex, Cursor, Gemini, Windsurf, Amp, Aider, Goose, and 7 more, with full diagnostic context pre-filled.
81
- - **Wire it into your agent (new in 0.6.0)**: `aislop hook install` plugs aislop into Claude Code, Cursor, Gemini CLI (runtime), plus Codex, Windsurf, Cline, Kilo Code, Antigravity, and Copilot (rules-only). The agent gets score + findings on the turn it wrote the code, not after.
82
+ - **Wire it into your agent**: `aislop hook install` plugs aislop into Claude Code, Cursor, Gemini CLI (runtime), plus Codex, Windsurf, Cline, Kilo Code, Antigravity, and Copilot (rules-only). The agent gets score + findings on the turn it wrote the code, not after.
82
83
  - **Deterministic**: regex, AST, and standard tooling. No LLMs, no API keys, no network dependency. Same repo in, same score out.
83
84
  - **Zero-config start**: `npx aislop scan` works on any repo. Add `.aislop/config.yml` when you want to tune thresholds or enable the architecture engine.
84
85
  - **Works across stacks**: TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo / React Native.
@@ -154,6 +155,18 @@ aislop scan --exclude "src/generated" --exclude "**/*.spec.*"
154
155
 
155
156
  CLI flags beat config; config beats defaults.
156
157
 
158
+ **Extend a shared config.** A project config can extend a parent and override specific keys. Useful for org-wide baselines: ship one strict config, let each repo soften or tighten as needed.
159
+
160
+ ```yaml
161
+ # .aislop/config.yml
162
+ extends: ../../.aislop/base.yml # relative path to a parent config
163
+
164
+ ci:
165
+ failBelow: 80 # override just this key, inherit the rest
166
+ ```
167
+
168
+ `extends:` accepts a single path or an array of paths. Later entries win. Deep-merged: nested objects (`scoring.weights`, `engines`) are merged key-by-key; arrays are replaced. Circular references and depths beyond 5 are rejected with a clear error.
169
+
157
170
  ### Fix issues automatically
158
171
 
159
172
  ```bash
@@ -237,6 +250,7 @@ aislop scan
237
250
  aislop init # create .aislop/config.yml
238
251
  aislop doctor # check which tools are available
239
252
  aislop rules # list all built-in rules
253
+ aislop badge # print the public score badge URL + README snippet
240
254
  aislop hook install # wire aislop into your coding agent
241
255
  aislop # interactive menu
242
256
  ```
@@ -313,6 +327,18 @@ ci:
313
327
 
314
328
  The CLI is MIT-licensed and always will be. [Learn more about the platform →](https://scanaislop.com)
315
329
 
330
+ ## Public score badge
331
+
332
+ Show your aislop score on a README. Free for any project that opts in on [scanaislop.com](https://scanaislop.com).
333
+
334
+ ```markdown
335
+ [![aislop](https://badges.scanaislop.com/score/<owner>/<repo>.svg)](https://scanaislop.com)
336
+ ```
337
+
338
+ Shields-compatible SVG, edge-cached on Cloudflare. Colour-coded: green ≥ 85, amber 70-84, red < 70, grey if no scans yet.
339
+
340
+ Run `aislop badge` to print the snippet pre-filled with your repo's owner/name, auto-detected from `git remote get-url origin`.
341
+
316
342
  ## Contributing
317
343
 
318
344
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how to add new rules. AI coding assistants can find project context in [AGENTS.md](AGENTS.md).
@@ -330,7 +356,15 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how to add new
330
356
 
331
357
  ## Contributors
332
358
 
333
- [![Contributors](https://contrib.rocks/image?repo=scanaislop/aislop)](https://github.com/scanaislop/aislop/graphs/contributors)
359
+ Thanks to everyone who has shipped code, ideas, docs, or bug reports.
360
+
361
+ <!-- CONTRIBUTORS-START -->
362
+ - [@heavykenny](https://github.com/heavykenny)
363
+ - [@myke-awoniran](https://github.com/myke-awoniran)
364
+ - [@yashrajoria](https://github.com/yashrajoria)
365
+ <!-- CONTRIBUTORS-END -->
366
+
367
+ This list is regenerated by `.github/workflows/contributors.yml` after every push to `develop` or `main`. The workflow reads git log, resolves each author's GitHub login, and opens a PR with any diff. If your commits aren't being credited, either link your commit email under [GitHub Settings → Emails](https://github.com/settings/emails) or add a mapping to [`.github/contributors-overrides.json`](.github/contributors-overrides.json).
334
368
 
335
369
  ## License
336
370
 
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import path from "node:path";
7
7
  import YAML from "yaml";
8
8
  import { z } from "zod/v4";
9
9
  import { performance } from "node:perf_hooks";
10
- import { spawn, spawnSync } from "node:child_process";
10
+ import { execSync, spawn, spawnSync } from "node:child_process";
11
11
  import micromatch from "micromatch";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import ts from "typescript";
@@ -209,6 +209,48 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
209
209
  # severity: error
210
210
  `;
211
211
 
212
+ //#endregion
213
+ //#region src/config/extends.ts
214
+ const MAX_DEPTH = 5;
215
+ const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
216
+ const deepMerge = (...sources) => {
217
+ const result = {};
218
+ for (const source of sources) for (const key of Object.keys(source)) {
219
+ const a = result[key];
220
+ const b = source[key];
221
+ result[key] = isPlainObject(a) && isPlainObject(b) ? deepMerge(a, b) : b;
222
+ }
223
+ return result;
224
+ };
225
+ const resolveExtendsRef = (ref, fromDir) => {
226
+ if (ref.startsWith("http://") || ref.startsWith("https://")) throw new Error(`URL-based extends not yet supported: ${ref}`);
227
+ if (ref.startsWith("./") || ref.startsWith("../") || path.isAbsolute(ref)) return path.resolve(fromDir, ref);
228
+ throw new Error(`Package-name extends not yet supported: ${ref} (use a relative path for now)`);
229
+ };
230
+ const normalizeExtends = (raw) => {
231
+ if (raw === void 0 || raw === null) return [];
232
+ if (typeof raw === "string") return [raw];
233
+ if (Array.isArray(raw) && raw.every((s) => typeof s === "string")) return raw;
234
+ throw new Error("`extends` must be a string or array of strings");
235
+ };
236
+ const loadConfigChain = (configPath, visited = /* @__PURE__ */ new Set(), depth = 0) => {
237
+ if (depth > MAX_DEPTH) throw new Error(`extends depth exceeded ${MAX_DEPTH} (cycle or runaway chain): ${configPath}`);
238
+ const absPath = path.resolve(configPath);
239
+ if (visited.has(absPath)) throw new Error(`circular extends detected: ${absPath}`);
240
+ if (!fs.existsSync(absPath)) throw new Error(`extends target not found: ${absPath}`);
241
+ const nextVisited = new Set(visited);
242
+ nextVisited.add(absPath);
243
+ const raw = fs.readFileSync(absPath, "utf-8");
244
+ const parsed = YAML.parse(raw) ?? {};
245
+ const refs = normalizeExtends(parsed.extends);
246
+ const fromDir = path.dirname(absPath);
247
+ const parents = refs.map((ref) => {
248
+ return loadConfigChain(resolveExtendsRef(ref, fromDir), nextVisited, depth + 1);
249
+ });
250
+ const { extends: _drop, ...own } = parsed;
251
+ return deepMerge(...parents, own);
252
+ };
253
+
212
254
  //#endregion
213
255
  //#region src/config/schema.ts
214
256
  const DEFAULT_WEIGHTS = {
@@ -343,8 +385,7 @@ const loadConfig = (directory) => {
343
385
  const configPath = path.join(configDir, CONFIG_FILE);
344
386
  if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
345
387
  try {
346
- const raw = fs.readFileSync(configPath, "utf-8");
347
- return parseConfig(YAML.parse(raw));
388
+ return parseConfig(loadConfigChain(configPath));
348
389
  } catch (error) {
349
390
  const msg = error instanceof Error ? error.message : String(error);
350
391
  process.stderr.write(` ⚠ Failed to parse ${configPath}: ${msg}\n ⚠ Using default configuration.\n`);
@@ -5918,6 +5959,83 @@ const registerHookCommand = (program) => {
5918
5959
  registerCallbacks(hook);
5919
5960
  };
5920
5961
 
5962
+ //#endregion
5963
+ //#region src/commands/badge.ts
5964
+ const GITHUB_REMOTE_RE = /^(?:git@github\.com:|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
5965
+ const renderBadgeOutput = ({ owner, repo, svgUrl, pageUrl }) => {
5966
+ const slug = `${owner}/${repo}`;
5967
+ const markdown = `[![aislop](${svgUrl})](${pageUrl})`;
5968
+ return [
5969
+ ``,
5970
+ ` Repository: ${slug}`,
5971
+ ` Badge URL: ${svgUrl}`,
5972
+ ``,
5973
+ ` Markdown:`,
5974
+ ``,
5975
+ ` ${markdown}`,
5976
+ ``,
5977
+ ` Drop the line above into your README. The badge auto-updates after every public scan.`,
5978
+ ``
5979
+ ].join("\n");
5980
+ };
5981
+ const detectGithubSlugFromGit = (directory) => {
5982
+ let raw;
5983
+ try {
5984
+ raw = execSync("git remote get-url origin", {
5985
+ cwd: path.resolve(directory),
5986
+ encoding: "utf-8",
5987
+ stdio: [
5988
+ "ignore",
5989
+ "pipe",
5990
+ "ignore"
5991
+ ]
5992
+ });
5993
+ } catch {
5994
+ return null;
5995
+ }
5996
+ const match = raw.trim().match(GITHUB_REMOTE_RE);
5997
+ if (!match) return null;
5998
+ const owner = match[1];
5999
+ const repo = match[2];
6000
+ if (!owner || !repo) return null;
6001
+ return {
6002
+ owner,
6003
+ repo
6004
+ };
6005
+ };
6006
+ const badgeCommand = async (options = {}) => {
6007
+ let owner = options.owner?.trim();
6008
+ let repo = options.repo?.trim();
6009
+ if (!owner || !repo) {
6010
+ const detected = detectGithubSlugFromGit(options.directory ?? ".");
6011
+ if (!detected) throw new Error("Could not detect a GitHub remote. Run from a repo with `git remote get-url origin` set, or pass --owner and --repo.");
6012
+ owner ??= detected.owner;
6013
+ repo ??= detected.repo;
6014
+ }
6015
+ const svgUrl = `https://badges.scanaislop.com/score/${owner}/${repo}.svg`;
6016
+ const pageUrl = `https://scanaislop.com/${owner}/${repo}`;
6017
+ const output = renderBadgeOutput({
6018
+ owner,
6019
+ repo,
6020
+ svgUrl,
6021
+ pageUrl
6022
+ });
6023
+ if (options.json) process.stdout.write(JSON.stringify({
6024
+ owner,
6025
+ repo,
6026
+ svgUrl,
6027
+ pageUrl
6028
+ }) + "\n");
6029
+ else process.stdout.write(output);
6030
+ return {
6031
+ owner,
6032
+ repo,
6033
+ svgUrl,
6034
+ pageUrl,
6035
+ output
6036
+ };
6037
+ };
6038
+
5921
6039
  //#endregion
5922
6040
  //#region src/ui/symbols.ts
5923
6041
  const TTY = {
@@ -6334,7 +6452,7 @@ const renderCleanRun = (input, deps = {}) => {
6334
6452
 
6335
6453
  //#endregion
6336
6454
  //#region src/version.ts
6337
- const APP_VERSION = "0.6.2";
6455
+ const APP_VERSION = "0.7.0";
6338
6456
 
6339
6457
  //#endregion
6340
6458
  //#region src/utils/telemetry.ts
@@ -8980,6 +9098,20 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
8980
9098
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
8981
9099
  await rulesCommand(directory);
8982
9100
  });
9101
+ program.command("badge [directory]").description("Print the public score badge URL + README markdown for this repo").option("--owner <owner>", "GitHub owner (auto-detected from git remote if omitted)").option("--repo <repo>", "GitHub repo name (auto-detected from git remote if omitted)").option("--json", "emit machine-readable JSON instead of the rendered output").action(async (directory = ".", _flags, command) => {
9102
+ const flags = command.optsWithGlobals();
9103
+ try {
9104
+ await badgeCommand({
9105
+ directory,
9106
+ owner: flags.owner,
9107
+ repo: flags.repo,
9108
+ json: Boolean(flags.json)
9109
+ });
9110
+ } catch (err) {
9111
+ process.stderr.write(`${err?.message ?? "Failed to print badge"}\n`);
9112
+ process.exit(1);
9113
+ }
9114
+ });
8983
9115
  registerHookCommand(program);
8984
9116
  const main = async () => {
8985
9117
  await program.parseAsync();
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-AmNwcw_U.js";
1
+ import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-BOJR1S8l.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
3
3
  import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
4
4
  import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
@@ -103,6 +103,48 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
103
103
  # severity: error
104
104
  `;
105
105
 
106
+ //#endregion
107
+ //#region src/config/extends.ts
108
+ const MAX_DEPTH = 5;
109
+ const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
110
+ const deepMerge = (...sources) => {
111
+ const result = {};
112
+ for (const source of sources) for (const key of Object.keys(source)) {
113
+ const a = result[key];
114
+ const b = source[key];
115
+ result[key] = isPlainObject(a) && isPlainObject(b) ? deepMerge(a, b) : b;
116
+ }
117
+ return result;
118
+ };
119
+ const resolveExtendsRef = (ref, fromDir) => {
120
+ if (ref.startsWith("http://") || ref.startsWith("https://")) throw new Error(`URL-based extends not yet supported: ${ref}`);
121
+ if (ref.startsWith("./") || ref.startsWith("../") || path.isAbsolute(ref)) return path.resolve(fromDir, ref);
122
+ throw new Error(`Package-name extends not yet supported: ${ref} (use a relative path for now)`);
123
+ };
124
+ const normalizeExtends = (raw) => {
125
+ if (raw === void 0 || raw === null) return [];
126
+ if (typeof raw === "string") return [raw];
127
+ if (Array.isArray(raw) && raw.every((s) => typeof s === "string")) return raw;
128
+ throw new Error("`extends` must be a string or array of strings");
129
+ };
130
+ const loadConfigChain = (configPath, visited = /* @__PURE__ */ new Set(), depth = 0) => {
131
+ if (depth > MAX_DEPTH) throw new Error(`extends depth exceeded ${MAX_DEPTH} (cycle or runaway chain): ${configPath}`);
132
+ const absPath = path.resolve(configPath);
133
+ if (visited.has(absPath)) throw new Error(`circular extends detected: ${absPath}`);
134
+ if (!fs.existsSync(absPath)) throw new Error(`extends target not found: ${absPath}`);
135
+ const nextVisited = new Set(visited);
136
+ nextVisited.add(absPath);
137
+ const raw = fs.readFileSync(absPath, "utf-8");
138
+ const parsed = YAML.parse(raw) ?? {};
139
+ const refs = normalizeExtends(parsed.extends);
140
+ const fromDir = path.dirname(absPath);
141
+ const parents = refs.map((ref) => {
142
+ return loadConfigChain(resolveExtendsRef(ref, fromDir), nextVisited, depth + 1);
143
+ });
144
+ const { extends: _drop, ...own } = parsed;
145
+ return deepMerge(...parents, own);
146
+ };
147
+
106
148
  //#endregion
107
149
  //#region src/config/schema.ts
108
150
  const DEFAULT_WEIGHTS = {
@@ -237,8 +279,7 @@ const loadConfig = (directory) => {
237
279
  const configPath = path.join(configDir, CONFIG_FILE);
238
280
  if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
239
281
  try {
240
- const raw = fs.readFileSync(configPath, "utf-8");
241
- return parseConfig(YAML.parse(raw));
282
+ return parseConfig(loadConfigChain(configPath));
242
283
  } catch (error) {
243
284
  const msg = error instanceof Error ? error.message : String(error);
244
285
  process.stderr.write(` ⚠ Failed to parse ${configPath}: ${msg}\n ⚠ Using default configuration.\n`);
@@ -5341,7 +5382,7 @@ const scanCommand = async (directory, config, options) => {
5341
5382
  });
5342
5383
  }
5343
5384
  if (options.json) {
5344
- const { buildJsonOutput } = await import("./json-ZItDVIZL.js");
5385
+ const { buildJsonOutput } = await import("./json-DwAcCqqG.js");
5345
5386
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
5346
5387
  console.log(JSON.stringify(jsonOut, null, 2));
5347
5388
  return { exitCode };
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, t as APP_VERSION } from "./version-AmNwcw_U.js";
1
+ import { n as ENGINE_INFO, t as APP_VERSION } from "./version-BOJR1S8l.js";
2
2
 
3
3
  //#region src/output/json.ts
4
4
  const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
@@ -29,7 +29,7 @@ const getEngineLabel = (engine) => ENGINE_INFO[engine].label;
29
29
 
30
30
  //#endregion
31
31
  //#region src/version.ts
32
- const APP_VERSION = "0.6.2";
32
+ const APP_VERSION = "0.7.0";
33
33
 
34
34
  //#endregion
35
35
  export { ENGINE_INFO as n, getEngineLabel as r, APP_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aislop",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Stop AI slop from shipping. A unified code quality CLI that catches the lazy patterns AI coding tools leave behind.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -81,5 +81,10 @@
81
81
  "@types/node": "^25.6.0",
82
82
  "tsdown": "^0.20.3",
83
83
  "vitest": "^4.0.18"
84
+ },
85
+ "pnpm": {
86
+ "overrides": {
87
+ "postcss@<8.5.10": "^8.5.10"
88
+ }
84
89
  }
85
90
  }