File size: 5,926 Bytes
34810d2 | 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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | 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<ChannelSetupFlagArity, "conflict"> {
return /<[^>]+>|\[[^\]]+\]/u.test(flags) ? "value" : "boolean";
}
function buildChannelSetupFlagArityMap(
options: readonly ChannelSetupCliOption[],
): Map<string, ChannelSetupFlagArity> {
const arityBySwitch = new Map<string, ChannelSetupFlagArity>();
const addSwitch = (flag: string, arity: Exclude<ChannelSetupFlagArity, "conflict">) => {
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<string | undefined> {
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<string, ChannelSetupFlagArity> | 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<string, unknown>,
command?: Pick<Command, "getOptionValueSource">,
params?: {
preserveLegacyDefaults?: boolean;
/** Attribute names whose empty Commander defaults should be dropped (legacy int options). */
dropEmptyLegacyDefaultsForAttributeNames?: ReadonlySet<string>;
},
): Record<string, unknown> {
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,
};
}
|