Repository: oclif/core
Pull request #802 (2023-09-29 21:49:27)

chore: remove util/index.ts


Patch:
diff --git a/src/args.ts b/src/args.ts
index b262f6f..09a9fca 100644
--- a/src/args.ts
+++ b/src/args.ts
@@ -1,7 +1,8 @@
 import {Arg, ArgDefinition} from './interfaces/parser'
-import {dirExists, fileExists, isNotFalsy} from './util/index'
+import {dirExists, fileExists} from './util/fs'
 import {Command} from './command'
 import {URL} from 'node:url'
+import {isNotFalsy} from './util/util'
 
 /**
  * Create a custom arg.
diff --git a/src/cli-ux/action/base.ts b/src/cli-ux/action/base.ts
index 38aa2cd..e06b9f8 100644
--- a/src/cli-ux/action/base.ts
+++ b/src/cli-ux/action/base.ts
@@ -1,6 +1,6 @@
 import {stderr, stdout} from '../stream'
 import {Options} from './types'
-import {castArray} from '../../util'
+import {castArray} from '../../util/util'
 import {inspect} from 'node:util'
 
 export interface ITask {
diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts
index db8e3ab..c6d8780 100644
--- a/src/cli-ux/config.ts
+++ b/src/cli-ux/config.ts
@@ -1,6 +1,6 @@
 import {ActionBase} from './action/base'
 import {PJSON} from '../interfaces/pjson'
-import {requireJson} from '../util/index'
+import {requireJson} from '../util/fs'
 import simple from './action/simple'
 import spinner from './action/spinner'
 
diff --git a/src/cli-ux/list.ts b/src/cli-ux/list.ts
index 3f399a7..e1f9859 100644
--- a/src/cli-ux/list.ts
+++ b/src/cli-ux/list.ts
@@ -1,4 +1,4 @@
-import {maxBy} from '../util/index'
+import {maxBy} from '../util/util'
 import {stdtermwidth} from '../screen'
 const wordwrap = require('wordwrap')
 
diff --git a/src/cli-ux/styled/table.ts b/src/cli-ux/styled/table.ts
index e22ac2f..94c58b4 100644
--- a/src/cli-ux/styled/table.ts
+++ b/src/cli-ux/styled/table.ts
@@ -1,6 +1,6 @@
 import * as F from '../../flags'
 import * as Interfaces from '../../interfaces'
-import {capitalize, sumBy} from '../../util'
+import {capitalize, sumBy} from '../../util/util'
 import chalk from 'chalk'
 import {inspect} from 'node:util'
 import {orderBy} from 'natural-orderby'
diff --git a/src/command.ts b/src/command.ts
index 262cd3d..2f0a617 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -16,7 +16,6 @@ import {
 } from './interfaces/parser'
 import {format, inspect} from 'node:util'
 import {formatCommandDeprecationWarning, formatFlagDeprecationWarning, normalizeArgv, toConfiguredId} from './help/util'
-import {requireJson, uniq} from './util/index'
 import {stderr, stdout} from './cli-ux/stream'
 import {CommandError} from './interfaces/errors'
 import {Config} from './config'
@@ -27,6 +26,8 @@ import {PrettyPrintableError} from './errors'
 import {aggregateFlags} from './util/aggregate-flags'
 import chalk from 'chalk'
 import {fileURLToPath} from 'node:url'
+import {requireJson} from './util/fs'
+import {uniq} from './util/util'
 import {ux} from './cli-ux'
 
 const pjson = requireJson<PJSON>(__dirname, '..', 'package.json')
diff --git a/src/config/config.ts b/src/config/config.ts
index 2e9bf08..98d7501 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -6,7 +6,8 @@ import {Hook, Hooks, PJSON, Topic} from '../interfaces'
 import {Plugin as IPlugin, Options} from '../interfaces/plugin'
 import {URL, fileURLToPath} from 'node:url'
 import {arch, userInfo as osUserInfo, release, tmpdir, type} from 'node:os'
-import {compact, getHomeDir, getPlatform, isProd, requireJson} from '../util/index'
+import {compact, isProd} from '../util/util'
+import {getHomeDir, getPlatform} from '../util/os'
 import {join, sep} from 'node:path'
 import {Command} from '../command'
 import {Performance} from '../performance'
@@ -15,6 +16,7 @@ import WSL from 'is-wsl'
 import {format} from 'node:util'
 import {getHelpFlagAdditions} from '../help/util'
 import {loadWithData} from '../module-loader'
+import {requireJson} from '../util/fs'
 import {settings} from '../settings'
 import {stdout} from '../cli-ux/stream'
 
diff --git a/src/config/plugin-loader.ts b/src/config/plugin-loader.ts
index 63df2ba..ce01e68 100644
--- a/src/config/plugin-loader.ts
+++ b/src/config/plugin-loader.ts
@@ -1,10 +1,11 @@
 import * as Plugin from './plugin'
 import {Plugin as IPlugin, Options} from '../interfaces/plugin'
-import {isProd, readJson} from '../util/index'
 import {Debug} from './util'
 import {PJSON} from '../interfaces'
 import {Performance} from '../performance'
+import {isProd} from '../util/util'
 import {join} from 'node:path'
+import {readJson} from '../util/fs'
 
 // eslint-disable-next-line new-cap
 const debug = Debug()
diff --git a/src/config/plugin.ts b/src/config/plugin.ts
index 972e66c..af5a9fe 100644
--- a/src/config/plugin.ts
+++ b/src/config/plugin.ts
@@ -1,8 +1,9 @@
 import {CLIError, error} from '../errors'
 import {Debug, getCommandIdPermutations, resolvePackage} from './util'
 import {Plugin as IPlugin, PluginOptions} from '../interfaces/plugin'
-import {compact, exists, isProd, mapValues, readJson, requireJson} from '../util/index'
+import {compact, isProd, mapValues} from '../util/util'
 import {dirname, join, parse, relative, sep} from 'node:path'
+import {exists, readJson, requireJson} from '../util/fs'
 import {loadWithData, loadWithDataFromManifest} from '../module-loader'
 import {Command} from '../command'
 import {Manifest} from '../interfaces/manifest'
diff --git a/src/config/ts-node.ts b/src/config/ts-node.ts
index ba0bbfe..8778457 100644
--- a/src/config/ts-node.ts
+++ b/src/config/ts-node.ts
@@ -1,10 +1,11 @@
 import * as TSNode from 'ts-node'
 import {Plugin, TSConfig} from '../interfaces'
-import {isProd, readJsonSync} from '../util/index'
 import {join, relative as pathRelative} from 'node:path'
 import {Debug} from './util'
 import {existsSync} from 'node:fs'
+import {isProd} from '../util/util'
 import {memoizedWarn} from '../errors'
+import {readJsonSync} from '../util/fs'
 import {settings} from '../settings'
 
 // eslint-disable-next-line new-cap
diff --git a/src/flags.ts b/src/flags.ts
index 42c23f0..b338db9 100644
--- a/src/flags.ts
+++ b/src/flags.ts
@@ -1,6 +1,6 @@
 /* eslint-disable valid-jsdoc */
 import {BooleanFlag, CustomOptions, FlagDefinition, OptionFlag} from './interfaces'
