gabriel / muse public
omzsh-plugin.md markdown
186 lines 5.4 KB
3ed08abf fix(omzsh-plugin): match Muse HEAD format (refs/heads/<branch>, no ref:… Gabriel Cardona <gabriel@tellurstori.com> 3d ago
1 # Oh My ZSH Plugin — Reference
2
3 Minimal, secure ZSH integration for Muse. Provides a prompt segment, core
4 aliases, and tab completion. Nothing runs automatically beyond what is needed to
5 keep the prompt accurate.
6
7 ---
8
9 ## Files
10
11 | File | Purpose |
12 |------|---------|
13 | `tools/omzsh-plugin/muse.plugin.zsh` | Main plugin (~175 lines) |
14 | `tools/omzsh-plugin/_muse` | ZSH completion function |
15 | `tools/install-omzsh-plugin.sh` | Symlink installer |
16
17 ---
18
19 ## Prompt segment
20
21 ```zsh
22 # In ~/.zshrc
23 PROMPT='%~ $(muse_prompt_info) %# '
24 ```
25
26 `muse_prompt_info` emits nothing outside a Muse repo. Inside one it emits:
27
28 ```
29 %F{magenta}<icon> <branch>%f[ %F{red}✗ <count>%f]
30 ```
31
32 The dirty segment (`✗ N`) only appears after a `muse` command has run in the
33 current shell, because the dirty check requires spawning a subprocess.
34
35 ### Domain icons
36
37 | Domain | Default icon | Config key |
38 |--------|-------------|-----------|
39 | `midi` | `♪` | `MUSE_DOMAIN_ICONS[midi]` |
40 | `code` | `⌥` | `MUSE_DOMAIN_ICONS[code]` |
41 | `bitcoin` | `₿` | `MUSE_DOMAIN_ICONS[bitcoin]` |
42 | `scaffold` | `⬡` | `MUSE_DOMAIN_ICONS[scaffold]` |
43 | (unknown) | `◈` | `MUSE_DOMAIN_ICONS[_default]` |
44
45 ---
46
47 ## Environment variables
48
49 ### Configuration (set before `plugins=(… muse …)`)
50
51 | Variable | Default | Meaning |
52 |----------|---------|---------|
53 | `MUSE_PROMPT_ICONS` | `1` | `0` renders `[domain]` instead of icon |
54 | `MUSE_DIRTY_TIMEOUT` | `1` | Seconds before dirty check aborts |
55
56 ### State (read-only, exported by plugin)
57
58 | Variable | Type | Meaning |
59 |----------|------|---------|
60 | `MUSE_REPO_ROOT` | string | Absolute path to repo root, or `""` |
61 | `MUSE_DOMAIN` | string | Active domain name |
62 | `MUSE_BRANCH` | string | Branch name, short SHA, or `?` |
63 | `MUSE_DIRTY` | integer | `1` if working tree has changes |
64 | `MUSE_DIRTY_COUNT` | integer | Number of changed paths |
65
66 ---
67
68 ## Hooks
69
70 | Hook | When it fires | What it does |
71 |------|--------------|--------------|
72 | `chpwd` | On `cd` | Re-finds repo root, re-reads HEAD and domain; clears dirty state |
73 | `preexec` | Before any command | Sets `_MUSE_CMD_RAN=1` when command is `muse` |
74 | `precmd` | Before prompt | Runs full refresh (including dirty check) only if `_MUSE_CMD_RAN=1` |
75
76 ---
77
78 ## Aliases
79
80 | Alias | Expands to |
81 |-------|-----------|
82 | `mst` | `muse status` |
83 | `msts` | `muse status --short` |
84 | `mcm` | `muse commit -m` |
85 | `mco` | `muse checkout` |
86 | `mlg` | `muse log` |
87 | `mlgo` | `muse log --oneline` |
88 | `mlgg` | `muse log --graph` |
89 | `mdf` | `muse diff` |
90 | `mdfst` | `muse diff --stat` |
91 | `mbr` | `muse branch` |
92 | `mtg` | `muse tag` |
93 | `mfh` | `muse fetch` |
94 | `mpull` | `muse pull` |
95 | `mpush` | `muse push` |
96 | `mrm` | `muse remote` |
97
98 ---
99
100 ## Completion
101
102 The `_muse` completion function handles:
103
104 - Top-level command names with descriptions.
105 - Branch names for `checkout`, `merge`, `cherry-pick`, `branch`, `reset`, `revert`, `diff`, `show`, `blame`.
106 - Remote names for `push`, `pull`, `fetch`.
107 - Tag names for `tag`.
108 - Config key suggestions for `config`.
109 - Subcommand names for `stash`, `remote`, `plumbing`, `commit` flags.
110
111 All branch/tag/remote lookups use ZSH glob patterns against `.muse/refs/` and
112 `.muse/remotes/` — no subprocess, no `ls`, instant.
113
114 ---
115
116 ## Performance model
117
118 | Trigger | Subprocesses | What runs |
119 |---------|-------------|-----------|
120 | Prompt render | 0 | Reads cached shell vars only |
121 | `cd` into repo | 1 (`python3`) | HEAD (ZSH read) + domain (python3) |
122 | `cd` outside repo | 0 | Clears vars only |
123 | After `muse` command | 1 (`muse status`) | Full refresh + dirty check |
124 | Tab completion | 0 | ZSH glob reads `.muse/refs/` |
125
126 ---
127
128 ## Security model
129
130 ### Branch name injection
131
132 `.muse/HEAD` is read with a pure ZSH `$(<file)` — no subprocess. Muse writes
133 the symbolic ref as `refs/heads/<branch>` (no `ref:` prefix). The result is
134 validated with `[[ "$branch" =~ '^[[:alnum:]/_.-]+$' ]]`. Any branch name that
135 contains characters outside this set (including `%`, `$`, backticks, quotes) is
136 replaced with `?`. Valid branch names are additionally `%`-escaped
137 (`${branch//\%/%%}`) before insertion into the prompt string, so ZSH never
138 interprets them as prompt directives.
139
140 ### Domain injection
141
142 The domain value from `.muse/repo.json` is extracted by `python3` and validated
143 with `safe.isalnum() and 1 <= len(v) <= 32` before printing. The path to
144 `repo.json` is passed via the `MUSE_REPO_JSON` environment variable — never
145 interpolated into a `-c` string — so a path containing single quotes, spaces,
146 or special characters is handled safely.
147
148 ### Path injection
149
150 `cd -- "$MUSE_REPO_ROOT"` uses `--` so the path cannot be interpreted as a
151 flag. `timeout -- ...` follows the same pattern.
152
153 ### No `eval`
154
155 No user-supplied data is ever passed to `eval`. The post-hook system from the
156 original plugin that `eval`-ed `MUSE_POST_*_CMD` variables has been removed.
157
158 ### Completion safety
159
160 The completion function uses ZSH glob expansion (`${refs_dir}/*(N:t)`) instead
161 of `$(ls ...)` to enumerate branches. This avoids word-splitting on filenames
162 that contain spaces, and prevents `ls` output from being treated as shell tokens.
163
164 ---
165
166 ## Installation
167
168 ```bash
169 bash tools/install-omzsh-plugin.sh
170 ```
171
172 The script creates a symlink from `~/.oh-my-zsh/custom/plugins/muse/` to
173 `tools/omzsh-plugin/`. Because it is a symlink, pulling new commits to the Muse
174 repo automatically updates the plugin.
175
176 Add to `~/.zshrc`:
177
178 ```zsh
179 plugins=(git muse)
180 ```
181
182 Then reload:
183
184 ```zsh
185 exec zsh
186 ```