refactor: type parsed flag values and remove entrypoint casts (#1153)

This commit is contained in:
Devansh Jain
2026-02-25 19:48:02 -08:00
committed by GitHub
parent 573ac5721c
commit a0fffeae35
4 changed files with 105 additions and 86 deletions

View File

@@ -271,14 +271,34 @@ export const CLI_FLAG_CATALOG = {
"max-turns": { parser: { type: "string" }, mode: "headless" },
} as const satisfies Record<string, CliFlagDefinition>;
type CliFlagCatalog = typeof CLI_FLAG_CATALOG;
type CliCatalogOptionDescriptors = {
[K in keyof CliFlagCatalog]: CliFlagCatalog[K]["parser"];
};
type CliParsedValueForDescriptor<Descriptor extends CliFlagParserConfig> =
Descriptor["type"] extends "boolean"
? Descriptor["multiple"] extends true
? boolean[]
: boolean
: Descriptor["multiple"] extends true
? string[]
: string;
export type CliParsedValues = {
[K in keyof CliCatalogOptionDescriptors]?: CliParsedValueForDescriptor<
CliCatalogOptionDescriptors[K]
>;
};
const CLI_FLAG_ENTRIES = Object.entries(CLI_FLAG_CATALOG) as Array<
[string, CliFlagDefinition]
[keyof CliFlagCatalog, CliFlagDefinition]
>;
export const CLI_OPTIONS: Record<string, CliFlagParserConfig> =
Object.fromEntries(
CLI_FLAG_ENTRIES.map(([name, definition]) => [name, definition.parser]),
);
export const CLI_OPTIONS = Object.fromEntries(
CLI_FLAG_ENTRIES.map(([name, definition]) => [name, definition.parser]),
) as CliCatalogOptionDescriptors;
// Column width for left-aligned flag labels in generated --help output.
const HELP_LABEL_WIDTH = 24;
@@ -334,12 +354,16 @@ export function preprocessCliArgs(args: string[]): string[] {
}
export function parseCliArgs(args: string[], strict: boolean) {
return parseArgs({
const parsed = parseArgs({
args,
options: CLI_OPTIONS,
strict,
allowPositionals: true,
});
return {
...parsed,
values: parsed.values as CliParsedValues,
};
}
export type ParsedCliArgs = ReturnType<typeof parseCliArgs>;

View File

@@ -151,11 +151,11 @@ type ReflectionOverrides = {
};
function parseReflectionOverrides(
values: Record<string, unknown>,
values: ParsedCliArgs["values"],
): ReflectionOverrides {
const triggerRaw = values["reflection-trigger"] as string | undefined;
const behaviorRaw = values["reflection-behavior"] as string | undefined;
const stepCountRaw = values["reflection-step-count"] as string | undefined;
const triggerRaw = values["reflection-trigger"];
const behaviorRaw = values["reflection-behavior"];
const stepCountRaw = values["reflection-step-count"];
if (!triggerRaw && !behaviorRaw && !stepCountRaw) {
return {};
@@ -275,11 +275,11 @@ export async function handleHeadlessCommand(
// Set tool filter if provided (controls which tools are loaded)
if (values.tools !== undefined) {
const { toolFilter } = await import("./tools/filter");
toolFilter.setEnabledTools(values.tools as string);
toolFilter.setEnabledTools(values.tools);
}
// Set permission mode if provided (or via --yolo alias)
const permissionModeValue = values["permission-mode"] as string | undefined;
const yoloMode = values.yolo as boolean | undefined;
const permissionModeValue = values["permission-mode"];
const yoloMode = values.yolo;
if (yoloMode || permissionModeValue) {
const { permissionMode } = await import("./permissions/mode");
if (yoloMode) {
@@ -307,15 +307,15 @@ export async function handleHeadlessCommand(
if (values.allowedTools || values.disallowedTools) {
const { cliPermissions } = await import("./permissions/cli");
if (values.allowedTools) {
cliPermissions.setAllowedTools(values.allowedTools as string);
cliPermissions.setAllowedTools(values.allowedTools);
}
if (values.disallowedTools) {
cliPermissions.setDisallowedTools(values.disallowedTools as string);
cliPermissions.setDisallowedTools(values.disallowedTools);
}
}
// Check for input-format early - if stream-json, we don't need a prompt
const inputFormat = values["input-format"] as string | undefined;
const inputFormat = values["input-format"];
const isBidirectionalMode = inputFormat === "stream-json";
// If headless output is being piped and the downstream closes early (e.g.
@@ -372,38 +372,35 @@ export async function handleHeadlessCommand(
}
// --new: Create a new conversation (for concurrent sessions)
let forceNewConversation = (values.new as boolean | undefined) ?? false;
const fromAgentId = values["from-agent"] as string | undefined;
let forceNewConversation = values.new ?? false;
const fromAgentId = values["from-agent"];
// Resolve agent (same logic as interactive mode)
let agent: AgentState | null = null;
let specifiedAgentId = values.agent as string | undefined;
const specifiedAgentName = values.name as string | undefined;
let specifiedConversationId = values.conversation as string | undefined;
const shouldContinue = values.continue as boolean | undefined;
const forceNew = values["new-agent"] as boolean | undefined;
const systemPromptPreset = values.system as string | undefined;
const systemCustom = values["system-custom"] as string | undefined;
const systemAppend = values["system-append"] as string | undefined;
const embeddingModel = values.embedding as string | undefined;
const memoryBlocksJson = values["memory-blocks"] as string | undefined;
const blockValueArgs = values["block-value"] as string[] | undefined;
const initBlocksRaw = values["init-blocks"] as string | undefined;
const baseToolsRaw = values["base-tools"] as string | undefined;
const skillsDirectory =
(values.skills as string | undefined) ?? skillsDirectoryOverride;
const noSkillsFlag = values["no-skills"] as boolean | undefined;
const noBundledSkillsFlag = values["no-bundled-skills"] as
| boolean
| undefined;
const skillSourcesRaw = values["skill-sources"] as string | undefined;
const memfsFlag = values.memfs as boolean | undefined;
const noMemfsFlag = values["no-memfs"] as boolean | undefined;
let specifiedAgentId = values.agent;
const specifiedAgentName = values.name;
let specifiedConversationId = values.conversation;
const shouldContinue = values.continue;
const forceNew = values["new-agent"];
const systemPromptPreset = values.system;
const systemCustom = values["system-custom"];
const systemAppend = values["system-append"];
const embeddingModel = values.embedding;
const memoryBlocksJson = values["memory-blocks"];
const blockValueArgs = values["block-value"];
const initBlocksRaw = values["init-blocks"];
const baseToolsRaw = values["base-tools"];
const skillsDirectory = values.skills ?? skillsDirectoryOverride;
const noSkillsFlag = values["no-skills"];
const noBundledSkillsFlag = values["no-bundled-skills"];
const skillSourcesRaw = values["skill-sources"];
const memfsFlag = values.memfs;
const noMemfsFlag = values["no-memfs"];
// Startup policy for the git-backed memory pull on session init.
// "blocking" (default): await the pull before proceeding.
// "background": fire the pull async, emit init without waiting.
// "skip": skip the pull entirely this session.
const memfsStartupRaw = values["memfs-startup"] as string | undefined;
const memfsStartupRaw = values["memfs-startup"];
const memfsStartupPolicy: "blocking" | "background" | "skip" =
memfsStartupRaw === "background" || memfsStartupRaw === "skip"
? memfsStartupRaw
@@ -415,13 +412,12 @@ export async function handleHeadlessCommand(
: undefined;
const shouldAutoEnableMemfsForNewAgent = !memfsFlag && !noMemfsFlag;
const fromAfFile = resolveImportFlagAlias({
importFlagValue: values.import as string | undefined,
fromAfFlagValue: values["from-af"] as string | undefined,
importFlagValue: values.import,
fromAfFlagValue: values["from-af"],
});
const preLoadSkillsRaw = values["pre-load-skills"] as string | undefined;
const preLoadSkillsRaw = values["pre-load-skills"];
const systemInfoReminderEnabled =
systemInfoReminderEnabledOverride ??
!(values["no-system-info-reminder"] as boolean | undefined);
systemInfoReminderEnabledOverride ?? !values["no-system-info-reminder"];
const reflectionOverrides = (() => {
try {
return parseReflectionOverrides(values);
@@ -432,8 +428,8 @@ export async function handleHeadlessCommand(
process.exit(1);
}
})();
const maxTurnsRaw = values["max-turns"] as string | undefined;
const tagsRaw = values.tags as string | undefined;
const maxTurnsRaw = values["max-turns"];
const tagsRaw = values.tags;
const resolvedSkillSources = (() => {
if (skillSourcesOverride) {
return skillSourcesOverride;
@@ -1098,8 +1094,7 @@ export async function handleHeadlessCommand(
setAgentContext(agent.id, skillsDirectory, resolvedSkillSources);
// Validate output format
const outputFormat =
(values["output-format"] as string | undefined) || "text";
const outputFormat = values["output-format"] || "text";
const includePartialMessages = Boolean(values["include-partial-messages"]);
if (!["text", "json", "stream-json"].includes(outputFormat)) {
console.error(

View File

@@ -443,19 +443,18 @@ async function main(): Promise<void> {
}
// --continue: Resume last session (agent + conversation) automatically
const shouldContinue = (values.continue as boolean | undefined) ?? false;
const shouldContinue = values.continue ?? false;
// --resume: Open agent selector UI after loading
const shouldResume = (values.resume as boolean | undefined) ?? false;
let specifiedConversationId =
(values.conversation as string | undefined) ?? null; // Specific conversation to resume
const forceNew = (values["new-agent"] as boolean | undefined) ?? false;
const shouldResume = values.resume ?? false;
let specifiedConversationId = values.conversation ?? null; // Specific conversation to resume
const forceNew = values["new-agent"] ?? false;
// --new: Create a new conversation (for concurrent sessions)
const forceNewConversation = (values.new as boolean | undefined) ?? false;
const forceNewConversation = values.new ?? false;
const initBlocksRaw = values["init-blocks"] as string | undefined;
const baseToolsRaw = values["base-tools"] as string | undefined;
let specifiedAgentId = (values.agent as string | undefined) ?? null;
const initBlocksRaw = values["init-blocks"];
const baseToolsRaw = values["base-tools"];
let specifiedAgentId = values.agent ?? null;
try {
const normalized = normalizeConversationShorthandFlags({
specifiedConversationId,
@@ -486,32 +485,26 @@ async function main(): Promise<void> {
process.exit(1);
}
const specifiedAgentName = (values.name as string | undefined) ?? null;
const specifiedModel = (values.model as string | undefined) ?? undefined;
const systemPromptPreset = (values.system as string | undefined) ?? undefined;
const systemCustom =
(values["system-custom"] as string | undefined) ?? undefined;
const specifiedAgentName = values.name ?? null;
const specifiedModel = values.model ?? undefined;
const systemPromptPreset = values.system ?? undefined;
const systemCustom = values["system-custom"] ?? undefined;
// Note: systemAppend is also parsed but only used in headless mode (headless.ts handles it)
const memoryBlocksJson =
(values["memory-blocks"] as string | undefined) ?? undefined;
const specifiedToolset = (values.toolset as string | undefined) ?? undefined;
const skillsDirectory = (values.skills as string | undefined) ?? undefined;
const memfsFlag = values.memfs as boolean | undefined;
const noMemfsFlag = values["no-memfs"] as boolean | undefined;
const memoryBlocksJson = values["memory-blocks"] ?? undefined;
const specifiedToolset = values.toolset ?? undefined;
const skillsDirectory = values.skills ?? undefined;
const memfsFlag = values.memfs;
const noMemfsFlag = values["no-memfs"];
const requestedMemoryPromptMode: "memfs" | "standard" | undefined = memfsFlag
? "memfs"
: noMemfsFlag
? "standard"
: undefined;
const shouldAutoEnableMemfsForNewAgent = !memfsFlag && !noMemfsFlag;
const noSkillsFlag = values["no-skills"] as boolean | undefined;
const noBundledSkillsFlag = values["no-bundled-skills"] as
| boolean
| undefined;
const skillSourcesRaw = values["skill-sources"] as string | undefined;
const noSystemInfoReminderFlag = values["no-system-info-reminder"] as
| boolean
| undefined;
const noSkillsFlag = values["no-skills"];
const noBundledSkillsFlag = values["no-bundled-skills"];
const skillSourcesRaw = values["skill-sources"];
const noSystemInfoReminderFlag = values["no-system-info-reminder"];
const resolvedSkillSources = (() => {
try {
return resolveSkillSourcesSelection({
@@ -527,8 +520,8 @@ async function main(): Promise<void> {
}
})();
const fromAfFile = resolveImportFlagAlias({
importFlagValue: values.import as string | undefined,
fromAfFlagValue: values["from-af"] as string | undefined,
importFlagValue: values.import,
fromAfFlagValue: values["from-af"],
});
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
@@ -865,23 +858,23 @@ async function main(): Promise<void> {
// Set tool filter if provided (controls which tools are loaded)
if (values.tools !== undefined) {
const { toolFilter } = await import("./tools/filter");
toolFilter.setEnabledTools(values.tools as string);
toolFilter.setEnabledTools(values.tools);
}
// Set CLI permission overrides if provided
if (values.allowedTools || values.disallowedTools) {
const { cliPermissions } = await import("./permissions/cli");
if (values.allowedTools) {
cliPermissions.setAllowedTools(values.allowedTools as string);
cliPermissions.setAllowedTools(values.allowedTools);
}
if (values.disallowedTools) {
cliPermissions.setDisallowedTools(values.disallowedTools as string);
cliPermissions.setDisallowedTools(values.disallowedTools);
}
}
// Set permission mode if provided (or via --yolo alias)
const permissionModeValue = values["permission-mode"] as string | undefined;
const yoloMode = values.yolo as boolean | undefined;
const permissionModeValue = values["permission-mode"];
const yoloMode = values.yolo;
if (yoloMode || permissionModeValue) {
if (yoloMode) {

View File

@@ -16,7 +16,14 @@ describe("shared CLI arg schema", () => {
const validModes = new Set(["interactive", "headless", "both"]);
const validTypes = new Set(["boolean", "string"]);
for (const [flagName, definition] of Object.entries(CLI_FLAG_CATALOG)) {
for (const [flagName, definition] of Object.entries(
CLI_FLAG_CATALOG,
) as Array<
[
keyof typeof CLI_FLAG_CATALOG,
(typeof CLI_FLAG_CATALOG)[keyof typeof CLI_FLAG_CATALOG],
]
>) {
expect(validModes.has(definition.mode)).toBe(true);
expect(validTypes.has(definition.parser.type)).toBe(true);
expect(CLI_OPTIONS[flagName]).toEqual(definition.parser);