fix(cmd): allow flags anywhere in argument list

Amolith created

Refactored argument parsing in add, clone, and new commands to use a
for-loop pattern that collects flags and positional args separately.
This allows POSIX-style flexible flag positioning (e.g., `wt a -b
branch` works the same as `wt a branch -b`).

Also adds @cast annotations after validation to satisfy the type
checker.

Assisted-by: Claude Opus 4.5 via Amp

Change summary

dist/wt              | 60 ++++++++++++++++++++++++++++-----------------
src/wt/cmd/add.lua   | 29 ++++++++++++---------
src/wt/cmd/clone.lua | 17 +++++++++---
src/wt/cmd/new.lua   | 17 +++++++++---
4 files changed, 77 insertions(+), 46 deletions(-)

Detailed changes

dist/wt 🔗

@@ -960,12 +960,15 @@ local M = {}
 ---@param args string[]
 function M.cmd_clone(args)
 	-- Parse arguments: <url> [--remote name]... [--own]
-	local url = nil
+	---@type string|nil
+	local url
 	---@type string[]
 	local remote_flags = {}
 	local own = false
 
 	local i = 1
+	-- Flags can appear anywhere in the argument list
+	local positional = {}
 	while i <= #args do
 		local a = args[i]
 		if a == "--remote" then
@@ -976,18 +979,21 @@ function M.cmd_clone(args)
 			i = i + 1
 		elseif a == "--own" then
 			own = true
-		elseif not url then
-			url = a
+		elseif a:match("^%-") then
+			shell.die("unknown flag: " .. a)
 		else
-			shell.die("unexpected argument: " .. a)
+			table.insert(positional, a)
 		end
 		i = i + 1
 	end
 
-	if not url then
+	if #positional == 0 then
 		shell.die("usage: wt c <url> [--remote name]... [--own]")
 		return
+	elseif #positional > 1 then
+		shell.die("unexpected argument: " .. positional[2])
 	end
+	url = positional[1]
 
 	-- Extract project name from URL
 	local project_name = config.extract_project_name(url)
@@ -1248,11 +1254,14 @@ local M = {}
 ---@param args string[]
 function M.cmd_new(args)
 	-- Parse arguments: <project-name> [--remote name]...
-	local project_name = nil
+	---@type string|nil
+	local project_name
 	---@type string[]
 	local remote_flags = {}
 
 	local i = 1
+	-- Flags can appear anywhere in the argument list
+	local positional = {}
 	while i <= #args do
 		local a = args[i]
 		if a == "--remote" then
@@ -1261,18 +1270,21 @@ function M.cmd_new(args)
 			end
 			table.insert(remote_flags, args[i + 1])
 			i = i + 1
-		elseif not project_name then
-			project_name = a
+		elseif a:match("^%-") then
+			shell.die("unknown flag: " .. a)
 		else
-			shell.die("unexpected argument: " .. a)
+			table.insert(positional, a)
 		end
 		i = i + 1
 	end
 
-	if not project_name then
+	if #positional == 0 then
 		shell.die("usage: wt n <project-name> [--remote name]...")
 		return
+	elseif #positional > 1 then
+		shell.die("unexpected argument: " .. positional[2])
 	end
+	project_name = positional[1]
 
 	-- Check if project directory already exists
 	local cwd = shell.get_cwd()
@@ -1444,27 +1456,29 @@ function M.cmd_add(args)
 	---@type string|nil
 	local start_point = nil
 
-	local i = 1
-	while i <= #args do
-		local a = args[i]
+	-- Collect flags and positional args (flags can appear anywhere)
+	local positional = {}
+	for _, a in ipairs(args) do
 		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
-		elseif not branch then
-			branch = a
+		elseif a:match("^%-") then
+			shell.die("unknown flag: " .. a)
 		else
-			shell.die("unexpected argument: " .. a)
+			table.insert(positional, a)
 		end
-		i = i + 1
 	end
 
-	if not branch then
+	-- Assign positional args: <branch> [<start-point>]
+	if #positional == 0 then
 		shell.die("usage: wt a <branch> [-b [<start-point>]]")
 		return
