@itcase/config 1.7.16 → 1.7.18

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/README.md CHANGED
@@ -103,7 +103,7 @@ npm install --save-dev commitizen @commitlint/cz-commitlint
103
103
 
104
104
  ## Smart commit
105
105
 
106
- `smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит. Lockfile (`package-lock.json` и аналоги) всегда кладётся в тот же коммит, что и соседний `package.json`; при одном только bump deps агент не вызывается.
106
+ `smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед вызовом агента запускаются те же проверки, что у husky в lint-staged. Сначала eslint и stylelint по изменённым js/ts/css/mdx (с `--fix`, как в хуке): если линтер падает, агент не вызывается и ни один коммит не создаётся. Затем проверка типов: `tsc --noEmit --pretty`, либо `npm run typecheck`, если конфиг — `lintStagedMonorepo`. Typecheck идёт только когда среди изменений есть существующие `.ts`/`.tsx`. `--no-verify` пропускает и эти проверки, и хуки. Перед коммитом CLI ждёт и снимает устаревший `.git/index.lock`, если его никто не держит. Lockfile (`package-lock.json` и аналоги) всегда кладётся в тот же коммит, что и соседний `package.json`; при одном только bump deps агент не вызывается.
107
107
 
108
108
  ```text
109
109
  [random pixel animation]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.7.16",
3
+ "version": "1.7.18",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -67,20 +67,20 @@
67
67
  "chokidar-cli": "^3.0.0",
68
68
  "commitizen": "^4.3.2",
69
69
  "conventional-changelog-conventionalcommits": "9.3.1",
70
- "conventional-changelog-writer": "9.2.1",
71
- "cssnano": "^9.1.1",
72
- "cssnano-preset-default": "^9.1.1",
73
- "inquirer": "^14.2.2",
70
+ "conventional-changelog-writer": "9.3.0",
71
+ "cssnano": "^9.3.2",
72
+ "cssnano-preset-default": "^9.3.2",
73
+ "inquirer": "^14.2.3",
74
74
  "log-update": "^8.0.0",
75
- "postcss": "^8.5.28",
75
+ "postcss": "^8.5.29",
76
76
  "postcss-aspect-ratio-polyfill": "^2.0.0",
77
77
  "postcss-clamp": "^4.1.0",
78
78
  "postcss-cli": "^12.0.0",
79
79
  "postcss-color-hsla-fallback": "^1.0.0",
80
- "postcss-combine-duplicated-selectors": "^10.0.3",
80
+ "postcss-combine-duplicated-selectors": "^11.0.0",
81
81
  "postcss-convert-unit": "^0.1.3",
82
82
  "postcss-dark-theme-class": "^2.0.0",
83
- "postcss-discard-duplicates": "^9.0.4",
83
+ "postcss-discard-duplicates": "^9.0.5",
84
84
  "postcss-each": "^1.1.0",
85
85
  "postcss-easings": "^4.0.0",
86
86
  "postcss-extend-rule": "^4.0.0",
@@ -96,7 +96,7 @@
96
96
  "postcss-nested-ancestors": "^3.0.0",
97
97
  "postcss-normalize": "^13.0.1",
98
98
  "postcss-prepend-imports": "^1.0.1",
99
- "postcss-preset-env": "^11.5.4",
99
+ "postcss-preset-env": "^11.6.1",
100
100
  "postcss-pxtorem": "^6.1.0",
101
101
  "postcss-sort-media-queries": "^6.7.1",
102
102
  "postcss-unit": "^0.0.3",
@@ -117,11 +117,11 @@
117
117
  "@itcase/lint": "^1.1.126",
118
118
  "eslint": "9.39.2",
119
119
  "husky": "^9.1.7",
120
- "inquirer": "^14.2.2",
120
+ "inquirer": "^14.2.3",
121
121
  "lint-staged": "^17.6.0",
122
122
  "prettier": "^3.9.9",
123
- "storybook": "^10.6.0",
124
- "stylelint": "^17.15.0",
123
+ "storybook": "^10.6.1",
124
+ "stylelint": "^17.16.0",
125
125
  "typescript": "^6.0.3"
126
126
  }
127
127
  }
@@ -50,6 +50,29 @@ const UPYACHKA_FRAME_MS = 100
50
50
  const INDEX_LOCK_RETRIES = 8
51
51
  const INDEX_LOCK_WAIT_MS = 250
52
52
  const STALE_LOCK_MS = 1500
53
+ const LINT_STAGED_CONFIGS = [
54
+ 'lint-staged.config.mjs',
55
+ 'lint-staged.config.js',
56
+ 'lint-staged.config.cjs',
57
+ '.lintstagedrc.mjs',
58
+ '.lintstagedrc.js',
59
+ '.lintstagedrc.cjs',
60
+ ]
61
+ const LINT_STAGED_TS_KEY = '*.{js,jsx,mjs,cjs,ts,tsx}'
62
+ const LINT_STAGED_FILE_GROUPS = [
63
+ {
64
+ key: LINT_STAGED_TS_KEY,
65
+ test: /\.(?:js|jsx|mjs|cjs|ts|tsx)$/,
66
+ },
67
+ {
68
+ key: '*.css',
69
+ test: /\.css$/,
70
+ },
71
+ {
72
+ key: '*.mdx',
73
+ test: /\.mdx$/,
74
+ },
75
+ ]
53
76
  const AGENT_WAIT_STEPS = [
54
77
  'Asking Cursor Agent',
55
78
  'Still thinking',
@@ -67,6 +90,13 @@ Default model: ${DEFAULT_MODEL} (override with --model / SMART_COMMIT_MODEL).
67
90
  (husky pre-commit / lint-staged and commit-msg). Commit message
68
91
  is still checked by smart-commit before git runs.
69
92
 
93
+ Before the agent, the same checks as husky lint-staged run.
94
+ eslint and stylelint (--fix) run on changed js/ts/css/mdx, then
95
+ typecheck when changes include .ts/.tsx (tsc --noEmit, or
96
+ npm run typecheck from lint-staged config). A lint or type
97
+ error exits before the agent is called and before any commit.
98
+ --no-verify skips these checks and the hooks.
99
+
70
100
  Progress sprite is random (nyan cat, banana, or upyachka).
71
101
  Force with --cat, --banana, or --upyachka. Agent log is below.
72
102
  `
