refactor(wt): extract cmd.clone module

Amolith created

Assisted-by: Claude Opus 4.5 via Amp

Change summary

dist/wt              | 1408 +++++++++++++++++++++++----------------------
src/main.lua         |  254 --------
src/wt/cmd/clone.lua |  273 ++++++++
3 files changed, 1,002 insertions(+), 933 deletions(-)

Detailed changes

dist/wt 🔗

@@ -861,7 +861,7 @@ end
 return M
 ]]
 
-_EMBEDDED_MODULES["wt.cmd.add"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+_EMBEDDED_MODULES["wt.cmd.clone"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 --
 -- SPDX-License-Identifier: GPL-3.0-or-later
 
@@ -870,261 +870,273 @@ local shell = require("wt.shell")
 local git = require("wt.git")
 local path_mod = require("wt.path")
 local config = require("wt.config")
-local hooks = require("wt.hooks")
 
----@class wt.cmd.add
+---@class wt.cmd.clone
 local M = {}
 
----Add a worktree for an existing or new branch
+---Clone a repository with bare repo structure
 ---@param args string[]
-function M.cmd_add(args)
-	-- Parse arguments: <branch> [-b [<start-point>]]
-	---@type string|nil
-	local branch = nil
-	local create_branch = false
-	---@type string|nil
-	local start_point = nil
+function M.cmd_clone(args)
+	-- Parse arguments: <url> [--remote name]... [--own]
+	local url = nil
+	---@type string[]
+	local remote_flags = {}
+	local own = false
 
 	local i = 1
 	while i <= #args do
 		local a = args[i]
-		if a == "-b" then
-			create_branch = true
-			-- Check if next arg is start-point (not another flag)
-			if args[i + 1] and not args[i + 1]:match("^%-") then
-				start_point = args[i + 1]
-				i = i + 1
+		if a == "--remote" then
+			if not args[i + 1] then
+				shell.die("--remote requires a name")
 			end
-		elseif not branch then
-			branch = a
+			table.insert(remote_flags, args[i + 1])
+			i = i + 1
+		elseif a == "--own" then
+			own = true
+		elseif not url then
+			url = a
 		else
 			shell.die("unexpected argument: " .. a)
 		end
 		i = i + 1
 	end
 
-	if not branch then
-		shell.die("usage: wt a <branch> [-b [<start-point>]]")
+	if not url then
+		shell.die("usage: wt c <url> [--remote name]... [--own]")
 		return
 	end
 
-	local root, err = git.find_project_root()
-	if not root then
-		shell.die(err --[[@as string]])
+	-- Extract project name from URL
+	local project_name = config.extract_project_name(url)
+	if not project_name then
+		shell.die("could not extract project name from URL: " .. url)
 		return
 	end
 
-	local git_dir = root .. "/.bare"
-	local source_worktree = git.detect_source_worktree(root)
-
-	-- Load config for path style
-	local global_config = config.load_global_config()
-	local style = global_config.branch_path_style or "nested"
-	local separator = global_config.flat_separator or "_"
-
-	local target_path = path_mod.branch_to_path(root, branch, style, separator)
-
-	-- Check if target already exists
-	local check = io.open(target_path .. "/.git", "r")
-	if check then
-		check:close()
-		shell.die("worktree already exists at " .. target_path)
-	end
-
-	local output, code
-	if create_branch then
-		-- Create new branch with worktree
-		if start_point then
-			output, code = shell.run_cmd(
-				"GIT_DIR="
-					.. git_dir
-					.. " git worktree add -b "
-					.. branch
-					.. " -- "
-					.. target_path
-					.. " "
-					.. start_point
-			)
-		else
-			output, code =
-				shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path)
-		end
-	else
-		-- Check if branch exists locally or on remotes
-		local exists_local = git.branch_exists_local(git_dir, branch)
-		local remotes = git.find_branch_remotes(git_dir, branch)
-
-		if not exists_local and #remotes == 0 then
-			shell.die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)")
-		end
-
-		if #remotes > 1 then
-			shell.die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", "))
-		end
-
-		output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch)
-	end
-
-	if code ~= 0 then
-		shell.die("failed to add worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
-	end
-
-	-- Run hooks if we have a source worktree
-	local project_config = config.load_project_config(root)
-	if source_worktree then
-		if project_config.hooks then
-			hooks.run_hooks(source_worktree, target_path, project_config.hooks, root)
-		end
-	elseif project_config.hooks then
-		io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n")
-	end
-
-	print(target_path)
-end
-
-return M
-]]
-
-_EMBEDDED_MODULES["wt.cmd.remove"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
---
--- SPDX-License-Identifier: GPL-3.0-or-later
-
-local exit = require("wt.exit")
-local shell = require("wt.shell")
-local git = require("wt.git")
-local path_mod = require("wt.path")
-
----@class wt.cmd.remove
-local M = {}
-
----Check if cwd is inside (or equal to) a given path
----@param target string
----@return boolean
-local function cwd_inside_path(target)
+	-- Check if project directory already exists
 	local cwd = shell.get_cwd()
 	if not cwd then
-		return false
+		shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
 	end
-	return path_mod.path_inside(cwd, target)
-end
-
----Get the bare repo's HEAD branch
----@param git_dir string
----@return string|nil branch name, nil on error
-local function get_bare_head(git_dir)
-	local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref --short HEAD")
-	if code ~= 0 then
-		return nil
+	---@cast cwd string
+	local project_path = cwd .. "/" .. project_name
+	local check = io.open(project_path, "r")
+	if check then
+		check:close()
+		shell.die("directory already exists: " .. project_path)
 	end
-	return (output:gsub("%s+$", ""))
-end
-
----Remove a worktree and optionally its branch
----@param args string[]
-function M.cmd_remove(args)
-	local branch = nil
-	local delete_branch = false
-	local force = false
 
-	for _, a in ipairs(args) do
-		if a == "-b" then
-			delete_branch = true
-		elseif a == "-f" then
-			force = true
-		elseif not branch then
-			branch = a
-		else
-			shell.die("unexpected argument: " .. a)
-		end
+	-- Clone bare repo
+	local bare_path = project_path .. "/.bare"
+	local output, code = shell.run_cmd("git clone --bare " .. url .. " " .. bare_path)
+	if code ~= 0 then
+		shell.die("failed to clone: " .. output, exit.EXIT_SYSTEM_ERROR)
 	end
 
-	if not branch then
-		shell.die("usage: wt r <branch> [-b] [-f]")
+	-- Write .git file pointing to .bare
+	local git_file_handle = io.open(project_path .. "/.git", "w")
+	if not git_file_handle then
+		shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
 		return
 	end
+	git_file_handle:write("gitdir: ./.bare\n")
+	git_file_handle:close()
 
-	local root, err = git.find_project_root()
-	if not root then
-		shell.die(err --[[@as string]])
-		return
-	end
+	-- Detect default branch
+	local git_dir = bare_path
+	local default_branch = git.detect_cloned_default_branch(git_dir)
 
-	local git_dir = root .. "/.bare"
+	-- Load global config
+	local global_config = config.load_global_config()
 
-	local wt_output, wt_code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
-	if wt_code ~= 0 then
-		shell.die("failed to list worktrees", exit.EXIT_SYSTEM_ERROR)
-		return
-	end
+	-- Determine which remotes to use
+	---@type string[]
+	local selected_remotes = {}
 
-	local worktrees = git.parse_worktree_list(wt_output)
-	local target_path = nil
-	for _, wt in ipairs(worktrees) do
-		if wt.branch == branch then
-			target_path = wt.path
-			break
+	if #remote_flags > 0 then
+		selected_remotes = remote_flags
+	elseif global_config.default_remotes then
+		if type(global_config.default_remotes) == "table" then
+			selected_remotes = global_config.default_remotes --[[@as string[] ]]
+		elseif global_config.default_remotes == "prompt" then
+			if global_config.remotes then
+				local keys = {}
+				for k in pairs(global_config.remotes) do
+					table.insert(keys, k)
+				end
+				table.sort(keys)
+				if #keys > 0 then
+					local input = table.concat(keys, "\n")
+					local choose_type = own and "" or " --no-limit"
+					local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
+					output, code = shell.run_cmd(cmd)
+					if code == 0 and output ~= "" then
+						for line in output:gmatch("[^\n]+") do
+							table.insert(selected_remotes, line)
+						end
+					end
+				end
+			end
 		end
-	end
-
-	if not target_path then
-		shell.die("no worktree found for branch '" .. branch .. "'")
-		return
-	end
-
-	if cwd_inside_path(target_path) then
-		shell.die("cannot remove worktree while inside it")
-	end
-
-	if not force then
-		local status_out = shell.run_cmd("git -C " .. target_path .. " status --porcelain")
-		if status_out ~= "" then
-			shell.die("worktree has uncommitted changes (use -f to force)")
+	elseif global_config.remotes then
+		local keys = {}
+		for k in pairs(global_config.remotes) do
+			table.insert(keys, k)
+		end
+		table.sort(keys)
+		if #keys > 0 then
+			local input = table.concat(keys, "\n")
+			local choose_type = own and "" or " --no-limit"
+			local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
+			output, code = shell.run_cmd(cmd)
+			if code == 0 and output ~= "" then
+				for line in output:gmatch("[^\n]+") do
+					table.insert(selected_remotes, line)
+				end
+			end
 		end
 	end
 
-	local remove_cmd = "GIT_DIR=" .. git_dir .. " git worktree remove"
-	if force then
-		remove_cmd = remove_cmd .. " --force"
-	end
-	remove_cmd = remove_cmd .. " -- " .. target_path
+	-- Track configured remotes for summary
+	---@type string[]
+	local configured_remotes = {}
 
-	local output, code = shell.run_cmd(remove_cmd)
-	if code ~= 0 then
-		shell.die("failed to remove worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
-	end
+	if own then
+		-- User's own project: origin is their canonical remote
+		if #selected_remotes > 0 then
+			local first_remote = selected_remotes[1]
+			-- Rename origin to first remote
+			output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
+			if code ~= 0 then
+				io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
+			else
+				-- Configure fetch refspec
+				shell.run_cmd(
+					"GIT_DIR="
+						.. git_dir
+						.. " git config remote."
+						.. first_remote
+						.. ".fetch '+refs/heads/*:refs/remotes/"
+						.. first_remote
+						.. "/*'"
+				)
+				table.insert(configured_remotes, first_remote)
+			end
 
-	if delete_branch then
-		local bare_head = get_bare_head(git_dir)
-		if bare_head and bare_head == branch then
-			io.stderr:write("warning: cannot delete branch '" .. branch .. "' (it's the bare repo's HEAD)\n")
-			print("Worktree removed; branch retained")
-			return
+			-- Add additional remotes and push to them
+			for j = 2, #selected_remotes do
+				local remote_name = selected_remotes[j]
+				local template = global_config.remotes and global_config.remotes[remote_name]
+				if template then
+					local remote_url = config.resolve_url_template(template, project_name)
+					output, code =
+						shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
+					if code ~= 0 then
+						io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+					else
+						shell.run_cmd(
+							"GIT_DIR="
+								.. git_dir
+								.. " git config remote."
+								.. remote_name
+								.. ".fetch '+refs/heads/*:refs/remotes/"
+								.. remote_name
+								.. "/*'"
+						)
+						-- Push to additional remotes
+						output, code =
+							shell.run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
+						if code ~= 0 then
+							io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
+						end
+						table.insert(configured_remotes, remote_name)
+					end
+				else
+					io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+				end
+			end
+		else
+			-- No remotes selected, keep origin as-is
+			shell.run_cmd(
+				"GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'"
+			)
+			table.insert(configured_remotes, "origin")
 		end
-
-		local checked_out = git.branch_checked_out_at(git_dir, branch)
-		if checked_out then
-			shell.die("cannot delete branch '" .. branch .. "': checked out at " .. checked_out)
+	else
+		-- Contributing to someone else's project
+		-- Rename origin to upstream
+		output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
+		if code ~= 0 then
+			io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
+		else
+			shell.run_cmd(
+				"GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
+			)
+			table.insert(configured_remotes, "upstream")
 		end
 
-		local delete_flag = force and "-D" or "-d"
-		local del_output, del_code =
-			shell.run_cmd("GIT_DIR=" .. git_dir .. " git branch " .. delete_flag .. " " .. branch)
-		if del_code ~= 0 then
-			io.stderr:write("warning: failed to delete branch: " .. del_output .. "\n")
-			print("Worktree removed; branch retained")
-			return
+		-- Add user's remotes and push to each
+		for _, remote_name in ipairs(selected_remotes) do
+			local template = global_config.remotes and global_config.remotes[remote_name]
+			if template then
+				local remote_url = config.resolve_url_template(template, project_name)
+				output, code =
+					shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
+				if code ~= 0 then
+					io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+				else
+					shell.run_cmd(
+						"GIT_DIR="
+							.. git_dir
+							.. " git config remote."
+							.. remote_name
+							.. ".fetch '+refs/heads/*:refs/remotes/"
+							.. remote_name
+							.. "/*'"
+					)
+					-- Push to this remote
+					output, code =
+						shell.run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
+					if code ~= 0 then
+						io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
+					end
+					table.insert(configured_remotes, remote_name)
+				end
+			else
+				io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+			end
 		end
+	end
 
-		print("Worktree and branch '" .. branch .. "' removed")
-	else
-		print("Worktree removed")
+	-- Fetch all remotes
+	shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
+
+	-- Load config for path style
+	local style = global_config.branch_path_style or "nested"
+	local separator = global_config.flat_separator
+	local worktree_path = path_mod.branch_to_path(project_path, default_branch, style, separator)
+
+	-- Create initial worktree
+	output, code =
+		shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
+	if code ~= 0 then
+		shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
+	end
+
+	-- Print summary
+	print("Created project: " .. project_path)
+	print("Default branch:  " .. default_branch)
+	print("Worktree:        " .. worktree_path)
+	if #configured_remotes > 0 then
+		print("Remotes:         " .. table.concat(configured_remotes, ", "))
 	end
 end
 
 return M
 ]]
 
