diff --git a/docs/src/pages/cli.astro b/docs/src/pages/cli.astro index 9667cd7..cce308d 100644 --- a/docs/src/pages/cli.astro +++ b/docs/src/pages/cli.astro @@ -163,12 +163,17 @@ dex complete abc123 --result "Planning complete, no code changes" --no-commit`}
  • -d, --description — Updated description
  • --add-blocker <id> — Add blocking dependency
  • --remove-blocker <id> — Remove blocking dependency
  • +
  • --parent <id> — Move task under a new parent
  • +
  • --remove-parent — Promote a subtask to a top-level task
  • +

    Note: --parent and --remove-parent cannot be used together.

    diff --git a/plugins/dex/skills/dex/cli-reference.md b/plugins/dex/skills/dex/cli-reference.md index da96c9e..f2314e3 100644 --- a/plugins/dex/skills/dex/cli-reference.md +++ b/plugins/dex/skills/dex/cli-reference.md @@ -67,8 +67,12 @@ This captures commit SHA, message, and branch automatically. The linked GitHub/S dex edit -n "Updated name" --description "Updated description" dex edit --add-blocker xyz123 # Add blocking dependency dex edit --remove-blocker xyz123 # Remove blocking dependency +dex edit --parent xyz123 # Move task under a new parent +dex edit --remove-parent # Promote a subtask to a top-level task ``` +Note: `--parent` and `--remove-parent` cannot be used together. + ## Delete a Task ```bash diff --git a/src/cli/completion.test.ts b/src/cli/completion.test.ts index 7aa359a..6cb83c8 100644 --- a/src/cli/completion.test.ts +++ b/src/cli/completion.test.ts @@ -52,6 +52,13 @@ describe("completion command", () => { expect(out).toContain("complete"); expect(out).toContain("completion"); }); + + it("includes --remove-parent flag for edit command", async () => { + await runCli(["completion", "bash"], { storage }); + + const out = getStdout(); + expect(out).toContain("--remove-parent"); + }); }); describe("zsh completion", () => { @@ -73,6 +80,15 @@ describe("completion command", () => { expect(out).toContain("list:List tasks"); expect(out).toContain("show:View task details"); }); + + it("includes --remove-parent flag for edit command", async () => { + await runCli(["completion", "zsh"], { storage }); + + const out = getStdout(); + expect(out).toContain( + "--remove-parent[Promote subtask to top-level task]", + ); + }); }); describe("fish completion", () => { @@ -93,6 +109,13 @@ describe("completion command", () => { expect(out).toContain('-a "list" -d "List tasks"'); expect(out).toContain('-a "show" -d "View task details"'); }); + + it("includes --remove-parent flag for edit command", async () => { + await runCli(["completion", "fish"], { storage }); + + const out = getStdout(); + expect(out).toContain("-l remove-parent"); + }); }); describe("help", () => { diff --git a/src/cli/completion/bash.ts b/src/cli/completion/bash.ts index 4dd9cb6..bc6451d 100644 --- a/src/cli/completion/bash.ts +++ b/src/cli/completion/bash.ts @@ -63,7 +63,7 @@ _dex_completion() { flags="--expand -e --full -f --json --help -h" ;; edit|update) - flags="--name -n --description -d --priority -p --parent --add-blocker --remove-blocker --help -h" + flags="--name -n --description -d --priority -p --parent --remove-parent --add-blocker --remove-blocker --help -h" ;; complete|done) flags="--result -r --commit -c --help -h" diff --git a/src/cli/completion/fish.ts b/src/cli/completion/fish.ts index 69c7749..e88e9c4 100644 --- a/src/cli/completion/fish.ts +++ b/src/cli/completion/fish.ts @@ -97,6 +97,7 @@ complete -c dex -n "contains -- edit (commandline -opc); or contains -- update ( complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s d -l description -d "New description" -r complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s p -l priority -d "New priority" -r complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -l parent -d "New parent task ID" -r -a "(__dex_task_ids)" +complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -l remove-parent -d "Promote subtask to top-level task" complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s s -l status -d "New status" -r -a "pending completed" complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s h -l help -d "Show help" diff --git a/src/cli/completion/zsh.ts b/src/cli/completion/zsh.ts index 13719a9..169f2a1 100644 --- a/src/cli/completion/zsh.ts +++ b/src/cli/completion/zsh.ts @@ -65,6 +65,7 @@ _dex() { '(-d --description)'{-d,--description}'[New description]:description:' \\ '(-p --priority)'{-p,--priority}'[New priority]:priority:' \\ '--parent[New parent task ID]:parent:_dex_task_ids' \\ + '--remove-parent[Promote subtask to top-level task]' \\ '--add-blocker[Add blocker task IDs]:blockers:_dex_task_ids' \\ '--remove-blocker[Remove blocker task IDs]:blockers:_dex_task_ids' \\ '(-h --help)'{-h,--help}'[Show help]' diff --git a/src/cli/edit.test.ts b/src/cli/edit.test.ts index 794dbb8..30809f6 100644 --- a/src/cli/edit.test.ts +++ b/src/cli/edit.test.ts @@ -285,6 +285,7 @@ describe("edit command", () => { expect(out).toContain("--description"); expect(out).toContain("--add-blocker"); expect(out).toContain("--remove-blocker"); + expect(out).toContain("--remove-parent"); }); it("requires task ID", async () => { @@ -292,6 +293,84 @@ describe("edit command", () => { expect(output.stderr.join("\n")).toContain("Task ID is required"); }); + it("fails if both --remove-parent and --parent is provided", async () => { + await expect( + runCli( + [ + "edit", + "abc123", // taskId + "--remove-parent", + "--parent", + "def456", // parentId + ], + { storage }, + ), + ).rejects.toThrow("process.exit"); + + const out = output.stderr.join("\n"); + expect(out).toContain( + "You cannot both remove a parent and set a new parent.", + ); + expect(out).toContain( + "Use --parent to overwrite the existing parent ID with another parent task.", + ); + }); + + it("removes parent_id from task if --remove-parent is provided", async () => { + // Create parent task + await runCli(["create", "-n", "Test task", "--description", "ctx"], { + storage, + }); + const parentTaskId = output.stdout.join("\n").match(TASK_ID_REGEX)?.[1]; + output.stdout.length = 0; + + // Create subtask + await runCli( + [ + "create", + "-n", + "Test subtask", + "--description", + "ctx", + "--parent", + parentTaskId!, + ], + { + storage, + }, + ); + const taskId = output.stdout.join("\n").match(TASK_ID_REGEX)?.[1]; + output.stdout.length = 0; + + // Test the paren taks lists the subtask as a child + await runCli(["show", parentTaskId!, "--json"], { storage }); + let showOut = output.stdout.join("\n"); + expect(JSON.parse(showOut).children.includes(taskId!)).toBe(true); + output.stdout.length = 0; + + // Test the task is a child of the parent task + await runCli(["show", taskId!], { storage }); + showOut = output.stdout.join("\n"); + expect(showOut).toContain(`View parent task: dex show ${parentTaskId!}`); + output.stdout.length = 0; + + // Remove the parent + await runCli(["edit", taskId!, "--remove-parent"], { storage }); + output.stdout.length = 0; + + // Test the task no longer shows it's parent task + await runCli(["show", taskId!], { storage }); + showOut = output.stdout.join("\n"); + expect(showOut).not.toContain("View parent task"); + output.stdout.length = 0; + + // Test the parent task no longer lists the subtask as a child + await runCli(["show", parentTaskId!, "--json"], { storage }); + showOut = output.stdout.join("\n"); + expect(JSON.parse(showOut).children.includes(taskId!)).toBe(false); + output.stdout.length = 0; + }); + it("links commit to task with --commit flag", async () => { await runCli(["create", "-n", "Test task", "--description", "ctx"], { storage, diff --git a/src/cli/edit.ts b/src/cli/edit.ts index d25ac67..3b36e05 100644 --- a/src/cli/edit.ts +++ b/src/cli/edit.ts @@ -23,6 +23,7 @@ export async function editCommand( parent: { hasValue: true }, "add-blocker": { hasValue: true }, "remove-blocker": { hasValue: true }, + "remove-parent": { hasValue: false }, commit: { short: "c", hasValue: true }, help: { short: "h", hasValue: false }, }, @@ -45,6 +46,7 @@ ${colors.bold}OPTIONS:${colors.reset} --parent New parent task ID --add-blocker Comma-separated task IDs to add as blockers --remove-blocker Comma-separated task IDs to remove as blockers + --remove-parent Change the task from a subtask to a task -c, --commit Link a git commit to the task -h, --help Show this help message @@ -54,6 +56,7 @@ ${colors.bold}EXAMPLE:${colors.reset} dex edit abc123 --description "More details about the task" dex edit abc123 --add-blocker def456 dex edit abc123 --remove-blocker def456 + dex edit abc123 --remove-parent dex edit abc123 --commit a1b2c3d `); return; @@ -97,6 +100,20 @@ ${colors.bold}EXAMPLE:${colors.reset} process.exit(1); } + // Determine what parentId to assign to the task + const removeParent = getBooleanFlag(flags, "remove-parent"); + const setParentId = getStringFlag(flags, "parent"); + if (removeParent && setParentId) { + console.error( + `${colors.red}Error:${colors.reset} You cannot both remove a parent and set a new parent.`, + ); + console.error( + ` Use --parent to overwrite the existing parent ID with another parent task.`, + ); + process.exit(1); + } + const newParentId = removeParent ? null : setParentId; + const service = createService(options); try { // Fetch existing task to merge metadata @@ -122,7 +139,7 @@ ${colors.bold}EXAMPLE:${colors.reset} id, name: getStringFlag(flags, "name"), description: getStringFlag(flags, "description"), - parent_id: getStringFlag(flags, "parent"), + parent_id: newParentId, priority: parseIntFlag(flags, "priority"), add_blocked_by: addBlockedBy, remove_blocked_by: removeBlockedBy,