summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-26 22:07:07 +0200
committerPaul Buetow <paul@buetow.org>2026-03-26 22:07:07 +0200
commitb10c6247e14a3acfe20e78426e131691e205a601 (patch)
tree907bb5b3ffa8239584ae9b7c2bfc34667b095b62
parentbef3cc7dd95745a5724d3569e45fe7be4aba02ee (diff)
ask: add fish completions for task CLI
-rw-r--r--README.md4
-rw-r--r--assets/ask.fish61
-rw-r--r--docs/buildandinstall.md1
-rw-r--r--docs/fish-completion.md20
-rw-r--r--internal/askcli/completion.go96
-rw-r--r--internal/askcli/completion_test.go46
6 files changed, 227 insertions, 1 deletions
diff --git a/README.md b/README.md
index 872c23a..acecf0e 100644
--- a/README.md
+++ b/README.md
@@ -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")
+ }
+}