-_EMBEDDED_MODULES["wt.cmd.list"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+_EMBEDDED_MODULES["wt.cmd.new"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 --
 -- SPDX-License-Identifier: GPL-3.0-or-later
 
@@ -1132,588 +1144,622 @@ local exit = require("wt.exit")
 local shell = require("wt.shell")
 local git = require("wt.git")
 local path_mod = require("wt.path")
+local config = require("wt.config")
 
----@class wt.cmd.list
+---@class wt.cmd.new
 local M = {}
 
----List all worktrees with status
-function M.cmd_list()
-	local root, err = git.find_project_root()
-	if not root then
-		shell.die(err --[[@as string]])
+---Create a new project with bare repo structure
+---@param args string[]
+function M.cmd_new(args)
+	-- Parse arguments: <project-name> [--remote name]...
+	local project_name = nil
+	---@type string[]
+	local remote_flags = {}
+
+	local i = 1
+	while i <= #args do
+		local a = args[i]
+		if a == "--remote" then
+			if not args[i + 1] then
+				shell.die("--remote requires a name")
+			end
+			table.insert(remote_flags, args[i + 1])
+			i = i + 1
+		elseif not project_name then
+			project_name = a
+		else
+			shell.die("unexpected argument: " .. a)
+		end
+		i = i + 1
+	end
+
+	if not project_name then
+		shell.die("usage: wt n <project-name> [--remote name]...")
 		return
 	end
 
-	local git_dir = root .. "/.bare"
-	local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
-	if code ~= 0 then
-		shell.die("failed to list worktrees: " .. output, exit.EXIT_SYSTEM_ERROR)
+	-- Check if project directory already exists
+	local cwd = shell.get_cwd()
+	if not cwd then
+		shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
+	end
+	---@cast cwd string
+	local project_path = cwd .. "/" .. project_name
+	local check = io.open(project_path, "r")
+	if check then
+		check:close()
+		shell.die("directory already exists: " .. project_path)
 	end
 
-	---@type {path: string, head: string, branch: string}[]
-	local worktrees = {}
-	local current = {}
+	-- Load global config
+	local global_config = config.load_global_config()
 
-	for line in output:gmatch("[^\n]+") do
-		local key, value = line:match("^(%S+)%s*(.*)$")
-		if key == "worktree" and value then
-			if current.path then
-				table.insert(worktrees, current)
+	-- Determine which remotes to use
+	---@type string[]
+	local selected_remotes = {}
+
+	if #remote_flags > 0 then
+		-- Use explicitly provided remotes
+		selected_remotes = remote_flags
+	elseif global_config.default_remotes then
+		if type(global_config.default_remotes) == "table" then
+			selected_remotes = global_config.default_remotes --[[@as string[] ]]
+		elseif global_config.default_remotes == "prompt" then
+			-- Prompt with gum choose
+			if global_config.remotes then
+				local keys = {}
+				for k in pairs(global_config.remotes) do
+					table.insert(keys, k)
+				end
+				table.sort(keys)
+				if #keys > 0 then
+					local input = table.concat(keys, "\n")
+					local cmd = "echo '" .. input .. "' | gum choose --no-limit"
+					local output, code = shell.run_cmd(cmd)
+					if code == 0 and output ~= "" then
+						for line in output:gmatch("[^\n]+") do
+							table.insert(selected_remotes, line)
+						end
+					end
+				end
 			end
-			if value:match("/%.bare$") then
-				current = {}
-			else
-				current = { path = value, head = "", branch = "(detached)" }
+		end
+	elseif global_config.remotes then
+		-- No default_remotes configured, prompt if remotes exist
+		local keys = {}
+		for k in pairs(global_config.remotes) do
+			table.insert(keys, k)
+		end
+		table.sort(keys)
+		if #keys > 0 then
+			local input = table.concat(keys, "\n")
+			local cmd = "echo '" .. input .. "' | gum choose --no-limit"
+			local output, code = shell.run_cmd(cmd)
+			if code == 0 and output ~= "" then
+				for line in output:gmatch("[^\n]+") do
+					table.insert(selected_remotes, line)
+				end
 			end
-		elseif key == "HEAD" and value then
-			current.head = value:sub(1, 7)
-		elseif key == "branch" and value then
-			current.branch = value:gsub("^refs/heads/", "")
-		elseif key == "bare" then
-			current = {}
 		end
 	end
-	if current.path then
-		table.insert(worktrees, current)
-	end
 
-	if #worktrees == 0 then
-		print("No worktrees found")
-		return
+	-- Create project structure
+	local bare_path = project_path .. "/.bare"
+	local output, code = shell.run_cmd("mkdir -p " .. bare_path)
+	if code ~= 0 then
+		shell.die("failed to create directory: " .. output, exit.EXIT_SYSTEM_ERROR)
 	end
 
-	local cwd = shell.get_cwd() or ""
-
-	local rows = {}
-	for _, wt in ipairs(worktrees) do
-		local rel_path = path_mod.relative_path(cwd, wt.path)
-
-		local status_out = shell.run_cmd("git -C " .. wt.path .. " status --porcelain")
-		local status = status_out == "" and "clean" or "dirty"
-
-		table.insert(rows, rel_path .. "," .. wt.branch .. "," .. wt.head .. "," .. status)
+	output, code = shell.run_cmd("git init --bare " .. bare_path)
+	if code ~= 0 then
+		shell.die("failed to init bare repo: " .. output, exit.EXIT_SYSTEM_ERROR)
 	end
 
-	local table_input = "Path,Branch,HEAD,Status\n" .. table.concat(rows, "\n")
-	table_input = table_input:gsub("EOF", "eof")
-	local table_cmd = "gum table --print <<'EOF'\n" .. table_input .. "\nEOF"
-	local table_handle = io.popen(table_cmd, "r")
-	if not table_handle then
+	-- Write .git file pointing to .bare
+	local git_file_handle = io.open(project_path .. "/.git", "w")
+	if not git_file_handle then
+		shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
 		return
 	end
-	io.write(table_handle:read("*a") or "")
-	table_handle:close()
-end
-
-return M
-]]
+	git_file_handle:write("gitdir: ./.bare\n")
+	git_file_handle:close()
 
