Compare commits
1 Commits
docs/auto-
...
ralph/chor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
111e69aaa9 |
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add Cursor IDE custom slash command support
|
|
||||||
|
|
||||||
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Change parent task back to "pending" when all subtasks are in "pending" state
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
## 0.27.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1248](https://github.com/eyaltoledano/claude-task-master/pull/1248) [`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix set-status for subtasks:
|
|
||||||
- Parent tasks are now set as `done` when subtasks are all `done`
|
|
||||||
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
|
|
||||||
|
|
||||||
## 0.27.1
|
## 0.27.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -93,57 +93,3 @@ task-master generate
|
|||||||
```
|
```
|
||||||
|
|
||||||
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
|
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
|
||||||
|
|
||||||
## Custom Slash Commands
|
|
||||||
|
|
||||||
<Check>
|
|
||||||
When you initialize Task Master with Cursor (`task-master init --cursor`), it automatically sets up custom slash commands for enhanced productivity.
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
Task Master automatically copies its complete set of slash commands to `.cursor/commands/` during project initialization. This provides you with convenient shortcuts for all Task Master operations directly within Cursor's interface.
|
|
||||||
|
|
||||||
### Available Slash Commands
|
|
||||||
|
|
||||||
The following slash commands become available in Cursor after initialization:
|
|
||||||
|
|
||||||
<Accordion title="Core Workflow Commands">
|
|
||||||
- `/tm-main` - Main Task Master help and overview
|
|
||||||
- `/tm-next-task` - Get the next available task to work on
|
|
||||||
- `/tm-show-task` - View detailed information about a specific task
|
|
||||||
- `/tm-list-tasks` - Display all tasks with various filtering options
|
|
||||||
- `/tm-project-status` - View overall project status
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Task Management Commands">
|
|
||||||
- `/tm-add-task` - Add a new task to your project
|
|
||||||
- `/tm-update-task` - Update an existing task
|
|
||||||
- `/tm-set-status-*` - Change task status (to-pending, to-in-progress, to-done, etc.)
|
|
||||||
- `/tm-remove-task` - Remove a task from your project
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Analysis and Planning Commands">
|
|
||||||
- `/tm-parse-prd` - Generate tasks from a PRD document
|
|
||||||
- `/tm-analyze-complexity` - Analyze task complexity
|
|
||||||
- `/tm-complexity-report` - View complexity analysis report
|
|
||||||
- `/tm-expand-task` - Break down tasks into subtasks
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Advanced Workflow Commands">
|
|
||||||
- `/tm-smart-workflow` - Intelligent task workflow management
|
|
||||||
- `/tm-auto-implement-tasks` - Automated task implementation pipeline
|
|
||||||
- `/tm-command-pipeline` - Execute multiple Task Master commands in sequence
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
### Using Slash Commands
|
|
||||||
|
|
||||||
Simply type the command name in Cursor's chat interface, and the AI will execute the corresponding Task Master operation:
|
|
||||||
|
|
||||||
```
|
|
||||||
/tm-next-task
|
|
||||||
```
|
|
||||||
|
|
||||||
The AI will understand the context and execute the appropriate `task-master` CLI command, providing you with the results directly in the chat.
|
|
||||||
|
|
||||||
### Command Cleanup
|
|
||||||
|
|
||||||
If you remove the Cursor profile from your project using `task-master remove --cursor`, the slash commands directory is automatically cleaned up to keep your project tidy.
|
|
||||||
|
|||||||
@@ -166,14 +166,7 @@ npx task-master init
|
|||||||
|
|
||||||
# Initialize project with specific rules
|
# Initialize project with specific rules
|
||||||
task-master init --rules cursor,windsurf,vscode
|
task-master init --rules cursor,windsurf,vscode
|
||||||
|
|
||||||
# Initialize project for Cursor (sets up rules and custom slash commands)
|
|
||||||
task-master init --cursor
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will prompt you for project details and set up a new project with the necessary files and structure.
|
This will prompt you for project details and set up a new project with the necessary files and structure.
|
||||||
|
|
||||||
<Note>
|
|
||||||
When using the `--cursor` flag, Task Master automatically copies custom slash commands to `.cursor/commands/` for enhanced Cursor IDE integration. See the [Cursor Setup Guide](/archive/cursor-setup) for more details about these commands.
|
|
||||||
</Note>
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## 0.25.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
|
|
||||||
- task-master-ai@0.27.2
|
|
||||||
|
|
||||||
## 0.25.2
|
## 0.25.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.3",
|
"version": "0.25.2",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.27.2"
|
"task-master-ai": "0.27.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
76
output.txt
76
output.txt
File diff suppressed because one or more lines are too long
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.2",
|
"version": "0.27.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.2",
|
"version": "0.27.1",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -357,9 +357,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.3",
|
"version": "0.25.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.27.2"
|
"task-master-ai": "0.27.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.2",
|
"version": "0.27.1",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,17 +3,7 @@
|
|||||||
* This file defines the contract for all storage implementations
|
* This file defines the contract for all storage implementations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
import type { Task, TaskMetadata } from '../types/index.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type for updateTaskStatus operations
|
|
||||||
*/
|
|
||||||
export interface UpdateStatusResult {
|
|
||||||
success: boolean;
|
|
||||||
oldStatus: TaskStatus;
|
|
||||||
newStatus: TaskStatus;
|
|
||||||
taskId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for storage operations on tasks
|
* Interface for storage operations on tasks
|
||||||
@@ -64,19 +54,6 @@ export interface IStorage {
|
|||||||
tag?: string
|
tag?: string
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Update task or subtask status by ID
|
|
||||||
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
|
|
||||||
* @param newStatus - New status to set
|
|
||||||
* @param tag - Optional tag context for the task
|
|
||||||
* @returns Promise that resolves to update result with old and new status
|
|
||||||
*/
|
|
||||||
updateTaskStatus(
|
|
||||||
taskId: string,
|
|
||||||
newStatus: TaskStatus,
|
|
||||||
tag?: string
|
|
||||||
): Promise<UpdateStatusResult>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a task by ID
|
* Delete a task by ID
|
||||||
* @param taskId - ID of the task to delete
|
* @param taskId - ID of the task to delete
|
||||||
@@ -214,11 +191,6 @@ export abstract class BaseStorage implements IStorage {
|
|||||||
updates: Partial<Task>,
|
updates: Partial<Task>,
|
||||||
tag?: string
|
tag?: string
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
abstract updateTaskStatus(
|
|
||||||
taskId: string,
|
|
||||||
newStatus: TaskStatus,
|
|
||||||
tag?: string
|
|
||||||
): Promise<UpdateStatusResult>;
|
|
||||||
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
|
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||||
abstract exists(tag?: string): Promise<boolean>;
|
abstract exists(tag?: string): Promise<boolean>;
|
||||||
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ export class TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update task status - delegates to storage layer which handles storage-specific logic
|
* Update task status
|
||||||
*/
|
*/
|
||||||
async updateTaskStatus(
|
async updateTaskStatus(
|
||||||
taskId: string | number,
|
taskId: string | number,
|
||||||
@@ -468,28 +468,49 @@ export class TaskService {
|
|||||||
|
|
||||||
// Use provided tag or get active tag
|
// Use provided tag or get active tag
|
||||||
const activeTag = tag || this.getActiveTag();
|
const activeTag = tag || this.getActiveTag();
|
||||||
|
|
||||||
const taskIdStr = String(taskId);
|
const taskIdStr = String(taskId);
|
||||||
|
|
||||||
try {
|
// TODO: For now, assume it's a regular task and just try to update directly
|
||||||
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
// In the future, we can add subtask support if needed
|
||||||
return await this.storage.updateTaskStatus(
|
if (taskIdStr.includes('.')) {
|
||||||
taskIdStr,
|
throw new TaskMasterError(
|
||||||
newStatus,
|
'Subtask status updates not yet supported in API storage',
|
||||||
activeTag
|
ERROR_CODES.NOT_IMPLEMENTED
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current task to get old status (simple, direct approach)
|
||||||
|
let currentTask: Task | null;
|
||||||
|
try {
|
||||||
|
// Try to get the task directly
|
||||||
|
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
`Failed to update task status for ${taskIdStr}`,
|
`Failed to load task ${taskIdStr}`,
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
ERROR_CODES.TASK_NOT_FOUND,
|
||||||
{
|
{ taskId: taskIdStr },
|
||||||
operation: 'updateTaskStatus',
|
|
||||||
resource: 'task',
|
|
||||||
taskId: taskIdStr,
|
|
||||||
newStatus,
|
|
||||||
tag: activeTag
|
|
||||||
},
|
|
||||||
error as Error
|
error as Error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentTask) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
`Task ${taskIdStr} not found`,
|
||||||
|
ERROR_CODES.TASK_NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStatus = currentTask.status;
|
||||||
|
|
||||||
|
// Simple, direct update - just change the status
|
||||||
|
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId: taskIdStr
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,9 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
IStorage,
|
IStorage,
|
||||||
StorageStats,
|
StorageStats
|
||||||
UpdateStatusResult
|
|
||||||
} from '../interfaces/storage.interface.js';
|
} from '../interfaces/storage.interface.js';
|
||||||
import type {
|
import type { Task, TaskMetadata, TaskTag } from '../types/index.js';
|
||||||
Task,
|
|
||||||
TaskMetadata,
|
|
||||||
TaskTag,
|
|
||||||
TaskStatus
|
|
||||||
} from '../types/index.js';
|
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
||||||
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
||||||
@@ -491,62 +485,6 @@ export class ApiStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update task or subtask status by ID - for API storage
|
|
||||||
*/
|
|
||||||
async updateTaskStatus(
|
|
||||||
taskId: string,
|
|
||||||
newStatus: TaskStatus,
|
|
||||||
tag?: string
|
|
||||||
): Promise<UpdateStatusResult> {
|
|
||||||
await this.ensureInitialized();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingTask = await this.retryOperation(() =>
|
|
||||||
this.repository.getTask(this.projectId, taskId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingTask) {
|
|
||||||
throw new Error(`Task ${taskId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldStatus = existingTask.status;
|
|
||||||
if (oldStatus === newStatus) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the task/subtask status
|
|
||||||
await this.retryOperation(() =>
|
|
||||||
this.repository.updateTask(this.projectId, taskId, {
|
|
||||||
status: newStatus,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: Parent status auto-adjustment is handled by the backend API service
|
|
||||||
// which has its own business logic for managing task relationships
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Failed to update task status via API',
|
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
|
||||||
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all available tags
|
* Get all available tags
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
* @fileoverview Refactored file-based storage implementation for Task Master
|
* @fileoverview Refactored file-based storage implementation for Task Master
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
|
import type { Task, TaskMetadata } from '../../types/index.js';
|
||||||
import type {
|
import type {
|
||||||
IStorage,
|
IStorage,
|
||||||
StorageStats,
|
StorageStats
|
||||||
UpdateStatusResult
|
|
||||||
} from '../../interfaces/storage.interface.js';
|
} from '../../interfaces/storage.interface.js';
|
||||||
import { FormatHandler } from './format-handler.js';
|
import { FormatHandler } from './format-handler.js';
|
||||||
import { FileOperations } from './file-operations.js';
|
import { FileOperations } from './file-operations.js';
|
||||||
@@ -282,159 +281,6 @@ export class FileStorage implements IStorage {
|
|||||||
await this.saveTasks(tasks, tag);
|
await this.saveTasks(tasks, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
|
|
||||||
*/
|
|
||||||
async updateTaskStatus(
|
|
||||||
taskId: string,
|
|
||||||
newStatus: TaskStatus,
|
|
||||||
tag?: string
|
|
||||||
): Promise<UpdateStatusResult> {
|
|
||||||
const tasks = await this.loadTasks(tag);
|
|
||||||
|
|
||||||
// Check if this is a subtask (contains a dot)
|
|
||||||
if (taskId.includes('.')) {
|
|
||||||
return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle regular task update
|
|
||||||
const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
|
|
||||||
|
|
||||||
if (taskIndex === -1) {
|
|
||||||
throw new Error(`Task ${taskId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldStatus = tasks[taskIndex].status;
|
|
||||||
if (oldStatus === newStatus) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId: String(taskId)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks[taskIndex] = {
|
|
||||||
...tasks[taskIndex],
|
|
||||||
status: newStatus,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.saveTasks(tasks, tag);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId: String(taskId)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update subtask status within file storage - handles parent status auto-adjustment
|
|
||||||
*/
|
|
||||||
private async updateSubtaskStatusInFile(
|
|
||||||
tasks: Task[],
|
|
||||||
subtaskId: string,
|
|
||||||
newStatus: TaskStatus,
|
|
||||||
tag?: string
|
|
||||||
): Promise<UpdateStatusResult> {
|
|
||||||
// Parse the subtask ID to get parent ID and subtask ID
|
|
||||||
const parts = subtaskId.split('.');
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [parentId, subIdRaw] = parts;
|
|
||||||
const subId = subIdRaw.trim();
|
|
||||||
if (!/^\d+$/.test(subId)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const subtaskNumericId = Number(subId);
|
|
||||||
|
|
||||||
// Find the parent task
|
|
||||||
const parentTaskIndex = tasks.findIndex(
|
|
||||||
(t) => String(t.id) === String(parentId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (parentTaskIndex === -1) {
|
|
||||||
throw new Error(`Parent task ${parentId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentTask = tasks[parentTaskIndex];
|
|
||||||
|
|
||||||
// Find the subtask within the parent task
|
|
||||||
const subtaskIndex = parentTask.subtasks.findIndex(
|
|
||||||
(st) => st.id === subtaskNumericId || String(st.id) === subId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (subtaskIndex === -1) {
|
|
||||||
throw new Error(
|
|
||||||
`Subtask ${subtaskId} not found in parent task ${parentId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending';
|
|
||||||
if (oldStatus === newStatus) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId: subtaskId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
|
|
||||||
// Update the subtask status
|
|
||||||
parentTask.subtasks[subtaskIndex] = {
|
|
||||||
...parentTask.subtasks[subtaskIndex],
|
|
||||||
status: newStatus,
|
|
||||||
updatedAt: now
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-adjust parent status based on subtask statuses
|
|
||||||
const subs = parentTask.subtasks;
|
|
||||||
let parentNewStatus = parentTask.status;
|
|
||||||
if (subs.length > 0) {
|
|
||||||
const norm = (s: any) => s.status || 'pending';
|
|
||||||
const isDoneLike = (s: any) => {
|
|
||||||
const st = norm(s);
|
|
||||||
return st === 'done' || st === 'completed';
|
|
||||||
};
|
|
||||||
const allDone = subs.every(isDoneLike);
|
|
||||||
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
|
|
||||||
const anyDone = subs.some(isDoneLike);
|
|
||||||
const allPending = subs.every((s) => norm(s) === 'pending');
|
|
||||||
|
|
||||||
if (allDone) parentNewStatus = 'done';
|
|
||||||
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
|
|
||||||
else if (allPending) parentNewStatus = 'pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always bump updatedAt; update status only if changed
|
|
||||||
tasks[parentTaskIndex] = {
|
|
||||||
...parentTask,
|
|
||||||
...(parentNewStatus !== parentTask.status
|
|
||||||
? { status: parentNewStatus }
|
|
||||||
: {}),
|
|
||||||
updatedAt: now
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.saveTasks(tasks, tag);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId: subtaskId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a task
|
* Delete a task
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,134 +1,5 @@
|
|||||||
// Cursor conversion profile for rule-transformer
|
// Cursor conversion profile for rule-transformer
|
||||||
import path from 'path';
|
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
|
||||||
import fs from 'fs';
|
|
||||||
import { log } from '../../scripts/modules/utils.js';
|
|
||||||
import { createProfile } from './base-profile.js';
|
|
||||||
|
|
||||||
// Helper copy; use cpSync when available, fallback to manual recursion
|
|
||||||
function copyRecursiveSync(src, dest) {
|
|
||||||
if (fs.cpSync) {
|
|
||||||
try {
|
|
||||||
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to copy ${src} to ${dest}: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const exists = fs.existsSync(src);
|
|
||||||
let stats = null;
|
|
||||||
let isDirectory = false;
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
try {
|
|
||||||
stats = fs.statSync(src);
|
|
||||||
isDirectory = stats.isDirectory();
|
|
||||||
} catch (err) {
|
|
||||||
// Handle TOCTOU race condition - treat as non-existent/not-a-directory
|
|
||||||
isDirectory = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectory) {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
||||||
for (const child of fs.readdirSync(src)) {
|
|
||||||
copyRecursiveSync(path.join(src, child), path.join(dest, child));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to copy directory ${src} to ${dest}: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// ensure parent exists for file copies
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to copy file ${src} to ${dest}: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to recursively remove directory
|
|
||||||
function removeDirectoryRecursive(dirPath) {
|
|
||||||
if (fs.existsSync(dirPath)) {
|
|
||||||
try {
|
|
||||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the Cursor profile directory from either project root, profile root, or rules dir
|
|
||||||
function resolveCursorProfileDir(baseDir) {
|
|
||||||
const base = path.basename(baseDir);
|
|
||||||
// If called with .../.cursor/rules -> return .../.cursor
|
|
||||||
if (base === 'rules' && path.basename(path.dirname(baseDir)) === '.cursor') {
|
|
||||||
return path.dirname(baseDir);
|
|
||||||
}
|
|
||||||
// If called with .../.cursor -> return as-is
|
|
||||||
if (base === '.cursor') return baseDir;
|
|
||||||
// Otherwise assume project root and append .cursor
|
|
||||||
return path.join(baseDir, '.cursor');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle functions for Cursor profile
|
|
||||||
function onAddRulesProfile(targetDir, assetsDir) {
|
|
||||||
// Copy commands directory recursively
|
|
||||||
const commandsSourceDir = path.join(assetsDir, 'claude', 'commands');
|
|
||||||
const profileDir = resolveCursorProfileDir(targetDir);
|
|
||||||
const commandsDestDir = path.join(profileDir, 'commands');
|
|
||||||
|
|
||||||
if (!fs.existsSync(commandsSourceDir)) {
|
|
||||||
log(
|
|
||||||
'warn',
|
|
||||||
`[Cursor] Source commands directory does not exist: ${commandsSourceDir}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure fresh state to avoid stale command files
|
|
||||||
try {
|
|
||||||
fs.rmSync(commandsDestDir, { recursive: true, force: true });
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
`[Cursor] Removed existing commands directory: ${commandsDestDir}`
|
|
||||||
);
|
|
||||||
} catch (deleteErr) {
|
|
||||||
// Directory might not exist, which is fine
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
`[Cursor] Commands directory did not exist or could not be removed: ${deleteErr.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRecursiveSync(commandsSourceDir, commandsDestDir);
|
|
||||||
log('debug', `[Cursor] Copied commands directory to ${commandsDestDir}`);
|
|
||||||
} catch (err) {
|
|
||||||
log(
|
|
||||||
'error',
|
|
||||||
`[Cursor] An error occurred during commands copy: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemoveRulesProfile(targetDir) {
|
|
||||||
// Remove .cursor/commands directory recursively
|
|
||||||
const profileDir = resolveCursorProfileDir(targetDir);
|
|
||||||
const commandsDir = path.join(profileDir, 'commands');
|
|
||||||
if (removeDirectoryRecursive(commandsDir)) {
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
`[Cursor] Ensured commands directory removed at ${commandsDir}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and export cursor profile using the base factory
|
// Create and export cursor profile using the base factory
|
||||||
export const cursorProfile = createProfile({
|
export const cursorProfile = createProfile({
|
||||||
@@ -137,10 +8,5 @@ export const cursorProfile = createProfile({
|
|||||||
url: 'cursor.so',
|
url: 'cursor.so',
|
||||||
docsUrl: 'docs.cursor.com',
|
docsUrl: 'docs.cursor.com',
|
||||||
targetExtension: '.mdc', // Cursor keeps .mdc extension
|
targetExtension: '.mdc', // Cursor keeps .mdc extension
|
||||||
supportsRulesSubdirectories: true,
|
supportsRulesSubdirectories: true
|
||||||
onAdd: onAddRulesProfile,
|
|
||||||
onRemove: onRemoveRulesProfile
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export lifecycle functions separately to avoid naming conflicts
|
|
||||||
export { onAddRulesProfile, onRemoveRulesProfile };
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
|
|||||||
if (typeof profile.onAddRulesProfile === 'function') {
|
if (typeof profile.onAddRulesProfile === 'function') {
|
||||||
try {
|
try {
|
||||||
const assetsDir = getAssetsDir();
|
const assetsDir = getAssetsDir();
|
||||||
profile.onAddRulesProfile(targetDir, assetsDir);
|
profile.onAddRulesProfile(projectRoot, assetsDir);
|
||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
|
`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
|
||||||
@@ -305,7 +305,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
|
|||||||
if (typeof profile.onPostConvertRulesProfile === 'function') {
|
if (typeof profile.onPostConvertRulesProfile === 'function') {
|
||||||
try {
|
try {
|
||||||
const assetsDir = getAssetsDir();
|
const assetsDir = getAssetsDir();
|
||||||
profile.onPostConvertRulesProfile(targetDir, assetsDir);
|
profile.onPostConvertRulesProfile(projectRoot, assetsDir);
|
||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
|
`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
|
||||||
@@ -347,7 +347,7 @@ export function removeProfileRules(projectRoot, profile) {
|
|||||||
// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
|
// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
|
||||||
if (typeof profile.onRemoveRulesProfile === 'function') {
|
if (typeof profile.onRemoveRulesProfile === 'function') {
|
||||||
try {
|
try {
|
||||||
profile.onRemoveRulesProfile(targetDir);
|
profile.onRemoveRulesProfile(projectRoot);
|
||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`
|
`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`
|
||||||
|
|||||||
@@ -54,33 +54,4 @@ describe('Cursor Profile Initialization Functionality', () => {
|
|||||||
);
|
);
|
||||||
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
|
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cursor.js has lifecycle functions for command copying', () => {
|
|
||||||
// Check that the source file contains our new lifecycle functions
|
|
||||||
expect(cursorProfileContent).toContain('function onAddRulesProfile');
|
|
||||||
expect(cursorProfileContent).toContain('function onRemoveRulesProfile');
|
|
||||||
expect(cursorProfileContent).toContain('copyRecursiveSync');
|
|
||||||
expect(cursorProfileContent).toContain('removeDirectoryRecursive');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cursor.js copies commands from claude/commands to .cursor/commands', () => {
|
|
||||||
// Check that the onAddRulesProfile function copies from the correct source
|
|
||||||
expect(cursorProfileContent).toContain(
|
|
||||||
"path.join(assetsDir, 'claude', 'commands')"
|
|
||||||
);
|
|
||||||
// Destination path is built via a resolver to handle both project root and rules dir
|
|
||||||
expect(cursorProfileContent).toContain('resolveCursorProfileDir(');
|
|
||||||
expect(cursorProfileContent).toMatch(
|
|
||||||
/path\.join\(\s*profileDir\s*,\s*['"]commands['"]\s*\)/
|
|
||||||
);
|
|
||||||
expect(cursorProfileContent).toContain(
|
|
||||||
'copyRecursiveSync(commandsSourceDir, commandsDestDir)'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check that lifecycle functions are properly registered with the profile
|
|
||||||
expect(cursorProfile.onAddRulesProfile).toBeDefined();
|
|
||||||
expect(cursorProfile.onRemoveRulesProfile).toBeDefined();
|
|
||||||
expect(typeof cursorProfile.onAddRulesProfile).toBe('function');
|
|
||||||
expect(typeof cursorProfile.onRemoveRulesProfile).toBe('function');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,37 +8,18 @@ jest.mock('child_process', () => ({
|
|||||||
execSync: jest.fn()
|
execSync: jest.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock console methods to avoid chalk issues
|
// Mock console methods
|
||||||
const mockLog = jest.fn();
|
jest.mock('console', () => ({
|
||||||
const originalConsole = global.console;
|
|
||||||
const mockConsole = {
|
|
||||||
log: jest.fn(),
|
log: jest.fn(),
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
warn: jest.fn(),
|
warn: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
clear: jest.fn()
|
clear: jest.fn()
|
||||||
};
|
|
||||||
global.console = mockConsole;
|
|
||||||
|
|
||||||
// Mock utils logger to avoid chalk dependency issues
|
|
||||||
await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
|
||||||
default: undefined,
|
|
||||||
log: mockLog,
|
|
||||||
isSilentMode: () => false
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Import the cursor profile after mocking
|
|
||||||
const { cursorProfile, onAddRulesProfile, onRemoveRulesProfile } = await import(
|
|
||||||
'../../../src/profiles/cursor.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Cursor Integration', () => {
|
describe('Cursor Integration', () => {
|
||||||
let tempDir;
|
let tempDir;
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
global.console = originalConsole;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
@@ -94,127 +75,4 @@ describe('Cursor Integration', () => {
|
|||||||
{ recursive: true }
|
{ recursive: true }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cursor profile has lifecycle functions for command copying', () => {
|
|
||||||
// Assert that the profile exports the lifecycle functions
|
|
||||||
expect(typeof onAddRulesProfile).toBe('function');
|
|
||||||
expect(typeof onRemoveRulesProfile).toBe('function');
|
|
||||||
expect(cursorProfile.onAddRulesProfile).toBe(onAddRulesProfile);
|
|
||||||
expect(cursorProfile.onRemoveRulesProfile).toBe(onRemoveRulesProfile);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('command copying lifecycle', () => {
|
|
||||||
let mockAssetsDir;
|
|
||||||
let mockTargetDir;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockAssetsDir = path.join(tempDir, 'assets');
|
|
||||||
mockTargetDir = path.join(tempDir, 'target');
|
|
||||||
|
|
||||||
// Reset all mocks
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
// Mock fs methods for the lifecycle functions
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
|
||||||
const pathStr = filePath.toString();
|
|
||||||
if (pathStr.includes('claude/commands')) {
|
|
||||||
return true; // Mock that source commands exist
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
|
||||||
jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['tm']);
|
|
||||||
jest
|
|
||||||
.spyOn(fs, 'statSync')
|
|
||||||
.mockImplementation(() => ({ isDirectory: () => true }));
|
|
||||||
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
|
|
||||||
jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onAddRulesProfile copies commands from assets to .cursor/commands', () => {
|
|
||||||
// Detect if cpSync exists and set up appropriate spy
|
|
||||||
if (fs.cpSync) {
|
|
||||||
const cpSpy = jest.spyOn(fs, 'cpSync').mockImplementation(() => {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
onAddRulesProfile(mockTargetDir, mockAssetsDir);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockAssetsDir, 'claude', 'commands')
|
|
||||||
);
|
|
||||||
expect(cpSpy).toHaveBeenCalledWith(
|
|
||||||
path.join(mockAssetsDir, 'claude', 'commands'),
|
|
||||||
path.join(mockTargetDir, '.cursor', 'commands'),
|
|
||||||
expect.objectContaining({ recursive: true, force: true })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Act
|
|
||||||
onAddRulesProfile(mockTargetDir, mockAssetsDir);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockAssetsDir, 'claude', 'commands')
|
|
||||||
);
|
|
||||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockTargetDir, '.cursor', 'commands'),
|
|
||||||
{ recursive: true }
|
|
||||||
);
|
|
||||||
expect(fs.copyFileSync).toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onAddRulesProfile handles missing source directory gracefully', () => {
|
|
||||||
// Arrange - mock source directory not existing
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
onAddRulesProfile(mockTargetDir, mockAssetsDir);
|
|
||||||
|
|
||||||
// Assert - should not attempt to copy anything
|
|
||||||
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
||||||
expect(fs.copyFileSync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onRemoveRulesProfile removes .cursor/commands directory', () => {
|
|
||||||
// Arrange - mock directory exists
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
onRemoveRulesProfile(mockTargetDir);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockTargetDir, '.cursor', 'commands'),
|
|
||||||
{ recursive: true, force: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onRemoveRulesProfile handles missing directory gracefully', () => {
|
|
||||||
// Arrange - mock directory doesn't exist
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
onRemoveRulesProfile(mockTargetDir);
|
|
||||||
|
|
||||||
// Assert - should still return true but not attempt removal
|
|
||||||
expect(fs.rmSync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onRemoveRulesProfile handles removal errors gracefully', () => {
|
|
||||||
// Arrange - mock directory exists but removal fails
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
|
|
||||||
jest.spyOn(fs, 'rmSync').mockImplementation(() => {
|
|
||||||
throw new Error('Permission denied');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act & Assert - should not throw
|
|
||||||
expect(() => onRemoveRulesProfile(mockTargetDir)).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user