-import {dirExists, fileExists} from './util/index'
+import {dirExists, fileExists} from './util/fs'
 import {CLIError} from './errors'
 import {URL} from 'node:url'
 import {loadHelpClass} from './help'
diff --git a/src/help/command.ts b/src/help/command.ts
index 6eda603..87d8660 100644
--- a/src/help/command.ts
+++ b/src/help/command.ts
@@ -1,9 +1,10 @@
 import * as Interfaces from '../interfaces'
 import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter'
-import {castArray, compact, ensureArgObject, sortBy} from '../util'
+import {castArray, compact, sortBy} from '../util/util'
 import {Command} from '../command'
 import {DocOpts} from './docopts'
 import chalk from 'chalk'
+import {ensureArgObject} from '../util/ensure-arg-object'
 import stripAnsi from 'strip-ansi'
 
 // Don't use os.EOL because we need to ensure that a string
diff --git a/src/help/docopts.ts b/src/help/docopts.ts
index ff1da03..e1bc74d 100644
--- a/src/help/docopts.ts
+++ b/src/help/docopts.ts
@@ -1,5 +1,5 @@
 import {Command} from '../command'
-import {ensureArgObject} from '../util/index'
+import {ensureArgObject} from '../util/ensure-arg-object'
 /**
  * DocOpts - See http://docopt.org/.
  *
diff --git a/src/help/index.ts b/src/help/index.ts
index 08ab901..1c07b9b 100644
--- a/src/help/index.ts
+++ b/src/help/index.ts
@@ -1,5 +1,5 @@
 import * as Interfaces from '../interfaces'
-import {compact, sortBy, uniqBy} from '../util/index'
+import {compact, sortBy, uniqBy} from '../util/util'
 import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFromArgv, toConfiguredId} from './util'
 import {Command} from '../command'
 import {CommandHelp} from './command'
diff --git a/src/help/root.ts b/src/help/root.ts
index 6ec56a9..d8cd375 100644
--- a/src/help/root.ts
+++ b/src/help/root.ts
@@ -1,6 +1,6 @@
 import * as Interfaces from '../interfaces'
 import {HelpFormatter} from './formatter'
-import {compact} from '../util/index'
+import {compact} from '../util/util'
 import stripAnsi from 'strip-ansi'
 
 export default class RootHelp extends HelpFormatter {
diff --git a/src/parser/errors.ts b/src/parser/errors.ts
index f1a583f..b52f20d 100644
--- a/src/parser/errors.ts
+++ b/src/parser/errors.ts
@@ -4,7 +4,7 @@ import {CLIError} from '../errors'
 import chalk from 'chalk'
 import {flagUsages} from './help'
 import {renderList} from '../cli-ux/list'
-import {uniq} from '../util/index'
+import {uniq} from '../util/util'
 
 export {CLIError} from '../errors'
 
diff --git a/src/parser/help.ts b/src/parser/help.ts
index 57e1cec..7ade44b 100644
--- a/src/parser/help.ts
+++ b/src/parser/help.ts
@@ -1,6 +1,6 @@
 import {Flag, FlagUsageOptions} from '../interfaces/parser'
 import chalk from 'chalk'
-import {sortBy} from '../util/index'
+import {sortBy} from '../util/util'
 
 export function flagUsage(flag: Flag<any>, options: FlagUsageOptions = {}): [string, string | undefined] {
   const label = []
diff --git a/src/parser/parse.ts b/src/parser/parse.ts
index 7fb88ed..e7e45fc 100644
--- a/src/parser/parse.ts
+++ b/src/parser/parse.ts
@@ -17,7 +17,7 @@ import {
   ParserOutput,
   ParsingToken,
 } from '../interfaces/parser'
-import {isTruthy, last, pickBy} from '../util/index'
+import {isTruthy, last, pickBy} from '../util/util'
 import {createInterface} from 'node:readline'
 
 let debug: any
diff --git a/src/parser/validate.ts b/src/parser/validate.ts
index ef13abc..634e8c1 100644
--- a/src/parser/validate.ts
+++ b/src/parser/validate.ts
@@ -7,7 +7,7 @@ import {
   UnexpectedArgsError,
   Validation,
 } from './errors'
-import {uniq} from '../util/index'
+import {uniq} from '../util/util'
 
 export async function validate(parse: {input: ParserInput; output: ParserOutput}): Promise<void> {
   let cachedResolvedFlags: Record<string, unknown> | undefined
diff --git a/src/util/cache-command.ts b/src/util/cache-command.ts
index 6d3b057..ae37894 100644
--- a/src/util/cache-command.ts
+++ b/src/util/cache-command.ts
@@ -1,9 +1,10 @@
 import {ArgInput, FlagInput} from '../interfaces/parser'
-import {ensureArgObject, pickBy} from './index'
 import {Command} from '../command'
 import {Plugin as IPlugin} from '../interfaces/plugin'
 import {aggregateFlags} from './aggregate-flags'
 import {cacheDefaultValue} from './cache-default-value'
+import {ensureArgObject} from './ensure-arg-object'
+import {pickBy} from './util'
 
 // In order to collect static properties up the inheritance chain, we need to recursively
 // access the prototypes until there's nothing left. This allows us to combine baseFlags
diff --git a/src/util/ensure-arg-object.ts b/src/util/ensure-arg-object.ts
new file mode 100644
index 0000000..4664849
--- /dev/null
+++ b/src/util/ensure-arg-object.ts
@@ -0,0 +1,15 @@
+import {ArgInput} from '../interfaces/parser'
+import {Command} from '../command'
+
+/**
+ * Ensure that the provided args are an object. This is for backwards compatibility with v1 commands which
+ * defined args as an array.
+ *
+ * @param args Either an array of args or an object of args
+ * @returns ArgInput
+ */
+export function ensureArgObject(args?: any[] | ArgInput | {[name: string]: Command.Arg.Cached}): ArgInput {
+  return (
+    Array.isArray(args) ? (args ?? []).reduce((x, y) => ({...x, [y.name]: y}), {} as ArgInput) : args ?? {}
+  ) as ArgInput
+}
diff --git a/src/util/fs.ts b/src/util/fs.ts
new file mode 100644
index 0000000..89f5f1c
--- /dev/null
+++ b/src/util/fs.ts
@@ -0,0 +1,57 @@
+import {access, readFile, stat} from 'node:fs/promises'
+import {join} from 'node:path'
+import {readFileSync} from 'node:fs'
+
+const debug = require('debug')
+
+export function requireJson<T>(...pathParts: string[]): T {
+  return JSON.parse(readFileSync(join(...pathParts), 'utf8'))
+}
+
+export async function exists(path: string): Promise<boolean> {
+  try {
+    await access(path)
+    return true
+  } catch {
+    return false
+  }
+}
+
+export const dirExists = async (input: string): Promise<string> => {
+  if (!(await exists(input))) {
+    throw new Error(`No directory found at ${input}`)
+  }
+
+  const fileStat = await stat(input)
+  if (!fileStat.isDirectory()) {
+    throw new Error(`${input} exists but is not a directory`)
+  }
+
+  return input
+}
+
+export const fileExists = async (input: string): Promise<string> => {
+  if (!(await exists(input))) {
+    throw new Error(`No file found at ${input}`)
+  }
+
+  const fileStat = await stat(input)
+  if (!fileStat.isFile()) {
+    throw new Error(`${input} exists but is not a file`)
+  }
+
+  return input
+}
+
+export async function readJson<T = unknown>(path: string): Promise<T> {
+  debug('config')('readJson %s', path)
+  const contents = await readFile(path, 'utf8')
+  return JSON.parse(contents) as T
+}
+
+export function readJsonSync(path: string, parse: false): string
+export function readJsonSync<T = unknown>(path: string, parse?: true): T
+export function readJsonSync<T = unknown>(path: string, parse = true): T | string {
+  const contents = readFileSync(path, 'utf8')
+  return parse ? (JSON.parse(contents) as T) : contents
+}
diff --git a/src/util/index.ts b/src/util/index.ts
deleted file mode 100644
index b7284ff..0000000
--- a/src/util/index.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import {access, readFile, stat} from 'node:fs/promises'
-import {homedir, platform} from 'node:os'
-import {ArgInput} from '../interfaces/parser'
-import {Command} from '../command'
-import {join} from 'node:path'
-import {readFileSync} from 'node:fs'
-
-const debug = require('debug')
-
-export function pickBy<T extends {[s: string]: T[keyof T]} | ArrayLike<T[keyof T]>>(
-  obj: T,
-  fn: (i: T[keyof T]) => boolean,
-): Partial<T> {
-  return Object.entries(obj).reduce((o, [k, v]) => {
-    if (fn(v)) o[k] = v
-    return o
-  }, {} as any)
-}
-
-export function compact<T>(a: (T | undefined)[]): T[] {
-  // eslint-disable-next-line unicorn/prefer-native-coercion-functions
-  return a.filter((a): a is T => Boolean(a))
-}
-
-export function uniqBy<T>(arr: T[], fn: (cur: T) => any): T[] {
-  return arr.filter((a, i) => {
-    const aVal = fn(a)
-    return !arr.find((b, j) => j > i && fn(b) === aVal)
-  })
-}
-
-export function last<T>(arr?: T[]): T | undefined {
-  if (!arr) return
-  return arr.at(-1)
-}
-
-type SortTypes = string | number | undefined | boolean
-
-function compare(a: SortTypes | SortTypes[], b: SortTypes | SortTypes[]): number {
-  a = a === undefined ? 0 : a
-  b = b === undefined ? 0 : b
-
-  if (Array.isArray(a) && Array.isArray(b)) {
-    if (a.length === 0 && b.length === 0) return 0
-    const diff = compare(a[0], b[0])
-    if (diff !== 0) return diff
-    return compare(a.slice(1), b.slice(1))
-  }
-
-  if (a < b) return -1
-  if (a > b) return 1
-  return 0
-}
-
-export function sortBy<T>(arr: T[], fn: (i: T) => SortTypes | SortTypes[]): T[] {
-  return arr.sort((a, b) => compare(fn(a), fn(b)))
-}
-
-export function castArray<T>(input?: T | T[]): T[] {
-  if (input === undefined) return []
-  return Array.isArray(input) ? input : [input]
-}
-
-export function isProd(): boolean {
-  return !['development', 'test'].includes(process.env.NODE_ENV ?? '')
-}
-
-export function maxBy<T>(arr: T[], fn: (i: T) => number): T | undefined {
-  if (arr.length === 0) {
-    return undefined
-  }
-
-  return arr.reduce((maxItem, i) => {
-    const curr = fn(i)
-    const max = fn(maxItem)
-    return curr > max ? i : maxItem
-  })
-}
-
-export function sumBy<T>(arr: T[], fn: (i: T) => number): number {
-  return arr.reduce((sum, i) => sum + fn(i), 0)
-}
-
-export function capitalize(s: string): string {
-  return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : ''
-}
-
-export async function exists(path: string): Promise<boolean> {
-  try {
-    await access(path)
-    return true
-  } catch {
-    return false
-  }
-}
-
-export const dirExists = async (input: string): Promise<string> => {
-  if (!(await exists(input))) {
-    throw new Error(`No directory found at ${input}`)
-  }
-
-  const fileStat = await stat(input)
-  if (!fileStat.isDirectory()) {
-    throw new Error(`${input} exists but is not a directory`)
-  }
-
-  return input
-}
-
-export const fileExists = async (input: string): Promise<string> => {
-  if (!(await exists(input))) {
-    throw new Error(`No file found at ${input}`)
-  }
-
-  const fileStat = await stat(input)
-  if (!fileStat.isFile()) {
-    throw new Error(`${input} exists but is not a file`)
-  }
-
-  return input
-}
-
-export function isTruthy(input: string): boolean {
-  return ['true', '1', 'yes', 'y'].includes(input.toLowerCase())
-}
-
-export function isNotFalsy(input: string): boolean {
-  return !['false', '0', 'no', 'n'].includes(input.toLowerCase())
-}
-
-export function requireJson<T>(...pathParts: string[]): T {
-  return JSON.parse(readFileSync(join(...pathParts), 'utf8'))
-}
-
-/**
- * Ensure that the provided args are an object. This is for backwards compatibility with v1 commands which
- * defined args as an array.
- *
- * @param args Either an array of args or an object of args
- * @returns ArgInput
- */
-export function ensureArgObject(args?: any[] | ArgInput | {[name: string]: Command.Arg.Cached}): ArgInput {
-  return (
-    Array.isArray(args) ? (args ?? []).reduce((x, y) => ({...x, [y.name]: y}), {} as ArgInput) : args ?? {}
-  ) as ArgInput
-}
-
-export function uniq<T>(arr: T[]): T[] {
-  return [...new Set(arr)].sort()
-}
-
-/**
- * Call os.homedir() and return the result
- *
- * Wrapping this allows us to stub these in tests since os.homedir() is
- * non-configurable and non-writable.
- *
- * @returns The user's home directory
- */
-export function getHomeDir(): string {
-  return homedir()
-}
-
-/**
- * Call os.platform() and return the result
- *
- * Wrapping this allows us to stub these in tests since os.platform() is
- * non-configurable and non-writable.
- *
- * @returns The process' platform
- */
-export function getPlatform(): NodeJS.Platform {
-  return platform()
-}
-
-export async function readJson<T = unknown>(path: string): Promise<T> {
-  debug('config')('readJson %s', path)
-  const contents = await readFile(path, 'utf8')
-  return JSON.parse(contents) as T
-}
-
-export function readJsonSync(path: string, parse: false): string
-export function readJsonSync<T = unknown>(path: string, parse?: true): T
-export function readJsonSync<T = unknown>(path: string, parse = true): T | string {
-  const contents = readFileSync(path, 'utf8')
-  return parse ? (JSON.parse(contents) as T) : contents
-}
-
-export function mapValues<T extends Record<string, any>, TResult>(
-  obj: {[P in keyof T]: T[P]},
-  fn: (i: T[keyof T], k: keyof T) => TResult,
-): {[P in keyof T]: TResult} {
-  return Object.entries(obj).reduce((o, [k, v]) => {
-    o[k] = fn(v as any, k as any)
-    return o
-  }, {} as any)
-}
diff --git a/src/util/os.ts b/src/util/os.ts
new file mode 100644
index 0000000..2fb3cdf
--- /dev/null
+++ b/src/util/os.ts
@@ -0,0 +1,25 @@
+import {homedir, platform} from 'node:os'
+
+/**
+ * Call os.homedir() and return the result
+ *
+ * Wrapping this allows us to stub these in tests since os.homedir() is
+ * non-configurable and non-writable.
+ *
+ * @returns The user's home directory
+ */
+export function getHomeDir(): string {
+  return homedir()
+}
+
+/**
+ * Call os.platform() and return the result
+ *
+ * Wrapping this allows us to stub these in tests since os.platform() is
+ * non-configurable and non-writable.
+ *
+ * @returns The process' platform
+ */
+export function getPlatform(): NodeJS.Platform {
+  return platform()
+}
diff --git a/src/util/util.ts b/src/util/util.ts
new file mode 100644
index 0000000..7f3ec10
--- /dev/null
+++ b/src/util/util.ts
@@ -0,0 +1,99 @@
+export function pickBy<T extends {[s: string]: T[keyof T]} | ArrayLike<T[keyof T]>>(
+  obj: T,
+  fn: (i: T[keyof T]) => boolean,
+): Partial<T> {
+  return Object.entries(obj).reduce((o, [k, v]) => {
+    if (fn(v)) o[k] = v
+    return o
+  }, {} as any)
+}
+
+export function compact<T>(a: (T | undefined)[]): T[] {
+  // eslint-disable-next-line unicorn/prefer-native-coercion-functions
+  return a.filter((a): a is T => Boolean(a))
+}
+
+export function uniqBy<T>(arr: T[], fn: (cur: T) => any): T[] {
+  return arr.filter((a, i) => {
+    const aVal = fn(a)
+    return !arr.find((b, j) => j > i && fn(b) === aVal)
+  })
+}
+
+export function last<T>(arr?: T[]): T | undefined {
+  if (!arr) return
+  return arr.at(-1)
+}
+
+type SortTypes = string | number | undefined | boolean
+
+function compare(a: SortTypes | SortTypes[], b: SortTypes | SortTypes[]): number {
+  a = a === undefined ? 0 : a
+  b = b === undefined ? 0 : b
+
+  if (Array.isArray(a) && Array.isArray(b)) {
+    if (a.length === 0 && b.length === 0) return 0
+    const diff = compare(a[0], b[0])
+    if (diff !== 0) return diff
+    return compare(a.slice(1), b.slice(1))
+  }
+
+  if (a < b) return -1
+  if (a > b) return 1
+  return 0
+}
+
+export function sortBy<T>(arr: T[], fn: (i: T) => SortTypes | SortTypes[]): T[] {
+  return arr.sort((a, b) => compare(fn(a), fn(b)))
+}
+
+export function castArray<T>(input?: T | T[]): T[] {
+  if (input === undefined) return []
+  return Array.isArray(input) ? input : [input]
+}
+
+export function isProd(): boolean {
+  return !['development', 'test'].includes(process.env.NODE_ENV ?? '')
+}
+
+export function maxBy<T>(arr: T[], fn: (i: T) => number): T | undefined {
+  if (arr.length === 0) {
+    return undefined
+  }
+
+  return arr.reduce((maxItem, i) => {
+    const curr = fn(i)
+    const max = fn(maxItem)
+    return curr > max ? i : maxItem
+  })
+}
+
+export function sumBy<T>(arr: T[], fn: (i: T) => number): number {
+  return arr.reduce((sum, i) => sum + fn(i), 0)
+}
+
+export function capitalize(s: string): string {
+  return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : ''
+}
+
+export function isTruthy(input: string): boolean {
+  return ['true', '1', 'yes', 'y'].includes(input.toLowerCase())
+}
+
+export function isNotFalsy(input: string): boolean {
+  return !['false', '0', 'no', 'n'].includes(input.toLowerCase())
+}
+
+export function uniq<T>(arr: T[]): T[] {
+  return [...new Set(arr)].sort()
+}
+
+export function mapValues<T extends Record<string, any>, TResult>(
+  obj: {[P in keyof T]: T[P]},
+  fn: (i: T[keyof T], k: keyof T) => TResult,
+): {[P in keyof T]: TResult} {
+  return Object.entries(obj).reduce((o, [k, v]) => {
+    o[k] = fn(v as any, k as any)
+    return o
+  }, {} as any)
+}