-_EMBEDDED_MODULES["wt.cmd.fetch"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
---
--- SPDX-License-Identifier: GPL-3.0-or-later
+	-- Add remotes
+	local git_dir = bare_path
+	for _, remote_name in ipairs(selected_remotes) do
+		local template = global_config.remotes and global_config.remotes[remote_name]
+		if template then
+			local url = config.resolve_url_template(template, project_name)
+			output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. url)
+			if code ~= 0 then
+				io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+			else
+				-- Configure fetch refspec for the remote
+				shell.run_cmd(
+					"GIT_DIR="
+						.. git_dir
+						.. " git config remote."
+						.. remote_name
+						.. ".fetch '+refs/heads/*:refs/remotes/"
+						.. remote_name
+						.. "/*'"
+				)
+			end
+		else
+			io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+		end
+	end
 
-local exit = require("wt.exit")
-local shell = require("wt.shell")
-local git = require("wt.git")
+	-- Detect default branch
+	local default_branch = git.get_default_branch()
 
----@class wt.cmd.fetch
-local M = {}
+	-- Load config for path style
+	local style = global_config.branch_path_style or "nested"
+	local separator = global_config.flat_separator
+	local worktree_path = path_mod.branch_to_path(project_path, default_branch, style, separator)
 
----Fetch all remotes with pruning
-function M.cmd_fetch()
-	local root, err = git.find_project_root()
-	if not root then
-		shell.die(err --[[@as string]])
-		return
+	-- Create orphan worktree
+	output, code = shell.run_cmd(
+		"GIT_DIR=" .. git_dir .. " git worktree add --orphan -b " .. default_branch .. " -- " .. worktree_path
+	)
+	if code ~= 0 then
+		shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
 	end
 
-	local git_dir = root .. "/.bare"
-	local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all --prune")
-	io.write(output)
-	if code ~= 0 then
-		os.exit(exit.EXIT_SYSTEM_ERROR)
+	-- Print summary
+	print("Created project: " .. project_path)
+	print("Default branch:  " .. default_branch)
+	print("Worktree:        " .. worktree_path)
+	if #selected_remotes > 0 then
+		print("Remotes:         " .. table.concat(selected_remotes, ", "))
 	end
 end
 
 return M
 ]]
 
