SLOPSHOPPER

linteljs

LintelJS project standards and safety hooks

newbandprocess
A shopper browsing a rack in a slop shop
README

<img src="https://raw.githubusercontent.com/Faran52/linteljs/main/docs/assets/logo.svg" alt="linteljs" width="96">

<h1 align="center">linteljs</h1>

<a href="https://github.com/Faran52/linteljs/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Faran52/linteljs/ci.yml?branch=main&logo=github&style=for-the-badge" alt="Build Status"></a> <a href="https://github.com/Faran52/linteljs/actions/workflows/ci.yml?query=branch%3Amain"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FFaran52%2Flinteljs%2Fbadges%2Fcoverage.json&style=for-the-badge" alt="Coverage"></a> <a href="https://www.npmjs.com/package/@linteljs/create"><img src="https://img.shields.io/npm/v/@linteljs/create.svg?style=for-the-badge" alt="npm"></a> <a href="https://www.npmjs.com/package/@linteljs/create"><img src="https://img.shields.io/node/v/@linteljs/create?style=for-the-badge" alt="Node"></a>

linteljs ships as three packages: a scaffolder, a shared ESLint flat config, and the custom rules behind it. The scaffolder writes a starter app with its tests, ESLint flat config, TypeScript settings, git hooks, and coding-agent rules and hooks, for React, Next.js, Vue, Nuxt, Svelte, Solid, Angular, Astro, React Native through Expo, Manifest V3 web extensions, and plain TypeScript libraries. Its check runs lint, the banned-pattern check, CSS lint, the typecheck, coverage at 100% when the project has tests, and the build, and passes on the first run.

Quick start

Node 22.18 or newer.

npm create @linteljs my-app
cd my-app
npm run check
pnpm create @linteljs my-app
cd my-app
pnpm check
yarn create @linteljs my-app
cd my-app
yarn run check
bun create @linteljs my-app
cd my-app
bun run check

For an existing project, npx @linteljs/create --existing applies the standard in place, and sync, run through the project's own manager, updates it. The @linteljs/create README has the details.

Packages

PackageVersionUse it for
@linteljs/createnpmStart a project, or bring an existing one under the standard.
@linteljs/eslint-confignpmCompose ESLint flat-config layers, by hand or through composeConfig.
@linteljs/eslint-pluginnpmUse the rules on their own. recommended holds the ones the config builds on.

Why

Copied configuration drifts quietly: a missing setting disables a rule while two config files still look alike. linteljs keeps the shared rules in a published package and the generated files explicit, so an update arrives as a reviewable diff.

Documentation

  • docs/DESIGN.md: the decisions the code cannot show, and the non-goals.
  • Each package's README, linked above: every option, flag and rule.

Contributing

pnpm install
pnpm check

Needs Node 26.10.0+ and pnpm 12.6+. docs/CONTRIBUTING.md covers the rest, including the networked end-to-end suite.

License

MIT

Source 2 files
hooks/checkBand.tsx 96 lines
1// The band above the prompt: what the commit gate would say about the work tree now.
2import {
3  atom,
4  type EngineInterface,
5  type On,
6  read,
7  type Register,
8  update,
9} from 'claude-code';
10
11import type { CheckWord } from '../types/index.d.ts';
12
13const check = atom({
14  plugin: 'linteljs',
15  key: 'check',
16} as const, null);
17
18// Each state's glyph and its colour.
19export const MARKS: Record<CheckWord, [string, string]> = {
20  passed: ['✓', 'green'],
21  stale: ['◐', 'yellow'],
22  failed: ['✗', 'red'],
23  running: ['◐', 'yellow'],
24  none: ['○', 'gray'],
25};
26
27const isWord = (text: string): text is CheckWord => {
28  return Object.hasOwn(MARKS, text);
29};
30
31// Outside a git project with a `check` script the script prints nothing, and the band stays away.
32const printed = async ($: EngineInterface, cwd: string): Promise<string> => {
33  try {
34    const status = await $.process.run(['node', `${$.plugin.root}/hooks/checkStatusHook.ts`], { cwd });
35
36    return status.stdout.trim();
37  }
38  catch {
39    return '';
40  }
41};
42
43const refresh = async ($: EngineInterface): Promise<void> => {
44  const cwd = await $.session.root();
45  const text = await printed($, cwd);
46
47  await update($, check, () => {
48    return isWord(text) ? text : null;
49  });
50};
51
52export const registerCheckRefresh = (on: On): void => {
53  on('session.start', async ($, e, next) => {
54    const started = await next(e);
55    await refresh($);
56
57    return started;
58  });
59
60  on('turn.complete', async ($, e, next) => {
61    const completed = await next(e);
62
63    if (e.agentId === undefined) {
64      await refresh($);
65    }
66
67    return completed;
68  });
69};
70
71export const register: Register = (on) => {
72  registerCheckRefresh(on);
73
74  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
75    const word = await read($, check);
76
77    if (e.props.hasSurvey || word === null) {
78      return next(e);
79    }
80
81    const { Box, Text } = $.ui.resolve(e);
82    const [glyph, color] = MARKS[word];
83    const below = await next(e);
84
85    return (
86      <Box flexDirection="column">
87        <Text>
88          <Text color={color}>{glyph}</Text>
89          {` check ${word}`}
90        </Text>
91        {below}
92      </Box>
93    );
94  });
95};
96
types/index.d.ts 10 lines
1// The state word `hooks/checkStatusHook.ts` prints, which the check band draws.
2export type CheckWord = 'failed' | 'none' | 'passed' | 'running' | 'stale';
3
4// Inline, since `claude plugin validate` reads the state's shape from this declaration.
5declare module 'claude-code' {
6  interface PluginState {
7    linteljs: { check: CheckWord | null };
8  }
9}
10