Skip to content

Commit 02e2b65

Browse files
committedMar 3, 2025·
Polish
1 parent 332e52f commit 02e2b65

18 files changed

+133
-74
lines changed
 

‎.changeset/fair-doodles-nail.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"clide-js": patch
3+
---
4+
5+
Fixed prompt types by adding missing `@types/prompts` dependency.

‎.changeset/tender-wings-lick.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"clide-js": patch
3+
---
4+
5+
General types and error formatting polish

‎.vscode/settings.json

-3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,5 @@
2323
},
2424
"[yaml]": {
2525
"editor.defaultFormatter": "biomejs.biome"
26-
},
27-
"[markdown]": {
28-
"editor.defaultFormatter": "biomejs.biome"
2926
}
3027
}

‎biome.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
}
3636
},
3737
"suspicious": {
38-
"noExplicitAny": "warn",
38+
"noExplicitAny": "info",
3939
"noDoubleEquals": "warn"
4040
}
4141
},

‎packages/clide-js/src/cli/commands/foo.ts

-21
This file was deleted.

‎packages/clide-js/src/cli/commands/foo/bar.ts

-14
This file was deleted.
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { command } from 'src/core/command';
2+
3+
export default command({
4+
description: 'Get a greeting',
5+
handler: async ({ client }) => {
6+
const foo = await client.prompt({
7+
type: 'invisible',
8+
message: 'Enter a value',
9+
});
10+
console.log({
11+
foo,
12+
type: typeof foo,
13+
});
14+
},
15+
});

‎packages/clide-js/src/core/client.ts

+33-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
import prompts from 'prompts';
1+
import prompts, { type PromptType, type PromptObject } from 'prompts';
2+
import type { } from 'src/core/options/option';
23
import { ClideError, ClientError } from './errors';
34

45
/**
5-
* Variation of `prompts.PromptObject` with a few changes:
6+
* A variation of {@linkcode PromptObject} with a few changes:
67
* - name must be a string
78
* - message is required
89
* - separator is always ','
910
*
10-
* @see https://github.com/terkelg/prompts#-prompt-objects
11+
* @see [GitHub - terkelg/prompts - Prompt Objects](https://github.com/terkelg/prompts#-prompt-objects)
1112
* @group Client
1213
*/
1314
// TODO: replace with own type?
14-
export type PromptOptions = Omit<
15-
prompts.PromptObject,
15+
export type PromptOptions<T extends PromptType = PromptType> = Omit<
16+
PromptObject,
1617
'name' | 'message' | 'separator' | 'type'
1718
> & {
1819
// make the message property required since prompts throws an error if it's
1920
// not defined.
2021
message: NonNullable<prompts.PromptObject['message']>;
2122
// make the type property optional since we'll default to 'text'
22-
type?: prompts.PromptObject['type'];
23+
type?: T;
2324
};
2425

