Skip to content

Commit

Permalink
feat: add profiles command (#83)
Browse files Browse the repository at this point in the history
* add profiles command

* add changeset

* add docs and changeset

* fix docs about convenience methods

* fix typo
  • Loading branch information
eegli authored Jan 2, 2024
1 parent 6526e1e commit 746c802
Show file tree
Hide file tree
Showing 15 changed files with 1,452 additions and 113 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-dancers-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spotifly/cli': minor
---

The core CLI now supports the `profiles` command to list available Spotifly profiles.
1 change: 0 additions & 1 deletion packages/auth-token/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ export const help = () =>
const pkg = require('../package.json');

export const packageName = pkg.name;
export const packageHomepage = pkg.homepage;
export const packageVersion = pkg.version;
88 changes: 10 additions & 78 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,8 @@
import * as authCli from '@spotifly/auth-token/cli';
import * as libraryCli from '@spotifly/library/cli';
import { colors } from '@spotifly/utils';
import {
credentialsFromConfig,
getAccessToken,
logError,
profileFromArgv,
readConfig,
} from './credentials';

export type Invoke = {
callback: (args: string[]) => unknown;
help: () => string;
packageName: string;
packageHomepage: string;
packageVersion: string;
};

const invoke = async (
argv: string[],
tokenFlag: string | null,
{ callback, help, packageName, packageVersion, packageHomepage }: Invoke,
): Promise<unknown> => {
if (argv.includes('--help') || argv.includes('-h')) {
console.info(`${colors.bold(
colors.cyan(`${packageName} v${packageVersion}`),
)}
${help()}
For docs & help, visit ${packageHomepage}
`);
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<unknown> => {
const cmd = process.argv[2];
Expand All @@ -66,54 +20,32 @@ export const run = async (): Promise<unknown> => {
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,
packageName: authCli.packageName,
packageHomepage: authCli.packageHomepage,
packageVersion: authCli.packageVersion,
});
case 'library':
// Library expects the token as a flag
return invoke(args, '--token', {
return invokePackage(args, '--token', {
callback: libraryCli.callback,
help: libraryCli.help,
packageName: authCli.packageName,
packageHomepage: authCli.packageHomepage,
packageVersion: authCli.packageVersion,
});
default:
console.info(
colors.yellow(
`Unknown argument '${cmd}'.\nRun 'spotifly --help' for available commands`,
),
);
console.info(commands.fallback(cmd));
}
};
89 changes: 89 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { colors as c } from '@spotifly/utils';
import ini from 'ini';
import pkg from '../package.json';
import { readConfigWithPath } from './credentials';

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:
${c.green(profiles)}
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
`)}
Optional global flags:
${c.yellow('* --profile')} [string]
The profile in your Spotifly config file to use for authentication.
Defaults to 'default'.
- In order to get help for a specific subcommand, run ${c.green(
'spotifly <subcommand> --help',
)}
- In order to see a list of available profiles, run ${c.green(
'spotifly profiles',
)}`;

export default {
packageHelp(
packageName: string,
packageVersion: string,
packageSpecificHelp: () => string,
) {
return (
versionHeader(packageName, packageVersion) +
lblb +
packageSpecificHelp() +
lblb +
helpFooter(pkg.homepage)
);
},
mainHelp() {
return (
versionHeader(pkg.name, pkg.version) +
lb +
`- ${pkg.description}` +
lblb +
subCommands +
lblb +
helpFooter(pkg.homepage)
);
},
profiles,
version() {
return versionHeader(pkg.name, pkg.version);
},
fallback(cmd: string) {
return fallback(cmd);
},
};
23 changes: 15 additions & 8 deletions packages/cli/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/src/invoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import commands from './commands';
import {
credentialsFromConfig,
getAccessToken,
logError,
profileFromArgv,
readConfig,
} from './credentials';

export type InvokePackageArgs = {
callback: (args: string[]) => unknown;
help: () => string;
packageName: string;
packageVersion: string;
};

export const invokePackage = async (
argv: string[],
tokenFlag: string | null,
{ callback, help, packageName, packageVersion }: InvokePackageArgs,
): Promise<unknown> => {
// Invoke package-specific help
if (argv.includes('--help') || argv.includes('-h')) {
console.info(commands.packageHelp(packageName, packageVersion, 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);
console.info(`Using profile "${profile}"`);
try {
const credentials = credentialsFromConfig(spotiflyConfig, profile);
const { access_token } = await getAccessToken(credentials);

return callback([...argv, tokenFlag, access_token]);
} catch (err) {
logError(err);
}
};
24 changes: 22 additions & 2 deletions packages/cli/test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
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(),
packageName: 'test',
packageHomepage: 'https://test.com',
packageVersion: '1.0.0',
};

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',
Expand All @@ -25,6 +25,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);
Expand Down Expand Up @@ -55,6 +59,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'],
Expand Down
2 changes: 1 addition & 1 deletion packages/library/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export const help = () =>
defaultHelp({
title: 'Command-line usage:',
});

// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../package.json');

export const packageName = pkg.name;
export const packageHomepage = pkg.homepage;
export const packageVersion = pkg.version;
Loading

0 comments on commit 746c802

Please sign in to comment.