Skip to content

Commit 6000d97

Browse files
committed
feat(taskmaster): add backup/restore/clear functionality with 3 slots
1 parent 0cf0535 commit 6000d97

6 files changed

Lines changed: 254 additions & 2 deletions

File tree

src/constants/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export const DEV_MODE = true;
55
export const FONT_PATH = "fonts/Standard.flf";
66
export const PRD_PATH = DEV_MODE ? "docs/PRD-todo.md" : "docs/PRD.md";
77
export const TASKS_PATH = ".taskmaster/tasks/tasks.json";
8+
export const TASKS_SRC_PATH = ".taskmaster";
9+
export const TASKS_BCK_DEST_PATH = "backups/tmai-backup";
810

911
// tasks
1012
export const MAIN_COMMAND = "task-master";
@@ -19,6 +21,13 @@ export const TASKS_STATUSES = [
1921
"todo",
2022
"blocked",
2123
];
24+
export const TASKS_FILES = [
25+
"windsurfrules",
26+
".cursor",
27+
".roo",
28+
".roomodes",
29+
".taskmaster",
30+
];
2231
export const DEFAULT_TAG = "master";
2332
export const DEFAULT_STATUS = "pending";
2433
export const DEFAULT_TASKS_TO_GENERATE = 10;

src/core/taskmaster/TaskMaster.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* libs */
22
import { oraPromise } from "ora";
3-
import { mkdir, writeFile } from "node:fs/promises";
3+
import { mkdir, writeFile, rm } from "node:fs/promises";
44
import inquirer from "inquirer";
55
import path from "node:path";
66
import chalk from "chalk";
77
import fs from "node:fs";
88
import figures from "figures";
9+
import compressing from "compressing";
910

1011
/* constants */
1112
import {
@@ -14,6 +15,9 @@ import {
1415
PACKAGE_MANAGERS,
1516
TASKMASTER_INIT_MSG,
1617
TASKS_FILE_WARN,
18+
TASKS_BCK_DEST_PATH,
19+
TASKS_SRC_PATH,
20+
TASKS_FILES,
1721
} from "@/constants";
1822

1923
/* extras */
@@ -595,4 +599,110 @@ export class TaskMaster {
595599
].filter(Boolean),
596600
);
597601
}
602+
603+
// ==============================================
604+
// Backup, Restore and Clear Methods
605+
// ==============================================
606+
607+
// TODO: done
608+
/**
609+
* @description Creates a backup of the .taskmaster directory
610+
* @param slot Slot number (1-3) for the backup
611+
*/
612+
public async backupAsync(slot: string): Promise<void> {
613+
const backupPath = path.join(TASKS_BCK_DEST_PATH, `slot_${slot}.zip`);
614+
const backupDir = path.dirname(backupPath);
615+
616+
if (!fs.existsSync(backupDir)) {
617+
await mkdir(backupDir, { recursive: true });
618+
}
619+
620+
// Vérifier si le répertoire source existe
621+
if (!fs.existsSync(TASKS_SRC_PATH)) {
622+
console.warn(
623+
chalk.yellow(
624+
`Source directory ${TASKS_SRC_PATH} does not exist. Skipping backup.`,
625+
),
626+
);
627+
return;
628+
}
629+
630+
const oraOptions = {
631+
text: `Creating backup in slot ${slot}...`,
632+
successText: chalk.bgGreen(
633+
`Backup created successfully in slot ${slot}!`,
634+
),
635+
failText: chalk.bgRed(`Failed to create backup in slot ${slot}`),
636+
};
637+
638+
await oraPromise(async () => {
639+
await compressing.zip.compressDir(TASKS_SRC_PATH, backupPath);
640+
}, oraOptions);
641+
}
642+
643+
// TODO: done
644+
/**
645+
* @description Restores a backup from the specified slot
646+
* @param slot Slot number (1-3) to restore from
647+
*/
648+
public async restoreAsync(slot: string): Promise<void> {
649+
const backupPath = path.join(TASKS_BCK_DEST_PATH, `slot_${slot}.zip`);
650+
651+
if (!fs.existsSync(backupPath)) {
652+
console.warn(
653+
chalk.yellow(`No backup found in slot ${slot}. Skipping restore.`),
654+
);
655+
return;
656+
}
657+
658+
const oraOptions = {
659+
text: `Restoring backup from slot ${slot}...`,
660+
successText: chalk.bgGreen(
661+
`Backup restored successfully from slot ${slot}!`,
662+
),
663+
failText: chalk.bgRed(`Failed to restore backup from slot ${slot}`),
664+
};
665+
666+
await oraPromise(async () => {
667+
if (fs.existsSync(TASKS_SRC_PATH)) {
668+
await rm(TASKS_SRC_PATH, { recursive: true, force: true });
669+
}
670+
671+
const parentDir = path.dirname(TASKS_SRC_PATH);
672+
if (!fs.existsSync(parentDir)) {
673+
await mkdir(parentDir, { recursive: true });
674+
}
675+
676+
await compressing.zip.uncompress(backupPath, parentDir);
677+
}, oraOptions);
678+
}
679+
680+
// TODO: done
681+
/**
682+
* @description Clears all task-related files and directories
683+
*/
684+
public async clearTasksAsync(): Promise<void> {
685+
for (const filePath of TASKS_FILES) {
686+
if (!fs.existsSync(filePath)) continue;
687+
688+
const { confirm } = await inquirer.prompt({
689+
type: "confirm",
690+
name: "confirm",
691+
message: chalk.red(`Delete ${filePath}?`),
692+
default: false,
693+
});
694+
695+
if (confirm) {
696+
const oraOptions = {
697+
text: `Deleting ${filePath}...`,
698+
successText: chalk.bgGreen(`${filePath} deleted successfully!`),
699+
failText: chalk.bgRed(`Failed to delete ${filePath}`),
700+
};
701+
702+
await oraPromise(async () => {
703+
await rm(filePath, { recursive: true, force: true });
704+
}, oraOptions);
705+
}
706+
}
707+
}
598708
}

