diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 5786e66..6fff649 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,55 +1,7 @@ import * as authCli from '@spotifly/auth-token/cli'; import * as libraryCli from '@spotifly/library/cli'; -import { colors } from '@spotifly/utils'; -import ownPackage from '../package.json'; -import { - credentialsFromConfig, - getAccessToken, - logError, - profileFromArgv, - readConfig, -} from './credentials'; - -export type Invoke = { - callback: (args: string[]) => unknown; - help: () => string; - pkg: { - name: string; - version: string; - homepage: string; - }; -}; - -const invoke = async ( - argv: string[], - tokenFlag: string | null, - { callback, help, pkg }: Invoke -): Promise => { - if (argv.includes('--help') || argv.includes('-h')) { - console.info(`${colors.bold(colors.cyan(`${pkg.name} v${pkg.version}`))} - -${help()} - -For docs & help, visit ${pkg.homepage} -`); - return; - } - - if (!tokenFlag || argv.includes(tokenFlag)) return callback(argv); - - const spotiflyConfig = readConfig(); - if (!spotiflyConfig) return callback(argv); - - const profile = profileFromArgv(argv); - try { - const credentials = credentialsFromConfig(spotiflyConfig, profile); - const { access_token } = await getAccessToken(credentials); - - return callback([...argv, tokenFlag, access_token]); - } catch (err) { - logError(err); - } -}; +import commands from './commands'; +import { invokePackage } from './invoke'; export const run = async (): Promise => { const cmd = process.argv[2]; @@ -58,50 +10,30 @@ export const run = async (): Promise => { switch (cmd) { case '--version': case '-v': - console.info( - `${colors.bold( - colors.cyan(`${ownPackage.name} v${ownPackage.version}`) - )}` - ); + console.info(commands.version()); return; case '--help': case '-h': case undefined: - console.info(`${colors.bold( - colors.cyan(`${ownPackage.name} v${ownPackage.version}`) - )} -- ${ownPackage.description} - -Available subcommands: ${colors.green(` -* auth -* library -`)} - -Optional flags: -${colors.yellow('* --profile')} [string] - The profile in your Spotifly config file to use for authentication. - Defaults to 'default'. - -For docs & help, visit ${ownPackage.homepage}`); + console.info(commands.mainHelp()); + return; + case 'profiles': + console.info(commands.profiles()); return; case 'auth': - return invoke(args, null, { + return invokePackage(args, null, { callback: authCli.callback, help: authCli.help, pkg: authCli.pkg, }); case 'library': // Library expects the token as a flag - return invoke(args, '--token', { + return invokePackage(args, '--token', { callback: libraryCli.callback, help: libraryCli.help, pkg: libraryCli.pkg, }); default: - console.info( - colors.yellow( - `Unknown argument '${cmd}'.\nRun 'spotifly --help' for available commands` - ) - ); + console.info(commands.fallback(cmd)); } }; diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts new file mode 100644 index 0000000..5183a94 --- /dev/null +++ b/packages/cli/src/commands.ts @@ -0,0 +1,87 @@ +import { colors as c } from '@spotifly/utils'; +import ini from 'ini'; +import pkg from '../package.json'; +import { readConfigWithPath } from './credentials'; +import { Package } from './invoke'; + +const lb = '\n'; +const lblb = '\n\n'; + +const versionHeader = (name: string, version: string) => { + return c.bold(c.cyan(`${name} v${version}`)); +}; +const helpFooter = (homepage: string) => { + return `For docs & help, visit ${homepage}`; +}; +const profiles = () => { + try { + const spotiflyConfigAndPath = readConfigWithPath(); + if (!spotiflyConfigAndPath) throw new Error(); + const [config, configPath] = spotiflyConfigAndPath; + const parsedConfig = ini.parse(config); + const profiles = Object.keys(parsedConfig) + .map(p => '* ' + p) + .join('\n'); + return ( + 'Available profiles:' + + lb + + c.green(profiles) + + lblb + + `Config file: ${configPath}` + ); + } catch (err) { + return 'No profiles found, does your config file exist?'; + } +}; +const fallback = (cmd: string) => { + return ( + c.yellow(`Unknown argument '${cmd}'`) + + lblb + + "Run 'spotifly --help' for available commands" + ); +}; +const subCommands = `Available subcommands: ${c.green(` +* auth +* library +* profiles +`)}`; +const optionalFlags = `Optional global flags: +${c.yellow('* --profile')} [string] +The profile in your Spotifly config file to use for authentication. +Defaults to 'default'. + +In order to see a list of available profiles, run ${c.green( + 'spotifly profiles' +)}`; + +export default { + packageHelp(pkg: Package, packageSpecificHelp: () => string) { + return ( + versionHeader(pkg.name, pkg.version) + + lblb + + packageSpecificHelp() + + lblb + + helpFooter(pkg.homepage) + ); + }, + mainHelp() { + return ( + versionHeader(pkg.name, pkg.version) + + lb + + `- ${pkg.description}` + + lblb + + subCommands + + lb + + optionalFlags + + lblb + + helpFooter(pkg.homepage) + ); + }, + profiles, + version() { + return versionHeader(pkg.name, pkg.version); + }, + fallback(cmd: string) { + return fallback(cmd); + }, +}; diff --git a/packages/cli/src/credentials.ts b/packages/cli/src/credentials.ts index e18ab6c..317c7a0 100644 --- a/packages/cli/src/credentials.ts +++ b/packages/cli/src/credentials.ts @@ -17,17 +17,24 @@ export function profileFromArgv(argv: string[]): string { return argv[withProfile + 1]; } -export function readConfig(): string | null { - const curDirConf = join(process.cwd(), configFileName); - if (existsSync(curDirConf)) return readFileSync(curDirConf, 'utf-8'); - - // Fallback dir is home - const homeDirConf = join(os.homedir(), configFileName); - if (existsSync(homeDirConf)) return readFileSync(homeDirConf, 'utf-8'); - +export function readConfigWithPath(): [string, string] | null { + let configPath = join(process.cwd(), configFileName); + if (!existsSync(configPath)) { + configPath = join(os.homedir(), configFileName); + } + if (existsSync(configPath)) { + const parsed = readFileSync(configPath, 'utf-8'); + return [parsed, configPath]; + } return null; } +export function readConfig() { + const configAndPath = readConfigWithPath(); + if (!configAndPath) return null; + return configAndPath[0]; +} + export function credentialsFromConfig( config: string, profile: string diff --git a/packages/cli/src/invoke.ts b/packages/cli/src/invoke.ts new file mode 100644 index 0000000..adde1d7 --- /dev/null +++ b/packages/cli/src/invoke.ts @@ -0,0 +1,46 @@ +import commands from './commands'; +import { + credentialsFromConfig, + getAccessToken, + logError, + profileFromArgv, + readConfig, +} from './credentials'; + +export type Package = Pick< + typeof import('../package.json'), + 'name' | 'version' | 'homepage' +>; + +export type InvokePackageArgs = { + callback: (args: string[]) => unknown; + help: () => string; + pkg: Package; +}; + +export const invokePackage = async ( + argv: string[], + tokenFlag: string | null, + { callback, help, pkg }: InvokePackageArgs +): Promise => { + // Invoke package-specific help + if (argv.includes('--help') || argv.includes('-h')) { + console.info(commands.packageHelp(pkg, help)); + return; + } + // If the package doesn't need a token, just invoke it + if (!tokenFlag || argv.includes(tokenFlag)) return callback(argv); + + const spotiflyConfig = readConfig(); + if (!spotiflyConfig) return callback(argv); + + const profile = profileFromArgv(argv); + try { + const credentials = credentialsFromConfig(spotiflyConfig, profile); + const { access_token } = await getAccessToken(credentials); + + return callback([...argv, tokenFlag, access_token]); + } catch (err) { + logError(err); + } +}; diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index 0997d2b..63f782f 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -1,7 +1,7 @@ import { AuthProvider } from '@spotifly/core/provider'; import * as credentialUtils from '../src/credentials'; -const mockPkg: cli.Invoke = { +const mockPkg: InvokePackageArgs = { callback: jest.fn(), help: jest.fn(), pkg: { @@ -15,6 +15,7 @@ jest.mock('@spotifly/auth-token/cli', () => mockPkg); jest.mock('@spotifly/library/cli', () => mockPkg); import * as cli from '../src/cli'; +import { InvokePackageArgs } from '../src/invoke'; jest.spyOn(AuthProvider, 'getAccessToken').mockResolvedValue({ access_token: 'spt_token', @@ -27,6 +28,10 @@ const configSpy = jest .spyOn(credentialUtils, 'readConfig') .mockReturnValue(null); +const configWithPathSpy = jest + .spyOn(credentialUtils, 'readConfigWithPath') + .mockReturnValue(null); + const consoleInfoSpy = jest .spyOn(global.console, 'info') .mockImplementation(jest.fn); @@ -57,6 +62,22 @@ describe('CLI', () => { }); }); + [['', '', 'profiles']].forEach(processArgs => { + test('Meta commands: ' + processArgs[2], async () => { + process.argv = processArgs; + await cli.run(); + expect(mockPkg.callback).not.toHaveBeenCalled(); + expect(configWithPathSpy).toHaveBeenCalled(); + expect(consoleInfoSpy.mock.calls[0][0]).toMatchInlineSnapshot( + `"No profiles found, does your config file exist?"` + ); + consoleInfoSpy.mockClear(); + configWithPathSpy.mockReturnValueOnce(['[default]', 'path']); + await cli.run(); + expect(consoleInfoSpy.mock.calls[0][0]).toMatch(/Available profiles/); + }); + }); + [ ['', '', 'auth', '-h'], ['', '', 'library', '--help'],