@allpepper/task-orchestrator 1.0.3 → 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/package.json +1 -1
- package/src/repos/base.ts +16 -0
- package/src/repos/features.ts +40 -5
- package/src/repos/projects.ts +66 -11
- package/src/tools/manage-container.ts +5 -2
package/package.json
CHANGED
package/src/repos/base.ts
CHANGED
|
@@ -149,3 +149,19 @@ export function countTasksByProject(projectId: string): TaskCounts {
|
|
|
149
149
|
}
|
|
150
150
|
return { total, byStatus };
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
export function countFeaturesByProject(projectId: string): number {
|
|
154
|
+
const row = queryOne<{ count: number }>(
|
|
155
|
+
'SELECT COUNT(*) as count FROM features WHERE project_id = ?',
|
|
156
|
+
[projectId]
|
|
157
|
+
);
|
|
158
|
+
return row?.count ?? 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function countTasksByFeatureId(featureId: string): number {
|
|
162
|
+
const row = queryOne<{ count: number }>(
|
|
163
|
+
'SELECT COUNT(*) as count FROM tasks WHERE feature_id = ?',
|
|
164
|
+
[featureId]
|
|
165
|
+
);
|
|
166
|
+
return row?.count ?? 0;
|
|
167
|
+
}
|
package/src/repos/features.ts
CHANGED
|
@@ -343,16 +343,51 @@ export function updateFeature(
|
|
|
343
343
|
/**
|
|
344
344
|
* Delete a feature
|
|
345
345
|
*/
|
|
346
|
-
export function deleteFeature(id: string): Result<boolean> {
|
|
346
|
+
export function deleteFeature(id: string, options?: { cascade?: boolean }): Result<boolean> {
|
|
347
347
|
try {
|
|
348
|
+
const cascade = options?.cascade ?? false;
|
|
349
|
+
|
|
350
|
+
// Check if feature exists
|
|
351
|
+
const exists = queryOne<{ id: string }>('SELECT id FROM features WHERE id = ?', [id]);
|
|
352
|
+
|
|
353
|
+
if (!exists) {
|
|
354
|
+
throw new NotFoundError('Feature', id);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Count children
|
|
358
|
+
const taskCounts = countTasksByFeature(id);
|
|
359
|
+
const taskCount = taskCounts.total;
|
|
360
|
+
|
|
361
|
+
// If children exist and no cascade, return error with counts
|
|
362
|
+
if (taskCount > 0 && !cascade) {
|
|
363
|
+
return err(
|
|
364
|
+
`Cannot delete feature: contains ${taskCount} task${taskCount > 1 ? 's' : ''}. Use cascade: true to delete all.`,
|
|
365
|
+
'HAS_CHILDREN'
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
348
369
|
const result = transaction(() => {
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
if (cascade) {
|
|
371
|
+
// Get all task IDs for this feature
|
|
372
|
+
const taskIds = queryAll<{ id: string }>(
|
|
373
|
+
'SELECT id FROM tasks WHERE feature_id = ?',
|
|
374
|
+
[id]
|
|
375
|
+
);
|
|
351
376
|
|
|
352
|
-
|
|
353
|
-
|
|
377
|
+
// Delete each task's dependencies, sections, and tags
|
|
378
|
+
for (const task of taskIds) {
|
|
379
|
+
execute('DELETE FROM dependencies WHERE from_task_id = ? OR to_task_id = ?', [task.id, task.id]);
|
|
380
|
+
execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.TASK, task.id]);
|
|
381
|
+
deleteTags(task.id, EntityType.TASK);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Delete all tasks for this feature
|
|
385
|
+
execute('DELETE FROM tasks WHERE feature_id = ?', [id]);
|
|
354
386
|
}
|
|
355
387
|
|
|
388
|
+
// Delete feature sections
|
|
389
|
+
execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.FEATURE, id]);
|
|
390
|
+
|
|
356
391
|
// Delete associated tags
|
|
357
392
|
deleteTags(id, EntityType.FEATURE);
|
|
358
393
|
|
package/src/repos/projects.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { queryOne, queryAll, execute, generateId, now, buildSearchVector, loadTags, saveTags, deleteTags, ok, err, buildPaginationClause, countTasksByProject, type TaskCounts } from './base';
|
|
1
|
+
import { queryOne, queryAll, execute, generateId, now, buildSearchVector, loadTags, saveTags, deleteTags, ok, err, buildPaginationClause, countTasksByProject, countFeaturesByProject, type TaskCounts } from './base';
|
|
2
2
|
import type { Project, Result } from '../domain/types';
|
|
3
|
-
import { ProjectStatus, NotFoundError, ConflictError, ValidationError } from '../domain/types';
|
|
3
|
+
import { ProjectStatus, NotFoundError, ConflictError, ValidationError, EntityType } from '../domain/types';
|
|
4
4
|
import { transaction } from '../db/client';
|
|
5
5
|
import { isValidTransition, getAllowedTransitions, isTerminalStatus } from '../services/status-validator';
|
|
6
6
|
|
|
@@ -221,20 +221,75 @@ export function updateProject(
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
export function deleteProject(id: string): Result<boolean> {
|
|
224
|
+
export function deleteProject(id: string, options?: { cascade?: boolean }): Result<boolean> {
|
|
225
225
|
try {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
const cascade = options?.cascade ?? false;
|
|
227
|
+
|
|
228
|
+
// Check if project exists
|
|
229
|
+
const existing = queryOne<ProjectRow>(
|
|
230
|
+
'SELECT id FROM projects WHERE id = ?',
|
|
231
|
+
[id]
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (!existing) {
|
|
235
|
+
throw new NotFoundError('Project', id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Count children
|
|
239
|
+
const featureCount = countFeaturesByProject(id);
|
|
240
|
+
const taskCounts = countTasksByProject(id);
|
|
241
|
+
const taskCount = taskCounts.total;
|
|
242
|
+
|
|
243
|
+
// If children exist and no cascade, return error with counts
|
|
244
|
+
if ((featureCount > 0 || taskCount > 0) && !cascade) {
|
|
245
|
+
const parts: string[] = [];
|
|
246
|
+
if (featureCount > 0) parts.push(`${featureCount} feature${featureCount > 1 ? 's' : ''}`);
|
|
247
|
+
if (taskCount > 0) parts.push(`${taskCount} task${taskCount > 1 ? 's' : ''}`);
|
|
248
|
+
return err(
|
|
249
|
+
`Cannot delete project: contains ${parts.join(' and ')}. Use cascade: true to delete all.`,
|
|
250
|
+
'HAS_CHILDREN'
|
|
230
251
|
);
|
|
252
|
+
}
|
|
231
253
|
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
const result = transaction(() => {
|
|
255
|
+
if (cascade) {
|
|
256
|
+
// Get all task IDs for this project
|
|
257
|
+
const taskIds = queryAll<{ id: string }>(
|
|
258
|
+
'SELECT id FROM tasks WHERE project_id = ?',
|
|
259
|
+
[id]
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Delete each task's dependencies, sections, and tags
|
|
263
|
+
for (const task of taskIds) {
|
|
264
|
+
execute('DELETE FROM dependencies WHERE from_task_id = ? OR to_task_id = ?', [task.id, task.id]);
|
|
265
|
+
execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.TASK, task.id]);
|
|
266
|
+
deleteTags(task.id, EntityType.TASK);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Delete all tasks for this project
|
|
270
|
+
execute('DELETE FROM tasks WHERE project_id = ?', [id]);
|
|
271
|
+
|
|
272
|
+
// Get all feature IDs for this project
|
|
273
|
+
const featureIds = queryAll<{ id: string }>(
|
|
274
|
+
'SELECT id FROM features WHERE project_id = ?',
|
|
275
|
+
[id]
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Delete each feature's sections and tags
|
|
279
|
+
for (const feature of featureIds) {
|
|
280
|
+
execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.FEATURE, feature.id]);
|
|
281
|
+
deleteTags(feature.id, EntityType.FEATURE);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Delete all features for this project
|
|
285
|
+
execute('DELETE FROM features WHERE project_id = ?', [id]);
|
|
234
286
|
}
|
|
235
287
|
|
|
236
|
-
// Delete
|
|
237
|
-
|
|
288
|
+
// Delete project sections
|
|
289
|
+
execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.PROJECT, id]);
|
|
290
|
+
|
|
291
|
+
// Delete project tags
|
|
292
|
+
deleteTags(id, EntityType.PROJECT);
|
|
238
293
|
|
|
239
294
|
// Delete project
|
|
240
295
|
execute('DELETE FROM projects WHERE id = ?', [id]);
|
|
@@ -52,6 +52,7 @@ export function registerManageContainerTool(server: McpServer): void {
|
|
|
52
52
|
complexity: z.number().int().optional(),
|
|
53
53
|
tags: z.string().optional(),
|
|
54
54
|
version: z.number().int().optional(),
|
|
55
|
+
cascade: z.boolean().optional().describe('For delete operation: if true, deletes all child entities (features, tasks) recursively. Required when deleting a project or feature that has children.'),
|
|
55
56
|
},
|
|
56
57
|
async (params) => {
|
|
57
58
|
try {
|
|
@@ -235,11 +236,13 @@ export function registerManageContainerTool(server: McpServer): void {
|
|
|
235
236
|
};
|
|
236
237
|
}
|
|
237
238
|
|
|
239
|
+
const cascadeOption = params.cascade ? { cascade: true } : undefined;
|
|
240
|
+
|
|
238
241
|
let result;
|
|
239
242
|
if (containerType === 'project') {
|
|
240
|
-
result = deleteProject(id);
|
|
243
|
+
result = deleteProject(id, cascadeOption);
|
|
241
244
|
} else if (containerType === 'feature') {
|
|
242
|
-
result = deleteFeature(id);
|
|
245
|
+
result = deleteFeature(id, cascadeOption);
|
|
243
246
|
} else {
|
|
244
247
|
result = deleteTask(id);
|
|
245
248
|
}
|