@askviraj/linter 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viraj Patel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/auto.js ADDED
@@ -0,0 +1,4 @@
1
+ import { createConfig } from "./config-factory.js";
2
+ import { loadYamlOptions } from "./config-loader.js";
3
+ const config = createConfig(loadYamlOptions(process.cwd()));
4
+ export default config;
@@ -0,0 +1,96 @@
1
+ import { ParseError } from "./types.js";
2
+ export function parseArgs(rawArgs) {
3
+ const flags = {
4
+ init: false,
5
+ force: false,
6
+ fix: false,
7
+ cache: false,
8
+ quiet: false,
9
+ ignore: true,
10
+ debug: false,
11
+ version: false,
12
+ help: false,
13
+ };
14
+ let cacheLocation;
15
+ let format;
16
+ let maxWarnings;
17
+ const positional = [];
18
+ for (let i = 0; i < rawArgs.length; i++) {
19
+ const arg = rawArgs[i];
20
+ if (arg === undefined) {
21
+ break;
22
+ }
23
+ switch (arg) {
24
+ case "--init":
25
+ flags.init = true;
26
+ break;
27
+ case "--force":
28
+ flags.force = true;
29
+ break;
30
+ case "--fix":
31
+ flags.fix = true;
32
+ break;
33
+ case "--cache":
34
+ flags.cache = true;
35
+ break;
36
+ case "--quiet":
37
+ flags.quiet = true;
38
+ break;
39
+ case "--no-ignore":
40
+ flags.ignore = false;
41
+ break;
42
+ case "--debug":
43
+ flags.debug = true;
44
+ break;
45
+ case "--version":
46
+ case "-v":
47
+ flags.version = true;
48
+ break;
49
+ case "--help":
50
+ case "-h":
51
+ flags.help = true;
52
+ break;
53
+ case "--cache-location": {
54
+ const next = rawArgs[++i];
55
+ if (!next) {
56
+ throw new ParseError("--cache-location requires a path argument");
57
+ }
58
+ cacheLocation = next;
59
+ break;
60
+ }
61
+ case "--format":
62
+ case "-f": {
63
+ const next = rawArgs[++i];
64
+ if (!next) {
65
+ throw new ParseError("--format requires a format name");
66
+ }
67
+ format = next;
68
+ break;
69
+ }
70
+ case "--max-warnings": {
71
+ const next = rawArgs[++i];
72
+ if (!next || Number.isNaN(Number(next))) {
73
+ throw new ParseError("--max-warnings requires a number");
74
+ }
75
+ maxWarnings = Number(next);
76
+ break;
77
+ }
78
+ case "--":
79
+ for (let j = i + 1; j < rawArgs.length; j++) {
80
+ const remaining = rawArgs[j];
81
+ if (remaining !== undefined) {
82
+ positional.push(remaining);
83
+ }
84
+ }
85
+ i = rawArgs.length;
86
+ break;
87
+ default:
88
+ if (arg.startsWith("-")) {
89
+ throw new ParseError(`Unknown flag: ${arg}`);
90
+ }
91
+ positional.push(arg);
92
+ break;
93
+ }
94
+ }
95
+ return { flags, cacheLocation, format, maxWarnings, positional };
96
+ }
@@ -0,0 +1,18 @@
1
+ export function runHelp() {
2
+ console.log(`@askviraj/linter [options] [files...]
3
+
4
+ Options:
5
+ --init Scaffold eslint.config.mjs and .config/linter.yaml
6
+ --force Overwrite existing files when used with --init
7
+ --fix Automatically fix problems
8
+ --cache Only check changed files
9
+ --cache-location Path to the cache file or directory
10
+ --no-ignore Disable use of ignore files
11
+ --quiet Report errors only
12
+ --debug Enable debug logging
13
+ --format, -f Use a specific output format
14
+ --max-warnings Number of warnings to trigger nonzero exit code
15
+ --version, -v Show version
16
+ --help, -h Show help
17
+ `);
18
+ }
@@ -0,0 +1,153 @@
1
+ import { applyEdits, findNodeAtLocation, getNodeValue, modify, parseTree, } from "jsonc-parser";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ const ESLINT_CONFIG_MJS = `// Auto-generated by @askviraj/linter
5
+ // Run \`npx @askviraj/linter --init\` to regenerate.
6
+ export { default } from "@askviraj/linter/auto";
7
+ `;
8
+ const LINTER_YAML = `# @askviraj/linter configuration
9
+ #
10
+ # Add custom ignores, rule overrides, and additional config blocks below.
11
+
12
+ version: 1
13
+
14
+ # ignores:
15
+ # - "**/generated/**"
16
+ # - "**/*.d.ts"
17
+ #
18
+ # overrides:
19
+ # typescript:
20
+ # rules:
21
+ # "@typescript-eslint/no-explicit-any": "error"
22
+ #
23
+ # configs:
24
+ # - name: "custom"
25
+ # files: ["**/*.ts"]
26
+ # rules: {}
27
+ `;
28
+ const VSCODE_SETTINGS = {
29
+ "eslint.useFlatConfig": true,
30
+ };
31
+ /**
32
+ * Read an existing .vscode/settings.json and update/add eslint.useFlatConfig.
33
+ * Uses jsonc-parser so comments, trailing commas, and formatting are preserved.
34
+ */
35
+ function updateVsCodeSettings(existingPath) {
36
+ const text = readFileSync(existingPath, "utf-8");
37
+ const root = parseTree(text);
38
+ if (!root) {
39
+ // Completely unparseable — fall back to a fresh object.
40
+ return {
41
+ content: JSON.stringify(VSCODE_SETTINGS, null, 2) + "\n",
42
+ action: "updated",
43
+ };
44
+ }
45
+ const node = findNodeAtLocation(root, ["eslint.useFlatConfig"]);
46
+ if (node) {
47
+ const value = getNodeValue(node);
48
+ if (value === true) {
49
+ return { content: text, action: "up-to-date" };
50
+ }
51
+ // Replace existing value.
52
+ const edits = modify(text, ["eslint.useFlatConfig"], true, {
53
+ formattingOptions: { tabSize: 2, insertSpaces: true, eol: "\n" },
54
+ });
55
+ return { content: applyEdits(text, edits), action: "updated" };
56
+ }
57
+ // Property missing — add it.
58
+ const edits = modify(text, ["eslint.useFlatConfig"], true, {
59
+ formattingOptions: { tabSize: 2, insertSpaces: true, eol: "\n" },
60
+ });
61
+ return { content: applyEdits(text, edits), action: "updated" };
62
+ }
63
+ export function runInit(options = {}) {
64
+ const cwd = options.cwd || process.cwd();
65
+ const actions = [];
66
+ let settingsError;
67
+ // 1. eslint.config.mjs
68
+ const eslintConfigPath = resolve(cwd, "eslint.config.mjs");
69
+ const eslintExisted = existsSync(eslintConfigPath);
70
+ if (eslintExisted && !options.force) {
71
+ actions.push({ path: eslintConfigPath, action: "skipped" });
72
+ }
73
+ else {
74
+ try {
75
+ writeFileSync(eslintConfigPath, ESLINT_CONFIG_MJS);
76
+ actions.push({
77
+ path: eslintConfigPath,
78
+ action: eslintExisted ? "overwritten" : "created",
79
+ });
80
+ }
81
+ catch (e) {
82
+ throw new Error(`Failed to write ${eslintConfigPath}: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
83
+ }
84
+ }
85
+ // 2. .config/linter.yaml
86
+ const configDir = resolve(cwd, ".config");
87
+ if (!existsSync(configDir)) {
88
+ mkdirSync(configDir, { recursive: true });
89
+ }
90
+ const linterYamlPath = resolve(configDir, "linter.yaml");
91
+ const linterExisted = existsSync(linterYamlPath);
92
+ if (linterExisted && !options.force) {
93
+ actions.push({ path: linterYamlPath, action: "skipped" });
94
+ }
95
+ else {
96
+ try {
97
+ writeFileSync(linterYamlPath, LINTER_YAML);
98
+ actions.push({
99
+ path: linterYamlPath,
100
+ action: linterExisted ? "overwritten" : "created",
101
+ });
102
+ }
103
+ catch (e) {
104
+ throw new Error(`Failed to write ${linterYamlPath}: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
105
+ }
106
+ }
107
+ // 3. .vscode/settings.json
108
+ const vscodeDir = resolve(cwd, ".vscode");
109
+ if (!existsSync(vscodeDir)) {
110
+ mkdirSync(vscodeDir, { recursive: true });
111
+ }
112
+ const settingsPath = resolve(vscodeDir, "settings.json");
113
+ if (existsSync(settingsPath)) {
114
+ try {
115
+ const result = updateVsCodeSettings(settingsPath);
116
+ if (result.action === "up-to-date") {
117
+ actions.push({ path: settingsPath, action: "up-to-date" });
118
+ }
119
+ else {
120
+ writeFileSync(settingsPath, result.content);
121
+ actions.push({ path: settingsPath, action: "updated" });
122
+ }
123
+ }
124
+ catch (e) {
125
+ settingsError = e instanceof Error ? e.message : String(e);
126
+ actions.push({ path: settingsPath, action: "failed" });
127
+ }
128
+ }
129
+ else {
130
+ try {
131
+ writeFileSync(settingsPath, JSON.stringify(VSCODE_SETTINGS, null, 2) + "\n");
132
+ actions.push({ path: settingsPath, action: "created" });
133
+ }
134
+ catch (e) {
135
+ throw new Error(`Failed to write ${settingsPath}: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
136
+ }
137
+ }
138
+ console.log("Initialized @askviraj/linter:");
139
+ for (const { path, action } of actions) {
140
+ console.log(` [${action}] ${path}`);
141
+ }
142
+ const skipped = actions.filter(a => a.action === "skipped");
143
+ if (skipped.length > 0) {
144
+ console.log("");
145
+ console.log("Some files were skipped because they already exist. Use --force to overwrite.");
146
+ }
147
+ if (settingsError) {
148
+ throw new Error(`Failed to update ${settingsPath}: ${settingsError}\n`
149
+ + "The file contains invalid JSONC. Fix the file and try again.");
150
+ }
151
+ console.log("");
152
+ console.log("Note: If you have VS Code open, restart the ESLint server (", "Cmd+Shift+P → 'ESLint: Restart ESLint Server'", ") or restart VS Code for changes to take effect.");
153
+ }
@@ -0,0 +1,47 @@
1
+ import { ESLint } from "eslint";
2
+ import { createRequire } from "node:module";
3
+ import { createConfig } from "../../config-factory.js";
4
+ import { loadYamlOptions } from "../../config-loader.js";
5
+ const require = createRequire(import.meta.url);
6
+ export async function runLint(args) {
7
+ if (args.flags.debug) {
8
+ const debug = require("debug");
9
+ debug.enable("eslint:*,-eslint:code-path,eslintrc:*");
10
+ }
11
+ const targets = args.positional.length > 0 ? args.positional : ["."];
12
+ const config = createConfig(loadYamlOptions(process.cwd()));
13
+ const eslint = new ESLint({
14
+ overrideConfigFile: true,
15
+ overrideConfig: config,
16
+ fix: args.flags.fix,
17
+ cache: args.flags.cache,
18
+ cacheLocation: args.cacheLocation,
19
+ errorOnUnmatchedPattern: false,
20
+ ignore: args.flags.ignore,
21
+ });
22
+ let results;
23
+ try {
24
+ results = await eslint.lintFiles(targets);
25
+ }
26
+ catch (e) {
27
+ throw new Error(e instanceof Error ? e.message : String(e), { cause: e });
28
+ }
29
+ if (args.flags.fix) {
30
+ await ESLint.outputFixes(results);
31
+ }
32
+ if (args.flags.quiet) {
33
+ results = ESLint.getErrorResults(results);
34
+ }
35
+ const formatter = await eslint.loadFormatter(args.format);
36
+ const output = await formatter.format(results);
37
+ if (output) {
38
+ console.log(output);
39
+ }
40
+ const errorCount = results.reduce((sum, r) => sum + r.errorCount, 0);
41
+ const warningCount = results.reduce((sum, r) => sum + r.warningCount, 0);
42
+ const failed = errorCount > 0
43
+ || (args.maxWarnings !== undefined
44
+ && args.maxWarnings >= 0
45
+ && warningCount > args.maxWarnings);
46
+ return failed ? 1 : 0;
47
+ }
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, resolve, } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const require = createRequire(import.meta.url);
6
+ function getPackageVersion(name, requireFn) {
7
+ try {
8
+ const mainPath = requireFn.resolve(name);
9
+ let dir = dirname(mainPath);
10
+ while (dir !== dirname(dir)) {
11
+ const pkgPath = resolve(dir, "package.json");
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
14
+ if (pkg.name === name && pkg.version) {
15
+ return pkg.version;
16
+ }
17
+ }
18
+ catch {
19
+ // Continue walking up.
20
+ }
21
+ dir = dirname(dir);
22
+ }
23
+ }
24
+ catch {
25
+ // Package main not resolvable; try fallback.
26
+ }
27
+ try {
28
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
29
+ const pkgPath = resolve(moduleDir, "..", "..", "..", "node_modules", name, "package.json");
30
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
31
+ if (pkg.name === name && pkg.version) {
32
+ return pkg.version;
33
+ }
34
+ }
35
+ catch {
36
+ // Not found in node_modules either.
37
+ }
38
+ return undefined;
39
+ }
40
+ export function runVersion() {
41
+ let ownVersion;
42
+ let deps;
43
+ try {
44
+ const ownPkgPath = require.resolve("../../../package.json");
45
+ const ownPkg = JSON.parse(readFileSync(ownPkgPath, "utf-8"));
46
+ ownVersion = ownPkg.version;
47
+ deps = ownPkg.dependencies;
48
+ }
49
+ catch {
50
+ throw new Error("Failed to read package metadata.");
51
+ }
52
+ if (!ownVersion) {
53
+ throw new Error("Package version not found.");
54
+ }
55
+ const patterns = [
56
+ /^eslint$/,
57
+ /^@eslint\//,
58
+ /^@typescript-eslint\//,
59
+ /^typescript-eslint$/,
60
+ /^@html-eslint\//,
61
+ /^@poupe\/eslint-plugin-/,
62
+ /^eslint-plugin-/,
63
+ ];
64
+ const depNames = deps
65
+ ? Object.keys(deps).filter(name => patterns.some(pattern => pattern.test(name)))
66
+ : [];
67
+ const pluginVersions = [];
68
+ for (const name of depNames) {
69
+ const version = getPackageVersion(name, require);
70
+ if (version) {
71
+ pluginVersions.push({ name, version });
72
+ }
73
+ }
74
+ pluginVersions.sort((a, b) => a.name.localeCompare(b.name));
75
+ console.log(`v${ownVersion}`);
76
+ console.log("------");
77
+ const eslintEntry = pluginVersions.find(p => p.name === "eslint");
78
+ if (eslintEntry) {
79
+ console.log(`eslint v${eslintEntry.version}`);
80
+ }
81
+ for (const { name, version } of pluginVersions) {
82
+ if (name !== "eslint") {
83
+ console.log(`${name} v${version}`);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,28 @@
1
+ import { parseArgs } from "./args.js";
2
+ import { runHelp } from "./commands/help.js";
3
+ import { runInit } from "./commands/init.js";
4
+ import { runLint } from "./commands/lint.js";
5
+ import { runVersion } from "./commands/version.js";
6
+ export async function run(rawArgs) {
7
+ try {
8
+ const args = parseArgs(rawArgs);
9
+ if (args.flags.help) {
10
+ runHelp();
11
+ return;
12
+ }
13
+ if (args.flags.version) {
14
+ runVersion();
15
+ return;
16
+ }
17
+ if (args.flags.init) {
18
+ runInit({ force: args.flags.force });
19
+ return;
20
+ }
21
+ const exitCode = await runLint(args);
22
+ process.exit(exitCode);
23
+ }
24
+ catch (e) {
25
+ console.error(e instanceof Error ? e.message : e);
26
+ process.exit(1);
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ export class ParseError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ParseError";
5
+ }
6
+ }
@@ -0,0 +1,12 @@
1
+ import { globalIgnores } from "eslint/config";
2
+ export const ignores = globalIgnores([
3
+ "**/node_modules/",
4
+ "**/dist/",
5
+ "**/.turbo/",
6
+ "**/.astro/",
7
+ "**/*.d.ts",
8
+ "**/AGENTS.md",
9
+ ".agents/**/*.md",
10
+ ".claude/**/*.md",
11
+ ".github/**/*.md",
12
+ ]);
@@ -0,0 +1,29 @@
1
+ import { defineConfig } from "eslint/config";
2
+ import { ignores } from "./ignores.js";
3
+ import { astroConfig } from "./presets/astro.js";
4
+ import { cssConfig } from "./presets/css.js";
5
+ import { htmlConfig } from "./presets/html.js";
6
+ import { javascriptConfig } from "./presets/javascript.js";
7
+ import { jsonConfig } from "./presets/json.js";
8
+ import { jsoncConfig } from "./presets/jsonc.js";
9
+ import { markdownTsConfig } from "./presets/markdown-ts.js";
10
+ import { markdownConfig } from "./presets/markdown.js";
11
+ import { tomlConfig } from "./presets/toml.js";
12
+ import { typescriptConfig } from "./presets/typescript.js";
13
+ import { yamlConfig } from "./presets/yaml.js";
14
+ export function composeBaseConfig() {
15
+ return defineConfig([
16
+ ignores,
17
+ javascriptConfig,
18
+ typescriptConfig,
19
+ astroConfig,
20
+ jsonConfig,
21
+ jsoncConfig,
22
+ markdownConfig,
23
+ markdownTsConfig,
24
+ yamlConfig,
25
+ tomlConfig,
26
+ htmlConfig,
27
+ cssConfig,
28
+ ]);
29
+ }
@@ -0,0 +1,6 @@
1
+ export const JSONC_FILES = [
2
+ "**/tsconfig.json",
3
+ "**/tsconfig.*.json",
4
+ "**/tsconfig-strict.json",
5
+ "**/.vscode/*.json",
6
+ ];
@@ -0,0 +1,25 @@
1
+ import tsParser from "@typescript-eslint/parser";
2
+ import astroParser from "astro-eslint-parser";
3
+ import astro from "eslint-plugin-astro";
4
+ import globals from "globals";
5
+ export const astroConfig = {
6
+ name: "astro",
7
+ files: ["**/*.astro"],
8
+ plugins: { astro },
9
+ languageOptions: {
10
+ globals: {
11
+ ...globals.node,
12
+ ...globals.astro,
13
+ },
14
+ parser: astroParser,
15
+ parserOptions: {
16
+ parser: tsParser,
17
+ extraFileExtensions: [".astro"],
18
+ sourceType: "module",
19
+ },
20
+ },
21
+ extends: [astro.configs.recommended],
22
+ rules: {
23
+ "no-unreachable": "off",
24
+ },
25
+ };
@@ -0,0 +1,10 @@
1
+ import css from "@eslint/css";
2
+ import tailwindcss from "@poupe/eslint-plugin-tailwindcss";
3
+ export const cssConfig = {
4
+ files: ["**/*.css"],
5
+ extends: [css.configs.recommended, tailwindcss.configs["recommended"]],
6
+ name: "css",
7
+ rules: {
8
+ "css/no-invalid-at-rules": "off",
9
+ },
10
+ };
@@ -0,0 +1,11 @@
1
+ import html from "@html-eslint/eslint-plugin";
2
+ export const htmlConfig = {
3
+ extends: [html.configs.recommended],
4
+ files: ["**/*.html"],
5
+ language: "html/html",
6
+ name: "html",
7
+ plugins: { html },
8
+ rules: {
9
+ "html/indent": ["error", 2],
10
+ },
11
+ };
@@ -0,0 +1,17 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ export const javascriptConfig = {
4
+ extends: [js.configs.recommended],
5
+ files: ["**/*.{js,mjs,cjs,jsx}"],
6
+ languageOptions: {
7
+ globals: {
8
+ ...globals.browser,
9
+ ...globals.node,
10
+ },
11
+ },
12
+ name: "javascript",
13
+ plugins: { js },
14
+ rules: {
15
+ "no-console": "warn",
16
+ },
17
+ };
@@ -0,0 +1,18 @@
1
+ import json from "@eslint/json";
2
+ import { JSONC_FILES } from "../jsonc-files.js";
3
+ export const jsonConfig = {
4
+ extends: [json.configs.recommended],
5
+ files: ["**/*.json"],
6
+ ignores: JSONC_FILES,
7
+ language: "json/json",
8
+ name: "json",
9
+ plugins: { json },
10
+ rules: {
11
+ "json/no-duplicate-keys": "error",
12
+ "json/no-empty-keys": "error",
13
+ "json/no-unnormalized-keys": "error",
14
+ "json/no-unsafe-values": "error",
15
+ "json/sort-keys": "off",
16
+ "json/top-level-interop": "error",
17
+ },
18
+ };
@@ -0,0 +1,21 @@
1
+ import json from "@eslint/json";
2
+ import { JSONC_FILES } from "../jsonc-files.js";
3
+ export const jsoncConfig = {
4
+ extends: [json.configs.recommended],
5
+ files: ["**/*.jsonc", ...JSONC_FILES],
6
+ language: "json/jsonc",
7
+ languageOptions: {
8
+ allowComments: true,
9
+ allowTrailingCommas: true,
10
+ },
11
+ name: "jsonc",
12
+ plugins: { json },
13
+ rules: {
14
+ "json/no-duplicate-keys": "error",
15
+ "json/no-empty-keys": "error",
16
+ "json/no-unnormalized-keys": "error",
17
+ "json/no-unsafe-values": "error",
18
+ "json/sort-keys": "off",
19
+ "json/top-level-interop": "error",
20
+ },
21
+ };
@@ -0,0 +1,9 @@
1
+ export const markdownTsConfig = {
2
+ name: "markdown-typescript",
3
+ files: ["**/*.md/*.ts", "**/*.md/*.tsx"],
4
+ rules: {
5
+ "@typescript-eslint/no-unused-vars": "off",
6
+ "@typescript-eslint/no-explicit-any": "off",
7
+ "@typescript-eslint/no-unused-expressions": "off",
8
+ },
9
+ };
@@ -0,0 +1,21 @@
1
+ import markdown from "@eslint/markdown";
2
+ export const markdownConfig = {
3
+ extends: [markdown.configs.recommended],
4
+ files: ["**/*.md"],
5
+ name: "markdown",
6
+ plugins: { markdown },
7
+ settings: { "markdown/frontmatter": true },
8
+ rules: {
9
+ "markdown/no-missing-label-refs": "off",
10
+ "markdown/no-bare-urls": "error",
11
+ "markdown/no-duplicate-headings": ["error", { checkSiblingsOnly: true }],
12
+ "markdown/no-empty-images": "error",
13
+ "markdown/no-empty-links": "error",
14
+ "markdown/no-missing-link-fragments": "error",
15
+ "markdown/no-multiple-h1": "error",
16
+ "markdown/no-reference-like-urls": "error",
17
+ "markdown/no-reversed-media-syntax": "error",
18
+ "markdown/no-space-in-emphasis": "error",
19
+ "markdown/table-column-count": "error",
20
+ },
21
+ };
@@ -0,0 +1,9 @@
1
+ import eslintPluginToml from "eslint-plugin-toml";
2
+ export const tomlConfig = {
3
+ extends: [eslintPluginToml.configs.recommended],
4
+ files: ["**/*.toml"],
5
+ language: "toml/toml",
6
+ name: "toml",
7
+ plugins: { toml: eslintPluginToml },
8
+ rules: {},
9
+ };
@@ -0,0 +1,33 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+ export const typescriptConfig = {
5
+ extends: [js.configs.recommended, tseslint.configs.recommended],
6
+ files: ["**/*.{ts,mts,cts,tsx}"],
7
+ ignores: ["**/*.md/*.ts", "**/*.md/*.tsx"],
8
+ languageOptions: {
9
+ globals: {
10
+ ...globals.browser,
11
+ ...globals.node,
12
+ },
13
+ },
14
+ name: "typescript",
15
+ plugins: {
16
+ js,
17
+ tseslint,
18
+ },
19
+ rules: {
20
+ "@typescript-eslint/no-empty-object-type": "off",
21
+ "@typescript-eslint/no-explicit-any": "off",
22
+ "@typescript-eslint/no-this-alias": "off",
23
+ "@typescript-eslint/no-unsafe-function-type": "off",
24
+ "@typescript-eslint/no-unused-vars": [
25
+ "error",
26
+ {
27
+ argsIgnorePattern: "^(_.*|.*Schema)$",
28
+ varsIgnorePattern: "^(_.*|.*Schema)$",
29
+ },
30
+ ],
31
+ "require-yield": "off",
32
+ },
33
+ };
@@ -0,0 +1,10 @@
1
+ import yaml from "eslint-plugin-yml";
2
+ import * as yamlParser from "yaml-eslint-parser";
3
+ export const yamlConfig = {
4
+ extends: [yaml.configs["flat/recommended"]],
5
+ files: ["**/*.yaml", "**/*.yml"],
6
+ languageOptions: { parser: yamlParser },
7
+ name: "yaml",
8
+ plugins: { yml: yaml },
9
+ rules: {},
10
+ };
@@ -0,0 +1,24 @@
1
+ import { globalIgnores } from "eslint/config";
2
+ import baseConfig from "./eslint.config.js";
3
+ export function createConfig(options = { version: 1 }) {
4
+ const configs = [];
5
+ if (options.ignores && options.ignores.length > 0) {
6
+ configs.push(globalIgnores(options.ignores));
7
+ }
8
+ for (const base of baseConfig) {
9
+ const clone = { ...base };
10
+ const override = base.name ? options.overrides?.[base.name] : undefined;
11
+ if (override?.rules) {
12
+ clone.rules = {
13
+ ...base.rules,
14
+ ...override.rules,
15
+ };
16
+ }
17
+ configs.push(clone);
18
+ }
19
+ if (options.configs) {
20
+ configs.push(...options.configs);
21
+ }
22
+ return configs;
23
+ }
24
+ export default createConfig;
@@ -0,0 +1,42 @@
1
+ import { existsSync, readFileSync, } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { parse } from "yaml";
4
+ export function findLinterYaml(startDir) {
5
+ let dir = startDir;
6
+ while (true) {
7
+ const candidate = resolve(dir, ".config", "linter.yaml");
8
+ if (existsSync(candidate)) {
9
+ return candidate;
10
+ }
11
+ const parent = resolve(dir, "..");
12
+ if (parent === dir) {
13
+ break;
14
+ }
15
+ dir = parent;
16
+ }
17
+ return undefined;
18
+ }
19
+ export function loadYamlOptions(startDir) {
20
+ const yamlPath = findLinterYaml(startDir);
21
+ if (!yamlPath) {
22
+ return { version: 1 };
23
+ }
24
+ const content = readFileSync(yamlPath, "utf-8");
25
+ let parsed;
26
+ try {
27
+ parsed = parse(content) || {};
28
+ }
29
+ catch (e) {
30
+ throw new Error(`Failed to parse ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
31
+ }
32
+ if (!("version" in parsed)) {
33
+ throw new Error(`Missing required "version" key in ${yamlPath}`);
34
+ }
35
+ const knownKeys = new Set(["version", "ignores", "overrides", "configs"]);
36
+ for (const key of Object.keys(parsed)) {
37
+ if (!knownKeys.has(key)) {
38
+ console.warn(`[@askviraj/linter] Warning: unknown key "${key}" in .config/linter.yaml`);
39
+ }
40
+ }
41
+ return parsed;
42
+ }
@@ -0,0 +1,2 @@
1
+ import { composeBaseConfig } from "./config/index.js";
2
+ export default composeBaseConfig();
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli/runner.js";
3
+ run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@askviraj/linter",
3
+ "version": "1.0.0",
4
+ "description": "A linter & formatter CLI for JavaScript, TypeScript, Astro, JSON, JSONC, CSS, HTML, Markdown, YAML, TOML, and more.",
5
+ "keywords": [
6
+ "lint",
7
+ "linter",
8
+ "eslint",
9
+ "cli",
10
+ "command-line",
11
+ "interface",
12
+ "javascript",
13
+ "typescript",
14
+ "astro",
15
+ "json",
16
+ "jsonc",
17
+ "css",
18
+ "html",
19
+ "markdown",
20
+ "yaml",
21
+ "toml"
22
+ ],
23
+ "homepage": "https://github.com/virajp/linter",
24
+ "bugs": {
25
+ "url": "https://github.com/virajp/linter/issues"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/virajp/linter.git"
30
+ },
31
+ "license": "MIT",
32
+ "author": {
33
+ "name": "Viraj Patel",
34
+ "email": "askviraj@gmail.com",
35
+ "url": "https://github.com/virajp"
36
+ },
37
+ "type": "module",
38
+ "exports": {
39
+ ".": "./dist/index.js",
40
+ "./auto": "./dist/auto.js"
41
+ },
42
+ "main": "dist/index.js",
43
+ "bin": {
44
+ "linter": "dist/index.js"
45
+ },
46
+ "files": [
47
+ "dist/"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json",
51
+ "prepublishOnly": "bun run build",
52
+ "test": "bun test tests/unit",
53
+ "test:all": "bun run build && bun test",
54
+ "test:integration": "bun run build && bun test tests/integration"
55
+ },
56
+ "dependencies": {
57
+ "@eslint/css": "1.1.0",
58
+ "@eslint/js": "10.0.1",
59
+ "@eslint/json": "1.2.0",
60
+ "@eslint/markdown": "8.0.1",
61
+ "@html-eslint/eslint-plugin": "0.59.0",
62
+ "@poupe/eslint-plugin-tailwindcss": "0.3.1",
63
+ "@typescript-eslint/parser": "8.59.1",
64
+ "astro-eslint-parser": "1.4.0",
65
+ "eslint": "10.2.1",
66
+ "eslint-plugin-astro": "1.7.0",
67
+ "eslint-plugin-toml": "1.3.1",
68
+ "eslint-plugin-yml": "3.3.1",
69
+ "globals": "17.5.0",
70
+ "jsonc-parser": "3.3.1",
71
+ "typescript-eslint": "8.59.1",
72
+ "yaml": "2.8.3",
73
+ "yaml-eslint-parser": "2.0.0"
74
+ },
75
+ "devDependencies": {
76
+ "@bun-security-scanner/osv": "1.0.0",
77
+ "@types/bun": "1.3.13",
78
+ "@types/node": "25.6.0",
79
+ "sort-package-json": "3.6.1",
80
+ "typescript": "6.0.3"
81
+ },
82
+ "peerDependencies": {
83
+ "eslint": "^10.0.0"
84
+ }
85
+ }
package/readme.md ADDED
@@ -0,0 +1,172 @@
1
+ # @askviraj/linter
2
+
3
+ A self-contained linter CLI for JavaScript, TypeScript, Astro, JSON, JSONC, CSS,
4
+ HTML, Markdown, YAML, TOML, and more. It bundles ESLint and all plugins so you
5
+ don't have to install them in every project.
6
+
7
+ ## Features
8
+
9
+ - **Zero-config** — Works out of the box with an opinionated flat config
10
+ - **Self-contained** — Bundles ESLint and all plugins; no peer dependency
11
+ headaches
12
+ - **Multi-language** — One command lints 10+ file types
13
+ - **Fast** — Optional caching and native Bun support
14
+ - **VS Code ready** — First-class integration with the official ESLint extension
15
+
16
+ ## Installation
17
+
18
+ No installation required for one-off use:
19
+
20
+ ```bash
21
+ # Via Bun
22
+ bunx --bun @askviraj/linter
23
+
24
+ # Via npx
25
+ npx @askviraj/linter
26
+ ```
27
+
28
+ Install locally for faster repeated runs:
29
+
30
+ ```bash
31
+ # With Bun
32
+ bun install --dev @askviraj/linter
33
+
34
+ # With npm
35
+ npm install --save-dev @askviraj/linter
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```bash
41
+ # Lint the current directory
42
+ bunx --bun @askviraj/linter
43
+
44
+ # Lint specific files or directories
45
+ bunx --bun @askviraj/linter src/ tests/
46
+
47
+ # Auto-fix issues
48
+ bunx --bun @askviraj/linter --fix
49
+
50
+ # Scaffold config files for your project
51
+ bunx --bun @askviraj/linter --init
52
+ ```
53
+
54
+ ## Supported File Types
55
+
56
+ | Language | Extensions |
57
+ | ---------- | ------------------------------------------- |
58
+ | JavaScript | `.js`, `.mjs`, `.cjs`, `.jsx` |
59
+ | TypeScript | `.ts`, `.mts`, `.cts`, `.tsx` |
60
+ | Astro | `.astro` |
61
+ | JSON | `.json` |
62
+ | JSONC | `.jsonc`, `tsconfig.json`, `.vscode/*.json` |
63
+ | Markdown | `.md` |
64
+ | CSS | `.css` |
65
+ | YAML | `.yaml`, `.yml` |
66
+ | TOML | `.toml` |
67
+ | HTML | `.html` |
68
+
69
+ ## CLI Reference
70
+
71
+ | Flag | Short | Description |
72
+ | ------------------------- | ----- | -------------------------------------------------------------------------------- |
73
+ | `--init` | | Scaffold `eslint.config.mjs`, `.config/linter.yaml`, and `.vscode/settings.json` |
74
+ | `--force` | | Overwrite existing files when used with `--init` |
75
+ | `--fix` | | Automatically fix problems |
76
+ | `--cache` | | Only check changed files |
77
+ | `--cache-location <path>` | | Path to the cache file or directory |
78
+ | `--no-ignore` | | Disable use of ignore files |
79
+ | `--quiet` | | Report errors only |
80
+ | `--debug` | | Enable ESLint debug logging |
81
+ | `--format`, `-f <name>` | | Use a specific output format |
82
+ | `--max-warnings <n>` | | Number of warnings to trigger nonzero exit code |
83
+ | `--version`, `-v` | | Show version and plugin versions |
84
+ | `--help`, `-h` | | Show help |
85
+
86
+ Positional arguments are treated as file or directory targets. Defaults to `.`
87
+ when none are provided. Use `--` to pass file names that look like flags (for
88
+ example, `linter -- --fix.js`).
89
+
90
+ ## Configuration
91
+
92
+ ### `.config/linter.yaml`
93
+
94
+ Create this file in your repo to customize the bundled config:
95
+
96
+ ```yaml
97
+ version: 1
98
+
99
+ ignores:
100
+ - "**/generated/**"
101
+ - "**/*.d.ts"
102
+
103
+ overrides:
104
+ typescript:
105
+ rules:
106
+ "@typescript-eslint/no-explicit-any": "error"
107
+
108
+ configs:
109
+ - name: "custom"
110
+ files: [ "**/*.test.ts" ]
111
+ rules:
112
+ "no-console": "off"
113
+ ```
114
+
115
+ #### Supported keys
116
+
117
+ | Key | Type | Description |
118
+ | ----------- | --------------------------- | --------------------------------------------------------- |
119
+ | `version` | `number \| string` | **Required.** Schema version (currently `1`). |
120
+ | `ignores` | `string[]` | Additional global ignore patterns. |
121
+ | `overrides` | `Record<string, { rules }>` | Rule overrides keyed by preset name. |
122
+ | `configs` | `unknown[]` | Raw ESLint config objects appended after the base config. |
123
+
124
+ #### Preset names for `overrides`
125
+
126
+ Use these names in the `overrides` section:
127
+
128
+ - `javascript`
129
+ - `typescript`
130
+ - `astro`
131
+ - `json`
132
+ - `jsonc`
133
+ - `markdown`
134
+ - `markdown-typescript`
135
+ - `yaml`
136
+ - `toml`
137
+ - `html`
138
+ - `css`
139
+
140
+ ### Scaffolding
141
+
142
+ Run `--init` to create the following files in your project:
143
+
144
+ - `eslint.config.mjs` — imports `@askviraj/linter/auto`
145
+ - `.config/linter.yaml` — your customization file
146
+ - `.vscode/settings.json` — enables flat config mode
147
+
148
+ Use `--force` to overwrite existing files.
149
+
150
+ ## VS Code Integration
151
+
152
+ The package exports `@askviraj/linter/auto` for use with the
153
+ `dbaeumer.vscode-eslint` extension.
154
+
155
+ After running `--init`, VS Code discovers `eslint.config.mjs` and loads the
156
+ bundled config automatically. Edit `.config/linter.yaml` to customize rules; the
157
+ extension picks up changes without restarting.
158
+
159
+ ## How It Works
160
+
161
+ 1. `npx` or `bunx` installs `@askviraj/linter` and its dependencies in a
162
+ temporary or local `node_modules`.
163
+ 2. The bundled CLI reads `.config/linter.yaml` from the target repo (if present)
164
+ to load user customizations.
165
+ 3. It builds the ESLint config by merging YAML overrides with the bundled base
166
+ config.
167
+ 4. ESLint's programmatic API lints the current working directory. Plugins are
168
+ resolved from the package's own `node_modules`, not the target repo's.
169
+
170
+ ## License
171
+
172
+ MIT — see [LICENSE](./LICENSE) for details.