@st1ggy/linter-config 6.3.2 → 7.2.2

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.
@@ -21,6 +21,7 @@ node scripts/inventory.mjs
21
21
  | React | `config.react.js` | React/JSX + hooks. |
22
22
  | Next.js | `config.next.js` | `@next/eslint-plugin-next`. |
23
23
  | Svelte | `svelte-stack.js` | `eslint-plugin-svelte` flat recommended + TS parser options. |
24
+ | Astro | `astro-stack.js` | `eslint-plugin-astro` recommended. |
24
25
 
25
26
  Inventory rule counts (sample files in this repository):
26
27
 
@@ -28,6 +29,7 @@ Inventory rule counts (sample files in this repository):
28
29
  - `eslint-react`: includes React layer on the same sample
29
30
  - `eslint-next`: Next layer on the same sample
30
31
  - `eslint-svelte`: effective rules on `src/examples/example.svelte`
32
+ - `eslint-astro`: effective rules on `src/examples/example.astro`
31
33
 
32
34
  ## Stylelint (SCSS preset)
33
35
 
package/index.d.ts CHANGED
@@ -11,9 +11,11 @@ declare module '@st1ggy/linter-config' {
11
11
  export const eslintReact: Linter.Config[]
12
12
  export const eslintNext: Linter.Config[]
13
13
  export const eslintSvelte: Linter.Config[]
14
+ export const eslintAstro: Linter.Config[]
14
15
 
15
16
  export const prettierCommon: PrettierConfig
16
17
  export const prettierSvelte: PrettierConfig
18
+ export const prettierAstro: PrettierConfig
17
19
 
18
20
  export const stylelintScss: Stylelint.Config
19
21
  }
@@ -42,6 +44,14 @@ declare module '@st1ggy/linter-config/eslint-svelte' {
42
44
  export default config
43
45
  }
44
46
 
