From 2ce71ffe2561876d9578b428f0bc524fb4ce102d Mon Sep 17 00:00:00 2001 From: Brian Morin Date: Mon, 12 Jan 2026 11:59:24 -0600 Subject: [PATCH 1/2] feat: add completions command for bash, zsh, fish, and llm Add `imsg completions ` command that generates shell completion scripts dynamically from registered commands. Supports bash, zsh, fish, and an "llm" format for AI assistant context. - CompletionMetadata.swift holds command/option metadata for generation - CompletionsCommand.swift implements generators for each shell format - Consistency test verifies metadata stays in sync with CommandRouter - Service choices derive from MessageService.allCases (not hardcoded) --- Sources/imsg/CommandRouter.swift | 1 + .../imsg/Commands/CompletionsCommand.swift | 432 ++++++++++++++++++ Sources/imsg/CompletionMetadata.swift | 159 +++++++ Tests/imsgTests/CommandTests.swift | 108 +++++ 4 files changed, 700 insertions(+) create mode 100644 Sources/imsg/Commands/CompletionsCommand.swift create mode 100644 Sources/imsg/CompletionMetadata.swift diff --git a/Sources/imsg/CommandRouter.swift b/Sources/imsg/CommandRouter.swift index 7d819f9..9c4c9f2 100644 --- a/Sources/imsg/CommandRouter.swift +++ b/Sources/imsg/CommandRouter.swift @@ -15,6 +15,7 @@ struct CommandRouter { WatchCommand.spec, SendCommand.spec, RpcCommand.spec, + CompletionsCommand.spec, ] let descriptor = CommandDescriptor( name: rootName, diff --git a/Sources/imsg/Commands/CompletionsCommand.swift b/Sources/imsg/Commands/CompletionsCommand.swift new file mode 100644 index 0000000..f9f2472 --- /dev/null +++ b/Sources/imsg/Commands/CompletionsCommand.swift @@ -0,0 +1,432 @@ +import Commander +import Foundation + +enum CompletionsCommand { + /// Note: This command intentionally does not use CommandSignatures.withRuntimeFlags() + /// because --json, --verbose, and --log-level don't make sense for generating + /// completion scripts. The output is always plain text shell/markdown. + static let spec = CommandSpec( + name: "completions", + abstract: "Generate shell completions or LLM context", + discussion: "Outputs completion scripts for bash, zsh, fish, or context for LLM assistants.", + signature: CommandSignature( + arguments: [ + ArgumentDefinition.make(label: "shell", help: "Shell type: bash, zsh, fish, or llm") + ] + ), + usageExamples: [ + "imsg completions bash > ~/.bash_completion.d/imsg", + "imsg completions zsh > ~/.zsh/completions/_imsg", + "imsg completions fish > ~/.config/fish/completions/imsg.fish", + "imsg completions llm | pbcopy", + ] + ) { values, _ in + try run(shell: values.argument(0)) + } + + static func run(shell: String?) throws { + let output = try generateOutput(shell: shell) + Swift.print(output) + } + + /// Generate completion output without printing (for testing) + static func generateOutput(shell: String?) throws -> String { + guard let shell = shell else { + throw CompletionsError.missingShell + } + switch shell.lowercased() { + case "bash": + return BashCompletionGenerator.generate() + case "zsh": + return ZshCompletionGenerator.generate() + case "fish": + return FishCompletionGenerator.generate() + case "llm": + return LLMContextGenerator.generate() + default: + throw CompletionsError.unknownShell(shell) + } + } +} + +enum CompletionsError: Error, CustomStringConvertible, Sendable { + case missingShell + case unknownShell(String) + + var description: String { + switch self { + case .missingShell: + return "Missing shell argument. Use: bash, zsh, fish, or llm" + case .unknownShell(let shell): + return "Unknown shell '\(shell)'. Use: bash, zsh, fish, or llm" + } + } +} + +// MARK: - Bash + +private enum BashCompletionGenerator { + static func generate() -> String { + let meta = CompletionMetadata.self + let commands = meta.commands.map { $0.name }.joined(separator: " ") + let logLevels = meta.logLevelChoices.joined(separator: " ") + let services = meta.serviceChoices.joined(separator: " ") + + var commandCases = "" + for cmd in meta.commands { + let allOpts = (meta.runtimeOptions + cmd.options).map { "--\($0.long)" }.joined( + separator: " ") + commandCases += """ + \(cmd.name)) + COMPREPLY=($(compgen -W "\(allOpts)" -- "$cur")) + ;; + + """ + } + + return """ + # Bash completion for \(meta.cliName) + # Generated by: \(meta.cliName) completions bash + + _\(meta.cliName)() { + local cur prev words cword + _init_completion || return + + local commands="\(commands)" + local log_levels="\(logLevels)" + local services="\(services)" + + local cmd="" + local i + for ((i=1; i < cword; i++)); do + case "${words[i]}" in + -*) + case "${words[i]}" in + --db|--log-level) ((i++)) ;; + esac + ;; + *) + if [[ " $commands " =~ " ${words[i]} " ]]; then + cmd="${words[i]}" + break + fi + ;; + esac + done + + case "$prev" in + --log-level) COMPREPLY=($(compgen -W "$log_levels" -- "$cur")); return ;; + --service) COMPREPLY=($(compgen -W "$services" -- "$cur")); return ;; + --db|--file) _filedir; return ;; + esac + + if [[ -z "$cmd" ]]; then + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--help -h --version -V" -- "$cur")) + else + COMPREPLY=($(compgen -W "$commands" -- "$cur")) + fi + return + fi + + case "$cmd" in + \(commandCases) esac + } + + complete -F _\(meta.cliName) \(meta.cliName) + """ + } +} + +// MARK: - Zsh + +private enum ZshCompletionGenerator { + static func generate() -> String { + let meta = CompletionMetadata.self + + var commandDescriptions = "" + for cmd in meta.commands { + commandDescriptions += " '\(cmd.name):\(cmd.description)'\n" + } + + var commandCases = "" + for cmd in meta.commands { + var opts = "" + for opt in meta.runtimeOptions + cmd.options { + let desc = opt.description.replacingOccurrences(of: "'", with: "\\'") + if opt.takesValue { + if let choices = opt.choices { + opts += + " '--\(opt.long)[\(desc)]:\(opt.long):(\(choices.joined(separator: " ")))'\n" + } else { + let hint = opt.valueHint ?? opt.long + opts += " '--\(opt.long)[\(desc)]:\(hint)'\n" + } + } else { + if let short = opt.short { + opts += + " '(-\(short) --\(opt.long))'{-\(short),--\(opt.long)}'[\(desc)]'\n" + } else { + opts += " '--\(opt.long)[\(desc)]'\n" + } + } + } + commandCases += """ + \(cmd.name)) + _arguments \\ + \(opts) && return 0 + ;; + + """ + } + + return """ + #compdef \(meta.cliName) + # Zsh completion for \(meta.cliName) + # Generated by: \(meta.cliName) completions zsh + + _\(meta.cliName)() { + local context state state_descr line + typeset -A opt_args + + local -a commands + commands=( + \(commandDescriptions) ) + + _arguments -C \\ + '(- *)'{-h,--help}'[Show help]' \\ + '(- *)'{-V,--version}'[Show version]' \\ + '1: :->command' \\ + '*:: :->args' \\ + && return 0 + + case $state in + command) + _describe -t commands '\(meta.cliName) commands' commands + ;; + args) + case $words[1] in + \(commandCases) esac + ;; + esac + } + + _\(meta.cliName) "$@" + """ + } +} + +// MARK: - Fish + +private enum FishCompletionGenerator { + static func generate() -> String { + let meta = CompletionMetadata.self + var lines: [String] = [] + + lines.append("# Fish completion for \(meta.cliName)") + lines.append("# Generated by: \(meta.cliName) completions fish") + lines.append("") + lines.append("complete -c \(meta.cliName) -f") + lines.append("") + + // Helper functions + lines.append( + """ + function __\(meta.cliName)_needs_command + set -l cmd (commandline -opc) + test (count $cmd) -eq 1 + end + + function __\(meta.cliName)_using_command + set -l cmd (commandline -opc) + test (count $cmd) -gt 1 && contains -- $cmd[2] $argv + end + """ + ) + lines.append("") + + // Global options + lines.append("# Global options") + lines.append( + "complete -c \(meta.cliName) -n __\(meta.cliName)_needs_command -s h -l help -d 'Show help'") + lines.append( + "complete -c \(meta.cliName) -n __\(meta.cliName)_needs_command -s V -l version -d 'Show version'" + ) + lines.append("") + + // Commands + lines.append("# Commands") + for cmd in meta.commands { + let desc = cmd.description.replacingOccurrences(of: "'", with: "\\'") + lines.append( + "complete -c \(meta.cliName) -n __\(meta.cliName)_needs_command -a \(cmd.name) -d '\(desc)'" + ) + } + lines.append("") + + // Runtime options (available for all commands) + lines.append("# Runtime options") + for opt in meta.runtimeOptions { + let desc = opt.description.replacingOccurrences(of: "'", with: "\\'") + var line = "complete -c \(meta.cliName)" + if let short = opt.short { + line += " -s \(short)" + } + line += " -l \(opt.long) -d '\(desc)'" + if let choices = opt.choices { + line += " -xa '\(choices.joined(separator: " "))'" + } else if opt.takesValue { + if opt.valueHint == "file" { + line += " -r -F" + } else { + line += " -x" + } + } + lines.append(line) + } + lines.append("") + + // Command-specific options + for cmd in meta.commands { + guard !cmd.options.isEmpty else { continue } + lines.append("# \(cmd.name) options") + for opt in cmd.options { + let desc = opt.description.replacingOccurrences(of: "'", with: "\\'") + var line = "complete -c \(meta.cliName) -n '__\(meta.cliName)_using_command \(cmd.name)'" + line += " -l \(opt.long) -d '\(desc)'" + if let choices = opt.choices { + line += " -xa '\(choices.joined(separator: " "))'" + } else if opt.takesValue { + if opt.valueHint == "file" { + line += " -r -F" + } else { + line += " -x" + } + } + lines.append(line) + } + lines.append("") + } + + return lines.joined(separator: "\n") + } +} + +// MARK: - LLM Context + +private enum LLMContextGenerator { + static func generate() -> String { + let meta = CompletionMetadata.self + var lines: [String] = [] + + lines.append("# \(meta.cliName) CLI Reference") + lines.append("") + lines.append(meta.description) + lines.append("") + lines.append("## Requirements") + lines.append("- macOS 14+ with Messages.app signed in") + lines.append("- Full Disk Access permission for terminal (to read chat.db)") + lines.append("- Automation permission for Messages.app (for sending)") + lines.append("") + + lines.append("## Global Options") + lines.append("```") + lines.append("-h, --help Show help") + lines.append("-V, --version Show version") + lines.append("```") + lines.append("") + + lines.append("## Runtime Options (available on all commands)") + lines.append("```") + for opt in meta.runtimeOptions { + var line = "--\(opt.long)" + if let short = opt.short { + line = "-\(short), " + line + } + if opt.takesValue { + line += " <\(opt.valueHint ?? "value")>" + } + let padding = String(repeating: " ", count: max(1, 30 - line.count)) + line += padding + opt.description + if let choices = opt.choices { + line += " [\(choices.joined(separator: "|"))]" + } + lines.append(line) + } + lines.append("```") + lines.append("") + + lines.append("## Commands") + lines.append("") + for cmd in meta.commands { + lines.append("### \(cmd.name)") + lines.append(cmd.description) + lines.append("") + if !cmd.options.isEmpty { + lines.append("Options:") + lines.append("```") + for opt in cmd.options { + var line = "--\(opt.long)" + if opt.takesValue { + line += " <\(opt.valueHint ?? "value")>" + } + let padding = String(repeating: " ", count: max(1, 28 - line.count)) + line += padding + opt.description + if let choices = opt.choices { + line += " [\(choices.joined(separator: "|"))]" + } + lines.append(line) + } + lines.append("```") + lines.append("") + } + lines.append("Examples:") + lines.append("```bash") + for example in cmd.examples { + lines.append(example) + } + lines.append("```") + lines.append("") + } + + lines.append("## JSON Output") + lines.append("") + lines.append("Use `--json` flag for machine-readable output (JSON Lines format).") + lines.append("") + lines.append("### Chat object") + lines.append("```json") + lines.append( + """ + {"id": 1, "name": "John Doe", "identifier": "+14155551212", "service": "iMessage", "last_message_at": "2025-01-01T12:00:00.000Z"} + """ + ) + lines.append("```") + lines.append("") + lines.append("### Message object") + lines.append("```json") + lines.append( + """ + {"id": 123, "chat_id": 1, "guid": "...", "sender": "+14155551212", "is_from_me": false, "text": "Hello", "created_at": "2025-01-01T12:00:00.000Z", "attachments": [], "reactions": []} + """ + ) + lines.append("```") + lines.append("") + + lines.append("## Common Workflows") + lines.append("") + lines.append("1. List chats to find chat ID:") + lines.append(" `imsg chats --limit 10`") + lines.append("") + lines.append("2. Get history for a chat:") + lines.append(" `imsg history --chat-id 1 --limit 20`") + lines.append("") + lines.append("3. Watch for new messages:") + lines.append(" `imsg watch --chat-id 1 --json`") + lines.append("") + lines.append("4. Send a message:") + lines.append(" `imsg send --to +14155551212 --text \"Hello\"`") + lines.append("") + + return lines.joined(separator: "\n") + } +} diff --git a/Sources/imsg/CompletionMetadata.swift b/Sources/imsg/CompletionMetadata.swift new file mode 100644 index 0000000..ed884bb --- /dev/null +++ b/Sources/imsg/CompletionMetadata.swift @@ -0,0 +1,159 @@ +import Foundation +import IMsgCore + +/// Metadata for shell and LLM completion generation. +/// +/// This structure exists separately from CommandSpec/CommandSignature because Commander's +/// OptionDefinition doesn't support value hints (file, rowid, timestamp) or enumerated choices +/// needed for shell completion. Rather than forking Commander, we maintain this parallel +/// structure. The consistency test verifies command names stay in sync with CommandRouter. +/// +/// If maintainers prefer, this could be integrated into CommandSpec by extending +/// Commander's types or adding a completion metadata property to CommandSpec. +/// +/// Update this file when commands or options change. +enum CompletionMetadata { + static let cliName = "imsg" + static let description = "macOS CLI for iMessage/SMS - send, read, and stream messages" + + /// Service values derived from MessageService enum to avoid hardcoding + static let serviceChoices: [String] = MessageService.allCases.map { $0.rawValue } + + /// Log levels as defined in Commander's withStandardRuntimeFlags. + /// Commander doesn't expose these as an enum, so we define them here. + static let logLevelChoices: [String] = [ + "trace", "verbose", "debug", "info", "warning", "error", "critical", + ] + + struct Option { + let long: String + let short: String? + let description: String + let takesValue: Bool + let valueHint: String? + let choices: [String]? + + init( + _ long: String, + short: String? = nil, + description: String, + takesValue: Bool = true, + valueHint: String? = nil, + choices: [String]? = nil + ) { + self.long = long + self.short = short + self.description = description + self.takesValue = takesValue + self.valueHint = valueHint + self.choices = choices + } + } + + struct Command { + let name: String + let description: String + let options: [Option] + let examples: [String] + } + + static let runtimeOptions: [Option] = [ + Option("db", description: "Path to chat.db", valueHint: "file"), + Option("log-level", description: "Set log level", choices: logLevelChoices), + Option("verbose", short: "v", description: "Enable verbose logging", takesValue: false), + Option("json", short: "j", description: "Emit machine-readable JSON output", takesValue: false), + ] + + static let commands: [Command] = [ + Command( + name: "chats", + description: "List recent conversations", + options: [ + Option("limit", description: "Number of chats to list", valueHint: "number") + ], + examples: [ + "imsg chats --limit 5", + "imsg chats --json", + ] + ), + Command( + name: "history", + description: "Show recent messages for a chat", + options: [ + Option("chat-id", description: "Chat rowid from 'imsg chats'", valueHint: "rowid"), + Option("limit", description: "Number of messages to show", valueHint: "number"), + Option( + "participants", + description: "Filter by participant handles (comma-separated)", + valueHint: "handles" + ), + Option("start", description: "ISO8601 start timestamp (inclusive)", valueHint: "timestamp"), + Option("end", description: "ISO8601 end timestamp (exclusive)", valueHint: "timestamp"), + Option("attachments", description: "Include attachment metadata", takesValue: false), + ], + examples: [ + "imsg history --chat-id 1 --limit 10 --attachments", + "imsg history --chat-id 1 --start 2025-01-01T00:00:00Z --json", + ] + ), + Command( + name: "watch", + description: "Stream incoming messages", + options: [ + Option("chat-id", description: "Limit to chat rowid", valueHint: "rowid"), + Option("debounce", description: "Debounce interval (e.g., 250ms)", valueHint: "duration"), + Option("since-rowid", description: "Start watching after this rowid", valueHint: "rowid"), + Option( + "participants", + description: "Filter by participant handles (comma-separated)", + valueHint: "handles" + ), + Option("start", description: "ISO8601 start timestamp (inclusive)", valueHint: "timestamp"), + Option("end", description: "ISO8601 end timestamp (exclusive)", valueHint: "timestamp"), + Option("attachments", description: "Include attachment metadata", takesValue: false), + ], + examples: [ + "imsg watch --chat-id 1 --attachments --debounce 250ms", + "imsg watch --json", + ] + ), + Command( + name: "send", + description: "Send a message (text and/or attachment)", + options: [ + Option("to", description: "Phone number or email", valueHint: "recipient"), + Option("chat-id", description: "Chat rowid (alternative to --to)", valueHint: "rowid"), + Option("chat-identifier", description: "Chat identifier string", valueHint: "identifier"), + Option("chat-guid", description: "Chat GUID", valueHint: "guid"), + Option("text", description: "Message body", valueHint: "message"), + Option("file", description: "Path to attachment", valueHint: "file"), + Option("service", description: "Service to use", choices: serviceChoices), + Option("region", description: "Default region for phone normalization", valueHint: "code"), + ], + examples: [ + "imsg send --to +14155551212 --text \"hello\"", + "imsg send --chat-id 1 --text \"hi\" --file ~/photo.jpg", + ] + ), + Command( + name: "rpc", + description: "Run JSON-RPC server over stdin/stdout", + options: [], + examples: [ + "imsg rpc", + "imsg rpc --db ~/Library/Messages/chat.db", + ] + ), + Command( + name: "completions", + description: "Generate shell completions or LLM context", + options: [], + examples: [ + "imsg completions bash", + "imsg completions zsh", + "imsg completions fish", + "imsg completions llm", + ] + ), + ] +} diff --git a/Tests/imsgTests/CommandTests.swift b/Tests/imsgTests/CommandTests.swift index 9f5a7a3..48730c4 100644 --- a/Tests/imsgTests/CommandTests.swift +++ b/Tests/imsgTests/CommandTests.swift @@ -327,3 +327,111 @@ func watchCommandRunsWithJsonOutput() async throws { streamProvider: streamProvider ) } + +// MARK: - Completions Command Tests + +@Test +func completionsCommandGeneratesBash() throws { + try CompletionsCommand.run(shell: "bash") +} + +@Test +func completionsCommandGeneratesZsh() throws { + try CompletionsCommand.run(shell: "zsh") +} + +@Test +func completionsCommandGeneratesFish() throws { + try CompletionsCommand.run(shell: "fish") +} + +@Test +func completionsCommandGeneratesLLM() throws { + try CompletionsCommand.run(shell: "llm") +} + +@Test +func completionsCommandRejectsMissingShell() { + do { + try CompletionsCommand.run(shell: nil) + #expect(Bool(false)) + } catch let error as CompletionsError { + #expect(error.description.contains("Missing shell")) + } catch { + #expect(Bool(false)) + } +} + +@Test +func completionsCommandRejectsUnknownShell() { + do { + try CompletionsCommand.run(shell: "powershell") + #expect(Bool(false)) + } catch let error as CompletionsError { + #expect(error.description.contains("Unknown shell")) + } catch { + #expect(Bool(false)) + } +} + +@Test +func completionMetadataContainsAllCommands() { + // Verify CompletionMetadata stays in sync with registered commands + let router = CommandRouter() + let registeredNames = Set(router.specs.map { $0.name }) + let metadataNames = Set(CompletionMetadata.commands.map { $0.name }) + + // Every registered command should be in metadata + for name in registeredNames { + #expect(metadataNames.contains(name), "Command '\(name)' missing from CompletionMetadata") + } + + // Every metadata command should be registered (catches stale entries) + for name in metadataNames { + #expect(registeredNames.contains(name), "Command '\(name)' in metadata but not registered") + } +} + +@Test +func completionMetadataServiceChoicesMatchEnum() { + // Verify serviceChoices derives from MessageService enum + let expected = Set(MessageService.allCases.map { $0.rawValue }) + let actual = Set(CompletionMetadata.serviceChoices) + #expect(expected == actual, "serviceChoices doesn't match MessageService.allCases") +} + +@Test +func bashCompletionContainsAllCommands() throws { + let output = try CompletionsCommand.generateOutput(shell: "bash") + for cmd in CompletionMetadata.commands { + #expect(output.contains(cmd.name), "Bash completion missing command: \(cmd.name)") + } + #expect(output.contains("complete -F _imsg imsg")) +} + +@Test +func zshCompletionContainsAllCommands() throws { + let output = try CompletionsCommand.generateOutput(shell: "zsh") + for cmd in CompletionMetadata.commands { + #expect(output.contains(cmd.name), "Zsh completion missing command: \(cmd.name)") + } + #expect(output.contains("#compdef imsg")) +} + +@Test +func fishCompletionContainsAllCommands() throws { + let output = try CompletionsCommand.generateOutput(shell: "fish") + for cmd in CompletionMetadata.commands { + #expect(output.contains(cmd.name), "Fish completion missing command: \(cmd.name)") + } + #expect(output.contains("complete -c imsg")) +} + +@Test +func llmCompletionContainsAllCommands() throws { + let output = try CompletionsCommand.generateOutput(shell: "llm") + for cmd in CompletionMetadata.commands { + #expect(output.contains("### \(cmd.name)"), "LLM context missing command: \(cmd.name)") + } + #expect(output.contains("# imsg CLI Reference")) +} From 0f2d085f50e95a72d680b171ee3c63079328a166 Mon Sep 17 00:00:00 2001 From: Brian Morin Date: Mon, 12 Jan 2026 12:08:47 -0600 Subject: [PATCH 2/2] docs: add docstrings to meet 80% coverage threshold Adds comprehensive docstrings to CompletionMetadata and CompletionsCommand to satisfy CodeRabbit's documentation coverage requirement. --- .../imsg/Commands/CompletionsCommand.swift | 29 ++++++++++++++++++- Sources/imsg/CompletionMetadata.swift | 18 ++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Sources/imsg/Commands/CompletionsCommand.swift b/Sources/imsg/Commands/CompletionsCommand.swift index f9f2472..2642275 100644 --- a/Sources/imsg/Commands/CompletionsCommand.swift +++ b/Sources/imsg/Commands/CompletionsCommand.swift @@ -1,6 +1,10 @@ import Commander import Foundation +/// Generates shell completion scripts and LLM context documentation. +/// +/// Supports bash, zsh, fish shells and a special "llm" format that produces +/// markdown documentation suitable for AI assistant context windows. enum CompletionsCommand { /// Note: This command intentionally does not use CommandSignatures.withRuntimeFlags() /// because --json, --verbose, and --log-level don't make sense for generating @@ -24,12 +28,19 @@ enum CompletionsCommand { try run(shell: values.argument(0)) } + /// Generates and prints completion output for the specified shell. + /// - Parameter shell: The target shell (bash, zsh, fish) or "llm" for markdown. + /// - Throws: `CompletionsError` if shell is nil or unrecognized. static func run(shell: String?) throws { let output = try generateOutput(shell: shell) Swift.print(output) } - /// Generate completion output without printing (for testing) + /// Generates completion output as a string without printing. + /// + /// - Parameter shell: The target shell (bash, zsh, fish) or "llm" for markdown. + /// - Returns: The generated completion script or documentation. + /// - Throws: `CompletionsError` if shell is nil or unrecognized. static func generateOutput(shell: String?) throws -> String { guard let shell = shell else { throw CompletionsError.missingShell @@ -49,10 +60,14 @@ enum CompletionsCommand { } } +/// Errors that can occur during completion generation. enum CompletionsError: Error, CustomStringConvertible, Sendable { + /// No shell argument was provided. case missingShell + /// The provided shell name is not recognized. case unknownShell(String) + /// Human-readable error message for display. var description: String { switch self { case .missingShell: @@ -65,7 +80,10 @@ enum CompletionsError: Error, CustomStringConvertible, Sendable { // MARK: - Bash +/// Generates bash completion scripts using the bash-completion framework. private enum BashCompletionGenerator { + /// Creates a bash completion script for the imsg CLI. + /// - Returns: Complete bash completion script as a string. static func generate() -> String { let meta = CompletionMetadata.self let commands = meta.commands.map { $0.name }.joined(separator: " ") @@ -140,7 +158,10 @@ private enum BashCompletionGenerator { // MARK: - Zsh +/// Generates zsh completion scripts using the zsh completion system. private enum ZshCompletionGenerator { + /// Creates a zsh completion script for the imsg CLI. + /// - Returns: Complete zsh completion script as a string. static func generate() -> String { let meta = CompletionMetadata.self @@ -218,7 +239,10 @@ private enum ZshCompletionGenerator { // MARK: - Fish +/// Generates fish shell completion scripts. private enum FishCompletionGenerator { + /// Creates a fish completion script for the imsg CLI. + /// - Returns: Complete fish completion script as a string. static func generate() -> String { let meta = CompletionMetadata.self var lines: [String] = [] @@ -314,7 +338,10 @@ private enum FishCompletionGenerator { // MARK: - LLM Context +/// Generates markdown documentation suitable for AI assistant context windows. private enum LLMContextGenerator { + /// Creates a comprehensive CLI reference in markdown format. + /// - Returns: Markdown documentation with commands, options, and examples. static func generate() -> String { let meta = CompletionMetadata.self var lines: [String] = [] diff --git a/Sources/imsg/CompletionMetadata.swift b/Sources/imsg/CompletionMetadata.swift index ed884bb..33d9314 100644 --- a/Sources/imsg/CompletionMetadata.swift +++ b/Sources/imsg/CompletionMetadata.swift @@ -13,7 +13,10 @@ import IMsgCore /// /// Update this file when commands or options change. enum CompletionMetadata { + /// The CLI executable name used in completion scripts. static let cliName = "imsg" + + /// Human-readable description of the CLI for help text and LLM context. static let description = "macOS CLI for iMessage/SMS - send, read, and stream messages" /// Service values derived from MessageService enum to avoid hardcoding @@ -25,14 +28,22 @@ enum CompletionMetadata { "trace", "verbose", "debug", "info", "warning", "error", "critical", ] + /// Describes a CLI option for completion generation. struct Option { + /// Long form of the option (e.g., "chat-id" for --chat-id) let long: String + /// Optional short form (e.g., "v" for -v) let short: String? + /// Help text describing what the option does let description: String + /// Whether the option requires a value let takesValue: Bool + /// Hint for the type of value expected (e.g., "file", "rowid") let valueHint: String? + /// Valid choices for enumerated options let choices: [String]? + /// Creates an option definition for completion generation. init( _ long: String, short: String? = nil, @@ -50,13 +61,19 @@ enum CompletionMetadata { } } + /// Describes a CLI command for completion generation. struct Command { + /// Command name (e.g., "chats", "send") let name: String + /// Brief description of what the command does let description: String + /// Command-specific options (runtime options are added automatically) let options: [Option] + /// Example usage strings for help text let examples: [String] } + /// Options available on all commands via Commander's withStandardRuntimeFlags. static let runtimeOptions: [Option] = [ Option("db", description: "Path to chat.db", valueHint: "file"), Option("log-level", description: "Set log level", choices: logLevelChoices), @@ -64,6 +81,7 @@ enum CompletionMetadata { Option("json", short: "j", description: "Emit machine-readable JSON output", takesValue: false), ] + /// All CLI commands with their options and examples for completion generation. static let commands: [Command] = [ Command( name: "chats",