@eslint-config-snapshot/cli 0.1.5 → 0.3.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/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  getConfigScaffold,
11
11
  hasDiff,
12
12
  loadConfig,
13
+ normalizePath,
13
14
  readSnapshotFile,
14
15
  sampleWorkspaceFiles,
15
16
  writeSnapshotFile
@@ -30,7 +31,7 @@ type SnapshotDiff = ReturnType<typeof diffSnapshots>
30
31
  type CheckFormat = 'summary' | 'status' | 'diff'
31
32
  type PrintFormat = 'json' | 'short'
32
33
  type InitTarget = 'file' | 'package-json'
33
- type InitPreset = 'minimal' | 'full'
34
+ type InitPreset = 'recommended' | 'minimal' | 'full'
34
35
 
35
36
  type RootOptions = {
36
37
  update?: boolean
@@ -138,11 +139,23 @@ function createProgram(cwd: string, onActionExit: (code: number) => void): Comma
138
139
  onActionExit(0)
139
140
  })
140
141
 
142
+ program
143
+ .command('config')
144
+ .description('Print effective evaluated config')
145
+ .option('--format <format>', 'Output format: json|short', parsePrintFormat, 'json')
146
+ .option('--short', 'Alias for --format short')
147
+ .action(async (opts: { format: PrintFormat; short?: boolean }) => {
148
+ const format: PrintFormat = opts.short ? 'short' : opts.format
149
+ await executeConfig(cwd, format)
150
+ onActionExit(0)
151
+ })
152
+
141
153
  program
142
154
  .command('init')
143
155
  .description('Initialize config (file or package.json)')
144
156
  .option('--target <target>', 'Config target: file|package-json', parseInitTarget)
145
- .option('--preset <preset>', 'Config preset: minimal|full', parseInitPreset)
157
+ .option('--preset <preset>', 'Config preset: recommended|minimal|full', parseInitPreset)
158
+ .option('--show-effective', 'Print the evaluated config that will be written')
146
159
  .option('-f, --force', 'Allow init even when an existing config is detected')
147
160
  .option('-y, --yes', 'Skip prompts and use defaults/options')
148
161
  .addHelpText(
@@ -150,18 +163,17 @@ function createProgram(cwd: string, onActionExit: (code: number) => void): Comma
150
163
  `
151
164
  Examples:
152
165
  $ eslint-config-snapshot init
153
- Runs interactive numbered prompts:
154
- target: 1) package-json, 2) file
155
- preset: 1) minimal, 2) full
166
+ Runs interactive select prompts for target/preset.
167
+ Recommended preset uses checkbox selection for non-default workspaces and group selection.
156
168
 
157
- $ eslint-config-snapshot init --yes --target package-json --preset minimal
158
- Non-interactive minimal setup in package.json.
169
+ $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
170
+ Non-interactive recommended setup in package.json, with effective preview.
159
171
 
160
172
  $ eslint-config-snapshot init --yes --force --target file --preset full
161
173
  Overwrite-safe bypass when a config is already detected.
162
174
  `
163
175
  )
164
- .action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean }) => {
176
+ .action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean; showEffective?: boolean }) => {
165
177
  onActionExit(await runInit(cwd, opts))
166
178
  })
167
179
 
@@ -217,11 +229,11 @@ function parseInitTarget(value: string): InitTarget {
217
229
 
218
230
  function parseInitPreset(value: string): InitPreset {
219
231
  const normalized = value.trim().toLowerCase()
220
- if (normalized === 'minimal' || normalized === 'full') {
232
+ if (normalized === 'recommended' || normalized === 'minimal' || normalized === 'full') {
221
233
  return normalized
222
234
  }
223
235
 
224
- throw new InvalidArgumentError('Expected one of: minimal, full')
236
+ throw new InvalidArgumentError('Expected one of: recommended, minimal, full')
225
237
  }
226
238
 
227
239
  async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation = false): Promise<number> {
@@ -349,23 +361,34 @@ async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
349
361
  process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
350
362
  }
351
363
 
352
- async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
364
+ async function executeConfig(cwd: string, format: PrintFormat): Promise<void> {
365
+ const foundConfig = await findConfigPath(cwd)
353
366
  const config = await loadConfig(cwd)
354
- const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
355
-
356
- const assignments =
357
- config.grouping.mode === 'standalone'
358
- ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] }))
359
- : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: 'default', match: ['**/*'] }])
367
+ const resolved = await resolveWorkspaceAssignments(cwd, config)
368
+ const payload = {
369
+ source: foundConfig?.path ?? 'built-in-defaults',
370
+ workspaceInput: config.workspaceInput,
371
+ workspaces: resolved.discovery.workspacesRel,
372
+ grouping: {
373
+ mode: config.grouping.mode,
374
+ allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
375
+ groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
376
+ },
377
+ sampling: config.sampling
378
+ }
360
379
 