47
+ declare module '@st1ggy/linter-config/eslint-astro' {
48
+ import type { Linter } from 'eslint'
49
+
50
+ const config: Linter.Config[]
51
+
52
+ export default config
53
+ }
54
+
45
55
  declare module '@st1ggy/linter-config/eslint-common' {
46
56
  import type { Linter } from 'eslint'
47
57
 
@@ -66,6 +76,14 @@ declare module '@st1ggy/linter-config/prettier-svelte' {
66
76
  export default config
67
77
  }
68
78
 
79
+ declare module '@st1ggy/linter-config/prettier-astro' {
80
+ import type { Config as PrettierConfig } from 'prettier'
81
+
82
+ const config: PrettierConfig
83
+
84
+ export default config
85
+ }
86
+
69
87
  declare module '@st1ggy/linter-config/stylelint-scss' {
70
88
  import type * as Stylelint from 'stylelint'
71
89
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@st1ggy/linter-config",
3
- "version": "6.3.2",
3
+ "version": "7.2.2",
4
4
  "description": "ESLint, Prettier, and Stylelint shareable configs (sources under src/).",
5
5
  "private": false,
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/st1ggy/linter-config.git"
8
+ "url": "https://github.com/St1ggy/linter-config.git"
9
9
  },
10
10
  "keywords": [
11
11
  "eslint",
@@ -22,41 +22,40 @@
22
22
  "@st1ggy/linter-config": "./scripts/linter-init.mjs"
23
23
  },
24
24
  "dependencies": {
25
- "@eslint/compat": "^2.0.5",
25
+ "@eslint/compat": "^2.1.0",
26
+ "@eslint/config-helpers": "^0.7.0",
26
27
  "@eslint/js": "^10.0.1",
27
- "@inquirer/core": "^10.3.2",
28
- "@inquirer/prompts": "^7.10.1",
29
- "@next/eslint-plugin-next": "^16.2.3",
28
+ "@inquirer/core": "^11.2.1",
29
+ "@inquirer/prompts": "^8.5.2",
30
30
  "@stylistic/eslint-plugin": "^5.10.0",
31
- "eslint": "^9.0.0 || ^10.0.0",
31
+ "eslint": "^10.8.0",
32
32
  "eslint-config-prettier": "^10.1.8",
33
33
  "eslint-import-resolver-custom-alias": "^1.3.2",
34
- "eslint-import-resolver-typescript": "^4.4.4",
35
- "eslint-plugin-import-x": "^4.16.2",
36
- "eslint-plugin-prettier": "^5.5.5",
37
- "eslint-plugin-react": "^7.37.5",
38
- "eslint-plugin-react-hooks": "^7.0.1",
39
- "eslint-plugin-sonarjs": "^4.0.2",
40
- "eslint-plugin-svelte": "^3.17.0",
41
- "eslint-plugin-unicorn": "^64.0.0",
34
+ "eslint-import-resolver-typescript": "^4.4.5",
35
+ "eslint-plugin-import-x": "^4.17.1",
36
+ "eslint-plugin-prettier": "^5.5.6",
37
+ "eslint-plugin-sonarjs": "^4.2.0",
38
+ "eslint-plugin-unicorn": "^73.0.0",
42
39
  "find-up-simple": "^1.0.1",
43
40
  "minimist": "^1.2.8",
44
- "ora": "^8.2.0",
45
- "prettier": "^3.0.0",
46
- "prettier-plugin-svelte": "^3.5.1",
47
- "stylelint": "^17.0.0",
48
- "stylelint-config-clean-order": "^8.0.1",
41
+ "ora": "^9.4.1",
42
+ "prettier": "^3.9.6",
43
+ "stylelint": "^17.14.1",
44
+ "stylelint-config-clean-order": "^10.0.0",
49
45
  "stylelint-config-recommended": "^18.0.0",
50
46
  "stylelint-config-recommended-scss": "^17.0.1",
51
47
  "stylelint-config-sass-guidelines": "^13.0.0",
52
48
  "stylelint-no-unsupported-browser-features": "^8.1.1",
53
49
  "stylelint-order": "^8.1.1",
54
50
  "stylelint-prettier": "^5.0.3",
55
- "typescript": "^6.0.2",
56
- "typescript-eslint": "^8.58.1"
51
+ "typescript": "~6.0.3",
52
+ "typescript-eslint": "^8.66.0"
57
53
  },
58
54
  "types": "./index.d.ts",
59
55
  "type": "module",
56
+ "engines": {
57
+ "node": ">=24.16.0"
58
+ },
60
59
  "sideEffects": false,
61
60
  "exports": {
62
61
  ".": {
@@ -79,6 +78,11 @@
79
78
  "import": "./src/eslint/eslint-svelte.config.js",
80
79
  "default": "./src/eslint/eslint-svelte.config.js"
81
80
  },
81
+ "./eslint-astro": {
82
+ "types": "./index.d.ts",
83
+ "import": "./src/eslint/eslint.config.astro.js",
84
+ "default": "./src/eslint/eslint.config.astro.js"
85
+ },
82
86
  "./eslint-common": {
83
87
  "types": "./index.d.ts",
84
88
  "import": "./src/eslint/eslint.config.common.js",
@@ -94,6 +98,11 @@
94
98
  "import": "./src/prettier/prettier-svelte.config.js",
95
99
  "default": "./src/prettier/prettier-svelte.config.js"
96
100
  },
101
+ "./prettier-astro": {
102
+ "types": "./index.d.ts",
103
+ "import": "./src/prettier/prettier-astro.config.js",
104
+ "default": "./src/prettier/prettier-astro.config.js"
105
+ },
97
106
  "./stylelint-scss": {
98
107
  "types": "./index.d.ts",
99
108
  "import": "./src/stylelint/stylelint.config.scss.js",
@@ -119,28 +128,61 @@
119
128
  "publish:npm": "npm publish --access public",
120
129
  "inventory": "node ./scripts/inventory.mjs",
121
130
  "config:init": "node ./scripts/linter-init.mjs init --common --dir ./examples/init-smoke",
131
+ "config:migrate": "node ./scripts/linter-init.mjs migrate --common --dir ./examples/init-smoke",
122
132
  "config:reinit": "node ./scripts/linter-init.mjs reinit --common --dir ./examples/init-smoke",
123
133
  "config:create": "node ./scripts/linter-init.mjs create --common --dir ./examples/init-smoke",
124
134
  "config:remove-current": "bash ./scripts/remove-current.sh"
125
135
  },
126
136
  "peerDependencies": {
127
- "eslint": "^9.0.0 || ^10.0.0",
128
- "next": "^16.0.0",
129
- "prettier": "^3.0.0",
137
+ "astro": ">=4.0.0",
138
+ "eslint": "^10.8.0",
139
+ "eslint-plugin-astro": "^3.1.0",
140
+ "eslint-plugin-jsx-a11y": "^6.10.2",
141
+ "eslint-plugin-react": "^7.37.5",
142
+ "eslint-plugin-react-hooks": "^7.1.1",
143
+ "eslint-plugin-svelte": "^3.22.0",
144
+ "next": "^16.3.0",
145
+ "prettier": "^3.9.6",
146
+ "prettier-plugin-astro": "^0.14.1",
147
+ "prettier-plugin-svelte": "^4.1.1",
130
148
  "react": "^18.0.0 || ^19.0.0",
131
- "stylelint": "^17.0.0",
149
+ "stylelint": "^17.14.1",
132
150
  "svelte": "^4.0.0 || ^5.0.0"
133
151
  },
134
152
  "peerDependenciesMeta": {
135
153
  "eslint": {
136
154
  "optional": true
137
155
  },
156
+ "eslint-plugin-astro": {
157
+ "optional": true
158
+ },
159
+ "eslint-plugin-jsx-a11y": {
160
+ "optional": true
161
+ },
162
+ "eslint-plugin-react": {
163
+ "optional": true
164
+ },
165
+ "eslint-plugin-react-hooks": {
166
+ "optional": true
167
+ },
168
+ "eslint-plugin-svelte": {
169
+ "optional": true
170
+ },
138
171
  "next": {
139
172
  "optional": true
140
173
  },
174
+ "astro": {
175
+ "optional": true
176
+ },
141
177
  "prettier": {
142
178
  "optional": true
143
179
  },
180
+ "prettier-plugin-astro": {
181
+ "optional": true
182
+ },
183
+ "prettier-plugin-svelte": {
184
+ "optional": true
185
+ },
144
186
  "react": {
145
187
  "optional": true
146
188
  },
@@ -152,8 +194,17 @@
152
194
  }
153
195
  },
154
196
  "devDependencies": {
197
+ "@next/eslint-plugin-next": "^16.3.0",
155
198
  "@stylistic/eslint-plugin-migrate": "^4.4.1",
156
- "conventional-changelog-cli": "^5.0.0",
157
- "conventional-changelog-conventionalcommits": "^9.0.0"
199
+ "conventional-changelog": "^8.1.0",
200
+ "conventional-changelog-conventionalcommits": "^10.2.1",
201
+ "eslint-plugin-astro": "^3.1.0",
202
+ "eslint-plugin-jsx-a11y": "^6.10.2",
203
+ "eslint-plugin-react": "^7.37.5",
204
+ "eslint-plugin-react-hooks": "^7.1.1",
205
+ "eslint-plugin-svelte": "^3.22.0",
206
+ "prettier-plugin-astro": "^0.14.1",
207
+ "prettier-plugin-svelte": "^4.1.1",
208
+ "svelte": "^5.56.8"
158
209
  }
159
210
  }
