wtcode 0.2.0__py3-none-any.whl → 0.4.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.
wtcode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """wtcode -- launch your favorite code tool in a git worktree."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
wtcode/wtcode.sh CHANGED
@@ -8,6 +8,10 @@ shopt -s extglob
8
8
  ${WTCODE_DEBUG:+set -x}
9
9
 
10
10
  --msg() { echo "wtcode: $*" >&2; }
11
+ @q() {
12
+ local quoted=$({ set +x; } 2>/dev/null; exec 2>&1; PS4=; set -x; : "$@")
13
+ echo "${quoted:2}" # lstrip the ': ' prefix to get the compact bash-quoted form
14
+ }
11
15
  --sanitize-branch-name() {
12
16
  printf '%s' "$1" |
13
17
  tr '[:upper:]' '[:lower:]' | # lowercase
@@ -16,7 +20,7 @@ ${WTCODE_DEBUG:+set -x}
16
20
  sed 's/^-//; s/-$//' # trim leading/trailing hyphens
17
21
  }
18
22
 
19
- WTCODE_VERSION=0.2.0
23
+ WTCODE_VERSION=0.4.0
20
24
  --version() { echo "wtcode $WTCODE_VERSION"; }
21
25
  --help() {
22
26
  cat <<USAGE
@@ -43,6 +47,12 @@ Environment variables:
43
47
  split-window - create split pane with send-keys
44
48
  new-window - create new window with send-keys
45
49
  exec - exec the tool directly (opt out of send-keys)
50
+ WTCODE_TERMINAL_MODE Outside tmux, control how the tool is launched:
51
+ send-keys - reuse the current tab/window via AppleScript
52
+ (default; macOS only). Terminal.app and Ghostty
53
+ 1.3+ use their own scripting support; any other
54
+ terminal falls back to simulated keystrokes
55
+ exec - exec the tool directly (opt out of send-keys)
46
56
  WTCODE_DEBUG Enable debug tracing when set
47
57
  GIT_WORKTREE_ROOT Override the directory where worktrees are created
48
58
 
@@ -69,6 +79,7 @@ WTCODE_CMDS_TO_TRY=(
69
79
  ###############################################################################
70
80
  --enter-git-worktree() {
71
81
  local branch_name=
82
+ WTCODE_WORKTREE_EXISTED=false
72
83
 
73
84
  # 1. determine which branch/worktree to use
74
85
  if [[ $# -gt 0 ]]; then
@@ -172,6 +183,7 @@ WTCODE_CMDS_TO_TRY=(
172
183
  if [[ -n $existing_worktree ]]; then
173
184
  --msg "branch '$branch_name' is checked out at: $existing_worktree"
174
185
  cd "$existing_worktree"
186
+ WTCODE_WORKTREE_EXISTED=true
175
187
  return 0
176
188
  fi
177
189
  fi
@@ -207,6 +219,7 @@ WTCODE_CMDS_TO_TRY=(
207
219
 
208
220
  if [[ -e "$worktree_path"/.git ]]; then
209
221
  --msg "using existing worktree: $worktree_path"
222
+ WTCODE_WORKTREE_EXISTED=true
210
223
  elif [[ -n ${remote_branch-} ]] && ! git rev-parse --verify "refs/heads/$branch_name" &>/dev/null; then
211
224
  # remote branch: create local tracking branch in a new worktree
212
225
  --msg "creating worktree for remote branch: $remote_branch"
@@ -228,6 +241,183 @@ WTCODE_CMDS_TO_TRY=(
228
241
  git checkout "$branch_name" --
229
242
  }
230
243
 
244
+ ###############################################################################
245
+ ## --provision-claude-settings -- Claude Code setup, only when launching claude
246
+ ###############################################################################
247
+ # Everything Claude-Code-specific lives here so it runs once, synchronously,
248
+ # before dispatch -- regardless of tmux mode -- and only when the resolved
249
+ # tool is actually `claude`. See the call site in --launch-code-tool.
250
+ --provision-claude-settings() {
251
+ # auto-trust this worktree in Claude Code's global config. Newer Claude
252
+ # Code versions also gate a separate "pre-approves N tool permissions"
253
+ # onboarding screen (shown when .claude/settings.local.json grants
254
+ # auto-approve rules) that hasTrustDialogAccepted alone doesn't suppress --
255
+ # hasCompletedProjectOnboarding is needed too.
256
+ if type jq &>/dev/null && [[ -f ~/.claude.json ]]; then
257
+ (
258
+ export worktree_path="$PWD"
259
+ jq -e '.projects[env.worktree_path]' ~/.claude.json &>/dev/null || {
260
+ jq '.projects[env.worktree_path] = ({} | .hasTrustDialogAccepted = true | .hasCompletedProjectOnboarding = true)' \
261
+ ~/.claude.json >~/.claude.json.wtcode.$$
262
+ mv -f ~/.claude.json.wtcode.$$ ~/.claude.json
263
+ }
264
+ )
265
+ fi
266
+
267
+ # symlink project-local settings from the main worktree, if not already
268
+ # present here -- never clobber a file that's git-tracked, hook-provisioned,
269
+ # or already customized in this worktree (including from a prior run: a
270
+ # dangling/previously-created symlink also counts as "already present").
271
+ local main_root
272
+ main_root=$(
273
+ git_common_dir=$(git rev-parse --git-common-dir)
274
+ cd "$git_common_dir"
275
+ cd ..
276
+ pwd
277
+ )
278
+ [[ -d "$main_root/.claude" ]] || return 0 # nothing to provision from
279
+ [[ "$main_root" == "$PWD" ]] && return 0 # already in the main worktree
280
+
281
+ local name created_names=()
282
+ for name in settings.json settings.local.json; do
283
+ local src="$main_root/.claude/$name" dst="$PWD/.claude/$name"
284
+ [[ -e "$src" ]] || continue # nothing to link
285
+ [[ -e "$dst" || -L "$dst" ]] && continue # don't clobber existing/broken link
286
+ mkdir -p "$PWD/.claude" # safe no-op if .claude/ (with other content) already exists
287
+ ln -s "$src" "$dst"
288
+ --msg "symlinked .claude/$name from main worktree"
289
+ created_names+=("$name")
290
+ done
291
+
292
+ (( ${#created_names[@]} > 0 )) || return 0
293
+
294
+ local gi="$PWD/.claude/.gitignore"
295
+ [[ -e $gi ]] && return 0 # respect a pre-existing, possibly customized file
296
+
297
+ local already_ignored=true
298
+ for name in "${created_names[@]}"; do
299
+ git check-ignore -q ".claude/$name" || already_ignored=false
300
+ done
301
+ $already_ignored && return 0
302
+
303
+ {
304
+ echo '# wtcode: symlinked from the main worktree; never commit these here'
305
+ echo 'settings.json'
306
+ echo 'settings.local.json'
307
+ } >"$gi"
308
+ --msg "created .claude/.gitignore"
309
+ }
310
+
311
+ ###############################################################################
312
+ ## --terminal-send-keys -- tmux-like send-keys for GUI terminal apps
313
+ ###############################################################################
314
+ # Reuses the current tab/window in the surrounding terminal app, outside
315
+ # tmux. cd_cmd/run_cmd are sent as two separate lines rather than one
316
+ # `cd X && cmd` line, since the shell's PROMPT_COMMAND (what updates the
317
+ # tab/window title via OSC 7/6) only fires between commands, at the next
318
+ # prompt -- a single `&&` line never shows one, so the title would stay
319
+ # stale for as long as cmd runs.
320
+ #
321
+ # macOS-only: dispatches on $TERM_PROGRAM by looking for a matching
322
+ # --terminal-send-keys--macos--$TERM_PROGRAM function -- add support for
323
+ # another terminal app by just defining one, named to match its
324
+ # $TERM_PROGRAM value exactly. `type osascript` fails (and this returns 1)
325
+ # on any other platform, so this whole mechanism is a no-op there and
326
+ # callers just fall back to exec. Also returns 1 if there's no terminal
327
+ # emulator signal (TERM_PROGRAM/TERMINAL_EMULATOR, as in ~/.zprofile) at all.
328
+ --terminal-send-keys() {
329
+ local worktree_path="$PWD"
330
+ local cd_cmd="cd $(@q "$worktree_path")"
331
+ local run_cmd="$(@q "$@")"
332
+
333
+ # TODO: support Linux/general-unix terminals (and SSH sessions) too --
334
+ # e.g. terminals with OSC 7/6 + a way to inject text (wezterm cli,
335
+ # kitty @ send-text, iterm2's escape sequences, etc). For now this whole
336
+ # mechanism is macOS-only and returns 1 (falls back to exec) elsewhere.
337
+ type osascript &>/dev/null || return 1
338
+
339
+ local termprog=${TERM_PROGRAM-${TERMINAL_EMULATOR-}}
340
+ [[ -n $termprog ]] || return 1
341
+ if type -- "--terminal-send-keys--macos--$termprog" &>/dev/null; then
342
+ "--terminal-send-keys--macos--$termprog" "$cd_cmd" "$run_cmd"
343
+ else
344
+ --terminal-send-keys--macos--system-events "$cd_cmd" "$run_cmd"
345
+ fi
346
+ }
347
+
348
+ # `do script ... in front window`: reuses Terminal's front window via a real
349
+ # Apple Event, not a keystroke. `osascript -` reads the script from stdin;
350
+ # without it, osascript treats argv[0] as a script *file* path.
351
+ --terminal-send-keys--macos--Apple_Terminal() {
352
+ local result
353
+ result=$(osascript - "$@" <<'APPLESCRIPT'
354
+ on run argv
355
+ tell application "Terminal"
356
+ try
357
+ repeat with line_cmd in argv
358
+ do script (line_cmd as text) in front window
359
+ end repeat
360
+ set frontmost of front window to true
361
+ return "OK"
362
+ on error
363
+ return "NOTFOUND"
364
+ end try
365
+ end tell
366
+ end run
367
+ APPLESCRIPT
368
+ ) || return 1
369
+ [[ $result == OK ]]
370
+ }
371
+
372
+ # Ghostty 1.3+ ships a real AppleScript dictionary: `input text ... to
373
+ # term` targets the terminal focused in the front window's selected tab.
374
+ # Falls back to --macos--system-events since this is still a young feature.
375
+ --terminal-send-keys--macos--ghostty() {
376
+ local result rc
377
+ result=$(osascript - "$@" <<'APPLESCRIPT'
378
+ on run argv
379
+ tell application "Ghostty"
380
+ try
381
+ set term to focused terminal of selected tab of front window
382
+ repeat with line_cmd in argv
383
+ input text ((line_cmd as text) & "\n") to term
384
+ end repeat
385
+ return "OK"
386
+ on error
387
+ return "NOTFOUND"
388
+ end try
389
+ end tell
390
+ end run
391
+ APPLESCRIPT
392
+ ); rc=$?
393
+ [[ $rc -eq 0 && $result == OK ]] || --terminal-send-keys--macos--system-events "$@"
394
+ }
395
+
396
+ # Last resort: fake keystrokes via System Events (needs Accessibility
397
+ # permission). No `activate` call -- $TERM_PROGRAM isn't always a real app
398
+ # name, so this relies on the calling terminal still being frontmost.
399
+ --terminal-send-keys--macos--system-events() {
400
+ local result
401
+ result=$(osascript - "$@" <<'APPLESCRIPT'
402
+ on run argv
403
+ tell application "System Events"
404
+ try
405
+ repeat with line_cmd in argv
406
+ keystroke (line_cmd as text)
407
+ key code 36 -- Return
408
+ delay 0.05
409
+ end repeat
410
+ return "OK"
411
+ on error
412
+ return "NOTFOUND"
413
+ end try
414
+ end tell
415
+ end run
416
+ APPLESCRIPT
417
+ ) || return 1
418
+ [[ $result == OK ]]
419
+ }
420
+
231
421
  ###############################################################################
232
422
  ## --launch-code-tool -- resolve and exec the tool in the worktree
233
423
  ###############################################################################
@@ -241,6 +431,16 @@ WTCODE_CMDS_TO_TRY=(
241
431
  [[ $# -gt 0 ]] || set -- "${SHELL:-bash}"
242
432
  fi
243
433
 
434
+ # default to resuming the last session when relaunching claude in a
435
+ # worktree that already existed; press Esc in the picker to start fresh
436
+ # instead. only kicks in when no CMD-ARGS were given, so explicit flags
437
+ # (e.g. `claude --resume`) are never overridden.
438
+ if [[ $1 == claude && $# -eq 1 && ${WTCODE_WORKTREE_EXISTED:-false} == true ]]; then
439
+ set -- "$1" /resume
440
+ fi
441
+
442
+ [[ $1 == claude ]] && --provision-claude-settings
443
+
244
444
  --msg "launching: $*"
245
445
 
246
446
  # when inside tmux, default to send-keys for command history access
@@ -251,12 +451,7 @@ WTCODE_CMDS_TO_TRY=(
251
451
  case $mode in
252
452
  send-keys|split-window|new-window)
253
453
  local worktree_path="$PWD"
254
- # quote each argument for shell safety
255
- local quoted_args=()
256
- for arg in "$@"; do
257
- quoted_args+=("$(printf '%q' "$arg")")
258
- done
259
- local cmd="cd $(printf '%q' "$worktree_path") && ${quoted_args[*]}"
454
+ local cmd="cd $(@q "$worktree_path") && $(@q "$@")"
260
455
 
261
456
  case $mode in
262
457
  send-keys)
@@ -287,35 +482,28 @@ WTCODE_CMDS_TO_TRY=(
287
482
  esac
288
483
  fi
289
484
 
290
- # outside tmux, WTCODE_TMUX_MODE=exec, or unknown mode: exec directly
291
- if [[ $(type -t "$1") == function ]]; then
292
- "$@"
293
- else
294
- exec "$@"
485
+ # outside tmux: reuse the current tab/window if the terminal app supports
486
+ # it (see --terminal-send-keys). set WTCODE_TERMINAL_MODE=exec to opt out
487
+ if [[ -z ${TMUX:-} ]]; then
488
+ local mode=${WTCODE_TERMINAL_MODE:-send-keys}
489
+ case $mode in
490
+ send-keys)
491
+ if --terminal-send-keys "$@"; then
492
+ return 0
493
+ fi
494
+ --msg "could not reuse the current tab/window; falling back to exec"
495
+ ;;
496
+ exec)
497
+ # explicitly opt out of send-keys, fall through to exec
498
+ ;;
499
+ *)
500
+ --msg "unknown WTCODE_TERMINAL_MODE: $mode (expected: send-keys, exec)"
501
+ ;;
502
+ esac
295
503
  fi
296
- }
297
504
 
298
- ###############################################################################
299
- ## Command wrappers -- override specific tools with pre-launch setup.
300
- ## Define a function with the tool's name to add custom behavior.
301
- ###############################################################################
302
-
303
- # claude: auto-trust the worktree in Claude Code's config
304
- claude() {
305
- if type jq &>/dev/null && [[ -f ~/.claude.json ]]; then
306
- (
307
- export worktree_path="$PWD"
308
- jq -e '.projects[env.worktree_path]' ~/.claude.json &>/dev/null || {
309
- jq '
310
- .projects[env.worktree_path] = ({}
311
- | .hasTrustDialogAccepted = true
312
- )
313
- ' ~/.claude.json >~/.claude.json.wtcode.$$
314
- mv -f ~/.claude.json.wtcode.$$ ~/.claude.json
315
- }
316
- )
317
- fi
318
- exec claude "$@"
505
+ # outside tmux, WTCODE_TMUX_MODE=exec, unsupported terminal, or unknown mode: exec directly
506
+ exec "$@"
319
507
  }
320
508
 
321
509
  ###############################################################################
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wtcode
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Launch your favorite code tool in a git worktree
5
5
  Author-email: Jaeho Shin <netj@sparcs.org>
6
6
  License-Expression: MIT
@@ -61,6 +61,7 @@ wtcode --exec CMD [CMD-ARGS...]
61
61
 
62
62
  - **`BRANCH`** -- Git branch or worktree name. If omitted and [fzf](https://github.com/junegunn/fzf) is available, interactively select one. Surround with `:` to create a new branch (use `:::name` or `name:::` to avoid fzf matching the colons).
63
63
  - **`CMD`** -- Command to launch in the worktree. Defaults to `$WTCODE_CMD`, or the first available of: `claude`, `aider`, `codex`, `$SHELL`.
64
+ When `CMD` resolves to plain `claude` (no extra args) in a worktree that already existed, wtcode launches `claude /resume` so it opens straight into your last session there. Press `Esc` in the picker to start a new session instead.
64
65
  - **`--exec`** -- Skip the branch argument; select interactively via fzf, then launch `CMD`.
65
66
  - **`--help`** / **`--version`** -- Show help or version info.
66
67
 
@@ -88,13 +89,14 @@ WTCODE_CMD=cursor wtcode feature # use cursor as default tool
88
89
 
89
90
  1. **Select/specify a branch** -- pass as argument or pick interactively with fzf
90
91
  2. **Create or switch to the worktree** -- worktrees are organized under `$GIT_WORKTREE_ROOT` (defaults to `../<repo>.worktrees/`)
91
- 3. **Launch a tool** -- runs the specified command (or smart default) inside the worktree
92
+ 3. **Provision Claude Code, if that's what you're launching** -- when the resolved tool is `claude`, the worktree is auto-trusted (skips the "do you trust this folder" dialog) and `.claude/settings.json` / `.claude/settings.local.json` are symlinked in from the main repo if not already present -- never for other tools, never clobbering a file that's already there
93
+ 4. **Launch the tool** -- runs the specified command (or smart default) inside the worktree
92
94
 
93
95
  ## Dependencies
94
96
 
95
97
  - **git** -- required
96
98
  - **[fzf](https://github.com/junegunn/fzf)** -- recommended, for interactive branch selection
97
- - **[jq](https://jqlang.github.io/jq/)** -- optional, for Claude Code auto-trust setup
99
+ - **[jq](https://jqlang.github.io/jq/)** -- optional, for Claude Code auto-trust setup (no dependency needed for the settings symlinking itself)
98
100
 
99
101
  ## License
100
102
 
@@ -0,0 +1,9 @@
1
+ wtcode/__init__.py,sha256=TqbbEAt77M6Nx-V2Hn5cdAYQHAuTIzcW1KhT-e_nzhw,89
2
+ wtcode/__main__.py,sha256=hOvSYANO-q_rf2yy-AQoEOr18KFRSOF8Ezosf8eeFmM,335
3
+ wtcode/wtcode.sh,sha256=4jJihpDcSMISByTOW7zo77xDk6bxstQWCEc3d6d-cX4,21098
4
+ wtcode-0.4.0.dist-info/licenses/LICENSE,sha256=MV9gr1nAVj5R60EM2BQuBhBxCG5V9ulX6-Ep_-LcLqA,1067
5
+ wtcode-0.4.0.dist-info/METADATA,sha256=tltadRB3o50NbV1Pwd0NhNLte-ezlBXRceRFSrAzs_I,4137
6
+ wtcode-0.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ wtcode-0.4.0.dist-info/entry_points.txt,sha256=QkSPHU86FQa-ujQk07fF4sX4DBQgWzDtgusop4AA5bc,48
8
+ wtcode-0.4.0.dist-info/top_level.txt,sha256=oftTSYYjDMqt33wa0c9MC15KuQ3eyRyoTLXy3PdkVfE,7
9
+ wtcode-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,9 +0,0 @@
1
- wtcode/__init__.py,sha256=8wIDZvCqXSwHi1obGTZsqnp-Zkf1KeYS-S3nx1vmTOY,89
2
- wtcode/__main__.py,sha256=hOvSYANO-q_rf2yy-AQoEOr18KFRSOF8Ezosf8eeFmM,335
3
- wtcode/wtcode.sh,sha256=tEljKNqwz899AuGUUn6IJUZd1a1whRH_UV0Xf5GanK8,13425
4
- wtcode-0.2.0.dist-info/licenses/LICENSE,sha256=MV9gr1nAVj5R60EM2BQuBhBxCG5V9ulX6-Ep_-LcLqA,1067
5
- wtcode-0.2.0.dist-info/METADATA,sha256=kEnP6efh4xTOr5vKH4Te-Wj8y2zqGEx5Ca6A7GVwSps,3477
6
- wtcode-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
7
- wtcode-0.2.0.dist-info/entry_points.txt,sha256=QkSPHU86FQa-ujQk07fF4sX4DBQgWzDtgusop4AA5bc,48
8
- wtcode-0.2.0.dist-info/top_level.txt,sha256=oftTSYYjDMqt33wa0c9MC15KuQ3eyRyoTLXy3PdkVfE,7
9
- wtcode-0.2.0.dist-info/RECORD,,