-
-if _VERSION < "Lua 5.2" then
-	io.stderr:write("error: wt requires Lua 5.2 or later\n")
-	os.exit(1)
-end
+_EMBEDDED_MODULES["wt.cmd.add"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
 
 local exit = require("wt.exit")
-local EXIT_SUCCESS = exit.EXIT_SUCCESS
-local EXIT_USER_ERROR = exit.EXIT_USER_ERROR
-local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR
-
 local shell = require("wt.shell")
-local run_cmd = shell.run_cmd
-local run_cmd_silent = shell.run_cmd_silent
-local get_cwd = shell.get_cwd
-local die = shell.die
-
+local git = require("wt.git")
 local path_mod = require("wt.path")
-local branch_to_path = path_mod.branch_to_path
-local split_path = path_mod.split_path
-local relative_path = path_mod.relative_path
-local path_inside = path_mod.path_inside
-local escape_pattern = path_mod.escape_pattern
-
-local git_mod = require("wt.git")
-local find_project_root = git_mod.find_project_root
-local detect_source_worktree = git_mod.detect_source_worktree
-local detect_cloned_default_branch = git_mod.detect_cloned_default_branch
-local get_default_branch = git_mod.get_default_branch
-local parse_branch_remotes = git_mod.parse_branch_remotes
-local parse_worktree_list = git_mod.parse_worktree_list
-
-local config_mod = require("wt.config")
-local resolve_url_template = config_mod.resolve_url_template
-local extract_project_name = config_mod.extract_project_name
-local load_global_config = config_mod.load_global_config
-local load_project_config = config_mod.load_project_config
-
-local hooks_mod = require("wt.hooks")
-local load_hook_permissions = hooks_mod.load_hook_permissions
-local save_hook_permissions = hooks_mod.save_hook_permissions
-local summarize_hooks = hooks_mod.summarize_hooks
-local run_hooks = hooks_mod.run_hooks
-
-local help_mod = require("wt.help")
-local print_usage = help_mod.print_usage
-local show_command_help = help_mod.show_command_help
-
-local fetch_mod = require("wt.cmd.fetch")
-local cmd_fetch = fetch_mod.cmd_fetch
-
-local list_mod = require("wt.cmd.list")
-local cmd_list = list_mod.cmd_list
-
-local remove_mod = require("wt.cmd.remove")
-local cmd_remove = remove_mod.cmd_remove
+local config = require("wt.config")
+local hooks = require("wt.hooks")
 
-local add_mod = require("wt.cmd.add")
-local cmd_add = add_mod.cmd_add
+---@class wt.cmd.add
+local M = {}
 
+---Add a worktree for an existing or new branch
 ---@param args string[]
-local function cmd_clone(args)
-	-- Parse arguments: <url> [--remote name]... [--own]
-	local url = nil
-	---@type string[]
-	local remote_flags = {}
-	local own = false
+function M.cmd_add(args)
+	-- Parse arguments: <branch> [-b [<start-point>]]
+	---@type string|nil
+	local branch = nil
+	local create_branch = false
+	---@type string|nil
+	local start_point = nil
 
 	local i = 1
 	while i <= #args do
 		local a = args[i]
-		if a == "--remote" then
-			if not args[i + 1] then
-				die("--remote requires a name")
+		if a == "-b" then
+			create_branch = true
+			-- Check if next arg is start-point (not another flag)
+			if args[i + 1] and not args[i + 1]:match("^%-") then
+				start_point = args[i + 1]
+				i = i + 1
 			end
-			table.insert(remote_flags, args[i + 1])
-			i = i + 1
-		elseif a == "--own" then
-			own = true
-		elseif not url then
-			url = a
+		elseif not branch then
+			branch = a
 		else
-			die("unexpected argument: " .. a)
+			shell.die("unexpected argument: " .. a)
 		end
 		i = i + 1
 	end
 
-	if not url then
-		die("usage: wt c <url> [--remote name]... [--own]")
+	if not branch then
+		shell.die("usage: wt a <branch> [-b [<start-point>]]")
 		return
 	end
 
-	-- Extract project name from URL
-	local project_name = extract_project_name(url)
-	if not project_name then
-		die("could not extract project name from URL: " .. url)
+	local root, err = git.find_project_root()
+	if not root then
+		shell.die(err --[[@as string]])
 		return
 	end
 
-	-- Check if project directory already exists
-	local cwd = get_cwd()
-	if not cwd then
-		die("failed to get current directory", EXIT_SYSTEM_ERROR)
-	end
-	local project_path = cwd .. "/" .. project_name
-	local check = io.open(project_path, "r")
+	local git_dir = root .. "/.bare"
+	local source_worktree = git.detect_source_worktree(root)
+
+	-- Load config for path style
+	local global_config = config.load_global_config()
+	local style = global_config.branch_path_style or "nested"
+	local separator = global_config.flat_separator or "_"
+
+	local target_path = path_mod.branch_to_path(root, branch, style, separator)
+
+	-- Check if target already exists
+	local check = io.open(target_path .. "/.git", "r")
 	if check then
 		check:close()
-		die("directory already exists: " .. project_path)
+		shell.die("worktree already exists at " .. target_path)
+	end
+
+	local output, code
+	if create_branch then
+		-- Create new branch with worktree
+		if start_point then
+			output, code = shell.run_cmd(
+				"GIT_DIR="
+					.. git_dir
+					.. " git worktree add -b "
+					.. branch
+					.. " -- "
+					.. target_path
+					.. " "
+					.. start_point
+			)
+		else
+			output, code =
+				shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path)
+		end
+	else
+		-- Check if branch exists locally or on remotes
+		local exists_local = git.branch_exists_local(git_dir, branch)
+		local remotes = git.find_branch_remotes(git_dir, branch)
+
+		if not exists_local and #remotes == 0 then
+			shell.die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)")
+		end
+
+		if #remotes > 1 then
+			shell.die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", "))
+		end
+
+		output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch)
 	end
 
-	-- Clone bare repo
-	local bare_path = project_path .. "/.bare"
-	local output, code = run_cmd("git clone --bare " .. url .. " " .. bare_path)
 	if code ~= 0 then
-		die("failed to clone: " .. output, EXIT_SYSTEM_ERROR)
+		shell.die("failed to add worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
 	end
 
-	-- Write .git file pointing to .bare
-	local git_file_handle = io.open(project_path .. "/.git", "w")
-	if not git_file_handle then
-		die("failed to create .git file", EXIT_SYSTEM_ERROR)
-		return
+	-- Run hooks if we have a source worktree
+	local project_config = config.load_project_config(root)
+	if source_worktree then
+		if project_config.hooks then
+			hooks.run_hooks(source_worktree, target_path, project_config.hooks, root)
+		end
+	elseif project_config.hooks then
+		io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n")
 	end
-	git_file_handle:write("gitdir: ./.bare\n")
-	git_file_handle:close()
 
-	-- Detect default branch
-	local git_dir = bare_path
-	local default_branch = detect_cloned_default_branch(git_dir)
+	print(target_path)
+end
 
-	-- Load global config
-	local global_config = load_global_config()
+return M
+]]
 