361
- const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false
362
- if (!allowEmptyGroups) {
363
- const empty = assignments.filter((group) => group.workspaces.length === 0)
364
- if (empty.length > 0) {
365
- throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
366
- }
380
+ if (format === 'short') {
381
+ process.stdout.write(formatShortConfig(payload))
382
+ return
367
383
  }
368
384
 
385
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
386
+ }
387
+
388
+ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
389
+ const config = await loadConfig(cwd)
390
+ const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
391
+
369
392
  const snapshots = new Map<string, BuiltSnapshot>()
370
393
 
371
394
  for (const group of assignments) {
@@ -411,6 +434,25 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
411
434
  return snapshots
412
435
  }
413
436
 
437
+ async function resolveWorkspaceAssignments(cwd: string, config: Awaited<ReturnType<typeof loadConfig>>) {
438
+ const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
439
+
440
+ const assignments =
441
+ config.grouping.mode === 'standalone'
442
+ ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] }))
443
+ : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: 'default', match: ['**/*'] }])
444
+
445
+ const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false
446
+ if (!allowEmptyGroups) {
447
+ const empty = assignments.filter((group) => group.workspaces.length === 0)
448
+ if (empty.length > 0) {
449
+ throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
450
+ }
451
+ }
452
+
453
+ return { discovery, assignments }
454
+ }
455
+
414
456
  async function loadStoredSnapshots(cwd: string): Promise<Map<string, StoredSnapshot>> {
415
457
  const dir = path.join(cwd, SNAPSHOT_DIR)
416
458
  const files = await fg('**/*.json', { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true })
@@ -515,9 +557,10 @@ function getDisplayOptionChanges(diff: SnapshotDiff): SnapshotDiff['optionChange
515
557
 
516
558
  async function runInit(
517
559
  cwd: string,
518
- opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean } = {}
560
+ opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean; showEffective?: boolean } = {}
519
561
  ): Promise<number> {
520
562
  const force = opts.force ?? false
563
+ const showEffective = opts.showEffective ?? false
521
564
  const existing = await findConfigPath(cwd)
522
565
  if (existing && !force) {
523
566
  process.stderr.write(
@@ -535,77 +578,50 @@ async function runInit(
535
578
  }
536
579
 
537
580
  const finalTarget = target ?? 'file'
538
- const finalPreset = preset ?? 'minimal'
581
+ const finalPreset = preset ?? 'recommended'
582
+ const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes))
539
583
 
540
- if (finalTarget === 'package-json') {
541
- return runInitInPackageJson(cwd, finalPreset, force)
584
+ if (showEffective) {
585
+ process.stdout.write(`Effective config preview:\n${JSON.stringify(configObject, null, 2)}\n`)
542
586
  }
543
587
 
544
- return runInitInFile(cwd, finalPreset, force)
545
- }
546
-
547
- async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
548
- const rl = createInterface({ input: process.stdin, output: process.stdout })
549
- try {
550
- const target = await askInitTarget(rl)
551
- const preset = await askInitPreset(rl)
552
- return { target, preset }
553
- } finally {
554
- rl.close()
588
+ if (finalTarget === 'package-json') {
589
+ return runInitInPackageJson(cwd, configObject, force)
555
590
  }
556
- }
557
591
 
558
- async function askInitTarget(rl: ReturnType<typeof createInterface>): Promise<InitTarget> {
559
- while (true) {
560
- const answer = await askQuestion(
561
- rl,
562
- 'Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: '
563
- )
564
- const parsed = parseInitTargetChoice(answer)
565
- if (parsed) {
566
- return parsed
567
- }
568
- process.stdout.write('Please choose 1 (package-json) or 2 (file).\n')
569
- }
592
+ return runInitInFile(cwd, configObject, force)
570
593
  }
571
594
 
572
- async function askInitPreset(rl: ReturnType<typeof createInterface>): Promise<InitPreset> {
573
- while (true) {
574
- const answer = await askQuestion(rl, 'Select preset:\n 1) minimal (recommended)\n 2) full\nChoose [1]: ')
575
- const parsed = parseInitPresetChoice(answer)
576
- if (parsed) {
577
- return parsed
578
- }
579
- process.stdout.write('Please choose 1 (minimal) or 2 (full).\n')
580
- }
595
+ async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
596
+ const { select } = await import('@inquirer/prompts')
597
+ const target = await askInitTarget(select)
598
+ const preset = await askInitPreset(select)
599
+ return { target, preset }
581
600
  }
582
601
 
583
- export function parseInitTargetChoice(value: string): InitTarget | undefined {
584
- const normalized = value.trim().toLowerCase()
585
- if (normalized === '') {
586
- return 'package-json'
587
- }
588
- if (normalized === '1' || normalized === 'package-json' || normalized === 'packagejson' || normalized === 'package' || normalized === 'pkg') {
589
- return 'package-json'
590
- }
591
- if (normalized === '2' || normalized === 'file') {
592
- return 'file'
593
- }
594
- return undefined
602
+ async function askInitTarget(
603
+ selectPrompt: (options: { message: string; choices: Array<{ name: string; value: InitTarget }> }) => Promise<InitTarget>
604
+ ): Promise<InitTarget> {
605
+ return selectPrompt({
606
+ message: 'Select config target',
607
+ choices: [
608
+ { name: 'package-json (recommended)', value: 'package-json' },
609
+ { name: 'file', value: 'file' }
610
+ ]
611
+ })
595
612
  }
596
613
 
597
- export function parseInitPresetChoice(value: string): InitPreset | undefined {
598
- const normalized = value.trim().toLowerCase()
599
- if (normalized === '') {
600
- return 'minimal'
601
- }
602
- if (normalized === '1' || normalized === 'minimal' || normalized === 'min') {
603
- return 'minimal'
604
- }
605
- if (normalized === '2' || normalized === 'full') {
606
- return 'full'
607
- }
608
- return undefined
614
+ async function askInitPreset(
615
+ selectPrompt: (options: { message: string; choices: Array<{ name: string; value: InitPreset }> }) => Promise<InitPreset>
616
+ ): Promise<InitPreset> {
617
+ return selectPrompt({
618
+ message: 'Select preset',
619
+ choices: [
620
+ { name: 'recommended (default group "*" + static overrides)', value: 'recommended' },
621
+ { name: 'minimal', value: 'minimal' },
622
+ { name: 'full', value: 'full' }
623
+ ]
624
+ })
609
625
  }
610
626
 
611
627
  function askQuestion(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string> {
@@ -631,7 +647,7 @@ async function askYesNo(prompt: string, defaultYes: boolean): Promise<boolean> {
631
647
  }
632
648
  }
633
649
 
634
- async function runInitInFile(cwd: string, preset: InitPreset, force: boolean): Promise<number> {
650
+ async function runInitInFile(cwd: string, configObject: Record<string, unknown>, force: boolean): Promise<number> {
635
651
  const candidates = [
636
652
  '.eslint-config-snapshot.js',
637
653
  '.eslint-config-snapshot.cjs',
@@ -654,12 +670,12 @@ async function runInitInFile(cwd: string, preset: InitPreset, force: boolean): P
654
670
  }
655
671
 
656
672
  const target = path.join(cwd, 'eslint-config-snapshot.config.mjs')
657
- await writeFile(target, getConfigScaffold(preset), 'utf8')
673
+ await writeFile(target, toConfigScaffold(configObject), 'utf8')
658
674
  process.stdout.write(`Created ${path.basename(target)}\n`)
659
675
  return 0
660
676
  }
661
677
 
662
- async function runInitInPackageJson(cwd: string, preset: InitPreset, force: boolean): Promise<number> {
678
+ async function runInitInPackageJson(cwd: string, configObject: Record<string, unknown>, force: boolean): Promise<number> {
663
679
  const packageJsonPath = path.join(cwd, 'package.json')
664
680
 
665
681
  let packageJsonRaw: string
@@ -683,12 +699,142 @@ async function runInitInPackageJson(cwd: string, preset: InitPreset, force: bool
683
699
  return 1
684
700
  }
685
701
 
686
- parsed['eslint-config-snapshot'] = preset === 'full' ? getFullPresetObject() : {}
702
+ parsed['eslint-config-snapshot'] = configObject
687
703
  await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8')
688
704
  process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n')
689
705
  return 0
690
706
  }
691
707
 
708
+ async function resolveInitConfigObject(
709
+ cwd: string,
710
+ preset: InitPreset,
711
+ nonInteractive: boolean
712
+ ): Promise<Record<string, unknown>> {
713
+ if (preset === 'minimal') {
714
+ return {}
715
+ }
716
+
717
+ if (preset === 'full') {
718
+ return getFullPresetObject()
719
+ }
720
+
721
+ return buildRecommendedPresetObject(cwd, nonInteractive)
722
+ }
723
+
724
+ async function buildRecommendedPresetObject(cwd: string, nonInteractive: boolean): Promise<Record<string, unknown>> {
725
+ const workspaces = await discoverInitWorkspaces(cwd)
726
+ const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY
727
+ const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : new Map<string, number>()
728
+ return buildRecommendedConfigFromAssignments(workspaces, assignments)
729
+ }
730
+
731
+ export function buildRecommendedConfigFromAssignments(
732
+ workspaces: string[],
733
+ assignments: Map<string, number>
734
+ ): Record<string, unknown> {
735
+ const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b)
736
+ if (groupNumbers.length === 0) {
737
+ return {}
738
+ }
739
+
740
+ const explicitGroups = groupNumbers.map((number) => ({
741
+ name: `group-${number}`,
742
+ match: workspaces.filter((workspace) => assignments.get(workspace) === number)
743
+ }))
744
+
745
+ return {
746
+ grouping: {
747
+ mode: 'match',
748
+ groups: [...explicitGroups, { name: 'default', match: ['**/*'] }]
749
+ }
750
+ }
751
+ }
752
+
753
+ async function discoverInitWorkspaces(cwd: string): Promise<string[]> {
754
+ const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: 'discover' } })
755
+ if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === '.')) {
756
+ return discovered.workspacesRel
757
+ }
758
+
759
+ const packageJsonPath = path.join(cwd, 'package.json')
760
+ try {
761
+ const raw = await readFile(packageJsonPath, 'utf8')
762
+ const parsed = JSON.parse(raw) as { workspaces?: string[] | { packages?: string[] } }
763
+ let workspacePatterns: string[] = []
764
+ if (Array.isArray(parsed.workspaces)) {
765
+ workspacePatterns = parsed.workspaces
766
+ } else if (parsed.workspaces && typeof parsed.workspaces === 'object' && Array.isArray(parsed.workspaces.packages)) {
767
+ workspacePatterns = parsed.workspaces.packages
768
+ }
769
+
770
+ if (workspacePatterns.length === 0) {
771
+ return discovered.workspacesRel
772
+ }
773
+
774
+ const workspacePackageFiles = await fg(
775
+ workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
776
+ { cwd, onlyFiles: true, dot: true }
777
+ )
778
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort((a, b) =>
779
+ a.localeCompare(b)
780
+ )
781
+ if (workspaceDirs.length > 0) {
782
+ return workspaceDirs
783
+ }
784
+ } catch {
785
+ // fallback to discovered output
786
+ }
787
+
788
+ return discovered.workspacesRel
789
+ }
790
+
791
+ function trimTrailingSlashes(value: string): string {
792
+ let normalized = value
793
+ while (normalized.endsWith('/')) {
794
+ normalized = normalized.slice(0, -1)
795
+ }
796
+ return normalized
797
+ }
798
+
799
+ async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map<string, number>> {
800
+ const { checkbox, select } = await import('@inquirer/prompts')
801
+ process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n')
802
+ const overrides = await checkbox<string>({
803
+ message: 'Workspaces outside default group:',
804
+ choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
805
+ pageSize: Math.min(12, Math.max(4, workspaces.length))
806
+ })
807
+
808
+ const assignments = new Map<string, number>()
809
+ let nextGroup = 1
810
+ for (const workspace of overrides) {
811
+ const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b)
812
+ while (usedGroups.includes(nextGroup)) {
813
+ nextGroup += 1
814
+ }
815
+
816
+ const selected = await select<number | 'new'>({
817
+ message: `Select group for ${workspace}`,
818
+ choices: [
819
+ ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
820
+ { name: `create new group (group-${nextGroup})`, value: 'new' }
821
+ ]
822
+ })
823
+ const groupNumber = selected === 'new' ? nextGroup : selected
824
+ assignments.set(workspace, groupNumber)
825
+ }
826
+
827
+ return assignments
828
+ }
829
+
830
+ function toConfigScaffold(configObject: Record<string, unknown>): string {
831
+ if (Object.keys(configObject).length === 0) {
832
+ return getConfigScaffold('minimal')
833
+ }
834
+
835
+ return `export default ${JSON.stringify(configObject, null, 2)}\n`
836
+ }
837
+
692
838
  function getFullPresetObject() {
693
839
  return {
694
840
  workspaceInput: { mode: 'discover' },
@@ -859,3 +1005,22 @@ function formatShortPrint(
859
1005
 
860
1006
  return `${lines.join('\n')}\n`
861
1007
  }
1008
+
1009
+ function formatShortConfig(payload: {
1010
+ source: string
1011
+ workspaceInput: unknown
1012
+ workspaces: string[]
1013
+ grouping: { mode: string; allowEmptyGroups: boolean; groups: Array<{ name: string; workspaces: string[] }> }
1014
+ sampling: unknown
1015
+ }): string {
1016
+ const lines: string[] = [
1017
+ `source: ${payload.source}`,
1018
+ `workspaces (${payload.workspaces.length}): ${payload.workspaces.join(', ') || '(none)'}`,
1019
+ `grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
1020
+ ]
1021
+ for (const group of payload.grouping.groups) {
1022
+ lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(', ') || '(none)'}`)
1023
+ }
1024
+ lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`)
1025
+ return `${lines.join('\n')}\n`
1026
+ }
@@ -1,18 +1,19 @@
1
1
  import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
- import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
5
 
6
- import { parseInitPresetChoice, parseInitTargetChoice, runCli } from '../src/index.js'
6
+ import { buildRecommendedConfigFromAssignments, runCli } from '../src/index.js'
7
7
 
8
- const fixtureRoot = path.resolve('test/fixtures/repo')
9
-
10
- afterAll(async () => {
11
- await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
12
- })
8
+ const fixtureTemplateRoot = path.resolve('test/fixtures/repo')
9
+ let tmpDir = ''
10
+ let fixtureRoot = ''
13
11
 
14
12
  beforeEach(async () => {
15
- await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
13
+ tmpDir = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-integration-'))
14
+ fixtureRoot = path.join(tmpDir, 'repo')
15
+ await cp(fixtureTemplateRoot, fixtureRoot, { recursive: true })
16
+
16
17
  await mkdir(path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/bin'), { recursive: true })
17
18
  await mkdir(path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin'), { recursive: true })
18
19
 
@@ -35,24 +36,31 @@ beforeEach(async () => {
35
36
  )
36
37
  })
37
38
 
38
- describe('cli integration', () => {
39
- it('parses init interactive target choices from numeric and aliases', () => {
40
- expect(parseInitTargetChoice('')).toBe('package-json')
41
- expect(parseInitTargetChoice('1')).toBe('package-json')
42
- expect(parseInitTargetChoice('package')).toBe('package-json')
43
- expect(parseInitTargetChoice('pkg')).toBe('package-json')
44
- expect(parseInitTargetChoice('2')).toBe('file')
45
- expect(parseInitTargetChoice('file')).toBe('file')
46
- expect(parseInitTargetChoice('invalid')).toBeUndefined()
39
+ afterEach(async () => {
40
+ if (tmpDir) {
41
+ await rm(tmpDir, { recursive: true, force: true })
42
+ tmpDir = ''
43
+ fixtureRoot = ''
44
+ }
45
+ })
46
+
47
+ describe.sequential('cli integration', () => {
48
+ it('builds recommended config as dynamic-only when no static overrides are selected', () => {
49
+ const config = buildRecommendedConfigFromAssignments(['packages/ws-a', 'packages/ws-b'], new Map())
50
+ expect(config).toEqual({})
47
51
  })
48
52
 
49
- it('parses init interactive preset choices from numeric and aliases', () => {
50
- expect(parseInitPresetChoice('')).toBe('minimal')
51
- expect(parseInitPresetChoice('1')).toBe('minimal')
52
- expect(parseInitPresetChoice('min')).toBe('minimal')
53
- expect(parseInitPresetChoice('2')).toBe('full')
54
- expect(parseInitPresetChoice('full')).toBe('full')
55
- expect(parseInitPresetChoice('invalid')).toBeUndefined()
53
+ it('builds recommended config with static overrides plus dynamic catch-all', () => {
54
+ const config = buildRecommendedConfigFromAssignments(['packages/ws-a', 'packages/ws-b'], new Map([['packages/ws-b', 2]]))
55
+ expect(config).toEqual({
56
+ grouping: {
57
+ mode: 'match',
58
+ groups: [
59
+ { name: 'group-2', match: ['packages/ws-b'] },
60
+ { name: 'default', match: ['**/*'] }
61
+ ]
62
+ }
63
+ })
56
64
  })
57
65
 
58
66
  it('snapshot writes deterministic snapshot files', async () => {
@@ -126,11 +134,22 @@ no-debugger: off
126
134
  expect(code).toBe(0)
127
135
 
128
136
  const content = await readFile(path.join(tmp, 'eslint-config-snapshot.config.mjs'), 'utf8')
129
- expect(content).toContain("workspaceInput: { mode: 'discover' }")
137
+ expect(content).toContain('"workspaceInput"')
138
+ expect(content).toContain('"grouping"')
139
+ expect(content).toContain('"sampling"')
130
140
 
131
141
  await rm(tmp, { recursive: true, force: true })
132
142
  })
133
143
 
144
+ it('config prints effective evaluated config and exits 0', async () => {
145
+ const writeSpy = vi.spyOn(process.stdout, 'write')
146
+ const code = await runCli('config', fixtureRoot)
147
+ expect(code).toBe(0)
148
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"workspaceInput"'))
149
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"workspaces"'))
150
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"groups"'))
151
+ })
152
+
134
153
  it('init writes minimal config to package.json when target=package-json', async () => {
135
154
  const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-init-pkg-'))
136
155
  await writeFile(path.join(tmp, 'package.json'), JSON.stringify({ name: 'fixture', private: true }, null, 2))
@@ -66,6 +66,7 @@ describe('cli terminal invocation', () => {
66
66
  expect(result.stdout).toContain('check [options]')
67
67
  expect(result.stdout).toContain('update|snapshot')
68
68
  expect(result.stdout).toContain('print [options]')
69
+ expect(result.stdout).toContain('config [options]')
69
70
  expect(result.stdout).toContain('init')
70
71
  expect(result.stderr).toBe('')
71
72
  })
@@ -232,6 +233,15 @@ no-debugger: off
232
233
  expect(existing.stderr).toContain('rerun with --force')
233
234
  })
234
235
 
236
+ it('config prints effective evaluated config output', () => {
237
+ const result = run(['config'])
238
+ expect(result.status).toBe(0)
239
+ expect(result.stdout).toContain('"workspaceInput"')
240
+ expect(result.stdout).toContain('"workspaces"')
241
+ expect(result.stdout).toContain('"groups"')
242
+ expect(result.stderr).toBe('')
243
+ })
244
+
235
245
  it('init can write config to package.json', async () => {
236
246
  const initRoot = path.join(tmpDir, 'init-package-json-case')
237
247
  await rm(initRoot, { recursive: true, force: true })
@@ -250,6 +260,44 @@ no-debugger: off
250
260
  expect(parsed['eslint-config-snapshot']).toEqual({})
251
261
  })
252
262
 
263
+ it('init recommended writes grouped workspace config in package.json', async () => {
264
+ const initRoot = path.join(tmpDir, 'init-recommended-package-json-case')
265
+ await rm(initRoot, { recursive: true, force: true })
266
+ await cp(fixtureRoot, initRoot, { recursive: true })
267
+ repoRoot = initRoot
268
+
269
+ await rm(path.join(repoRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
270
+
271
+ const created = run(['init', '--yes', '--target', 'package-json', '--preset', 'recommended'])
272
+ expect(created.status).toBe(0)
273
+ expect(created.stdout).toBe('Created config in package.json under "eslint-config-snapshot"\n')
274
+ expect(created.stderr).toBe('')
275
+
276
+ const packageJsonRaw = await readFile(path.join(repoRoot, 'package.json'), 'utf8')
277
+ const parsed = JSON.parse(packageJsonRaw) as {
278
+ 'eslint-config-snapshot'?: Record<string, unknown>
279
+ }
280
+
281
+ expect(parsed['eslint-config-snapshot']).toEqual({})
282
+ })
283
+
284
+ it('init recommended --show-effective prints preview without explicit sampling block', async () => {
285
+ const initRoot = path.join(tmpDir, 'init-recommended-preview-case')
286
+ await rm(initRoot, { recursive: true, force: true })
287
+ await cp(fixtureRoot, initRoot, { recursive: true })
288
+ repoRoot = initRoot
289
+
290
+ await rm(path.join(repoRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
291
+
292
+ const result = run(['init', '--yes', '--target', 'package-json', '--preset', 'recommended', '--show-effective'])
293
+ expect(result.status).toBe(0)
294
+ expect(result.stdout).toContain('Effective config preview:')
295
+ expect(result.stdout).toContain('{}')
296
+ expect(result.stdout).not.toContain('"workspaceInput"')
297
+ expect(result.stdout).not.toContain('"grouping"')
298
+ expect(result.stdout).not.toContain('"sampling"')
299
+ })
300
+
253
301
  it('init fails early on existing config unless --force is provided', async () => {
254
302
  const initRoot = path.join(tmpDir, 'init-force-case')
255
303
  await rm(initRoot, { recursive: true, force: true })
@@ -335,14 +383,14 @@ no-debugger: off
335
383
  expect(result.stderr).toBe('')
336
384
  })
337
385
 
338
- it('prints init help with numbered prompt and force guidance', () => {
386
+ it('prints init help with select-prompt and force guidance', () => {
339
387
  const result = run(['init', '--help'])
340
388
  expect(result.status).toBe(0)
341
389
  expect(result.stdout).toContain('Initialize config (file or package.json)')
342
390
  expect(result.stdout).toContain('-f, --force')
343
- expect(result.stdout).toContain('Runs interactive numbered prompts:')
344
- expect(result.stdout).toContain('target: 1) package-json, 2) file')
345
- expect(result.stdout).toContain('preset: 1) minimal, 2) full')
391
+ expect(result.stdout).toContain('Runs interactive select prompts for target/preset.')
392
+ expect(result.stdout).toContain('Recommended preset uses checkbox selection')
393
+ expect(result.stdout).toContain('--show-effective')
346
394
  expect(result.stdout).toContain('--yes --force --target file --preset full')
347
395
  expect(result.stderr).toBe('')
348
396
  })
@@ -1 +0,0 @@
1
- console.log(JSON.stringify({ rules: { 'no-console': 1, eqeqeq: [2, 'always'] } }))
@@ -1,4 +0,0 @@
1
- {
2
- "name": "eslint",
3
- "version": "9.0.0"
4
- }
@@ -1 +0,0 @@
1
- console.log(JSON.stringify({ rules: { 'no-console': 2, 'no-debugger': 0 } }))