+	elseif #positional == 1 then
+		branch = positional[1]
+	elseif #positional == 2 then
+		branch = positional[1]
+		start_point = positional[2]
+	else
+		shell.die("unexpected argument: " .. positional[3])
 	end
 
 	local root, err = git.find_project_root()

src/wt/cmd/add.lua 🔗

@@ -22,28 +22,31 @@ function M.cmd_add(args)
 	---@type string|nil
 	local start_point = nil
 
-	local i = 1
-	while i <= #args do
-		local a = args[i]
+	-- Collect flags and positional args (flags can appear anywhere)
+	local positional = {}
+	for _, a in ipairs(args) do
 		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
-		elseif not branch then
-			branch = a
+		elseif a:match("^%-") then
+			shell.die("unknown flag: " .. a)
 		else
-			shell.die("unexpected argument: " .. a)
+			table.insert(positional, a)
 		end
-		i = i + 1
 	end
 
-	if not branch then
+	-- Assign positional args: <branch> [<start-point>]
+	if #positional == 0 then
 		shell.die("usage: wt a <branch> [-b [<start-point>]]")
 		return
+	elseif #positional == 1 then
+		branch = positional[1]
+	elseif #positional == 2 then
+		branch = positional[1]
+		start_point = positional[2]
+	else
+		shell.die("unexpected argument: " .. positional[3])
 	end
+	---@cast branch string
 
 	local root, err = git.find_project_root()
 	if not root then

src/wt/cmd/clone.lua 🔗

@@ -15,12 +15,15 @@ local M = {}
 ---@param args string[]
 function M.cmd_clone(args)
 	-- Parse arguments: <url> [--remote name]... [--own]
-	local url = nil
+	---@type string|nil
+	local url
 	---@type string[]
 	local remote_flags = {}
 	local own = false
 
 	local i = 1
+	-- Flags can appear anywhere in the argument list
+	local positional = {}
 	while i <= #args do
 		local a = args[i]
 		if a == "--remote" then
@@ -31,18 +34,22 @@ function M.cmd_clone(args)
 			i = i + 1
 		elseif a == "--own" then
 			own = true
-		elseif not url then
-			url = a
+		elseif a:match("^%-") then
+			shell.die("unknown flag: " .. a)
 		else
-			shell.die("unexpected argument: " .. a)
+			table.insert(positional, a)
 		end
 		i = i + 1
 	end
 
-	if not url then
+	if #positional == 0 then
 		shell.die("usage: wt c <url> [--remote name]... [--own]")
 		return
+	elseif #positional > 1 then
+		shell.die("unexpected argument: " .. positional[2])
 	end
+	url = positional[1]
+	---@cast url string
 
 	-- Extract project name from URL
 	local project_name = config.extract_project_name(url)

src/wt/cmd/new.lua 🔗

@@ -15,11 +15,14 @@ local M = {}
 ---@param args string[]
 function M.cmd_new(args)
 	-- Parse arguments: <project-name> [--remote name]...
-	local project_name = nil
+	---@type string|nil
+	local project_name
 	---@type string[]
 	local remote_flags = {}
 
 	local i = 1
+	-- Flags can appear anywhere in the argument list
+	local positional = {}
 	while i <= #args do
 		local a = args[i]
 		if a == "--remote" then
@@ -28,18 +31,22 @@ function M.cmd_new(args)
 			end
 			table.insert(remote_flags, args[i + 1])
 			i = i + 1
-		elseif not project_name then
-			project_name = a
+		elseif a:match("^%-") then
+			shell.die("unknown flag: " .. a)
 		else
-			shell.die("unexpected argument: " .. a)
+			table.insert(positional, a)
 		end
 		i = i + 1
 	end
 
-	if not project_name then
+	if #positional == 0 then
 		shell.die("usage: wt n <project-name> [--remote name]...")
 		return
+	elseif #positional > 1 then
+		shell.die("unexpected argument: " .. positional[2])
 	end
+	project_name = positional[1]
+	---@cast project_name string
 
 	-- Check if project directory already exists
 	local cwd = shell.get_cwd()