Test patch:
diff --git a/test/command/command.test.ts b/test/command/command.test.ts
index 8922842..9cb949e 100644
--- a/test/command/command.test.ts
+++ b/test/command/command.test.ts
@@ -1,6 +1,7 @@
 import {expect, fancy} from 'fancy-test'
 // import path = require('path')
 import {Command as Base, Flags} from '../../src'
+import {ensureArgObject} from '../../src/util/ensure-arg-object'
 // import {TestHelpClassConfig} from './helpers/test-help-in-src/src/test-help-plugin'
 
 // const pjson = require('../package.json')
@@ -449,3 +450,22 @@ describe('command', () => {
       .it('json disabled/pass through enable/--json flag before --/jsonEnabled() should be false')
   })
 })
+
+describe('ensureArgObject', () => {
+  it('should convert array of arguments to an object', () => {
+    const args = [
+      {name: 'foo', description: 'foo desc', required: true},
+      {name: 'bar', description: 'bar desc'},
+    ]
+    const expected = {foo: args[0], bar: args[1]}
+    expect(ensureArgObject(args)).to.deep.equal(expected)
+  })
+
+  it('should do nothing to an arguments object', () => {
+    const args = {
+      foo: {name: 'foo', description: 'foo desc', required: true},
+      bar: {name: 'bar', description: 'bar desc'},
+    }
+    expect(ensureArgObject(args)).to.deep.equal(args)
+  })
+})
diff --git a/test/command/main.test.ts b/test/command/main.test.ts
index 0aa5a1f..c93ca83 100644
--- a/test/command/main.test.ts
+++ b/test/command/main.test.ts
@@ -2,7 +2,7 @@ import {expect} from 'chai'
 import {resolve} from 'node:path'
 import {SinonSandbox, SinonStub, createSandbox} from 'sinon'
 import stripAnsi = require('strip-ansi')
