diff --git a/src/auto-reply/reply/commands-setunset.ts b/src/auto-reply/reply/commands-setunset.ts new file mode 100644 index 000000000..137973a5e --- /dev/null +++ b/src/auto-reply/reply/commands-setunset.ts @@ -0,0 +1,38 @@ +import { parseConfigValue } from "./config-value.js"; + +export type SetUnsetParseResult = + | { kind: "set"; path: string; value: unknown } + | { kind: "unset"; path: string } + | { kind: "error"; message: string }; + +export function parseSetUnsetCommand(params: { + slash: string; + action: "set" | "unset"; + args: string; +}): SetUnsetParseResult { + const action = params.action; + const args = params.args.trim(); + if (action === "unset") { + if (!args) { + return { kind: "error", message: `Usage: ${params.slash} unset path` }; + } + return { kind: "unset", path: args }; + } + if (!args) { + return { kind: "error", message: `Usage: ${params.slash} set path=value` }; + } + const eqIndex = args.indexOf("="); + if (eqIndex <= 0) { + return { kind: "error", message: `Usage: ${params.slash} set path=value` }; + } + const path = args.slice(0, eqIndex).trim(); + const rawValue = args.slice(eqIndex + 1); + if (!path) { + return { kind: "error", message: `Usage: ${params.slash} set path=value` }; + } + const parsed = parseConfigValue(rawValue); + if (parsed.error) { + return { kind: "error", message: parsed.error }; + } + return { kind: "set", path, value: parsed.value }; +} diff --git a/src/auto-reply/reply/commands-slash-parse.ts b/src/auto-reply/reply/commands-slash-parse.ts new file mode 100644 index 000000000..8cf5541e3 --- /dev/null +++ b/src/auto-reply/reply/commands-slash-parse.ts @@ -0,0 +1,46 @@ +export type SlashCommandParseResult = + | { kind: "no-match" } + | { kind: "empty" } + | { kind: "invalid" } + | { kind: "parsed"; action: string; args: string }; + +export type ParsedSlashCommand = + | { ok: true; action: string; args: string } + | { ok: false; message: string }; + +export function parseSlashCommandActionArgs(raw: string, slash: string): SlashCommandParseResult { + const trimmed = raw.trim(); + const slashLower = slash.toLowerCase(); + if (!trimmed.toLowerCase().startsWith(slashLower)) { + return { kind: "no-match" }; + } + const rest = trimmed.slice(slash.length).trim(); + if (!rest) { + return { kind: "empty" }; + } + const match = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/); + if (!match) { + return { kind: "invalid" }; + } + const action = match[1]?.toLowerCase() ?? ""; + const args = (match[2] ?? "").trim(); + return { kind: "parsed", action, args }; +} + +export function parseSlashCommandOrNull( + raw: string, + slash: string, + opts: { invalidMessage: string; defaultAction?: string }, +): ParsedSlashCommand | null { + const parsed = parseSlashCommandActionArgs(raw, slash); + if (parsed.kind === "no-match") { + return null; + } + if (parsed.kind === "invalid") { + return { ok: false, message: opts.invalidMessage }; + } + if (parsed.kind === "empty") { + return { ok: true, action: opts.defaultAction ?? "show", args: "" }; + } + return { ok: true, action: parsed.action, args: parsed.args }; +} diff --git a/src/auto-reply/reply/config-commands.ts b/src/auto-reply/reply/config-commands.ts index b78baa459..fc924985c 100644 --- a/src/auto-reply/reply/config-commands.ts +++ b/src/auto-reply/reply/config-commands.ts @@ -1,4 +1,5 @@ -import { parseConfigValue } from "./config-value.js"; +import { parseSetUnsetCommand } from "./commands-setunset.js"; +import { parseSlashCommandOrNull } from "./commands-slash-parse.js"; export type ConfigCommand = | { action: "show"; path?: string } @@ -7,60 +8,31 @@ export type ConfigCommand = | { action: "error"; message: string }; export function parseConfigCommand(raw: string): ConfigCommand | null { - const trimmed = raw.trim(); - if (!trimmed.toLowerCase().startsWith("/config")) { + const parsed = parseSlashCommandOrNull(raw, "/config", { + invalidMessage: "Invalid /config syntax.", + }); + if (!parsed) { return null; } - const rest = trimmed.slice("/config".length).trim(); - if (!rest) { - return { action: "show" }; + if (!parsed.ok) { + return { action: "error", message: parsed.message }; } - - const match = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/); - if (!match) { - return { action: "error", message: "Invalid /config syntax." }; - } - const action = match[1].toLowerCase(); - const args = (match[2] ?? "").trim(); + const { action, args } = parsed; switch (action) { case "show": return { action: "show", path: args || undefined }; case "get": return { action: "show", path: args || undefined }; - case "unset": { - if (!args) { - return { action: "error", message: "Usage: /config unset path" }; - } - return { action: "unset", path: args }; - } + case "unset": case "set": { - if (!args) { - return { - action: "error", - message: "Usage: /config set path=value", - }; + const parsed = parseSetUnsetCommand({ slash: "/config", action, args }); + if (parsed.kind === "error") { + return { action: "error", message: parsed.message }; } - const eqIndex = args.indexOf("="); - if (eqIndex <= 0) { - return { - action: "error", - message: "Usage: /config set path=value", - }; - } - const path = args.slice(0, eqIndex).trim(); - const rawValue = args.slice(eqIndex + 1); - if (!path) { - return { - action: "error", - message: "Usage: /config set path=value", - }; - } - const parsed = parseConfigValue(rawValue); - if (parsed.error) { - return { action: "error", message: parsed.error }; - } - return { action: "set", path, value: parsed.value }; + return parsed.kind === "set" + ? { action: "set", path: parsed.path, value: parsed.value } + : { action: "unset", path: parsed.path }; } default: return { diff --git a/src/auto-reply/reply/debug-commands.ts b/src/auto-reply/reply/debug-commands.ts index 5f9f8c9fd..089caf2a5 100644 --- a/src/auto-reply/reply/debug-commands.ts +++ b/src/auto-reply/reply/debug-commands.ts @@ -1,4 +1,5 @@ -import { parseConfigValue } from "./config-value.js"; +import { parseSetUnsetCommand } from "./commands-setunset.js"; +import { parseSlashCommandOrNull } from "./commands-slash-parse.js"; export type DebugCommand = | { action: "show" } @@ -8,60 +9,31 @@ export type DebugCommand = | { action: "error"; message: string }; export function parseDebugCommand(raw: string): DebugCommand | null { - const trimmed = raw.trim(); - if (!trimmed.toLowerCase().startsWith("/debug")) { + const parsed = parseSlashCommandOrNull(raw, "/debug", { + invalidMessage: "Invalid /debug syntax.", + }); + if (!parsed) { return null; } - const rest = trimmed.slice("/debug".length).trim(); - if (!rest) { - return { action: "show" }; + if (!parsed.ok) { + return { action: "error", message: parsed.message }; } - - const match = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/); - if (!match) { - return { action: "error", message: "Invalid /debug syntax." }; - } - const action = match[1].toLowerCase(); - const args = (match[2] ?? "").trim(); + const { action, args } = parsed; switch (action) { case "show": return { action: "show" }; case "reset": return { action: "reset" }; - case "unset": { - if (!args) { - return { action: "error", message: "Usage: /debug unset path" }; - } - return { action: "unset", path: args }; - } + case "unset": case "set": { - if (!args) { - return { - action: "error", - message: "Usage: /debug set path=value", - }; + const parsed = parseSetUnsetCommand({ slash: "/debug", action, args }); + if (parsed.kind === "error") { + return { action: "error", message: parsed.message }; } - const eqIndex = args.indexOf("="); - if (eqIndex <= 0) { - return { - action: "error", - message: "Usage: /debug set path=value", - }; - } - const path = args.slice(0, eqIndex).trim(); - const rawValue = args.slice(eqIndex + 1); - if (!path) { - return { - action: "error", - message: "Usage: /debug set path=value", - }; - } - const parsed = parseConfigValue(rawValue); - if (parsed.error) { - return { action: "error", message: parsed.error }; - } - return { action: "set", path, value: parsed.value }; + return parsed.kind === "set" + ? { action: "set", path: parsed.path, value: parsed.value } + : { action: "unset", path: parsed.path }; } default: return {