1/**
2 * Help Documentation
3 * ================
4 *
5 * Internal helpers for generating help documentation.
6 * Extracted from command.ts to avoid circular dependencies.
7 */
8import * as Effect from "../../Effect.ts"
9import type { Command } from "../Command.ts"
10import type * as GlobalFlag from "../GlobalFlag.ts"
11import type { FlagDoc, HelpDoc } from "../HelpDoc.ts"
12import * as Param from "../Param.ts"
13import { toFlagDoc, toImpl } from "./command.ts"
14
15const dedupeGlobalFlags = (
16 flags: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
17): ReadonlyArray<GlobalFlag.GlobalFlag<any>> => {
18 const seen = new Set<GlobalFlag.GlobalFlag<any>>()
19 const deduped: Array<GlobalFlag.GlobalFlag<any>> = []
20 for (const flag of flags) {
21 if (seen.has(flag)) {
22 continue
23 }
24 seen.add(flag)
25 deduped.push(flag)
26 }
27 return deduped
28}
29
30/**
31 * Returns the resolved command lineage for the provided path.
32 * Includes the root command as the first element.
33 */
34export const getCommandsForCommandPath = <Name extends string, Input, E, R, ContextInput>(
35 command: Command<Name, Input, ContextInput, E, R>,
36 commandPath: ReadonlyArray<string>
37): ReadonlyArray<Command.Any> => {
38 const commands: Array<Command.Any> = [command]
39 let currentCommand: Command.Any = command
40
41 for (let i = 1; i < commandPath.length; i++) {
42 const subcommandName = commandPath[i]
43 let subcommand: Command.Any | undefined = undefined
44
45 for (const group of currentCommand.subcommands) {
46 subcommand = group.commands.find((sub) => sub.name === subcommandName)
47 if (subcommand) {
48 break
49 }
50 }
51
52 if (!subcommand) {
53 break
54 }
55
56 commands.push(subcommand)
57 currentCommand = subcommand
58 }
59
60 return commands
61}
62
63/**
64 * Returns active global flags for a command path.
65 * Built-ins are prepended and declarations are collected root -> leaf.
66 */
67export const getGlobalFlagsForCommandPath = <Name extends string, Input, E, R, ContextInput>(
68 command: Command<Name, Input, ContextInput, E, R>,
69 commandPath: ReadonlyArray<string>,
70 builtIns: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
71): ReadonlyArray<GlobalFlag.GlobalFlag<any>> => {
72 const commands = getCommandsForCommandPath(command, commandPath)
73 const declared = commands.flatMap((current) => toImpl(current).globalFlags)
74 return dedupeGlobalFlags([
75 ...builtIns,
76 ...declared
77 ])
78}
79
80const collectDeclaredGlobalFlags = (command: Command.Any): ReadonlyArray<GlobalFlag.GlobalFlag<any>> => {
81 const collected: Array<GlobalFlag.GlobalFlag<any>> = []
82
83 const visit = (current: Command.Any): void => {
84 const impl = toImpl(current)
85 for (const flag of impl.globalFlags) {
86 collected.push(flag)
87 }
88 for (const group of current.subcommands) {
89 for (const subcommand of group.commands) {
90 visit(subcommand)
91 }
92 }
93 }
94
95 visit(command)
96 return dedupeGlobalFlags(collected)
97}
98
99const getSharedFlagsForCommandPath = (
100 commands: ReadonlyArray<Command.Any>,
101 currentFlags: ReadonlyArray<FlagDoc>
102): ReadonlyArray<FlagDoc> => {
103 if (commands.length <= 1) {
104 return []
105 }
106
107 const seen = new Set(currentFlags.map((flag) => flag.name))
108 const sharedFlags: Array<FlagDoc> = []
109
110 for (const ancestor of commands.slice(0, -1)) {
111 const ancestorImpl = toImpl(ancestor)
112 for (const flag of ancestorImpl.contextConfig.flags) {
113 const singles = Param.extractSingleParams(flag)
114 const metadata = Param.getParamMetadata(flag)
115 for (const single of singles) {
116 if (seen.has(single.name)) {
117 continue
118 }
119 // Hidden ancestor flags are excluded from the shared-flags section
120 // of subcommand help, the same way they're excluded from the owning
121 // command's own help output.
122 if (single.hidden) {
123 continue
124 }
125 seen.add(single.name)
126 sharedFlags.push(toFlagDoc(single, metadata))
127 }
128 }
129 }
130
131 return sharedFlags
132}
133
134/**
135 * Returns all global flags declared in a command tree.
136 * Built-ins are prepended and command declarations are deduplicated by identity.
137 */
138export const getGlobalFlagsForCommandTree = <Name extends string, Input, E, R, ContextInput>(
139 command: Command<Name, Input, ContextInput, E, R>,
140 builtIns: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
141): ReadonlyArray<GlobalFlag.GlobalFlag<any>> =>
142 dedupeGlobalFlags([
143 ...builtIns,
144 ...collectDeclaredGlobalFlags(command)
145 ])
146
147/**
148 * Helper function to get help documentation for a specific command path.
149 * Navigates through the command hierarchy to find the right command.
150 * Reads active global flags for the path and includes them in the help doc.
151 */
152export const getHelpForCommandPath = <Name extends string, Input, E, R, ContextInput>(
153 command: Command<Name, Input, ContextInput, E, R>,
154 commandPath: ReadonlyArray<string>,
155 builtIns: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
156): Effect.Effect<HelpDoc, never, never> =>
157 Effect.gen(function*() {
158 const commands = getCommandsForCommandPath(command, commandPath)
159 const currentCommand = commands.length > 0 ? commands[commands.length - 1] : command
160
161 const baseDoc = toImpl(currentCommand).buildHelpDoc(commandPath)
162
163 const sharedFlags = getSharedFlagsForCommandPath(commands, baseDoc.flags)
164
165 const flags = getGlobalFlagsForCommandPath(command, commandPath, builtIns)
166 const globalFlagDocs: Array<FlagDoc> = []
167 for (const flag of flags) {
168 const singles = Param.extractSingleParams(flag.flag)
169 const metadata = Param.getParamMetadata(flag.flag)
170 for (const single of singles) {
171 // Same rule as command-local flags: hidden globals are still parsed
172 // and still trigger their handlers, they just don't appear under
173 // "GLOBAL FLAGS" in --help.
174 if (single.hidden) continue
175 globalFlagDocs.push({
176 ...toFlagDoc(single, metadata),
177 required: false
178 })
179 }
180 }
181
182 return { ...baseDoc, flags: [...sharedFlags, ...baseDoc.flags], globalFlags: globalFlagDocs }
183 })