-	-- Determine which remotes to use
-	---@type string[]
-	local selected_remotes = {}
+_EMBEDDED_MODULES["wt.cmd.remove"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
 
-	if #remote_flags > 0 then
-		selected_remotes = remote_flags
-	elseif global_config.default_remotes then
-		if type(global_config.default_remotes) == "table" then
-			selected_remotes = global_config.default_remotes
-		elseif global_config.default_remotes == "prompt" then
-			if global_config.remotes then
-				local keys = {}
-				for k in pairs(global_config.remotes) do
-					table.insert(keys, k)
-				end
-				table.sort(keys)
-				if #keys > 0 then
-					local input = table.concat(keys, "\n")
-					local choose_type = own and "" or " --no-limit"
-					local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
-					output, code = run_cmd(cmd)
-					if code == 0 and output ~= "" then
-						for line in output:gmatch("[^\n]+") do
-							table.insert(selected_remotes, line)
-						end
-					end
-				end
-			end
+local exit = require("wt.exit")
+local shell = require("wt.shell")
+local git = require("wt.git")
+local path_mod = require("wt.path")
+
+---@class wt.cmd.remove
+local M = {}
+
+---Check if cwd is inside (or equal to) a given path
+---@param target string
+---@return boolean
+local function cwd_inside_path(target)
+	local cwd = shell.get_cwd()
+	if not cwd then
+		return false
+	end
+	return path_mod.path_inside(cwd, target)
+end
+
+---Get the bare repo's HEAD branch
+---@param git_dir string
+---@return string|nil branch name, nil on error
+local function get_bare_head(git_dir)
+	local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref --short HEAD")
+	if code ~= 0 then
+		return nil
+	end
+	return (output:gsub("%s+$", ""))
+end
+
+---Remove a worktree and optionally its branch
+---@param args string[]
+function M.cmd_remove(args)
+	local branch = nil
+	local delete_branch = false
+	local force = false
+
+	for _, a in ipairs(args) do
+		if a == "-b" then
+			delete_branch = true
+		elseif a == "-f" then
+			force = true
+		elseif not branch then
+			branch = a
+		else
+			shell.die("unexpected argument: " .. a)
 		end
-	elseif global_config.remotes then
-		local keys = {}
-		for k in pairs(global_config.remotes) do
-			table.insert(keys, k)
+	end
+
+	if not branch then
+		shell.die("usage: wt r <branch> [-b] [-f]")
+		return
+	end
+
+	local root, err = git.find_project_root()
+	if not root then
+		shell.die(err --[[@as string]])
+		return
+	end
+
+	local git_dir = root .. "/.bare"
+
+	local wt_output, wt_code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
+	if wt_code ~= 0 then
+		shell.die("failed to list worktrees", exit.EXIT_SYSTEM_ERROR)
+		return
+	end
+
+	local worktrees = git.parse_worktree_list(wt_output)
+	local target_path = nil
+	for _, wt in ipairs(worktrees) do
+		if wt.branch == branch then
+			target_path = wt.path
+			break
 		end
-		table.sort(keys)
-		if #keys > 0 then
-			local input = table.concat(keys, "\n")
-			local choose_type = own and "" or " --no-limit"
-			local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
-			output, code = run_cmd(cmd)
-			if code == 0 and output ~= "" then
-				for line in output:gmatch("[^\n]+") do
-					table.insert(selected_remotes, line)
-				end
-			end
+	end
+
+	if not target_path then
+		shell.die("no worktree found for branch '" .. branch .. "'")
+		return
+	end
+
+	if cwd_inside_path(target_path) then
+		shell.die("cannot remove worktree while inside it")
+	end
+
+	if not force then
+		local status_out = shell.run_cmd("git -C " .. target_path .. " status --porcelain")
+		if status_out ~= "" then
+			shell.die("worktree has uncommitted changes (use -f to force)")
 		end
 	end
 
-	-- Track configured remotes for summary
-	---@type string[]
-	local configured_remotes = {}
+	local remove_cmd = "GIT_DIR=" .. git_dir .. " git worktree remove"
+	if force then
+		remove_cmd = remove_cmd .. " --force"
+	end
+	remove_cmd = remove_cmd .. " -- " .. target_path
 
-	if own then
-		-- User's own project: origin is their canonical remote
-		if #selected_remotes > 0 then
-			local first_remote = selected_remotes[1]
-			-- Rename origin to first remote
-			output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
-			if code ~= 0 then
-				io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
-			else
-				-- Configure fetch refspec
-				run_cmd(
-					"GIT_DIR="
-						.. git_dir
-						.. " git config remote."
-						.. first_remote
-						.. ".fetch '+refs/heads/*:refs/remotes/"
-						.. first_remote
-						.. "/*'"
-				)
-				table.insert(configured_remotes, first_remote)
-			end
+	local output, code = shell.run_cmd(remove_cmd)
+	if code ~= 0 then
+		shell.die("failed to remove worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
+	end
 
-			-- Add additional remotes and push to them
-			for j = 2, #selected_remotes do
-				local remote_name = selected_remotes[j]
-				local template = global_config.remotes and global_config.remotes[remote_name]
-				if template then
-					local remote_url = resolve_url_template(template, project_name)
-					output, code =
-						run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
-					if code ~= 0 then
-						io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
-					else
-						run_cmd(
-							"GIT_DIR="
-								.. git_dir
-								.. " git config remote."
-								.. remote_name
-								.. ".fetch '+refs/heads/*:refs/remotes/"
-								.. remote_name
-								.. "/*'"
-						)
-						-- Push to additional remotes
-						output, code =
-							run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
-						if code ~= 0 then
-							io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
-						end
-						table.insert(configured_remotes, remote_name)
-					end
-				else
-					io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
-				end
-			end
-		else
-			-- No remotes selected, keep origin as-is
-			run_cmd("GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'")
-			table.insert(configured_remotes, "origin")
+	if delete_branch then
+		local bare_head = get_bare_head(git_dir)
+		if bare_head and bare_head == branch then
+			io.stderr:write("warning: cannot delete branch '" .. branch .. "' (it's the bare repo's HEAD)\n")
+			print("Worktree removed; branch retained")
+			return
 		end
-	else
-		-- Contributing to someone else's project
-		-- Rename origin to upstream
-		output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
-		if code ~= 0 then
-			io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
-		else
-			run_cmd(
-				"GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
-			)
-			table.insert(configured_remotes, "upstream")
+
+		local checked_out = git.branch_checked_out_at(git_dir, branch)
+		if checked_out then
+			shell.die("cannot delete branch '" .. branch .. "': checked out at " .. checked_out)
 		end
 
-		-- Add user's remotes and push to each
-		for _, remote_name in ipairs(selected_remotes) do
-			local template = global_config.remotes and global_config.remotes[remote_name]
-			if template then
-				local remote_url = resolve_url_template(template, project_name)
-				output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
-				if code ~= 0 then
-					io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
-				else
-					run_cmd(
-						"GIT_DIR="
-							.. git_dir
-							.. " git config remote."
-							.. remote_name
-							.. ".fetch '+refs/heads/*:refs/remotes/"
-							.. remote_name
-							.. "/*'"
-					)
-					-- Push to this remote
-					output, code =
-						run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
-					if code ~= 0 then
-						io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
-					end
-					table.insert(configured_remotes, remote_name)
-				end
+		local delete_flag = force and "-D" or "-d"
+		local del_output, del_code =
+			shell.run_cmd("GIT_DIR=" .. git_dir .. " git branch " .. delete_flag .. " " .. branch)
+		if del_code ~= 0 then
+			io.stderr:write("warning: failed to delete branch: " .. del_output .. "\n")
+			print("Worktree removed; branch retained")
+			return
+		end
+
+		print("Worktree and branch '" .. branch .. "' removed")
+	else
+		print("Worktree removed")
+	end
+end
+
+return M
+]]
+
+_EMBEDDED_MODULES["wt.cmd.list"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local exit = require("wt.exit")
+local shell = require("wt.shell")
+local git = require("wt.git")
+local path_mod = require("wt.path")
+
+---@class wt.cmd.list
+local M = {}
+
+---List all worktrees with status
+function M.cmd_list()
+	local root, err = git.find_project_root()
+	if not root then
+		shell.die(err --[[@as string]])
+		return
+	end
+
+	local git_dir = root .. "/.bare"
+	local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
+	if code ~= 0 then
+		shell.die("failed to list worktrees: " .. output, exit.EXIT_SYSTEM_ERROR)
+	end
+
+	---@type {path: string, head: string, branch: string}[]
+	local worktrees = {}
+	local current = {}
+
+	for line in output:gmatch("[^\n]+") do
+		local key, value = line:match("^(%S+)%s*(.*)$")
+		if key == "worktree" and value then
+			if current.path then
+				table.insert(worktrees, current)
+			end
+			if value:match("/%.bare$") then
+				current = {}
 			else
-				io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+				current = { path = value, head = "", branch = "(detached)" }
 			end
+		elseif key == "HEAD" and value then
+			current.head = value:sub(1, 7)
+		elseif key == "branch" and value then
+			current.branch = value:gsub("^refs/heads/", "")
+		elseif key == "bare" then
+			current = {}
 		end
 	end
+	if current.path then
+		table.insert(worktrees, current)
+	end
 
-	-- Fetch all remotes
-	run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
+	if #worktrees == 0 then
+		print("No worktrees found")
+		return
+	end
 
-	-- Load config for path style
-	local style = global_config.branch_path_style or "nested"
-	local separator = global_config.flat_separator
-	local worktree_path = branch_to_path(project_path, default_branch, style, separator)
+	local cwd = shell.get_cwd() or ""
 
-	-- Create initial worktree
-	output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
-	if code ~= 0 then
-		die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
+	local rows = {}
+	for _, wt in ipairs(worktrees) do
+		local rel_path = path_mod.relative_path(cwd, wt.path)
+
+		local status_out = shell.run_cmd("git -C " .. wt.path .. " status --porcelain")
+		local status = status_out == "" and "clean" or "dirty"
+
+		table.insert(rows, rel_path .. "," .. wt.branch .. "," .. wt.head .. "," .. status)
 	end
 
-	-- Print summary
-	print("Created project: " .. project_path)
-	print("Default branch:  " .. default_branch)
-	print("Worktree:        " .. worktree_path)
-	if #configured_remotes > 0 then
-		print("Remotes:         " .. table.concat(configured_remotes, ", "))
+	local table_input = "Path,Branch,HEAD,Status\n" .. table.concat(rows, "\n")
+	table_input = table_input:gsub("EOF", "eof")
+	local table_cmd = "gum table --print <<'EOF'\n" .. table_input .. "\nEOF"
+	local table_handle = io.popen(table_cmd, "r")
+	if not table_handle then
+		return
 	end
+	io.write(table_handle:read("*a") or "")
+	table_handle:close()
 end
 
----@param args string[]
-local function cmd_new(args)
-	-- Parse arguments: <project-name> [--remote name]...
-	local project_name = nil
-	---@type string[]
-	local remote_flags = {}
+return M
+]]
 
