@itcase/config 1.6.77 → 1.6.78

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/smart-commit/cli.js +139 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.6.77",
3
+ "version": "1.6.78",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -71,16 +71,16 @@ async function main(argv) {
71
71
  const cwd = process.cwd()
72
72
  assertItcaseConfigPackage(cwd)
73
73
  const { scopes, types } = await loadCommitlintConfig(cwd)
74
- const files = listFiles(cwd, option)
75
- if (!files.length) {
74
+ const changedFiles = listFiles(cwd, option)
75
+ if (!changedFiles.length) {
76
76
  process.stdout.write('No changes to commit.\n')
77
77
  return
78
78
  }
79
79
 
80
- const prompt = buildPrompt(scopes, types, files, cwd, option.type)
80
+ const prompt = buildPrompt(scopes, types, changedFiles, cwd, option.type)
81
81
  const raw = await withAgentProgress(
82
82
  {
83
- fileCount: files.length,
83
+ fileCount: changedFiles.length,
84
84
  sprite: pickSprite(option.sprite),
85
85
  verbose: option.verbose,
86
86
  },
@@ -93,7 +93,8 @@ async function main(argv) {
93
93
  progress.onAgentActivity,
94
94
  ),
95
95
  )
96
- const plans = parsePlan(raw, files)
96
+ let plans = parsePlan(raw, changedFiles)
97
+ plans = assignOmittedFiles(plans, changedFiles)
97
98
  await validateAndNormalizePlans(cwd, plans)
98
99
 
99
100
  process.stdout.write('\nCommits:\n')
@@ -131,21 +132,21 @@ async function main(argv) {
131
132
 
132
133
  let created = 0
133
134
  for (const plan of activePlans) {
134
- const files = plan.files.filter((file) => fileHasChanges(cwd, file))
135
- if (!files.length) {
135
+ const planFiles = plan.files.filter((file) => fileHasChanges(cwd, file))
136
+ if (!planFiles.length) {
136
137
  process.stdout.write(`Skipping empty commit: ${plan.message}\n`)
137
138
  continue
138
139
  }
139
140
 
140
141
  // Drop leftover index entries so only this plan's files are committed.
141
142
  git(cwd, ['reset', '-q', 'HEAD'])
142
- git(cwd, ['add', '--', ...files])
143
+ stageFiles(cwd, planFiles)
143
144
  const staged = lines(cwd, ['diff', '--cached', '--name-only'])
144
- const stagedForPlan = files.filter((file) => staged.includes(file))
145
+ const stagedForPlan = planFiles.filter((file) => staged.includes(file))
145
146
  if (!stagedForPlan.length) {
146
147
  throw new Error(
147
148
  `Nothing staged for commit: ${plan.message}\n` +
148
- `Files: ${files.join(', ')}`,
149
+ `Files: ${planFiles.join(', ')}`,
149
150
  )
150
151
  }
151
152
 
@@ -159,6 +160,14 @@ async function main(argv) {
159
160
  created++
160
161
  }
161
162
  process.stdout.write(`Created ${created} commit(s).\n`)
163
+
164
+ const remaining = changedFiles.filter((file) => fileHasChanges(cwd, file))
165
+ if (remaining.length) {
166
+ process.stdout.write(
167
+ `\nUncommitted changes (${remaining.length}):\n` +
168
+ `${remaining.map((file) => ` ${file}`).join('\n')}\n`,
169
+ )
170
+ }
162
171
  }
163
172
 
164
173
  function parseArgs(argv) {
@@ -306,15 +315,17 @@ function readReleaseBranches(cwd) {
306
315
  }
307
316
 
308
317
  function listFiles(cwd, option) {
309
- const staged = lines(cwd, [
310
- 'diff',
311
- '--cached',
312
- '--name-only',
313
- '--diff-filter=ACDMR',
314
- ])
315
- if (option.staged || staged.length) return staged
318
+ if (option.staged) {
319
+ return lines(cwd, [
320
+ 'diff',
321
+ '--cached',
322
+ '--name-only',
323
+ '--diff-filter=ACDMR',
324
+ ])
325
+ }
316
326
  return [
317
327
  ...new Set([
328
+ ...lines(cwd, ['diff', '--cached', '--name-only', '--diff-filter=ACDMR']),
318
329
  ...lines(cwd, ['diff', '--name-only', '--diff-filter=ACDMR']),
319
330
  ...lines(cwd, ['ls-files', '--others', '--exclude-standard']),
320
331
  ]),
@@ -322,10 +333,14 @@ function listFiles(cwd, option) {
322
333
  }
323
334
 
324
335
  function buildPrompt(scopes, types, files, cwd, type) {
325
- let diff = [
326
- out(cwd, ['diff', '--cached', '--', ...files]),
327
- out(cwd, ['diff', '--', ...files]),
328
- ]
336
+ const statusByFile = new Map(
337
+ lines(cwd, ['status', '--short', '--porcelain', '--', ...files]).map(
338
+ (line) => [line.slice(3), line.slice(0, 2).trim()],
339
+ ),
340
+ )
341
+
342
+ let diff = files
343
+ .map((file) => buildFileDiff(cwd, file))
329
344
  .filter(Boolean)
330
345
  .join('\n\n')
331
346
  if (diff.length > MAX_DIFF) diff = `${diff.slice(0, MAX_DIFF)}\n…truncated`
@@ -337,11 +352,17 @@ Scopes (exact): ${buildScopeEnum(scopes).join(', ')}
337
352
  Subject rules:
338
353
  ${promptSubjectRules.map((rule) => `- ${rule}`).join('\n')}
339
354
  Prefer one commit when changes belong to the same feature.
340
- Split by scope. Each listed file in exactly one commit.
355
+ Split by scope. Each listed file in exactly one commit — including new/untracked files (??).
341
356
  Do not add Co-authored-by or Made-with trailers.
342
357
  ${type ? `Prefer type "${type}".\n` : ''}SCOPES: ${JSON.stringify(scopes)}
343
358
  Files:
344
- ${files.map((file) => `- ${file}`).join('\n')}
359
+ ${files
360
+ .map((file) => {
361
+ const status =
362
+ statusByFile.get(file) || (isUntracked(cwd, file) ? '??' : ' M')
363
+ return `- ${file} (${status})`
364
+ })
365
+ .join('\n')}
345
366
  status:
346
367
  ${out(cwd, ['status', '--short', '--', ...files]) || '(empty)'}
347
368
  diff:
@@ -349,6 +370,31 @@ ${diff || '(none)'}
349
370
  `
350
371
  }
351
372
 
373
+ function buildFileDiff(cwd, file) {
374
+ const filePath = path.join(cwd, file)
375
+ if (!fs.existsSync(filePath)) {
376
+ return (
377
+ out(cwd, ['diff', '--cached', '--', file]) ||
378
+ out(cwd, ['diff', '--', file])
379
+ )
380
+ }
381
+
382
+ if (isUntracked(cwd, file)) {
383
+ const nullPath = process.platform === 'win32' ? 'NUL' : '/dev/null'
384
+ const untrackedDiff = out(cwd, ['diff', '--no-index', nullPath, file])
385
+ return untrackedDiff
386
+ ? `new file ${file}:\n${untrackedDiff}`
387
+ : `new file ${file}: (empty)`
388
+ }
389
+
390
+ return [
391
+ out(cwd, ['diff', '--cached', '--', file]),
392
+ out(cwd, ['diff', '--', file]),
393
+ ]
394
+ .filter(Boolean)
395
+ .join('\n')
396
+ }
397
+
352
398
  function buildAgentProgressSteps(fileCount) {
353
399
  const fileLabel =
354
400
  fileCount === 1 ? '1 changed file' : `${fileCount} changed files`
@@ -555,12 +601,60 @@ function parsePlan(raw, allowedFiles) {
555
601
  }
556
602
 
557
603
  if (!plans.length) throw new Error(`Empty commit plan:\n${raw}`)
558
- const missing = allowedFiles.filter((file) => !seen.has(file))
559
- if (missing.length)
560
- throw new Error(`Cursor omitted files:\n${missing.join('\n')}`)
561
604
  return plans
562
605
  }
563
606
 
607
+ function assignOmittedFiles(plans, allowedFiles) {
608
+ const assigned = new Set(plans.flatMap((plan) => plan.files))
609
+ const missing = allowedFiles.filter((file) => !assigned.has(file))
610
+ if (!missing.length) return plans
611
+
612
+ for (const file of missing) {
613
+ const target = findBestCommitForFile(plans, file)
614
+ target.files.push(file)
615
+ process.stderr.write(
616
+ `Auto-assigned omitted file to commit: ${file} → ${target.message}\n`,
617
+ )
618
+ }
619
+ return plans
620
+ }
621
+
622
+ function findBestCommitForFile(plans, file) {
623
+ const fileDir = path.dirname(file)
624
+
625
+ const sameDir = plans.find((plan) =>
626
+ plan.files.some((candidate) => path.dirname(candidate) === fileDir),
627
+ )
628
+ if (sameDir) return sameDir
629
+
630
+ let best = plans[0]
631
+ let bestScore = -1
632
+ for (const plan of plans) {
633
+ for (const candidate of plan.files) {
634
+ const score = commonPathPrefixLength(candidate, file)
635
+ if (score > bestScore) {
636
+ bestScore = score
637
+ best = plan
638
+ }
639
+ }
640
+ }
641
+ return best
642
+ }
643
+
644
+ function commonPathPrefixLength(left, right) {
645
+ const leftParts = left.split('/')
646
+ const rightParts = right.split('/')
647
+ let index = 0
648
+ while (
649
+ index < leftParts.length &&
650
+ index < rightParts.length &&
651
+ leftParts[index] === rightParts[index]
652
+ ) {
653
+ index++
654
+ }
655
+ return index
656
+ }
657
+
564
658
  function normalizeCommitMessage(message) {
565
659
  const lines = String(message || '')
566
660
  .split('\n')
@@ -595,17 +689,25 @@ async function validateAndNormalizePlans(cwd, plans) {
595
689
  }
596
690
  }
597
691
 
598
- function fileHasChanges(cwd, file) {
599
- const filePath = path.join(cwd, file)
600
- if (!fs.existsSync(filePath)) return false
601
-
602
- const tracked =
692
+ function isTracked(cwd, file) {
693
+ return (
603
694
  spawnSync('git', ['ls-files', '--error-unmatch', '--', file], {
604
695
  cwd: cwd,
605
696
  encoding: 'utf8',
606
697
  }).status === 0
698
+ )
699
+ }
607
700
 
608
- if (!tracked) return true
701
+ function isUntracked(cwd, file) {
702
+ return !isTracked(cwd, file) && fs.existsSync(path.join(cwd, file))
703
+ }
704
+
705
+ function fileHasChanges(cwd, file) {
706
+ const filePath = path.join(cwd, file)
707
+ const tracked = isTracked(cwd, file)
708
+
709
+ if (!tracked) return fs.existsSync(filePath)
710
+ if (!fs.existsSync(filePath)) return true
609
711
 
610
712
  return (
611
713
  spawnSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
@@ -615,6 +717,11 @@ function fileHasChanges(cwd, file) {
615
717
  )
616
718
  }
617
719
 
720
+ function stageFiles(cwd, files) {
721
+ if (!files.length) return
722
+ git(cwd, ['add', '-A', '--', ...files])
723
+ }
724
+
618
725
  async function confirm(count) {
619
726
  if (!input.isTTY) throw new Error('No TTY — use --yes or --dry-run')
620
727
  const rl = readline.createInterface({ input, output })