-import {requireJson} from '../../src/util'
+import {requireJson} from '../../src/util/fs'
 import {run} from '../../src/main'
 import {Interfaces, stdout} from '../../src/index'
 
diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts
index be69446..1cbc711 100644
--- a/test/config/config.flexible.test.ts
+++ b/test/config/config.flexible.test.ts
@@ -5,7 +5,7 @@ import {expect, fancy} from './test'
 import {Flags, Interfaces} from '../../src'
 import {Command} from '../../src/command'
 import {getCommandIdPermutations} from '../../src/config/util'
-import * as util from '../../src/util'
+import * as os from '../../src/util/os'
 import {join} from 'node:path'
 
 interface Options {
@@ -44,8 +44,8 @@ describe('Config with flexible taxonomy', () => {
     let test = fancy
       .resetConfig()
       .env(env, {clear: true})
-      .stub(util, 'getHomeDir', (stub) => stub.returns(join(homedir)))
-      .stub(util, 'getPlatform', (stub) => stub.returns(platform))
+      .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir)))
+      .stub(os, 'getPlatform', (stub) => stub.returns(platform))
 
     const load = async (): Promise<void> => {}
     const findCommand = async (): Promise<Command.Class> => MyCommandClass
diff --git a/test/config/config.test.ts b/test/config/config.test.ts
index d92c9f2..24f5d85 100644
--- a/test/config/config.test.ts
+++ b/test/config/config.test.ts
@@ -1,7 +1,8 @@
 import {join} from 'node:path'
 
 import {Plugin as IPlugin} from '../../src/interfaces'
