-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathfig.ts
More file actions
123 lines (106 loc) · 3.19 KB
/
fig.ts
File metadata and controls
123 lines (106 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import type { CommandDef, ArgsDef, PositionalArgDef, CommandMeta } from 'citty';
interface FigSpec {
name: string;
description: string;
options?: FigOption[];
subcommands?: FigSubcommand[];
args?: FigArg[];
}
interface FigOption {
name: string;
description: string;
args?: FigArg[];
isRequired?: boolean;
}
interface FigSubcommand {
name: string;
description: string;
options?: FigOption[];
subcommands?: FigSubcommand[];
args?: FigArg[];
}
interface FigArg {
name: string;
description?: string;
isOptional?: boolean;
isVariadic?: boolean;
suggestions?: FigSuggestion[];
}
interface FigSuggestion {
name: string;
description?: string;
}
async function processArgs<T extends ArgsDef>(
args: T
): Promise<{ options: FigOption[]; args: FigArg[] }> {
const options: FigOption[] = [];
const positionalArgs: FigArg[] = [];
for (const [name, arg] of Object.entries(args)) {
if (arg.type === 'positional') {
const positionalArg = arg as PositionalArgDef;
positionalArgs.push({
name,
description: positionalArg.description,
isOptional: !positionalArg.required,
// Assume variadic if the name suggests it (e.g. [...files])
isVariadic: name.startsWith('[...') || name.startsWith('<...'),
});
} else {
const option: FigOption = {
name: `--${name}`,
description: arg.description || '',
isRequired: arg.required,
};
if ('alias' in arg && arg.alias) {
// Handle both string and array aliases
const aliases = Array.isArray(arg.alias) ? arg.alias : [arg.alias];
aliases.forEach((alias) => {
options.push({
...option,
name: `-${alias}`,
});
});
}
options.push(option);
}
}
return { options, args: positionalArgs };
}
async function processCommand<T extends ArgsDef>(
command: CommandDef<T>,
parentName = ''
): Promise<FigSpec> {
const resolvedMeta = await Promise.resolve(command.meta);
const meta = resolvedMeta as CommandMeta;
const subCommands = await Promise.resolve(command.subCommands);
if (!meta || !meta.name) {
throw new Error('Command meta or name is missing');
}
const spec: FigSpec = {
name: parentName ? `${parentName} ${meta.name}` : meta.name,
description: meta.description || '',
};
if (command.args) {
const resolvedArgs = await Promise.resolve(command.args);
// Cast to ArgsDef since we know the resolved value will be compatible
const { options, args } = await processArgs(resolvedArgs as ArgsDef);
if (options.length > 0) spec.options = options;
if (args.length > 0) spec.args = args;
}
if (subCommands) {
spec.subcommands = await Promise.all(
Object.entries(subCommands).map(async ([_, subCmd]) => {
const resolved = await Promise.resolve(subCmd);
return processCommand(resolved, spec.name);
})
);
}
return spec;
}
// TODO: this should be an extension of t.setup function and not something like this.
export async function generateFigSpec<T extends ArgsDef>(
command: CommandDef<T>
): Promise<string> {
const spec = await processCommand(command);
return JSON.stringify(spec, null, 2);
}