package/scripts/README.md CHANGED
@@ -2,23 +2,25 @@
2
2
 
3
3
  From this repository root, use the npm scripts in the root [`package.json`](../package.json). They run **`node ./scripts/linter-init.mjs`** against **[`examples/init-smoke`](../examples/init-smoke)** so we never write consumer stubs into the **`@st1ggy/linter-config`** package root (the CLI blocks that).
4
4
 
5
- | Script | Effect |
6
- | ----------------------- | --------------------------------------------------------------------------------------- |
7
- | `npm run config:init` | `init` — create missing `eslint.config.js`, `prettier.config.js`, `stylelint.config.js` |
8
- | `npm run config:reinit` | `reinit` — overwrite those three files |
9
- | `npm run config:create` | same as `config:init` |
5
+ | Script | Effect |
6
+ | ------------------------ | --------------------------------------------------------------------------------------- |
7
+ | `npm run config:init` | `init` — create missing `eslint.config.js`, `prettier.config.js`, `stylelint.config.js` |
8
+ | `npm run config:migrate` | `migrate` — optionally remove legacy configs, then overwrite the three wrappers |
9
+ | `npm run config:reinit` | alias for `config:migrate` |
10
+ | `npm run config:create` | same as `config:init` |
10
11
 
11
- Each run installs **`@st1ggy/linter-config`** into `examples/init-smoke` when needed (unless you add **`--skip-install`** to the underlying command in `package.json`).
12
+ Each run installs **`@st1ggy/linter-config`** and only the selected stack's integration plugins into `examples/init-smoke` when needed (unless you add **`--skip-install`** to the underlying command in `package.json`).
12
13
 
