diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-26 22:07:07 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-26 22:07:07 +0200 |
| commit | b10c6247e14a3acfe20e78426e131691e205a601 (patch) | |
| tree | 907bb5b3ffa8239584ae9b7c2bfc34667b095b62 | |
| parent | bef3cc7dd95745a5724d3569e45fe7be4aba02ee (diff) | |
ask: add fish completions for task CLI
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | assets/ask.fish | 61 | ||||
| -rw-r--r-- | docs/buildandinstall.md | 1 | ||||
| -rw-r--r-- | docs/fish-completion.md | 20 | ||||
| -rw-r--r-- | internal/askcli/completion.go | 96 | ||||
| -rw-r--r-- | internal/askcli/completion_test.go | 46 |
6 files changed, 227 insertions, 1 deletions
@@ -18,7 +18,8 @@ It has got improved capabilities for Go code understanding (for example, create - Auto-scopes to `project:<repo> +agent` (derived from git repo root) - Never exposes numeric task IDs — uses UUIDs only - Machine-friendly output: UUID-only tables, suppressed decorative text - - Subcommands: `ask add`, `ask list`, `ask info`, `ask annotate`, `ask start`, `ask stop`, `ask done`, `ask priority`, `ask tag`, `ask dep`, `ask urgency`, `ask modify`, `ask denotate`, `ask delete`, `ask export` + - Subcommands: `ask add`, `ask list`, `ask info`, `ask annotate`, `ask start`, `ask stop`, `ask done`, `ask priority`, `ask tag`, `ask dep`, `ask urgency`, `ask modify`, `ask denotate`, `ask delete`, `ask help` + - Fish completion asset: [`assets/ask.fish`](assets/ask.fish) * Parallel completions and CLI responses from multiple providers/models for side-by-side comparison * **MCP server for prompt/runbook management** (`hexai-mcp-server`) - **⚠️ DEPRECATED/EXPERIMENTAL** - Create, update, delete, and retrieve prompts via MCP protocol @@ -43,6 +44,7 @@ It has got improved capabilities for Go code understanding (for example, create * [Configuration guide](docs/configuration.md) * [Usage examples](docs/usage.md) * [Helix + tmux quickstart](docs/tmux.md) +* [Fish shell completion](docs/fish-completion.md) * [MCP server setup guide](docs/mcp-setup.md) *(deprecated - reference only)* * [Creating custom prompts](docs/mcp-prompts.md) *(deprecated - reference only)* diff --git a/assets/ask.fish b/assets/ask.fish new file mode 100644 index 0000000..e4d4654 --- /dev/null +++ b/assets/ask.fish @@ -0,0 +1,61 @@ +# Fish completion for ask. +# Install as ~/.config/fish/completions/ask.fish or $XDG_CONFIG_HOME/fish/completions/ask.fish. + +function __ask_needs_root_completion + set -l tokens (commandline -opc) + if test (count $tokens) -le 1 + return 0 + end + for token in $tokens[2..-1] + if not string match -qr '^-' -- $token + return 1 + end + end + return 0 +end + +function __ask_in_dep_context + set -l tokens (commandline -opc) + if test (count $tokens) -lt 2 + return 1 + end + set -l seen_dep 0 + for token in $tokens[2..-1] + if string match -qr '^-' -- $token + continue + end + if test $seen_dep -eq 0 + if test $token = dep + set seen_dep 1 + else + return 1 + end + else + return 1 + end + end + test $seen_dep -eq 1 +end + +complete -c ask -f +complete -c ask -s j -l json -d 'Emit JSON output' +complete -c ask -n '__ask_needs_root_completion' -a 'add' -d 'Create a new task' +complete -c ask -n '__ask_needs_root_completion' -a 'list' -d 'List active tasks' +complete -c ask -n '__ask_needs_root_completion' -a 'all' -d 'List all tasks' +complete -c ask -n '__ask_needs_root_completion' -a 'ready' -d 'List READY tasks' +complete -c ask -n '__ask_needs_root_completion' -a 'info' -d 'Show task details' +complete -c ask -n '__ask_needs_root_completion' -a 'annotate' -d 'Add an annotation' +complete -c ask -n '__ask_needs_root_completion' -a 'start' -d 'Start a task' +complete -c ask -n '__ask_needs_root_completion' -a 'stop' -d 'Stop a task' +complete -c ask -n '__ask_needs_root_completion' -a 'done' -d 'Mark a task complete' +complete -c ask -n '__ask_needs_root_completion' -a 'priority' -d 'Set priority' +complete -c ask -n '__ask_needs_root_completion' -a 'tag' -d 'Add or remove a tag' +complete -c ask -n '__ask_needs_root_completion' -a 'dep' -d 'Manage dependencies' +complete -c ask -n '__ask_needs_root_completion' -a 'urgency' -d 'List tasks sorted by urgency' +complete -c ask -n '__ask_needs_root_completion' -a 'modify' -d 'Modify task fields' +complete -c ask -n '__ask_needs_root_completion' -a 'denotate' -d 'Remove an annotation' +complete -c ask -n '__ask_needs_root_completion' -a 'delete' -d 'Delete a task' +complete -c ask -n '__ask_needs_root_completion' -a 'help' -d 'Show help' +complete -c ask -n '__ask_in_dep_context' -a 'add' -d 'Add a dependency' +complete -c ask -n '__ask_in_dep_context' -a 'rm' -d 'Remove a dependency' +complete -c ask -n '__ask_in_dep_context' -a 'list' -d 'List dependencies' diff --git a/docs/buildandinstall.md b/docs/buildandinstall.md index 3d70a4a..7e63ada 100644 --- a/docs/buildandinstall.md +++ b/docs/buildandinstall.md @@ -11,6 +11,7 @@ Hexai uses Mage for developer tasks. Install Mage, then run targets like build, - In restricted sandboxes/CI (no sockets), skip network-based tests: - `HEXAI_TEST_SKIP_NET=1 go test ./... -cover` - Install binaries to `GOPATH/bin`: `mage install` +- Install Fish completions: `install -Dm644 assets/ask.fish ~/.config/fish/completions/ask.fish` Note: `mage lint` uses `golangci-lint`. Install via `mage devinstall` if needed. diff --git a/docs/fish-completion.md b/docs/fish-completion.md new file mode 100644 index 0000000..116a0a6 --- /dev/null +++ b/docs/fish-completion.md @@ -0,0 +1,20 @@ +# Fish Completion + +Hexai ships a Fish completion file for the `ask` task-management CLI at [`assets/ask.fish`](../assets/ask.fish). + +It completes the top-level `ask` subcommands and the nested `ask dep` operations. +The script also preserves the global `--json` flag. + +Install it into Fish's completion directory: + +```sh +install -Dm644 assets/ask.fish ~/.config/fish/completions/ask.fish +``` + +If you use a custom XDG config directory, copy it to: + +```sh +$XDG_CONFIG_HOME/fish/completions/ask.fish +``` + +The completion file is static and ships with the repository, so it does not require a build step. diff --git a/internal/askcli/completion.go b/internal/askcli/completion.go new file mode 100644 index 0000000..f787849 --- /dev/null +++ b/internal/askcli/completion.go @@ -0,0 +1,96 @@ +package askcli + +import ( + "strings" +) + +type fishCompletionItem struct { + name string + description string +} + +var askRootCompletionItems = []fishCompletionItem{ + {name: "add", description: "Create a new task"}, + {name: "list", description: "List active tasks"}, + {name: "all", description: "List all tasks"}, + {name: "ready", description: "List READY tasks"}, + {name: "info", description: "Show task details"}, + {name: "annotate", description: "Add an annotation"}, + {name: "start", description: "Start a task"}, + {name: "stop", description: "Stop a task"}, + {name: "done", description: "Mark a task complete"}, + {name: "priority", description: "Set priority"}, + {name: "tag", description: "Add or remove a tag"}, + {name: "dep", description: "Manage dependencies"}, + {name: "urgency", description: "List tasks sorted by urgency"}, + {name: "modify", description: "Modify task fields"}, + {name: "denotate", description: "Remove an annotation"}, + {name: "delete", description: "Delete a task"}, + {name: "help", description: "Show help"}, +} + +var askDepCompletionItems = []fishCompletionItem{ + {name: "add", description: "Add a dependency"}, + {name: "rm", description: "Remove a dependency"}, + {name: "list", description: "List dependencies"}, +} + +func FishCompletion() string { + var b strings.Builder + b.WriteString("# Fish completion for ask.\n") + b.WriteString("# Install as ~/.config/fish/completions/ask.fish or") + b.WriteString(" $XDG_CONFIG_HOME/fish/completions/ask.fish.\n\n") + b.WriteString("function __ask_needs_root_completion\n") + b.WriteString(" set -l tokens (commandline -opc)\n") + b.WriteString(" if test (count $tokens) -le 1\n") + b.WriteString(" return 0\n") + b.WriteString(" end\n") + b.WriteString(" for token in $tokens[2..-1]\n") + b.WriteString(" if not string match -qr '^-' -- $token\n") + b.WriteString(" return 1\n") + b.WriteString(" end\n") + b.WriteString(" end\n") + b.WriteString(" return 0\n") + b.WriteString("end\n\n") + b.WriteString("function __ask_in_dep_context\n") + b.WriteString(" set -l tokens (commandline -opc)\n") + b.WriteString(" if test (count $tokens) -lt 2\n") + b.WriteString(" return 1\n") + b.WriteString(" end\n") + b.WriteString(" set -l seen_dep 0\n") + b.WriteString(" for token in $tokens[2..-1]\n") + b.WriteString(" if string match -qr '^-' -- $token\n") + b.WriteString(" continue\n") + b.WriteString(" end\n") + b.WriteString(" if test $seen_dep -eq 0\n") + b.WriteString(" if test $token = dep\n") + b.WriteString(" set seen_dep 1\n") + b.WriteString(" else\n") + b.WriteString(" return 1\n") + b.WriteString(" end\n") + b.WriteString(" else\n") + b.WriteString(" return 1\n") + b.WriteString(" end\n") + b.WriteString(" end\n") + b.WriteString(" test $seen_dep -eq 1\n") + b.WriteString("end\n\n") + b.WriteString("complete -c ask -f\n") + b.WriteString("complete -c ask -s j -l json -d 'Emit JSON output'\n") + for _, item := range askRootCompletionItems { + writeFishCompletionLine(&b, "__ask_needs_root_completion", item) + } + for _, item := range askDepCompletionItems { + writeFishCompletionLine(&b, "__ask_in_dep_context", item) + } + return b.String() +} + +func writeFishCompletionLine(b *strings.Builder, condition string, item fishCompletionItem) { + b.WriteString("complete -c ask -n '") + b.WriteString(condition) + b.WriteString("' -a '") + b.WriteString(item.name) + b.WriteString("' -d '") + b.WriteString(strings.ReplaceAll(item.description, "'", "\\'")) + b.WriteString("'\n") +} diff --git a/internal/askcli/completion_test.go b/internal/askcli/completion_test.go new file mode 100644 index 0000000..e717575 --- /dev/null +++ b/internal/askcli/completion_test.go @@ -0,0 +1,46 @@ +package askcli + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestFishCompletion_MatchesAsset(t *testing.T) { + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + assetPath := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "assets", "ask.fish")) + want, err := os.ReadFile(assetPath) + if err != nil { + t.Fatalf("read asset: %v", err) + } + got := FishCompletion() + if got != string(want) { + t.Fatalf("fish completion asset mismatch\n--- got ---\n%s\n--- want ---\n%s", got, string(want)) + } +} + +func TestFishCompletion_IncludesCommandsAndExcludesExport(t *testing.T) { + script := FishCompletion() + for _, name := range []string{"add", "list", "all", "ready", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete", "help"} { + if !strings.Contains(script, " -a '"+name+"' ") { + t.Fatalf("script missing root completion for %q", name) + } + } + for _, line := range []string{ + "complete -c ask -n '__ask_in_dep_context' -a 'add' -d 'Add a dependency'", + "complete -c ask -n '__ask_in_dep_context' -a 'rm' -d 'Remove a dependency'", + "complete -c ask -n '__ask_in_dep_context' -a 'list' -d 'List dependencies'", + } { + if !strings.Contains(script, line) { + t.Fatalf("script missing dep completion line %q", line) + } + } + if strings.Contains(script, "ask export") { + t.Fatalf("script should not advertise non-existent export command") + } +} |
