my-code-style 1.2.0 → 1.5.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/bin/init CHANGED
@@ -12,7 +12,6 @@
12
12
 
13
13
  const fs = require("fs")
14
14
  const path = require("path")
15
- const { execSync } = require("child_process")
16
15
 
17
16
  const PROJECT_ROOT = process.cwd()
18
17
  const PACKAGE_DIR = path.resolve(__dirname, "..")
@@ -51,6 +50,24 @@ function readTemplate(relativePath) {
51
50
  return fs.readFileSync(path.resolve(PACKAGE_DIR, relativePath), "utf8")
52
51
  }
53
52
 
53
+ /**
54
+ * Append an entry to the project .gitignore (idempotent, keeps existing lines).
55
+ */
56
+ function ensureGitignoreEntry(entry) {
57
+ const gitignorePath = path.resolve(PROJECT_ROOT, ".gitignore")
58
+ if (!fs.existsSync(gitignorePath)) return
59
+ const content = fs.readFileSync(gitignorePath, "utf8")
60
+ const hasEntry = content.split("\n").some((line) => line.trim() === entry)
61
+ if (hasEntry) return
62
+ const separator = content.length === 0 || content.endsWith("\n") ? "" : "\n"
63
+ fs.writeFileSync(
64
+ gitignorePath,
65
+ `${content}${separator}\n# my-code-style 初始化备份目录\n${entry}\n`,
66
+ "utf8",
67
+ )
68
+ log(`已将 ${entry} 追加到 .gitignore`)
69
+ }
70
+
54
71
  function getProjectPkg() {
55
72
  const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
56
73
  if (!fs.existsSync(pkgPath)) return null
@@ -62,76 +79,381 @@ function getProjectPkg() {
62
79
  }
63
80
 
64
81
  function getDependencyVersion(pkg, depName) {
65
- return (pkg.dependencies && pkg.dependencies[depName])
66
- || (pkg.devDependencies && pkg.devDependencies[depName])
67
- || null
82
+ return (
83
+ (pkg.dependencies && pkg.dependencies[depName]) ||
84
+ (pkg.devDependencies && pkg.devDependencies[depName]) ||
85
+ (pkg.peerDependencies && pkg.peerDependencies[depName]) ||
86
+ null
87
+ )
68
88
  }
69
89
 
70
90
  // --- Detection helpers ---
71
91
 
72
92
  /**
73
- * Detect ESLint major version from package.json
74
- * Returns 8, 9, or null (not installed)
93
+ * Detect if project is configured as ECMAScript Module (ESM)
75
94
  */
76
- function detectEslintVersion() {
77
- const pkg = getProjectPkg()
95
+ function isEsmProject(pkg = getProjectPkg()) {
96
+ return pkg?.type === "module"
97
+ }
98
+
99
+ // --- Semver range helpers (dependency-free) ---
100
+ // Detection only needs to know which major versions a declared range can
101
+ // install. Regex guessing on the raw string misreads "<9.0.0" (8 only) and
102
+ // ">=8.0.0" (8 or 9), so ranges are parsed into half-open [lower, upper)
103
+ // intervals with the subset of npm range syntax that appears in practice.
104
+
105
+ const MAX_VERSION = [Number.POSITIVE_INFINITY, 0, 0]
106
+
107
+ function compareVersion(a, b) {
108
+ for (let i = 0; i < 3; i += 1) {
109
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1
110
+ }
111
+ return 0
112
+ }
113
+
114
+ function isWildcardField(field) {
115
+ return field === undefined || field === "" || /^[xX*]$/.test(field)
116
+ }
117
+
118
+ function parseVersionFields(text) {
119
+ const [release] = String(text).trim().replace(/^v/, "").split("-")
120
+ const fields = release.split(".")
121
+ if (fields.length > 3) return null
122
+ const parts = [0, 1, 2].map((index) => {
123
+ const field = fields[index]
124
+ if (isWildcardField(field)) return null
125
+ return /^\d+$/.test(field) ? parseInt(field, 10) : NaN
126
+ })
127
+ return parts.some((part) => Number.isNaN(part)) ? null : parts
128
+ }
129
+
130
+ function lowerBound(parts) {
131
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0]
132
+ }
133
+
134
+ function partialInterval(parts) {
135
+ const lower = lowerBound(parts)
136
+ const missing = parts.findIndex((part) => part === null)
137
+ if (missing === -1) return [lower, [lower[0], lower[1], lower[2] + 1]]
138
+ if (missing === 0) return [[0, 0, 0], MAX_VERSION]
139
+ const upper = [0, 0, 0]
140
+ for (let i = 0; i < missing; i += 1) upper[i] = parts[i]
141
+ upper[missing - 1] += 1
142
+ return [lower, upper]
143
+ }
144
+
145
+ function caretInterval(parts) {
146
+ const lower = lowerBound(parts)
147
+ if (parts[0] === null) return [[0, 0, 0], MAX_VERSION]
148
+ if (parts[0] > 0 || parts[1] === null) return [lower, [parts[0] + 1, 0, 0]]
149
+ if (parts[1] > 0 || parts[2] === null) return [lower, [0, parts[1] + 1, 0]]
150
+ return [lower, [0, 0, parts[2] + 1]]
151
+ }
152
+
153
+ function tildeInterval(parts) {
154
+ const lower = lowerBound(parts)
155
+ if (parts[0] === null) return [[0, 0, 0], MAX_VERSION]
156
+ if (parts[1] === null) return [lower, [parts[0] + 1, 0, 0]]
157
+ return [lower, [parts[0], parts[1] + 1, 0]]
158
+ }
159
+
160
+ function tokenInterval(token) {
161
+ const match = /^(>=|<=|>|<|\^|~|=)?\s*(.*)$/.exec(token.trim())
162
+ if (!match || !match[2]) return null
163
+ const [, operator = "", text] = match
164
+ if (/^[xX*]$/.test(text.trim())) return [[0, 0, 0], MAX_VERSION]
165
+ const parts = parseVersionFields(text)
166
+ if (!parts) return null
167
+ const lower = lowerBound(parts)
168
+ switch (operator) {
169
+ case ">=":
170
+ return [lower, MAX_VERSION]
171
+ case ">":
172
+ return [[lower[0], lower[1], lower[2] + 1], MAX_VERSION]
173
+ case "<":
174
+ return [[0, 0, 0], lower]
175
+ case "<=":
176
+ return [
177
+ [0, 0, 0],
178
+ [lower[0], lower[1], lower[2] + 1],
179
+ ]
180
+ case "^":
181
+ return caretInterval(parts)
182
+ case "~":
183
+ return tildeInterval(parts)
184
+ default:
185
+ return partialInterval(parts)
186
+ }
187
+ }
188
+
189
+ function rangeIntervals(range) {
190
+ const intervals = []
191
+ for (const clause of range.split("||")) {
192
+ const text = clause.trim()
193
+ if (!text) continue
194
+ const hyphen = text.split(/\s+-\s+/)
195
+ const tokens =
196
+ hyphen.length === 2 ? [`>=${hyphen[0]}`, `<=${hyphen[1]}`] : text.split(/\s+/)
197
+ let lower = [0, 0, 0]
198
+ let upper = MAX_VERSION
199
+ let valid = true
200
+ for (const token of tokens) {
201
+ const interval = tokenInterval(token)
202
+ if (!interval) {
203
+ valid = false
204
+ break
205
+ }
206
+ if (compareVersion(interval[0], lower) > 0) lower = interval[0]
207
+ if (compareVersion(interval[1], upper) < 0) upper = interval[1]
208
+ }
209
+ if (valid && compareVersion(lower, upper) < 0) intervals.push([lower, upper])
210
+ }
211
+ return intervals
212
+ }
213
+
214
+ /**
215
+ * List every major version a declared range can install (0-12), or null when
216
+ * the range cannot be parsed.
217
+ */
218
+ function majorsForRange(range) {
219
+ const intervals = rangeIntervals(range)
220
+ if (!intervals.length) return null
221
+ const majors = []
222
+ for (let major = 0; major <= 12; major += 1) {
223
+ const lower = [major, 0, 0]
224
+ const upper = [major + 1, 0, 0]
225
+ const intersects = intervals.some(
226
+ ([intervalLower, intervalUpper]) =>
227
+ compareVersion(intervalLower, upper) < 0 &&
228
+ compareVersion(intervalUpper, lower) > 0,
229
+ )
230
+ if (intersects) majors.push(major)
231
+ }
232
+ return majors.length ? majors : null
233
+ }
234
+
235
+ /**
236
+ * Detect ESLint major version from package.json or installed node_modules
237
+ * Returns 8, 9, or null (not installed or ambiguous)
238
+ */
239
+ function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
78
240
  if (!pkg) return null
79
- const version = getDependencyVersion(pkg, "eslint")
80
- if (!version) return null
81
- // Handle range specifiers like "^9.0.0", ">=8.0.0", "9.x"
82
- const match = version.match(/(\d+)/)
83
- return match ? parseInt(match[1], 10) : null
241
+
242
+ // 1. If eslint is installed in node_modules, read its actual major version directly
243
+ try {
244
+ const installedPkgPath = path.resolve(rootDir, "node_modules", "eslint", "package.json")
245
+ if (fs.existsSync(installedPkgPath)) {
246
+ const installedPkg = JSON.parse(fs.readFileSync(installedPkgPath, "utf8"))
247
+ const installedMajor = parseInt(installedPkg.version?.split(".")[0], 10)
248
+ if (!Number.isNaN(installedMajor)) {
249
+ return installedMajor
250
+ }
251
+ }
252
+ } catch {
253
+ // Fall through to package.json dependency declaration
254
+ }
255
+
256
+ // 2. Parse package.json dependency declaration
257
+ let version = getDependencyVersion(pkg, "eslint")
258
+ if (!version || typeof version !== "string") return null
259
+
260
+ version = version.trim()
261
+
262
+ // Handle npm alias: e.g. "npm:eslint@^8.57.0"
263
+ if (version.startsWith("npm:")) {
264
+ const atIdx = version.lastIndexOf("@")
265
+ if (atIdx > 3) {
266
+ version = version.slice(atIdx + 1).trim()
267
+ }
268
+ }
269
+
270
+ // Handle workspace protocol: e.g. "workspace:^8.0.0", "workspace:*"
271
+ if (version.startsWith("workspace:")) {
272
+ version = version.slice("workspace:".length).trim()
273
+ if (version === "*" || version === "^" || version === "~") {
274
+ return null
275
+ }
276
+ }
277
+
278
+ // Unspecified or dynamic ranges
279
+ if (
280
+ version === "*" ||
281
+ version === "" ||
282
+ version.startsWith("catalog:") ||
283
+ version.startsWith("file:") ||
284
+ version.startsWith("link:")
285
+ ) {
286
+ return null
287
+ }
288
+
289
+ // npm tag: latest
290
+ if (version === "latest") {
291
+ return 9
292
+ }
293
+
294
+ const majors = majorsForRange(version)
295
+ if (!majors) {
296
+ // Fallback: check leading digit if available
297
+ const match = version.match(/(\d+)/)
298
+ return match ? parseInt(match[1], 10) : null
299
+ }
300
+
301
+ // Ranges such as ">=8.0.0" or "^8.57.0 || ^9.0.0" can install either
302
+ // major; without a local install npm resolves the newest one (9).
303
+ if (majors.includes(8) && majors.includes(9)) {
304
+ log("ESLint 依赖同时支持 8 与 9 且未安装本地副本,默认采用 ESLint 9 Flat Config")
305
+ return 9
306
+ }
307
+ if (majors.includes(9)) return 9
308
+ if (majors.includes(8)) return 8
309
+ return majors[0]
310
+ }
311
+
312
+ /**
313
+ * Detect if project is a uni-app project
314
+ */
315
+ function isUniAppProject() {
316
+ const pkg = getProjectPkg()
317
+ if (!pkg) return false
318
+ const allDeps = {
319
+ ...(pkg.dependencies || {}),
320
+ ...(pkg.devDependencies || {}),
321
+ ...(pkg.peerDependencies || {}),
322
+ }
323
+ return !!(allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
324
+ }
325
+
326
+ /**
327
+ * Detect if project is a Vue project
328
+ */
329
+ function isVueProject() {
330
+ const pkg = getProjectPkg()
331
+ if (!pkg) return false
332
+ const allDeps = {
333
+ ...(pkg.dependencies || {}),
334
+ ...(pkg.devDependencies || {}),
335
+ ...(pkg.peerDependencies || {}),
336
+ }
337
+ return !!(allDeps["vue"] || allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
338
+ }
339
+
340
+ /**
341
+ * Detect the package manager in use ("pnpm" | "npm" | "yarn" | "bun" | null).
342
+ * The lockfile is the project's own truth, so it wins over the user agent of
343
+ * whatever launched this CLI (e.g. `npx` inside a pnpm project).
344
+ */
345
+ function detectPackageManager() {
346
+ if (fileExists("pnpm-lock.yaml")) return "pnpm"
347
+ if (fileExists("yarn.lock")) return "yarn"
348
+ if (fileExists("bun.lockb") || fileExists("bun.lock")) return "bun"
349
+ if (fileExists("package-lock.json")) return "npm"
350
+
351
+ const agent = process.env.npm_config_user_agent || ""
352
+ if (agent.startsWith("pnpm/")) return "pnpm"
353
+ if (agent.startsWith("yarn/")) return "yarn"
354
+ if (agent.startsWith("bun/")) return "bun"
355
+ if (agent.startsWith("npm/")) return "npm"
356
+ return null
357
+ }
358
+
359
+ function addDevCommand(packageManager) {
360
+ switch (packageManager) {
361
+ case "npm":
362
+ return "npm i -D"
363
+ case "yarn":
364
+ return "yarn add -D"
365
+ case "bun":
366
+ return "bun add -d"
367
+ default:
368
+ return "pnpm add -D"
369
+ }
370
+ }
371
+
372
+ /**
373
+ * `pnpm prepare` / `yarn prepare` run the script directly, but npm needs an
374
+ * explicit `npm run prepare` (plain `npm prepare` is not a command).
375
+ */
376
+ function runScriptCommand(packageManager, script) {
377
+ switch (packageManager) {
378
+ case "npm":
379
+ case "bun":
380
+ return `${packageManager} run ${script}`
381
+ default:
382
+ return `${packageManager} ${script}`
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Detect project type: "uniapp", "vue", or "base"
388
+ */
389
+ function detectProjectType() {
390
+ if (isUniAppProject()) return "uniapp"
391
+ if (isVueProject()) return "vue"
392
+ return "base"
84
393
  }
85
394
 
86
395
  /**
87
396
  * Detect CSS preprocessor in use
88
- * Returns "scss", "less", or "none"
397
+ * Returns "scss", "less", "both", or "none"
89
398
  */
90
399
  function detectCssPreprocessor() {
91
400
  const pkg = getProjectPkg()
92
401
  if (!pkg) return "scss"
93
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
402
+ const allDeps = {
403
+ ...(pkg.dependencies || {}),
404
+ ...(pkg.devDependencies || {}),
405
+ ...(pkg.peerDependencies || {}),
406
+ }
94
407
 
95
408
  const hasSass = allDeps["sass"] || allDeps["node-sass"] || allDeps["sass-loader"]
96
409
  const hasLess = allDeps["less"] || allDeps["less-loader"]
97
410
 
411
+ if (hasSass && hasLess) return "both"
98
412
  if (hasLess && !hasSass) return "less"
99
413
  if (hasSass) return "scss"
100
- if (hasLess) return "less"
101
414
 
102
415
  // Fallback: check for existing stylelint config
103
416
  if (fileExists(".stylelintrc.cjs") || fileExists(".stylelintrc.js")) {
417
+ const stylelintFile = fileExists(".stylelintrc.cjs")
418
+ ? ".stylelintrc.cjs"
419
+ : ".stylelintrc.js"
420
+ let content = ""
104
421
  try {
105
- const stylelintFile = fileExists(".stylelintrc.cjs")
106
- ? ".stylelintrc.cjs"
107
- : ".stylelintrc.js"
108
- const content = fs.readFileSync(
109
- path.resolve(PROJECT_ROOT, stylelintFile),
110
- "utf8"
111
- )
112
- if (content.includes("postcss-less")) return "less"
422
+ content = fs.readFileSync(path.resolve(PROJECT_ROOT, stylelintFile), "utf8")
113
423
  } catch {}
424
+ // "my-code-style/stylelint" is a prefix of "my-code-style/stylelint/less",
425
+ // so the Less entry must be matched before the SCSS one.
426
+ const usesLess =
427
+ /my-code-style\/stylelint\/less(?:-override)?/.test(content) ||
428
+ content.includes("postcss-less") ||
429
+ content.includes("stylelint-config-recommended-less")
430
+ const usesScss =
431
+ /my-code-style\/stylelint(?![-\w/])/.test(content) ||
432
+ content.includes("postcss-scss") ||
433
+ content.includes("stylelint-config-recommended-scss")
434
+ if (usesLess && usesScss) return "both"
435
+ if (usesLess) return "less"
436
+ if (usesScss) return "scss"
437
+ return "scss"
114
438
  }
115
439
 
116
- return "scss" // default
117
- }
440
+ // If not a Vue/uni-app project and no css preprocessor found, skip stylelint
441
+ if (!isVueProject()) {
442
+ return "none"
443
+ }
118
444
 
119
- /**
120
- * Detect if project is a uni-app project
121
- */
122
- function isUniAppProject() {
123
- const pkg = getProjectPkg()
124
- if (!pkg) return false
125
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
126
- return !!(allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
445
+ return "scss" // default for Vue/uni-app projects
127
446
  }
128
447
 
129
448
  // --- Config templates ---
130
449
 
131
- function eslintrcContent(isUniApp) {
132
- const importPath = isUniApp
133
- ? "my-code-style/eslint/uniapp"
134
- : "my-code-style/eslint/vue3"
450
+ function eslintrcContent(projectType) {
451
+ let importPath = "my-code-style/eslint"
452
+ if (projectType === "uniapp") {
453
+ importPath = "my-code-style/eslint/uniapp"
454
+ } else if (projectType === "vue") {
455
+ importPath = "my-code-style/eslint/vue3"
456
+ }
135
457
 
136
458
  return `// ESLint config — powered by my-code-style
137
459
  // https://www.npmjs.com/package/my-code-style
@@ -144,19 +466,22 @@ module.exports = config
144
466
  `
145
467
  }
146
468
 
147
- function eslintFlatConfigContent(isUniApp) {
148
- const importPath = isUniApp
149
- ? "my-code-style/eslint/flat/uniapp"
150
- : "my-code-style/eslint/flat/vue3"
151
- const varName = isUniApp ? "uniappConfig" : "vue3Config"
469
+ function eslintFlatConfigContent(projectType) {
470
+ let importPath = "my-code-style/eslint/flat"
471
+ let varName = "baseConfig"
472
+ if (projectType === "uniapp") {
473
+ importPath = "my-code-style/eslint/flat/uniapp"
474
+ varName = "uniappConfig"
475
+ } else if (projectType === "vue") {
476
+ importPath = "my-code-style/eslint/flat/vue3"
477
+ varName = "vue3Config"
478
+ }
152
479
 
153
480
  return `// ESLint Flat Config — powered by my-code-style
154
481
  // https://www.npmjs.com/package/my-code-style
155
482
  import ${varName} from "${importPath}"
156
483
 
157
- export default [
158
- ...${varName},
159
- ]
484
+ export default [...${varName}]
160
485
  `
161
486
  }
162
487
 
@@ -171,9 +496,8 @@ function prettierignoreContent() {
171
496
  }
172
497
 
173
498
  function stylelintrcContent(cssPreprocessor) {
174
- const importPath = cssPreprocessor === "less"
175
- ? "my-code-style/stylelint/less"
176
- : "my-code-style/stylelint"
499
+ const importPath =
500
+ cssPreprocessor === "less" ? "my-code-style/stylelint/less" : "my-code-style/stylelint"
177
501
 
178
502
  return `// Stylelint config — powered by my-code-style
179
503
  module.exports = require("${importPath}")
@@ -181,6 +505,11 @@ module.exports = require("${importPath}")
181
505
  }
182
506
 
183
507
  function commitlintrcContent() {
508
+ const mockScope = ["mock", "mocks", "src/mock", "src/mocks"].some((dir) =>
509
+ fs.existsSync(path.resolve(PROJECT_ROOT, dir)),
510
+ )
511
+ ? `, "mock"`
512
+ : ""
184
513
  return `// Commitlint config — powered by my-code-style
185
514
  const base = require("my-code-style/commitlint")
186
515
  const { generateScopes, guessCurrentScope } = require("my-code-style/commitlint/scopes")
@@ -194,7 +523,7 @@ module.exports = {
194
523
  ...base.prompt,
195
524
  customScopesAlign: !scopeComplete ? "top" : "bottom",
196
525
  defaultScope: scopeComplete,
197
- scopes: [...scopes, "mock"],
526
+ scopes: [...scopes${mockScope}],
198
527
  allowEmptyIssuePrefixs: false,
199
528
  allowCustomIssuePrefixs: false,
200
529
  },
@@ -208,20 +537,14 @@ module.exports = require("my-code-style/versionrc")
208
537
  `
209
538
  }
210
539
 
540
+ // Hook bodies live in src/husky/* so the shipped templates cannot drift from
541
+ // what the CLI writes. Normalise the trailing newline.
211
542
  function huskyCommitMsg() {
212
- return `#!/usr/bin/env sh
213
- . "$(dirname -- "$0")/_/husky.sh"
214
-
215
- npx --no-install commitlint --edit
216
- `
543
+ return readTemplate("src/husky/commit-msg").replace(/\s*$/, "\n")
217
544
  }
218
545
 
219
546
  function huskyPreCommit() {
220
- return `#!/usr/bin/env sh
221
- . "$(dirname -- "$0")/_/husky.sh"
222
-
223
- npx --no-install -- lint-staged
224
- `
547
+ return readTemplate("src/husky/pre-commit").replace(/\s*$/, "\n")
225
548
  }
226
549
 
227
550
  function editorconfigContent() {
@@ -232,26 +555,59 @@ function gitattributesContent() {
232
555
  return readTemplate("src/gitattributes")
233
556
  }
234
557
 
558
+ function gitignoreContent() {
559
+ return readTemplate("src/gitignore")
560
+ }
561
+
235
562
  /**
236
563
  * Generate the lint-staged config based on detected CSS preprocessor
237
564
  */
238
565
  function getLintStagedConfig(cssPreprocessor) {
239
- const styleFiles = cssPreprocessor === "less"
240
- ? "**/*.{vue,css,less,html}"
241
- : cssPreprocessor === "none"
242
- ? "**/*.{vue,html}"
243
- : "**/*.{vue,css,scss,html}"
244
-
245
- return {
246
- "**/*.{html,vue,ts,cjs,json,md}": ["prettier --write"],
247
- "**/*.{vue,js,ts,jsx,tsx}": ["eslint --cache --fix"],
248
- [styleFiles]: ["stylelint --fix"],
566
+ // Disjoint groups: a file is owned by exactly one task array.
567
+ // lint-staged runs each array in order, while unrelated groups may run concurrently.
568
+ const componentTasks = ["prettier --write", "eslint --fix"]
569
+ const styleTasks = ["prettier --write"]
570
+ if (cssPreprocessor !== "none") {
571
+ componentTasks.push("stylelint --fix")
572
+ styleTasks.push("stylelint --fix")
249
573
  }
574
+ const styleFiles =
575
+ cssPreprocessor === "both"
576
+ ? "**/*.{html,css,scss,less}"
577
+ : cssPreprocessor === "less"
578
+ ? "**/*.{html,css,less}"
579
+ : cssPreprocessor === "scss"
580
+ ? "**/*.{html,css,scss}"
581
+ : "**/*.{html,css}"
582
+ const config = {
583
+ "**/*.{vue,nvue}": componentTasks,
584
+ "**/*.{js,ts,jsx,tsx,cjs,mjs,mts,cts}": ["prettier --write", "eslint --fix"],
585
+ [styleFiles]: styleTasks,
586
+ "**/*.{json,json5,md,yml,yaml}": ["prettier --write"],
587
+ }
588
+
589
+ return config
250
590
  }
251
591
 
252
592
  // --- Main ---
253
593
 
254
594
  function main() {
595
+ const args = process.argv.slice(2)
596
+ const supported = new Set(["--dry-run", "--backup", "--version", "-v", "--help", "-h"])
597
+ const unknown = args.filter((arg) => !supported.has(arg))
598
+ if (unknown.length) {
599
+ console.error(`未知参数: ${unknown.join(", ")};使用 --help 查看帮助`)
600
+ process.exitCode = 1
601
+ return
602
+ }
603
+ if (args.includes("--help") || args.includes("-h")) {
604
+ console.log("Usage: my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]")
605
+ console.log(" --backup 覆盖已有配置前将其备份到 .my-code-style-backup/ 目录")
606
+ console.log(" --dry-run 试运行,仅输出将执行的变更,不写入任何文件")
607
+ console.log("初始化会覆盖部分配置和 hooks;请先备份并运行 --dry-run。")
608
+ return
609
+ }
610
+
255
611
  // Handle --version / -v
256
612
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
257
613
  const pkg = require("../package.json")
@@ -259,6 +615,34 @@ function main() {
259
615
  return
260
616
  }
261
617
 
618
+ // Validate before detection or any filesystem writes. Missing package.json
619
+ // remains supported, but malformed manifests must never be treated as empty.
620
+ if (fileExists("package.json")) {
621
+ try {
622
+ const pkg = JSON.parse(
623
+ fs.readFileSync(path.resolve(PROJECT_ROOT, "package.json"), "utf8"),
624
+ )
625
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
626
+ throw new Error("package.json 必须是 JSON 对象")
627
+ }
628
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "scripts"]) {
629
+ if (pkg[key] === undefined) continue
630
+ if (
631
+ !pkg[key] ||
632
+ typeof pkg[key] !== "object" ||
633
+ Array.isArray(pkg[key]) ||
634
+ Object.values(pkg[key]).some((value) => typeof value !== "string")
635
+ ) {
636
+ throw new Error(`${key} 必须是值为字符串的对象`)
637
+ }
638
+ }
639
+ } catch (error) {
640
+ console.error(`无法读取有效的 package.json:${error.message};未修改任何文件`)
641
+ process.exitCode = 1
642
+ return
643
+ }
644
+ }
645
+
262
646
  const dryRun = process.argv.includes("--dry-run")
263
647
  const label = dryRun ? "[DRY RUN] " : ""
264
648
 
@@ -272,33 +656,88 @@ function main() {
272
656
  }
273
657
 
274
658
  // --- Detection phase ---
659
+ const pm = detectPackageManager() || "pnpm"
275
660
  const eslintVersion = detectEslintVersion()
276
661
  const cssPreprocessor = detectCssPreprocessor()
277
- const uniApp = isUniAppProject()
662
+ const projectType = detectProjectType()
278
663
 
279
664
  // Determine ESLint format
280
- const useFlatConfig = eslintVersion === 9
665
+ const hasLegacyConfigFile =
666
+ fileExists(".eslintrc.js") ||
667
+ fileExists(".eslintrc.cjs") ||
668
+ fileExists(".eslintrc.json") ||
669
+ fileExists(".eslintrc.yaml") ||
670
+ fileExists(".eslintrc.yml") ||
671
+ fileExists(".eslintrc")
672
+
673
+ const hasFlatConfigFile =
674
+ fileExists("eslint.config.mjs") ||
675
+ fileExists("eslint.config.js") ||
676
+ fileExists("eslint.config.ts") ||
677
+ fileExists("eslint.config.cjs")
678
+
679
+ if (eslintVersion !== null && eslintVersion < 8) {
680
+ console.error(
681
+ `不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 或 9,未修改任何文件`,
682
+ )
683
+ process.exitCode = 1
684
+ return
685
+ }
686
+
687
+ if (eslintVersion !== null && eslintVersion >= 10) {
688
+ warn(
689
+ `ESLint ${eslintVersion} 尚未在 peerDependencies 声明支持(^8.57.0 || ^9.0.0),将按 Flat Config 生成`,
690
+ )
691
+ }
692
+
693
+ // Do not silently mix an installed major version with an incompatible format.
694
+ if (
695
+ (eslintVersion >= 9 && hasLegacyConfigFile && !hasFlatConfigFile) ||
696
+ (eslintVersion === 8 && hasFlatConfigFile)
697
+ ) {
698
+ console.error("ESLint 版本与已有配置格式冲突;请先手动迁移或确认配置格式,未修改任何文件")
699
+ process.exitCode = 1
700
+ return
701
+ }
702
+ if (!eslintVersion && hasLegacyConfigFile && !hasFlatConfigFile) {
703
+ console.error(
704
+ "检测到传统 ESLint 配置但无法确定版本;请先声明 ESLint 版本或迁移到 Flat Config,未修改任何文件",
705
+ )
706
+ process.exitCode = 1
707
+ return
708
+ }
709
+ const useFlatConfig = eslintVersion !== 8
281
710
  const eslintFormat = useFlatConfig ? "flat" : "eslintrc"
282
711
 
283
- log(`检测到 ESLint 版本: ${eslintVersion || "未知"} → 使用 ${eslintFormat} 格式`)
284
- log(`检测到 CSS 预处理器: ${cssPreprocessor}`)
285
- if (uniApp) log(`检测到 uni-app 项目`)
712
+ log(`检测到 ESLint 版本: ${eslintVersion || "默认最新 (9+)"} → 使用 ${eslintFormat} 格式`)
713
+ log(
714
+ `检测到项目类型: ${projectType === "uniapp" ? "uni-app" : projectType === "vue" ? "Vue" : "Node/TS 基础"}`,
715
+ )
716
+ log(
717
+ `检测到 CSS 预处理器: ${cssPreprocessor === "both" ? "SCSS + Less (混合)" : cssPreprocessor}`,
718
+ )
286
719
  console.log("")
287
720
 
288
721
  // --- Build file list based on detection ---
289
722
  const files = []
290
723
 
291
- if (useFlatConfig) {
724
+ if (hasFlatConfigFile || hasLegacyConfigFile) {
725
+ log("保留已有 ESLint 配置,不生成额外入口")
726
+ } else if (useFlatConfig) {
292
727
  files.push({
293
728
  path: "eslint.config.mjs",
294
- exists: fileExists("eslint.config.mjs") || fileExists("eslint.config.js") || fileExists("eslint.config.ts"),
295
- content: eslintFlatConfigContent(uniApp),
729
+ exists:
730
+ fileExists("eslint.config.mjs") ||
731
+ fileExists("eslint.config.js") ||
732
+ fileExists("eslint.config.ts") ||
733
+ fileExists("eslint.config.cjs"),
734
+ content: eslintFlatConfigContent(projectType),
296
735
  })
297
736
  } else {
298
737
  files.push({
299
738
  path: ".eslintrc.cjs",
300
739
  exists: fileExists(".eslintrc.cjs"),
301
- content: eslintrcContent(uniApp),
740
+ content: eslintrcContent(projectType),
302
741
  })
303
742
  }
304
743
 
@@ -331,9 +770,21 @@ function main() {
331
770
  content: commitlintrcContent(),
332
771
  })
333
772
 
773
+ const isEsm = isEsmProject()
774
+ let versionrcPath = ".versionrc.js"
775
+ if (fileExists(".versionrc.cjs") || isEsm) {
776
+ versionrcPath = ".versionrc.cjs"
777
+ }
778
+
779
+ if (isEsm && fileExists(".versionrc.js") && !fileExists(".versionrc.cjs")) {
780
+ warn(
781
+ "检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs",
782
+ )
783
+ }
784
+
334
785
  files.push({
335
- path: ".versionrc.js",
336
- exists: fileExists(".versionrc.js"),
786
+ path: versionrcPath,
787
+ exists: fileExists(versionrcPath),
337
788
  content: versionrcContent(),
338
789
  })
339
790
 
@@ -349,6 +800,14 @@ function main() {
349
800
  content: gitattributesContent(),
350
801
  })
351
802
 
803
+ if (!fileExists(".gitignore")) {
804
+ files.push({
805
+ path: ".gitignore",
806
+ exists: false,
807
+ content: gitignoreContent(),
808
+ })
809
+ }
810
+
352
811
  // Show what will be created/overwritten
353
812
  for (const f of files) {
354
813
  if (f.exists) {
@@ -385,52 +844,172 @@ function main() {
385
844
  return
386
845
  }
387
846
 
388
- // Actually write files
389
- for (const f of files) {
390
- writeFile(f.path, f.content)
847
+ // Optional backup of existing files before modifying
848
+ const backup = args.includes("--backup")
849
+ if (backup) {
850
+ const backupDir = path.resolve(PROJECT_ROOT, ".my-code-style-backup")
851
+ ensureDir(backupDir)
852
+ // package.json is rewritten (scripts + lint-staged), so it must be backed
853
+ // up as well — users lose custom lint-staged groups otherwise.
854
+ for (const item of [...files, ...hooks, { path: "package.json" }]) {
855
+ if (fileExists(item.path)) {
856
+ const srcPath = path.resolve(PROJECT_ROOT, item.path)
857
+ const targetPath = path.resolve(backupDir, item.path)
858
+ ensureDir(path.dirname(targetPath))
859
+ fs.copyFileSync(srcPath, targetPath)
860
+ log(`已备份 ${item.path} -> .my-code-style-backup/${item.path}`)
861
+ }
862
+ }
391
863
  }
392
864
 
393
- // Husky
394
- if (!hasHusky) {
395
- ensureDir(huskyDir)
865
+ // ESLint 8 directory traversal defaults to .js; explicitly include typed
866
+ // files and the selected component formats so `lint` cannot silently skip them.
867
+ const legacyExtensions =
868
+ ".js,.cjs,.mjs,.ts,.mts,.cts,.jsx,.tsx" +
869
+ (projectType !== "base" ? ",.vue" : "") +
870
+ (projectType === "uniapp" ? ",.nvue" : "")
871
+ const lintCommand = useFlatConfig ? "eslint ." : `eslint . --ext ${legacyExtensions}`
872
+
873
+ // Take snapshot of files and hooks before making any filesystem modifications
874
+ const snapshot = new Map()
875
+ for (const f of files) {
876
+ const abs = path.resolve(PROJECT_ROOT, f.path)
877
+ let existed = false
878
+ let isFile = false
879
+ let content = null
880
+ try {
881
+ if (fs.existsSync(abs)) {
882
+ existed = true
883
+ isFile = fs.statSync(abs).isFile()
884
+ if (isFile) content = fs.readFileSync(abs, "utf8")
885
+ }
886
+ } catch {}
887
+ snapshot.set(f.path, { existed, isFile, content })
396
888
  }
397
889
  for (const h of hooks) {
398
- writeFile(h.path, h.content)
399
- fs.chmodSync(path.resolve(PROJECT_ROOT, h.path), 0o755)
400
- }
401
-
402
- // package.json scripts + lint-staged
403
- modifyPackageJson({
404
- scripts: {
405
- prepare: "husky",
406
- release: "standard-version",
407
- cz: "czg",
408
- },
409
- "lint-staged": getLintStagedConfig(cssPreprocessor),
410
- })
411
- success("更新 package.json (scripts + lint-staged)")
890
+ const abs = path.resolve(PROJECT_ROOT, h.path)
891
+ let existed = false
892
+ let isFile = false
893
+ let content = null
894
+ let mode = undefined
895
+ try {
896
+ if (fs.existsSync(abs)) {
897
+ existed = true
898
+ const stat = fs.statSync(abs)
899
+ isFile = stat.isFile()
900
+ mode = stat.mode
901
+ if (isFile) content = fs.readFileSync(abs, "utf8")
902
+ }
903
+ } catch {}
904
+ snapshot.set(h.path, { existed, isFile, content, mode })
905
+ }
906
+ const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
907
+ const hadPkg = fs.existsSync(pkgPath)
908
+ let pkgContent = null
909
+ try {
910
+ if (hadPkg && fs.statSync(pkgPath).isFile()) {
911
+ pkgContent = fs.readFileSync(pkgPath, "utf8")
912
+ }
913
+ } catch {}
914
+ const hadHuskyDir = fs.existsSync(huskyDir)
915
+
916
+ try {
917
+ // Actually write files
918
+ for (const f of files) {
919
+ writeFile(f.path, f.content)
920
+ }
921
+
922
+ // Husky
923
+ if (!hasHusky) {
924
+ ensureDir(huskyDir)
925
+ }
926
+ for (const h of hooks) {
927
+ writeFile(h.path, h.content)
928
+ fs.chmodSync(path.resolve(PROJECT_ROOT, h.path), 0o755)
929
+ }
930
+
931
+ // package.json scripts + lint-staged
932
+ modifyPackageJson({
933
+ scripts: {
934
+ lint: lintCommand,
935
+ "lint:fix": `${lintCommand} --fix`,
936
+ format: "prettier --write .",
937
+ prepare: "husky",
938
+ release: "standard-version",
939
+ cz: "czg",
940
+ },
941
+ "lint-staged": getLintStagedConfig(cssPreprocessor),
942
+ })
943
+ success("更新 package.json (scripts + lint-staged)")
944
+
945
+ // The backup directory is ours, not the user's source — keep it out of git
946
+ if (backup) {
947
+ ensureGitignoreEntry(".my-code-style-backup/")
948
+ }
949
+ } catch (err) {
950
+ console.error(`\n ✖ 初始化写入失败: ${err.message};正在回滚...`)
951
+ for (const [relPath, info] of snapshot.entries()) {
952
+ const abs = path.resolve(PROJECT_ROOT, relPath)
953
+ try {
954
+ if (info.existed && info.isFile) {
955
+ fs.writeFileSync(abs, info.content, "utf8")
956
+ if (info.mode !== undefined) {
957
+ fs.chmodSync(abs, info.mode)
958
+ }
959
+ } else if (!info.existed && fs.existsSync(abs)) {
960
+ fs.rmSync(abs, { force: true, recursive: true })
961
+ }
962
+ } catch (rollbackErr) {
963
+ console.error(` 回滚文件失败: ${relPath} (${rollbackErr.message})`)
964
+ }
965
+ }
966
+ if (hadPkg) {
967
+ fs.writeFileSync(pkgPath, pkgContent, "utf8")
968
+ } else if (fs.existsSync(pkgPath)) {
969
+ fs.rmSync(pkgPath, { force: true })
970
+ }
971
+ if (!hadHuskyDir && fs.existsSync(huskyDir)) {
972
+ try {
973
+ if (fs.readdirSync(huskyDir).length === 0) {
974
+ fs.rmdirSync(huskyDir)
975
+ }
976
+ } catch {}
977
+ }
978
+ console.error(" ✓ 已成功回滚所有更改,工作区已恢复原始状态。\n")
979
+ process.exitCode = 1
980
+ return
981
+ }
412
982
 
413
983
  // Check for missing peerDependencies
414
- checkPeerDeps({ useFlatConfig, cssPreprocessor })
984
+ checkPeerDeps({ useFlatConfig, cssPreprocessor, projectType })
415
985
 
416
986
  console.log("")
417
987
  success("配置初始化完成!")
418
988
  console.log("")
419
989
  console.log(" 下一步:")
420
990
  console.log(" 1. 确保已安装 peerDependencies:")
421
- console.log(" pnpm add -D my-code-style")
991
+ console.log(` ${addDevCommand(pm)} my-code-style`)
422
992
  if (useFlatConfig) {
423
993
  console.log(" 2. Flat Config 需要的额外依赖:")
424
- console.log(" pnpm add -D typescript-eslint globals eslint-plugin-import-x @eslint/js")
994
+ console.log(
995
+ ` ${addDevCommand(pm)} typescript-eslint globals eslint-plugin-import-x @eslint/js`,
996
+ )
425
997
  console.log(" 3. 初始化 husky:")
426
- console.log(" pnpm prepare")
998
+ console.log(` ${runScriptCommand(pm, "prepare")}`)
427
999
  console.log(" 4. 使用 czg 提交 commit:")
428
1000
  } else {
429
1001
  console.log(" 2. 初始化 husky:")
430
- console.log(" pnpm prepare")
1002
+ console.log(` ${runScriptCommand(pm, "prepare")}`)
431
1003
  console.log(" 3. 使用 czg 提交 commit:")
432
1004
  }
433
- console.log(" pnpm cz")
1005
+ console.log(` ${runScriptCommand(pm, "cz")}`)
1006
+ if (pm === "pnpm") {
1007
+ console.log("")
1008
+ console.log(" 提示:pnpm 10+ 默认拦截依赖的构建脚本,若安装时提示")
1009
+ console.log(
1010
+ " ERR_PNPM_IGNORED_BUILDS(如 unrs-resolver),执行 pnpm approve-builds 后重试。",
1011
+ )
1012
+ }
434
1013
  console.log("")
435
1014
  }
436
1015
 
@@ -441,25 +1020,61 @@ function modifyPackageJson(updates) {
441
1020
  pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
442
1021
  }
443
1022
 
444
- // Merge scripts
1023
+ // A generated manifest must be installable on its own: npm requires
1024
+ // name/version (e.g. running init in an empty directory).
1025
+ if (typeof pkg.name !== "string" || !pkg.name.trim()) {
1026
+ pkg.name =
1027
+ path
1028
+ .basename(PROJECT_ROOT)
1029
+ .toLowerCase()
1030
+ .replace(/[^a-z0-9._-]+/g, "-")
1031
+ .replace(/^[._-]+|[._-]+$/g, "") || "my-project"
1032
+ }
1033
+ if (typeof pkg.version !== "string" || !pkg.version.trim()) {
1034
+ pkg.version = "1.0.0"
1035
+ }
1036
+
1037
+ // Merge scripts: updates won't overwrite existing scripts with the same key
445
1038
  if (updates.scripts) {
446
- pkg.scripts = { ...pkg.scripts, ...updates.scripts }
1039
+ pkg.scripts = { ...updates.scripts, ...(pkg.scripts || {}) }
447
1040
  }
448
1041
 
449
- // Merge lint-staged
1042
+ // Set lint-staged (replace existing lint-staged to avoid stale/duplicate patterns)
450
1043
  if (updates["lint-staged"]) {
451
- pkg["lint-staged"] = { ...pkg["lint-staged"], ...updates["lint-staged"] }
1044
+ pkg["lint-staged"] = updates["lint-staged"]
452
1045
  }
453
1046
 
454
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf8")
1047
+ // Keep the project's own indentation (a 2-space repo should not be reflowed)
1048
+ const previous = fileExists("package.json") ? fs.readFileSync(pkgPath, "utf8") : ""
1049
+ const indentMatch = previous.match(/^[ \t]+(?=")/m)
1050
+ fs.writeFileSync(
1051
+ pkgPath,
1052
+ JSON.stringify(pkg, null, indentMatch ? indentMatch[0] : 4) + "\n",
1053
+ "utf8",
1054
+ )
455
1055
  }
456
1056
 
457
- function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {}) {
1057
+ function checkPeerDeps({
1058
+ useFlatConfig = false,
1059
+ cssPreprocessor = "scss",
1060
+ projectType = "base",
1061
+ } = {}) {
1062
+ const pkg = getProjectPkg()
1063
+ if (!pkg) return
1064
+
1065
+ // If running within the package itself, skip external missing deps check
1066
+ if (pkg.name === "my-code-style") {
1067
+ return
1068
+ }
1069
+
458
1070
  const baseDeps = [
459
1071
  "eslint",
1072
+ // Required by @typescript-eslint/parser and typescript-eslint at runtime
1073
+ "typescript",
460
1074
  "prettier",
461
- "eslint-plugin-vue",
1075
+ ...(projectType !== "base" ? ["eslint-plugin-vue"] : []),
462
1076
  "@commitlint/cli",
1077
+ "@commitlint/config-conventional",
463
1078
  "husky",
464
1079
  "lint-staged",
465
1080
  "czg",
@@ -467,25 +1082,73 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
467
1082
  ]
468
1083
 
469
1084
  const flatDeps = useFlatConfig
470
- ? ["typescript-eslint", "vue-eslint-parser", "globals", "eslint-plugin-import-x", "@eslint/js", "eslint-plugin-prettier", "eslint-config-prettier"]
471
- : ["@typescript-eslint/parser", "@typescript-eslint/eslint-plugin", "eslint-plugin-prettier", "eslint-config-prettier"]
472
-
473
- const cssDeps = cssPreprocessor === "scss"
474
- ? ["stylelint", "postcss-scss", "postcss-html"]
475
- : cssPreprocessor === "less"
476
- ? ["stylelint", "postcss-less", "postcss-html"]
477
- : []
1085
+ ? [
1086
+ "typescript-eslint",
1087
+ "eslint-import-resolver-typescript",
1088
+ ...(projectType !== "base" ? ["vue-eslint-parser"] : []),
1089
+ "globals",
1090
+ "eslint-plugin-import-x",
1091
+ "@eslint/js",
1092
+ "eslint-plugin-prettier",
1093
+ "eslint-config-prettier",
1094
+ ]
1095
+ : [
1096
+ "@typescript-eslint/parser",
1097
+ "@typescript-eslint/eslint-plugin",
1098
+ ...(projectType !== "base" ? ["vue-eslint-parser"] : []),
1099
+ "eslint-plugin-import",
1100
+ "eslint-import-resolver-typescript",
1101
+ "eslint-plugin-prettier",
1102
+ "eslint-config-prettier",
1103
+ ]
1104
+
1105
+ const cssDeps =
1106
+ cssPreprocessor === "both"
1107
+ ? [
1108
+ "stylelint",
1109
+ "postcss-scss",
1110
+ "postcss-less",
1111
+ "postcss-html",
1112
+ "stylelint-config-recommended",
1113
+ "stylelint-config-recommended-scss",
1114
+ "stylelint-config-recommended-vue",
1115
+ "stylelint-config-html",
1116
+ "stylelint-config-recess-order",
1117
+ "stylelint-prettier",
1118
+ ]
1119
+ : cssPreprocessor === "scss"
1120
+ ? [
1121
+ "stylelint",
1122
+ "postcss-scss",
1123
+ "postcss-html",
1124
+ "stylelint-config-recommended",
1125
+ "stylelint-config-recommended-scss",
1126
+ "stylelint-config-recommended-vue",
1127
+ "stylelint-config-html",
1128
+ "stylelint-config-recess-order",
1129
+ "stylelint-prettier",
1130
+ ]
1131
+ : cssPreprocessor === "less"
1132
+ ? [
1133
+ "stylelint",
1134
+ "postcss-less",
1135
+ "postcss-html",
1136
+ "stylelint-config-recommended",
1137
+ "stylelint-config-recommended-vue",
1138
+ "stylelint-config-html",
1139
+ "stylelint-config-recess-order",
1140
+ "stylelint-prettier",
1141
+ ]
1142
+ : []
478
1143
 
479
1144
  const allPeerDeps = [...baseDeps, ...flatDeps, ...cssDeps]
480
1145
 
481
1146
  // Read peer dependency versions from my-code-style package.json
482
- const packagePkg = JSON.parse(fs.readFileSync(path.resolve(PACKAGE_DIR, "package.json"), "utf8"))
1147
+ const packagePkg = JSON.parse(
1148
+ fs.readFileSync(path.resolve(PACKAGE_DIR, "package.json"), "utf8"),
1149
+ )
483
1150
  const peerDeps = packagePkg.peerDependencies || {}
484
1151
 
485
- const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
486
- if (!fileExists("package.json")) return
487
-
488
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
489
1152
  const allDeps = {
490
1153
  ...(pkg.dependencies || {}),
491
1154
  ...(pkg.devDependencies || {}),
@@ -497,9 +1160,10 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
497
1160
  warn(`以下依赖未安装:${missing.join(", ")}`)
498
1161
  console.log("")
499
1162
  warn("一键安装命令:")
500
- console.log(` pnpm add -D my-code-style \\`)
1163
+ console.log(` ${addDevCommand(detectPackageManager())} my-code-style \\`)
501
1164
  missing.forEach((dep, i) => {
502
- const version = peerDeps[dep] || ""
1165
+ const version =
1166
+ dep === "eslint" ? (useFlatConfig ? "^9.0.0" : "^8.57.0") : peerDeps[dep] || ""
503
1167
  const depWithVersion = version ? `${dep}@${version}` : dep
504
1168
  const suffix = i < missing.length - 1 ? " \\" : ""
505
1169
  console.log(` ${depWithVersion}${suffix}`)
@@ -507,4 +1171,7 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
507
1171
  }
508
1172
  }
509
1173
 
510
- main()
1174
+ // Only run when executed directly; requiring this file must be side-effect free.
1175
+ if (require.main === module) {
1176
+ main()
1177
+ }