13
14
  ### Legacy config cleanup (optional)
14
15
 
15
- **`reinit`** only overwrites `eslint.config.js`, `prettier.config.js`, and `stylelint.config.js`. It does **not** call [`remove-current.sh`](remove-current.sh): that script is **interactive** (prompts per file) and is meant for one-off migration when old filenames still sit next to the new wrappers. From the repo root: **`npm run config:remove-current`** (runs `bash ./scripts/remove-current.sh` in the current shell cwd — run from the directory you want to clean, or `cd` there first).
16
+ **`migrate`** presents every legacy config file for confirmation before it overwrites `eslint.config.js`, `prettier.config.js`, and `stylelint.config.js`. [`remove-current.sh`](remove-current.sh) remains available as a standalone cleanup helper. From the repo root: **`npm run config:remove-current`**.
16
17
 
17
18
  Direct CLI (paths from repo root — same entry as published **`@st1ggy/linter-config`**):
18
19
 
19
20
  ```bash
20
21
  node ./scripts/linter-init.mjs init --react
21
- node ./scripts/linter-init.mjs reinit --svelte --dir ./my-app
22
+ node ./scripts/linter-init.mjs migrate --svelte --dir ./my-app
23
+ node ./scripts/linter-init.mjs init --astro
22
24
  ```
23
25
 
24
26
  ## Flags (published package)