2526
/**
@@ -35,7 +36,7 @@ export class Client {
3536
* Log a message to stdout.
3637
* @param message - Any number of arguments to log.
3738
*/
38-
log(...message: any) {
39+
log(...message: unknown[]) {
3940
console.log(...message);
4041
}
4142

@@ -71,7 +72,9 @@ export class Client {
7172
*
7273
* @see https://github.com/terkelg/prompts#-prompt-objects
7374
*/
74-
async prompt(prompt: PromptOptions): Promise<any> {
75+
async prompt<T extends PromptType = PromptType>(
76+
prompt: PromptOptions<T>,
77+
): Promise<PromptTypeMap[T]> {
7578
const { value } = await prompts({
7679
type: 'text',
7780
active: 'yes',
@@ -83,3 +86,25 @@ export class Client {
8386
return value;
8487
}
8588
}
89+
90+
type PromptTypeMap = PromptTypeMapDef<{
91+
text: string;
92+
password: string;
93+
invisible: string;
94+
number: number;
95+
confirm: boolean;
96+
list: string[];
97+
toggle: boolean;
98+
select: string;
99+
multiselect: string[];
100+
autocomplete: string;
101+
date: Date;
102+
autocompleteMultiselect: string[];
103+
}>;
104+
105+
/**
106+
* Ensures the type map is up-to-date. If any types are missing, a type error
107+
* will be thrown.
108+
*/
109+
type PromptTypeMapDef<T extends Record<PromptType, unknown>> = T;
110+
// & Record<string, unknown>;

‎packages/clide-js/src/core/context.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import {
2-
parseCommand,
32
type OptionValues,
43
type ParseCommandFn,
4+
parseCommand,
55
} from 'src/core/parse';
66
import { Client } from './client';
77
import { ClideError, RequiredSubcommandError } from './errors';
88
import { HooksEmitter } from './hooks';
99
import type { OptionsConfig } from './options/option';
1010
import type { Plugin, PluginInfo } from './plugin';
1111
import {
12-
resolveCommand,
1312
type ResolveCommandFn,
1413
type ResolvedCommand,
14+
resolveCommand,
1515
} from './resolve';
1616
import { State } from './state';
1717

@@ -189,6 +189,7 @@ export class Context<TOptions extends OptionsConfig = OptionsConfig> {
189189
// 1. Initialize plugins
190190
for (const { name, init } of this._plugins) {
191191
const pluginInfo = this.plugins[name];
192+
if (!pluginInfo) continue;
192193
pluginInfo.isReady = await init(this);
193194
Object.freeze(pluginInfo);
194195
}

‎packages/clide-js/src/core/errors.ts

+50-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
1-
interface ClideErrorOptions {
2-
cause?: unknown;
1+
export interface ClideErrorOptions extends ErrorOptions {
2+
/**
3+
* A custom prefix to use in place of {@linkcode DriftError.prefix}.
4+
*/
5+
prefix?: string;
6+
7+
/**
8+
* A custom name to use in place of {@linkcode DriftError.name}.
9+
*/
10+
name?: string;
311
}
412

513
/**
614
* An error thrown by the CLI engine.
15+
*
16+
* This error is designed to ensure clean stack trace formatting even when
17+
* minified and can be extended to create other error types with the same
18+
* behavior.
19+
*
20+
* @example
21+
* ```ts
22+
* class FooCliError extends ClideError {
23+
* constructor(message: string, options?: ErrorOptions) {
24+
* super(message, {
25+
* ...options,
26+
* prefix: "🚨 ",
27+
* name: "Foo CLI Error",
28+
* });
29+
* }
30+
* }
31+
*
32+
* throw new FooCliError("Something went wrong");
33+
* // 🚨 Foo CLI Error: Something went wrong
34+
* // at ...
35+
* ```
36+
*
737
* @group Errors
838
*/
939
export class ClideError extends Error {
1040
static prefix = '✖ ';
41+
static name = 'CLI Error' as const;
1142

1243
constructor(error: any, options?: ClideErrorOptions) {
1344
// Coerce the error to a string, or throw the original error if unable.
@@ -19,7 +50,7 @@ export class ClideError extends Error {
1950
}
2051

2152
super(message);
22-
this.name = 'CLI Error';
53+
this.name = options?.name ?? ClideError.name;
2354

2455
// Minification can mangle the stack traces of custom errors by obfuscating
2556
// the class name and including large chunks of minified code in the output.
@@ -43,28 +74,38 @@ export class ClideError extends Error {
4374
customName = error.constructor.name;
4475
}
4576

46-
// Doing this in constructor prevents the need to add custom properties to
47-
// the prototype, which would be displayed in the stack trace. The getter
77+
// Doing this in the constructor prevents the need to add custom properties
78+
// to the prototype, which would be displayed in the stack trace. The getter
4879
// ensures the name and message are up-to-date when accessed (e.g., after
4980
// subclassing and changing the name).
5081
Object.defineProperty(this, 'stack', {
5182
get(): string {
52-
let stack = `${ClideError.prefix}${this.name}`;
83+
let stack = `${options?.prefix ?? ClideError.prefix}${this.name}`;
5384

5485
if (customName) {
5586
stack += ` [${customName}]`;
5687
}
5788

5889
if (this.message) {
59-
stack += `: ${this.message}`;
90+
stack += `: ${this.message.replaceAll('\n', '\n ')}`;
6091
}
6192

6293
if (stackTarget.stack) {
63-
stack += `\n${stackTarget.stack.replace(/^.*\n/, '')}`;
94+
const stackLines = stackTarget.stack
95+
.replace(this.message, '')
96+
.split('\n')
97+
.slice(1)
98+
.join('\n');
99+
if (stackLines) {
100+
stack += `\n${stackLines}`;
101+
}
64102
}
65103

66104
if (cause) {
67-
stack += `\n Caused by: ${cause.stack || cause}`;
105+
stack += `\nCaused by: ${cause.stack || cause}`.replaceAll(
106+
'\n',
107+
'\n ',
108+
);
68109
}
69110

70111
return stack.trim();

‎packages/clide-js/src/core/help.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import initCliui from 'cliui';
21
import fs from 'node:fs';
2+
import initCliui from 'cliui';
33
import { getBin } from 'src/utils/argv';
4-
import { convert, type Converted } from 'src/utils/convert';
4+
import { type Converted, convert } from 'src/utils/convert';
55
import { isDirectory } from 'src/utils/fs';
66
import { parseFileName } from 'src/utils/parse-file-name';
77
import { removeFileExtension } from 'src/utils/remove-file-extension';
@@ -115,9 +115,8 @@ export async function getHelp({
115115
const allOptions: OptionsConfig = { ...context.options };
116116

117117
// Get the last resolved command
118-
const finalResolved = context.resolvedCommands[
119-
context.resolvedCommands.length - 1
120-
] as ResolvedCommand | undefined;
118+
const finalResolved =
119+
context.resolvedCommands[context.resolvedCommands.length - 1];
121120

122121
// Build up the usage string based on the resolved commands
123122
for (const resolved of context.resolvedCommands) {

‎packages/clide-js/src/core/options/option-getter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export function createOptionGetter<
210210
}
211211
}
212212

213-
value = await client.prompt(promptOptions);
213+
value = (await client.prompt(promptOptions)) as TValue;
214214
if (didCancel) {
215215
onPromptCancel?.();
216216
}

‎packages/clide-js/src/core/resolve.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ export async function resolveCommand({
6565
}: ResolveCommandOptions): Promise<ResolvedCommand> {
6666
if (!commandString.length) throw new UsageError('Command required.');
6767

68-
const [commandName, ...remainingTokens] = commandString.split(' ');
68+
const [commandName, ...remainingTokens] = commandString.split(' ') as [
69+
string,
70+
...string[],
71+
];
6972

7073
// Check if the first token is an option.
7174
if (commandName.startsWith('-')) {
@@ -153,8 +156,10 @@ async function resolveParamCommand({
153156
commandsDir,
154157
parseFn = parseCommand,
155158
}: ResolveCommandOptions): Promise<ResolvedCommand | undefined> {
159+
if (!commandString.length) throw new UsageError('Command required.');
160+
156161
const fileNames = await fs.promises.readdir(commandsDir);
157-
let tokens = commandString.split(' ');
162+
let tokens = commandString.split(' ') as [string, ...string[]];
158163
let resolved: ResolvedCommand | undefined;
159164

160165
// optimization opportunities:
@@ -189,7 +194,7 @@ async function resolveParamCommand({
189194
// Parse the command string to separate the tokens from the options.
190195
if (command.options) {
191196
const parsedString = await parseFn(commandString, command.options);
192-
tokens = parsedString.tokens;
197+
tokens = parsedString.tokens as [string, ...string[]];
193198
}
194199

195200
// If the param has a spread operator (e.g., [...param].ts), then pass
@@ -274,7 +279,7 @@ export async function prepareResolvedCommand(
274279
} else {
275280
// Otherwise, remove the leading options.
276281
const indexOfNextCommand = resolved.remainingCommandString.indexOf(
277-
tokens[0],
282+
tokens[0]!,
278283
);
279284
resolved.remainingCommandString =
280285
resolved.remainingCommandString.slice(indexOfNextCommand);

‎packages/clide-js/src/utils/argv.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @group Utils
44
*/
55
export function getBin() {
6-
return process.argv[getBinIndex()];
6+
return process.argv[getBinIndex()] || '';
77
}
88

99
/**

‎packages/clide-js/src/utils/caller-path.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export function getCallerPath(): string | undefined {
5757
// If the stack is a string, we're in a non-v8 environment and need to parse
5858
// the stack string to get the file path.
5959
// https://regex101.com/r/yyMI5W/1
60-
const callerLine = target.stack.split('\n')[callerIndex].trim();
61-
const callerFile = callerLine.match(/(file|\/)[^\s]+?(?=(:\d|\)))/)?.[0];
60+
const callerLine = target.stack.split('\n')[callerIndex]?.trim();
61+
const callerFile = callerLine?.match(/(file|\/)[^\s]+?(?=(:\d|\)))/)?.[0];
6262

6363
if (!callerFile) return undefined;
6464

‎packages/clide-js/src/utils/find-similar.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function findSimilar(
4848

4949
for (const [i, score] of distances.entries()) {
5050
if (score <= threshold) {
51-
similar.push(choices[i]);
51+
similar.push(choices[i]!);
5252
if (similar.length >= maxResults) {
5353
break;
5454
}

‎packages/clide-js/test/utils/command-modules.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const modules = {
2121

2222
describe('mockCommandModule', () => {
2323
it('Mocks and unmocks single command modules', async () => {
24-
const [path] = Object.entries(modules)[0];
24+
const [path] = Object.entries(modules)[0]!;
2525

2626
const { mock, unmock } = mockCommandModule(path);
2727
expect(mock).toMatchObject({
@@ -39,7 +39,7 @@ describe('mockCommandModule', () => {
3939
});
4040

4141
it('Uses the provided module', async () => {
42-
const [path, module] = Object.entries(modules)[0];
42+
const [path, module] = Object.entries(modules)[0]!;
4343

4444
const { mock } = mockCommandModule(path, {
4545
handler: module.handler,

‎packages/clide-js/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"skipLibCheck": true,
1414
"noUnusedLocals": true,
1515
"noUnusedParameters": true,
16+
"noUncheckedIndexedAccess": true,
1617
"baseUrl": ".",
1718
"rootDir": ".",
1819
"outDir": "dist",

0 commit comments

Comments
 (0)
Please sign in to comment.