claude-worktree 0.1.2 ā 0.1.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/claude/worktree/version.rb +1 -1
- data/lib/cwt/app.rb +35 -7
- data/lib/cwt/git.rb +73 -15
- data/lib/cwt/model.rb +2 -0
- metadata +1 -4
- data/Rakefile +0 -8
- data/assets/demo.gif +0 -0
- data/scripts/setup_demo.rb +0 -70
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa2000361072768df44df230f654d22c5bd8e435bceb02bb597e7bbbfc369b28
|
|
4
|
+
data.tar.gz: 7c42b53349ffcb6dc3050c68db40b869da936d94c61f4cc25ac7899adbfcdc66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2a27c69c6d1117cadefc144e85b04816580f58acd621ce036a69a64f54200c1705c564ed11df4b481ed7d552b1554c10d8a2e7d1dfbb3e7003eb697f812a277
|
|
7
|
+
data.tar.gz: 78601cef3ca323a17c8b9333d54405209b7470545c86c15e13b5518cc17764d2edf46ca2b072fc6df4245bdbd47040924fb3fcc23e4ddbacd51e8114467a2fec
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.4] - 2026-01-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Permanent CD on exit**: After quitting cwt, your shell stays in the last resumed worktree directory
|
|
7
|
+
- **Visible setup output**: `.cwt/setup` script now runs with visible output on first resume (not during worktree creation)
|
|
8
|
+
- **Teardown support**: Optional `.cwt/teardown` script runs before worktree deletion
|
|
9
|
+
- **CWT_ROOT environment variable**: Setup and teardown scripts receive `$CWT_ROOT` pointing to the repo root
|
|
10
|
+
- Integration tests for setup/teardown functionality
|
|
11
|
+
- Homebrew update instructions in deploy script
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Setup now runs on first resume instead of during worktree creation
|
|
15
|
+
- Setup only runs once per worktree (tracked via `.cwt_needs_setup` marker)
|
|
16
|
+
|
|
3
17
|
## [0.1.0] - 2026-01-29
|
|
4
18
|
|
|
5
19
|
- Initial release
|
data/lib/cwt/app.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Cwt
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
event = tui.poll_event(timeout: 0.1)
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
# Process TUI Event
|
|
57
57
|
cmd = nil
|
|
58
58
|
if event.key?
|
|
@@ -62,7 +62,7 @@ module Cwt
|
|
|
62
62
|
elsif event.none?
|
|
63
63
|
cmd = Update.handle(model, { type: :tick })
|
|
64
64
|
end
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
handle_command(cmd, model, tui, main_queue) if cmd
|
|
67
67
|
|
|
68
68
|
# Process Background Queue
|
|
@@ -74,6 +74,12 @@ module Cwt
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
# After TUI exits, cd into last worktree if one was resumed
|
|
79
|
+
if model.exit_directory && Dir.exist?(model.exit_directory)
|
|
80
|
+
Dir.chdir(model.exit_directory)
|
|
81
|
+
exec ENV.fetch('SHELL', '/bin/zsh')
|
|
82
|
+
end
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
def self.handle_command(cmd, model, tui, main_queue)
|
|
@@ -88,11 +94,18 @@ module Cwt
|
|
|
88
94
|
case cmd[:type]
|
|
89
95
|
when :quit
|
|
90
96
|
model.quit
|
|
91
|
-
when :
|
|
97
|
+
when :delete_worktree
|
|
98
|
+
# Suspend TUI for visible teardown output
|
|
99
|
+
RatatuiRuby.restore_terminal
|
|
100
|
+
puts "\e[H\e[2J" # Clear screen
|
|
101
|
+
result = Update.handle(model, cmd)
|
|
102
|
+
RatatuiRuby.init_terminal
|
|
103
|
+
handle_command(result, model, tui, main_queue)
|
|
104
|
+
when :create_worktree, :refresh_list
|
|
92
105
|
result = Update.handle(model, cmd)
|
|
93
106
|
handle_command(result, model, tui, main_queue)
|
|
94
107
|
when :resume_worktree, :suspend_and_resume
|
|
95
|
-
suspend_tui_and_run(cmd[:path], tui)
|
|
108
|
+
suspend_tui_and_run(cmd[:path], model, tui)
|
|
96
109
|
Update.refresh_list(model)
|
|
97
110
|
start_background_fetch(model, main_queue)
|
|
98
111
|
end
|
|
@@ -134,11 +147,24 @@ module Cwt
|
|
|
134
147
|
end
|
|
135
148
|
end
|
|
136
149
|
|
|
137
|
-
def self.suspend_tui_and_run(path, tui)
|
|
150
|
+
def self.suspend_tui_and_run(path, model, tui)
|
|
138
151
|
RatatuiRuby.restore_terminal
|
|
139
|
-
|
|
152
|
+
|
|
140
153
|
puts "\e[H\e[2J" # Clear screen
|
|
141
|
-
|
|
154
|
+
|
|
155
|
+
# Run setup if this is a new worktree
|
|
156
|
+
if Git.needs_setup?(path)
|
|
157
|
+
begin
|
|
158
|
+
Git.run_setup_visible(path)
|
|
159
|
+
Git.mark_setup_complete(path)
|
|
160
|
+
rescue Interrupt
|
|
161
|
+
puts "\nSetup aborted."
|
|
162
|
+
RatatuiRuby.init_terminal
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
puts "Launching claude in #{path}..."
|
|
142
168
|
begin
|
|
143
169
|
Dir.chdir(path) do
|
|
144
170
|
if defined?(Bundler)
|
|
@@ -147,6 +173,8 @@ module Cwt
|
|
|
147
173
|
system("claude")
|
|
148
174
|
end
|
|
149
175
|
end
|
|
176
|
+
# Track last resumed path for exit
|
|
177
|
+
model.exit_directory = path
|
|
150
178
|
rescue => e
|
|
151
179
|
puts "Error: #{e.message}"
|
|
152
180
|
print "Press any key to return..."
|
data/lib/cwt/git.rb
CHANGED
|
@@ -49,6 +49,8 @@ module Cwt
|
|
|
49
49
|
{ dirty: false }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
SETUP_MARKER = ".cwt_needs_setup"
|
|
53
|
+
|
|
52
54
|
def self.add_worktree(name)
|
|
53
55
|
# Sanitize name
|
|
54
56
|
safe_name = name.strip.gsub(/[^a-zA-Z0-9_\-]/, '_')
|
|
@@ -66,13 +68,81 @@ module Cwt
|
|
|
66
68
|
return { success: false, error: stderr }
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
+
# Mark worktree as needing setup (will run on first resume)
|
|
72
|
+
mark_needs_setup(path)
|
|
71
73
|
|
|
72
74
|
{ success: true, path: path }
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
def self.needs_setup?(path)
|
|
78
|
+
File.exist?(File.join(path, SETUP_MARKER))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.mark_needs_setup(path)
|
|
82
|
+
FileUtils.touch(File.join(path, SETUP_MARKER))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.mark_setup_complete(path)
|
|
86
|
+
marker = File.join(path, SETUP_MARKER)
|
|
87
|
+
File.delete(marker) if File.exist?(marker)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.run_setup_visible(path)
|
|
91
|
+
root = Dir.pwd
|
|
92
|
+
setup_script = File.join(root, ".cwt", "setup")
|
|
93
|
+
|
|
94
|
+
if File.exist?(setup_script) && File.executable?(setup_script)
|
|
95
|
+
puts "\e[1;36m=== Running .cwt/setup ===\e[0m"
|
|
96
|
+
puts
|
|
97
|
+
|
|
98
|
+
success = Dir.chdir(path) do
|
|
99
|
+
system({ "CWT_ROOT" => root }, setup_script)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
puts
|
|
103
|
+
|
|
104
|
+
unless success
|
|
105
|
+
puts "\e[1;33mWarning: .cwt/setup failed (exit code: #{$?.exitstatus})\e[0m"
|
|
106
|
+
print "Press Enter to continue or Ctrl+C to abort..."
|
|
107
|
+
begin
|
|
108
|
+
STDIN.gets
|
|
109
|
+
rescue Interrupt
|
|
110
|
+
raise
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
# Default behavior: Symlink .env and node_modules (silent, fast)
|
|
115
|
+
setup_default_symlinks(path, root)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.run_teardown(path)
|
|
120
|
+
root = Dir.pwd
|
|
121
|
+
teardown_script = File.join(root, ".cwt", "teardown")
|
|
122
|
+
|
|
123
|
+
return { ran: false } unless File.exist?(teardown_script) && File.executable?(teardown_script)
|
|
124
|
+
|
|
125
|
+
puts "\e[1;36m=== Running .cwt/teardown ===\e[0m"
|
|
126
|
+
puts
|
|
127
|
+
|
|
128
|
+
success = Dir.chdir(path) do
|
|
129
|
+
system({ "CWT_ROOT" => root }, teardown_script)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
puts
|
|
133
|
+
|
|
134
|
+
{ ran: true, success: success }
|
|
135
|
+
end
|
|
136
|
+
|
|
75
137
|
def self.remove_worktree(path, force: false)
|
|
138
|
+
# Step 0: Run teardown script if directory exists
|
|
139
|
+
if Dir.exist?(path)
|
|
140
|
+
result = run_teardown(path)
|
|
141
|
+
if result[:ran] && !result[:success] && !force
|
|
142
|
+
return { success: false, error: "Teardown script failed. Use 'D' to force delete." }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
76
146
|
# Step 1: Cleanup symlinks/copies (Best effort)
|
|
77
147
|
# This helps 'safe delete' succeed if only untracked files are present.
|
|
78
148
|
[".env", "node_modules"].each do |file|
|
|
@@ -153,19 +223,7 @@ module Cwt
|
|
|
153
223
|
worktrees
|
|
154
224
|
end
|
|
155
225
|
|
|
156
|
-
def self.
|
|
157
|
-
root = Dir.pwd
|
|
158
|
-
setup_script = File.join(root, ".cwt", "setup")
|
|
159
|
-
|
|
160
|
-
# 1. Custom Setup Script
|
|
161
|
-
if File.exist?(setup_script) && File.executable?(setup_script)
|
|
162
|
-
# Execute the script inside the new worktree
|
|
163
|
-
# passing the root path as an argument might be helpful, but relying on relative paths is standard.
|
|
164
|
-
Open3.capture2(setup_script, chdir: target_path)
|
|
165
|
-
return
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# 2. Default Behavior: Symlink .env and node_modules
|
|
226
|
+
def self.setup_default_symlinks(target_path, root)
|
|
169
227
|
files_to_link = [".env", "node_modules"]
|
|
170
228
|
|
|
171
229
|
files_to_link.each do |file|
|
data/lib/cwt/model.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Cwt
|
|
2
2
|
class Model
|
|
3
3
|
attr_reader :worktrees, :selection_index, :mode, :input_buffer, :message, :running, :fetch_generation, :filter_query
|
|
4
|
+
attr_accessor :exit_directory
|
|
4
5
|
|
|
5
6
|
def initialize
|
|
6
7
|
@worktrees = []
|
|
@@ -11,6 +12,7 @@ module Cwt
|
|
|
11
12
|
@message = "Welcome to CWT"
|
|
12
13
|
@running = true
|
|
13
14
|
@fetch_generation = 0
|
|
15
|
+
@exit_directory = nil
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def update_worktrees(list)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-worktree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Garcia
|
|
@@ -35,8 +35,6 @@ files:
|
|
|
35
35
|
- CODE_OF_CONDUCT.md
|
|
36
36
|
- LICENSE.txt
|
|
37
37
|
- README.md
|
|
38
|
-
- Rakefile
|
|
39
|
-
- assets/demo.gif
|
|
40
38
|
- exe/cwt
|
|
41
39
|
- lib/claude/worktree.rb
|
|
42
40
|
- lib/claude/worktree/version.rb
|
|
@@ -45,7 +43,6 @@ files:
|
|
|
45
43
|
- lib/cwt/model.rb
|
|
46
44
|
- lib/cwt/update.rb
|
|
47
45
|
- lib/cwt/view.rb
|
|
48
|
-
- scripts/setup_demo.rb
|
|
49
46
|
- sig/claude/worktree.rbs
|
|
50
47
|
homepage: https://github.com/bengarcia/claude-worktree
|
|
51
48
|
licenses:
|
data/Rakefile
DELETED
data/assets/demo.gif
DELETED
|
Binary file
|
data/scripts/setup_demo.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
require 'fileutils'
|
|
3
|
-
require 'open3'
|
|
4
|
-
|
|
5
|
-
# Configuration
|
|
6
|
-
DEMO_DIR = "cwt_demo_repo"
|
|
7
|
-
WORKTREES_DIR = ".worktrees"
|
|
8
|
-
|
|
9
|
-
# Funny session names (The Prototyper's chaos)
|
|
10
|
-
SESSIONS = [
|
|
11
|
-
"feature/add-lasers-to-ui",
|
|
12
|
-
"fix/undefined-is-not-a-function-again",
|
|
13
|
-
"chore/upgrade-everything-yolo",
|
|
14
|
-
"experiment/rewrite-in-assembly",
|
|
15
|
-
"refactor/rename-all-variables-to-emoji",
|
|
16
|
-
"feature/dark-mode-for-logs",
|
|
17
|
-
"bug/why-is-production-down",
|
|
18
|
-
"wip/ai-will-fix-it-eventually",
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
def run(cmd)
|
|
22
|
-
stdout, stderr, status = Open3.capture3(cmd)
|
|
23
|
-
unless status.success?
|
|
24
|
-
puts "Error running: #{cmd}"
|
|
25
|
-
puts stderr
|
|
26
|
-
exit 1
|
|
27
|
-
end
|
|
28
|
-
stdout
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
puts "š Setting up CWT Demo Environment..."
|
|
32
|
-
|
|
33
|
-
# 1. Create and Init Repo
|
|
34
|
-
if Dir.exist?(DEMO_DIR)
|
|
35
|
-
puts "Cleaning up old demo..."
|
|
36
|
-
FileUtils.rm_rf(DEMO_DIR)
|
|
37
|
-
end
|
|
38
|
-
FileUtils.mkdir_p(DEMO_DIR)
|
|
39
|
-
Dir.chdir(DEMO_DIR)
|
|
40
|
-
|
|
41
|
-
puts "š¦ Initializing git repo..."
|
|
42
|
-
run "git init"
|
|
43
|
-
run "git config user.email 'demo@example.com'"
|
|
44
|
-
run "git config user.name 'Demo User'"
|
|
45
|
-
run "touch README.md"
|
|
46
|
-
run "git add README.md"
|
|
47
|
-
run "git commit -m 'Initial commit'"
|
|
48
|
-
|
|
49
|
-
# 2. Create Worktrees
|
|
50
|
-
puts "š³ Spawning worktrees..."
|
|
51
|
-
SESSIONS.each do |session|
|
|
52
|
-
# Create branch and worktree
|
|
53
|
-
path = File.join(WORKTREES_DIR, session.gsub('/', '-'))
|
|
54
|
-
run "git worktree add -b #{session} #{path}"
|
|
55
|
-
|
|
56
|
-
# Add some "dirty" state to random sessions
|
|
57
|
-
if rand < 0.4
|
|
58
|
-
puts " - Making #{session} dirty..."
|
|
59
|
-
File.write(File.join(path, "dirty_file.txt"), "This is uncommitted work")
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# 3. Create a fake .env to show off symlinking
|
|
64
|
-
File.write(".env", "SECRET_KEY=12345\nAPI_HOST=localhost:3000")
|
|
65
|
-
File.write("node_modules", "fake_node_modules") # Just a file for demo
|
|
66
|
-
|
|
67
|
-
puts "\n⨠Demo Ready!"
|
|
68
|
-
puts "To run the demo:"
|
|
69
|
-
puts " cd #{DEMO_DIR}"
|
|
70
|
-
puts " cwt"
|