Skip to content

Commit

Permalink
add profiles command
Browse files Browse the repository at this point in the history
  • Loading branch information
eegli committed Jan 2, 2024
1 parent d2e983c commit 1ad7384
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 87 deletions.
88 changes: 10 additions & 78 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> => {
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<unknown> => {
const cmd = process.argv[2];
Expand All @@ -58,50 +10,30 @@ 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,
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));
}
};
87 changes: 87 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
@@ -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);
},
};
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);

Check warning on line 23 in packages/cli/src/credentials.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli/src/credentials.ts#L21-L23

Added lines #L21 - L23 were not covered by tests
}
if (existsSync(configPath)) {
const parsed = readFileSync(configPath, 'utf-8');
return [parsed, configPath];

Check warning on line 27 in packages/cli/src/credentials.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli/src/credentials.ts#L25-L27

Added lines #L25 - L27 were not covered by tests
}
return null;
}

export function readConfig() {
const configAndPath = readConfigWithPath();
if (!configAndPath) return null;
return configAndPath[0];

Check warning on line 35 in packages/cli/src/credentials.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli/src/credentials.ts#L33-L35

Added lines #L33 - L35 were not covered by tests
}

export function credentialsFromConfig(
config: string,
profile: string
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/invoke.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> => {
// 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);
}
};
23 changes: 22 additions & 1 deletion packages/cli/test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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',
Expand All @@ -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);
Expand Down Expand Up @@ -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'],
Expand Down

0 comments on commit 1ad7384

Please sign in to comment.