src/core/taskmaster/asks.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* libs */
22
import inquirer from "inquirer";
33
import chalk from "chalk";
4+
import path from "node:path";
5+
import * as emoji from "node-emoji";
6+
import fs from "node:fs";
47

58
/* constants */
69
import {
@@ -21,8 +24,12 @@ import {
2124
PRD_PATH,
2225
TASKS_PRIORITIES,
2326
TASKS_STATUSES,
27+
TASKS_BCK_DEST_PATH,
2428
} from "@/constants";
2529

30+
/* utils */
31+
import { existsAsync } from "@/utils/extras";
32+
2633
// ===============================
2734

2835
/**
@@ -326,6 +333,47 @@ export async function askNumSubtasks() {
326333
/**
327334
* @description Asks the user for manual subtask parameters
328335
*/
336+
export async function askBackupSlot() {
337+
const slots = [1, 2, 3];
338+
const slotChoices = [];
339+
340+
for (const slot of slots) {
341+
const backupPath = path.join(TASKS_BCK_DEST_PATH, `slot_${slot}.zip`);
342+
const exists = await existsAsync(backupPath);
343+
344+
let slotInfo = "";
345+
if (exists) {
346+
const stats = fs.statSync(backupPath);
347+
const date = new Date(stats.mtime);
348+
const formattedDate = date.toLocaleDateString("fr-FR", {
349+
day: "2-digit",
350+
month: "2-digit",
351+
year: "numeric",
352+
hour: "2-digit",
353+
minute: "2-digit",
354+
second: "2-digit",
355+
});
356+
slotInfo = `${emoji.get("floppy_disk")} ${formattedDate}`;
357+
} else {
358+
slotInfo = `${emoji.get("open_file_folder")} Empty`;
359+
}
360+
361+
slotChoices.push({
362+
name: `Slot ${slot}: ${slotInfo}`,
363+
value: slot.toString(),
364+
});
365+
}
366+
367+
const { selectedSlot } = await inquirer.prompt({
368+
type: "list",
369+
name: "selectedSlot",
370+
message: "Choisissez un slot de sauvegarde :",
371+
choices: slotChoices,
372+
});
373+
374+
return selectedSlot;
375+
}
376+
329377
export async function askSubtaskManualParams() {
330378
return await inquirer.prompt([
331379
{

src/core/taskmaster/exec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ import {
2828
askSubtaskParentId,
2929
askNumSubtasks,
3030
askSubtaskManualParams,
31+
askBackupSlot,
3132
} from "@/core/taskmaster/asks";
3233

34+
import chalk from "chalk";
35+
3336
/* prompt */
3437
import {
3538
tmaiInitMenu_prompt,
@@ -40,6 +43,7 @@ import {
4043
tmaiUpdateTasksMenu_prompt,
4144
tmaiDeleteTasksMenu_prompt,
4245
tmaiStatusTrackingMenu_prompt,
46+
tmaiBackupRestoreClearClear_prompt,
4347
} from "@/prompt";
4448

4549
// ===============================
@@ -244,3 +248,44 @@ export async function tmaiManageAsync() {
244248

245249
await restartAsync();
246250
}
251+
252+
// TODO: done
253+
export async function tmaiBackupRestoreClearAsync() {
254+
const { tmaiBackupRestoreClearMenu } = await inquirer.prompt(
255+
tmaiBackupRestoreClearClear_prompt,
256+
);
257+
258+
switch (tmaiBackupRestoreClearMenu) {
259+
case "tmai-backup": {
260+
const slot = await askBackupSlot();
261+
await tmai.backupAsync(slot);
262+
break;
263+
}
264+
case "tmai-restore": {
265+
const slot = await askBackupSlot();
266+
const { confirm } = await inquirer.prompt({
267+
type: "confirm",
268+
name: "confirm",
269+
message: chalk.yellow(
270+
`Are you sure you want to restore slot ${slot}? This will overwrite current data.`,
271+
),
272+
default: false,
273+
});
274+
275+
if (confirm) {
276+
await tmai.restoreAsync(slot);
277+
} else {
278+
console.log("Restore operation cancelled!");
279+
}
280+
break;
281+
}
282+
case "tmai-clear": {
283+
await tmai.clearTasksAsync();
284+
break;
285+
}
286+
default:
287+
console.log("Invalid option selected.");
288+
}
289+
290+
await restartAsync();
291+
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
tmaiInitAsync,
88
tmaiGenAsync,
99
tmaiManageAsync,
10+
tmaiBackupRestoreClearAsync,
1011
} from "@/core/taskmaster/exec";
1112

1213
/* utils */
@@ -58,6 +59,9 @@ export async function taskmasterCLI(): Promise<void> {
5859
case "tmai-dev":
5960
console.log("TM operations for development ...");
6061
break;
62+
case "tmai-bckrestore":
63+
await tmaiBackupRestoreClearAsync();
64+
break;
6165
case "exit":
6266
exitCLI();
6367
break;

src/prompt.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export const mainMenu_prompt = [
4141
value: "tmai-analysis",
4242
},
4343
new inquirer.Separator("―――――――――――――――――――――――――――――――――――"),
44+
{
45+
name: `${emoji.get("floppy_disk")} Backup, Restore and Clear`,
46+
value: "tmai-bckrestore",
47+
},
4448
...(DEV_MODE
4549
? [
4650
{
@@ -387,7 +391,7 @@ export const tmaiDepsMenu_prompt = [
387391
export const tmaiAnalysisReportDocs_prompt = [
388392
{
389393
type: "list",
390-
name: "tmaiAnalysisReportDocs",
394+
name: "tmaiAnalysisReportDocsMenu",
391395
message: chalk.bgBlue("Choose an operation"),
392396
loop: true,
393397
pageSize: 7,
@@ -408,3 +412,35 @@ export const tmaiAnalysisReportDocs_prompt = [
408412
],
409413
},
410414
];
415+
416+
// TODO: done
417+
// ==============================
418+
// Backup, Restore and Clear menu
419+
// ==============================
420+
421+
export const tmaiBackupRestoreClearClear_prompt = [
422+
{
423+
type: "list",
424+
name: "tmaiBackupRestoreClearMenu",
425+
message: chalk.bgBlue("Choose an operation"),
426+
loop: true,
427+
pageSize: 5,
428+
choices: [
429+
new inquirer.Separator("=== Backup, Restore and Clear ==="),
430+
{
431+
name: chalk.blue(`${emoji.get("floppy_disk")} Backup tasks`),
432+
value: "tmai-backup",
433+
},
434+
{
435+
name: chalk.yellow(
436+
`${emoji.get("arrows_counterclockwise")} Restore tasks`,
437+
),
438+
value: "tmai-restore",
439+
},
440+
{
441+
name: chalk.red(`${emoji.get("boom")} Clear all current tasks`),
442+
value: "tmai-clear",
443+
},
444+
],
445+
},
446+
];

0 commit comments

Comments
 (0)