-	local i = 1
-	while i <= #args do
-		local a = args[i]
-		if a == "--remote" then
-			if not args[i + 1] then
-				die("--remote requires a name")
-			end
-			table.insert(remote_flags, args[i + 1])
-			i = i + 1
-		elseif not project_name then
-			project_name = a
-		else
-			die("unexpected argument: " .. a)
-		end
-		i = i + 1
-	end
+_EMBEDDED_MODULES["wt.cmd.fetch"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
 
-	if not project_name then
-		die("usage: wt n <project-name> [--remote name]...")
+local exit = require("wt.exit")
+local shell = require("wt.shell")
+local git = require("wt.git")
+
+---@class wt.cmd.fetch
+local M = {}
+
+---Fetch all remotes with pruning
+function M.cmd_fetch()
+	local root, err = git.find_project_root()
+	if not root then
+		shell.die(err --[[@as string]])
 		return
 	end
 
-	-- Check if project directory already exists
-	local cwd = get_cwd()
-	if not cwd then
-		die("failed to get current directory", EXIT_SYSTEM_ERROR)
-	end
-	local project_path = cwd .. "/" .. project_name
-	local check = io.open(project_path, "r")
-	if check then
-		check:close()
-		die("directory already exists: " .. project_path)
+	local git_dir = root .. "/.bare"
+	local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all --prune")
+	io.write(output)
+	if code ~= 0 then
+		os.exit(exit.EXIT_SYSTEM_ERROR)
 	end
+end
 
-	-- Load global config
-	local global_config = load_global_config()
+return M
+]]
 
-	-- Determine which remotes to use
-	---@type string[]
-	local selected_remotes = {}
 
-	if #remote_flags > 0 then
-		-- Use explicitly provided remotes
-		selected_remotes = remote_flags
-	elseif global_config.default_remotes then
-		if type(global_config.default_remotes) == "table" then
-			selected_remotes = global_config.default_remotes
-		elseif global_config.default_remotes == "prompt" then
-			-- Prompt with gum choose
-			if global_config.remotes then
-				local keys = {}
-				for k in pairs(global_config.remotes) do
-					table.insert(keys, k)
-				end
-				table.sort(keys)
-				if #keys > 0 then
-					local input = table.concat(keys, "\n")
-					local cmd = "echo '" .. input .. "' | gum choose --no-limit"
-					local output, code = run_cmd(cmd)
-					if code == 0 and output ~= "" then
-						for line in output:gmatch("[^\n]+") do
-							table.insert(selected_remotes, line)
-						end
-					end
-				end
-			end
-		end
-	elseif global_config.remotes then
-		-- No default_remotes configured, prompt if remotes exist
-		local keys = {}
-		for k in pairs(global_config.remotes) do
-			table.insert(keys, k)
-		end
-		table.sort(keys)
-		if #keys > 0 then
-			local input = table.concat(keys, "\n")
-			local cmd = "echo '" .. input .. "' | gum choose --no-limit"
-			local output, code = run_cmd(cmd)
-			if code == 0 and output ~= "" then
-				for line in output:gmatch("[^\n]+") do
-					table.insert(selected_remotes, line)
-				end
-			end
-		end
-	end
+if _VERSION < "Lua 5.2" then
+	io.stderr:write("error: wt requires Lua 5.2 or later\n")
+	os.exit(1)
+end
 
-	-- Create project structure
-	local bare_path = project_path .. "/.bare"
-	local output, code = run_cmd("mkdir -p " .. bare_path)
-	if code ~= 0 then
-		die("failed to create directory: " .. output, EXIT_SYSTEM_ERROR)
-	end
+local exit = require("wt.exit")
+local EXIT_SUCCESS = exit.EXIT_SUCCESS
+local EXIT_USER_ERROR = exit.EXIT_USER_ERROR
+local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR
 
-	output, code = run_cmd("git init --bare " .. bare_path)
-	if code ~= 0 then
-		die("failed to init bare repo: " .. output, EXIT_SYSTEM_ERROR)
-	end
+local shell = require("wt.shell")
+local run_cmd = shell.run_cmd
+local run_cmd_silent = shell.run_cmd_silent
+local get_cwd = shell.get_cwd
+local die = shell.die
 
-	-- Write .git file pointing to .bare
-	local git_file_handle = io.open(project_path .. "/.git", "w")
-	if not git_file_handle then
-		die("failed to create .git file", EXIT_SYSTEM_ERROR)
-		return
-	end
-	git_file_handle:write("gitdir: ./.bare\n")
-	git_file_handle:close()
+local path_mod = require("wt.path")
+local branch_to_path = path_mod.branch_to_path
+local split_path = path_mod.split_path
+local relative_path = path_mod.relative_path
+local path_inside = path_mod.path_inside
+local escape_pattern = path_mod.escape_pattern
 
-	-- Add remotes
-	local git_dir = bare_path
-	for _, remote_name in ipairs(selected_remotes) do
-		local template = global_config.remotes and global_config.remotes[remote_name]
-		if template then
-			local url = resolve_url_template(template, project_name)
-			output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. url)
-			if code ~= 0 then
-				io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
-			else
-				-- Configure fetch refspec for the remote
-				run_cmd(
-					"GIT_DIR="
-						.. git_dir
-						.. " git config remote."
-						.. remote_name
-						.. ".fetch '+refs/heads/*:refs/remotes/"
-						.. remote_name
-						.. "/*'"
-				)
-			end
-		else
-			io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
-		end
-	end
+local git_mod = require("wt.git")
+local find_project_root = git_mod.find_project_root
+local detect_source_worktree = git_mod.detect_source_worktree
+local detect_cloned_default_branch = git_mod.detect_cloned_default_branch
+local parse_branch_remotes = git_mod.parse_branch_remotes
+local parse_worktree_list = git_mod.parse_worktree_list
 
-	-- Detect default branch
-	local default_branch = get_default_branch()
+local config_mod = require("wt.config")
+local resolve_url_template = config_mod.resolve_url_template
+local extract_project_name = config_mod.extract_project_name
+local load_global_config = config_mod.load_global_config
+local load_project_config = config_mod.load_project_config
 
-	-- Load config for path style
-	local style = global_config.branch_path_style or "nested"
-	local separator = global_config.flat_separator
-	local worktree_path = branch_to_path(project_path, default_branch, style, separator)
+local hooks_mod = require("wt.hooks")
+local load_hook_permissions = hooks_mod.load_hook_permissions
+local save_hook_permissions = hooks_mod.save_hook_permissions
+local summarize_hooks = hooks_mod.summarize_hooks
+local run_hooks = hooks_mod.run_hooks
 
