import { Option, type Command } from "commander"; import { getCommandArgsWithRootOptions } from "../infra/cli-root-options.js"; import { createLazyPromise } from "../shared/lazy-promise.js"; import { normalizeWindowsArgv } from "./windows-argv.js"; type ChannelSetupFlagArity = "boolean" | "value" | "conflict"; export type ChannelSetupCliOption = { flags: string; negatedFlags?: string; description: string; defaultValue?: boolean | string; }; const CHANNEL_ADD_SHARED_BOOLEAN_OPTIONS = new Set(["--help", "-h"]); const CHANNEL_ADD_SHARED_VALUE_OPTIONS = new Set(["--agent", "--channel", "--account", "--name"]); const CHANNEL_ADD_SHARED_VALUE_OPTION_PREFIXES = [...CHANNEL_ADD_SHARED_VALUE_OPTIONS].map( (flag) => `${flag}=`, ); export const loadChannelSetupCliOptions = createLazyPromise( () => import("../channels/plugins/cli-add-options.js"), ); export function getChannelSetupOptionSwitches(option: Option): string[] { return [option.short, option.long].filter((flag): flag is string => Boolean(flag)); } function resolveChannelSetupFlagArity(flags: string): Exclude { return /<[^>]+>|\[[^\]]+\]/u.test(flags) ? "value" : "boolean"; } function buildChannelSetupFlagArityMap( options: readonly ChannelSetupCliOption[], ): Map { const arityBySwitch = new Map(); const addSwitch = (flag: string, arity: Exclude) => { const existing = arityBySwitch.get(flag); arityBySwitch.set(flag, existing === undefined || existing === arity ? arity : "conflict"); }; for (const option of options) { const arity = resolveChannelSetupFlagArity(option.flags); for (const flag of getChannelSetupOptionSwitches(new Option(option.flags))) { addSwitch(flag, arity); } if (option.negatedFlags) { for (const flag of getChannelSetupOptionSwitches(new Option(option.negatedFlags))) { addSwitch(flag, "boolean"); } } } return arityBySwitch; } export async function resolveChannelsAddChannelFromArgv( argv: string[], ): Promise { const normalizedArgv = normalizeWindowsArgv(argv); const args = getCommandArgsWithRootOptions(normalizedArgv, { commandPath: ["channels", "add"], valueFlags: ["--agent"], mode: "command-path", }); if (!args) { return undefined; } let explicitChannel: string | undefined; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (!arg || arg === "--") { break; } if (arg === "--channel") { const value = args[index + 1]?.trim(); explicitChannel = value || explicitChannel; index += 1; continue; } if (arg.startsWith("--channel=")) { const value = arg.slice("--channel=".length).trim(); explicitChannel = value || explicitChannel; } } if (explicitChannel) { return explicitChannel; } let channelFlagArities: Map | undefined; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (!arg || arg === "--") { break; } if (CHANNEL_ADD_SHARED_VALUE_OPTIONS.has(arg)) { index += 1; continue; } if (CHANNEL_ADD_SHARED_VALUE_OPTION_PREFIXES.some((prefix) => arg.startsWith(prefix))) { continue; } if (CHANNEL_ADD_SHARED_BOOLEAN_OPTIONS.has(arg)) { continue; } if (arg.startsWith("-")) { // Shipped `channels add` accepted channel flags before a positional id by registering every // channel option. Lazily inspect serialized all-channel metadata for arity only; actual // option registration remains scoped to the selected channel. if (!channelFlagArities) { const { resolveChannelSetupCliOptionMetadata } = await loadChannelSetupCliOptions(); const { optionCandidates } = resolveChannelSetupCliOptionMetadata(undefined, { includeAll: true, }); channelFlagArities = buildChannelSetupFlagArityMap(optionCandidates); } const equalsIndex = arg.indexOf("="); const optionSwitch = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex); const arity = channelFlagArities.get(optionSwitch); if (!arity || arity === "conflict") { return undefined; } if (equalsIndex === -1 && arity === "value") { index += 1; } continue; } return arg; } return undefined; } export function resolveChannelsAddOptions( channelArg: string | undefined, opts: Record, command?: Pick, params?: { preserveLegacyDefaults?: boolean; /** Attribute names whose empty Commander defaults should be dropped (legacy int options). */ dropEmptyLegacyDefaultsForAttributeNames?: ReadonlySet; }, ): Record { const forwardedOpts = command ? Object.fromEntries( Object.entries(opts).filter(([key, value]) => { const source = command.getOptionValueSource(key); if (source === "cli") { return true; } // Legacy plugins still install manifest defaults through Commander. // Keep those values. Drop empty-string defaults only for int options so // omitted ints are not treated as explicitly blank user input, while // empty text defaults remain valid plugin metadata. if ( params?.preserveLegacyDefaults !== true || source !== "default" || value === undefined ) { return false; } if (value === "") { return !params.dropEmptyLegacyDefaultsForAttributeNames?.has(key); } return true; }), ) : opts; return { ...forwardedOpts, channel: forwardedOpts.channel ?? channelArg, }; }