@@ -104,6 +134,13 @@ async function main(argv) {
104
134
  return
105
135
  }
106
136
 
137
+ // Husky lints and typechecks in pre-commit. By then the agent has spent
138
+ // tokens and earlier commits in the plan may already exist.
139
+ if (!option.noVerify) {
140
+ await runHuskyLint(cwd, changedFiles)
141
+ await runHuskyTypecheck(cwd, changedFiles)
142
+ }
143
+
107
144
  let plans = tryFastPlan(changedFiles, cwd, scopes, types)
108
145
  if (!plans) {
109
146
  plans = await planWithAgent({
@@ -371,6 +408,144 @@ function readReleaseBranches(cwd) {
371
408
  return match[1].split(/\s+/).filter(Boolean)
372
409
  }
373
410
 
411
+ async function runHuskyLint(cwd, files) {
412
+ const commands = await lintCommandsForFiles(cwd, files)
413
+ if (!commands.length) return
414
+
415
+ for (const entry of commands) {
416
+ runShellCommand(
417
+ cwd,
418
+ 'Lint',
419
+ lintDisplay(entry.command, entry.fileCount),
420
+ entry.command,
421
+ 'Lint failed. Fix the errors above — the agent was not called.',
422
+ )
423
+ }
424
+ process.stdout.write('Lint passed.\n\n')
425
+ }
426
+
427
+ async function lintCommandsForFiles(cwd, files) {
428
+ const config = await loadLintStagedConfig(cwd)
429
+ if (!config) return []
430
+
431
+ const commands = []
432
+ for (const group of LINT_STAGED_FILE_GROUPS) {
433
+ const task = config[group.key]
434
+ if (typeof task !== 'function') continue
435
+
436
+ const matched = files.filter(
437
+ (file) => group.test.test(file) && fs.existsSync(path.resolve(cwd, file)),
438
+ )
439
+ if (!matched.length) continue
440
+
441
+ const absolute = matched.map((file) => path.resolve(cwd, file))
442
+ const result = await task(absolute)
443
+ const list = (Array.isArray(result) ? result : [result]).filter(
444
+ (command) => typeof command === 'string' && isLintCommand(command),
445
+ )
446
+ for (const command of list) {
447
+ commands.push({
448
+ command: command.trim(),
449
+ fileCount: matched.length,
450
+ })
451
+ }
452
+ }
453
+ return commands
454
+ }
455
+
456
+ function lintDisplay(command, fileCount) {
457
+ const tool = command.match(/"?(eslint|stylelint)\b/)?.[1] || 'lint'
458
+ const fix = /(?:^|\s)"?--fix"?(?:\s|$)/.test(command) ? ' --fix' : ''
459
+ const filesLabel = fileCount === 1 ? '1 file' : `${fileCount} files`
460
+ return `${tool}${fix} (${filesLabel})`
461
+ }
462
+
463
+ function isLintCommand(command) {
464
+ return /^\s*"?(?:eslint|stylelint)\b/.test(command)
465
+ }
466
+
467
+ async function runHuskyTypecheck(cwd, files) {
468
+ const command = await typecheckCommandForFiles(cwd, files)
469
+ if (!command) return
470
+
471
+ runShellCommand(
472
+ cwd,
473
+ 'Typecheck',
474
+ command,
475
+ command,
476
+ 'Typecheck failed. Fix the errors above — the agent was not called.',
477
+ )
478
+ process.stdout.write('Typecheck passed.\n\n')
479
+ }
480
+
481
+ function runShellCommand(cwd, label, displayCommand, command, failMessage) {
482
+ process.stdout.write(`${paint('1', label)} ${dim(displayCommand)}\n`)
483
+ const bin = path.join(cwd, 'node_modules', '.bin')
484
+ const result = spawnSync(command, {
485
+ cwd: cwd,
486
+ env: {
487
+ ...process.env,
488
+ PATH: `${bin}${path.delimiter}${process.env.PATH || ''}`,
489
+ },
490
+ shell: true,
491
+ stdio: 'inherit',
492
+ })
493
+ if (result.error) {
494
+ throw new Error(`${label} failed to start: ${result.error.message}`)
495
+ }
496
+ if (result.status !== 0) {
497
+ throw new Error(failMessage)
498
+ }
499
+ }
500
+
501
+ async function typecheckCommandForFiles(cwd, files) {
502
+ const tsFiles = files.filter(
503
+ (file) =>
504
+ /\.(ts|tsx)$/.test(file) && fs.existsSync(path.resolve(cwd, file)),
505
+ )
506
+ if (!tsFiles.length) return ''
507
+
508
+ const config = await loadLintStagedConfig(cwd)
509
+ const task = config?.[LINT_STAGED_TS_KEY]
510
+ if (typeof task !== 'function') return 'tsc --noEmit --pretty'
511
+
512
+ const absolute = tsFiles.map((file) => path.resolve(cwd, file))
513
+ const commands = await task(absolute)
514
+ return pickTypecheckCommand(commands)
515
+ }
516
+
517
+ function pickTypecheckCommand(commands) {
518
+ const list = (Array.isArray(commands) ? commands : [commands]).filter(
519
+ (command) => typeof command === 'string' && command.trim(),
520
+ )
521
+ const explicit = list.find((command) =>
522
+ /^(?:tsc|npm(?:\s+run)?\s+typecheck|pnpm(?:\s+run)?\s+typecheck|yarn(?:\s+run)?\s+typecheck)\b/.test(
523
+ command.trim(),
524
+ ),
525
+ )
526
+ if (explicit) return explicit.trim()
527
+
528
+ const other = list.find((command) => !isLintCommand(command))
529
+ return other ? other.trim() : ''
530
+ }
531
+
532
+ async function loadLintStagedConfig(cwd) {
533
+ const file = LINT_STAGED_CONFIGS.map((name) => path.join(cwd, name)).find(
534
+ (candidate) => fs.existsSync(candidate),
535
+ )
536
+ if (file) {
537
+ const loaded = await import(pathToFileURL(file).href)
538
+ return loaded.default ?? loaded
539
+ }
540
+
541
+ const packageJsonPath = path.join(cwd, 'package.json')
542
+ if (!fs.existsSync(packageJsonPath)) return null
543
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
544
+ const field = packageJson['lint-staged']
545
+ if (field && typeof field === 'object') return field
546
+ return null
547
+ }
548
+
374
549
  function listFiles(cwd, option) {
375
550
  if (option.staged) {
376
551
  return lines(cwd, [