mulby-cli 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGIN_DEVELOP_PROMPT.md +1164 -0
- package/README.md +852 -0
- package/assets/default-icon.png +0 -0
- package/dist/commands/ai-session.js +44 -0
- package/dist/commands/build.js +111 -0
- package/dist/commands/config-ai.js +291 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create/ai-create.js +183 -0
- package/dist/commands/create/assets.js +53 -0
- package/dist/commands/create/basic.js +72 -0
- package/dist/commands/create/index.js +73 -0
- package/dist/commands/create/react.js +136 -0
- package/dist/commands/create/templates/basic.js +383 -0
- package/dist/commands/create/templates/react/backend.js +72 -0
- package/dist/commands/create/templates/react/config.js +166 -0
- package/dist/commands/create/templates/react/docs.js +78 -0
- package/dist/commands/create/templates/react/hooks.js +469 -0
- package/dist/commands/create/templates/react/index.js +41 -0
- package/dist/commands/create/templates/react/types.js +1228 -0
- package/dist/commands/create/templates/react/ui.js +528 -0
- package/dist/commands/create/templates/react.js +1888 -0
- package/dist/commands/dev.js +141 -0
- package/dist/commands/pack.js +160 -0
- package/dist/commands/resume.js +97 -0
- package/dist/commands/test-ui.js +50 -0
- package/dist/index.js +71 -0
- package/dist/services/ai/PLUGIN_API.md +1102 -0
- package/dist/services/ai/PLUGIN_DEVELOP_PROMPT.md +1164 -0
- package/dist/services/ai/context-manager.js +639 -0
- package/dist/services/ai/index.js +88 -0
- package/dist/services/ai/knowledge.js +52 -0
- package/dist/services/ai/prompts.js +114 -0
- package/dist/services/ai/providers/base.js +38 -0
- package/dist/services/ai/providers/claude.js +284 -0
- package/dist/services/ai/providers/deepseek.js +28 -0
- package/dist/services/ai/providers/gemini.js +191 -0
- package/dist/services/ai/providers/glm.js +31 -0
- package/dist/services/ai/providers/minimax.js +27 -0
- package/dist/services/ai/providers/openai.js +177 -0
- package/dist/services/ai/tools.js +204 -0
- package/dist/services/ai-generator.js +968 -0
- package/dist/services/config-manager.js +117 -0
- package/dist/services/dependency-manager.js +236 -0
- package/dist/services/file-writer.js +66 -0
- package/dist/services/plan-adapter.js +244 -0
- package/dist/services/plan-command-handler.js +172 -0
- package/dist/services/plan-manager.js +502 -0
- package/dist/services/session-manager.js +113 -0
- package/dist/services/task-analyzer.js +136 -0
- package/dist/services/tui/index.js +57 -0
- package/dist/services/tui/store.js +123 -0
- package/dist/types/ai.js +172 -0
- package/dist/types/plan.js +2 -0
- package/dist/ui/Terminal.js +56 -0
- package/dist/ui/components/InputArea.js +176 -0
- package/dist/ui/components/LogArea.js +19 -0
- package/dist/ui/components/PlanPanel.js +69 -0
- package/dist/ui/components/SelectArea.js +13 -0
- package/package.json +45 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PlanCommandHandler = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const tui_1 = require("./tui");
|
|
9
|
+
/**
|
|
10
|
+
* Simplified Plan Command Handler
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* - /plan Show current plan status
|
|
14
|
+
* - /plan <需求> Force plan mode with requirement
|
|
15
|
+
*
|
|
16
|
+
* Everything else is automatic - AI manages the todo list
|
|
17
|
+
*/
|
|
18
|
+
class PlanCommandHandler {
|
|
19
|
+
constructor(planManager, getCurrentPlan, setCurrentPlan, savePlan) {
|
|
20
|
+
this.planManager = planManager;
|
|
21
|
+
this.getCurrentPlan = getCurrentPlan;
|
|
22
|
+
this.setCurrentPlan = setCurrentPlan;
|
|
23
|
+
this.savePlan = savePlan;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Handle /plan command
|
|
27
|
+
* Returns: true if handled, false if should pass to AI
|
|
28
|
+
*/
|
|
29
|
+
async handlePlanCommand(args) {
|
|
30
|
+
const requirement = args.join(' ').trim();
|
|
31
|
+
if (requirement) {
|
|
32
|
+
// /plan <requirement> - Force plan mode, pass to AI
|
|
33
|
+
tui_1.tui.log(chalk_1.default.cyan('📋 进入计划模式...'));
|
|
34
|
+
return { handled: false, requirement };
|
|
35
|
+
}
|
|
36
|
+
// /plan with no args - show current plan
|
|
37
|
+
const plan = this.getCurrentPlan();
|
|
38
|
+
if (!plan) {
|
|
39
|
+
tui_1.tui.log(chalk_1.default.gray('当前没有活动的计划。'));
|
|
40
|
+
tui_1.tui.log(chalk_1.default.gray('提示: 输入 /plan <你的需求> 来创建计划'));
|
|
41
|
+
return { handled: true };
|
|
42
|
+
}
|
|
43
|
+
this.displayPlan(plan);
|
|
44
|
+
return { handled: true };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Display plan with progress bar
|
|
48
|
+
*/
|
|
49
|
+
displayPlan(plan) {
|
|
50
|
+
const summary = this.planManager.getProgressSummary(plan);
|
|
51
|
+
tui_1.tui.log(chalk_1.default.cyan(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
52
|
+
tui_1.tui.log(chalk_1.default.cyan(`📋 ${plan.goal}`));
|
|
53
|
+
tui_1.tui.log(chalk_1.default.cyan(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
54
|
+
// Progress bar
|
|
55
|
+
const progressBar = this.renderProgressBar(summary.percentage, 30);
|
|
56
|
+
tui_1.tui.log(`${progressBar} ${summary.completed}/${summary.total} (${Math.round(summary.percentage)}%)`);
|
|
57
|
+
tui_1.tui.log('');
|
|
58
|
+
// Task list
|
|
59
|
+
for (const task of plan.tasks) {
|
|
60
|
+
const icon = this.getStatusIcon(task.status);
|
|
61
|
+
const color = task.status === 'in_progress' ? chalk_1.default.yellow :
|
|
62
|
+
task.status === 'completed' ? chalk_1.default.green :
|
|
63
|
+
task.status === 'failed' ? chalk_1.default.red : chalk_1.default.gray;
|
|
64
|
+
tui_1.tui.log(color(` ${icon} ${task.title}`));
|
|
65
|
+
}
|
|
66
|
+
tui_1.tui.log(chalk_1.default.cyan(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Display compact progress line (for status updates during execution)
|
|
70
|
+
*/
|
|
71
|
+
displayProgress(plan) {
|
|
72
|
+
const summary = this.planManager.getProgressSummary(plan);
|
|
73
|
+
const currentTask = plan.tasks.find(t => t.status === 'in_progress');
|
|
74
|
+
const progressBar = this.renderProgressBar(summary.percentage, 15);
|
|
75
|
+
let status = `${progressBar} ${summary.completed}/${summary.total}`;
|
|
76
|
+
if (currentTask) {
|
|
77
|
+
status += chalk_1.default.gray(` | ${currentTask.title.slice(0, 30)}`);
|
|
78
|
+
}
|
|
79
|
+
tui_1.tui.log(chalk_1.default.cyan(`📋 ${status}`));
|
|
80
|
+
}
|
|
81
|
+
// --- Methods for AI to call automatically ---
|
|
82
|
+
/**
|
|
83
|
+
* Start a task (mark as in_progress)
|
|
84
|
+
*/
|
|
85
|
+
async startTask(taskId) {
|
|
86
|
+
const plan = this.getCurrentPlan();
|
|
87
|
+
if (!plan)
|
|
88
|
+
return;
|
|
89
|
+
const task = plan.tasks.find(t => t.id === taskId);
|
|
90
|
+
if (task && task.status === 'pending') {
|
|
91
|
+
task.status = 'in_progress';
|
|
92
|
+
task.startedAt = new Date();
|
|
93
|
+
plan.status = 'in_progress';
|
|
94
|
+
await this.savePlan(plan);
|
|
95
|
+
this.displayProgress(plan);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Complete a task
|
|
100
|
+
*/
|
|
101
|
+
async completeTask(taskId) {
|
|
102
|
+
const plan = this.getCurrentPlan();
|
|
103
|
+
if (!plan)
|
|
104
|
+
return;
|
|
105
|
+
const task = plan.tasks.find(t => t.id === taskId);
|
|
106
|
+
if (task) {
|
|
107
|
+
task.status = 'completed';
|
|
108
|
+
task.completedAt = new Date();
|
|
109
|
+
await this.savePlan(plan);
|
|
110
|
+
// Check if all tasks completed
|
|
111
|
+
const allDone = plan.tasks.every(t => t.status === 'completed');
|
|
112
|
+
if (allDone) {
|
|
113
|
+
plan.status = 'completed';
|
|
114
|
+
await this.savePlan(plan);
|
|
115
|
+
tui_1.tui.log(chalk_1.default.green('\n🎉 所有任务已完成!'));
|
|
116
|
+
this.displayPlan(plan);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.displayProgress(plan);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Mark task as failed
|
|
125
|
+
*/
|
|
126
|
+
async failTask(taskId, error) {
|
|
127
|
+
const plan = this.getCurrentPlan();
|
|
128
|
+
if (!plan)
|
|
129
|
+
return;
|
|
130
|
+
const task = plan.tasks.find(t => t.id === taskId);
|
|
131
|
+
if (task) {
|
|
132
|
+
task.status = 'failed';
|
|
133
|
+
task.error = error;
|
|
134
|
+
await this.savePlan(plan);
|
|
135
|
+
tui_1.tui.log(chalk_1.default.red(`❌ 任务失败: ${task.title}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get next pending task
|
|
140
|
+
*/
|
|
141
|
+
getNextTask() {
|
|
142
|
+
const plan = this.getCurrentPlan();
|
|
143
|
+
if (!plan)
|
|
144
|
+
return null;
|
|
145
|
+
return plan.tasks.find(t => t.status === 'pending') || null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get current in-progress task
|
|
149
|
+
*/
|
|
150
|
+
getCurrentTask() {
|
|
151
|
+
const plan = this.getCurrentPlan();
|
|
152
|
+
if (!plan)
|
|
153
|
+
return null;
|
|
154
|
+
return plan.tasks.find(t => t.status === 'in_progress') || null;
|
|
155
|
+
}
|
|
156
|
+
// --- Helper methods ---
|
|
157
|
+
getStatusIcon(status) {
|
|
158
|
+
switch (status) {
|
|
159
|
+
case 'completed': return '✅';
|
|
160
|
+
case 'in_progress': return '🔄';
|
|
161
|
+
case 'failed': return '❌';
|
|
162
|
+
case 'skipped': return '⏭️';
|
|
163
|
+
default: return '⏸️';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
renderProgressBar(percentage, width) {
|
|
167
|
+
const filled = Math.round((percentage / 100) * width);
|
|
168
|
+
const empty = width - filled;
|
|
169
|
+
return chalk_1.default.green('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.PlanCommandHandler = PlanCommandHandler;
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PlanManager = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const uuid_1 = require("uuid");
|
|
10
|
+
/**
|
|
11
|
+
* Plan Manager - handles task plan creation, persistence, and management
|
|
12
|
+
*/
|
|
13
|
+
class PlanManager {
|
|
14
|
+
constructor(baseDir = '.mulby') {
|
|
15
|
+
this.plansDir = path_1.default.join(baseDir, 'plans');
|
|
16
|
+
this.sessionsDir = path_1.default.join(baseDir, 'sessions');
|
|
17
|
+
this.ensureDirectories();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ensure required directories exist
|
|
21
|
+
*/
|
|
22
|
+
ensureDirectories() {
|
|
23
|
+
fs_extra_1.default.ensureDirSync(this.plansDir);
|
|
24
|
+
fs_extra_1.default.ensureDirSync(path_1.default.join(this.plansDir, 'templates'));
|
|
25
|
+
fs_extra_1.default.ensureDirSync(this.sessionsDir);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a new task plan
|
|
29
|
+
*/
|
|
30
|
+
createPlan(goal, tasks) {
|
|
31
|
+
const plan = {
|
|
32
|
+
id: `plan-${Date.now()}-${(0, uuid_1.v4)().slice(0, 8)}`,
|
|
33
|
+
goal,
|
|
34
|
+
tasks: tasks.map((t, index) => ({
|
|
35
|
+
...t,
|
|
36
|
+
id: `task-${index + 1}`,
|
|
37
|
+
createdAt: new Date()
|
|
38
|
+
})),
|
|
39
|
+
totalEstimatedTokens: tasks.reduce((sum, t) => sum + (t.estimatedTokens || 0), 0),
|
|
40
|
+
createdAt: new Date(),
|
|
41
|
+
updatedAt: new Date(),
|
|
42
|
+
status: 'draft',
|
|
43
|
+
currentTaskIndex: 0
|
|
44
|
+
};
|
|
45
|
+
return plan;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Save plan to file
|
|
49
|
+
*/
|
|
50
|
+
async savePlan(plan, sessionId) {
|
|
51
|
+
const serializable = this.toSerializable(plan);
|
|
52
|
+
// Save to plans directory
|
|
53
|
+
const planPath = path_1.default.join(this.plansDir, `${plan.id}.json`);
|
|
54
|
+
await fs_extra_1.default.writeJson(planPath, serializable, { spaces: 2 });
|
|
55
|
+
// Also save to session directory if sessionId provided
|
|
56
|
+
if (sessionId) {
|
|
57
|
+
const sessionDir = path_1.default.join(this.sessionsDir, sessionId);
|
|
58
|
+
await fs_extra_1.default.ensureDir(sessionDir);
|
|
59
|
+
const sessionPlanPath = path_1.default.join(sessionDir, 'plan.json');
|
|
60
|
+
await fs_extra_1.default.writeJson(sessionPlanPath, serializable, { spaces: 2 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Load plan from file
|
|
65
|
+
*/
|
|
66
|
+
async loadPlan(planId) {
|
|
67
|
+
const planPath = path_1.default.join(this.plansDir, `${planId}.json`);
|
|
68
|
+
if (!await fs_extra_1.default.pathExists(planPath)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const serializable = await fs_extra_1.default.readJson(planPath);
|
|
72
|
+
return this.fromSerializable(serializable);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load plan from session
|
|
76
|
+
*/
|
|
77
|
+
async loadSessionPlan(sessionId) {
|
|
78
|
+
const sessionPlanPath = path_1.default.join(this.sessionsDir, sessionId, 'plan.json');
|
|
79
|
+
if (!await fs_extra_1.default.pathExists(sessionPlanPath)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const serializable = await fs_extra_1.default.readJson(sessionPlanPath);
|
|
83
|
+
return this.fromSerializable(serializable);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Update task status
|
|
87
|
+
*/
|
|
88
|
+
updateTaskStatus(plan, taskId, status, error) {
|
|
89
|
+
const task = plan.tasks.find(t => t.id === taskId);
|
|
90
|
+
if (!task) {
|
|
91
|
+
throw new Error(`Task ${taskId} not found`);
|
|
92
|
+
}
|
|
93
|
+
task.status = status;
|
|
94
|
+
task.error = error;
|
|
95
|
+
if (status === 'in_progress' && !task.startedAt) {
|
|
96
|
+
task.startedAt = new Date();
|
|
97
|
+
}
|
|
98
|
+
if (status === 'completed' || status === 'failed' || status === 'skipped') {
|
|
99
|
+
task.completedAt = new Date();
|
|
100
|
+
}
|
|
101
|
+
plan.updatedAt = new Date();
|
|
102
|
+
// Update plan status
|
|
103
|
+
this.updatePlanStatus(plan);
|
|
104
|
+
return plan;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Update plan status based on task statuses
|
|
108
|
+
*/
|
|
109
|
+
updatePlanStatus(plan) {
|
|
110
|
+
const allCompleted = plan.tasks.every(t => t.status === 'completed' || t.status === 'skipped');
|
|
111
|
+
const anyFailed = plan.tasks.some(t => t.status === 'failed');
|
|
112
|
+
const anyInProgress = plan.tasks.some(t => t.status === 'in_progress');
|
|
113
|
+
if (allCompleted) {
|
|
114
|
+
plan.status = 'completed';
|
|
115
|
+
}
|
|
116
|
+
else if (anyFailed) {
|
|
117
|
+
plan.status = 'failed';
|
|
118
|
+
}
|
|
119
|
+
else if (anyInProgress) {
|
|
120
|
+
plan.status = 'in_progress';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get next pending task
|
|
125
|
+
*/
|
|
126
|
+
getNextTask(plan) {
|
|
127
|
+
const completedTaskIds = new Set(plan.tasks
|
|
128
|
+
.filter(t => t.status === 'completed' || t.status === 'skipped')
|
|
129
|
+
.map(t => t.id));
|
|
130
|
+
// Find first pending task whose dependencies are satisfied
|
|
131
|
+
for (const task of plan.tasks) {
|
|
132
|
+
if (task.status !== 'pending')
|
|
133
|
+
continue;
|
|
134
|
+
const dependenciesSatisfied = task.dependencies.every(depId => completedTaskIds.has(depId));
|
|
135
|
+
if (dependenciesSatisfied) {
|
|
136
|
+
return task;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Calculate progress percentage
|
|
143
|
+
*/
|
|
144
|
+
calculateProgress(plan) {
|
|
145
|
+
const total = plan.tasks.length;
|
|
146
|
+
if (total === 0)
|
|
147
|
+
return 0;
|
|
148
|
+
const completed = plan.tasks.filter(t => t.status === 'completed').length;
|
|
149
|
+
const inProgress = plan.tasks.filter(t => t.status === 'in_progress').length;
|
|
150
|
+
// In-progress tasks count as 0.5
|
|
151
|
+
return ((completed + inProgress * 0.5) / total) * 100;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get progress summary
|
|
155
|
+
*/
|
|
156
|
+
getProgressSummary(plan) {
|
|
157
|
+
const total = plan.tasks.length;
|
|
158
|
+
const completed = plan.tasks.filter(t => t.status === 'completed').length;
|
|
159
|
+
const inProgress = plan.tasks.filter(t => t.status === 'in_progress').length;
|
|
160
|
+
const pending = plan.tasks.filter(t => t.status === 'pending').length;
|
|
161
|
+
const failed = plan.tasks.filter(t => t.status === 'failed').length;
|
|
162
|
+
const skipped = plan.tasks.filter(t => t.status === 'skipped').length;
|
|
163
|
+
return {
|
|
164
|
+
total,
|
|
165
|
+
completed,
|
|
166
|
+
inProgress,
|
|
167
|
+
pending,
|
|
168
|
+
failed,
|
|
169
|
+
skipped,
|
|
170
|
+
percentage: this.calculateProgress(plan)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Add task to plan
|
|
175
|
+
*/
|
|
176
|
+
addTask(plan, task, afterTaskId) {
|
|
177
|
+
const newTask = {
|
|
178
|
+
...task,
|
|
179
|
+
id: `task-${plan.tasks.length + 1}`,
|
|
180
|
+
createdAt: new Date()
|
|
181
|
+
};
|
|
182
|
+
if (afterTaskId) {
|
|
183
|
+
const index = plan.tasks.findIndex(t => t.id === afterTaskId);
|
|
184
|
+
if (index !== -1) {
|
|
185
|
+
plan.tasks.splice(index + 1, 0, newTask);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
plan.tasks.push(newTask);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
plan.tasks.push(newTask);
|
|
193
|
+
}
|
|
194
|
+
plan.updatedAt = new Date();
|
|
195
|
+
return plan;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Remove task from plan
|
|
199
|
+
*/
|
|
200
|
+
removeTask(plan, taskId) {
|
|
201
|
+
const index = plan.tasks.findIndex(t => t.id === taskId);
|
|
202
|
+
if (index === -1) {
|
|
203
|
+
throw new Error(`Task ${taskId} not found`);
|
|
204
|
+
}
|
|
205
|
+
// Check if other tasks depend on this task
|
|
206
|
+
const dependentTasks = plan.tasks.filter(t => t.dependencies.includes(taskId));
|
|
207
|
+
if (dependentTasks.length > 0) {
|
|
208
|
+
throw new Error(`Cannot remove task ${taskId}: ${dependentTasks.length} tasks depend on it`);
|
|
209
|
+
}
|
|
210
|
+
plan.tasks.splice(index, 1);
|
|
211
|
+
plan.updatedAt = new Date();
|
|
212
|
+
return plan;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* List all plans
|
|
216
|
+
*/
|
|
217
|
+
async listPlans() {
|
|
218
|
+
const files = await fs_extra_1.default.readdir(this.plansDir);
|
|
219
|
+
const planFiles = files.filter(f => f.endsWith('.json') && !f.startsWith('template-'));
|
|
220
|
+
const plans = [];
|
|
221
|
+
for (const file of planFiles) {
|
|
222
|
+
const planPath = path_1.default.join(this.plansDir, file);
|
|
223
|
+
const serializable = await fs_extra_1.default.readJson(planPath);
|
|
224
|
+
plans.push(this.fromSerializable(serializable));
|
|
225
|
+
}
|
|
226
|
+
return plans.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Delete plan
|
|
230
|
+
*/
|
|
231
|
+
async deletePlan(planId) {
|
|
232
|
+
const planPath = path_1.default.join(this.plansDir, `${planId}.json`);
|
|
233
|
+
if (await fs_extra_1.default.pathExists(planPath)) {
|
|
234
|
+
await fs_extra_1.default.remove(planPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ========== Template System ==========
|
|
238
|
+
/**
|
|
239
|
+
* Save plan as a template
|
|
240
|
+
*/
|
|
241
|
+
async saveTemplate(plan, templateName) {
|
|
242
|
+
const template = {
|
|
243
|
+
name: templateName,
|
|
244
|
+
description: plan.goal,
|
|
245
|
+
tasks: plan.tasks.map(t => ({
|
|
246
|
+
title: t.title,
|
|
247
|
+
description: t.description,
|
|
248
|
+
priority: t.priority,
|
|
249
|
+
dependencies: t.dependencies,
|
|
250
|
+
acceptanceCriteria: t.acceptanceCriteria,
|
|
251
|
+
files: t.files,
|
|
252
|
+
estimatedTokens: t.estimatedTokens
|
|
253
|
+
})),
|
|
254
|
+
createdAt: new Date().toISOString(),
|
|
255
|
+
tags: this.inferTags(plan)
|
|
256
|
+
};
|
|
257
|
+
const templatePath = path_1.default.join(this.plansDir, 'templates', `${templateName}.json`);
|
|
258
|
+
await fs_extra_1.default.writeJson(templatePath, template, { spaces: 2 });
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Load a template and create a new plan from it
|
|
262
|
+
*/
|
|
263
|
+
async loadTemplate(templateName, customGoal) {
|
|
264
|
+
const templatePath = path_1.default.join(this.plansDir, 'templates', `${templateName}.json`);
|
|
265
|
+
if (!await fs_extra_1.default.pathExists(templatePath)) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const template = await fs_extra_1.default.readJson(templatePath);
|
|
269
|
+
const plan = this.createPlan(customGoal || template.description, template.tasks.map(t => ({
|
|
270
|
+
...t,
|
|
271
|
+
status: 'pending'
|
|
272
|
+
})));
|
|
273
|
+
return plan;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* List all available templates
|
|
277
|
+
*/
|
|
278
|
+
async listTemplates() {
|
|
279
|
+
const templatesDir = path_1.default.join(this.plansDir, 'templates');
|
|
280
|
+
await fs_extra_1.default.ensureDir(templatesDir);
|
|
281
|
+
const files = await fs_extra_1.default.readdir(templatesDir);
|
|
282
|
+
const templates = [];
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
if (file.endsWith('.json')) {
|
|
285
|
+
const templatePath = path_1.default.join(templatesDir, file);
|
|
286
|
+
const template = await fs_extra_1.default.readJson(templatePath);
|
|
287
|
+
templates.push(template);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return templates.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Delete a template
|
|
294
|
+
*/
|
|
295
|
+
async deleteTemplate(templateName) {
|
|
296
|
+
const templatePath = path_1.default.join(this.plansDir, 'templates', `${templateName}.json`);
|
|
297
|
+
if (await fs_extra_1.default.pathExists(templatePath)) {
|
|
298
|
+
await fs_extra_1.default.remove(templatePath);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get built-in templates
|
|
305
|
+
*/
|
|
306
|
+
getBuiltInTemplates() {
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
name: 'feature',
|
|
310
|
+
description: 'Standard feature implementation workflow',
|
|
311
|
+
tasks: [
|
|
312
|
+
{
|
|
313
|
+
title: 'Analyze requirements',
|
|
314
|
+
description: 'Understand the feature requirements and identify affected components',
|
|
315
|
+
priority: 'high',
|
|
316
|
+
dependencies: [],
|
|
317
|
+
acceptanceCriteria: ['Requirements are clear', 'Affected files identified'],
|
|
318
|
+
files: []
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
title: 'Design solution',
|
|
322
|
+
description: 'Design the technical approach and data structures',
|
|
323
|
+
priority: 'high',
|
|
324
|
+
dependencies: ['task-1'],
|
|
325
|
+
acceptanceCriteria: ['Technical design documented', 'Edge cases considered'],
|
|
326
|
+
files: []
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
title: 'Implement core functionality',
|
|
330
|
+
description: 'Implement the main feature logic',
|
|
331
|
+
priority: 'high',
|
|
332
|
+
dependencies: ['task-2'],
|
|
333
|
+
acceptanceCriteria: ['Core functionality works', 'Code follows project conventions'],
|
|
334
|
+
files: []
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
title: 'Add tests',
|
|
338
|
+
description: 'Write unit and integration tests',
|
|
339
|
+
priority: 'medium',
|
|
340
|
+
dependencies: ['task-3'],
|
|
341
|
+
acceptanceCriteria: ['Tests cover main scenarios', 'All tests pass'],
|
|
342
|
+
files: []
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
title: 'Review and refine',
|
|
346
|
+
description: 'Code review, documentation, and final polish',
|
|
347
|
+
priority: 'low',
|
|
348
|
+
dependencies: ['task-4'],
|
|
349
|
+
acceptanceCriteria: ['Code is clean', 'Documentation updated'],
|
|
350
|
+
files: []
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
354
|
+
tags: ['feature', 'standard']
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'bugfix',
|
|
358
|
+
description: 'Bug fix workflow',
|
|
359
|
+
tasks: [
|
|
360
|
+
{
|
|
361
|
+
title: 'Reproduce the bug',
|
|
362
|
+
description: 'Understand and consistently reproduce the issue',
|
|
363
|
+
priority: 'high',
|
|
364
|
+
dependencies: [],
|
|
365
|
+
acceptanceCriteria: ['Bug can be reproduced', 'Steps documented'],
|
|
366
|
+
files: []
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
title: 'Identify root cause',
|
|
370
|
+
description: 'Debug and find the source of the problem',
|
|
371
|
+
priority: 'high',
|
|
372
|
+
dependencies: ['task-1'],
|
|
373
|
+
acceptanceCriteria: ['Root cause identified', 'Fix approach determined'],
|
|
374
|
+
files: []
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
title: 'Implement fix',
|
|
378
|
+
description: 'Fix the bug with minimal code changes',
|
|
379
|
+
priority: 'high',
|
|
380
|
+
dependencies: ['task-2'],
|
|
381
|
+
acceptanceCriteria: ['Bug is fixed', 'No regressions introduced'],
|
|
382
|
+
files: []
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
title: 'Add regression test',
|
|
386
|
+
description: 'Write a test to prevent the bug from recurring',
|
|
387
|
+
priority: 'medium',
|
|
388
|
+
dependencies: ['task-3'],
|
|
389
|
+
acceptanceCriteria: ['Test covers the bug scenario', 'Test passes'],
|
|
390
|
+
files: []
|
|
391
|
+
}
|
|
392
|
+
],
|
|
393
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
394
|
+
tags: ['bugfix', 'standard']
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'refactor',
|
|
398
|
+
description: 'Code refactoring workflow',
|
|
399
|
+
tasks: [
|
|
400
|
+
{
|
|
401
|
+
title: 'Analyze current code',
|
|
402
|
+
description: 'Understand the existing implementation and its issues',
|
|
403
|
+
priority: 'high',
|
|
404
|
+
dependencies: [],
|
|
405
|
+
acceptanceCriteria: ['Current structure understood', 'Pain points identified'],
|
|
406
|
+
files: []
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
title: 'Design new structure',
|
|
410
|
+
description: 'Plan the improved architecture',
|
|
411
|
+
priority: 'high',
|
|
412
|
+
dependencies: ['task-1'],
|
|
413
|
+
acceptanceCriteria: ['New design documented', 'Migration path clear'],
|
|
414
|
+
files: []
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
title: 'Ensure test coverage',
|
|
418
|
+
description: 'Add tests for existing functionality before refactoring',
|
|
419
|
+
priority: 'high',
|
|
420
|
+
dependencies: ['task-2'],
|
|
421
|
+
acceptanceCriteria: ['Critical paths tested', 'Tests pass'],
|
|
422
|
+
files: []
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
title: 'Refactor incrementally',
|
|
426
|
+
description: 'Apply changes in small, verifiable steps',
|
|
427
|
+
priority: 'high',
|
|
428
|
+
dependencies: ['task-3'],
|
|
429
|
+
acceptanceCriteria: ['Code improved', 'Tests still pass'],
|
|
430
|
+
files: []
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
title: 'Verify and clean up',
|
|
434
|
+
description: 'Final verification and removal of old code',
|
|
435
|
+
priority: 'medium',
|
|
436
|
+
dependencies: ['task-4'],
|
|
437
|
+
acceptanceCriteria: ['All tests pass', 'No dead code remains'],
|
|
438
|
+
files: []
|
|
439
|
+
}
|
|
440
|
+
],
|
|
441
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
442
|
+
tags: ['refactor', 'standard']
|
|
443
|
+
}
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Infer tags from plan content
|
|
448
|
+
*/
|
|
449
|
+
inferTags(plan) {
|
|
450
|
+
const tags = [];
|
|
451
|
+
const goalLower = plan.goal.toLowerCase();
|
|
452
|
+
if (goalLower.includes('fix') || goalLower.includes('bug')) {
|
|
453
|
+
tags.push('bugfix');
|
|
454
|
+
}
|
|
455
|
+
if (goalLower.includes('feature') || goalLower.includes('implement') || goalLower.includes('add')) {
|
|
456
|
+
tags.push('feature');
|
|
457
|
+
}
|
|
458
|
+
if (goalLower.includes('refactor') || goalLower.includes('improve') || goalLower.includes('optimize')) {
|
|
459
|
+
tags.push('refactor');
|
|
460
|
+
}
|
|
461
|
+
if (goalLower.includes('test')) {
|
|
462
|
+
tags.push('testing');
|
|
463
|
+
}
|
|
464
|
+
if (goalLower.includes('doc')) {
|
|
465
|
+
tags.push('documentation');
|
|
466
|
+
}
|
|
467
|
+
return tags;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Convert TaskPlan to serializable format
|
|
471
|
+
*/
|
|
472
|
+
toSerializable(plan) {
|
|
473
|
+
return {
|
|
474
|
+
...plan,
|
|
475
|
+
tasks: plan.tasks.map(t => ({
|
|
476
|
+
...t,
|
|
477
|
+
createdAt: t.createdAt.toISOString(),
|
|
478
|
+
startedAt: t.startedAt?.toISOString(),
|
|
479
|
+
completedAt: t.completedAt?.toISOString()
|
|
480
|
+
})),
|
|
481
|
+
createdAt: plan.createdAt.toISOString(),
|
|
482
|
+
updatedAt: plan.updatedAt.toISOString()
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Convert serializable format to TaskPlan
|
|
487
|
+
*/
|
|
488
|
+
fromSerializable(serializable) {
|
|
489
|
+
return {
|
|
490
|
+
...serializable,
|
|
491
|
+
tasks: serializable.tasks.map(t => ({
|
|
492
|
+
...t,
|
|
493
|
+
createdAt: new Date(t.createdAt),
|
|
494
|
+
startedAt: t.startedAt ? new Date(t.startedAt) : undefined,
|
|
495
|
+
completedAt: t.completedAt ? new Date(t.completedAt) : undefined
|
|
496
|
+
})),
|
|
497
|
+
createdAt: new Date(serializable.createdAt),
|
|
498
|
+
updatedAt: new Date(serializable.updatedAt)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
exports.PlanManager = PlanManager;
|