git-worktree-wrapper 0.1.0__py3-none-any.whl
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.
- git_worktree_wrapper-0.1.0.dist-info/METADATA +473 -0
- git_worktree_wrapper-0.1.0.dist-info/RECORD +35 -0
- git_worktree_wrapper-0.1.0.dist-info/WHEEL +4 -0
- git_worktree_wrapper-0.1.0.dist-info/entry_points.txt +2 -0
- gww/__init__.py +3 -0
- gww/actions/__init__.py +224 -0
- gww/actions/types.py +187 -0
- gww/cli/__init__.py +1 -0
- gww/cli/commands/__init__.py +1 -0
- gww/cli/commands/add.py +122 -0
- gww/cli/commands/clone.py +97 -0
- gww/cli/commands/init.py +147 -0
- gww/cli/commands/migrate.py +81 -0
- gww/cli/commands/pull.py +62 -0
- gww/cli/commands/remove.py +153 -0
- gww/cli/context.py +382 -0
- gww/cli/main.py +285 -0
- gww/config/__init__.py +1 -0
- gww/config/loader.py +305 -0
- gww/config/resolver.py +188 -0
- gww/config/validator.py +344 -0
- gww/git/__init__.py +1 -0
- gww/git/branch.py +264 -0
- gww/git/repository.py +403 -0
- gww/git/worktree.py +395 -0
- gww/migration/__init__.py +44 -0
- gww/migration/executor.py +342 -0
- gww/migration/planner.py +260 -0
- gww/template/__init__.py +1 -0
- gww/template/evaluator.py +281 -0
- gww/template/functions.py +378 -0
- gww/utils/__init__.py +1 -0
- gww/utils/shell.py +894 -0
- gww/utils/uri.py +171 -0
- gww/utils/xdg.py +71 -0
gww/utils/shell.py
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
"""Shell autocompletion generation utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from gww.utils.xdg import APP_NAME
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_completion_path(shell: str) -> Path:
|
|
12
|
+
"""Get the default installation path for completion scripts.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
shell: Shell name (bash, zsh, fish).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Path where completion script should be installed.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If shell is not supported.
|
|
22
|
+
"""
|
|
23
|
+
home = Path.home()
|
|
24
|
+
|
|
25
|
+
if shell == "bash":
|
|
26
|
+
return home / ".bash_completion.d" / APP_NAME
|
|
27
|
+
elif shell == "zsh":
|
|
28
|
+
return home / ".zsh" / "completions" / f"_{APP_NAME}"
|
|
29
|
+
elif shell == "fish":
|
|
30
|
+
return home / ".config" / "fish" / "completions" / f"{APP_NAME}.fish"
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"Unsupported shell: {shell}. Must be one of: bash, zsh, fish")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_aliases_path(shell: str) -> Path | dict[str, Path]:
|
|
36
|
+
"""Get the default installation path for alias functions.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
shell: Shell name (bash, zsh, fish).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
For bash/zsh: Path where aliases script should be installed.
|
|
43
|
+
For fish: Dictionary mapping function name to its file path.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If shell is not supported.
|
|
47
|
+
"""
|
|
48
|
+
home = Path.home()
|
|
49
|
+
|
|
50
|
+
if shell == "bash":
|
|
51
|
+
return home / ".bash_completion.d" / f"{APP_NAME}-aliases"
|
|
52
|
+
elif shell == "zsh":
|
|
53
|
+
return home / ".zsh" / "functions" / f"{APP_NAME}-aliases"
|
|
54
|
+
elif shell == "fish":
|
|
55
|
+
functions_dir = home / ".config" / "fish" / "functions"
|
|
56
|
+
return {
|
|
57
|
+
"gwc": functions_dir / "gwc.fish",
|
|
58
|
+
"gwa": functions_dir / "gwa.fish",
|
|
59
|
+
"gwr": functions_dir / "gwr.fish",
|
|
60
|
+
}
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unsupported shell: {shell}. Must be one of: bash, zsh, fish")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_BASH_REMOVE_AWK = r'''
|
|
66
|
+
/^worktree / { path = substr($0, 10) }
|
|
67
|
+
/^HEAD / { head = substr($0, 6) }
|
|
68
|
+
/^branch refs\/heads\// { branch = substr($0, 19) }
|
|
69
|
+
/^$/ {
|
|
70
|
+
if (skip) {
|
|
71
|
+
skip = 0
|
|
72
|
+
path = ""; branch = ""; head = ""
|
|
73
|
+
} else if (path != "") {
|
|
74
|
+
if (branch != "") {
|
|
75
|
+
print path
|
|
76
|
+
print branch
|
|
77
|
+
} else {
|
|
78
|
+
print path
|
|
79
|
+
}
|
|
80
|
+
path = ""; branch = ""; head = ""
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
END {
|
|
84
|
+
if (path != "" && !skip) {
|
|
85
|
+
if (branch != "") {
|
|
86
|
+
print path
|
|
87
|
+
print branch
|
|
88
|
+
} else {
|
|
89
|
+
print path
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
'''.strip("\n")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def generate_bash_completion() -> str:
|
|
97
|
+
"""Generate bash completion script.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Bash completion script content.
|
|
101
|
+
"""
|
|
102
|
+
return f'''# Bash completion for {APP_NAME}
|
|
103
|
+
# Generated by {APP_NAME} init shell bash
|
|
104
|
+
|
|
105
|
+
_{APP_NAME}_completions() {{
|
|
106
|
+
local cur prev opts
|
|
107
|
+
COMPREPLY=()
|
|
108
|
+
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
109
|
+
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
|
110
|
+
|
|
111
|
+
# Main commands
|
|
112
|
+
local commands="clone add remove pull migrate init"
|
|
113
|
+
|
|
114
|
+
# Options
|
|
115
|
+
local global_opts="--help --verbose --quiet"
|
|
116
|
+
|
|
117
|
+
case "${{COMP_CWORD}}" in
|
|
118
|
+
1)
|
|
119
|
+
COMPREPLY=( $(compgen -W "${{commands}}" -- "${{cur}}") )
|
|
120
|
+
return 0
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
|
|
124
|
+
case "${{prev}}" in
|
|
125
|
+
clone)
|
|
126
|
+
# No specific completions for clone URI
|
|
127
|
+
return 0
|
|
128
|
+
;;
|
|
129
|
+
add)
|
|
130
|
+
# Complete with branch names
|
|
131
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
132
|
+
local branches=$(git branch --format='%(refname:short)' 2>/dev/null)
|
|
133
|
+
local remote_branches=$(git branch -r --format='%(refname:short)' 2>/dev/null | sed 's/origin\\///')
|
|
134
|
+
COMPREPLY=( $(compgen -W "${{branches}} ${{remote_branches}}" -- "${{cur}}") )
|
|
135
|
+
fi
|
|
136
|
+
return 0
|
|
137
|
+
;;
|
|
138
|
+
remove)
|
|
139
|
+
# Complete with worktree paths and checked-out branches only (not all branches).
|
|
140
|
+
# `gwr` mirrors `gww remove` which accepts a branch or a path.
|
|
141
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
142
|
+
# First entry of `git worktree list --porcelain` is the source repo — git's invariant. Skip it.
|
|
143
|
+
local candidates=$(git worktree list --porcelain 2>/dev/null | awk -v skip=1 '{_BASH_REMOVE_AWK}')
|
|
144
|
+
COMPREPLY=( $(compgen -W "${{candidates}}" -- "${{cur}}") )
|
|
145
|
+
fi
|
|
146
|
+
return 0
|
|
147
|
+
;;
|
|
148
|
+
-t|--tag)
|
|
149
|
+
# Tag value is a free-form key[=value]; no completion to offer.
|
|
150
|
+
return 0
|
|
151
|
+
;;
|
|
152
|
+
init)
|
|
153
|
+
COMPREPLY=( $(compgen -W "config shell" -- "${{cur}}") )
|
|
154
|
+
return 0
|
|
155
|
+
;;
|
|
156
|
+
shell)
|
|
157
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${{cur}}") )
|
|
158
|
+
return 0
|
|
159
|
+
;;
|
|
160
|
+
migrate)
|
|
161
|
+
# Complete with directories
|
|
162
|
+
COMPREPLY=( $(compgen -d -- "${{cur}}") )
|
|
163
|
+
return 0
|
|
164
|
+
;;
|
|
165
|
+
-c|--create-branch)
|
|
166
|
+
return 0
|
|
167
|
+
;;
|
|
168
|
+
-f|--force)
|
|
169
|
+
return 0
|
|
170
|
+
;;
|
|
171
|
+
--dry-run|-n|--move)
|
|
172
|
+
return 0
|
|
173
|
+
;;
|
|
174
|
+
esac
|
|
175
|
+
|
|
176
|
+
# Default to global options
|
|
177
|
+
COMPREPLY=( $(compgen -W "${{global_opts}}" -- "${{cur}}") )
|
|
178
|
+
return 0
|
|
179
|
+
}}
|
|
180
|
+
|
|
181
|
+
complete -F _{APP_NAME}_completions {APP_NAME}
|
|
182
|
+
'''
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_ZSH_REMOVE_AWK = r'''
|
|
186
|
+
/^worktree / { path = substr($0, 10) }
|
|
187
|
+
/^HEAD / { head = substr($0, 6) }
|
|
188
|
+
/^branch refs\/heads\// { branch = substr($0, 19) }
|
|
189
|
+
/^$/ {
|
|
190
|
+
if (skip) {
|
|
191
|
+
skip = 0
|
|
192
|
+
path = ""; branch = ""; head = ""
|
|
193
|
+
} else if (path != "") {
|
|
194
|
+
if (branch != "") {
|
|
195
|
+
print path "\tpath (branch: " branch ")"
|
|
196
|
+
print branch "\tbranch (worktree at " path ")"
|
|
197
|
+
} else {
|
|
198
|
+
print path "\tpath (detached at " substr(head, 1, 7) ")"
|
|
199
|
+
}
|
|
200
|
+
path = ""; branch = ""; head = ""
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
END {
|
|
204
|
+
if (path != "" && !skip) {
|
|
205
|
+
if (branch != "") {
|
|
206
|
+
print path "\tpath (branch: " branch ")"
|
|
207
|
+
print branch "\tbranch (worktree at " path ")"
|
|
208
|
+
} else {
|
|
209
|
+
print path "\tpath (detached at " substr(head, 1, 7) ")"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
'''.strip("\n")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def generate_zsh_completion() -> str:
|
|
217
|
+
"""Generate zsh completion script.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Zsh completion script content.
|
|
221
|
+
"""
|
|
222
|
+
return f'''#compdef {APP_NAME}
|
|
223
|
+
# Zsh completion for {APP_NAME}
|
|
224
|
+
# Generated by {APP_NAME} init shell zsh
|
|
225
|
+
|
|
226
|
+
_{APP_NAME}() {{
|
|
227
|
+
local -a commands
|
|
228
|
+
commands=(
|
|
229
|
+
'clone:Clone a repository to configured location'
|
|
230
|
+
'add:Add a worktree for a branch'
|
|
231
|
+
'remove:Remove a worktree'
|
|
232
|
+
'pull:Update source repository'
|
|
233
|
+
'migrate:Migrate repositories to new locations'
|
|
234
|
+
'init:Initialize config or shell completion'
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
local -a init_commands
|
|
238
|
+
init_commands=(
|
|
239
|
+
'config:Create default configuration file'
|
|
240
|
+
'shell:Install shell completion'
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
local -a shells
|
|
244
|
+
shells=(bash zsh fish)
|
|
245
|
+
|
|
246
|
+
_arguments -C \\
|
|
247
|
+
'1: :->command' \\
|
|
248
|
+
'*: :->args' \\
|
|
249
|
+
&& return 0
|
|
250
|
+
|
|
251
|
+
case $state in
|
|
252
|
+
command)
|
|
253
|
+
_describe -t commands '{APP_NAME} command' commands
|
|
254
|
+
;;
|
|
255
|
+
args)
|
|
256
|
+
case $words[2] in
|
|
257
|
+
clone)
|
|
258
|
+
_message 'repository URI'
|
|
259
|
+
;;
|
|
260
|
+
add)
|
|
261
|
+
_arguments \\
|
|
262
|
+
'-c[Create branch if not exists]' \\
|
|
263
|
+
'--create-branch[Create branch if not exists]' \\
|
|
264
|
+
'1:branch:_git_branch_names'
|
|
265
|
+
;;
|
|
266
|
+
remove)
|
|
267
|
+
_arguments \\
|
|
268
|
+
'-f[Force removal]' \\
|
|
269
|
+
'--force[Force removal]' \\
|
|
270
|
+
'-t[Tag in key=value form]:tag:' \\
|
|
271
|
+
'--tag[Tag in key=value form]:tag:' \\
|
|
272
|
+
'1:branch or path:_{APP_NAME}_worktrees'
|
|
273
|
+
;;
|
|
274
|
+
pull)
|
|
275
|
+
;;
|
|
276
|
+
migrate)
|
|
277
|
+
_arguments \\
|
|
278
|
+
'-n[Dry run]' \\
|
|
279
|
+
'--dry-run[Dry run]' \\
|
|
280
|
+
'--move[Move instead of copy]' \\
|
|
281
|
+
'1:old repos directory:_files -/'
|
|
282
|
+
;;
|
|
283
|
+
init)
|
|
284
|
+
case $words[3] in
|
|
285
|
+
shell)
|
|
286
|
+
_describe -t shells 'shell' shells
|
|
287
|
+
;;
|
|
288
|
+
*)
|
|
289
|
+
_describe -t init_commands 'init command' init_commands
|
|
290
|
+
;;
|
|
291
|
+
esac
|
|
292
|
+
;;
|
|
293
|
+
esac
|
|
294
|
+
;;
|
|
295
|
+
esac
|
|
296
|
+
}}
|
|
297
|
+
|
|
298
|
+
_{APP_NAME}_worktrees() {{
|
|
299
|
+
local -a candidates
|
|
300
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
301
|
+
# First entry of `git worktree list --porcelain` is the source repo — git's invariant. Skip it.
|
|
302
|
+
local raw=$(git worktree list --porcelain 2>/dev/null | awk -v skip=1 '{_ZSH_REMOVE_AWK}')
|
|
303
|
+
local IFS=$'\\n'
|
|
304
|
+
for entry in $raw; do
|
|
305
|
+
candidates+=("$entry")
|
|
306
|
+
done
|
|
307
|
+
fi
|
|
308
|
+
_describe -t worktrees 'worktree' candidates
|
|
309
|
+
}}
|
|
310
|
+
|
|
311
|
+
_git_branch_names() {{
|
|
312
|
+
local -a branches
|
|
313
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
314
|
+
branches=($(git branch --format='%(refname:short)' 2>/dev/null))
|
|
315
|
+
_describe -t branches 'branch' branches
|
|
316
|
+
fi
|
|
317
|
+
}}
|
|
318
|
+
|
|
319
|
+
_{APP_NAME} "$@"
|
|
320
|
+
'''
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
_FISH_REMOVE_AWK = r'''
|
|
324
|
+
/^worktree / { path = substr($0, 10) }
|
|
325
|
+
/^HEAD / { head = substr($0, 6) }
|
|
326
|
+
/^branch refs\/heads\// { branch = substr($0, 19) }
|
|
327
|
+
/^$/ {
|
|
328
|
+
if (skip) {
|
|
329
|
+
skip = 0
|
|
330
|
+
path = ""; branch = ""; head = ""
|
|
331
|
+
} else if (path != "") {
|
|
332
|
+
if (branch != "") {
|
|
333
|
+
print path "\tpath (branch: " branch ")"
|
|
334
|
+
print branch "\tbranch (worktree at " path ")"
|
|
335
|
+
} else {
|
|
336
|
+
print path "\tpath (detached at " substr(head, 1, 7) ")"
|
|
337
|
+
}
|
|
338
|
+
path = ""; branch = ""; head = ""
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
END {
|
|
342
|
+
if (path != "" && !skip) {
|
|
343
|
+
if (branch != "") {
|
|
344
|
+
print path "\tpath (branch: " branch ")"
|
|
345
|
+
print branch "\tbranch (worktree at " path ")"
|
|
346
|
+
} else {
|
|
347
|
+
print path "\tpath (detached at " substr(head, 1, 7) ")"
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
'''.strip("\n")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def generate_fish_completion() -> str:
|
|
355
|
+
"""Generate fish completion script.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Fish completion script content.
|
|
359
|
+
"""
|
|
360
|
+
return f'''# Fish completion for {APP_NAME}
|
|
361
|
+
# Generated by {APP_NAME} init shell fish
|
|
362
|
+
|
|
363
|
+
# Source git.fish to import __fish_git_branches and other git completion functions
|
|
364
|
+
# This uses $__fish_data_dir which is set by fish at runtime
|
|
365
|
+
if test -f $__fish_data_dir/completions/git.fish
|
|
366
|
+
source $__fish_data_dir/completions/git.fish
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Disable file completions for all commands
|
|
370
|
+
complete -c {APP_NAME} -f
|
|
371
|
+
|
|
372
|
+
# Main commands
|
|
373
|
+
complete -c {APP_NAME} -n __fish_use_subcommand -a clone -d 'Clone a repository'
|
|
374
|
+
complete -c {APP_NAME} -n __fish_use_subcommand -a add -d 'Add a worktree'
|
|
375
|
+
complete -c {APP_NAME} -n __fish_use_subcommand -a remove -d 'Remove a worktree'
|
|
376
|
+
complete -c {APP_NAME} -n __fish_use_subcommand -a pull -d 'Update source repository'
|
|
377
|
+
complete -c {APP_NAME} -n __fish_use_subcommand -a migrate -d 'Migrate repositories'
|
|
378
|
+
complete -c {APP_NAME} -n __fish_use_subcommand -a init -d 'Initialize config or completion'
|
|
379
|
+
|
|
380
|
+
# clone completions
|
|
381
|
+
# (no specific completions for URI)
|
|
382
|
+
|
|
383
|
+
# add completions
|
|
384
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from add' -s c -l create-branch -d 'Create branch if not exists'
|
|
385
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from add' -a '(__fish_git_branches)'
|
|
386
|
+
|
|
387
|
+
# remove completions — only worktree paths and checked-out branches, not all branches.
|
|
388
|
+
# First entry of `git worktree list --porcelain` is the source repo — git's invariant. Skip it.
|
|
389
|
+
function __{APP_NAME}_remove_worktrees
|
|
390
|
+
git worktree list --porcelain 2>/dev/null | awk -v skip=1 '{_FISH_REMOVE_AWK}'
|
|
391
|
+
end
|
|
392
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from remove' -s f -l force -d 'Force removal'
|
|
393
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from remove' -s t -l tag -d 'Tag in key=value form'
|
|
394
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from remove' -a '(__{APP_NAME}_remove_worktrees)'
|
|
395
|
+
|
|
396
|
+
# migrate completions
|
|
397
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from migrate' -s n -l dry-run -d 'Show what would be migrated'
|
|
398
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from migrate' -l move -d 'Move instead of copy'
|
|
399
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from migrate' -a '(__fish_complete_directories)'
|
|
400
|
+
|
|
401
|
+
# init subcommands
|
|
402
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from init; and not __fish_seen_subcommand_from config shell' -a config -d 'Create default config file'
|
|
403
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from init; and not __fish_seen_subcommand_from config shell' -a shell -d 'Install shell completion'
|
|
404
|
+
|
|
405
|
+
# init shell completions
|
|
406
|
+
complete -c {APP_NAME} -n '__fish_seen_subcommand_from init; and __fish_seen_subcommand_from shell' -a 'bash zsh fish'
|
|
407
|
+
|
|
408
|
+
# Global options
|
|
409
|
+
complete -c {APP_NAME} -s h -l help -d 'Show help'
|
|
410
|
+
complete -c {APP_NAME} -s v -l verbose -d 'Increase verbosity'
|
|
411
|
+
complete -c {APP_NAME} -s q -l quiet -d 'Suppress output'
|
|
412
|
+
'''
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def generate_bash_aliases() -> str:
|
|
416
|
+
"""Generate bash alias functions for gwc, gwa, and gwr.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Bash function definitions for gwc (clone), gwa (add), and gwr (remove).
|
|
420
|
+
"""
|
|
421
|
+
return f'''# Alias functions for {APP_NAME}
|
|
422
|
+
# Generated by {APP_NAME} init shell bash
|
|
423
|
+
|
|
424
|
+
# gwc - Clone a repository and navigate to it
|
|
425
|
+
# Streams git's progress (stderr) to the terminal in real time.
|
|
426
|
+
# Only stdout's last line (the new path) is captured for navigation.
|
|
427
|
+
gwc() {{
|
|
428
|
+
local target_path
|
|
429
|
+
local exit_code
|
|
430
|
+
# `set -o pipefail` inside the subshell so $? reflects `gww clone`'s exit,
|
|
431
|
+
# not `tail`'s. Stderr from the subshell still streams to the terminal.
|
|
432
|
+
target_path=$(set -o pipefail; command {APP_NAME} clone "$@" | tail -n 1)
|
|
433
|
+
exit_code=$?
|
|
434
|
+
if [ $exit_code -eq 0 ]; then
|
|
435
|
+
if [ -n "$target_path" ] && [ -d "$target_path" ]; then
|
|
436
|
+
printf "Navigate to %s? [Y/n] " "$target_path"
|
|
437
|
+
read -r reply
|
|
438
|
+
if [ -z "$reply" ] || [ "$reply" = "y" ] || [ "$reply" = "Y" ]; then
|
|
439
|
+
cd "$target_path" || return 1
|
|
440
|
+
fi
|
|
441
|
+
fi
|
|
442
|
+
else
|
|
443
|
+
return $exit_code
|
|
444
|
+
fi
|
|
445
|
+
}}
|
|
446
|
+
|
|
447
|
+
# gwa - Add a worktree and navigate to it
|
|
448
|
+
# Streams git's progress (stderr) to the terminal in real time.
|
|
449
|
+
# Only stdout's last line (the new path) is captured for navigation.
|
|
450
|
+
gwa() {{
|
|
451
|
+
local target_path
|
|
452
|
+
local exit_code
|
|
453
|
+
target_path=$(set -o pipefail; command {APP_NAME} add "$@" | tail -n 1)
|
|
454
|
+
exit_code=$?
|
|
455
|
+
if [ $exit_code -eq 0 ]; then
|
|
456
|
+
if [ -n "$target_path" ] && [ -d "$target_path" ]; then
|
|
457
|
+
printf "Navigate to %s? [Y/n] " "$target_path"
|
|
458
|
+
read -r reply
|
|
459
|
+
if [ -z "$reply" ] || [ "$reply" = "y" ] || [ "$reply" = "Y" ]; then
|
|
460
|
+
cd "$target_path" || return 1
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
else
|
|
464
|
+
return $exit_code
|
|
465
|
+
fi
|
|
466
|
+
}}
|
|
467
|
+
|
|
468
|
+
# gwr - Remove a worktree (prompts for force if dirty)
|
|
469
|
+
# Streams stdout to the terminal; only stderr is captured so the dirty-state
|
|
470
|
+
# check (looking for "uncommitted changes" / "untracked files") still works.
|
|
471
|
+
gwr() {{
|
|
472
|
+
local errfile
|
|
473
|
+
local exit_code
|
|
474
|
+
errfile=$(mktemp)
|
|
475
|
+
command {APP_NAME} remove "$@" 2>"$errfile"
|
|
476
|
+
exit_code=$?
|
|
477
|
+
if [ $exit_code -eq 0 ]; then
|
|
478
|
+
rm -f "$errfile"
|
|
479
|
+
elif grep -q "uncommitted changes\\|untracked files" "$errfile"; then
|
|
480
|
+
cat "$errfile" >&2
|
|
481
|
+
rm -f "$errfile"
|
|
482
|
+
printf "Force removal? [y/N] "
|
|
483
|
+
read -r reply
|
|
484
|
+
if [ "$reply" = "y" ] || [ "$reply" = "Y" ]; then
|
|
485
|
+
command {APP_NAME} remove --force "$@"
|
|
486
|
+
else
|
|
487
|
+
return 1
|
|
488
|
+
fi
|
|
489
|
+
else
|
|
490
|
+
cat "$errfile" >&2
|
|
491
|
+
rm -f "$errfile"
|
|
492
|
+
return $exit_code
|
|
493
|
+
fi
|
|
494
|
+
}}
|
|
495
|
+
|
|
496
|
+
# Completion wrappers - reuse gww completions for aliases
|
|
497
|
+
_gwc_completions() {{
|
|
498
|
+
COMP_WORDS=({APP_NAME} clone "${{COMP_WORDS[@]:1}}")
|
|
499
|
+
COMP_CWORD=$((COMP_CWORD + 1))
|
|
500
|
+
_{APP_NAME}_completions
|
|
501
|
+
}}
|
|
502
|
+
|
|
503
|
+
_gwa_completions() {{
|
|
504
|
+
COMP_WORDS=({APP_NAME} add "${{COMP_WORDS[@]:1}}")
|
|
505
|
+
COMP_CWORD=$((COMP_CWORD + 1))
|
|
506
|
+
_{APP_NAME}_completions
|
|
507
|
+
}}
|
|
508
|
+
|
|
509
|
+
_gwr_completions() {{
|
|
510
|
+
COMP_WORDS=({APP_NAME} remove "${{COMP_WORDS[@]:1}}")
|
|
511
|
+
COMP_CWORD=$((COMP_CWORD + 1))
|
|
512
|
+
_{APP_NAME}_completions
|
|
513
|
+
}}
|
|
514
|
+
|
|
515
|
+
complete -F _gwc_completions gwc
|
|
516
|
+
complete -F _gwa_completions gwa
|
|
517
|
+
complete -F _gwr_completions gwr
|
|
518
|
+
'''
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def generate_zsh_aliases() -> str:
|
|
522
|
+
"""Generate zsh alias functions for gwc, gwa, and gwr.
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Zsh function definitions for gwc (clone), gwa (add), and gwr (remove).
|
|
526
|
+
"""
|
|
527
|
+
return f'''# Alias functions for {APP_NAME}
|
|
528
|
+
# Generated by {APP_NAME} init shell zsh
|
|
529
|
+
|
|
530
|
+
# gwc - Clone a repository and navigate to it
|
|
531
|
+
# Streams git's progress (stderr) to the terminal in real time.
|
|
532
|
+
# Only stdout's last line (the new path) is captured for navigation.
|
|
533
|
+
gwc() {{
|
|
534
|
+
local target_path
|
|
535
|
+
local exit_code
|
|
536
|
+
# `setopt pipefail` inside the subshell so $? reflects `gww clone`'s exit,
|
|
537
|
+
# not `tail`'s. Stderr from the subshell still streams to the terminal.
|
|
538
|
+
target_path=$(setopt pipefail; command {APP_NAME} clone "$@" | tail -n 1)
|
|
539
|
+
exit_code=$?
|
|
540
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
541
|
+
if [[ -n "$target_path" ]] && [[ -d "$target_path" ]]; then
|
|
542
|
+
printf "Navigate to %s? [Y/n] " "$target_path"
|
|
543
|
+
read -r reply
|
|
544
|
+
if [[ -z "$reply" ]] || [[ "$reply" == "y" ]] || [[ "$reply" == "Y" ]]; then
|
|
545
|
+
cd "$target_path" || return 1
|
|
546
|
+
fi
|
|
547
|
+
fi
|
|
548
|
+
else
|
|
549
|
+
return $exit_code
|
|
550
|
+
fi
|
|
551
|
+
}}
|
|
552
|
+
|
|
553
|
+
# gwa - Add a worktree and navigate to it
|
|
554
|
+
# Streams git's progress (stderr) to the terminal in real time.
|
|
555
|
+
# Only stdout's last line (the new path) is captured for navigation.
|
|
556
|
+
gwa() {{
|
|
557
|
+
local target_path
|
|
558
|
+
local exit_code
|
|
559
|
+
target_path=$(setopt pipefail; command {APP_NAME} add "$@" | tail -n 1)
|
|
560
|
+
exit_code=$?
|
|
561
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
562
|
+
if [[ -n "$target_path" ]] && [[ -d "$target_path" ]]; then
|
|
563
|
+
printf "Navigate to %s? [Y/n] " "$target_path"
|
|
564
|
+
read -r reply
|
|
565
|
+
if [[ -z "$reply" ]] || [[ "$reply" == "y" ]] || [[ "$reply" == "Y" ]]; then
|
|
566
|
+
cd "$target_path" || return 1
|
|
567
|
+
fi
|
|
568
|
+
fi
|
|
569
|
+
else
|
|
570
|
+
return $exit_code
|
|
571
|
+
fi
|
|
572
|
+
}}
|
|
573
|
+
|
|
574
|
+
# gwr - Remove a worktree (prompts for force if dirty)
|
|
575
|
+
# Streams stdout to the terminal; only stderr is captured so the dirty-state
|
|
576
|
+
# check (looking for "uncommitted changes" / "untracked files") still works.
|
|
577
|
+
gwr() {{
|
|
578
|
+
local errfile
|
|
579
|
+
local exit_code
|
|
580
|
+
errfile=$(mktemp)
|
|
581
|
+
command {APP_NAME} remove "$@" 2>"$errfile"
|
|
582
|
+
exit_code=$?
|
|
583
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
584
|
+
rm -f "$errfile"
|
|
585
|
+
elif grep -q "uncommitted changes\\|untracked files" "$errfile"; then
|
|
586
|
+
cat "$errfile" >&2
|
|
587
|
+
rm -f "$errfile"
|
|
588
|
+
printf "Force removal? [y/N] "
|
|
589
|
+
read -r reply
|
|
590
|
+
if [[ "$reply" == "y" ]] || [[ "$reply" == "Y" ]]; then
|
|
591
|
+
command {APP_NAME} remove --force "$@"
|
|
592
|
+
else
|
|
593
|
+
return 1
|
|
594
|
+
fi
|
|
595
|
+
else
|
|
596
|
+
cat "$errfile" >&2
|
|
597
|
+
rm -f "$errfile"
|
|
598
|
+
return $exit_code
|
|
599
|
+
fi
|
|
600
|
+
}}
|
|
601
|
+
|
|
602
|
+
# Completion wrappers - reuse gww completions for aliases
|
|
603
|
+
_gwc() {{
|
|
604
|
+
words[1]={APP_NAME}
|
|
605
|
+
words=(clone "${{words[@]:1}}")
|
|
606
|
+
((CURRENT++))
|
|
607
|
+
_{APP_NAME}
|
|
608
|
+
}}
|
|
609
|
+
|
|
610
|
+
_gwa() {{
|
|
611
|
+
words[1]={APP_NAME}
|
|
612
|
+
words=(add "${{words[@]:1}}")
|
|
613
|
+
((CURRENT++))
|
|
614
|
+
_{APP_NAME}
|
|
615
|
+
}}
|
|
616
|
+
|
|
617
|
+
_gwr() {{
|
|
618
|
+
words[1]={APP_NAME}
|
|
619
|
+
words=(remove "${{words[@]:1}}")
|
|
620
|
+
((CURRENT++))
|
|
621
|
+
_{APP_NAME}
|
|
622
|
+
}}
|
|
623
|
+
|
|
624
|
+
compdef _gwc gwc
|
|
625
|
+
compdef _gwa gwa
|
|
626
|
+
compdef _gwr gwr
|
|
627
|
+
'''
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def generate_fish_aliases() -> dict[str, str]:
|
|
631
|
+
"""Generate fish alias functions for gwc, gwa, and gwr.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Dictionary mapping function name to fish function content.
|
|
635
|
+
Fish convention: one function per file.
|
|
636
|
+
"""
|
|
637
|
+
gwc_content = f'''# gwc - Clone a repository and navigate to it
|
|
638
|
+
# Generated by {APP_NAME} init shell fish
|
|
639
|
+
#
|
|
640
|
+
# Streams git's progress (stderr) to the terminal in real time.
|
|
641
|
+
# Only stdout's last line (the new path) is captured for navigation.
|
|
642
|
+
|
|
643
|
+
function gwc --wraps="{APP_NAME} clone" --description "Clone a repository and navigate to it"
|
|
644
|
+
set -l target_path (command {APP_NAME} clone $argv | tail -n 1)
|
|
645
|
+
set -l exit_code $pipestatus[1]
|
|
646
|
+
if test $exit_code -eq 0
|
|
647
|
+
if test -n "$target_path" -a -d "$target_path"
|
|
648
|
+
read -P "Navigate to $target_path? [Y/n] " reply
|
|
649
|
+
if test -z "$reply" -o "$reply" = "y" -o "$reply" = "Y"
|
|
650
|
+
cd "$target_path"
|
|
651
|
+
commandline -f repaint
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
else
|
|
655
|
+
return $exit_code
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
'''
|
|
659
|
+
|
|
660
|
+
gwa_content = f'''# gwa - Add a worktree and navigate to it
|
|
661
|
+
# Generated by {APP_NAME} init shell fish
|
|
662
|
+
#
|
|
663
|
+
# Streams git's progress (stderr) to the terminal in real time.
|
|
664
|
+
# Only stdout's last line (the new path) is captured for navigation.
|
|
665
|
+
|
|
666
|
+
function gwa --wraps="{APP_NAME} add" --description "Add a worktree and navigate to it"
|
|
667
|
+
set -l target_path (command {APP_NAME} add $argv | tail -n 1)
|
|
668
|
+
set -l exit_code $pipestatus[1]
|
|
669
|
+
if test $exit_code -eq 0
|
|
670
|
+
if test -n "$target_path" -a -d "$target_path"
|
|
671
|
+
read -P "Navigate to $target_path? [Y/n] " reply
|
|
672
|
+
if test -z "$reply" -o "$reply" = "y" -o "$reply" = "Y"
|
|
673
|
+
cd "$target_path"
|
|
674
|
+
commandline -f repaint
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
else
|
|
678
|
+
return $exit_code
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
'''
|
|
682
|
+
|
|
683
|
+
gwr_content = f'''# gwr - Remove a worktree (prompts for force if dirty)
|
|
684
|
+
# Generated by {APP_NAME} init shell fish
|
|
685
|
+
#
|
|
686
|
+
# Streams stdout to the terminal; only stderr is captured so the dirty-state
|
|
687
|
+
# check (looking for "uncommitted changes" / "untracked files") still works.
|
|
688
|
+
|
|
689
|
+
function gwr --wraps="{APP_NAME} remove" --description "Remove a worktree (prompts for force if dirty)"
|
|
690
|
+
set -l errfile (mktemp)
|
|
691
|
+
command {APP_NAME} remove $argv 2>$errfile
|
|
692
|
+
set -l exit_code $status
|
|
693
|
+
if test $exit_code -eq 0
|
|
694
|
+
rm -f $errfile
|
|
695
|
+
commandline -f repaint
|
|
696
|
+
else if grep -q -E "uncommitted changes|untracked files" $errfile
|
|
697
|
+
cat $errfile >&2
|
|
698
|
+
rm -f $errfile
|
|
699
|
+
read -P "Force removal? [y/N] " reply
|
|
700
|
+
if test "$reply" = "y" -o "$reply" = "Y"
|
|
701
|
+
command {APP_NAME} remove --force $argv
|
|
702
|
+
commandline -f repaint
|
|
703
|
+
else
|
|
704
|
+
commandline -f repaint
|
|
705
|
+
return 1
|
|
706
|
+
end
|
|
707
|
+
else
|
|
708
|
+
cat $errfile >&2
|
|
709
|
+
rm -f $errfile
|
|
710
|
+
commandline -f repaint
|
|
711
|
+
return $exit_code
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
'''
|
|
715
|
+
|
|
716
|
+
return {"gwc": gwc_content, "gwa": gwa_content, "gwr": gwr_content}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def generate_completion(shell: str) -> str:
|
|
720
|
+
"""Generate completion script for specified shell.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
shell: Shell name (bash, zsh, fish).
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
Completion script content.
|
|
727
|
+
|
|
728
|
+
Raises:
|
|
729
|
+
ValueError: If shell is not supported.
|
|
730
|
+
"""
|
|
731
|
+
if shell == "bash":
|
|
732
|
+
return generate_bash_completion()
|
|
733
|
+
elif shell == "zsh":
|
|
734
|
+
return generate_zsh_completion()
|
|
735
|
+
elif shell == "fish":
|
|
736
|
+
return generate_fish_completion()
|
|
737
|
+
else:
|
|
738
|
+
raise ValueError(f"Unsupported shell: {shell}. Must be one of: bash, zsh, fish")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def install_completion(shell: str, path: Optional[Path] = None) -> Path:
|
|
742
|
+
"""Install completion script for specified shell.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
shell: Shell name (bash, zsh, fish).
|
|
746
|
+
path: Custom installation path (optional).
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
Path where script was installed.
|
|
750
|
+
|
|
751
|
+
Raises:
|
|
752
|
+
ValueError: If shell is not supported.
|
|
753
|
+
OSError: If installation fails.
|
|
754
|
+
"""
|
|
755
|
+
if path is None:
|
|
756
|
+
path = get_completion_path(shell)
|
|
757
|
+
|
|
758
|
+
script = generate_completion(shell)
|
|
759
|
+
|
|
760
|
+
# Ensure parent directory exists
|
|
761
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
762
|
+
|
|
763
|
+
# Write script
|
|
764
|
+
path.write_text(script)
|
|
765
|
+
|
|
766
|
+
return path
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def install_aliases(shell: str) -> Path | list[Path]:
|
|
770
|
+
"""Install alias functions for specified shell.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
shell: Shell name (bash, zsh, fish).
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
For bash/zsh: Path where aliases script was installed.
|
|
777
|
+
For fish: List of paths where function files were installed.
|
|
778
|
+
|
|
779
|
+
Raises:
|
|
780
|
+
ValueError: If shell is not supported.
|
|
781
|
+
OSError: If installation fails.
|
|
782
|
+
"""
|
|
783
|
+
if shell == "bash":
|
|
784
|
+
path = get_aliases_path(shell)
|
|
785
|
+
assert isinstance(path, Path)
|
|
786
|
+
script = generate_bash_aliases()
|
|
787
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
788
|
+
path.write_text(script)
|
|
789
|
+
return path
|
|
790
|
+
elif shell == "zsh":
|
|
791
|
+
path = get_aliases_path(shell)
|
|
792
|
+
assert isinstance(path, Path)
|
|
793
|
+
script = generate_zsh_aliases()
|
|
794
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
795
|
+
path.write_text(script)
|
|
796
|
+
return path
|
|
797
|
+
elif shell == "fish":
|
|
798
|
+
paths_dict = get_aliases_path(shell)
|
|
799
|
+
assert isinstance(paths_dict, dict)
|
|
800
|
+
functions = generate_fish_aliases()
|
|
801
|
+
installed_paths = []
|
|
802
|
+
for name, content in functions.items():
|
|
803
|
+
path = paths_dict[name]
|
|
804
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
805
|
+
path.write_text(content)
|
|
806
|
+
installed_paths.append(path)
|
|
807
|
+
return installed_paths
|
|
808
|
+
else:
|
|
809
|
+
raise ValueError(f"Unsupported shell: {shell}. Must be one of: bash, zsh, fish")
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def get_installation_instructions(
|
|
813
|
+
shell: str,
|
|
814
|
+
completion_path: Path,
|
|
815
|
+
aliases_path: Path | list[Path] | None = None,
|
|
816
|
+
) -> str:
|
|
817
|
+
"""Get instructions for activating completion and aliases after installation.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
shell: Shell name.
|
|
821
|
+
completion_path: Path where completion was installed.
|
|
822
|
+
aliases_path: Path(s) where aliases were installed (optional).
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
Human-readable activation instructions.
|
|
826
|
+
"""
|
|
827
|
+
if shell == "bash":
|
|
828
|
+
instructions = (
|
|
829
|
+
f"Installed bash completion script: {completion_path}\n"
|
|
830
|
+
)
|
|
831
|
+
if aliases_path:
|
|
832
|
+
assert isinstance(aliases_path, Path)
|
|
833
|
+
instructions += f"Installed bash alias functions: {aliases_path}\n"
|
|
834
|
+
instructions += (
|
|
835
|
+
f"\nTo activate, add to ~/.bashrc:\n"
|
|
836
|
+
f" source {completion_path}\n"
|
|
837
|
+
f" source {aliases_path}\n"
|
|
838
|
+
f"\nAlias functions installed:\n"
|
|
839
|
+
f" gwc - Clone a repository and navigate to it\n"
|
|
840
|
+
f" gwa - Add a worktree and navigate to it\n"
|
|
841
|
+
f" gwr - Remove a worktree (prompts for force if dirty)"
|
|
842
|
+
)
|
|
843
|
+
else:
|
|
844
|
+
instructions += (
|
|
845
|
+
f"To activate, run: source {completion_path}\n"
|
|
846
|
+
f"Or add to ~/.bashrc: source {completion_path}"
|
|
847
|
+
)
|
|
848
|
+
return instructions
|
|
849
|
+
elif shell == "zsh":
|
|
850
|
+
instructions = (
|
|
851
|
+
f"Installed zsh completion script: {completion_path}\n"
|
|
852
|
+
)
|
|
853
|
+
if aliases_path:
|
|
854
|
+
assert isinstance(aliases_path, Path)
|
|
855
|
+
instructions += f"Installed zsh alias functions: {aliases_path}\n"
|
|
856
|
+
instructions += (
|
|
857
|
+
f"\nTo activate, add to ~/.zshrc:\n"
|
|
858
|
+
f" fpath=(~/.zsh/completions $fpath)\n"
|
|
859
|
+
f" autoload -Uz compinit && compinit\n"
|
|
860
|
+
f" source {aliases_path}\n"
|
|
861
|
+
f"\nAlias functions installed:\n"
|
|
862
|
+
f" gwc - Clone a repository and navigate to it\n"
|
|
863
|
+
f" gwa - Add a worktree and navigate to it\n"
|
|
864
|
+
f" gwr - Remove a worktree (prompts for force if dirty)\n"
|
|
865
|
+
f"\nThen restart your shell."
|
|
866
|
+
)
|
|
867
|
+
else:
|
|
868
|
+
instructions += (
|
|
869
|
+
f"To activate, ensure your ~/.zshrc contains:\n"
|
|
870
|
+
f" fpath=(~/.zsh/completions $fpath)\n"
|
|
871
|
+
f" autoload -Uz compinit && compinit\n"
|
|
872
|
+
f"Then restart your shell."
|
|
873
|
+
)
|
|
874
|
+
return instructions
|
|
875
|
+
elif shell == "fish":
|
|
876
|
+
instructions = (
|
|
877
|
+
f"Installed fish completion script: {completion_path}\n"
|
|
878
|
+
)
|
|
879
|
+
if aliases_path:
|
|
880
|
+
assert isinstance(aliases_path, list)
|
|
881
|
+
for p in aliases_path:
|
|
882
|
+
instructions += f"Installed fish function: {p}\n"
|
|
883
|
+
instructions += (
|
|
884
|
+
f"\nAlias functions installed:\n"
|
|
885
|
+
f" gwc - Clone a repository and navigate to it\n"
|
|
886
|
+
f" gwa - Add a worktree and navigate to it\n"
|
|
887
|
+
f" gwr - Remove a worktree (prompts for force if dirty)\n"
|
|
888
|
+
f"\nRestart fish shell to activate."
|
|
889
|
+
)
|
|
890
|
+
else:
|
|
891
|
+
instructions += f"Restart fish shell or run: source {completion_path}"
|
|
892
|
+
return instructions
|
|
893
|
+
else:
|
|
894
|
+
return f"Installed completion script: {completion_path}"
|