@@ -28,16 +30,17 @@ The CLI is published under the name **`@st1ggy/linter-config`** (see [`package.j
28
30
  ```bash
29
31
  npx @st1ggy/linter-config init --common
30
32
  npx @st1ggy/linter-config init --next
31
- npm exec @st1ggy/linter-config -- reinit --svelte
33
+ npm exec @st1ggy/linter-config -- migrate --svelte
32
34
  ```
33
35
 
34
36
  **Without** installing first:
35
37
 
36
38
  ```bash
37
- npx --yes @st1ggy/linter-config init --react
39
+ npx --yes @st1ggy/linter-config init --astro
38
40
  ```
39
41
 
40
- - **Stack (optional, default common):** `--common` | `--react` | `--next` | `--svelte` (at most one).
42
+ - **Stack (optional, default common):** `--common` | `--react` | `--next` | `--svelte` | `--astro` (at most one).
41
43
  - **`--skip-install`:** only write wrapper files; do not run npm/pnpm/yarn/bun.
44
+ - Every CLI command opens an interactive wizard. `init` creates missing wrappers; `migrate` can remove selected legacy configs and replaces wrappers.
42
45
 
43
46
  Shell shortcuts in this folder (`init-common.sh`, etc.) call **`node …/linter-init.mjs`** (same script as the published **`@st1ggy/linter-config`** CLI) with the matching stack; pass **`--skip-install`** through when needed.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # See init-common.sh — same as npx @st1ggy/linter-config, stack --astro.
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec node "${SCRIPT_DIR}/linter-init.mjs" init --astro "$@"
@@ -3,7 +3,7 @@
3
3
  // Or set `LINTER_CONFIG_ROOT` to override the default (repository root).
4
4
 
5
5
  import { spawnSync } from 'node:child_process'
6
- import { mkdirSync, writeFileSync } from 'node:fs'
6
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
7
  import { createRequire } from 'node:module'
8
8
  import path from 'node:path'
9
9
  import process from 'node:process'
@@ -12,7 +12,8 @@ import { fileURLToPath } from 'node:url'
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
13
  const LINTER_ROOT = process.env.LINTER_CONFIG_ROOT ?? path.resolve(__dirname, '..')
14
14
 
15
- const SOURCE_LABEL = '@st1ggy/linter-config@6.3.2 (local tree)'
15
+ const packageJson = JSON.parse(readFileSync(path.join(LINTER_ROOT, 'package.json'), 'utf8'))
16
+ const SOURCE_LABEL = `${packageJson.name}@${packageJson.version} (local tree)`
16
17
 
17
18
  const flatConfigPresets = [
18
19
  {
@@ -35,6 +36,11 @@ const flatConfigPresets = [
35
36
  module: 'src/eslint/eslint-svelte.config.js',
36
37
  sampleFile: 'src/examples/example.svelte',
37
38
  },
39
+ {
40
+ id: 'eslint-astro',
41
+ module: 'src/eslint/eslint.config.astro.js',
42
+ sampleFile: 'src/examples/example.astro',
43
+ },
38
44
  ]
39
45
 
40
46
  function compareKeysAlphabetically(left, right) {
@@ -99,6 +105,7 @@ async function inventoryFlatConfigs() {
99
105
  const engine = new ESLint({
100
106
  cwd: LINTER_ROOT,
101
107
  overrideConfig: config,
108
+ overrideConfigFile: true,
102
109
  })
103
110
  const filePath = path.join(LINTER_ROOT, preset.sampleFile)
104
111
  const calculated = await engine.calculateConfigForFile(filePath)
@@ -1,33 +1,43 @@
1
1
  // Shared logic for @st1ggy/linter-config CLI (no prompts).
2
2
 
3
3
  import { spawnSync } from 'node:child_process'
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
5
5
  import path from 'node:path'
6
6
 
7
7
  export const PACKAGE = '@st1ggy/linter-config'
8
8
 
9
- export const STACK_KEYS = ['common', 'react', 'next', 'svelte']
9
+ export const STACK_KEYS = ['common', 'react', 'next', 'svelte', 'astro']
10
10
 
11
11
  export const STACKS = {
12
12
  common: {
13
13
  eslint: 'eslint-common',
14
14
  prettier: 'prettier-common',
15
15
  stylelint: 'stylelint-scss',
16
+ packages: [],
16
17
  },
17
18
  react: {
18
19
  eslint: 'eslint-react',
19
20
  prettier: 'prettier-common',
20
21
  stylelint: 'stylelint-scss',
22
+ packages: ['eslint-plugin-react', 'eslint-plugin-react-hooks'],
21
23
  },
22
24
  next: {
23
25
  eslint: 'eslint-next',
24
26
  prettier: 'prettier-common',
25
27
  stylelint: 'stylelint-scss',
28
+ packages: ['@next/eslint-plugin-next', 'eslint-plugin-react', 'eslint-plugin-react-hooks'],
26
29
  },
27
30
  svelte: {
28
31
  eslint: 'eslint-svelte',
29
32
  prettier: 'prettier-svelte',
30
33
  stylelint: 'stylelint-scss',
34
+ packages: ['eslint-plugin-svelte', 'prettier-plugin-svelte'],
35
+ },
36
+ astro: {
37
+ eslint: 'eslint-astro',
38
+ prettier: 'prettier-astro',
39
+ stylelint: 'stylelint-scss',
40
+ packages: ['eslint-plugin-astro', 'eslint-plugin-jsx-a11y', 'prettier-plugin-astro'],
31
41
  },
32
42
  }
33
43
 
@@ -48,6 +58,10 @@ export const STACK_CHOICES = [
48
58
  value: 'svelte',
49
59
  name: 'svelte — Svelte + Prettier plugin for Svelte',
50
60
  },
61
+ {
62
+ value: 'astro',
63
+ name: 'astro — Astro + Prettier plugin for Astro',
64
+ },
51
65
  ]
52
66
 
53
67
  export function readPackageJson(directory) {
@@ -64,7 +78,7 @@ export function readPackageJson(directory) {
64
78
  }
65
79
  }
66
80
 
67
- export function mergedDeps(packageJson) {
81
+ export function mergedDependencies(packageJson) {
68
82
  return {
69
83
  ...packageJson.optionalDependencies,
70
84
  ...packageJson.peerDependencies,
@@ -79,11 +93,12 @@ export function isSelfPackageRoot(directory) {
79
93
  return packageJson?.name === PACKAGE
80
94
  }
81
95
 
82
- export function hasResolvablePackage(startDirectory) {
96
+ export function hasResolvablePackage(startDirectory, packageName = PACKAGE) {
83
97
  let currentDirectory = path.resolve(startDirectory)
98
+ const packageParts = packageName.split('/')
84
99
 
85
100
  while (true) {
86
- const marker = path.join(currentDirectory, 'node_modules', '@st1ggy', 'linter-config', 'package.json')
101
+ const marker = path.join(currentDirectory, 'node_modules', ...packageParts, 'package.json')
87
102
 
88
103
  if (existsSync(marker)) {
89
104
  return true
@@ -144,10 +159,10 @@ export function runPmSync(command, args, cwd) {
144
159
  }
145
160
 
146
161
  // skipInstall: skip npm install; options.quiet: suppress stderr warnings
147
- export function ensureDevDependency(targetDirectory, skipInstall, options = {}) {
162
+ export function ensureDevDependencies(targetDirectory, packages, shouldSkipInstall, options = {}) {
148
163
  const { quiet = false } = options
149
164
 
150
- if (skipInstall) {
165
+ if (shouldSkipInstall) {
151
166
  return
152
167
  }
153
168
 
@@ -158,29 +173,32 @@ export function ensureDevDependency(targetDirectory, skipInstall, options = {})
158
173
  if (!existsSync(packageJsonPath)) {
159
174
  if (!quiet) {
160
175
  process.stderr.write(
161
- `warning: no package.json in ${directory}; add the dependency manually: npm i -D ${PACKAGE}\n`,
176
+ `warning: no package.json in ${directory}; add the packages manually: npm i -D ${packages.join(' ')}\n`,
162
177
  )
163
178
  }
164
179
 
165
180
  return
166
181
  }
167
182
 
168
- if (hasResolvablePackage(directory)) {
183
+ const packageJson = readPackageJson(directory)
184
+
185
+ if (!packageJson) {
169
186
  return
170
187
  }
171
188
 
172
- const packageJson = readPackageJson(directory)
189
+ const dependencies = mergedDependencies(packageJson)
190
+ const packagesToAdd = packages.filter((packageName) => !Object.hasOwn(dependencies, packageName))
191
+ const hasUnresolvedPackage = packages.some((packageName) => !hasResolvablePackage(directory, packageName))
173
192
 
174
- if (!packageJson) {
193
+ if (!hasUnresolvedPackage && packagesToAdd.length === 0) {
175
194
  return
176
195
  }
177
196
 
178
197
  const pm = detectPackageManager(directory)
179
- const listed = Boolean(mergedDeps(packageJson)[PACKAGE])
180
198
 
181
- if (listed) {
199
+ if (packagesToAdd.length === 0) {
182
200
  if (!quiet) {
183
- process.stdout.write(`install: ${pm} install (${directory}) — dependency already listed\n`)
201
+ process.stdout.write(`install: ${pm} install (${directory}) — selected packages already listed\n`)
184
202
  }
185
203
 
186
204
  const sync = {
@@ -196,53 +214,69 @@ export function ensureDevDependency(targetDirectory, skipInstall, options = {})
196
214
  }
197
215
 
198
216
  if (!quiet) {
199
- process.stdout.write(`install: ${pm} add -D ${PACKAGE} (${directory})\n`)
217
+ process.stdout.write(`install: ${pm} add -D ${packagesToAdd.join(' ')} (${directory})\n`)
200
218
  }
201
219
 
202
220
  const add = {
203
- npm: () => runPmSync('npm', ['install', '-D', PACKAGE], directory),
204
- pnpm: () => runPmSync('pnpm', ['add', '-D', PACKAGE], directory),
205
- yarn: () => runPmSync('yarn', ['add', '-D', PACKAGE], directory),
206
- bun: () => runPmSync('bun', ['add', '-d', PACKAGE], directory),
221
+ npm: () => runPmSync('npm', ['install', '-D', ...packagesToAdd], directory),
222
+ pnpm: () => runPmSync('pnpm', ['add', '-D', ...packagesToAdd], directory),
223
+ yarn: () => runPmSync('yarn', ['add', '-D', ...packagesToAdd], directory),
224
+ bun: () => runPmSync('bun', ['add', '-d', ...packagesToAdd], directory),
207
225
  }
208
226
 
209
227
  add[pm]()
210
228
  }
211
229
 
230
+ export function ensureDevDependency(targetDirectory, shouldSkipInstall, options = {}) {
231
+ ensureDevDependencies(targetDirectory, [PACKAGE], shouldSkipInstall, options)
232
+ }
233
+
234
+ export function stackPackages(stackKey) {
235
+ const stack = STACKS[stackKey]
236
+
237
+ if (!stack) {
238
+ throw new Error(`Unknown stack "${stackKey}". Use: ${STACK_KEYS.join(', ')}`)
239
+ }
240
+
241
+ return [PACKAGE, ...stack.packages]
242
+ }
243
+
212
244
  export function printHelp() {
213
245
  process.stdout.write(`\
214
246
  ${PACKAGE} — generate local wrapper configs in a consumer project.
215
247
 
216
- With no command in an interactive terminal, opens a guided menu (or pass -i / --interactive).
248
+ Always opens a guided menu in an interactive terminal. Stack flags preselect a choice.
217
249
 
218
250
  Commands:
219
251
  init Create missing files only (skip existing).
220
- reinit Overwrite selected files.
221
- create Same as init.
252
+ migrate Optionally remove legacy configs, then overwrite wrapper files.
253
+ reinit Alias for migrate.
254
+ create Alias for init.
222
255
 
223
256
  Writes (each command):
224
257
  eslint.config.js, prettier.config.js, stylelint.config.js — re-exports for one stack.
225
258
 
226
- Unless --skip-install: if package.json exists and ${PACKAGE} is not yet resolvable from node_modules
227
- (walking up to the filesystem root), runs the detected package manager to add or sync the dependency.
259
+ Unless --skip-install: if package.json exists, runs the detected package manager to add or sync
260
+ ${PACKAGE} and the selected stack's integration plugins.
228
261
 
229
262
  Stack (at most one; default: common):
230
- --common | --react | --next | --svelte
263
+ --common | --react | --next | --svelte | --astro
231
264
 
232
265
  Options:
233
266
  --dir, -d Target directory (default: current working directory).
234
267
  --skip-install Do not run npm/pnpm/yarn/bun (only write wrapper files).
235
- -i, --interactive Open the menu even when a command is passed (experimental).
268
+ -i, --interactive Accepted for compatibility; the menu is always interactive.
236
269
 
237
270
  Examples (after: npm i -D ${PACKAGE}):
238
271
  npx ${PACKAGE}
239
272
  npx ${PACKAGE} init
240
273
  npx ${PACKAGE} init --react
241
- npx ${PACKAGE} reinit --svelte --dir ./apps/web
274
+ npx ${PACKAGE} migrate --svelte --dir ./apps/web
275
+ npx ${PACKAGE} init --astro
242
276
  npm exec ${PACKAGE} -- init --common
243
277
 
244
278
  Without prior install (downloads this package; then runs the same CLI):
245
- npx --yes ${PACKAGE} init --common
279
+ npx --yes ${PACKAGE} init --astro
246
280
 
247
281
  Repo (paths from root):
248
282
  node scripts/linter-init.mjs init --common
@@ -279,6 +313,33 @@ export function writeFile(targetDirectory, name, content, overwrite, options = {
279
313
  return true
280
314
  }
281
315
 
316
+ export function wrapperFileNames() {
317
+ return ['eslint.config.js', 'prettier.config.js', 'stylelint.config.js']
318
+ }
319
+
320
+ export function existingWrapperFiles(targetDirectory) {
321
+ return wrapperFileNames().filter((name) => existsSync(path.join(targetDirectory, name)))
322
+ }
323
+
324
+ export function legacyConfigFiles(targetDirectory) {
325
+ if (!existsSync(targetDirectory)) {
326
+ return []
327
+ }
328
+
329
+ const wrappers = new Set(wrapperFileNames())
330
+
331
+ return readdirSync(targetDirectory, { withFileTypes: true })
332
+ .filter((entry) => entry.isFile() && /(?:eslint|prettier|stylelint)/i.test(entry.name) && !wrappers.has(entry.name))
333
+ .map((entry) => entry.name)
334
+ .toSorted((left, right) => left.localeCompare(right))
335
+ }
336
+
337
+ export function removeFiles(targetDirectory, names) {
338
+ for (const name of names) {
339
+ rmSync(path.join(targetDirectory, name), { force: true })
340
+ }
341
+ }
342
+
282
343
  export function resolveStackKey(values) {
283
344
  const chosen = []
284
345
 
@@ -309,9 +370,9 @@ export function run(mode, targetDirectory, stackKey, options = {}) {
309
370
  throw new Error(`Unknown stack "${stackKey}". Use: ${STACK_KEYS.join(', ')}`)
310
371
  }
311
372
 
312
- const overwrite = mode === 'reinit'
373
+ const shouldOverwrite = mode === 'migrate' || mode === 'reinit'
313
374
 
314
- writeFile(targetDirectory, 'eslint.config.js', jsReExport(stack.eslint), overwrite, { quiet })
315
- writeFile(targetDirectory, 'prettier.config.js', jsReExport(stack.prettier), overwrite, { quiet })
316
- writeFile(targetDirectory, 'stylelint.config.js', jsReExport(stack.stylelint), overwrite, { quiet })
375
+ writeFile(targetDirectory, 'eslint.config.js', jsReExport(stack.eslint), shouldOverwrite, { quiet })
376
+ writeFile(targetDirectory, 'prettier.config.js', jsReExport(stack.prettier), shouldOverwrite, { quiet })
377
+ writeFile(targetDirectory, 'stylelint.config.js', jsReExport(stack.stylelint), shouldOverwrite, { quiet })
317
378
  }