Skip to content

Commit a3e034c

Browse files
committed
refactor(taskmaster): extract/modularize reusable prompts
1 parent 89c5f0e commit a3e034c

2 files changed

Lines changed: 198 additions & 109 deletions

File tree

src/core/taskmaster/asks.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/* libs */
2+
import inquirer from "inquirer";
3+
import chalk from "chalk";
4+
5+
/* constants */
6+
import { PRD_PATH, TASKS_STATUSES } from "@/constants";
7+
8+
// ===============================
9+
10+
/**
11+
* @description Asks the user for confirmation to overwrite the existing tasks.json file.
12+
*/
13+
export async function askOverwriteConfirmation() {
14+
const { overwrite } = await inquirer.prompt({
15+
type: "confirm",
16+
name: "overwrite",
17+
message: chalk.bgYellow(
18+
"tasks.json already exists. Do you want to overwrite it?",
19+
),
20+
default: false,
21+
});
22+
return overwrite;
23+
}
24+
25+
/**
26+
* @description Asks the user for the path to their PRD file.
27+
*/
28+
export async function askPrdPath() {
29+
const { prdPath } = await inquirer.prompt({
30+
type: "input",
31+
name: "prdPath",
32+
message: "Enter the path to your PRD file:",
33+
default: PRD_PATH,
34+
validate: (input) => {
35+
const regex = /^[\w\s/]+(?:\.(?:txt|md))$/;
36+
if (!regex.test(input)) {
37+
return "Please enter a valid PRD file with .txt or .md extension and without special characters, except for /";
38+
}
39+
return true;
40+
},
41+
});
42+
return prdPath;
43+
}
44+
45+
/**
46+
* @description Asks the user for the number of tasks to generate.
47+
*/
48+
export async function askNumTasksToGenerate() {
49+
const { numTasksToGenerate } = await inquirer.prompt({
50+
type: "number",
51+
name: "numTasksToGenerate",
52+
message: "Enter the number of tasks to generate:",
53+
default: 10,
54+
validate: (input) => {
55+
const num = Number(input);
56+
if (Number.isNaN(num) || num < 3 || num > 30) {
57+
return "Please enter a valid number between 3 and 30";
58+
}
59+
return true;
60+
},
61+
});
62+
return numTasksToGenerate;
63+
}
64+
65+
/**
66+
* @description Asks the user for confirmation to allow advanced research for task generation (using AI).
67+
*/
68+
export async function askAdvancedResearchConfirmation() {
69+
const { allowAdvancedResearch } = await inquirer.prompt({
70+
type: "confirm",
71+
name: "allowAdvancedResearch",
72+
message: "Allow advanced research for task generation ?",
73+
default: false,
74+
});
75+
return allowAdvancedResearch;
76+
}
77+
78+
/**
79+
* @description Asks the user to enter a tag for the tasks.
80+
*/
81+
export async function askTaskTag() {
82+
const { tag } = await inquirer.prompt({
83+
type: "input",
84+
name: "tag",
85+
message: "Enter a tag for the tasks:",
86+
default: "master",
87+
validate: (input) => {
88+
const regex = /^[a-z0_9_]+$/;
89+
if (!regex.test(input)) {
90+
return "Please enter a valid tag with only lowercase letters, numbers, and underscores (_).";
91+
}
92+
return true;
93+
},
94+
});
95+
return tag;
96+
}
97+
98+
/**
99+
* @description Asks the user for confirmation to decompose all tasks.
100+
*/
101+
export async function askDecompositionConfirmation() {
102+
const { confirmDecomposition } = await inquirer.prompt({
103+
type: "confirm",
104+
name: "confirmDecomposition",
105+
message: chalk.yellow(
106+
"Confirm that you want to expand all tasks? This action will decompose every task into smaller subtasks and may increase the total number of tasks to manage.",
107+
),
108+
default: false,
109+
});
110+
return confirmDecomposition;
111+
}
112+
113+
/**
114+
* @description Asks the user to select task statuses.
115+
*/
116+
export async function askStatusSelection() {
117+
const { status: validatedStatus } = await inquirer.prompt([
118+
{
119+
type: "checkbox",
120+
name: "status",
121+
message: "Select task statuses:",
122+
choices: TASKS_STATUSES.map((status) => ({
123+
name: status,
124+
value: status,
125+
})),
126+
validate: (input) => {
127+
if (!input.length) return "At least one status is required";
128+
return true;
129+
},
130+
filter: (input) => input.join(","),
131+
},
132+
]);
133+
return validatedStatus;
134+
}
135+
136+
/**
137+
* @description Asks the user for display options when listing tasks.
138+
*/
139+
export async function askDisplayOptions() {
140+
const { quickly, withSubtasks } = await inquirer.prompt([
141+
{
142+
type: "confirm",
143+
name: "quickly",
144+
message: "Show tasks quickly ?",
145+
default: true,
146+
},
147+
{
148+
type: "confirm",
149+
name: "withSubtasks",
150+
message: "Show with subtasks ?",
151+
default: true,
152+
},
153+
]);
154+
return { quickly, withSubtasks };
155+
}
156+
157+
/**
158+
* @description Asks the user to enter a tag for the tasks.
159+
*/
160+
export async function askTaskIdInput() {
161+
const { taskId } = await inquirer.prompt({
162+
type: "input",
163+
name: "taskId",
164+
message: "Enter the task ID:",
165+
validate: (input) => {
166+
if (!input || !/^(\d+)(\.\d+)*$/.test(input)) {
167+
return "Invalid task ID. Must be an integer or hierarchical ID (e.g. 1, 2.1, 5.1.1)";
168+
}
169+
return true;
170+
},
171+
});
172+
return taskId;
173+
}

