gabriel / muse public
omzsh-plugin.md markdown
214 lines 6.1 KB
bb2f58b3 docs(omzsh-plugin): update for muse:(domain:branch) format and icon-off… 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 (~230 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{cyan}muse:(%F{magenta}<domain>:<branch>%F{cyan})%f[ %F{red}✗ <count>%f]
30 ```
31
32 This mirrors the Git plugin's `git:(branch)` format, extended with the domain
33 name — the key differentiator from a single-domain VCS.
34
35 ### Examples
36
37 | Output | Meaning |
38 |--------|---------|
39 | `muse:(midi:main)` | `midi` domain, branch `main` |
40 | `muse:(bitcoin:lightning)` | `bitcoin` domain, branch `lightning` |
41 | `muse:(code:feature/x)` | `code` domain, branch `feature/x` |
42 | `muse:(midi:a1b2c3d4)` | detached HEAD — short SHA shown |
43 | `muse:(midi:main) ✗ 3` | dirty working tree, 3 changed paths |
44
45 The dirty segment only appears after a `muse` command runs in the current shell.
46
47 ### Domain icons (optional)
48
49 Icons are off by default. Set `MUSE_PROMPT_ICONS=1` to prepend one:
50
51 | Domain | Icon | Config key |
52 |--------|------|-----------|
53 | `midi` | `♪` | `MUSE_DOMAIN_ICONS[midi]` |
54 | `code` | `⌥` | `MUSE_DOMAIN_ICONS[code]` |
55 | `bitcoin` | `₿` | `MUSE_DOMAIN_ICONS[bitcoin]` |
56 | `scaffold` | `⬡` | `MUSE_DOMAIN_ICONS[scaffold]` |
57 | (unknown) | `◈` | `MUSE_DOMAIN_ICONS[_default]` |
58
59 With icons on, the prompt becomes `♪ muse:(midi:main)`.
60
61 ---
62
63 ## Environment variables
64
65 ### Configuration (set before `plugins=(… muse …)`)
66
67 | Variable | Default | Meaning |
68 |----------|---------|---------|
69 | `MUSE_PROMPT_ICONS` | `0` | `1` prepends a domain icon before `muse:(…)` |
70 | `MUSE_DIRTY_TIMEOUT` | `1` | Seconds before dirty check aborts |
71
72 ### State (read-only, exported by plugin)
73
74 | Variable | Type | Meaning |
75 |----------|------|---------|
76 | `MUSE_REPO_ROOT` | string | Absolute path to repo root, or `""` |
77 | `MUSE_DOMAIN` | string | Active domain name |
78 | `MUSE_BRANCH` | string | Branch name, short SHA, or `?` |
79 | `MUSE_DIRTY` | integer | `1` if working tree has changes |
80 | `MUSE_DIRTY_COUNT` | integer | Number of changed paths |
81
82 ---
83
84 ## Hooks
85
86 | Hook | When it fires | What it does |
87 |------|--------------|--------------|
88 | `chpwd` | On `cd` | Re-finds repo root, re-reads HEAD and domain; clears dirty state |
89 | `preexec` | Before any command | Sets `_MUSE_CMD_RAN=1` when command is `muse` |
90 | `precmd` | Before prompt | Runs full refresh (including dirty check) only if `_MUSE_CMD_RAN=1` |
91
92 ---
93
94 ## Aliases
95
96 | Alias | Expands to |
97 |-------|-----------|
98 | `mst` | `muse status` |
99 | `msts` | `muse status --short` |
100 | `mcm` | `muse commit -m` |
101 | `mco` | `muse checkout` |
102 | `mlg` | `muse log` |
103 | `mlgo` | `muse log --oneline` |
104 | `mlgg` | `muse log --graph` |
105 | `mdf` | `muse diff` |
106 | `mdfst` | `muse diff --stat` |
107 | `mbr` | `muse branch` |
108 | `mtg` | `muse tag` |
109 | `mfh` | `muse fetch` |
110 | `mpull` | `muse pull` |
111 | `mpush` | `muse push` |
112 | `mrm` | `muse remote` |
113
114 ---
115
116 ## Completion
117
118 The `_muse` completion function handles:
119
120 - Top-level command names with descriptions.
121 - Branch names for `checkout`, `merge`, `cherry-pick`, `branch`, `reset`, `revert`, `diff`, `show`, `blame`.
122 - Remote names for `push`, `pull`, `fetch`.
123 - Tag names for `tag`.
124 - Config key suggestions for `config`.
125 - Subcommand names for `stash`, `remote`, `plumbing`, `commit` flags.
126
127 All branch/tag/remote lookups use ZSH glob patterns against `.muse/refs/` and
128 `.muse/remotes/` — no subprocess, no `ls`, instant.
129
130 ---
131
132 ## Performance model
133
134 | Trigger | Subprocesses | What runs |
135 |---------|-------------|-----------|
136 | Prompt render | 0 | Reads cached shell vars only |
137 | `cd` into repo | 1 (`python3`) | HEAD (ZSH read) + domain (python3) |
138 | `cd` outside repo | 0 | Clears vars only |
139 | After `muse` command | 1 (`muse status`) | Full refresh + dirty check |
140 | Tab completion | 0 | ZSH glob reads `.muse/refs/` |
141
142 ---
143
144 ## Security model
145
146 ### Branch name injection
147
148 `.muse/HEAD` is read with a pure ZSH `$(<file)` — no subprocess. Muse writes
149 HEAD in one of two self-describing forms (set by `muse/core/store.py`):
150
151 ```
152 ref: refs/heads/<branch> — on a branch
153 commit: <sha256> — detached HEAD
154 ```
155
156 The branch name is validated with `[[ "$branch" =~ '^[[:alnum:]/_.-]+$' ]]`.
157 Any name containing characters outside this set (including `%`, `$`, backticks,
158 quotes) is replaced with `?`. Valid names are additionally `%`-escaped
159 (`${branch//\%/%%}`) before insertion into the prompt string, so ZSH never
160 interprets them as prompt directives.
161
162 ### Domain injection
163
164 The domain value from `.muse/repo.json` is extracted by `python3` and validated
165 with `safe.isalnum() and 1 <= len(v) <= 32` before printing. The path to
166 `repo.json` is passed via the `MUSE_REPO_JSON` environment variable — never
167 interpolated into a `-c` string — so a path containing single quotes, spaces,
168 or special characters is handled safely.
169
170 ### Path injection
171
172 `cd -- "$MUSE_REPO_ROOT"` uses `--` so the path cannot be interpreted as a
173 flag. `timeout -- ...` follows the same pattern.
174
175 ### No `eval`
176
177 No user-supplied data is ever passed to `eval`.
178
179 ### Completion safety
180
181 The completion function uses ZSH glob expansion (`${refs_dir}/*(N:t)`) instead
182 of `$(ls ...)` to enumerate branches. This avoids word-splitting on filenames
183 that contain spaces, and prevents `ls` output from being treated as shell tokens.
184
185 ---
186
187 ## Installation
188
189 ```bash
190 bash tools/install-omzsh-plugin.sh
191 ```
192
193 The script creates a symlink from `~/.oh-my-zsh/custom/plugins/muse/` to
194 `tools/omzsh-plugin/`. Because it is a symlink, pulling new commits to the Muse
195 repo automatically updates the plugin.
196
197 Add to `~/.zshrc`:
198
199 ```zsh
200 plugins=(git muse)
201 ```
202
203 Then add the prompt segment:
204
205 ```zsh
206 # Append to your existing PROMPT, or set a new one:
207 PROMPT+='$(muse_prompt_info) '
208 ```
209
210 Then reload:
211
212 ```zsh
213 exec zsh
214 ```