@itcase/config 1.6.74 → 1.6.76

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.
@@ -28,7 +28,9 @@ function buildTypeChoices(types, typeEnum) {
28
28
  const name = meta
29
29
  ? [meta.emoji, value, meta.description].filter(Boolean).join(': ')
30
30
  : value
31
- return { name, value }
31
+ // `short` is what inquirer reprints after select. Without it the
32
+ // full emoji + description wraps and the next input line jumps.
33
+ return { name: name, short: value, value: value }
32
34
  })
33
35
  }
34
36
 
@@ -82,7 +84,11 @@ async function prompter(inquirer, commit) {
82
84
  type: 'list',
83
85
  name: 'scope',
84
86
  message: messages.scope,
85
- choices: scopeList.map((value) => ({ name: value, value: value })),
87
+ choices: scopeList.map((value) => ({
88
+ name: value,
89
+ short: value,
90
+ value: value,
91
+ })),
86
92
  },
87
93
  {
88
94
  type: 'list',
@@ -91,9 +97,13 @@ async function prompter(inquirer, commit) {
91
97
  choices: function (answers) {
92
98
  const subs = isScopesArray ? [] : SCOPES[answers.scope] || []
93
99
  return [
94
- ...subs.map((value) => ({ name: value, value: value })),
100
+ ...subs.map((value) => ({
101
+ name: value,
102
+ short: value,
103
+ value: value,
104
+ })),
95
105
  new inquirer.Separator(),
96
- { name: 'none', value: '' },
106
+ { name: 'none', short: 'none', value: '' },
97
107
  ]
98
108
  },
99
109
  when: function (answers) {
@@ -16,7 +16,7 @@ export const commonRules = {
16
16
  }
17
17
 
18
18
  export const promptSubjectDescription =
19
- 'Short imperative description in lower-case (no camelCase: use "public dir" not "publicDir")'
19
+ 'Short description (lower-case, no camelCase)'
20
20
 
21
21
  export const promptSubjectRules = [
22
22
  'lower-case only (commitlint subject-case)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.6.74",
3
+ "version": "1.6.76",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -95,7 +95,7 @@
95
95
  "postcss-nested-ancestors": "^3.0.0",
96
96
  "postcss-normalize": "^13.0.1",
97
97
  "postcss-prepend-imports": "^1.0.1",
98
- "postcss-preset-env": "^11.5.0",
98
+ "postcss-preset-env": "^11.5.1",
99
99
  "postcss-pxtorem": "^6.1.0",
100
100
  "postcss-sort-media-queries": "^6.7.1",
101
101
  "postcss-unit": "^0.0.3",
@@ -24,6 +24,9 @@ const MAX_DIFF = 80_000
24
24
  const AGENT_TIMEOUT_MS = 120_000
25
25
  const AGENT_STEP_MS = 2_500
26
26
  const BANANA_FRAME_MS = 100
27
+ const INDEX_LOCK_RETRIES = 8
28
+ const INDEX_LOCK_WAIT_MS = 250
29
+ const STALE_LOCK_MS = 1500
27
30
  const AGENT_WAIT_STEPS = [
28
31
  'Asking Cursor Agent',
29
32
  'Still thinking',
@@ -115,24 +118,35 @@ async function main(argv) {
115
118
  )
116
119
  }
117
120
 
121
+ ensureGitIndexReady(cwd)
122
+
118
123
  let created = 0
119
124
  for (const plan of activePlans) {
120
- git(cwd, ['add', '--', ...plan.files])
125
+ const files = plan.files.filter((file) => fileHasChanges(cwd, file))
126
+ if (!files.length) {
127
+ process.stdout.write(`Skipping empty commit: ${plan.message}\n`)
128
+ continue
129
+ }
130
+
131
+ // Drop leftover index entries so only this plan's files are committed.
132
+ git(cwd, ['reset', '-q', 'HEAD'])
133
+ git(cwd, ['add', '--', ...files])
121
134
  const staged = lines(cwd, ['diff', '--cached', '--name-only'])
122
- const stagedForPlan = plan.files.filter((file) => staged.includes(file))
135
+ const stagedForPlan = files.filter((file) => staged.includes(file))
123
136
  if (!stagedForPlan.length) {
124
137
  throw new Error(
125
138
  `Nothing staged for commit: ${plan.message}\n` +
126
- `Files: ${plan.files.join(', ')}`,
139
+ `Files: ${files.join(', ')}`,
127
140
  )
128
141
  }
129
142
 
130
- const commit = spawnSync('git', ['commit', '-m', plan.message], {
131
- cwd: cwd,
132
- encoding: 'utf8',
133
- stdio: ['ignore', 'inherit', 'inherit'],
134
- })
135
- if (commit.status) throw new Error(`git commit failed: ${plan.message}`)
143
+ try {
144
+ git(cwd, ['commit', '-m', plan.message], { inherit: true })
145
+ } catch (error) {
146
+ throw new Error(`git commit failed: ${plan.message}\n${error.message}`, {
147
+ cause: error,
148
+ })
149
+ }
136
150
  created++
137
151
  }
138
152
  process.stdout.write(`Created ${created} commit(s).\n`)
@@ -585,10 +599,89 @@ async function confirm(count) {
585
599
  }
586
600
  }
587
601
 
588
- function git(cwd, args) {
589
- const result = spawnSync('git', args, { cwd: cwd, encoding: 'utf8' })
590
- if (result.status) throw new Error(result.stderr || `git ${args.join(' ')}`)
591
- return result
602
+ function isIndexLockError(stderr) {
603
+ return /index\.lock/.test(stderr || '')
604
+ }
605
+
606
+ function sleepSync(ms) {
607
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
608
+ }
609
+
610
+ function indexLockPath(cwd) {
611
+ const result = spawnSync('git', ['rev-parse', '--git-path', 'index.lock'], {
612
+ cwd: cwd,
613
+ encoding: 'utf8',
614
+ })
615
+ const gitPath = (result.stdout || '').trim()
616
+ if (!gitPath) return path.join(cwd, '.git', 'index.lock')
617
+ return path.resolve(cwd, gitPath)
618
+ }
619
+
620
+ function lockHolders(lockPath) {
621
+ const result = spawnSync('lsof', ['-t', lockPath], { encoding: 'utf8' })
622
+ if (result.error || result.status === 127) return null
623
+ return (result.stdout || '').trim().split('\n').filter(Boolean)
624
+ }
625
+
626
+ function clearStaleIndexLock(cwd) {
627
+ const lockPath = indexLockPath(cwd)
628
+ let stat
629
+ try {
630
+ stat = fs.statSync(lockPath)
631
+ } catch {
632
+ return false
633
+ }
634
+ if (Date.now() - stat.mtimeMs < STALE_LOCK_MS) return false
635
+ const holders = lockHolders(lockPath)
636
+ if (holders && holders.length) return false
637
+ try {
638
+ fs.unlinkSync(lockPath)
639
+ return true
640
+ } catch {
641
+ return false
642
+ }
643
+ }
644
+
645
+ function ensureGitIndexReady(cwd) {
646
+ for (let attempt = 1; attempt <= INDEX_LOCK_RETRIES; attempt++) {
647
+ const lockPath = indexLockPath(cwd)
648
+ if (!fs.existsSync(lockPath)) return
649
+ if (attempt === 1) {
650
+ process.stderr.write('Waiting for git index.lock to clear…\n')
651
+ }
652
+ clearStaleIndexLock(cwd)
653
+ if (!fs.existsSync(lockPath)) return
654
+ sleepSync(INDEX_LOCK_WAIT_MS * attempt)
655
+ }
656
+ const lockPath = indexLockPath(cwd)
657
+ if (!fs.existsSync(lockPath)) return
658
+ throw new Error(
659
+ `git index.lock still exists: ${lockPath}\n` +
660
+ 'Another git process may be running. If it crashed, remove the file and retry.',
661
+ )
662
+ }
663
+
664
+ function git(cwd, args, option = {}) {
665
+ for (let attempt = 1; attempt <= INDEX_LOCK_RETRIES; attempt++) {
666
+ const result = spawnSync('git', args, {
667
+ cwd: cwd,
668
+ encoding: 'utf8',
669
+ stdio: option.inherit ? ['ignore', 'inherit', 'pipe'] : undefined,
670
+ })
671
+ if (!result.status) return result
672
+
673
+ const stderr = result.stderr || ''
674
+ if (!isIndexLockError(stderr) || attempt === INDEX_LOCK_RETRIES) {
675
+ if (option.inherit && stderr) process.stderr.write(stderr)
676
+ throw new Error(stderr.trim() || `git ${args.join(' ')}`)
677
+ }
678
+
679
+ if (attempt === 1) {
680
+ process.stderr.write('Waiting for git index.lock to clear…\n')
681
+ }
682
+ clearStaleIndexLock(cwd)
683
+ sleepSync(INDEX_LOCK_WAIT_MS * attempt)
684
+ }
592
685
  }
593
686
 
594
687
  function out(cwd, args) {