workforest 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {workforest-0.3.0 → workforest-0.4.0}/PKG-INFO +77 -24
  2. {workforest-0.3.0 → workforest-0.4.0}/README.md +76 -23
  3. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/__init__.py +1 -1
  4. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/cli.py +8 -1
  5. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/commands.py +4 -0
  6. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/completions.py +4 -4
  7. workforest-0.4.0/src/workforest/config.py +313 -0
  8. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/examples/config.yaml +78 -47
  9. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/launch.py +99 -49
  10. {workforest-0.3.0 → workforest-0.4.0}/tests/test_cli.py +12 -0
  11. {workforest-0.3.0 → workforest-0.4.0}/tests/test_config.py +158 -29
  12. {workforest-0.3.0 → workforest-0.4.0}/tests/test_launch.py +187 -58
  13. {workforest-0.3.0 → workforest-0.4.0}/tests/test_shell.py +11 -4
  14. {workforest-0.3.0 → workforest-0.4.0}/tests/test_tui.py +13 -3
  15. workforest-0.3.0/src/workforest/config.py +0 -200
  16. {workforest-0.3.0 → workforest-0.4.0}/.claude/rules/architecture.md +0 -0
  17. {workforest-0.3.0 → workforest-0.4.0}/.claude/rules/conventions.md +0 -0
  18. {workforest-0.3.0 → workforest-0.4.0}/.claude/rules/tests.md +0 -0
  19. {workforest-0.3.0 → workforest-0.4.0}/.github/workflows/ci.yml +0 -0
  20. {workforest-0.3.0 → workforest-0.4.0}/.github/workflows/publish_aur.yml +0 -0
  21. {workforest-0.3.0 → workforest-0.4.0}/.github/workflows/publish_homebrew.yml +0 -0
  22. {workforest-0.3.0 → workforest-0.4.0}/.github/workflows/publish_pypi.yml +0 -0
  23. {workforest-0.3.0 → workforest-0.4.0}/.github/workflows/release.yml +0 -0
  24. {workforest-0.3.0 → workforest-0.4.0}/.gitignore +0 -0
  25. {workforest-0.3.0 → workforest-0.4.0}/LICENSE +0 -0
  26. {workforest-0.3.0 → workforest-0.4.0}/Makefile +0 -0
  27. {workforest-0.3.0 → workforest-0.4.0}/completions/_workforest +0 -0
  28. {workforest-0.3.0 → workforest-0.4.0}/packaging/AUR/PKGBUILD.template +0 -0
  29. {workforest-0.3.0 → workforest-0.4.0}/packaging/homebrew/workforest.rb.template +0 -0
  30. {workforest-0.3.0 → workforest-0.4.0}/pyproject.toml +0 -0
  31. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/__main__.py +0 -0
  32. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/errors.py +0 -0
  33. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/gitutil.py +0 -0
  34. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/hooks.py +0 -0
  35. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/integrations/__init__.py +0 -0
  36. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/integrations/claude.py +0 -0
  37. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/output.py +0 -0
  38. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/shell/completion.bash +0 -0
  39. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/shell/completion.zsh +0 -0
  40. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/shell/workforest.sh +0 -0
  41. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/shellinit.py +0 -0
  42. {workforest-0.3.0 → workforest-0.4.0}/src/workforest/tui.py +0 -0
  43. {workforest-0.3.0 → workforest-0.4.0}/tests/__init__.py +0 -0
  44. {workforest-0.3.0 → workforest-0.4.0}/tests/conftest.py +0 -0
  45. {workforest-0.3.0 → workforest-0.4.0}/tests/test_claude.py +0 -0
  46. {workforest-0.3.0 → workforest-0.4.0}/tests/test_commands.py +0 -0
  47. {workforest-0.3.0 → workforest-0.4.0}/tests/test_gitutil.py +0 -0
  48. {workforest-0.3.0 → workforest-0.4.0}/tests/test_harness.py +0 -0
  49. {workforest-0.3.0 → workforest-0.4.0}/tests/test_hooks.py +0 -0
  50. {workforest-0.3.0 → workforest-0.4.0}/tests/test_output.py +0 -0
  51. {workforest-0.3.0 → workforest-0.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: workforest
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Git worktree forest management: create, open, and clean up per-branch worktrees with project-defined setup hooks
5
5
  Project-URL: Homepage, https://github.com/ArkadyBuryakov/workforest
6
6
  Author-email: Arkady Buryakov <arkady@buryakov.pro>
@@ -44,8 +44,8 @@ opened, and cleaned up with one command.
44
44
  - **Create** a worktree for any branch (local, remote, or brand new) and have
45
45
  it set up automatically: symlinks for untracked assets (`node_modules`,
46
46
  `.env`, …) and project-defined setup scripts.
47
- - **Open** it in your editor — in the current shell, or in a new terminal
48
- window via a configurable command template.
47
+ - **Open** it in your editor — in your terminal, in a new terminal window or
48
+ multiplexer pane, or in a GUI app, all from a small shell-command config.
49
49
  - **Run** named project scripts with well-known `WF_*` environment variables.
50
50
  - **Delete** worktrees safely, or **checkout**: collapse one back into the
51
51
  main checkout.
@@ -106,27 +106,77 @@ Layered, YAML or JSON; later layers override earlier ones:
106
106
  | project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
107
107
  | project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
108
108
 
109
- Scalars and lists replace; the `scripts`/`openers` mappings merge per key
110
- (`null` removes an entry). `workforest config` shows the merged result and
111
- where each layer came from; `workforest init` scaffolds a project file
112
- (`--local` for a personal one).
109
+ Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
110
+ merge per key (`null` removes an entry). `workforest config` shows the
111
+ merged result and where each layer came from; `workforest init` scaffolds a
112
+ project file (`--local` for a personal one). Nothing in the environment
113
+ changes the result — files and flags only.
113
114
 
114
115
  All keys, with defaults:
115
116
 
116
117
  ```yaml
117
118
  worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
118
- opener: "" # default opener; "" → $VISUAL → $EDITOR
119
- openers: {} # name -> shell command, e.g. edit: '$EDITOR "$WF_TARGET"'
120
- window_command: "" # "" → current shell; or e.g. kitty --title "$WF_TITLE"
121
- # --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
119
+ opener: "" # default opener: an `openers` name or a shell command;
120
+ # "" → $VISUAL → $EDITOR
121
+ openers: {} # name -> what `-o NAME` runs, and where
122
+ wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
122
123
  symlinks: [] # untracked assets linked from main into new worktrees
123
124
  setup_scripts: [] # shell snippets run in a fresh worktree
124
125
  scripts: {} # name -> snippet for `wf run NAME`
125
126
  ```
126
127
 
127
- Openers and `window_command` are plain shell commands, run via `$SHELL -c`
128
- with one variable family in the environment — the same family the launched
129
- process and every script receive:
128
+ ### Openers
129
+
130
+ An opener is a shell command that runs with the worktree root as working
131
+ directory, either **in your terminal** (the default: the `wf` wrapper does
132
+ `cd` there and runs it) or **in the background** (`background: true`:
133
+ spawned detached, `wf` returns immediately — for GUI apps and commands that
134
+ hand off to a daemon or multiplexer). Optionally it runs **through a
135
+ wrapper**, a command that receives it as `$WF_COMMAND` and takes it
136
+ somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
137
+ no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
138
+
139
+ ```yaml
140
+ opener: win # what `wf create` / `wf open` run by default
141
+
142
+ wrappers: # get the opener command as $WF_COMMAND
143
+ kitty:
144
+ command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
145
+ background: true
146
+ tmux: # the tmux server has its own environment: WF_ENV re-creates ours
147
+ command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
148
+ background: true
149
+
150
+ openers:
151
+ edit: $EDITOR "$WF_TARGET" # in your terminal
152
+ code: # GUI app: detached
153
+ command: code "$WF_WORKTREE"
154
+ background: true
155
+ kitty: # edit's command, in a new kitty window
156
+ from: edit
157
+ wrap: kitty
158
+ tmux: # edit's command, in a new tmux window
159
+ from: edit
160
+ wrap: tmux
161
+ git: lazygit
162
+ ```
163
+
164
+ An entry is a shell command string, or a mapping with either `command` (a
165
+ shell command, never a name) or `from` (another opener's command — one
166
+ level: the target has a `command` of its own — inheriting its `background`
167
+ unless the entry sets one), plus optionally `background: true` or
168
+ `wrap: NAME`. `wrap` and `background` never sit on the same entry: the
169
+ wrapper decides where the whole thing runs, via its own `background` flag.
170
+ `-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
171
+ `-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
172
+ none). Wrappers are environment-specific, so they belong in the per-machine
173
+ user config — a host without your terminal emulator simply has none.
174
+ `from` and `wrap` names are checked when the config loads, so `wf config`
175
+ reports a misspelled one.
176
+
177
+ All of them are plain shell commands, run via `$SHELL -c` with one variable
178
+ family in the environment — the same family the launched process and every
179
+ script receive:
130
180
 
