@girardmedia/bootspring 1.1.0
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/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
package/cli/task.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Task Command
|
|
3
|
+
* Manage development tasks with lifecycle tracking
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @command task
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
const utils = require('../core/utils');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Task statuses with emoji indicators
|
|
16
|
+
*/
|
|
17
|
+
const TASK_STATUS = {
|
|
18
|
+
draft: { emoji: '📝', label: 'Draft', color: 'dim' },
|
|
19
|
+
ready: { emoji: '📋', label: 'Ready', color: 'cyan' },
|
|
20
|
+
'in-progress': { emoji: '🔨', label: 'In Progress', color: 'yellow' },
|
|
21
|
+
review: { emoji: '👀', label: 'Review', color: 'magenta' },
|
|
22
|
+
testing: { emoji: '🧪', label: 'Testing', color: 'blue' },
|
|
23
|
+
done: { emoji: '✅', label: 'Done', color: 'green' },
|
|
24
|
+
blocked: { emoji: '🚫', label: 'Blocked', color: 'red' }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Task priorities
|
|
29
|
+
*/
|
|
30
|
+
const TASK_PRIORITY = {
|
|
31
|
+
P0: { label: 'Critical', color: 'red' },
|
|
32
|
+
P1: { label: 'High', color: 'yellow' },
|
|
33
|
+
P2: { label: 'Medium', color: 'cyan' },
|
|
34
|
+
P3: { label: 'Low', color: 'dim' }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Task lifecycle phases (11-phase workflow)
|
|
39
|
+
*/
|
|
40
|
+
const LIFECYCLE_PHASES = [
|
|
41
|
+
{ id: 'context', name: 'Context Gathering', description: 'Understand the task fully' },
|
|
42
|
+
{ id: 'research', name: 'Research', description: 'Investigate options and approaches' },
|
|
43
|
+
{ id: 'design', name: 'Design', description: 'Plan the implementation' },
|
|
44
|
+
{ id: 'setup', name: 'Setup', description: 'Prepare environment and dependencies' },
|
|
45
|
+
{ id: 'implement', name: 'Implementation', description: 'Write the code' },
|
|
46
|
+
{ id: 'test', name: 'Testing', description: 'Write and run tests' },
|
|
47
|
+
{ id: 'review', name: 'Review', description: 'Code review and feedback' },
|
|
48
|
+
{ id: 'refine', name: 'Refinement', description: 'Address feedback and polish' },
|
|
49
|
+
{ id: 'document', name: 'Documentation', description: 'Update docs and comments' },
|
|
50
|
+
{ id: 'deploy', name: 'Deployment', description: 'Deploy to target environment' },
|
|
51
|
+
{ id: 'verify', name: 'Verification', description: 'Verify in production' }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get tasks file path
|
|
56
|
+
*/
|
|
57
|
+
function getTasksPath(projectRoot) {
|
|
58
|
+
return path.join(projectRoot, '.bootspring', 'tasks.json');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load tasks from file
|
|
63
|
+
*/
|
|
64
|
+
function loadTasks(projectRoot) {
|
|
65
|
+
const tasksPath = getTasksPath(projectRoot);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(tasksPath)) {
|
|
68
|
+
return { tasks: [], nextId: 1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(tasksPath, 'utf-8');
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { tasks: [], nextId: 1 };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Save tasks to file
|
|
81
|
+
*/
|
|
82
|
+
function saveTasks(projectRoot, data) {
|
|
83
|
+
const tasksPath = getTasksPath(projectRoot);
|
|
84
|
+
const dir = path.dirname(tasksPath);
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(dir)) {
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(tasksPath, JSON.stringify(data, null, 2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a new task
|
|
95
|
+
*/
|
|
96
|
+
function createTask(options = {}) {
|
|
97
|
+
const cfg = config.load();
|
|
98
|
+
const projectRoot = cfg._projectRoot;
|
|
99
|
+
const data = loadTasks(projectRoot);
|
|
100
|
+
|
|
101
|
+
const task = {
|
|
102
|
+
id: data.nextId,
|
|
103
|
+
title: options.title || 'Untitled Task',
|
|
104
|
+
description: options.description || '',
|
|
105
|
+
status: 'draft',
|
|
106
|
+
priority: options.priority || 'P2',
|
|
107
|
+
phase: 'context',
|
|
108
|
+
tags: options.tags || [],
|
|
109
|
+
assignee: options.assignee || null,
|
|
110
|
+
created: new Date().toISOString(),
|
|
111
|
+
updated: new Date().toISOString(),
|
|
112
|
+
blockedBy: [],
|
|
113
|
+
blocks: [],
|
|
114
|
+
notes: [],
|
|
115
|
+
timeTracking: {
|
|
116
|
+
estimated: options.estimate || null,
|
|
117
|
+
actual: 0,
|
|
118
|
+
sessions: []
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
data.tasks.push(task);
|
|
123
|
+
data.nextId++;
|
|
124
|
+
saveTasks(projectRoot, data);
|
|
125
|
+
|
|
126
|
+
utils.print.success(`Created task #${task.id}: ${task.title}`);
|
|
127
|
+
console.log(`${utils.COLORS.dim}Status: ${TASK_STATUS[task.status].label}${utils.COLORS.reset}`);
|
|
128
|
+
console.log(`${utils.COLORS.dim}Priority: ${TASK_PRIORITY[task.priority].label}${utils.COLORS.reset}`);
|
|
129
|
+
|
|
130
|
+
return task;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List tasks
|
|
135
|
+
*/
|
|
136
|
+
function listTasks(options = {}) {
|
|
137
|
+
const cfg = config.load();
|
|
138
|
+
const projectRoot = cfg._projectRoot;
|
|
139
|
+
const data = loadTasks(projectRoot);
|
|
140
|
+
|
|
141
|
+
let tasks = data.tasks;
|
|
142
|
+
|
|
143
|
+
// Filter by status
|
|
144
|
+
if (options.status) {
|
|
145
|
+
tasks = tasks.filter(t => t.status === options.status);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Filter by priority
|
|
149
|
+
if (options.priority) {
|
|
150
|
+
tasks = tasks.filter(t => t.priority === options.priority);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Filter by tag
|
|
154
|
+
if (options.tag) {
|
|
155
|
+
tasks = tasks.filter(t => t.tags.includes(options.tag));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Exclude done unless requested
|
|
159
|
+
if (!options.all && !options.status) {
|
|
160
|
+
tasks = tasks.filter(t => t.status !== 'done');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`
|
|
164
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Tasks${utils.COLORS.reset}
|
|
165
|
+
${utils.COLORS.dim}${tasks.length} task(s) found${utils.COLORS.reset}
|
|
166
|
+
`);
|
|
167
|
+
|
|
168
|
+
if (tasks.length === 0) {
|
|
169
|
+
utils.print.dim('No tasks found. Use "bootspring task create" to add one.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Group by status
|
|
174
|
+
const grouped = {};
|
|
175
|
+
for (const task of tasks) {
|
|
176
|
+
if (!grouped[task.status]) {
|
|
177
|
+
grouped[task.status] = [];
|
|
178
|
+
}
|
|
179
|
+
grouped[task.status].push(task);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const [status, statusTasks] of Object.entries(grouped)) {
|
|
183
|
+
const statusInfo = TASK_STATUS[status];
|
|
184
|
+
console.log(`${statusInfo.emoji} ${utils.COLORS.bold}${statusInfo.label}${utils.COLORS.reset}`);
|
|
185
|
+
|
|
186
|
+
for (const task of statusTasks) {
|
|
187
|
+
const priorityInfo = TASK_PRIORITY[task.priority];
|
|
188
|
+
const priorityColor = utils.COLORS[priorityInfo.color] || '';
|
|
189
|
+
|
|
190
|
+
console.log(` ${utils.COLORS.dim}#${task.id}${utils.COLORS.reset} ${task.title}`);
|
|
191
|
+
console.log(` ${priorityColor}${task.priority}${utils.COLORS.reset} ${utils.COLORS.dim}| Phase: ${task.phase}${utils.COLORS.reset}`);
|
|
192
|
+
|
|
193
|
+
if (task.tags.length > 0) {
|
|
194
|
+
console.log(` ${utils.COLORS.dim}Tags: ${task.tags.join(', ')}${utils.COLORS.reset}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Show task details
|
|
203
|
+
*/
|
|
204
|
+
function showTask(taskId) {
|
|
205
|
+
const cfg = config.load();
|
|
206
|
+
const projectRoot = cfg._projectRoot;
|
|
207
|
+
const data = loadTasks(projectRoot);
|
|
208
|
+
|
|
209
|
+
const task = data.tasks.find(t => t.id === parseInt(taskId));
|
|
210
|
+
|
|
211
|
+
if (!task) {
|
|
212
|
+
utils.print.error(`Task #${taskId} not found`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const statusInfo = TASK_STATUS[task.status];
|
|
217
|
+
const priorityInfo = TASK_PRIORITY[task.priority];
|
|
218
|
+
const phaseInfo = LIFECYCLE_PHASES.find(p => p.id === task.phase);
|
|
219
|
+
|
|
220
|
+
console.log(`
|
|
221
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Task #${task.id}${utils.COLORS.reset}
|
|
222
|
+
${utils.COLORS.bold}${task.title}${utils.COLORS.reset}
|
|
223
|
+
|
|
224
|
+
${utils.COLORS.bold}Status${utils.COLORS.reset}
|
|
225
|
+
${statusInfo.emoji} ${statusInfo.label}
|
|
226
|
+
Priority: ${utils.COLORS[priorityInfo.color]}${task.priority}${utils.COLORS.reset} (${priorityInfo.label})
|
|
227
|
+
Phase: ${task.phase} - ${phaseInfo?.name || 'Unknown'}
|
|
228
|
+
|
|
229
|
+
${utils.COLORS.bold}Description${utils.COLORS.reset}
|
|
230
|
+
${task.description || utils.COLORS.dim + 'No description' + utils.COLORS.reset}
|
|
231
|
+
|
|
232
|
+
${utils.COLORS.bold}Details${utils.COLORS.reset}
|
|
233
|
+
Created: ${utils.formatRelativeTime(new Date(task.created))}
|
|
234
|
+
Updated: ${utils.formatRelativeTime(new Date(task.updated))}
|
|
235
|
+
${task.assignee ? `Assignee: ${task.assignee}` : ''}
|
|
236
|
+
${task.tags.length > 0 ? `Tags: ${task.tags.join(', ')}` : ''}
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
// Show lifecycle progress
|
|
240
|
+
console.log(`${utils.COLORS.bold}Lifecycle Progress${utils.COLORS.reset}`);
|
|
241
|
+
const currentPhaseIndex = LIFECYCLE_PHASES.findIndex(p => p.id === task.phase);
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < LIFECYCLE_PHASES.length; i++) {
|
|
244
|
+
const phase = LIFECYCLE_PHASES[i];
|
|
245
|
+
let icon, color;
|
|
246
|
+
|
|
247
|
+
if (i < currentPhaseIndex) {
|
|
248
|
+
icon = '✓';
|
|
249
|
+
color = utils.COLORS.green;
|
|
250
|
+
} else if (i === currentPhaseIndex) {
|
|
251
|
+
icon = '●';
|
|
252
|
+
color = utils.COLORS.cyan;
|
|
253
|
+
} else {
|
|
254
|
+
icon = '○';
|
|
255
|
+
color = utils.COLORS.dim;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(` ${color}${icon} ${phase.name}${utils.COLORS.reset}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Show notes
|
|
262
|
+
if (task.notes.length > 0) {
|
|
263
|
+
console.log(`\n${utils.COLORS.bold}Notes${utils.COLORS.reset}`);
|
|
264
|
+
for (const note of task.notes.slice(-5)) {
|
|
265
|
+
console.log(` ${utils.COLORS.dim}${note.timestamp}${utils.COLORS.reset}`);
|
|
266
|
+
console.log(` ${note.content}`);
|
|
267
|
+
console.log();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Show blocking info
|
|
272
|
+
if (task.blockedBy.length > 0) {
|
|
273
|
+
console.log(`${utils.COLORS.red}Blocked by: ${task.blockedBy.map(id => `#${id}`).join(', ')}${utils.COLORS.reset}`);
|
|
274
|
+
}
|
|
275
|
+
if (task.blocks.length > 0) {
|
|
276
|
+
console.log(`${utils.COLORS.yellow}Blocks: ${task.blocks.map(id => `#${id}`).join(', ')}${utils.COLORS.reset}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Update task status
|
|
282
|
+
*/
|
|
283
|
+
function updateTaskStatus(taskId, newStatus) {
|
|
284
|
+
const cfg = config.load();
|
|
285
|
+
const projectRoot = cfg._projectRoot;
|
|
286
|
+
const data = loadTasks(projectRoot);
|
|
287
|
+
|
|
288
|
+
const task = data.tasks.find(t => t.id === parseInt(taskId));
|
|
289
|
+
|
|
290
|
+
if (!task) {
|
|
291
|
+
utils.print.error(`Task #${taskId} not found`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!TASK_STATUS[newStatus]) {
|
|
296
|
+
utils.print.error(`Invalid status: ${newStatus}`);
|
|
297
|
+
utils.print.dim(`Valid statuses: ${Object.keys(TASK_STATUS).join(', ')}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const oldStatus = task.status;
|
|
302
|
+
task.status = newStatus;
|
|
303
|
+
task.updated = new Date().toISOString();
|
|
304
|
+
|
|
305
|
+
saveTasks(projectRoot, data);
|
|
306
|
+
|
|
307
|
+
const oldInfo = TASK_STATUS[oldStatus];
|
|
308
|
+
const newInfo = TASK_STATUS[newStatus];
|
|
309
|
+
|
|
310
|
+
utils.print.success(`Task #${taskId}: ${oldInfo.emoji} ${oldInfo.label} → ${newInfo.emoji} ${newInfo.label}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Advance task to next phase
|
|
315
|
+
*/
|
|
316
|
+
function advancePhase(taskId) {
|
|
317
|
+
const cfg = config.load();
|
|
318
|
+
const projectRoot = cfg._projectRoot;
|
|
319
|
+
const data = loadTasks(projectRoot);
|
|
320
|
+
|
|
321
|
+
const task = data.tasks.find(t => t.id === parseInt(taskId));
|
|
322
|
+
|
|
323
|
+
if (!task) {
|
|
324
|
+
utils.print.error(`Task #${taskId} not found`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const currentIndex = LIFECYCLE_PHASES.findIndex(p => p.id === task.phase);
|
|
329
|
+
|
|
330
|
+
if (currentIndex === LIFECYCLE_PHASES.length - 1) {
|
|
331
|
+
utils.print.success(`Task #${taskId} is already at final phase`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const nextPhase = LIFECYCLE_PHASES[currentIndex + 1];
|
|
336
|
+
const oldPhase = task.phase;
|
|
337
|
+
task.phase = nextPhase.id;
|
|
338
|
+
task.updated = new Date().toISOString();
|
|
339
|
+
|
|
340
|
+
// Auto-update status based on phase
|
|
341
|
+
if (nextPhase.id === 'implement' && task.status === 'ready') {
|
|
342
|
+
task.status = 'in-progress';
|
|
343
|
+
} else if (nextPhase.id === 'review') {
|
|
344
|
+
task.status = 'review';
|
|
345
|
+
} else if (nextPhase.id === 'test') {
|
|
346
|
+
task.status = 'testing';
|
|
347
|
+
} else if (nextPhase.id === 'verify') {
|
|
348
|
+
task.status = 'done';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
saveTasks(projectRoot, data);
|
|
352
|
+
|
|
353
|
+
utils.print.success(`Task #${taskId}: ${oldPhase} → ${nextPhase.id}`);
|
|
354
|
+
console.log(`${utils.COLORS.dim}${nextPhase.name}: ${nextPhase.description}${utils.COLORS.reset}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Add note to task
|
|
359
|
+
*/
|
|
360
|
+
function addNote(taskId, noteContent) {
|
|
361
|
+
const cfg = config.load();
|
|
362
|
+
const projectRoot = cfg._projectRoot;
|
|
363
|
+
const data = loadTasks(projectRoot);
|
|
364
|
+
|
|
365
|
+
const task = data.tasks.find(t => t.id === parseInt(taskId));
|
|
366
|
+
|
|
367
|
+
if (!task) {
|
|
368
|
+
utils.print.error(`Task #${taskId} not found`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
task.notes.push({
|
|
373
|
+
timestamp: new Date().toISOString(),
|
|
374
|
+
content: noteContent
|
|
375
|
+
});
|
|
376
|
+
task.updated = new Date().toISOString();
|
|
377
|
+
|
|
378
|
+
saveTasks(projectRoot, data);
|
|
379
|
+
|
|
380
|
+
utils.print.success(`Added note to task #${taskId}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Set task priority
|
|
385
|
+
*/
|
|
386
|
+
function setPriority(taskId, priority) {
|
|
387
|
+
const cfg = config.load();
|
|
388
|
+
const projectRoot = cfg._projectRoot;
|
|
389
|
+
const data = loadTasks(projectRoot);
|
|
390
|
+
|
|
391
|
+
const task = data.tasks.find(t => t.id === parseInt(taskId));
|
|
392
|
+
|
|
393
|
+
if (!task) {
|
|
394
|
+
utils.print.error(`Task #${taskId} not found`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!TASK_PRIORITY[priority]) {
|
|
399
|
+
utils.print.error(`Invalid priority: ${priority}`);
|
|
400
|
+
utils.print.dim(`Valid priorities: ${Object.keys(TASK_PRIORITY).join(', ')}`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
task.priority = priority;
|
|
405
|
+
task.updated = new Date().toISOString();
|
|
406
|
+
|
|
407
|
+
saveTasks(projectRoot, data);
|
|
408
|
+
|
|
409
|
+
const priorityInfo = TASK_PRIORITY[priority];
|
|
410
|
+
utils.print.success(`Task #${taskId} priority set to ${priority} (${priorityInfo.label})`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Delete a task
|
|
415
|
+
*/
|
|
416
|
+
function deleteTask(taskId) {
|
|
417
|
+
const cfg = config.load();
|
|
418
|
+
const projectRoot = cfg._projectRoot;
|
|
419
|
+
const data = loadTasks(projectRoot);
|
|
420
|
+
|
|
421
|
+
const taskIndex = data.tasks.findIndex(t => t.id === parseInt(taskId));
|
|
422
|
+
|
|
423
|
+
if (taskIndex === -1) {
|
|
424
|
+
utils.print.error(`Task #${taskId} not found`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const task = data.tasks[taskIndex];
|
|
429
|
+
data.tasks.splice(taskIndex, 1);
|
|
430
|
+
|
|
431
|
+
saveTasks(projectRoot, data);
|
|
432
|
+
|
|
433
|
+
utils.print.success(`Deleted task #${taskId}: ${task.title}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Show lifecycle phases
|
|
438
|
+
*/
|
|
439
|
+
function showLifecycle() {
|
|
440
|
+
console.log(`
|
|
441
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Task Lifecycle Phases${utils.COLORS.reset}
|
|
442
|
+
${utils.COLORS.dim}11-phase development workflow${utils.COLORS.reset}
|
|
443
|
+
`);
|
|
444
|
+
|
|
445
|
+
for (let i = 0; i < LIFECYCLE_PHASES.length; i++) {
|
|
446
|
+
const phase = LIFECYCLE_PHASES[i];
|
|
447
|
+
console.log(` ${utils.COLORS.cyan}${i + 1}. ${phase.name}${utils.COLORS.reset}`);
|
|
448
|
+
console.log(` ${utils.COLORS.dim}${phase.description}${utils.COLORS.reset}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log(`
|
|
452
|
+
${utils.COLORS.bold}Usage${utils.COLORS.reset}
|
|
453
|
+
${utils.COLORS.dim}bootspring task advance <id>${utils.COLORS.reset} Move to next phase
|
|
454
|
+
${utils.COLORS.dim}bootspring task show <id>${utils.COLORS.reset} See current phase
|
|
455
|
+
`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Show task help
|
|
460
|
+
*/
|
|
461
|
+
function showHelp() {
|
|
462
|
+
console.log(`
|
|
463
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Task${utils.COLORS.reset}
|
|
464
|
+
${utils.COLORS.dim}Manage development tasks with lifecycle tracking${utils.COLORS.reset}
|
|
465
|
+
|
|
466
|
+
${utils.COLORS.bold}Usage:${utils.COLORS.reset}
|
|
467
|
+
bootspring task <command> [args] [options]
|
|
468
|
+
|
|
469
|
+
${utils.COLORS.bold}Commands:${utils.COLORS.reset}
|
|
470
|
+
${utils.COLORS.cyan}list${utils.COLORS.reset} List all tasks
|
|
471
|
+
${utils.COLORS.cyan}create${utils.COLORS.reset} Create a new task
|
|
472
|
+
${utils.COLORS.cyan}show${utils.COLORS.reset} <id> Show task details
|
|
473
|
+
${utils.COLORS.cyan}status${utils.COLORS.reset} <id> <s> Update task status
|
|
474
|
+
${utils.COLORS.cyan}advance${utils.COLORS.reset} <id> Advance to next phase
|
|
475
|
+
${utils.COLORS.cyan}priority${utils.COLORS.reset} <id> <p> Set task priority
|
|
476
|
+
${utils.COLORS.cyan}note${utils.COLORS.reset} <id> <text> Add note to task
|
|
477
|
+
${utils.COLORS.cyan}delete${utils.COLORS.reset} <id> Delete a task
|
|
478
|
+
${utils.COLORS.cyan}lifecycle${utils.COLORS.reset} Show lifecycle phases
|
|
479
|
+
|
|
480
|
+
${utils.COLORS.bold}Options:${utils.COLORS.reset}
|
|
481
|
+
--title <title> Task title
|
|
482
|
+
--description <desc> Task description
|
|
483
|
+
--priority <P0-P3> Task priority
|
|
484
|
+
--status <status> Filter by status
|
|
485
|
+
--tag <tag> Filter by tag
|
|
486
|
+
--all Include completed tasks
|
|
487
|
+
|
|
488
|
+
${utils.COLORS.bold}Statuses:${utils.COLORS.reset}
|
|
489
|
+
draft, ready, in-progress, review, testing, done, blocked
|
|
490
|
+
|
|
491
|
+
${utils.COLORS.bold}Examples:${utils.COLORS.reset}
|
|
492
|
+
bootspring task create --title "Add auth" --priority P1
|
|
493
|
+
bootspring task list --status in-progress
|
|
494
|
+
bootspring task status 1 in-progress
|
|
495
|
+
bootspring task advance 1
|
|
496
|
+
bootspring task note 1 "Completed API integration"
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Run task command
|
|
502
|
+
*/
|
|
503
|
+
async function run(args) {
|
|
504
|
+
const parsedArgs = utils.parseArgs(args);
|
|
505
|
+
const subcommand = parsedArgs._[0] || 'list';
|
|
506
|
+
const subargs = parsedArgs._.slice(1);
|
|
507
|
+
|
|
508
|
+
switch (subcommand) {
|
|
509
|
+
case 'list':
|
|
510
|
+
case 'ls':
|
|
511
|
+
listTasks({
|
|
512
|
+
status: parsedArgs.status,
|
|
513
|
+
priority: parsedArgs.priority,
|
|
514
|
+
tag: parsedArgs.tag,
|
|
515
|
+
all: parsedArgs.all
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
|
|
519
|
+
case 'create':
|
|
520
|
+
case 'new':
|
|
521
|
+
case 'add':
|
|
522
|
+
createTask({
|
|
523
|
+
title: parsedArgs.title || subargs.join(' '),
|
|
524
|
+
description: parsedArgs.description,
|
|
525
|
+
priority: parsedArgs.priority,
|
|
526
|
+
tags: parsedArgs.tags ? parsedArgs.tags.split(',') : [],
|
|
527
|
+
estimate: parsedArgs.estimate
|
|
528
|
+
});
|
|
529
|
+
break;
|
|
530
|
+
|
|
531
|
+
case 'show':
|
|
532
|
+
case 'view':
|
|
533
|
+
if (!subargs[0]) {
|
|
534
|
+
utils.print.error('Please specify a task ID');
|
|
535
|
+
utils.print.dim('Usage: bootspring task show <id>');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
showTask(subargs[0]);
|
|
539
|
+
break;
|
|
540
|
+
|
|
541
|
+
case 'status':
|
|
542
|
+
if (!subargs[0] || !subargs[1]) {
|
|
543
|
+
utils.print.error('Please specify task ID and new status');
|
|
544
|
+
utils.print.dim('Usage: bootspring task status <id> <status>');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
updateTaskStatus(subargs[0], subargs[1]);
|
|
548
|
+
break;
|
|
549
|
+
|
|
550
|
+
case 'advance':
|
|
551
|
+
case 'next':
|
|
552
|
+
if (!subargs[0]) {
|
|
553
|
+
utils.print.error('Please specify a task ID');
|
|
554
|
+
utils.print.dim('Usage: bootspring task advance <id>');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
advancePhase(subargs[0]);
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
case 'priority':
|
|
561
|
+
case 'prio':
|
|
562
|
+
if (!subargs[0] || !subargs[1]) {
|
|
563
|
+
utils.print.error('Please specify task ID and priority');
|
|
564
|
+
utils.print.dim('Usage: bootspring task priority <id> <P0-P3>');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
setPriority(subargs[0], subargs[1].toUpperCase());
|
|
568
|
+
break;
|
|
569
|
+
|
|
570
|
+
case 'note': {
|
|
571
|
+
if (!subargs[0]) {
|
|
572
|
+
utils.print.error('Please specify a task ID');
|
|
573
|
+
utils.print.dim('Usage: bootspring task note <id> <text>');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const noteContent = subargs.slice(1).join(' ') || parsedArgs.text;
|
|
577
|
+
if (!noteContent) {
|
|
578
|
+
utils.print.error('Please provide note content');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
addNote(subargs[0], noteContent);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case 'delete':
|
|
586
|
+
case 'rm':
|
|
587
|
+
case 'remove':
|
|
588
|
+
if (!subargs[0]) {
|
|
589
|
+
utils.print.error('Please specify a task ID');
|
|
590
|
+
utils.print.dim('Usage: bootspring task delete <id>');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
deleteTask(subargs[0]);
|
|
594
|
+
break;
|
|
595
|
+
|
|
596
|
+
case 'lifecycle':
|
|
597
|
+
case 'phases':
|
|
598
|
+
showLifecycle();
|
|
599
|
+
break;
|
|
600
|
+
|
|
601
|
+
case 'help':
|
|
602
|
+
case '-h':
|
|
603
|
+
case '--help':
|
|
604
|
+
showHelp();
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
default:
|
|
608
|
+
// Check if it's a task ID (number)
|
|
609
|
+
if (/^\d+$/.test(subcommand)) {
|
|
610
|
+
showTask(subcommand);
|
|
611
|
+
} else {
|
|
612
|
+
utils.print.error(`Unknown subcommand: ${subcommand}`);
|
|
613
|
+
showHelp();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
module.exports = {
|
|
619
|
+
run,
|
|
620
|
+
createTask,
|
|
621
|
+
listTasks,
|
|
622
|
+
showTask,
|
|
623
|
+
updateTaskStatus,
|
|
624
|
+
advancePhase,
|
|
625
|
+
TASK_STATUS,
|
|
626
|
+
TASK_PRIORITY,
|
|
627
|
+
LIFECYCLE_PHASES
|
|
628
|
+
};
|