src/core/taskmaster/exec.ts

Lines changed: 25 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
/* libs */
22
import inquirer from "inquirer";
33
import path from "node:path";
4-
import chalk from "chalk";
54

65
/* constants */
7-
import { PRD_PATH, TASKS_PATH, TASKS_STATUSES } from "@/constants";
6+
import { TASKS_PATH, TASKS_STATUSES } from "@/constants";
87

98
/* core */
109
import { TaskMaster } from "@/core/taskmaster/TaskMaster";
@@ -13,6 +12,19 @@ import { restartAsync } from "@/core/restart";
1312
/* utils */
1413
import { existsAsync } from "@/utils/extras";
1514

15+
/* asks */
16+
import {
17+
askOverwriteConfirmation,
18+
askPrdPath,
19+
askNumTasksToGenerate,
20+
askAdvancedResearchConfirmation,
21+
askTaskTag,
22+
askDecompositionConfirmation,
23+
askStatusSelection,
24+
askDisplayOptions,
25+
askTaskIdInput,
26+
} from "@/core/taskmaster/asks";
27+
1628
/* prompt */
1729
import {
1830
tmaiInitMenu_prompt,
@@ -32,7 +44,6 @@ const tmai = new TaskMaster({
3244
isTestMode: false,
3345
});
3446

35-
/******* be6e15c7-9970-4578-baf6-d31421004679 *******/
3647
// TODO: done
3748
export async function tmaiInitAsync() {
3849
const choice = await inquirer.prompt(tmaiInitMenu_prompt);
@@ -56,67 +67,16 @@ export async function tmaiGenAsync() {
5667
const tasksJsonPath = path.join(".taskmaster", "tasks", "tasks.json");
5768

5869
if (await existsAsync(tasksJsonPath)) {
59-
const { overwrite } = await inquirer.prompt({
60-
type: "confirm",
61-
name: "overwrite",
62-
message: chalk.bgYellow(
63-
"tasks.json already exists. Do you want to overwrite it?",
64-
),
65-
});
66-
70+
const overwrite = await askOverwriteConfirmation();
6771
if (!overwrite) {
6872
return restartAsync();
6973
}
7074
}
7175

72-
const { prdPath } = await inquirer.prompt({
73-
type: "input",
74-
name: "prdPath",
75-
message: "Enter the path to your PRD file:",
76-
default: PRD_PATH,
77-
validate: (input) => {
78-
const regex = /^[\w\s/]+(?:\.(?:txt|md))$/;
79-
if (!regex.test(input)) {
80-
return "Please enter a valid PRD file with .txt or .md extension and without special characters, except for /";
81-
}
82-
return true;
83-
},
84-
});
85-
86-
const { numTasksToGenerate } = await inquirer.prompt({
87-
type: "number",
88-
name: "numTasksToGenerate",
89-
message: "Enter the number of tasks to generate:",
90-
validate: (input) => {
91-
const num = Number(input);
92-
if (Number.isNaN(num) || num < 3 || num > 30) {
93-
return "Please enter a valid number between 3 and 30";
94-
}
95-
return true;
96-
},
97-
default: 10,
98-
});
99-
100-
const { allowAdvancedResearch } = await inquirer.prompt({
101-
type: "confirm",
102-
name: "allowAdvancedResearch",
103-
message: "Allow advanced research for task generation ?",
104-
default: false,
105-
});
106-
107-
const { tag } = await inquirer.prompt({
108-
type: "input",
109-
name: "tag",
110-
message: "Enter a tag for the tasks:",
111-
default: "master",
112-
validate: (input) => {
113-
const regex = /^[a-z0-9_]+$/;
114-
if (!regex.test(input)) {
115-
return "Please enter a valid tag with only lowercase letters, numbers, and underscores (_).";
116-
}
117-
return true;
118-
},
119-
});
76+
const prdPath = await askPrdPath();
77+
const numTasksToGenerate = await askNumTasksToGenerate();
78+
const allowAdvancedResearch = await askAdvancedResearchConfirmation();
79+
const tag = await askTaskTag();
12080

12181
await tmai.parseAsync(
12282
prdPath,
@@ -128,20 +88,14 @@ export async function tmaiGenAsync() {
12888
} else if (choice.tmaiGenDecMenu === "tmai-gen") {
12989
await tmai.genAsync();
13090
} else if (choice.tmaiGenDecMenu === "tmai-dec") {
131-
const { confirmDecomposition } = await inquirer.prompt({
132-
type: "confirm",
133-
name: "confirmDecomposition",
134-
message: chalk.yellow(
135-
"Confirm that you want to expand all tasks? This action will decompose every task into smaller subtasks and may increase the total number of tasks to manage.",
136-
),
137-
default: false,
138-
});
91+
const confirmDecomposition = await askDecompositionConfirmation();
13992
if (!confirmDecomposition) {
14093
console.log("Decomposition of tasks cancelled!");
14194
return restartAsync();
14295
}
14396

144-
await tmai.decomposeAsync();
97+
const tag = await askTaskTag();
98+
await tmai.decomposeAsync(tag);
14599
}
146100

147101
await restartAsync();
@@ -156,52 +110,14 @@ export async function tmaiManageAsync() {
156110
case "tmai-listnav": {
157111
const { tmaiListNavMenu } = await inquirer.prompt(tmaiListNavMenu_prompt);
158112
if (tmaiListNavMenu === "tmai-list") {
159-
const { status: validatedStatus } = await inquirer.prompt([
160-
{
161-
type: "checkbox",
162-
name: "status",
163-
message: "Select task statuses:",
164-
choices: TASKS_STATUSES.map((status) => ({
165-
name: status,
166-
value: status,
167-
})),
168-
validate: (input) => {
169-
if (!input.length) return "At least one status is required";
170-
return true;
171-
},
172-
filter: (input) => input.join(","),
173-
},
174-
]);
175-
const { quickly, withSubtasks } = await inquirer.prompt([
176-
{
177-
type: "confirm",
178-
name: "quickly",
179-
message: "Show tasks quickly ?",
180-
default: true,
181-
},
182-
{
183-
type: "confirm",
184-
name: "withSubtasks",
185-
message: "Show with subtasks ?",
186-
default: true,
187-
},
188-
]);
113+
const validatedStatus = await askStatusSelection();
114+
const { quickly, withSubtasks } = await askDisplayOptions();
189115
await tmai.listAsync(tasks, validatedStatus, quickly, withSubtasks);
190116
} else if (tmaiListNavMenu === "tmai-show") {
191117
console.log(
192118
await tmai.listQuickAsync(tasks, TASKS_STATUSES.join(","), true),
193119
);
194-
const { taskId } = await inquirer.prompt({
195-
type: "input",
196-
name: "taskId",
197-
message: "Enter the task ID:",
198-
validate: (input) => {
199-
if (!input || !/^(\d+)(\.\d+)*$/.test(input)) {
200-
return "Invalid task ID. Must be an integer or hierarchical ID (e.g. 1, 2.1, 5.1.1)";
201-
}
202-
return true;
203-
},
204-
});
120+
const taskId = await askTaskIdInput();
205121
await tmai.showAsync(taskId);
206122
} else if (tmaiListNavMenu === "tmai-next") {
207123
await tmai.nextAsync();

0 commit comments

Comments
 (0)