zencommit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +421 -0
  2. package/bin/zencommit.js +37 -0
  3. package/package.json +68 -0
  4. package/scripts/install.mjs +146 -0
  5. package/scripts/platform.mjs +34 -0
  6. package/src/auth/secrets.ts +234 -0
  7. package/src/commands/auth.ts +138 -0
  8. package/src/commands/config.ts +83 -0
  9. package/src/commands/default.ts +322 -0
  10. package/src/commands/models.ts +67 -0
  11. package/src/config/load.test.ts +47 -0
  12. package/src/config/load.ts +118 -0
  13. package/src/config/merge.test.ts +25 -0
  14. package/src/config/merge.ts +30 -0
  15. package/src/config/types.ts +119 -0
  16. package/src/config/validate.ts +139 -0
  17. package/src/git/commit.ts +17 -0
  18. package/src/git/diff.ts +89 -0
  19. package/src/git/repo.ts +10 -0
  20. package/src/index.ts +207 -0
  21. package/src/llm/generate.ts +188 -0
  22. package/src/llm/prompt-template.ts +44 -0
  23. package/src/llm/prompt.ts +83 -0
  24. package/src/llm/prompts/base.md +119 -0
  25. package/src/llm/prompts/conventional.md +123 -0
  26. package/src/llm/prompts/gitmoji.md +212 -0
  27. package/src/llm/prompts/system.md +21 -0
  28. package/src/llm/providers.ts +102 -0
  29. package/src/llm/tokens.test.ts +22 -0
  30. package/src/llm/tokens.ts +46 -0
  31. package/src/llm/truncate.test.ts +60 -0
  32. package/src/llm/truncate.ts +552 -0
  33. package/src/metadata/cache.ts +28 -0
  34. package/src/metadata/index.ts +94 -0
  35. package/src/metadata/providers/local.ts +66 -0
  36. package/src/metadata/providers/modelsdev.ts +145 -0
  37. package/src/metadata/types.ts +20 -0
  38. package/src/ui/editor.ts +33 -0
  39. package/src/ui/prompts.ts +99 -0
  40. package/src/util/exec.ts +57 -0
  41. package/src/util/fs.ts +46 -0
  42. package/src/util/logger.ts +50 -0
  43. package/src/util/redact.ts +30 -0
