# Scanner CLI reference

> Invocation, path argument, --json mode, walk/scan behavior, console formatting helpers, and hard constraint that the scanner never edits files.

- Repository: yetone/kill-ai-slop
- GitHub: https://github.com/yetone/kill-ai-slop
- Human docs: https://grok-wiki.com/public/docs/yetone-kill-ai-slop-c0d8c9670cbb
- Complete Markdown: https://grok-wiki.com/public/docs/yetone-kill-ai-slop-c0d8c9670cbb/llms-full.txt

## Source Files

- `skill/scripts/scan.mjs`
- `skill/README.md`
- `skill/references/detection.md`
- `README.md`

---

---
title: "Scanner CLI reference"
description: "Invocation, path argument, --json mode, walk/scan behavior, console formatting helpers, and hard constraint that the scanner never edits files."
---

`skill/scripts/scan.mjs` is a dependency-free Node CLI that walks frontend source under a root path, matches line-level patterns for 23 AI-slop tells, and prints either a grouped console report or JSON. It only uses `node:fs` and `node:path`, and its header states it **NEVER edits files**.

## Invocation

```bash
node skill/scripts/scan.mjs [root] [--json] [--no-color]
```

From inside the installed skill directory (paths relative to `skill/`):

```bash
node scripts/scan.mjs path/to/src          # human-readable report
node scripts/scan.mjs path/to/src --json   # machine-readable
```

From the repository root (as in the project README):

```bash
node skill/scripts/scan.mjs path/to/project
node skill/scripts/scan.mjs path/to/project --json
```

The script shebang is `#!/usr/bin/env node`. No npm packages, install step, or build are required.

## Arguments and flags

| Input | Type | Default | Behavior |
|---|---|---|---|
| `[root]` | path (non-flag argv) | `"."` | First `process.argv` entry that does not start with `-`. Directory tree root for `walk()`. |
| `--json` | flag | off | Emit a single JSON document on stdout; skip ANSI color and the human report. Exit with `process.exit(0)` after print. |
| `--no-color` | flag | off | Force plain text even on a TTY (when not in `--json`). |

<ParamField body="root" type="string" default=".">
First non-flag argument. Used as the walk root and as the base for `path.relative` when recording hit file paths.
</ParamField>

<ParamField body="--json" type="boolean" default="false">
When present (`args.includes("--json")`), sets `asJson` and disables color.
</ParamField>

<ParamField body="--no-color" type="boolean" default="false">
When present, or when stdout is not a TTY, or when `--json` is set, ANSI helpers become no-ops.
</ParamField>

Color enablement is exact:

```js
const useColor =
  !args.includes("--no-color") && process.stdout.isTTY && !asJson;
```

## Hard constraint: read-only

<Warning>
The scanner never writes, renames, or deletes project files. The script header, root README, and skill README all state that it only reads and reports.
</Warning>

| Surface | Edit behavior |
|---|---|
| `scripts/scan.mjs` alone | Read-only: `readdirSync`, `statSync`, `readFileSync`, `console.log`. |
| Agent skill host | Scan → triage → report → propose/apply fixes is the skill workflow in `SKILL.md` / host agent — not this CLI. |

Hits are leads to confirm in the source, not automatic remediations. A gradient, serif, or emoji may be an intentional design choice.

## Walk behavior

`walk(dir, files = [])` recursively collects candidate absolute paths.

### Skipped directories (`SKIP_DIRS`)

```
node_modules  .git  dist  build  out  .next  .astro
.output  .svelte-kit  .nuxt  coverage  vendor  .cache
.vercel  .turbo
```

Directory names in this set are not entered. If `readdirSync` throws, `walk` returns the `files` array unchanged.

### Included files

A file is pushed when:

1. Basename is **not** `*.min.js` / `*.min.css`
2. Basename is **not** a lockfile match: `package-lock`, `pnpm-lock`, or `yarn.lock`
3. Extension is in `EXTS`, **or** basename matches `/^tailwind\.config\./`

`EXTS`:

| Category | Extensions |
|---|---|
| Markup / style | `.html` `.css` `.scss` `.sass` `.less` |
| Script / components | `.tsx` `.jsx` `.ts` `.js` `.mjs` `.cjs` `.vue` `.svelte` `.astro` |
| Prose | `.md` `.mdx` |

`skill/references/detection.md` documents the same frontend-only intent; the executable skip set and extension list in `scan.mjs` are slightly broader (for example `.sass`, `.less`, `.mjs`, `.cjs`, and extra tool dirs such as `.turbo`).

## Scan behavior

`scanFile(path)` returns an array of hit objects (or `[]` on skip/error).

| Guard | Rule |
|---|---|
| Size | Skip if `statSync(path).size > 512 * 1024` (512 KiB). |
| Read errors | `catch` → `[]`. |
| Line length | Skip any line with `line.length > 2000` (minified-ish). |
| Code vs copy | `isCode = ![".md", ".mdx"].includes(extname(path))`. Tells **without** `copy: true` are skipped on prose files (`if (!tell.copy && !isCode) continue`). |
| Matching | For each remaining line and each tell, if `tell.patterns.some((re) => re.test(line))`, record a hit. |

Per-hit shape from scanning and aggregation:

| Field | Source |
|---|---|
| `line` | 1-based index (`i + 1`) |
| `text` | `line.trim().slice(0, 100)` |
| `file` (aggregated) | `relative(root, f) \|\| f` |

Aggregation: hits bucket into `Map` by `tell.id`, then sorted with `a.tell.id.localeCompare(b.tell.id)`.

### Copy-scoped tells

Only these embedded tell records set `copy: true` (also match `.md` / `.mdx`):

| id | name |
|---|---|
| `10` | highlighted keywords |
| `11` | AI copywriting voice |
| `12` | emoji everywhere |

All other tells are code/style-only relative to prose files.

## Embedded tell catalogue

The CLI encodes 23 tells inline as `TELLS` (ids `"01"`–`"23"`). Each entry has `id`, `group`, `name`, `fix`, `patterns` (array of `RegExp`), and optional `copy`.

| id | group | name | fix (one-line) |
|---|---|---|---|
| 01 | color | indigo→violet gradient | one solid, chosen accent |
| 02 | color | gradient-clip headline | solid ink, scale up |
| 03 | color | warm ‘cozy’ palette | neutral base + one warm accent |
| 04 | color | default semantic palette | one palette: neutrals + a couple of chosen states |
| 05 | color | one-hue status box | state in words; one muted accent on neutral |
| 06 | color | gradients as atmosphere | one flat bg; depth from a hairline |
| 07 | type | serif-italic emphasis | emphasise by weight, one voice |
| 08 | type | serif where sans belongs | one legible UI sans |
| 09 | type | decorative strikes & highlights | strike for edits, underline for links |
| 10 | copy | highlighted keywords | let structure carry emphasis |
| 11 | copy | AI copywriting voice | say the specific thing |
| 12 | copy | emoji everywhere | cut emoji from product copy |
| 13 | component | glowing status dot | flat dot + a word; no halo |
| 14 | component | left-border callout | one aside, rest is body |
| 15 | component | pastel icon tiles | labelled list with specifics |
| 16 | component | max-radius / glassmorphism | one small radius, solid surfaces |
| 17 | component | oversized drop shadow | tight elevation, never bigger than the element |
| 18 | component | corners that don't nest | inner = outer − padding |
| 19 | component | badge / pill spam | badges only for real status |
| 20 | component | AI-drawn SVG icon | a real, high-quality icon (a designer, or a strong image model) |
| 21 | component | icon in a tint of itself | no tinted tile; inherit text color |
| 22 | layout | all-caps card grid | show the one key thing fully |
| 23 | evolved | tasteful-terminal | mono for code only |

Human-readable pattern notes and false positives live in `skill/references/detection.md`; the regexes that actually run are those in `TELLS` inside `scan.mjs`.

## Output modes

### Grouped console report (default)

When `asJson` is false:

1. Header:  
   `kill-ai-slop — scanned ${files.length} files under ${root}`  
   (`kill-ai-slop` bold when color is on).
2. If no tell groups:  
   `No slop signals found. (Still trust your eyes — open the pages.)`  
   then `process.exit(0)`.
3. Else, for each group (sorted by tell id):  
   - Line: `slop` (red) + bold `id` + `name` + dim `→ ${fix}`  
   - Hit list limited with `g.hits.slice(0, 12)` (at most 12 hits printed per tell).  
   - Loop over those hits starts at `for (const h of shown)`; each aggregated hit carries `file`, `line`, and `text`. The script header describes the product output as a grouped report of **file:line** hits.

### JSON mode (`--json`)

Pretty-printed (`JSON.stringify(..., null, 2)`), then `process.exit(0)`.

```json
{
  "root": "<scan root path>",
  "filesScanned": 0,
  "groups": 0,
  "hits": 0,
  "findings": [
    {
      "id": "01",
      "group": "color",
      "name": "indigo→violet gradient",
      "fix": "one solid, chosen accent",
      "hits": [
        { "file": "src/App.tsx", "line": 42, "text": "..." }
      ]
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `root` | Walk root string used for this run |
| `filesScanned` | `files.length` from `walk(root)` |
| `groups` | Number of tell ids with ≥1 hit |
| `hits` | Total hit count across groups |
| `findings[]` | One object per matched tell, sorted by `id` |
| `findings[].hits[]` | `{ file, line, text }` (relative path, 1-based line, ≤100-char snippet) |

Color is always off in this mode (`useColor` requires `!asJson`).

## Console formatting helpers

Used only for the human report path (after the `--json` early exit):

```js
const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
const red = (s) => c("31", s);   // "slop" label
const dim = (s) => c("2", s);    // "→ " + fix
const bold = (s) => c("1", s);   // product name, tell id
```

When `useColor` is false, helpers return the string unchanged.

## Exit behavior

| Condition | Exit |
|---|---|
| `--json` after printing payload | `process.exit(0)` |
| Human report, zero tell groups | `process.exit(0)` after empty message |
| Human report with findings | Prints groups (≤12 hits each); no non-zero status is defined in the available script body for this path |

There is no flag that enables writing fixes from this CLI.

## Runtime shape

```text
argv → root / --json / --no-color
        │
        ▼
   walk(root)  ── skip SKIP_DIRS, lockfiles, *.min.*, size/ext filters
        │
        ▼
   scanFile(f) ── line regexes over TELLS (copy vs code)
        │
        ▼
   byTell Map ── sort by tell id
        │
   ┌────┴────┐
   │ --json  │  JSON { root, filesScanned, groups, hits, findings }
   │ default │  ANSI grouped report (header, slop lines, ≤12 hits/tell)
   └─────────┘
        │
        ▼  (read-only; never mutates project files)
```

## Verification signals

| Check | Expected |
|---|---|
| Dependency-free run | `node skill/scripts/scan.mjs .` runs without `npm install` in `skill/` |
| Empty tree / clean UI | Message includes `No slop signals found` |
| Machine pipeline | `--json` parses as object with `findings` array |
| Color off in CI | Non-TTY or `--no-color` or `--json` → no CSI sequences from helpers |
| Scope | Hits only under included extensions / `tailwind.config.*`; not under `node_modules`, `dist`, etc. |

## Related pages

<CardGroup>
  <Card title="Scan a web project" href="/scan-web-project">
    Point the scanner at a project path, read grouped vs JSON reports, and confirm hits against detection rules.
  </Card>
  <Card title="Detection patterns" href="/detection-patterns">
    Human reference for code-level signals per tell and usual false positives.
  </Card>
  <Card title="Skill workflow" href="/skill-workflow">
    How an agent host scans, triages, reports, then proposes or applies fixes without silent scanner edits.
  </Card>
  <Card title="Remediation playbook" href="/fixes-playbook">
    Map tell ids and fix one-liners to clean before/after remediation steps.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Shortest path to a first scanner report and success signals.
  </Card>
</CardGroup>
