@itcase/config 1.7.15 → 1.7.17

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: `tsc --noEmit --pretty`, либо `npm run typecheck`, если конфиг — `lintStagedMonorepo`. Проверка идёт только когда среди изменений есть существующие `.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.15",
3
+ "version": "1.7.17",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -50,6 +50,15 @@ 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}'
53
62
  const AGENT_WAIT_STEPS = [
54
63
  'Asking Cursor Agent',
55
64
  'Still thinking',
@@ -67,6 +76,11 @@ Default model: ${DEFAULT_MODEL} (override with --model / SMART_COMMIT_MODEL).
67
76
  (husky pre-commit / lint-staged and commit-msg). Commit message
68
77
  is still checked by smart-commit before git runs.
69
78
 
79
+ Before the agent, the same typecheck as husky lint-staged runs
80
+ when changes include .ts/.tsx (tsc --noEmit, or npm run typecheck
81
+ from lint-staged config). A type error exits before the agent
82
+ is called. --no-verify skips this check too.
83
+
70
84
  Progress sprite is random (nyan cat, banana, or upyachka).
71
85
  Force with --cat, --banana, or --upyachka. Agent log is below.
72
86
  `
@@ -104,6 +118,11 @@ async function main(argv) {
104
118
  return
105
119
  }
106
120
 
121
+ // Husky typechecks in pre-commit, after the agent has already spent tokens.
122
+ if (!option.noVerify) {
123
+ await runHuskyTypecheck(cwd, changedFiles)
124
+ }
125
+
107
126
  let plans = tryFastPlan(changedFiles, cwd, scopes, types)
108
127
  if (!plans) {
109
128
  plans = await planWithAgent({
@@ -371,6 +390,82 @@ function readReleaseBranches(cwd) {
371
390
  return match[1].split(/\s+/).filter(Boolean)
372
391
  }
373
392
 
393
+ async function runHuskyTypecheck(cwd, files) {
394
+ const command = await typecheckCommandForFiles(cwd, files)
395
+ if (!command) return
396
+
397
+ process.stdout.write(`${paint('1', 'Typecheck')} ${dim(command)}\n`)
398
+ const bin = path.join(cwd, 'node_modules', '.bin')
399
+ const result = spawnSync(command, {
400
+ cwd: cwd,
401
+ env: {
402
+ ...process.env,
403
+ PATH: `${bin}${path.delimiter}${process.env.PATH || ''}`,
404
+ },
405
+ shell: true,
406
+ stdio: 'inherit',
407
+ })
408
+ if (result.error) {
409
+ throw new Error(`Typecheck failed to start: ${result.error.message}`)
410
+ }
411
+ if (result.status !== 0) {
412
+ throw new Error(
413
+ 'Typecheck failed. Fix the errors above — the agent was not called.',
414
+ )
415
+ }
416
+ process.stdout.write('Typecheck passed.\n\n')
417
+ }
418
+
419
+ async function typecheckCommandForFiles(cwd, files) {
420
+ const tsFiles = files.filter(
421
+ (file) =>
422
+ /\.(ts|tsx)$/.test(file) && fs.existsSync(path.resolve(cwd, file)),
423
+ )
424
+ if (!tsFiles.length) return ''
425
+
426
+ const config = await loadLintStagedConfig(cwd)
427
+ const task = config?.[LINT_STAGED_TS_KEY]
428
+ if (typeof task !== 'function') return 'tsc --noEmit --pretty'
429
+
430
+ const absolute = tsFiles.map((file) => path.resolve(cwd, file))
431
+ const commands = await task(absolute)
432
+ return pickTypecheckCommand(commands)
433
+ }
434
+
435
+ function pickTypecheckCommand(commands) {
436
+ const list = (Array.isArray(commands) ? commands : [commands]).filter(
437
+ (command) => typeof command === 'string' && command.trim(),
438
+ )
439
+ const explicit = list.find((command) =>
440
+ /^(?:tsc|npm(?:\s+run)?\s+typecheck|pnpm(?:\s+run)?\s+typecheck|yarn(?:\s+run)?\s+typecheck)\b/.test(
441
+ command.trim(),
442
+ ),
443
+ )
444
+ if (explicit) return explicit.trim()
445
+
446
+ const other = list.find(
447
+ (command) => !/^\s*"?(?:eslint|stylelint)\b/.test(command),
448
+ )
449
+ return other ? other.trim() : ''
450
+ }
451
+
452
+ async function loadLintStagedConfig(cwd) {
453
+ const file = LINT_STAGED_CONFIGS.map((name) => path.join(cwd, name)).find(
454
+ (candidate) => fs.existsSync(candidate),
455
+ )
456
+ if (file) {
457
+ const loaded = await import(pathToFileURL(file).href)
458
+ return loaded.default ?? loaded
459
+ }
460
+
461
+ const packageJsonPath = path.join(cwd, 'package.json')
462
+ if (!fs.existsSync(packageJsonPath)) return null
463
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
464
+ const field = packageJson['lint-staged']
465
+ if (field && typeof field === 'object') return field
466
+ return null
467
+ }
468
+
374
469
  function listFiles(cwd, option) {
375
470
  if (option.staged) {
376
471
  return lines(cwd, [
@@ -1447,9 +1542,14 @@ function stageFiles(cwd, files) {
1447
1542
  async function confirm(count) {
1448
1543
  if (!input.isTTY) throw new Error('No TTY — use --yes or --dry-run')
1449
1544
  const rl = readline.createInterface({ input, output })
1545
+ const prompt = `Create ${count} commit(s)? [y/N] `
1450
1546
  try {
1451
- const answer = await rl.question(`Create ${count} commit(s)? [y/N] `)
1452
- return /^y(es)?$/i.test(answer.trim())
1547
+ for (;;) {
1548
+ const answer = (await rl.question(prompt)).trim()
1549
+ if (/^y(es)?$/i.test(answer)) return true
1550
+ if (/^(n(o)?)?$/i.test(answer)) return false
1551
+ output.write('Enter y or n.\n')
1552
+ }
1453
1553
  } finally {
1454
1554
  rl.close()
1455
1555
  }