Skip to content

Commit 1806bbe

Browse files
committed
feat(cli): add config options to CLI
Support setting the mermaidchart baseURL by using `--base-url`, and the auth token with `--auth-token`. The values will override the values in the config file, if specified.
1 parent 6c12137 commit 1806bbe

File tree

4 files changed

+102
-40
lines changed

4 files changed

+102
-40
lines changed

cSpell.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"Cataa",
66
"colour",
77
"Cookiebot",
8+
"ENONET",
89
"gantt",
910
"jsnext",
1011
"lintstagedrc",

packages/cli/src/commander.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ describe('whoami', () => {
136136
).rejects.toThrowError('Invalid access token.');
137137
});
138138

139+
it('--auth-token should override config file', async () => {
140+
const { program } = mockedProgram();
141+
142+
const myCliAuthToken = 'my-cli-auth-token';
143+
144+
await program.parseAsync(
145+
['--config', CONFIG_AUTHED, '--auth-token', myCliAuthToken, 'whoami'],
146+
{ from: 'user' },
147+
);
148+
149+
expect(vi.mocked(MermaidChart.prototype.setAccessToken)).toHaveBeenCalledWith(myCliAuthToken);
150+
});
151+
139152
it('should print email of logged in user', async () => {
140153
const { program } = mockedProgram();
141154

packages/cli/src/commander.ts

+75-40
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import {
33
CommanderError,
44
createCommand,
55
InvalidArgumentError,
6+
Option,
67
} from '@commander-js/extra-typings';
78
import { readFile, writeFile } from 'fs/promises';
89
import { MermaidChart } from '@mermaidchart/sdk';
910

1011
import confirm from '@inquirer/confirm';
1112
import input from '@inquirer/input';
1213
import select, { Separator } from '@inquirer/select';
13-
import { type Config, defaultConfigPath, readConfig, writeConfig } from './config.js';
14+
import { defaultConfigPath, readConfig, writeConfig, optionNameMap } from './config.js';
1415
import { type Cache, link, type LinkOptions, pull, push } from './methods.js';
1516
import { processMarkdown } from './remark.js';
1617

@@ -25,32 +26,40 @@ import { processMarkdown } from './remark.js';
2526
*/
2627
type CommonOptions = ReturnType<ReturnType<typeof createCommanderCommand>['opts']>;
2728

28-
async function createClient(options: CommonOptions, config?: Config) {
29-
if (config === undefined) {
30-
try {
31-
config = await readConfig(options['config']);
32-
} catch (error) {
33-
if (
34-
error instanceof Error &&
35-
'errno' in error &&
36-
(error as NodeJS.ErrnoException).code === 'ENOENT' &&
37-
options['config'] === defaultConfigPath()
38-
) {
39-
config = {};
40-
} else {
41-
throw new InvalidArgumentError(
42-
`Failed to load config file ${options['config']} due to: ${error}`,
43-
);
44-
}
29+
/**
30+
* Reads the config file from the given `--config <configPath>` argument, ignoring
31+
* ENONET errors if `ignoreENONET` is `true`.
32+
*
33+
* @param configPath - The path to the config file.
34+
* @param ignoreENONET - Whether to ignore ENONET errors.
35+
* @throws {@link InvalidArgumentError}
36+
* Thrown if:
37+
* - The config file exists, but is not a valid TOML file.
38+
* - The config file does not exist, and `ignoreENONET` is `false`.
39+
*/
40+
async function readConfigFromConfigArg(configPath: string, ignoreENONET: boolean = false) {
41+
try {
42+
return await readConfig(configPath);
43+
} catch (error) {
44+
if (
45+
error instanceof Error &&
46+
'errno' in error &&
47+
(error as NodeJS.ErrnoException).code === 'ENOENT' &&
48+
ignoreENONET
49+
) {
50+
return {};
4551
}
52+
throw new InvalidArgumentError(`Failed to load config file ${configPath} due to: ${error}`);
4653
}
54+
}
4755

56+
async function createClient(options: CommonOptions) {
4857
const client = new MermaidChart({
4958
clientID: '018bf0ff-7e8c-7952-ab4e-a7bd7c7f54f3',
50-
baseURL: new URL(config.base_url ?? 'https://mermaidchart.com').toString(),
59+
baseURL: new URL(options.baseUrl).toString(),
5160
});
5261

53-
if (config.auth_token === undefined) {
62+
if (options.authToken === undefined) {
5463
throw new CommanderError(
5564
/*exitCode=*/ 1,
5665
'ENEEDAUTH',
@@ -59,7 +68,7 @@ async function createClient(options: CommonOptions, config?: Config) {
5968
}
6069

6170
try {
62-
await client.setAccessToken(config.auth_token);
71+
await client.setAccessToken(options.authToken);
6372
} catch (error) {
6473
if (error instanceof Error && error.message.includes('401')) {
6574
throw new CommanderError(
@@ -91,32 +100,22 @@ function login() {
91100
.description('Login to a Mermaid Chart account')
92101
.action(async (_options, command) => {
93102
const optsWithGlobals = command.optsWithGlobals<CommonOptions>();
94-
let config;
95-
try {
96-
config = await readConfig(optsWithGlobals['config']);
97-
} catch (error) {
98-
if (
99-
error instanceof Error &&
100-
'errno' in error &&
101-
(error as NodeJS.ErrnoException).code === 'ENOENT'
102-
) {
103-
config = {};
104-
} else {
105-
throw error;
106-
}
107-
}
108103

109-
const baseURL = new URL(config.base_url ?? 'https://mermaidchart.com').toString();
104+
// empty if default config file doesn't exist
105+
const config = await readConfigFromConfigArg(
106+
optsWithGlobals['config'],
107+
/*ignoreENONET=*/ true,
108+
);
110109

111110
const client = new MermaidChart({
112111
clientID: '018bf0ff-7e8c-7952-ab4e-a7bd7c7f54f3',
113-
baseURL: baseURL,
112+
baseURL: optsWithGlobals.baseUrl,
114113
});
115114

116115
const answer = await input({
117116
message: `Enter your API token. You can generate one at ${new URL(
118117
'/app/user/settings',
119-
baseURL,
118+
optsWithGlobals.baseUrl,
120119
)}`,
121120
async validate(key) {
122121
try {
@@ -150,7 +149,7 @@ function logout() {
150149
await writeConfig(optsWithGlobals['config'], config);
151150

152151
try {
153-
const user = await (await createClient(optsWithGlobals, config)).getUser();
152+
const user = await (await createClient(optsWithGlobals)).getUser();
154153
console.log(`API token for ${user.emailAddress} removed from ${optsWithGlobals['config']}`);
155154
} catch (error) {
156155
// API token might have been expired
@@ -295,7 +294,43 @@ export function createCommanderCommand() {
295294
'-c, --config <config_file>',
296295
'The path to the config file to use.',
297296
defaultConfigPath(),
298-
);
297+
)
298+
.addOption(
299+
new Option(
300+
'--base-url <base_url>',
301+
'The base URL of the Mermaid Chart instance to use.',
302+
).default('https://mermaidchart.com'),
303+
)
304+
.addOption(new Option('--auth-token <auth_token>', 'The Mermaid Chart API token to use.'))
305+
.hook('preSubcommand', async (command, actionCommand) => {
306+
const configPath = command.getOptionValue('config');
307+
/**
308+
* config file is allowed to not exist if:
309+
* - the user is running the `login` command, we'll create a new config file if it doesn't exist
310+
*/
311+
const ignoreENONET = configPath === defaultConfigPath() || actionCommand.name() === 'login';
312+
313+
const config = await readConfigFromConfigArg(configPath, ignoreENONET);
314+
for (const key in config) {
315+
if (!(key in optionNameMap)) {
316+
console.warn(`Warning: Ignoring unrecognized config key: ${key} in ${configPath}`);
317+
continue;
318+
}
319+
const optionCommanderName = optionNameMap[key as keyof typeof optionNameMap];
320+
// config values only override default/implied values
321+
if (
322+
[undefined, 'default', 'implied'].includes(
323+
command.getOptionValueSource(optionCommanderName),
324+
)
325+
) {
326+
command.setOptionValueWithSource(
327+
optionNameMap[key as keyof typeof optionNameMap],
328+
config[key],
329+
'config',
330+
);
331+
}
332+
}
333+
});
299334

300335
return program
301336
.addCommand(whoami())

packages/cli/src/config.ts

+13
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ export function defaultConfigPath() {
3838
return join(userConfigDir(), 'mermaid-chart.toml');
3939
}
4040

41+
/**
42+
* Maps the TOML keys to the Commander option names.
43+
*
44+
* Commander uses:
45+
* - kebab-case for CLI options, but
46+
* - camelCase for option names in JavaScript,
47+
* while TOML uses snake_case.
48+
*/
49+
export const optionNameMap = {
50+
auth_token: 'authToken',
51+
base_url: 'baseUrl',
52+
} as const;
53+
4154
export interface Config extends JsonMap {
4255
/**
4356
* Mermaid-Chart API token.

0 commit comments

Comments
 (0)