131
181
  | Variable | Value |
132
182
  |---|---|
@@ -137,18 +187,21 @@ process and every script receive:
137
187
  | `WF_BRANCH` | its branch (empty if detached) |
138
188
  | `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
139
189
  | `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
190
+ | `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
191
+ | `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
140
192
 
141
193
  Standard shell rules apply — there is no workforest template syntax:
142
194
  `"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
143
195
  braces, pipes, and `&&` mean whatever your shell says they mean (a
144
- misspelled `$WF_VAR` expands to empty, as in any shell). Openers run with
145
- the worktree root as working directory. In `window_command` the resolved
146
- opener command is additionally available as `$WF_COMMAND` — still
147
- unexpanded, so run it through a shell of its own for its `$WF_*` references
148
- to resolve: `$SHELL -c "$WF_COMMAND"`. Spawned windows shed activation
149
- state inherited from the invoking shell (Python venv, conda, nvm, rvm) so
150
- the new session starts clean instead of carrying an environment it cannot
151
- deactivate.
196
+ misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
197
+ reaches the wrapper unexpanded, so run it through a shell of its own for its
198
+ `$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
199
+ runs somewhere this environment is not inherited — a tmux server, an ssh
200
+ host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
201
+ re-parsed on the far side, quoting intact. Background
202
+ processes shed activation state inherited from the invoking shell (Python
203
+ venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
204
+ environment it cannot deactivate.
152
205
 
153
206
  Fully commented reference configs:
154
207
  [`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
@@ -187,8 +240,8 @@ scripts:
187
240
  ## Commands
188
241
 
189
242
  ```
190
- workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
191
- workforest open [NAME] [-o OPENER] [-p PATH]
243
+ workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
244
+ workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
192
245
  workforest list [--porcelain]
193
246
  workforest delete NAME... [--force] [--delete-branch | --keep-branch]
194
247
  workforest checkout NAME [--force]
@@ -24,8 +24,8 @@ opened, and cleaned up with one command.
24
24
  - **Create** a worktree for any branch (local, remote, or brand new) and have
25
25
  it set up automatically: symlinks for untracked assets (`node_modules`,
26
26
  `.env`, …) and project-defined setup scripts.
27
- - **Open** it in your editor — in the current shell, or in a new terminal
28
- window via a configurable command template.
27
+ - **Open** it in your editor — in your terminal, in a new terminal window or
28
+ multiplexer pane, or in a GUI app, all from a small shell-command config.
29
29
  - **Run** named project scripts with well-known `WF_*` environment variables.
30
30
  - **Delete** worktrees safely, or **checkout**: collapse one back into the
31
31
  main checkout.
@@ -86,27 +86,77 @@ Layered, YAML or JSON; later layers override earlier ones:
86
86
  | project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
87
87
  | project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
88
88
 
89
- Scalars and lists replace; the `scripts`/`openers` mappings merge per key
90
- (`null` removes an entry). `workforest config` shows the merged result and
91
- where each layer came from; `workforest init` scaffolds a project file
92
- (`--local` for a personal one).
89
+ Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
90
+ merge per key (`null` removes an entry). `workforest config` shows the
91
+ merged result and where each layer came from; `workforest init` scaffolds a
92
+ project file (`--local` for a personal one). Nothing in the environment
93
+ changes the result — files and flags only.
93
94
 
94
95
  All keys, with defaults:
95
96
 
96
97
  ```yaml
97
98
  worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
98
- opener: "" # default opener; "" → $VISUAL → $EDITOR
99
- openers: {} # name -> shell command, e.g. edit: '$EDITOR "$WF_TARGET"'
100
- window_command: "" # "" → current shell; or e.g. kitty --title "$WF_TITLE"
101
- # --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
99
+ opener: "" # default opener: an `openers` name or a shell command;
100
+ # "" → $VISUAL → $EDITOR
101
+ openers: {} # name -> what `-o NAME` runs, and where
102
+ wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
102
103
  symlinks: [] # untracked assets linked from main into new worktrees
103
104
  setup_scripts: [] # shell snippets run in a fresh worktree
104
105
  scripts: {} # name -> snippet for `wf run NAME`
105
106
  ```
106
107
 
107
- Openers and `window_command` are plain shell commands, run via `$SHELL -c`
108
- with one variable family in the environment — the same family the launched
109
- process and every script receive:
108
+ ### Openers
109
+
110
+ An opener is a shell command that runs with the worktree root as working
111
+ directory, either **in your terminal** (the default: the `wf` wrapper does
112
+ `cd` there and runs it) or **in the background** (`background: true`:
113
+ spawned detached, `wf` returns immediately — for GUI apps and commands that
114
+ hand off to a daemon or multiplexer). Optionally it runs **through a
115
+ wrapper**, a command that receives it as `$WF_COMMAND` and takes it
116
+ somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
117
+ no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
118
+
119
+ ```yaml
120
+ opener: win # what `wf create` / `wf open` run by default
121
+
122
+ wrappers: # get the opener command as $WF_COMMAND
123
+ kitty:
124
+ command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
125
+ background: true
126
+ tmux: # the tmux server has its own environment: WF_ENV re-creates ours
127
+ command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
128
+ background: true
129
+
130
+ openers:
131
+ edit: $EDITOR "$WF_TARGET" # in your terminal
132
+ code: # GUI app: detached
133
+ command: code "$WF_WORKTREE"
134
+ background: true
135
+ kitty: # edit's command, in a new kitty window
136
+ from: edit
137
+ wrap: kitty
138
+ tmux: # edit's command, in a new tmux window
139
+ from: edit
140
+ wrap: tmux
141
+ git: lazygit
142
+ ```
143
+
144
+ An entry is a shell command string, or a mapping with either `command` (a
145
+ shell command, never a name) or `from` (another opener's command — one
146
+ level: the target has a `command` of its own — inheriting its `background`
147
+ unless the entry sets one), plus optionally `background: true` or
148
+ `wrap: NAME`. `wrap` and `background` never sit on the same entry: the
149
+ wrapper decides where the whole thing runs, via its own `background` flag.
150
+ `-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
151
+ `-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
152
+ none). Wrappers are environment-specific, so they belong in the per-machine
153
+ user config — a host without your terminal emulator simply has none.
154
+ `from` and `wrap` names are checked when the config loads, so `wf config`
155
+ reports a misspelled one.
156
+
157
+ All of them are plain shell commands, run via `$SHELL -c` with one variable
158
+ family in the environment — the same family the launched process and every
159
+ script receive:
110
160
 
111
161
  | Variable | Value |
112
162
  |---|---|
@@ -117,18 +167,21 @@ process and every script receive:
117
167
  | `WF_BRANCH` | its branch (empty if detached) |
118
168
  | `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
119
169
  | `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
170
+ | `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
171
+ | `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
120
172
 
121
173
  Standard shell rules apply — there is no workforest template syntax:
122
174
  `"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
123
175
  braces, pipes, and `&&` mean whatever your shell says they mean (a
124
- misspelled `$WF_VAR` expands to empty, as in any shell). Openers run with
125
- the worktree root as working directory. In `window_command` the resolved
126
- opener command is additionally available as `$WF_COMMAND` — still
127
- unexpanded, so run it through a shell of its own for its `$WF_*` references
128
- to resolve: `$SHELL -c "$WF_COMMAND"`. Spawned windows shed activation
129
- state inherited from the invoking shell (Python venv, conda, nvm, rvm) so
130
- the new session starts clean instead of carrying an environment it cannot
131
- deactivate.
176
+ misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
177
+ reaches the wrapper unexpanded, so run it through a shell of its own for its
178
+ `$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
179
+ runs somewhere this environment is not inherited — a tmux server, an ssh
180
+ host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
181
+ re-parsed on the far side, quoting intact. Background
182
+ processes shed activation state inherited from the invoking shell (Python
183
+ venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
184
+ environment it cannot deactivate.
132
185
 
133
186
  Fully commented reference configs:
134
187
  [`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
@@ -167,8 +220,8 @@ scripts:
167
220
  ## Commands
168
221
 
169
222
  ```
170
- workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
171
- workforest open [NAME] [-o OPENER] [-p PATH]
223
+ workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
224
+ workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
172
225
  workforest list [--porcelain]
173
226
  workforest delete NAME... [--force] [--delete-branch | --keep-branch]
174
227
  workforest checkout NAME [--force]
@@ -1,3 +1,3 @@
1
1
  """Workforest — git worktree forest management."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
@@ -68,6 +68,7 @@ def _handle_create(ns: argparse.Namespace) -> CommandResult:
68
68
  ctx,
69
69
  ns.branch,
70
70
  opener=ns.opener,
71
+ wrap=ns.wrap,
71
72
  path_arg=ns.path,
72
73
  no_hooks=ns.no_hooks,
73
74
  no_open=ns.no_open,
@@ -76,7 +77,7 @@ def _handle_create(ns: argparse.Namespace) -> CommandResult:
76
77
 
77
78
  def _handle_open(ns: argparse.Namespace) -> CommandResult:
78
79
  ctx = commands.build_context()
79
- return commands.cmd_open(ctx, ns.name, opener=ns.opener, path_arg=ns.path)
80
+ return commands.cmd_open(ctx, ns.name, opener=ns.opener, wrap=ns.wrap, path_arg=ns.path)
80
81
 
81
82
 
82
83
  def _handle_list(ns: argparse.Namespace) -> CommandResult:
@@ -149,6 +150,12 @@ def build_parser() -> argparse.ArgumentParser:
149
150
 
150
151
  def opener_args(p: argparse.ArgumentParser) -> None:
151
152
  p.add_argument("-o", "--opener", help="opener name or shell command")
153
+ p.add_argument(
154
+ "-w",
155
+ "--wrap",
156
+ metavar="WRAPPER",
157
+ help="run the opener through this `wrappers` entry ('' for none)",
158
+ )
152
159
  p.add_argument(
153
160
  "-p", "--path", help="path inside the worktree, passed to the opener as $WF_TARGET"
154
161
  )
@@ -118,6 +118,7 @@ def cmd_create(
118
118
  branch: str | None,
119
119
  *,
120
120
  opener: str | None = None,
121
+ wrap: str | None = None,
121
122
  path_arg: str | None = None,
122
123
  no_hooks: bool = False,
123
124
  no_open: bool = False,
@@ -164,6 +165,7 @@ def cmd_create(
164
165
  worktrees_dir=ctx.worktrees_dir,
165
166
  branch=branch,
166
167
  opener_arg=opener,
168
+ wrap_arg=wrap,
167
169
  path_arg=path_arg,
168
170
  )
169
171
 
@@ -173,6 +175,7 @@ def cmd_open(
173
175
  name: str | None,
174
176
  *,
175
177
  opener: str | None = None,
178
+ wrap: str | None = None,
176
179
  path_arg: str | None = None,
177
180
  ) -> CommandResult:
178
181
  if name:
@@ -190,6 +193,7 @@ def cmd_open(
190
193
  worktrees_dir=ctx.worktrees_dir,
191
194
  branch=worktree.branch,
192
195
  opener_arg=opener,
196
+ wrap_arg=wrap,
193
197
  path_arg=path_arg,
194
198
  )
195
199
 
@@ -7,7 +7,7 @@ names; the `commands` topic emits `NAME<TAB>KIND<TAB>DESCRIPTION` and the
7
7
  (zsh) do, while others take field 1.
8
8
  """
9
9
 
10
- from workforest import commands, gitutil
10
+ from workforest import commands, gitutil, launch
11
11
  from workforest.config import Config, load_config
12
12
  from workforest.errors import WorkforestError
13
13
 
@@ -50,13 +50,13 @@ def _commands() -> list[str]:
50
50
  from workforest.cli import SUBCOMMAND_HELP, _known_subcommands
51
51
 
52
52
  known = _known_subcommands()
53
- openers = _config().openers
53
+ config = _config()
54
54
  # An opener sharing a subcommand's name is shadowed by it (cli._preprocess
55
55
  # dispatches known subcommands first), so don't offer it.
56
56
  return [f"{name}\tcommand\t{SUBCOMMAND_HELP[name]}" for name in sorted(known)] + [
57
57
  # collapse whitespace: a tab/newline in an opener command would break the line protocol
58
- f"{name}\topener\t{' '.join(openers[name].split())}"
59
- for name in sorted(openers)
58
+ f"{name}\topener\t{' '.join(launch.describe_opener(config, name).split())}"
59
+ for name in sorted(config.openers)
60
60
  if name not in known
61
61
  ]
62
62