-	-- Create orphan worktree
-	output, code =
-		run_cmd("GIT_DIR=" .. git_dir .. " git worktree add --orphan -b " .. default_branch .. " -- " .. worktree_path)
-	if code ~= 0 then
-		die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
-	end
+local help_mod = require("wt.help")
+local print_usage = help_mod.print_usage
+local show_command_help = help_mod.show_command_help
 
-	-- Print summary
-	print("Created project: " .. project_path)
-	print("Default branch:  " .. default_branch)
-	print("Worktree:        " .. worktree_path)
-	if #selected_remotes > 0 then
-		print("Remotes:         " .. table.concat(selected_remotes, ", "))
-	end
-end
+local fetch_mod = require("wt.cmd.fetch")
+local cmd_fetch = fetch_mod.cmd_fetch
+
+local list_mod = require("wt.cmd.list")
+local cmd_list = list_mod.cmd_list
+
+local remove_mod = require("wt.cmd.remove")
+local cmd_remove = remove_mod.cmd_remove
+
+local add_mod = require("wt.cmd.add")
+local cmd_add = add_mod.cmd_add
+
+local new_mod = require("wt.cmd.new")
+local cmd_new = new_mod.cmd_new
+
+local clone_mod = require("wt.cmd.clone")
+local cmd_clone = clone_mod.cmd_clone
 
 ---List directory entries (excluding . and ..)
 ---@param path string

src/main.lua 🔗

@@ -65,258 +65,8 @@ local cmd_add = add_mod.cmd_add
 local new_mod = require("wt.cmd.new")
 local cmd_new = new_mod.cmd_new
 
----@param args string[]
-local function cmd_clone(args)
-	-- Parse arguments: <url> [--remote name]... [--own]
-	local url = nil
-	---@type string[]
-	local remote_flags = {}
-	local own = false
-
-	local i = 1
-	while i <= #args do
-		local a = args[i]
-		if a == "--remote" then
-			if not args[i + 1] then
-				die("--remote requires a name")
-			end
-			table.insert(remote_flags, args[i + 1])
-			i = i + 1
-		elseif a == "--own" then
-			own = true
-		elseif not url then
-			url = a
-		else
-			die("unexpected argument: " .. a)
-		end
-		i = i + 1
-	end
-
-	if not url then
-		die("usage: wt c <url> [--remote name]... [--own]")
-		return
-	end
-
-	-- Extract project name from URL
-	local project_name = extract_project_name(url)
-	if not project_name then
-		die("could not extract project name from URL: " .. url)
-		return
-	end
-
-	-- Check if project directory already exists
-	local cwd = get_cwd()
-	if not cwd then
-		die("failed to get current directory", EXIT_SYSTEM_ERROR)
-	end
-	local project_path = cwd .. "/" .. project_name
-	local check = io.open(project_path, "r")
-	if check then
-		check:close()
-		die("directory already exists: " .. project_path)
-	end
-
-	-- Clone bare repo
-	local bare_path = project_path .. "/.bare"
-	local output, code = run_cmd("git clone --bare " .. url .. " " .. bare_path)
-	if code ~= 0 then
-		die("failed to clone: " .. output, EXIT_SYSTEM_ERROR)
-	end
-
-	-- Write .git file pointing to .bare
-	local git_file_handle = io.open(project_path .. "/.git", "w")
-	if not git_file_handle then
-		die("failed to create .git file", EXIT_SYSTEM_ERROR)
-		return
-	end
-	git_file_handle:write("gitdir: ./.bare\n")
-	git_file_handle:close()
-
-	-- Detect default branch
-	local git_dir = bare_path
-	local default_branch = detect_cloned_default_branch(git_dir)
-
-	-- Load global config
-	local global_config = load_global_config()
-
-	-- Determine which remotes to use
-	---@type string[]
-	local selected_remotes = {}
-
-	if #remote_flags > 0 then
-		selected_remotes = remote_flags
-	elseif global_config.default_remotes then
-		if type(global_config.default_remotes) == "table" then
-			selected_remotes = global_config.default_remotes
-		elseif global_config.default_remotes == "prompt" then
-			if global_config.remotes then
-				local keys = {}
-				for k in pairs(global_config.remotes) do
-					table.insert(keys, k)
-				end
-				table.sort(keys)
-				if #keys > 0 then
-					local input = table.concat(keys, "\n")
-					local choose_type = own and "" or " --no-limit"
-					local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
-					output, code = run_cmd(cmd)
-					if code == 0 and output ~= "" then
-						for line in output:gmatch("[^\n]+") do
-							table.insert(selected_remotes, line)
-						end
-					end
-				end
-			end
-		end
-	elseif global_config.remotes then
-		local keys = {}
-		for k in pairs(global_config.remotes) do
-			table.insert(keys, k)
-		end
-		table.sort(keys)
-		if #keys > 0 then
-			local input = table.concat(keys, "\n")
-			local choose_type = own and "" or " --no-limit"
-			local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
-			output, code = run_cmd(cmd)
-			if code == 0 and output ~= "" then
-				for line in output:gmatch("[^\n]+") do
-					table.insert(selected_remotes, line)
-				end
-			end
-		end
-	end
-
-	-- Track configured remotes for summary
-	---@type string[]
-	local configured_remotes = {}
-
-	if own then
-		-- User's own project: origin is their canonical remote
-		if #selected_remotes > 0 then
-			local first_remote = selected_remotes[1]
-			-- Rename origin to first remote
-			output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
-			if code ~= 0 then
-				io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
-			else
-				-- Configure fetch refspec
-				run_cmd(
-					"GIT_DIR="
-						.. git_dir
-						.. " git config remote."
-						.. first_remote
-						.. ".fetch '+refs/heads/*:refs/remotes/"
-						.. first_remote
-						.. "/*'"
-				)
-				table.insert(configured_remotes, first_remote)
-			end
-
-			-- Add additional remotes and push to them
-			for j = 2, #selected_remotes do
-				local remote_name = selected_remotes[j]
-				local template = global_config.remotes and global_config.remotes[remote_name]
-				if template then
-					local remote_url = resolve_url_template(template, project_name)
-					output, code =
-						run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
-					if code ~= 0 then
-						io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
-					else
-						run_cmd(
-							"GIT_DIR="
-								.. git_dir
-								.. " git config remote."
-								.. remote_name
-								.. ".fetch '+refs/heads/*:refs/remotes/"
-								.. remote_name
-								.. "/*'"
-						)
-						-- Push to additional remotes
-						output, code =
-							run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
-						if code ~= 0 then
-							io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
-						end
-						table.insert(configured_remotes, remote_name)
-					end
-				else
-					io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
-				end
-			end
-		else
-			-- No remotes selected, keep origin as-is
-			run_cmd("GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'")
-			table.insert(configured_remotes, "origin")
-		end
-	else
-		-- Contributing to someone else's project
-		-- Rename origin to upstream
-		output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
-		if code ~= 0 then
-			io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
-		else
-			run_cmd(
-				"GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
-			)
-			table.insert(configured_remotes, "upstream")
-		end
-
-		-- Add user's remotes and push to each
-		for _, remote_name in ipairs(selected_remotes) do
-			local template = global_config.remotes and global_config.remotes[remote_name]
-			if template then
-				local remote_url = resolve_url_template(template, project_name)
-				output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
-				if code ~= 0 then
-					io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
-				else
-					run_cmd(
-						"GIT_DIR="
-							.. git_dir
-							.. " git config remote."
-							.. remote_name
-							.. ".fetch '+refs/heads/*:refs/remotes/"
-							.. remote_name
-							.. "/*'"
-					)
-					-- Push to this remote
-					output, code =
-						run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
-					if code ~= 0 then
-						io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
-					end
-					table.insert(configured_remotes, remote_name)
-				end
-			else
-				io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
-			end
-		end
-	end
-
-	-- Fetch all remotes
-	run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
-
-	-- Load config for path style
-	local style = global_config.branch_path_style or "nested"
-	local separator = global_config.flat_separator
-	local worktree_path = branch_to_path(project_path, default_branch, style, separator)
-
-	-- Create initial worktree
-	output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
-	if code ~= 0 then
-		die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
-	end
-
-	-- Print summary
-	print("Created project: " .. project_path)
-	print("Default branch:  " .. default_branch)
-	print("Worktree:        " .. worktree_path)
-	if #configured_remotes > 0 then
-		print("Remotes:         " .. table.concat(configured_remotes, ", "))
-	end
-end
+local clone_mod = require("wt.cmd.clone")
+local cmd_clone = clone_mod.cmd_clone
 
 ---List directory entries (excluding . and ..)
 ---@param path string