@@ -0,0 +1,119 @@
1
+ export type CommitStyle = 'conventional' | 'freeform';
2
+ export type DiffMode = 'staged' | 'unstaged' | 'all';
3
+ export type TruncateStrategy = 'byFile' | 'smart';
4
+ export type MetadataProviderName = 'auto' | 'modelsdev' | 'local';
5
+
6
+ export interface AiConfig {
7
+ model: string;
8
+ temperature: number;
9
+ maxOutputTokens: number;
10
+ timeoutMs: number;
11
+ openaiCompatible?: OpenAICompatibleConfig;
12
+ }
13
+
14
+ export interface OpenAICompatibleConfig {
15
+ baseUrl?: string;
16
+ name?: string;
17
+ }
18
+
19
+ export interface CommitConfig {
20
+ style: CommitStyle;
21
+ language: string;
22
+ includeBody: boolean;
23
+ emoji: boolean;
24
+ }
25
+
26
+ export interface GitConfig {
27
+ diffMode: DiffMode;
28
+ autoStage: boolean;
29
+ confirmBeforeCommit: boolean;
30
+ }
31
+
32
+ export interface DiffSmartConfig {
33
+ maxAddedLinesPerHunk: number;
34
+ maxRemovedLinesPerHunk: number;
35
+ }
36
+
37
+ export interface DiffConfig {
38
+ truncateStrategy: TruncateStrategy;
39
+ includeFileList: boolean;
40
+ excludeGitignoreFiles: boolean;
41
+ maxFiles: number;
42
+ smart: DiffSmartConfig;
43
+ }
44
+
45
+ export interface ModelsDevProviderConfig {
46
+ url: string;
47
+ cacheTtlHours: number;
48
+ }
49
+
50
+ export interface LocalProviderConfig {
51
+ path: string;
52
+ }
53
+
54
+ export interface MetadataProvidersConfig {
55
+ modelsdev: ModelsDevProviderConfig;
56
+ local: LocalProviderConfig;
57
+ }
58
+
59
+ export interface MetadataConfig {
60
+ provider: MetadataProviderName;
61
+ fallbackOrder: Array<Exclude<MetadataProviderName, 'auto'>>;
62
+ providers: MetadataProvidersConfig;
63
+ }
64
+
65
+ export interface AuthConfig {
66
+ preferredEnvKey?: string;
67
+ }
68
+
69
+ export interface ResolvedConfig {
70
+ ai: AiConfig;
71
+ commit: CommitConfig;
72
+ git: GitConfig;
73
+ diff: DiffConfig;
74
+ metadata: MetadataConfig;
75
+ auth?: AuthConfig;
76
+ }
77
+
78
+ export const defaultConfig: ResolvedConfig = {
79
+ ai: {
80
+ model: 'openai/gpt-5',
81
+ temperature: 0.2,
82
+ maxOutputTokens: 4096,
83
+ timeoutMs: 20000,
84
+ },
85
+ commit: {
86
+ style: 'conventional',
87
+ language: 'en',
88
+ includeBody: true,
89
+ emoji: false,
90
+ },
91
+ git: {
92
+ diffMode: 'staged',
93
+ autoStage: false,
94
+ confirmBeforeCommit: true,
95
+ },
96
+ diff: {
97
+ truncateStrategy: 'smart',
98
+ includeFileList: true,
99
+ excludeGitignoreFiles: true,
100
+ maxFiles: 200,
101
+ smart: {
102
+ maxAddedLinesPerHunk: 12,
103
+ maxRemovedLinesPerHunk: 12,
104
+ },
105
+ },
106
+ metadata: {
107
+ provider: 'auto',
108
+ fallbackOrder: ['modelsdev', 'local'],
109
+ providers: {
110
+ modelsdev: {
111
+ url: 'https://models.dev/api.json',
112
+ cacheTtlHours: 24,
113
+ },
114
+ local: {
115
+ path: './models.metadata.json',
116
+ },
117
+ },
118
+ },
119
+ };
@@ -0,0 +1,139 @@
1
+ import type { DiffMode, MetadataProviderName, ResolvedConfig, TruncateStrategy } from './types.js';
2
+
3
+ export interface ValidationError {
4
+ path: string;
5
+ message: string;
6
+ }
7
+
8
+ export interface ValidationResult {
9
+ valid: boolean;
10
+ errors: ValidationError[];
11
+ }
12
+
13
+ const addError = (errors: ValidationError[], path: string, message: string): void => {
14
+ errors.push({ path, message });
15
+ };
16
+
17
+ const isString = (value: unknown): value is string => typeof value === 'string';
18
+ const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean';
19
+ const isNumber = (value: unknown): value is number =>
20
+ typeof value === 'number' && !Number.isNaN(value);
21
+
22
+ const isDiffMode = (value: unknown): value is DiffMode =>
23
+ value === 'staged' || value === 'unstaged' || value === 'all';
24
+
25
+ const isTruncateStrategy = (value: unknown): value is TruncateStrategy =>
26
+ value === 'byFile' || value === 'smart';
27
+
28
+ const isProviderName = (value: unknown): value is MetadataProviderName =>
29
+ value === 'auto' || value === 'modelsdev' || value === 'local';
30
+
31
+ export const validateConfig = (config: ResolvedConfig): ValidationResult => {
32
+ const errors: ValidationError[] = [];
33
+
34
+ if (!isString(config.ai.model) || config.ai.model.trim().length === 0) {
35
+ addError(errors, 'ai.model', 'Model must be a non-empty string.');
36
+ }
37
+ if (!isNumber(config.ai.temperature)) {
38
+ addError(errors, 'ai.temperature', 'Temperature must be a number.');
39
+ }
40
+ if (!isNumber(config.ai.maxOutputTokens) || config.ai.maxOutputTokens <= 0) {
41
+ addError(errors, 'ai.maxOutputTokens', 'Max output tokens must be a positive number.');
42
+ }
43
+ if (!isNumber(config.ai.timeoutMs) || config.ai.timeoutMs <= 0) {
44
+ addError(errors, 'ai.timeoutMs', 'Timeout must be a positive number.');
45
+ }
46
+ if (config.ai.openaiCompatible) {
47
+ if (
48
+ config.ai.openaiCompatible.baseUrl !== undefined &&
49
+ (!isString(config.ai.openaiCompatible.baseUrl) ||
50
+ config.ai.openaiCompatible.baseUrl.trim().length === 0)
51
+ ) {
52
+ addError(
53
+ errors,
54
+ 'ai.openaiCompatible.baseUrl',
55
+ 'openaiCompatible.baseUrl must be a non-empty string.',
56
+ );
57
+ }
58
+ if (
59
+ config.ai.openaiCompatible.name !== undefined &&
60
+ !isString(config.ai.openaiCompatible.name)
61
+ ) {
62
+ addError(errors, 'ai.openaiCompatible.name', 'openaiCompatible.name must be a string.');
63
+ }
64
+ }
65
+
66
+ if (config.commit.style !== 'conventional' && config.commit.style !== 'freeform') {
67
+ addError(errors, 'commit.style', 'Style must be conventional or freeform.');
68
+ }
69
+ if (!isString(config.commit.language)) {
70
+ addError(errors, 'commit.language', 'Language must be a string.');
71
+ }
72
+ if (!isBoolean(config.commit.includeBody)) {
73
+ addError(errors, 'commit.includeBody', 'includeBody must be a boolean.');
74
+ }
75
+ if (!isBoolean(config.commit.emoji)) {
76
+ addError(errors, 'commit.emoji', 'emoji must be a boolean.');
77
+ }
78
+
79
+ if (!isDiffMode(config.git.diffMode)) {
80
+ addError(errors, 'git.diffMode', 'diffMode must be staged, unstaged, or all.');
81
+ }
82
+ if (!isBoolean(config.git.autoStage)) {
83
+ addError(errors, 'git.autoStage', 'autoStage must be a boolean.');
84
+ }
85
+ if (!isBoolean(config.git.confirmBeforeCommit)) {
86
+ addError(errors, 'git.confirmBeforeCommit', 'confirmBeforeCommit must be a boolean.');
87
+ }
88
+
89
+ if (!isTruncateStrategy(config.diff.truncateStrategy)) {
90
+ addError(errors, 'diff.truncateStrategy', 'truncateStrategy must be byFile or smart.');
91
+ }
92
+ if (!isBoolean(config.diff.includeFileList)) {
93
+ addError(errors, 'diff.includeFileList', 'includeFileList must be a boolean.');
94
+ }
95
+ if (!isBoolean(config.diff.excludeGitignoreFiles)) {
96
+ addError(errors, 'diff.excludeGitignoreFiles', 'excludeGitignoreFiles must be a boolean.');
97
+ }
98
+ if (!isNumber(config.diff.maxFiles) || config.diff.maxFiles <= 0) {
99
+ addError(errors, 'diff.maxFiles', 'maxFiles must be a positive number.');
100
+ }
101
+ if (
102
+ !isNumber(config.diff.smart.maxAddedLinesPerHunk) ||
103
+ config.diff.smart.maxAddedLinesPerHunk <= 0
104
+ ) {
105
+ addError(errors, 'diff.smart.maxAddedLinesPerHunk', 'maxAddedLinesPerHunk must be positive.');
106
+ }
107
+ if (
108
+ !isNumber(config.diff.smart.maxRemovedLinesPerHunk) ||
109
+ config.diff.smart.maxRemovedLinesPerHunk <= 0
110
+ ) {
111
+ addError(
112
+ errors,
113
+ 'diff.smart.maxRemovedLinesPerHunk',
114
+ 'maxRemovedLinesPerHunk must be positive.',
115
+ );
116
+ }
117
+
118
+ if (!isProviderName(config.metadata.provider)) {
119
+ addError(errors, 'metadata.provider', 'provider must be auto, modelsdev, or local.');
120
+ }
121
+ if (!Array.isArray(config.metadata.fallbackOrder)) {
122
+ addError(errors, 'metadata.fallbackOrder', 'fallbackOrder must be an array.');
123
+ }
124
+ if (!isString(config.metadata.providers.modelsdev.url)) {
125
+ addError(errors, 'metadata.providers.modelsdev.url', 'modelsdev url must be a string.');
126
+ }
127
+ if (!isNumber(config.metadata.providers.modelsdev.cacheTtlHours)) {
128
+ addError(
129
+ errors,
130
+ 'metadata.providers.modelsdev.cacheTtlHours',
131
+ 'cacheTtlHours must be a number.',
132
+ );
133
+ }
134
+ if (!isString(config.metadata.providers.local.path)) {
135
+ addError(errors, 'metadata.providers.local.path', 'local path must be a string.');
136
+ }
137
+
138
+ return { valid: errors.length === 0, errors };
139
+ };
@@ -0,0 +1,17 @@
1
+ import { exec } from '../util/exec.js';
2
+
3
+ export const commitMessage = async (
4
+ subject: string,
5
+ body: string,
6
+ extraArgs: string[] = [],
7
+ cwd?: string,
8
+ ): Promise<void> => {
9
+ const args = ['git', 'commit', '-m', subject];
10
+ if (body && body.trim().length > 0) {
11
+ args.push('-m', body.trim());
12
+ }
13
+ if (extraArgs.length > 0) {
14
+ args.push(...extraArgs);
15
+ }
16
+ await exec(args, { cwd });
17
+ };
@@ -0,0 +1,89 @@
1
+ import type { DiffMode } from '../config/types.js';
2
+ import { exec } from '../util/exec.js';
3
+
4
+ export interface DiffOptions {
5
+ mode: DiffMode;
6
+ cwd?: string;
7
+ compact?: boolean;
8
+ }
9
+
10
+ const diffBaseArgs = (mode: DiffMode): string[] => {
11
+ if (mode === 'staged') {
12
+ return ['diff', '--cached'];
13
+ }
14
+ return ['diff'];
15
+ };
16
+
17
+ export const getDiff = async ({ mode, cwd, compact }: DiffOptions): Promise<string> => {
18
+ const args = diffBaseArgs(mode);
19
+ if (compact) {
20
+ args.push(
21
+ '--unified=0',
22
+ '--no-color',
23
+ '--no-ext-diff',
24
+ '--diff-algorithm=histogram',
25
+ '--no-prefix',
26
+ );
27
+ } else {
28
+ args.push('--no-color');
29
+ }
30
+ const result = await exec(['git', ...args], { cwd });
31
+ return result.stdout;
32
+ };
33
+
34
+ export const getFileList = async ({ mode, cwd }: DiffOptions): Promise<string[]> => {
35
+ const args = diffBaseArgs(mode);
36
+ args.push('--name-only');
37
+ const result = await exec(['git', ...args], { cwd });
38
+ return result.stdout
39
+ .split(/\r?\n/)
40
+ .map((line) => line.trim())
41
+ .filter(Boolean);
42
+ };
43
+
44
+ const parseNumstat = (
45
+ raw: string,
46
+ ): Map<string, { added: number | null; removed: number | null }> => {
47
+ const map = new Map<string, { added: number | null; removed: number | null }>();
48
+ const lines = raw.split(/\r?\n/).filter(Boolean);
49
+ for (const line of lines) {
50
+ const [addedRaw, removedRaw, ...pathParts] = line.split('\t');
51
+ const filePath = pathParts.join('\t');
52
+ const added = addedRaw === '-' ? null : Number(addedRaw);
53
+ const removed = removedRaw === '-' ? null : Number(removedRaw);
54
+ map.set(filePath, {
55
+ added: Number.isFinite(added) ? added : null,
56
+ removed: Number.isFinite(removed) ? removed : null,
57
+ });
58
+ }
59
+ return map;
60
+ };
61
+
62
+ export const getFileSummary = async ({ mode, cwd }: DiffOptions): Promise<string> => {
63
+ const baseArgs = diffBaseArgs(mode);
64
+ const [nameStatus, numstat] = await Promise.all([
65
+ exec(['git', ...baseArgs, '--name-status'], { cwd }),
66
+ exec(['git', ...baseArgs, '--numstat'], { cwd }),
67
+ ]);
68
+
69
+ const numstatMap = parseNumstat(numstat.stdout);
70
+ const lines = nameStatus.stdout.split(/\r?\n/).filter(Boolean);
71
+
72
+ const summaryLines = lines.map((line) => {
73
+ const parts = line.split('\t');
74
+ const status = parts[0] ?? '';
75
+ const pathPart = parts.length > 2 ? `${parts[1]} -> ${parts[2]}` : (parts[1] ?? '');
76
+ const numstatEntry =
77
+ numstatMap.get(parts[2] ?? '') ??
78
+ numstatMap.get(parts[1] ?? '') ??
79
+ numstatMap.get(pathPart) ??
80
+ null;
81
+ const added = numstatEntry?.added ?? null;
82
+ const removed = numstatEntry?.removed ?? null;
83
+ const addedText = added === null ? '?' : String(added);
84
+ const removedText = removed === null ? '?' : String(removed);
85
+ return `${status} ${pathPart} (+${addedText} -${removedText})`.trim();
86
+ });
87
+
88
+ return summaryLines.join('\n');
89
+ };
@@ -0,0 +1,10 @@
1
+ import { exec } from '../util/exec.js';
2
+
3
+ export const getRepoRoot = async (): Promise<string | null> => {
4
+ try {
5
+ const result = await exec(['git', 'rev-parse', '--show-toplevel']);
6
+ return result.stdout.trim() || null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ };
package/src/index.ts ADDED
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bun
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
6
+ import yargs from 'yargs/yargs';
7
+ import { hideBin } from 'yargs/helpers';
8
+ import type { Argv } from 'yargs';
9
+ import { runDefaultCommand } from './commands/default.js';
10
+ import { runAuthLogin, runAuthLogout, runAuthStatus } from './commands/auth.js';
11
+ import { runConfigInit, runConfigPrint, runConfigValidate } from './commands/config.js';
12
+ import { runModelsInfo, runModelsSearch } from './commands/models.js';
13
+ import { setVerbosity } from './util/logger.js';
14
+
15
+ type DefaultArgs = {
16
+ yes: boolean;
17
+ 'dry-run': boolean;
18
+ all: boolean;
19
+ unstaged: boolean;
20
+ commit: boolean;
21
+ model?: string;
22
+ format?: 'conventional' | 'freeform';
23
+ lang?: string;
24
+ 'no-body': boolean;
25
+ verbose: number;
26
+ '--'?: string[];
27
+ };
28
+
29
+ type AuthArgs = {
30
+ 'env-key'?: string;
31
+ token?: string;
32
+ };
33
+
34
+ type ModelsSearchArgs = {
35
+ query?: string;
36
+ maxItems?: number;
37
+ };
38
+
39
+ type ModelsInfoArgs = {
40
+ modelId: string;
41
+ };
42
+
43
+ const cli = (yargs(hideBin(process.argv)) as Argv<DefaultArgs>)
44
+ .scriptName('zencommit')
45
+ .parserConfiguration({ 'populate--': true })
46
+ .option('verbose', {
47
+ alias: 'v',
48
+ count: true,
49
+ default: 0,
50
+ describe: 'Increase verbosity (-v, -vv, -vvv)',
51
+ })
52
+ .middleware((argv) => {
53
+ const verboseCount = typeof argv.verbose === 'number' ? (argv.verbose as number) : 0;
54
+ setVerbosity(verboseCount);
55
+ })
56
+ .command(
57
+ '$0',
58
+ 'Generate commit message',
59
+ (yargsBuilder) =>
60
+ yargsBuilder
61
+ .option('yes', {
62
+ type: 'boolean',
63
+ default: false,
64
+ describe: 'Skip confirmation and commit',
65
+ })
66
+ .option('dry-run', {
67
+ type: 'boolean',
68
+ default: false,
69
+ describe: 'Do not commit; print output',
70
+ })
71
+ .option('all', {
72
+ type: 'boolean',
73
+ default: false,
74
+ describe: 'Stage all changes before generating',
75
+ })
76
+ .option('unstaged', { type: 'boolean', default: false, describe: 'Use unstaged diff' })
77
+ .option('commit', {
78
+ type: 'boolean',
79
+ default: false,
80
+ describe: 'Allow committing with --unstaged',
81
+ })
82
+ .option('model', { type: 'string', describe: 'Override model id' })
83
+ .option('format', {
84
+ type: 'string',
85
+ choices: ['conventional', 'freeform'] as const,
86
+ describe: 'Commit style',
87
+ })
88
+ .option('lang', { type: 'string', describe: 'Commit language' })
89
+ .option('no-body', { type: 'boolean', default: false, describe: 'Subject only' }),
90
+ async (argv: DefaultArgs) => {
91
+ await runDefaultCommand({
92
+ yes: argv.yes,
93
+ dryRun: argv['dry-run'],
94
+ all: argv.all,
95
+ unstaged: argv.unstaged,
96
+ commit: argv.commit,
97
+ model: argv.model,
98
+ format: argv.format,
99
+ lang: argv.lang,
100
+ noBody: argv['no-body'],
101
+ verbose: argv.verbose,
102
+ '--': argv['--'],
103
+ });
104
+ },
105
+ )
106
+ .command(
107
+ 'auth',
108
+ 'Manage credentials',
109
+ (yargsBuilder) =>
110
+ yargsBuilder
111
+ .command(
112
+ 'login',
113
+ 'Store an API key in Bun secrets',
114
+ (sub) =>
115
+ sub
116
+ .option('env-key', { type: 'string', describe: 'Environment key name' })
117
+ .option('token', { type: 'string', describe: 'Secret token value' }),
118
+ async (argv: AuthArgs) => {
119
+ await runAuthLogin({ envKey: argv['env-key'], token: argv.token });
120
+ },
121
+ )
122
+ .command(
123
+ 'logout',
124
+ 'Remove an API key from Bun secrets',
125
+ (sub) => sub.option('env-key', { type: 'string', describe: 'Environment key name' }),
126
+ async (argv: AuthArgs) => {
127
+ await runAuthLogout({ envKey: argv['env-key'] });
128
+ },
129
+ )
130
+ .command(
131
+ 'status',
132
+ 'Show stored credentials',
133
+ () => {},
134
+ async () => {
135
+ await runAuthStatus();
136
+ },
137
+ )
138
+ .demandCommand(1, 'Specify a subcommand'),
139
+ () => {},
140
+ )
141
+ .command(
142
+ 'config',
143
+ 'Configuration commands',
144
+ (yargsBuilder) =>
145
+ yargsBuilder
146
+ .command(
147
+ 'print',
148
+ 'Print resolved config',
149
+ () => {},
150
+ async () => {
151
+ await runConfigPrint();
152
+ },
153
+ )
154
+ .command(
155
+ 'init',
156
+ 'Write a starter config file',
157
+ () => {},
158
+ async () => {
159
+ await runConfigInit();
160
+ },
161
+ )
162
+ .command(
163
+ 'validate',
164
+ 'Validate resolved config',
165
+ () => {},
166
+ async () => {
167
+ await runConfigValidate();
168
+ },
169
+ )
170
+ .demandCommand(1, 'Specify a subcommand'),
171
+ () => {},
172
+ )
173
+ .command(
174
+ 'models',
175
+ 'Model metadata commands',
176
+ (yargsBuilder) =>
177
+ yargsBuilder
178
+ .command(
179
+ 'search [query]',
180
+ 'Search models',
181
+ (sub) =>
182
+ sub
183
+ .positional('query', { type: 'string' })
184
+ .option('max-items', {
185
+ type: 'number',
186
+ default: 10,
187
+ describe: 'Max items to display in autocomplete',
188
+ }),
189
+ async (argv: ModelsSearchArgs) => {
190
+ await runModelsSearch(argv.query, argv.maxItems ?? 10);
191
+ },
192
+ )
193
+ .command(
194
+ 'info <modelId>',
195
+ 'Show model info',
196
+ (sub) => sub.positional('modelId', { type: 'string', demandOption: true }),
197
+ async (argv: ModelsInfoArgs) => {
198
+ await runModelsInfo(argv.modelId);
199
+ },
200
+ )
201
+ .demandCommand(1, 'Specify a subcommand'),
202
+ () => {},
203
+ )
204
+ .strict()
205
+ .help();
206
+
207
+ await cli.parse();