-import * as util from '../../src/util'
+import * as os from '../../src/util/os'
+import * as fs from '../../src/util/fs'
 
 import {expect, fancy} from './test'
 import {Config, Interfaces} from '../../src'
@@ -49,9 +50,9 @@ describe('Config', () => {
     let test = fancy
       .resetConfig()
       .env(env, {clear: true})
-      .stub(util, 'getHomeDir', (stub) => stub.returns(join(homedir)))
-      .stub(util, 'getPlatform', (stub) => stub.returns(platform))
-    if (pjson) test = test.stub(util, 'readJson', (stub) => stub.resolves(pjson))
+      .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir)))
+      .stub(os, 'getPlatform', (stub) => stub.returns(platform))
+    if (pjson) test = test.stub(fs, 'readJson', (stub) => stub.resolves(pjson))
 
     test = test.add('config', () => Config.load())
 
@@ -317,10 +318,10 @@ describe('Config', () => {
       let test = fancy
         .resetConfig()
         .env(env, {clear: true})
-        .stub(util, 'getHomeDir', (stub) => stub.returns(join(homedir)))
-        .stub(util, 'getPlatform', (stub) => stub.returns(platform))
+        .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir)))
+        .stub(os, 'getPlatform', (stub) => stub.returns(platform))
 