src/wt/cmd/clone.lua 🔗

@@ -0,0 +1,273 @@
+-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local exit = require("wt.exit")
+local shell = require("wt.shell")
+local git = require("wt.git")
+local path_mod = require("wt.path")
+local config = require("wt.config")
+
+---@class wt.cmd.clone
+local M = {}
+
+---Clone a repository with bare repo structure
+---@param args string[]
+function M.cmd_clone(args)
+	-- Parse arguments: <url> [--remote name]... [--own]
+	local url = nil
+	---@type string[]
+	local remote_flags = {}
+	local own = false
+
+	local i = 1
+	while i <= #args do
+		local a = args[i]
+		if a == "--remote" then
+			if not args[i + 1] then
+				shell.die("--remote requires a name")
+			end
+			table.insert(remote_flags, args[i + 1])
+			i = i + 1
+		elseif a == "--own" then
+			own = true
+		elseif not url then
+			url = a
+		else
+			shell.die("unexpected argument: " .. a)
+		end
+		i = i + 1
+	end
+
+	if not url then
+		shell.die("usage: wt c <url> [--remote name]... [--own]")
+		return
+	end
+
+	-- Extract project name from URL
+	local project_name = config.extract_project_name(url)
+	if not project_name then
+		shell.die("could not extract project name from URL: " .. url)
+		return
+	end
+
+	-- Check if project directory already exists
+	local cwd = shell.get_cwd()
+	if not cwd then
+		shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
+	end
+	---@cast cwd string
+	local project_path = cwd .. "/" .. project_name
+	local check = io.open(project_path, "r")
+	if check then
+		check:close()
+		shell.die("directory already exists: " .. project_path)
+	end
+
+	-- Clone bare repo
+	local bare_path = project_path .. "/.bare"
+	local output, code = shell.run_cmd("git clone --bare " .. url .. " " .. bare_path)
+	if code ~= 0 then
+		shell.die("failed to clone: " .. output, exit.EXIT_SYSTEM_ERROR)
+	end
+
+	-- Write .git file pointing to .bare
+	local git_file_handle = io.open(project_path .. "/.git", "w")
+	if not git_file_handle then
+		shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
+		return
+	end
+	git_file_handle:write("gitdir: ./.bare\n")
+	git_file_handle:close()
+
+	-- Detect default branch
+	local git_dir = bare_path
+	local default_branch = git.detect_cloned_default_branch(git_dir)
+
+	-- Load global config
+	local global_config = config.load_global_config()
+
+	-- Determine which remotes to use
+	---@type string[]
+	local selected_remotes = {}
+
+	if #remote_flags > 0 then
+		selected_remotes = remote_flags
+	elseif global_config.default_remotes then
+		if type(global_config.default_remotes) == "table" then
+			selected_remotes = global_config.default_remotes --[[@as string[] ]]
+		elseif global_config.default_remotes == "prompt" then
+			if global_config.remotes then
+				local keys = {}
+				for k in pairs(global_config.remotes) do
+					table.insert(keys, k)
+				end
+				table.sort(keys)
+				if #keys > 0 then
+					local input = table.concat(keys, "\n")
+					local choose_type = own and "" or " --no-limit"
+					local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
+					output, code = shell.run_cmd(cmd)
+					if code == 0 and output ~= "" then
+						for line in output:gmatch("[^\n]+") do
+							table.insert(selected_remotes, line)
+						end
+					end
+				end
+			end
+		end
+	elseif global_config.remotes then
+		local keys = {}
+		for k in pairs(global_config.remotes) do
+			table.insert(keys, k)
+		end
+		table.sort(keys)
+		if #keys > 0 then
+			local input = table.concat(keys, "\n")
+			local choose_type = own and "" or " --no-limit"
+			local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
+			output, code = shell.run_cmd(cmd)
+			if code == 0 and output ~= "" then
+				for line in output:gmatch("[^\n]+") do
+					table.insert(selected_remotes, line)
+				end
+			end
+		end
+	end
+
+	-- Track configured remotes for summary
+	---@type string[]
+	local configured_remotes = {}
+
+	if own then
+		-- User's own project: origin is their canonical remote
+		if #selected_remotes > 0 then
+			local first_remote = selected_remotes[1]
+			-- Rename origin to first remote
+			output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
+			if code ~= 0 then
+				io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
+			else
+				-- Configure fetch refspec
+				shell.run_cmd(
+					"GIT_DIR="
+						.. git_dir
+						.. " git config remote."
+						.. first_remote
+						.. ".fetch '+refs/heads/*:refs/remotes/"
+						.. first_remote
+						.. "/*'"
+				)
+				table.insert(configured_remotes, first_remote)
+			end
+
+			-- Add additional remotes and push to them
+			for j = 2, #selected_remotes do
+				local remote_name = selected_remotes[j]
+				local template = global_config.remotes and global_config.remotes[remote_name]
+				if template then
+					local remote_url = config.resolve_url_template(template, project_name)
+					output, code =
+						shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
+					if code ~= 0 then
+						io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+					else
+						shell.run_cmd(
+							"GIT_DIR="
+								.. git_dir
+								.. " git config remote."
+								.. remote_name
+								.. ".fetch '+refs/heads/*:refs/remotes/"
+								.. remote_name
+								.. "/*'"
+						)
+						-- Push to additional remotes
+						output, code =
+							shell.run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
+						if code ~= 0 then
+							io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
+						end
+						table.insert(configured_remotes, remote_name)
+					end
+				else
+					io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+				end
+			end
+		else
+			-- No remotes selected, keep origin as-is
+			shell.run_cmd(
+				"GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'"
+			)
+			table.insert(configured_remotes, "origin")
+		end
+	else
+		-- Contributing to someone else's project
+		-- Rename origin to upstream
+		output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
+		if code ~= 0 then
+			io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
+		else
+			shell.run_cmd(
+				"GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
+			)
+			table.insert(configured_remotes, "upstream")
+		end
+
+		-- Add user's remotes and push to each
+		for _, remote_name in ipairs(selected_remotes) do
+			local template = global_config.remotes and global_config.remotes[remote_name]
+			if template then
+				local remote_url = config.resolve_url_template(template, project_name)
+				output, code =
+					shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
+				if code ~= 0 then
+					io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+				else
+					shell.run_cmd(
+						"GIT_DIR="
+							.. git_dir
+							.. " git config remote."
+							.. remote_name
+							.. ".fetch '+refs/heads/*:refs/remotes/"
+							.. remote_name
+							.. "/*'"
+					)
+					-- Push to this remote
+					output, code =
+						shell.run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
+					if code ~= 0 then
+						io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
+					end
+					table.insert(configured_remotes, remote_name)
+				end
+			else
+				io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+			end
+		end
+	end
+
+	-- Fetch all remotes
+	shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
+
+	-- Load config for path style
+	local style = global_config.branch_path_style or "nested"
+	local separator = global_config.flat_separator
+	local worktree_path = path_mod.branch_to_path(project_path, default_branch, style, separator)
+
+	-- Create initial worktree
+	output, code =
+		shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
+	if code ~= 0 then
+		shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
+	end
+
+	-- Print summary
+	print("Created project: " .. project_path)
+	print("Default branch:  " .. default_branch)
+	print("Worktree:        " .. worktree_path)
+	if #configured_remotes > 0 then
+		print("Remotes:         " .. table.concat(configured_remotes, ", "))
+	end
+end
+
+return M