1/**
2 * CommandDescriptor — pure-data representation of a command tree for
3 * shell completion generation.
4 *
5 * @internal
6 */
7import * as Option from "../../../Option.ts"
8import type { Command } from "../../Command.ts"
9import type * as Completions from "../../Completions.ts"
10import * as Param from "../../Param.ts"
11import * as Primitive from "../../Primitive.ts"
12import { toImpl } from "../command.ts"
13
14// ---------------------------------------------------------------------------
15// Helpers
16// ---------------------------------------------------------------------------
17
18const toFlagType = (single: Param.Single<"flag", unknown>): Completions.FlagType => {
19 const tag = single.primitiveType._tag
20 switch (tag) {
21 case "Boolean":
22 return { _tag: "Boolean" }
23 case "Int":
24 return { _tag: "Int" }
25 case "Finite":
26 return { _tag: "Finite" }
27 case "Date":
28 return { _tag: "Date" }
29 case "Choice": {
30 const keys = Primitive.getChoiceKeys(single.primitiveType)
31 return { _tag: "Choice", values: keys ?? [] }
32 }
33 case "Path":
34 return { _tag: "Path", pathType: Primitive.getPathType(single.primitiveType) ?? "either" }
35 case "FileText":
36 case "FileParse":
37 case "FileSchema":
38 return { _tag: "Path", pathType: "file" }
39 default:
40 return { _tag: "String" }
41 }
42}
43
44const toArgumentType = (single: Param.Single<"argument", unknown>): Completions.ArgumentType => {
45 const tag = single.primitiveType._tag
46 switch (tag) {
47 case "Int":
48 return { _tag: "Int" }
49 case "Finite":
50 return { _tag: "Finite" }
51 case "Date":
52 return { _tag: "Date" }
53 case "Choice": {
54 const keys = Primitive.getChoiceKeys(single.primitiveType)
55 return { _tag: "Choice", values: keys ?? [] }
56 }
57 case "Path":
58 return { _tag: "Path", pathType: Primitive.getPathType(single.primitiveType) ?? "either" }
59 case "FileText":
60 case "FileParse":
61 case "FileSchema":
62 return { _tag: "Path", pathType: "file" }
63 default:
64 return { _tag: "String" }
65 }
66}
67
68// ---------------------------------------------------------------------------
69// Extraction
70// ---------------------------------------------------------------------------
71
72/**
73 * @internal
74 */
75export const fromCommand = (
76 cmd: Command.Any,
77 inheritedFlags: ReadonlyArray<Param.AnyFlag> = []
78): Completions.CommandDescriptor => {
79 const impl = toImpl(cmd)
80 const config = impl.config
81
82 const flags: Array<Completions.FlagDescriptor> = []
83 const seen = new Set<string>()
84 // Match help's precedence: local flags first, then ancestors root to leaf.
85 for (const [index, flag] of [...config.flags, ...inheritedFlags].entries()) {
86 const singles = Param.extractSingleParams(flag)
87 for (const single of singles) {
88 if (single.kind !== "flag") continue
89 // Omit hidden flags from completion scripts so tab-completion in the
90 // shell does not advertise flags that are absent from --help.
91 if (single.hidden) continue
92 if (index >= config.flags.length && seen.has(single.name)) continue
93 seen.add(single.name)
94 flags.push({
95 name: single.name,
96 aliases: single.aliases,
97 description: Option.getOrUndefined(single.description),
98 type: toFlagType(single as Param.Single<"flag", unknown>)
99 })
100 }
101 }
102
103 const args: Array<Completions.ArgumentDescriptor> = []
104 for (const arg of config.arguments) {
105 const singles = Param.extractSingleParams(arg)
106 const metadata = Param.getParamMetadata(arg)
107 for (const single of singles) {
108 if (single.kind !== "argument") continue
109 args.push({
110 name: single.name,
111 description: Option.getOrUndefined(single.description),
112 required: !metadata.isOptional,
113 variadic: metadata.isVariadic,
114 type: toArgumentType(single as Param.Single<"argument", unknown>)
115 })
116 }
117 }
118
119 const subcommands: Array<Completions.CommandDescriptor> = []
120 const sharedFlags = [...inheritedFlags, ...impl.contextConfig.flags]
121 for (const group of cmd.subcommands) {
122 for (const subcommand of group.commands) {
123 // Omit unlisted subcommands from completion scripts so tab-completion in
124 // the shell does not advertise commands that are absent from --help.
125 if (subcommand.unlisted) continue
126 const descriptor = fromCommand(subcommand, sharedFlags)
127 subcommands.push(descriptor)
128 if (subcommand.alias && subcommand.alias !== subcommand.name) {
129 subcommands.push({ ...descriptor, name: subcommand.alias })
130 }
131 }
132 }
133
134 return {
135 name: cmd.name,
136 description: cmd.shortDescription ?? cmd.description,
137 flags,
138 arguments: args,
139 subcommands
140 }
141}