diff --git a/src/cli/capability-cli/tts.ts b/src/cli/capability-cli/tts.ts index 3207fdf85b46..145434dd3796 100644 --- a/src/cli/capability-cli/tts.ts +++ b/src/cli/capability-cli/tts.ts @@ -18,18 +18,13 @@ import { runTtsVoices, } from "./tts-runtime.js"; -export function registerTtsCapabilityCommands(capability: Command): void { - const tts = capability.command("tts").description("Text to speech"); - - tts - .command("convert") - .description("Convert text to speech") - .requiredOption("--text ", "Input text") - .option("--channel ", "Channel hint") - .option("--voice ", "Voice hint") - .option("--provider ", "Speech provider id") - .option("--model ", "Model override") - .option("--output ", "Output path") +function registerTransportTtsCommand( + command: Command, + defaultTransport: "local" | "gateway", + run: (opts: Record, transport: "local" | "gateway") => Promise, + formatText: (value: unknown) => string = (value) => JSON.stringify(value, null, 2), +): void { + command .option("--local", "Force local execution", false) .option("--gateway", "Force gateway execution", false) .option("--json", "Output JSON", false) @@ -39,33 +34,54 @@ export function registerTtsCapabilityCommands(capability: Command): void { local: Boolean(opts.local), gateway: Boolean(opts.gateway), supported: ["local", "gateway"], - defaultTransport: "local", + defaultTransport, }); - const modelRef = resolveModelRefOverride(opts.model as string | undefined); - if (opts.model && !modelRef.provider) { - throw new Error("TTS model overrides must use the form ."); - } - const provider = normalizeSpeechProviderId( - typeof opts.provider === "string" && opts.provider.trim() - ? opts.provider.trim() - : modelRef.provider, - ); - const modelProvider = normalizeSpeechProviderId(modelRef.provider); - if (provider && modelProvider && provider !== modelProvider) { - throw new Error("TTS --provider must match the provider in --model."); - } - const result = await runTtsConvert({ - text: String(opts.text), - channel: opts.channel as string | undefined, - provider, - modelId: modelProvider ? modelRef.model : undefined, - voiceId: opts.voice as string | undefined, - output: opts.output as string | undefined, - transport, - }); - emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText); + const result = await run(opts, transport); + emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatText); }); }); +} + +export function registerTtsCapabilityCommands(capability: Command): void { + const tts = capability.command("tts").description("Text to speech"); + + registerTransportTtsCommand( + tts + .command("convert") + .description("Convert text to speech") + .requiredOption("--text ", "Input text") + .option("--channel ", "Channel hint") + .option("--voice ", "Voice hint") + .option("--provider ", "Speech provider id") + .option("--model ", "Model override") + .option("--output ", "Output path"), + "local", + async (opts, transport) => { + const modelRef = resolveModelRefOverride(opts.model as string | undefined); + if (opts.model && !modelRef.provider) { + throw new Error("TTS model overrides must use the form ."); + } + const provider = normalizeSpeechProviderId( + typeof opts.provider === "string" && opts.provider.trim() + ? opts.provider.trim() + : modelRef.provider, + ); + const modelProvider = normalizeSpeechProviderId(modelRef.provider); + if (provider && modelProvider && provider !== modelProvider) { + throw new Error("TTS --provider must match the provider in --model."); + } + return await runTtsConvert({ + text: String(opts.text), + channel: opts.channel as string | undefined, + provider, + modelId: modelProvider ? modelRef.model : undefined, + voiceId: opts.voice as string | undefined, + output: opts.output as string | undefined, + transport, + }); + }, + formatEnvelopeForText, + ); tts .command("voices") @@ -79,47 +95,16 @@ export function registerTtsCapabilityCommands(capability: Command): void { }); }); - tts - .command("providers") - .description("List speech providers") - .option("--local", "Force local execution", false) - .option("--gateway", "Force gateway execution", false) - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runCommandWithRuntime(defaultRuntime, async () => { - const transport = resolveTransport({ - local: Boolean(opts.local), - gateway: Boolean(opts.gateway), - supported: ["local", "gateway"], - defaultTransport: "local", - }); - const result = await runTtsProviders(transport); - emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => - JSON.stringify(value, null, 2), - ); - }); - }); - - tts - .command("personas") - .description("List TTS personas") - .option("--local", "Force local execution", false) - .option("--gateway", "Force gateway execution", false) - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runCommandWithRuntime(defaultRuntime, async () => { - const transport = resolveTransport({ - local: Boolean(opts.local), - gateway: Boolean(opts.gateway), - supported: ["local", "gateway"], - defaultTransport: "local", - }); - const result = await runTtsPersonas(transport); - emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => - JSON.stringify(value, null, 2), - ); - }); - }); + for (const [name, description, run] of [ + ["providers", "List speech providers", runTtsProviders], + ["personas", "List TTS personas", runTtsPersonas], + ] as const) { + registerTransportTtsCommand( + tts.command(name).description(description), + "local", + (_, transport) => run(transport), + ); + } tts .command("status") @@ -147,84 +132,45 @@ export function registerTtsCapabilityCommands(capability: Command): void { ["enable", "tts.enable"], ["disable", "tts.disable"], ] as const) { - tts - .command(commandName) - .description(`${commandName === "enable" ? "Enable" : "Disable"} TTS`) - .option("--local", "Force local execution", false) - .option("--gateway", "Force gateway execution", false) - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runCommandWithRuntime(defaultRuntime, async () => { - const transport = resolveTransport({ - local: Boolean(opts.local), - gateway: Boolean(opts.gateway), - supported: ["local", "gateway"], - defaultTransport: "gateway", - }); - const result = await runTtsStateMutation({ - capability: capabilityId, - transport, - }); - emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => - JSON.stringify(value, null, 2), - ); - }); - }); + registerTransportTtsCommand( + tts + .command(commandName) + .description(`${commandName === "enable" ? "Enable" : "Disable"} TTS`), + "gateway", + (_, transport) => runTtsStateMutation({ capability: capabilityId, transport }), + ); } - tts - .command("set-provider") - .description("Set the active TTS provider") - .requiredOption("--provider ", "Speech provider id") - .option("--local", "Force local execution", false) - .option("--gateway", "Force gateway execution", false) - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runCommandWithRuntime(defaultRuntime, async () => { - const transport = resolveTransport({ - local: Boolean(opts.local), - gateway: Boolean(opts.gateway), - supported: ["local", "gateway"], - defaultTransport: "gateway", - }); - const result = await runTtsStateMutation({ - capability: "tts.set-provider", - provider: String(opts.provider), - transport, - }); - emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => - JSON.stringify(value, null, 2), - ); - }); - }); + registerTransportTtsCommand( + tts + .command("set-provider") + .description("Set the active TTS provider") + .requiredOption("--provider ", "Speech provider id"), + "gateway", + (opts, transport) => + runTtsStateMutation({ + capability: "tts.set-provider", + provider: String(opts.provider), + transport, + }), + ); - tts - .command("set-persona") - .description("Set the active TTS persona") - .option("--persona ", "TTS persona id") - .option("--off", "Disable the active TTS persona", false) - .option("--local", "Force local execution", false) - .option("--gateway", "Force gateway execution", false) - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runCommandWithRuntime(defaultRuntime, async () => { - const transport = resolveTransport({ - local: Boolean(opts.local), - gateway: Boolean(opts.gateway), - supported: ["local", "gateway"], - defaultTransport: "gateway", - }); - if (!opts.off && !opts.persona) { - throw new Error("--persona is required unless --off is set"); - } - const result = await runTtsStateMutation({ - capability: "tts.set-persona", - persona: opts.off ? null : String(opts.persona), - transport, - }); - emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => - JSON.stringify(value, null, 2), - ); + registerTransportTtsCommand( + tts + .command("set-persona") + .description("Set the active TTS persona") + .option("--persona ", "TTS persona id") + .option("--off", "Disable the active TTS persona", false), + "gateway", + (opts, transport) => { + if (!opts.off && !opts.persona) { + throw new Error("--persona is required unless --off is set"); + } + return runTtsStateMutation({ + capability: "tts.set-persona", + persona: opts.off ? null : String(opts.persona), + transport, }); - }); + }, + ); } diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index fcad800ccdae..111867944eb7 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -191,102 +191,77 @@ export function registerModelsCli(program: Command) { }); }); - const fallbacks = models.command("fallbacks").description("Manage model fallback list"); + const fallbackGroups = [ + { + name: "fallbacks", + modelType: "model", + noun: "fallback", + article: "a", + load: async () => { + const commands = await loadModelsFallbacksCommands(); + return { + list: commands.modelsFallbacksListCommand, + add: commands.modelsFallbacksAddCommand, + remove: commands.modelsFallbacksRemoveCommand, + clear: commands.modelsFallbacksClearCommand, + }; + }, + }, + { + name: "image-fallbacks", + modelType: "image model", + noun: "image fallback", + article: "an", + load: async () => { + const commands = await loadModelsImageFallbacksCommands(); + return { + list: commands.modelsImageFallbacksListCommand, + add: commands.modelsImageFallbacksAddCommand, + remove: commands.modelsImageFallbacksRemoveCommand, + clear: commands.modelsImageFallbacksClearCommand, + }; + }, + }, + ] as const; - fallbacks - .command("list") - .description("List fallback models") - .option("--json", "Output JSON", false) - .option("--plain", "Plain output", false) - .action(async (opts) => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsFallbacksListCommand } = await loadModelsFallbacksCommands(); - await modelsFallbacksListCommand({ ...opts, json: hasJsonOutput(opts) }, defaultRuntime); + for (const { name, modelType, noun, article, load } of fallbackGroups) { + const group = models.command(name).description(`Manage ${modelType} fallback list`); + + group + .command("list") + .description(`List ${noun} models`) + .option("--json", "Output JSON", false) + .option("--plain", "Plain output", false) + .action(async (opts) => { + await withModelsRuntime(async ({ defaultRuntime }) => { + const commands = await load(); + await commands.list({ ...opts, json: hasJsonOutput(opts) }, defaultRuntime); + }); }); - }); - fallbacks - .command("add") - .description("Add a fallback model") - .argument("", "Model id or alias") - .action(async (model: string) => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsFallbacksAddCommand } = await loadModelsFallbacksCommands(); - await modelsFallbacksAddCommand(model, defaultRuntime); + for (const action of ["add", "remove"] as const) { + group + .command(action) + .description(`${action === "add" ? "Add" : "Remove"} ${article} ${noun} model`) + .argument("", "Model id or alias") + .action(async (model: string) => { + await withModelsRuntime(async ({ defaultRuntime }) => { + const commands = await load(); + await commands[action](model, defaultRuntime); + }); + }); + } + + group + .command("clear") + .description(`Clear all ${noun} models`) + .action(async () => { + await withModelsRuntime(async ({ defaultRuntime }) => { + const commands = await load(); + await commands.clear(defaultRuntime); + }); }); - }); - - fallbacks - .command("remove") - .description("Remove a fallback model") - .argument("", "Model id or alias") - .action(async (model: string) => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsFallbacksRemoveCommand } = await loadModelsFallbacksCommands(); - await modelsFallbacksRemoveCommand(model, defaultRuntime); - }); - }); - - fallbacks - .command("clear") - .description("Clear all fallback models") - .action(async () => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsFallbacksClearCommand } = await loadModelsFallbacksCommands(); - await modelsFallbacksClearCommand(defaultRuntime); - }); - }); - - const imageFallbacks = models - .command("image-fallbacks") - .description("Manage image model fallback list"); - - imageFallbacks - .command("list") - .description("List image fallback models") - .option("--json", "Output JSON", false) - .option("--plain", "Plain output", false) - .action(async (opts) => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsImageFallbacksListCommand } = await loadModelsImageFallbacksCommands(); - await modelsImageFallbacksListCommand( - { ...opts, json: hasJsonOutput(opts) }, - defaultRuntime, - ); - }); - }); - - imageFallbacks - .command("add") - .description("Add an image fallback model") - .argument("", "Model id or alias") - .action(async (model: string) => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsImageFallbacksAddCommand } = await loadModelsImageFallbacksCommands(); - await modelsImageFallbacksAddCommand(model, defaultRuntime); - }); - }); - - imageFallbacks - .command("remove") - .description("Remove an image fallback model") - .argument("", "Model id or alias") - .action(async (model: string) => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsImageFallbacksRemoveCommand } = await loadModelsImageFallbacksCommands(); - await modelsImageFallbacksRemoveCommand(model, defaultRuntime); - }); - }); - - imageFallbacks - .command("clear") - .description("Clear all image fallback models") - .action(async () => { - await withModelsRuntime(async ({ defaultRuntime }) => { - const { modelsImageFallbacksClearCommand } = await loadModelsImageFallbacksCommands(); - await modelsImageFallbacksClearCommand(defaultRuntime); - }); - }); + } models .command("scan") diff --git a/src/cli/node-cli/register.ts b/src/cli/node-cli/register.ts index e53c70e9422e..dce0bc4191e8 100644 --- a/src/cli/node-cli/register.ts +++ b/src/cli/node-cli/register.ts @@ -122,35 +122,20 @@ export function registerNodeCli(program: Command) { await runNodeDaemonInstall(opts); }); - node - .command("uninstall") - .description("Uninstall the node host service (launchd/systemd/schtasks)") - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runNodeDaemonUninstall(opts); - }); - - node - .command("stop") - .description("Stop the node host service (launchd/systemd/schtasks)") - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runNodeDaemonStop(opts); - }); - - node - .command("start") - .description("Start the node host service (launchd/systemd/schtasks)") - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runNodeDaemonStart(opts); - }); - - node - .command("restart") - .description("Restart the node host service (launchd/systemd/schtasks)") - .option("--json", "Output JSON", false) - .action(async (opts) => { - await runNodeDaemonRestart(opts); - }); + for (const [name, action] of [ + ["uninstall", runNodeDaemonUninstall], + ["stop", runNodeDaemonStop], + ["start", runNodeDaemonStart], + ["restart", runNodeDaemonRestart], + ] as const) { + node + .command(name) + .description( + `${name.charAt(0).toUpperCase()}${name.slice(1)} the node host service (launchd/systemd/schtasks)`, + ) + .option("--json", "Output JSON", false) + .action(async (opts) => { + await action(opts); + }); + } } diff --git a/src/cli/system-cli.ts b/src/cli/system-cli.ts index f6b9fa1497c1..cfada786e343 100644 --- a/src/cli/system-cli.ts +++ b/src/cli/system-cli.ts @@ -117,37 +117,26 @@ export function registerSystemCli(program: Command) { }); }); - addGatewayClientOptions( - heartbeat - .command("enable") - .description("Enable heartbeats") - .option("--json", "Output JSON", false), - ).action(async (opts: SystemGatewayOpts) => { - await runSystemGatewayCommand(opts, async () => { - return await callGatewayFromCli( - "set-heartbeats", - opts, - { enabled: true }, - { expectFinal: false }, - ); + for (const [name, enabled] of [ + ["enable", true], + ["disable", false], + ] as const) { + addGatewayClientOptions( + heartbeat + .command(name) + .description(`${enabled ? "Enable" : "Disable"} heartbeats`) + .option("--json", "Output JSON", false), + ).action(async (opts: SystemGatewayOpts) => { + await runSystemGatewayCommand(opts, async () => { + return await callGatewayFromCli( + "set-heartbeats", + opts, + { enabled }, + { expectFinal: false }, + ); + }); }); - }); - - addGatewayClientOptions( - heartbeat - .command("disable") - .description("Disable heartbeats") - .option("--json", "Output JSON", false), - ).action(async (opts: SystemGatewayOpts) => { - await runSystemGatewayCommand(opts, async () => { - return await callGatewayFromCli( - "set-heartbeats", - opts, - { enabled: false }, - { expectFinal: false }, - ); - }); - }); + } addGatewayClientOptions( system