fg-devkit 1.7.4 → 1.7.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -34,7 +34,7 @@
34
34
  "@tanstack/vue-query-devtools": "6.1.24",
35
35
  "@types/node": "22.16.5",
36
36
  "@vitejs/plugin-vue": "6.0.6",
37
- "fg-devkit": "1.7.4",
37
+ "fg-devkit": "1.7.5",
38
38
  "start-server-and-test": "2.1.3",
39
39
  "tailwindcss": "4.2.4",
40
40
  "tsx": "4.21.0",
@@ -42,10 +42,8 @@
42
42
 
43
43
  import { spawnSync } from 'node:child_process'
44
44
  import fs from 'node:fs'
45
- import readline from 'node:readline/promises'
46
45
  import path from 'node:path'
47
46
  import { fileURLToPath } from 'node:url'
48
- import { stdin as input, stdout as output } from 'node:process'
49
47
  import { createRequire } from 'node:module'
50
48
 
51
49
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
@@ -271,30 +269,6 @@ function runMaintainerAuditAll() {
271
269
  return runNode('repo-vulns.mjs')
272
270
  }
273
271
 
274
- /** Prompt release type in CLI before invoking release.sh to avoid a "stuck" terminal feeling. */
275
- async function promptReleaseType(rl) {
276
- console.log('\nSelect release type:')
277
- console.log(' 1) patch')
278
- console.log(' 2) minor')
279
- console.log(' 3) major')
280
- console.log(' 0) Cancel\n')
281
- const choice = (await rl.question('Release type [1-3] (default: 1): ')).trim() || '1'
282
- if (choice === '0') {
283
- return null
284
- }
285
- if (choice === '1') {
286
- return 'patch'
287
- }
288
- if (choice === '2') {
289
- return 'minor'
290
- }
291
- if (choice === '3') {
292
- return 'major'
293
- }
294
- console.error('[fg-devkit] Invalid release type selection.')
295
- return null
296
- }
297
-
298
272
  /**
299
273
  * Run Commitizen from fg-devkit's bundled dependency set.
300
274
  * Always forces fg-devkit's bundled cz adapter so it stays interactive.
@@ -342,6 +316,18 @@ bootstrap(
342
316
 
343
317
  /** Run ESLint from fg-devkit's bundled dependency set. */