-      if (pjson) test = test.stub(util, 'readJson', (stub) => stub.resolves(pjson))
+      if (pjson) test = test.stub(fs, 'readJson', (stub) => stub.resolves(pjson))
       test = test.add('config', async () => {
         const config = await Config.load()
         config.plugins = plugins
diff --git a/test/config/ts-node.test.ts b/test/config/ts-node.test.ts
index 6742238..111794b 100644
--- a/test/config/ts-node.test.ts
+++ b/test/config/ts-node.test.ts
@@ -5,7 +5,7 @@ import {SinonSandbox, createSandbox} from 'sinon'
 
 import {Interfaces, settings} from '../../src'
 import * as configTsNode from '../../src/config/ts-node'
-import * as util from '../../src/util'
+import * as util from '../../src/util/fs'
 import {expect} from 'chai'
 
 const root = resolve(__dirname, 'fixtures/typescript')
diff --git a/test/util/fs.test.ts b/test/util/fs.test.ts
new file mode 100644
index 0000000..edc93e2
--- /dev/null
+++ b/test/util/fs.test.ts
@@ -0,0 +1,19 @@
+import {expect} from 'chai'
+import {readJson} from '../../src/util/fs'
+
+describe('readJson', () => {
+  it('should return parsed JSON', async () => {
+    const json = await readJson<{name: string}>('package.json')
+    expect(json.name).to.equal('@oclif/core')
+  })
+
+  it('should throw an error if the file does not exist', async () => {
+    try {
+      await readJson('does-not-exist.json')
+      throw new Error('Expected an error to be thrown')
+    } catch (error) {
+      const err = error as Error
+      expect(err.message).to.include('ENOENT: no such file or directory')
+    }
+  })
+})
diff --git a/test/util/os.test.ts b/test/util/os.test.ts
new file mode 100644
index 0000000..f8cb686
--- /dev/null
+++ b/test/util/os.test.ts
@@ -0,0 +1,9 @@
+import {expect} from 'chai'
+import {homedir} from 'node:os'
+import {getHomeDir} from '../../src/util/os'
+
+describe('getHomeDir', () => {
+  it('should return the home directory', () => {
+    expect(getHomeDir()).to.equal(homedir())
+  })
+})
diff --git a/test/util/index.test.ts b/test/util/util.test.ts
similarity index 64%
rename from test/util/index.test.ts
rename to test/util/util.test.ts
index 0b93c81..d8ce40f 100644
--- a/test/util/index.test.ts
+++ b/test/util/util.test.ts
@@ -1,17 +1,5 @@
 import {expect} from 'chai'
-import {homedir} from 'node:os'
-import {
-  capitalize,
-  castArray,
-  ensureArgObject,
-  getHomeDir,
-  isNotFalsy,
-  isTruthy,
-  last,
-  maxBy,
-  readJson,
-  sumBy,
-} from '../../src/util/index'
+import {capitalize, castArray, isNotFalsy, isTruthy, last, maxBy, sumBy} from '../../src/util/util'
 
 describe('capitalize', () => {
   it('capitalizes the string', () => {
@@ -62,25 +50,6 @@ describe('last', () => {
   })
 })
 
-describe('ensureArgObject', () => {
-  it('should convert array of arguments to an object', () => {
-    const args = [
-      {name: 'foo', description: 'foo desc', required: true},
-      {name: 'bar', description: 'bar desc'},
-    ]
-    const expected = {foo: args[0], bar: args[1]}
-    expect(ensureArgObject(args)).to.deep.equal(expected)
-  })
-
-  it('should do nothing to an arguments object', () => {
-    const args = {
-      foo: {name: 'foo', description: 'foo desc', required: true},
-      bar: {name: 'bar', description: 'bar desc'},
-    }
-    expect(ensureArgObject(args)).to.deep.equal(args)
-  })
-})
-
 describe('isNotFalsy', () => {
   it('should return true for truthy values', () => {
     expect(isNotFalsy('true')).to.be.true
@@ -113,29 +82,6 @@ describe('isTruthy', () => {
   })
 })
 
-describe('getHomeDir', () => {
-  it('should return the home directory', () => {
-    expect(getHomeDir()).to.equal(homedir())
-  })
-})
-
-describe('readJson', () => {
-  it('should return parsed JSON', async () => {
-    const json = await readJson<{name: string}>('package.json')
-    expect(json.name).to.equal('@oclif/core')
-  })
-
-  it('should throw an error if the file does not exist', async () => {
-    try {
-      await readJson('does-not-exist.json')
-      throw new Error('Expected an error to be thrown')
-    } catch (error) {
-      const err = error as Error
-      expect(err.message).to.include('ENOENT: no such file or directory')
-    }
-  })
-})
-
 describe('castArray', () => {
   it('should cast a value to an array', () => {
     expect(castArray('foo')).to.deep.equal(['foo'])