344
318
  function runBundledEslint(args = []) {
319
+ const hasExplicitConfig = args.some((arg) => arg === '--config' || arg === '-c' || arg.startsWith('--config='))
320
+ const hasLocalFlatConfig =
321
+ fs.existsSync(path.join(process.cwd(), 'eslint.config.js')) ||
322
+ fs.existsSync(path.join(process.cwd(), 'eslint.config.mjs')) ||
323
+ fs.existsSync(path.join(process.cwd(), 'eslint.config.cjs'))
324
+
325
+ if (!hasExplicitConfig && !hasLocalFlatConfig) {
326
+ console.error('[fg-devkit] Missing eslint.config.js|mjs|cjs in current directory.')
327
+ console.error('[fg-devkit] Run "fgd sync-project-configs" or "fgd sync-tooling-configs" and retry.')
328
+ return 1
329
+ }
330
+
345
331
  let eslintBin
346
332
  try {
347
333
  const eslintPkgPath = require.resolve('eslint/package.json')
@@ -438,6 +424,34 @@ function runBundledFallow(args = []) {
438
424
  return r.status ?? 1
439
425
  }
440
426
 
427
+ function syncFileFromTemplate(relativeSourcePath, targetPath) {
428
+ const sourcePath = path.join(fgRoot, relativeSourcePath)
429
+ if (!fs.existsSync(sourcePath)) {
430
+ console.error(`[fg-devkit] Missing template: ${sourcePath}`)
431
+ return 1
432
+ }
433
+ try {
434
+ fs.copyFileSync(sourcePath, path.join(process.cwd(), targetPath))
435
+ console.log(`[fg-devkit] Synced ${targetPath}`)
436
+ return 0
437
+ } catch (err) {
438
+ console.error(`[fg-devkit] Failed to sync ${targetPath}: ${err.message}`)
439
+ return 1
440
+ }
441
+ }
442
+
443
+ function runSyncConfigTarget(target) {
444
+ if (target === 'vscode') return runNode('sync-vscode-settings.mjs')
445
+ if (target === 'eslint') return syncFileFromTemplate('configs/eslint.sync-stub.js', 'eslint.config.js')
446
+ if (target === 'prettier') return syncFileFromTemplate('configs/prettier.sync-stub.js', 'prettier.config.js')
447
+ if (target === 'tsconfig') return syncFileFromTemplate('configs/tsconfig.app.sync.json', 'tsconfig.json')
448
+ if (target === 'fallow') return syncFileFromTemplate('configs/fallowrc.sync-stub.json', '.fallowrc.json')
449
+ if (target === 'cursor') return runNode('sync-core-coding-standards.mjs')
450
+ if (target === 'all') return runNode('sync-project-configs.mjs')
451
+ console.error(`[fg-devkit] Unknown config sync target: ${target}`)
452
+ return 1
453
+ }
454
+
441
455
  /** Map subcommand name → script file (extra argv after subcommand is forwarded). */
442
456
  const DIRECT_COMMANDS = {
443
457
  'check-deps': 'check-dependency-policy.mjs',
@@ -503,124 +517,12 @@ Commands:
503
517
  sync-scaffold-vue-deps [policy-file]
504
518
  Align scaffold/vue/package.json versions to the dependency policy
505
519
 
506
- With no command, opens an interactive menu.
520
+ With no command, opens an interactive Inquirer menu.
507
521
 
508
522
  Options:
509
523
  -h, --help Show this help`)
510
524
  }
511
525
 
512
- /** Numbered actions shown in the interactive menu (keys must match user input). */
513
- const MENU = [
514
- {
515
- key: '1',
516
- label: 'Check dependency policy',
517
- run: () => runNode('check-dependency-policy.mjs'),
518
- },
519
- {
520
- key: '2',
521
- label: 'Sync dependency policy',
522
- run: () => runNode('sync-dependency-policy.mjs'),
523
- },
524
- {
525
- key: '3',
526
- label: 'Sync configs (vscode, eslint, prettier, tsconfig, fallow)',
527
- run: () => runNode('sync-project-configs.mjs'),
528
- },
529
- {
530
- key: '4',
531
- label: 'Create release',
532
- run: () => runReleaseScript(),
533
- },
534
- {
535
- key: '5',
536
- label: 'Commit (Commitizen)',
537
- run: () => runCommitizen(),
538
- },
539
- {
540
- key: '6',
541
- label: 'Commit vulnerability (Commitizen)',
542
- run: () => runCommitizen([], { FG_DEVKIT_CZ_DEFAULT_TYPE: 'vuln' }),
543
- },
544
- {
545
- key: '7',
546
- label: 'Remove unnecessary deps in package.json',
547
- run: () => runNode('prune-consumer-deps.mjs'),
548
- },
549
- {
550
- key: '8',
551
- label: 'Full consumer setup (7 -> scripts -> lefthook -> 3 -> 2 -> pnpm install)',
552
- run: () => runFullConsumerSetup(),
553
- },
554
- {
555
- key: '9',
556
- label: 'Setup lefthook (migrate from husky/lint-staged)',
557
- run: () => runSetupLefthook(),
558
- },
559
- {
560
- key: '10',
561
- label: 'PoEditor: Download translations',
562
- run: () => runNode('poeditor-download.mjs'),
563
- },
564
- {
565
- key: '11',
566
- label: 'PoEditor: Upload translations',
567
- run: () => runNode('poeditor-upload.mjs'),
568
- },
569
- {
570
- key: '12',
571
- label: 'Sync .cursor core coding standards',
572
- run: () => runNode('sync-core-coding-standards.mjs'),
573
- },
574
- {
575
- key: '13',
576
- label: 'Fallow analysis',
577
- run: () => 0,
578
- },
579
- ]
580
-
581
- const MAINTAINER_MENU = [
582
- {
583
- key: 'm1',
584
- label: 'Maintainer: Show outdated dependency policy packages',
585
- run: () => runNode('policy-outdated.mjs'),
586
- },
587
- {
588
- key: 'm2',
589
- label: 'Maintainer: Upgrade dependency policy (interactive)',
590
- run: () => runNode('policy-upgrade.mjs'),
591
- },
592
- {
593
- key: 'm3',
594
- label: 'Maintainer: Refresh dependency policy from repos (and scaffold/vue package.json)',
595
- run: () => runNode('refresh-dependency-policy-from-repos.mjs'),
596
- },
597
- {
598
- key: 'm4',
599
- label: 'Maintainer: Upgrade one policy package, stage, and auto-commit',
600
- run: () => runMaintainerPolicyUpgradeAndCommit(),
601
- },
602
- {
603
- key: 'm5',
604
- label: 'Maintainer: Check policy vulnerabilities (audit)',
605
- run: () => runNode('policy-vulns.mjs'),
606
- },
607
- {
608
- key: 'm6',
609
- label: 'Maintainer: Check fg-devkit repo vulnerabilities',
610
- run: () => runMaintainerAuditAll(),
611
- },
612
- {
613
- key: 'm7',
614
- label: 'Maintainer: Check vulnerabilities across all Vue repos',
615
- run: () => runNode('fleet-vulns.mjs'),
616
- },
617
- {
618
- key: 'm8',
619
- label: 'Maintainer: Bump fg-devkit in all Vue repos, run sync-deps, and pnpm install',
620
- run: () => runNode('fleet-bump-fg-devkit.mjs'),
621
- },
622
- ]
623
-
624
526
  function isFgDevkitMaintainerContext() {
625
527
  if (path.resolve(process.cwd()) !== fgRoot) {
626
528
  return false
@@ -638,85 +540,252 @@ function isFgDevkitMaintainerContext() {
638
540
  }
639
541
 
640
542
  async function runInteractiveMenu() {
641
- const rl = readline.createInterface({ input, output })
642
- const isMaintainer = isFgDevkitMaintainerContext()
643
- const menu = isMaintainer ? [...MENU, ...MAINTAINER_MENU] : MENU
644
-
645
- console.log(`\nfg-devkit — choose a task (cwd: ${process.cwd()})\n`)
646
-
647
- for (const item of menu) {
648
- console.log(` ${item.key}) ${item.label}`)
543
+ let selectPrompt
544
+ try {
545
+ const inquirerSelect = await import('@inquirer/select')
546
+ selectPrompt = inquirerSelect.default
547
+ } catch {
548
+ console.error('[fg-devkit] Interactive menu requires @inquirer/select but it is not available.')
549
+ console.error('[fg-devkit] Run pnpm install in your project and try again.')
550
+ return 1
649
551
  }
650
- console.log(' 0) Exit\n')
651
-
652
- const choice = (await rl.question('Number: ')).trim()
653
552
 
654
- if (choice === '0' || choice === '') {
655
- rl.close()
553
+ const runSelectedAction = async (action) => {
554
+ const out = action()
555
+ if (typeof out === 'object' && out !== null && typeof out.then === 'function') {
556
+ return await out
557
+ }
558
+ if (typeof out === 'number') {
559
+ return out
560
+ }
656
561
  return 0
657
562
  }
658
563
 
659
- const item = menu.find((m) => m.key === choice)
660
- if (!item) {
661
- console.error('[fg-devkit] Unknown option. Run again and choose a number from the list.')
662
- rl.close()
663
- return 1
564
+ const maintainerOnly = () => {
565
+ if (!isFgDevkitMaintainerContext()) {
566
+ console.error('[fg-devkit] Maintainer actions are only available in the fg-devkit repository root.')
567
+ return 1
568
+ }
569
+ return 0
664
570
  }
665
571
 
666
- let out
667
- if (item.key === '4') {
668
- const bumpType = await promptReleaseType(rl)
669
- if (!bumpType) {
670
- rl.close()
671
- return 0
572
+ const groups = [
573
+ {
574
+ name: '1) Create commit',
575
+ value: 'commit',
576
+ },
577
+ {
578
+ name: '2) Create release',
579
+ value: 'release',
580
+ },
581
+ {
582
+ name: '3) Lint and typecheck',
583
+ value: 'lint-typecheck',
584
+ },
585
+ {
586
+ name: '4) Poeditor',
587
+ value: 'poeditor',
588
+ },
589
+ {
590
+ name: '5) Dependecy policy',
591
+ value: 'dependency-policy',
592
+ },
593
+ {
594
+ name: '6) Sync configs',
595
+ value: 'sync-configs',
596
+ },
597
+ {
598
+ name: '7) Setup',
599
+ value: 'setup',
600
+ },
601
+ {
602
+ name: '8) Fallow analysis',
603
+ value: 'fallow-analysis',
604
+ },
605
+ {
606
+ name: '9) Maintainer',
607
+ value: 'maintainer',
608
+ },
609
+ {
610
+ name: '10) Exit',
611
+ value: 'exit',
612
+ },
613
+ ]
614
+
615
+ while (true) {
616
+ const group = await selectPrompt({
617
+ message: `fg-devkit — choose a task (cwd: ${process.cwd()})`,
618
+ choices: groups,
619
+ loop: false,
620
+ })
621
+ if (group === 'exit') return 0
622
+
623
+ if (group === 'commit') {
624
+ const choice = await selectPrompt({
625
+ message: 'Create commit',
626
+ choices: [
627
+ { name: 'Commit', value: 'commit' },
628
+ { name: 'Commit vulnerability', value: 'commit-vuln' },
629
+ { name: 'Cancel', value: 'cancel' },
630
+ ],
631
+ default: 'commit',
632
+ loop: false,
633
+ })
634
+ if (choice === 'cancel') continue
635
+ return runSelectedAction(() => (choice === 'commit' ? runCommitizen() : runNode('vuln-commit.mjs')))
636
+ }
637
+
638
+ if (group === 'release') {
639
+ const releaseChoice = await selectPrompt({
640
+ message: 'Create release',
641
+ choices: [
642
+ { name: 'Patch', value: 'patch' },
643
+ { name: 'Minor', value: 'minor' },
644
+ { name: 'Major', value: 'major' },
645
+ { name: 'Cancel', value: 'cancel' },
646
+ ],
647
+ default: 'patch',
648
+ loop: false,
649
+ })
650
+ if (releaseChoice === 'cancel') continue
651
+ return runSelectedAction(() => runReleaseScript([releaseChoice]))
652
+ }
653
+
654
+ if (group === 'lint-typecheck') {
655
+ const choice = await selectPrompt({
656
+ message: 'Lint and typecheck',
657
+ choices: [
658
+ { name: 'Lint and typecheck', value: 'both' },
659
+ { name: 'Lint', value: 'lint' },
660
+ { name: 'Typecheck', value: 'typecheck' },
661
+ { name: 'Cancel', value: 'cancel' },
662
+ ],
663
+ default: 'both',
664
+ loop: false,
665
+ })
666
+ if (choice === 'cancel') continue
667
+ if (choice === 'lint') return runSelectedAction(() => runBundledEslint(['.']))
668
+ if (choice === 'typecheck') {
669
+ return runSelectedAction(() => runBundledTypeScript(['--noEmit', '-p', 'tsconfig.json']))
670
+ }
671
+ const lintCode = runBundledEslint(['.'])
672
+ if (lintCode !== 0) return lintCode
673
+ return runSelectedAction(() => runBundledTypeScript(['--noEmit', '-p', 'tsconfig.json']))
674
+ }
675
+
676
+ if (group === 'poeditor') {
677
+ const choice = await selectPrompt({
678
+ message: 'Poeditor',
679
+ choices: [
680
+ { name: 'Download', value: 'download' },
681
+ { name: 'Upload', value: 'upload' },
682
+ { name: 'Cancel', value: 'cancel' },
683
+ ],
684
+ default: 'download',
685
+ loop: false,
686
+ })
687
+ if (choice === 'cancel') continue
688
+ return runSelectedAction(() => runNode(choice === 'download' ? 'poeditor-download.mjs' : 'poeditor-upload.mjs'))
689
+ }
690
+
691
+ if (group === 'dependency-policy') {
692
+ const choice = await selectPrompt({
693
+ message: 'Dependecy policy',
694
+ choices: [
695
+ { name: 'Check', value: 'check' },
696
+ { name: 'Sync', value: 'sync' },
697
+ { name: 'Cancel', value: 'cancel' },
698
+ ],
699
+ default: 'check',
700
+ loop: false,
701
+ })
702
+ if (choice === 'cancel') continue
703
+ return runSelectedAction(() =>
704
+ runNode(choice === 'check' ? 'check-dependency-policy.mjs' : 'sync-dependency-policy.mjs'),
705
+ )
706
+ }
707
+
708
+ if (group === 'sync-configs') {
709
+ const choice = await selectPrompt({
710
+ message: 'Sync configs',
711
+ choices: [
712
+ { name: 'vscode', value: 'vscode' },
713
+ { name: 'eslint', value: 'eslint' },
714
+ { name: 'prettier', value: 'prettier' },
715
+ { name: 'tsconfig', value: 'tsconfig' },
716
+ { name: 'Fallow', value: 'fallow' },
717
+ { name: 'Cursor', value: 'cursor' },
718
+ { name: 'All', value: 'all' },
719
+ { name: 'Cancel', value: 'cancel' },
720
+ ],
721
+ default: 'all',
722
+ loop: false,
723
+ })
724
+ if (choice === 'cancel') continue
725
+ return runSelectedAction(() => runSyncConfigTarget(choice))
672
726
  }
673
- // Close menu readline before spawning interactive child processes.
674
- rl.close()
675
- out = runReleaseScript([bumpType])
676
- } else if (item.key === '13') {
677
- let subChoice = 'cancel'
678
- let selectPrompt = null
679
- try {
680
- const inquirerSelect = await import('@inquirer/select')
681
- selectPrompt = inquirerSelect.default
682
- } catch {
683
- // Optional dependency; fail with a clear message if not available.
727
+
728
+ if (group === 'setup') {
729
+ const choice = await selectPrompt({
730
+ message: 'Setup',
731
+ choices: [
732
+ { name: 'Lefthook', value: 'lefthook' },
733
+ { name: 'Full consumer setup', value: 'full-consumer-setup' },
734
+ { name: 'Cancel', value: 'cancel' },
735
+ ],
736
+ default: 'lefthook',
737
+ loop: false,
738
+ })
739
+ if (choice === 'cancel') continue
740
+ return runSelectedAction(() => (choice === 'lefthook' ? runSetupLefthook() : runFullConsumerSetup()))
684
741
  }
685
742
 
686
- if (selectPrompt) {
687
- // Close menu readline before running interactive child prompt.
688
- rl.close()
689
- subChoice = await selectPrompt({
690
- message: 'Fallow — choose analysis',
743
+ if (group === 'fallow-analysis') {
744
+ const choice = await selectPrompt({
745
+ message: 'Fallow analysis',
691
746
  choices: [
692
747
  { name: 'dead code', value: 'dead-code' },
693
748
  { name: 'Cancel', value: 'cancel' },
694
749
  ],
695
750
  default: 'dead-code',
751
+ loop: false,
696
752
  })
697
- } else {
698
- rl.close()
699
- console.error('[fg-devkit] Option 13 requires @inquirer/select but it is not available.')
700
- console.error('[fg-devkit] Run pnpm install in your project and try again.')
701
- return 1
753
+ if (choice === 'cancel') continue
754
+ return runSelectedAction(() => runBundledFallow([choice]))
702
755
  }
703
756
 
704
- if (subChoice === 'cancel') {
705
- return 0
757
+ if (group === 'maintainer') {
758
+ const choice = await selectPrompt({
759
+ message: 'Maintainer',
760
+ choices: [
761
+ { name: 'Show outdated dependency policy packages', value: 'm1' },
762
+ { name: 'Upgrade dependency policy (interactive)', value: 'm2' },
763
+ { name: 'Refresh dependency policy from repos (and scaffold/vue package.json)', value: 'm3' },
764
+ { name: 'Upgrade one policy package, stage, and auto-commit', value: 'm4' },
765
+ { name: 'Check policy vulnerabilities (audit)', value: 'm5' },
766
+ { name: 'Check fg-devkit repo vulnerabilities', value: 'm6' },
767
+ { name: 'Check vulnerabilities across all Vue repos', value: 'm7' },
768
+ { name: 'Bump fg-devkit in all Vue repos, run sync-deps, and pnpm install', value: 'm8' },
769
+ { name: 'Cancel', value: 'cancel' },
770
+ ],
771
+ default: 'm1',
772
+ loop: false,
773
+ })
774
+ if (choice === 'cancel') continue
775
+ const maintainerCode = maintainerOnly()
776
+ if (maintainerCode !== 0) return maintainerCode
777
+ if (choice === 'm1') return runSelectedAction(() => runNode('policy-outdated.mjs'))
778
+ if (choice === 'm2') return runSelectedAction(() => runNode('policy-upgrade.mjs'))
779
+ if (choice === 'm3') return runSelectedAction(() => runNode('refresh-dependency-policy-from-repos.mjs'))
780
+ if (choice === 'm4') return runSelectedAction(() => runMaintainerPolicyUpgradeAndCommit())
781
+ if (choice === 'm5') return runSelectedAction(() => runNode('policy-vulns.mjs'))
782
+ if (choice === 'm6') return runSelectedAction(() => runMaintainerAuditAll())
783
+ if (choice === 'm7') return runSelectedAction(() => runNode('fleet-vulns.mjs'))
784
+ if (choice === 'm8') return runSelectedAction(() => runNode('fleet-bump-fg-devkit.mjs'))
785
+ return 1
706
786
  }
707
- out = runBundledFallow([subChoice])
708
- } else {
709
- // Close menu readline before spawning interactive child processes.
710
- rl.close()
711
- out = item.run()
712
787
  }
713
788
 
714
- if (typeof out === 'object' && out !== null && typeof out.then === 'function') {
715
- return await out
716
- }
717
- if (typeof out === 'number') {
718
- return out
719
- }
720
789
  return 0
721
790
  }
722
791