Compare commits
1 Commits
task-maste
...
feat/add.g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e77047d08f |
@@ -8,11 +8,10 @@
|
|||||||
],
|
],
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [
|
"linked": [],
|
||||||
["task-master-ai", "@tm/cli", "@tm/core"]
|
|
||||||
],
|
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"docs"
|
"docs"
|
||||||
]
|
]
|
||||||
|
|||||||
27
.changeset/crazy-meals-hope.md
Normal file
27
.changeset/crazy-meals-hope.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add cross-tag task movement functionality for organizing tasks across different contexts.
|
||||||
|
|
||||||
|
This feature enables moving tasks between different tags (contexts) in your project, making it easier to organize work across different branches, environments, or project phases.
|
||||||
|
|
||||||
|
## CLI Usage Examples
|
||||||
|
|
||||||
|
Move a single task from one tag to another:
|
||||||
|
```bash
|
||||||
|
# Move task 5 from backlog tag to in-progress tag
|
||||||
|
task-master move --from=5 --from-tag=backlog --to-tag=feature-1
|
||||||
|
|
||||||
|
# Move task with its dependencies
|
||||||
|
task-master move --from=5 --from-tag=backlog --to-tag=feature-2 --with-dependencies
|
||||||
|
|
||||||
|
# Move task without checking dependencies
|
||||||
|
task-master move --from=5 --from-tag=backlog --to-tag=bug-3 --ignore-dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Move multiple tasks at once:
|
||||||
|
```bash
|
||||||
|
# Move multiple tasks between tags
|
||||||
|
task-master move --from=5,6,7 --from-tag=backlog --to-tag=bug-4 --with-dependencies
|
||||||
|
```
|
||||||
12
.changeset/cute-files-pay.md
Normal file
12
.changeset/cute-files-pay.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add compact mode --compact / -c flag to the `tm list` CLI command
|
||||||
|
|
||||||
|
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
||||||
|
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
||||||
|
- Color-coded status, priority, and dependencies
|
||||||
|
- Smart title truncation and dependency abbreviation
|
||||||
|
- Subtask support with indentation
|
||||||
|
- Full backward compatibility with existing list options
|
||||||
5
.changeset/light-crabs-warn.md
Normal file
5
.changeset/light-crabs-warn.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"extension": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Display current task ID on task details page
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Test out the RC
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@tm/cli": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
testing this stuff out to see how the release candidate works with monorepo
|
|
||||||
7
.changeset/rude-moments-search.md
Normal file
7
.changeset/rude-moments-search.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix `add-tag --from-branch` command error where `projectRoot` was not properly referenced
|
||||||
|
|
||||||
|
The command was failing with "projectRoot is not defined" error because the code was directly referencing `projectRoot` instead of `context.projectRoot` in the git repository checks. This fix corrects the variable references to use the proper context object.
|
||||||
5
.changeset/slow-readers-deny.md
Normal file
5
.changeset/slow-readers-deny.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for ollama `gpt-oss:20b` and `gpt-oss:120b`
|
||||||
5
.changeset/wet-seas-float.md
Normal file
5
.changeset/wet-seas-float.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"extension": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Added a Start Build button to the VSCODE Task Properties Right Panel
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh api:*), Bash(gh issue comment:*)
|
|
||||||
description: Find duplicate GitHub issues
|
|
||||||
---
|
|
||||||
|
|
||||||
Find up to 3 likely duplicate issues for a given GitHub issue.
|
|
||||||
|
|
||||||
To do this, follow these steps precisely:
|
|
||||||
|
|
||||||
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
|
|
||||||
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
|
|
||||||
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
|
|
||||||
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
|
|
||||||
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
|
|
||||||
|
|
||||||
Notes (be sure to tell this to your agents, too):
|
|
||||||
|
|
||||||
- Use `gh` to interact with Github, rather than web fetch
|
|
||||||
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
|
|
||||||
- Make a todo list first
|
|
||||||
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Found 3 possible duplicate issues:
|
|
||||||
|
|
||||||
1. <link to issue>
|
|
||||||
2. <link to issue>
|
|
||||||
3. <link to issue>
|
|
||||||
|
|
||||||
This issue will be automatically closed as a duplicate in 3 days.
|
|
||||||
|
|
||||||
- If your issue is a duplicate, please close it and 👍 the existing issue instead
|
|
||||||
- To prevent auto-closure, add a comment or 👎 this comment
|
|
||||||
|
|
||||||
🤖 Generated with \[Task Master Bot\]
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
Clear all subtasks from all tasks globally.
|
|
||||||
|
|
||||||
## Global Subtask Clearing
|
|
||||||
|
|
||||||
Remove all subtasks across the entire project. Use with extreme caution.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
task-master clear-subtasks --all
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-Clear Analysis
|
|
||||||
|
|
||||||
1. **Project-Wide Summary**
|
|
||||||
```
|
|
||||||
Global Subtask Summary
|
|
||||||
━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total parent tasks: 12
|
|
||||||
Total subtasks: 47
|
|
||||||
- Completed: 15
|
|
||||||
- In-progress: 8
|
|
||||||
- Pending: 24
|
|
||||||
|
|
||||||
Work at risk: ~120 hours
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Critical Warnings**
|
|
||||||
- In-progress subtasks that will lose work
|
|
||||||
- Completed subtasks with valuable history
|
|
||||||
- Complex dependency chains
|
|
||||||
- Integration test results
|
|
||||||
|
|
||||||
## Double Confirmation
|
|
||||||
|
|
||||||
```
|
|
||||||
⚠️ DESTRUCTIVE OPERATION WARNING ⚠️
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
This will remove ALL 47 subtasks from your project
|
|
||||||
Including 8 in-progress and 15 completed subtasks
|
|
||||||
|
|
||||||
This action CANNOT be undone
|
|
||||||
|
|
||||||
Type 'CLEAR ALL SUBTASKS' to confirm:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smart Safeguards
|
|
||||||
|
|
||||||
- Require explicit confirmation phrase
|
|
||||||
- Create automatic backup
|
|
||||||
- Log all removed data
|
|
||||||
- Option to export first
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
Valid reasons for global clear:
|
|
||||||
- Project restructuring
|
|
||||||
- Major pivot in approach
|
|
||||||
- Starting fresh breakdown
|
|
||||||
- Switching to different task organization
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Full project analysis
|
|
||||||
2. Create backup file
|
|
||||||
3. Show detailed impact
|
|
||||||
4. Require confirmation
|
|
||||||
5. Execute removal
|
|
||||||
6. Generate summary report
|
|
||||||
|
|
||||||
## Alternative Suggestions
|
|
||||||
|
|
||||||
Before clearing all:
|
|
||||||
- Export subtasks to file
|
|
||||||
- Clear only pending subtasks
|
|
||||||
- Clear by task category
|
|
||||||
- Archive instead of delete
|
|
||||||
|
|
||||||
## Post-Clear Report
|
|
||||||
|
|
||||||
```
|
|
||||||
Global Subtask Clear Complete
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Removed: 47 subtasks from 12 tasks
|
|
||||||
Backup saved: .taskmaster/backup/subtasks-20240115.json
|
|
||||||
Parent tasks updated: 12
|
|
||||||
Time estimates adjusted: Yes
|
|
||||||
|
|
||||||
Next steps:
|
|
||||||
- Review updated task list
|
|
||||||
- Re-expand complex tasks as needed
|
|
||||||
- Check project timeline
|
|
||||||
```
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
Clear all subtasks from a specific task.
|
|
||||||
|
|
||||||
Arguments: $ARGUMENTS (task ID)
|
|
||||||
|
|
||||||
Remove all subtasks from a parent task at once.
|
|
||||||
|
|
||||||
## Clearing Subtasks
|
|
||||||
|
|
||||||
Bulk removal of all subtasks from a parent task.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
task-master clear-subtasks --id=<task-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-Clear Analysis
|
|
||||||
|
|
||||||
1. **Subtask Summary**
|
|
||||||
- Number of subtasks
|
|
||||||
- Completion status of each
|
|
||||||
- Work already done
|
|
||||||
- Dependencies affected
|
|
||||||
|
|
||||||
2. **Impact Assessment**
|
|
||||||
- Data that will be lost
|
|
||||||
- Dependencies to be removed
|
|
||||||
- Effect on project timeline
|
|
||||||
- Parent task implications
|
|
||||||
|
|
||||||
## Confirmation Required
|
|
||||||
|
|
||||||
```
|
|
||||||
Clear Subtasks Confirmation
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Parent Task: #5 "Implement user authentication"
|
|
||||||
Subtasks to remove: 4
|
|
||||||
- #5.1 "Setup auth framework" (done)
|
|
||||||
- #5.2 "Create login form" (in-progress)
|
|
||||||
- #5.3 "Add validation" (pending)
|
|
||||||
- #5.4 "Write tests" (pending)
|
|
||||||
|
|
||||||
⚠️ This will permanently delete all subtask data
|
|
||||||
Continue? (y/n)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smart Features
|
|
||||||
|
|
||||||
- Option to convert to standalone tasks
|
|
||||||
- Backup task data before clearing
|
|
||||||
- Preserve completed work history
|
|
||||||
- Update parent task appropriately
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. List all subtasks for confirmation
|
|
||||||
2. Check for in-progress work
|
|
||||||
3. Remove all subtasks
|
|
||||||
4. Update parent task
|
|
||||||
5. Clean up dependencies
|
|
||||||
|
|
||||||
## Alternative Options
|
|
||||||
|
|
||||||
Suggest alternatives:
|
|
||||||
- Convert important subtasks to tasks
|
|
||||||
- Keep completed subtasks
|
|
||||||
- Archive instead of delete
|
|
||||||
- Export subtask data first
|
|
||||||
|
|
||||||
## Post-Clear
|
|
||||||
|
|
||||||
- Show updated parent task
|
|
||||||
- Recalculate time estimates
|
|
||||||
- Update task complexity
|
|
||||||
- Suggest next steps
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```
|
|
||||||
/project:tm/clear-subtasks 5
|
|
||||||
→ Found 4 subtasks to remove
|
|
||||||
→ Warning: Subtask #5.2 is in-progress
|
|
||||||
→ Cleared all subtasks from task #5
|
|
||||||
→ Updated parent task estimates
|
|
||||||
→ Suggestion: Consider re-expanding with better breakdown
|
|
||||||
```
|
|
||||||
259
.github/scripts/auto-close-duplicates.mjs
vendored
259
.github/scripts/auto-close-duplicates.mjs
vendored
@@ -1,259 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
async function githubRequest(endpoint, token, method = 'GET', body) {
|
|
||||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent': 'auto-close-duplicates-script',
|
|
||||||
...(body && { 'Content-Type': 'application/json' })
|
|
||||||
},
|
|
||||||
...(body && { body: JSON.stringify(body) })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`GitHub API request failed: ${response.status} ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractDuplicateIssueNumber(commentBody) {
|
|
||||||
const match = commentBody.match(/#(\d+)/);
|
|
||||||
return match ? parseInt(match[1], 10) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function closeIssueAsDuplicate(
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issueNumber,
|
|
||||||
duplicateOfNumber,
|
|
||||||
token
|
|
||||||
) {
|
|
||||||
await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
||||||
token,
|
|
||||||
'PATCH',
|
|
||||||
{
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned',
|
|
||||||
labels: ['duplicate']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
||||||
token,
|
|
||||||
'POST',
|
|
||||||
{
|
|
||||||
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
|
|
||||||
|
|
||||||
If this is incorrect, please re-open this issue or create a new one.
|
|
||||||
|
|
||||||
🤖 Generated with [Task Master Bot]`
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function autoCloseDuplicates() {
|
|
||||||
console.log('[DEBUG] Starting auto-close duplicates script');
|
|
||||||
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
if (!token) {
|
|
||||||
throw new Error('GITHUB_TOKEN environment variable is required');
|
|
||||||
}
|
|
||||||
console.log('[DEBUG] GitHub token found');
|
|
||||||
|
|
||||||
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano';
|
|
||||||
const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master';
|
|
||||||
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
|
|
||||||
|
|
||||||
const threeDaysAgo = new Date();
|
|
||||||
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('[DEBUG] Fetching open issues created more than 3 days ago...');
|
|
||||||
const allIssues = [];
|
|
||||||
let page = 1;
|
|
||||||
const perPage = 100;
|
|
||||||
|
|
||||||
const MAX_PAGES = 50; // Increase limit for larger repos
|
|
||||||
let foundRecentIssue = false;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const pageIssues = await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pageIssues.length === 0) break;
|
|
||||||
|
|
||||||
// Filter for issues created more than 3 days ago
|
|
||||||
const oldEnoughIssues = pageIssues.filter(
|
|
||||||
(issue) => new Date(issue.created_at) <= threeDaysAgo
|
|
||||||
);
|
|
||||||
|
|
||||||
allIssues.push(...oldEnoughIssues);
|
|
||||||
|
|
||||||
// If all issues on this page are newer than 3 days, we can stop
|
|
||||||
if (oldEnoughIssues.length === 0 && page === 1) {
|
|
||||||
foundRecentIssue = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found some old issues but not all, continue to next page
|
|
||||||
// as there might be more old issues
|
|
||||||
page++;
|
|
||||||
|
|
||||||
// Safety limit to avoid infinite loops
|
|
||||||
if (page > MAX_PAGES) {
|
|
||||||
console.log(`[WARNING] Reached maximum page limit of ${MAX_PAGES}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const issues = allIssues;
|
|
||||||
console.log(`[DEBUG] Found ${issues.length} open issues`);
|
|
||||||
|
|
||||||
let processedCount = 0;
|
|
||||||
let candidateCount = 0;
|
|
||||||
|
|
||||||
for (const issue of issues) {
|
|
||||||
processedCount++;
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
|
|
||||||
const comments = await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
|
|
||||||
);
|
|
||||||
|
|
||||||
const dupeComments = comments.filter(
|
|
||||||
(comment) =>
|
|
||||||
comment.body.includes('Found') &&
|
|
||||||
comment.body.includes('possible duplicate') &&
|
|
||||||
comment.user.type === 'Bot'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dupeComments.length === 0) {
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastDupeComment = dupeComments[dupeComments.length - 1];
|
|
||||||
const dupeCommentDate = new Date(lastDupeComment.created_at);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${
|
|
||||||
issue.number
|
|
||||||
} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dupeCommentDate > threeDaysAgo) {
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${
|
|
||||||
issue.number
|
|
||||||
} - duplicate comment is old enough (${Math.floor(
|
|
||||||
(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
|
|
||||||
)} days)`
|
|
||||||
);
|
|
||||||
|
|
||||||
const commentsAfterDupe = comments.filter(
|
|
||||||
(comment) => new Date(comment.created_at) > dupeCommentDate
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (commentsAfterDupe.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
|
|
||||||
);
|
|
||||||
const reactions = await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
|
|
||||||
);
|
|
||||||
|
|
||||||
const authorThumbsDown = reactions.some(
|
|
||||||
(reaction) =>
|
|
||||||
reaction.user.id === issue.user.id && reaction.content === '-1'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (authorThumbsDown) {
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateIssueNumber = extractDuplicateIssueNumber(
|
|
||||||
lastDupeComment.body
|
|
||||||
);
|
|
||||||
if (!duplicateIssueNumber) {
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidateCount++;
|
|
||||||
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(
|
|
||||||
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
|
|
||||||
);
|
|
||||||
await closeIssueAsDuplicate(
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue.number,
|
|
||||||
duplicateIssueNumber,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
autoCloseDuplicates().catch(console.error);
|
|
||||||
178
.github/scripts/backfill-duplicate-comments.mjs
vendored
178
.github/scripts/backfill-duplicate-comments.mjs
vendored
@@ -1,178 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
async function githubRequest(endpoint, token, method = 'GET', body) {
|
|
||||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent': 'backfill-duplicate-comments-script',
|
|
||||||
...(body && { 'Content-Type': 'application/json' })
|
|
||||||
},
|
|
||||||
...(body && { body: JSON.stringify(body) })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`GitHub API request failed: ${response.status} ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function triggerDedupeWorkflow(
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issueNumber,
|
|
||||||
token,
|
|
||||||
dryRun = true
|
|
||||||
) {
|
|
||||||
if (dryRun) {
|
|
||||||
console.log(
|
|
||||||
`[DRY RUN] Would trigger dedupe workflow for issue #${issueNumber}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/actions/workflows/claude-dedupe-issues.yml/dispatches`,
|
|
||||||
token,
|
|
||||||
'POST',
|
|
||||||
{
|
|
||||||
ref: 'main',
|
|
||||||
inputs: {
|
|
||||||
issue_number: issueNumber.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function backfillDuplicateComments() {
|
|
||||||
console.log('[DEBUG] Starting backfill duplicate comments script');
|
|
||||||
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
if (!token) {
|
|
||||||
throw new Error(`GITHUB_TOKEN environment variable is required
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
node .github/scripts/backfill-duplicate-comments.mjs
|
|
||||||
|
|
||||||
Environment Variables:
|
|
||||||
GITHUB_TOKEN - GitHub personal access token with repo and actions permissions (required)
|
|
||||||
DRY_RUN - Set to "false" to actually trigger workflows (default: true for safety)
|
|
||||||
DAYS_BACK - How many days back to look for old issues (default: 90)`);
|
|
||||||
}
|
|
||||||
console.log('[DEBUG] GitHub token found');
|
|
||||||
|
|
||||||
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano';
|
|
||||||
const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master';
|
|
||||||
const dryRun = process.env.DRY_RUN !== 'false';
|
|
||||||
const daysBack = parseInt(process.env.DAYS_BACK || '90', 10);
|
|
||||||
|
|
||||||
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
|
|
||||||
console.log(`[DEBUG] Dry run mode: ${dryRun}`);
|
|
||||||
console.log(`[DEBUG] Looking back ${daysBack} days`);
|
|
||||||
|
|
||||||
const cutoffDate = new Date();
|
|
||||||
cutoffDate.setDate(cutoffDate.getDate() - daysBack);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Fetching issues created since ${cutoffDate.toISOString()}...`
|
|
||||||
);
|
|
||||||
const allIssues = [];
|
|
||||||
let page = 1;
|
|
||||||
const perPage = 100;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const pageIssues = await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&since=${cutoffDate.toISOString()}`,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pageIssues.length === 0) break;
|
|
||||||
|
|
||||||
allIssues.push(...pageIssues);
|
|
||||||
page++;
|
|
||||||
|
|
||||||
// Safety limit to avoid infinite loops
|
|
||||||
if (page > 100) {
|
|
||||||
console.log('[DEBUG] Reached page limit, stopping pagination');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Found ${allIssues.length} issues from the last ${daysBack} days`
|
|
||||||
);
|
|
||||||
|
|
||||||
let processedCount = 0;
|
|
||||||
let candidateCount = 0;
|
|
||||||
let triggeredCount = 0;
|
|
||||||
|
|
||||||
for (const issue of allIssues) {
|
|
||||||
processedCount++;
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
|
|
||||||
const comments = await githubRequest(
|
|
||||||
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Look for existing duplicate detection comments (from the dedupe bot)
|
|
||||||
const dupeDetectionComments = comments.filter(
|
|
||||||
(comment) =>
|
|
||||||
comment.body.includes('Found') &&
|
|
||||||
comment.body.includes('possible duplicate') &&
|
|
||||||
comment.user.type === 'Bot'
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip if there's already a duplicate detection comment
|
|
||||||
if (dupeDetectionComments.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidateCount++;
|
|
||||||
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(
|
|
||||||
`[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
|
|
||||||
);
|
|
||||||
await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
console.log(
|
|
||||||
`[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
triggeredCount++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a delay between workflow triggers to avoid overwhelming the system
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
backfillDuplicateComments().catch(console.error);
|
|
||||||
54
.github/scripts/pre-release.mjs
vendored
Executable file
54
.github/scripts/pre-release.mjs
vendored
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import {
|
||||||
|
findRootDir,
|
||||||
|
runCommand,
|
||||||
|
getPackageVersion,
|
||||||
|
createAndPushTag
|
||||||
|
} from './utils.mjs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const rootDir = findRootDir(__dirname);
|
||||||
|
const extensionPkgPath = join(rootDir, 'apps', 'extension', 'package.json');
|
||||||
|
|
||||||
|
console.log('🚀 Starting pre-release process...');
|
||||||
|
|
||||||
|
// Check if we're in RC mode
|
||||||
|
const preJsonPath = join(rootDir, '.changeset', 'pre.json');
|
||||||
|
if (!existsSync(preJsonPath)) {
|
||||||
|
console.error('⚠️ Not in RC mode. Run "npx changeset pre enter rc" first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const preJson = JSON.parse(readFileSync(preJsonPath, 'utf8'));
|
||||||
|
if (preJson.tag !== 'rc') {
|
||||||
|
console.error(`⚠️ Not in RC mode. Current tag: ${preJson.tag}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read pre.json:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current extension version
|
||||||
|
const extensionVersion = getPackageVersion(extensionPkgPath);
|
||||||
|
console.log(`Extension version: ${extensionVersion}`);
|
||||||
|
|
||||||
|
// Run changeset publish for npm packages
|
||||||
|
console.log('📦 Publishing npm packages...');
|
||||||
|
runCommand('npx', ['changeset', 'publish']);
|
||||||
|
|
||||||
|
// Create tag for extension pre-release if it doesn't exist
|
||||||
|
const extensionTag = `extension-rc@${extensionVersion}`;
|
||||||
|
const tagCreated = createAndPushTag(extensionTag);
|
||||||
|
|
||||||
|
if (tagCreated) {
|
||||||
|
console.log('This will trigger the extension-pre-release workflow...');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Pre-release process completed!');
|
||||||
31
.github/workflows/auto-close-duplicates.yml
vendored
31
.github/workflows/auto-close-duplicates.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Auto-close duplicate issues
|
|
||||||
# description: Auto-closes issues that are duplicates of existing issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 9 * * *" # Runs daily at 9 AM UTC
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-close-duplicates:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write # Need write permission to close issues and add comments
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Auto-close duplicate issues
|
|
||||||
run: node .github/scripts/auto-close-duplicates.mjs
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
|
||||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
name: Backfill Duplicate Comments
|
|
||||||
# description: Triggers duplicate detection for old issues that don't have duplicate comments
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
days_back:
|
|
||||||
description: "How many days back to look for old issues"
|
|
||||||
required: false
|
|
||||||
default: "90"
|
|
||||||
type: string
|
|
||||||
dry_run:
|
|
||||||
description: "Dry run mode (true to only log what would be done)"
|
|
||||||
required: false
|
|
||||||
default: "true"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "true"
|
|
||||||
- "false"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
backfill-duplicate-comments:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: read
|
|
||||||
actions: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Backfill duplicate comments
|
|
||||||
run: node .github/scripts/backfill-duplicate-comments.mjs
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
|
||||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
|
||||||
DAYS_BACK: ${{ inputs.days_back }}
|
|
||||||
DRY_RUN: ${{ inputs.dry_run }}
|
|
||||||
120
.github/workflows/ci.yml
vendored
120
.github/workflows/ci.yml
vendored
@@ -9,121 +9,70 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- next
|
- next
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
DO_NOT_TRACK: 1
|
|
||||||
NODE_ENV: development
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Fast checks that can run in parallel
|
setup:
|
||||||
format-check:
|
|
||||||
name: Format Check
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install Dependencies
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
id: install
|
||||||
timeout-minutes: 5
|
run: npm ci
|
||||||
|
timeout-minutes: 2
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
|
format-check:
|
||||||
|
needs: setup
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Restore node_modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Format Check
|
- name: Format Check
|
||||||
run: npm run format-check
|
run: npm run format-check
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
typecheck:
|
|
||||||
name: Typecheck
|
|
||||||
timeout-minutes: 10
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: "npm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
|
||||||
timeout-minutes: 5
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
run: npm run turbo:typecheck
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
# Build job to ensure everything compiles
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: "npm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
|
||||||
timeout-minutes: 5
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run turbo:build
|
|
||||||
env:
|
|
||||||
NODE_ENV: production
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-artifacts
|
|
||||||
path: dist/
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test
|
needs: setup
|
||||||
timeout-minutes: 15
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [format-check, typecheck, build]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: "npm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Restore node_modules
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
uses: actions/cache@v4
|
||||||
timeout-minutes: 5
|
|
||||||
|
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
path: node_modules
|
||||||
path: dist/
|
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
@@ -132,6 +81,7 @@ jobs:
|
|||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
CI: true
|
CI: true
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
81
.github/workflows/claude-dedupe-issues.yml
vendored
81
.github/workflows/claude-dedupe-issues.yml
vendored
@@ -1,81 +0,0 @@
|
|||||||
name: Claude Issue Dedupe
|
|
||||||
# description: Automatically dedupe GitHub issues using Claude Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
issue_number:
|
|
||||||
description: "Issue number to process for duplicate detection"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-dedupe-issues:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Run Claude Code slash command
|
|
||||||
uses: anthropics/claude-code-base-action@beta
|
|
||||||
with:
|
|
||||||
prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}"
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
claude_env: |
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Log duplicate comment event to Statsig
|
|
||||||
if: always()
|
|
||||||
env:
|
|
||||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
|
||||||
run: |
|
|
||||||
ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }}
|
|
||||||
REPO=${{ github.repository }}
|
|
||||||
|
|
||||||
if [ -z "$STATSIG_API_KEY" ]; then
|
|
||||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prepare the event payload
|
|
||||||
EVENT_PAYLOAD=$(jq -n \
|
|
||||||
--arg issue_number "$ISSUE_NUMBER" \
|
|
||||||
--arg repo "$REPO" \
|
|
||||||
--arg triggered_by "${{ github.event_name }}" \
|
|
||||||
'{
|
|
||||||
events: [{
|
|
||||||
eventName: "github_duplicate_comment_added",
|
|
||||||
value: 1,
|
|
||||||
metadata: {
|
|
||||||
repository: $repo,
|
|
||||||
issue_number: ($issue_number | tonumber),
|
|
||||||
triggered_by: $triggered_by,
|
|
||||||
workflow_run_id: "${{ github.run_id }}"
|
|
||||||
},
|
|
||||||
time: (now | floor | tostring)
|
|
||||||
}]
|
|
||||||
}')
|
|
||||||
|
|
||||||
# Send to Statsig API
|
|
||||||
echo "Logging duplicate comment event to Statsig for issue #${ISSUE_NUMBER}"
|
|
||||||
|
|
||||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
|
|
||||||
-d "$EVENT_PAYLOAD")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
||||||
BODY=$(echo "$RESPONSE" | head -n-1)
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
|
|
||||||
echo "Successfully logged duplicate comment event for issue #${ISSUE_NUMBER}"
|
|
||||||
else
|
|
||||||
echo "Failed to log duplicate comment event for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
|
|
||||||
fi
|
|
||||||
57
.github/workflows/claude-docs-trigger.yml
vendored
57
.github/workflows/claude-docs-trigger.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: Trigger Claude Documentation Update
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- next
|
|
||||||
paths-ignore:
|
|
||||||
- "apps/docs/**"
|
|
||||||
- "*.md"
|
|
||||||
- ".github/workflows/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
trigger-docs-update:
|
|
||||||
# Only run if changes were merged (not direct pushes from bots)
|
|
||||||
if: github.actor != 'github-actions[bot]' && github.actor != 'dependabot[bot]'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2 # Need previous commit for comparison
|
|
||||||
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
run: |
|
|
||||||
echo "Changed files in this push:"
|
|
||||||
git diff --name-only HEAD^ HEAD | tee changed_files.txt
|
|
||||||
|
|
||||||
# Store changed files for Claude to analyze (escaped for JSON)
|
|
||||||
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD | jq -Rs .)
|
|
||||||
echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Get the commit message (escaped for JSON)
|
|
||||||
COMMIT_MSG=$(git log -1 --pretty=%B | jq -Rs .)
|
|
||||||
echo "commit_message=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Get diff for documentation context (escaped for JSON)
|
|
||||||
COMMIT_DIFF=$(git diff HEAD^ HEAD --stat | jq -Rs .)
|
|
||||||
echo "commit_diff=$COMMIT_DIFF" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Get commit SHA
|
|
||||||
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Trigger Claude workflow
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Trigger the Claude docs updater workflow with the change information
|
|
||||||
gh workflow run claude-docs-updater.yml \
|
|
||||||
--ref next \
|
|
||||||
-f commit_sha="${{ steps.changed-files.outputs.commit_sha }}" \
|
|
||||||
-f commit_message=${{ steps.changed-files.outputs.commit_message }} \
|
|
||||||
-f changed_files=${{ steps.changed-files.outputs.changed_files }} \
|
|
||||||
-f commit_diff=${{ steps.changed-files.outputs.commit_diff }}
|
|
||||||
145
.github/workflows/claude-docs-updater.yml
vendored
145
.github/workflows/claude-docs-updater.yml
vendored
@@ -1,145 +0,0 @@
|
|||||||
name: Claude Documentation Updater
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
commit_sha:
|
|
||||||
description: 'The commit SHA that triggered this update'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
commit_message:
|
|
||||||
description: 'The commit message'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
changed_files:
|
|
||||||
description: 'List of changed files'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
commit_diff:
|
|
||||||
description: 'Diff summary of changes'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-docs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: next
|
|
||||||
fetch-depth: 0 # Need full history to checkout specific commit
|
|
||||||
|
|
||||||
- name: Create docs update branch
|
|
||||||
id: create-branch
|
|
||||||
run: |
|
|
||||||
BRANCH_NAME="docs/auto-update-$(date +%Y%m%d-%H%M%S)"
|
|
||||||
git checkout -b $BRANCH_NAME
|
|
||||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Run Claude Code to Update Documentation
|
|
||||||
uses: anthropics/claude-code-action@beta
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
timeout_minutes: "30"
|
|
||||||
mode: "agent"
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
experimental_allowed_domains: |
|
|
||||||
.anthropic.com
|
|
||||||
.github.com
|
|
||||||
api.github.com
|
|
||||||
.githubusercontent.com
|
|
||||||
registry.npmjs.org
|
|
||||||
.task-master.dev
|
|
||||||
base_branch: "next"
|
|
||||||
direct_prompt: |
|
|
||||||
You are a documentation specialist. Analyze the recent changes pushed to the 'next' branch and update the documentation accordingly.
|
|
||||||
|
|
||||||
Recent changes:
|
|
||||||
- Commit: ${{ inputs.commit_message }}
|
|
||||||
- Changed files:
|
|
||||||
${{ inputs.changed_files }}
|
|
||||||
|
|
||||||
- Changes summary:
|
|
||||||
${{ inputs.commit_diff }}
|
|
||||||
|
|
||||||
Your task:
|
|
||||||
1. Analyze the changes to understand what functionality was added, modified, or removed
|
|
||||||
2. Check if these changes require documentation updates in apps/docs/
|
|
||||||
3. If documentation updates are needed:
|
|
||||||
- Update relevant documentation files in apps/docs/
|
|
||||||
- Ensure examples are updated if APIs changed
|
|
||||||
- Update any configuration documentation if config options changed
|
|
||||||
- Add new documentation pages if new features were added
|
|
||||||
- Update the changelog or release notes if applicable
|
|
||||||
4. If no documentation updates are needed, skip creating changes
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Focus only on user-facing changes that need documentation
|
|
||||||
- Keep documentation clear, concise, and helpful
|
|
||||||
- Include code examples where appropriate
|
|
||||||
- Maintain consistent documentation style with existing docs
|
|
||||||
- Don't document internal implementation details unless they affect users
|
|
||||||
- Update navigation/menu files if new pages are added
|
|
||||||
|
|
||||||
Only make changes if the documentation truly needs updating based on the code changes.
|
|
||||||
|
|
||||||
- name: Check if changes were made
|
|
||||||
id: check-changes
|
|
||||||
run: |
|
|
||||||
if git diff --quiet; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
git add -A
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git commit -m "docs: auto-update documentation based on changes in next branch
|
|
||||||
|
|
||||||
This PR was automatically generated to update documentation based on recent changes.
|
|
||||||
|
|
||||||
Original commit: ${{ inputs.commit_message }}
|
|
||||||
|
|
||||||
Co-authored-by: Claude <claude-assistant@anthropic.com>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Push changes and create PR
|
|
||||||
if: steps.check-changes.outputs.has_changes == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
git push origin ${{ steps.create-branch.outputs.branch_name }}
|
|
||||||
|
|
||||||
# Create PR using GitHub CLI
|
|
||||||
gh pr create \
|
|
||||||
--title "docs: update documentation for recent changes" \
|
|
||||||
--body "## 📚 Documentation Update
|
|
||||||
|
|
||||||
This PR automatically updates documentation based on recent changes merged to the \`next\` branch.
|
|
||||||
|
|
||||||
### Original Changes
|
|
||||||
**Commit:** ${{ inputs.commit_sha }}
|
|
||||||
**Message:** ${{ inputs.commit_message }}
|
|
||||||
|
|
||||||
### Changed Files in Original Commit
|
|
||||||
\`\`\`
|
|
||||||
${{ inputs.changed_files }}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Documentation Updates
|
|
||||||
This PR includes documentation updates to reflect the changes above. Please review to ensure:
|
|
||||||
- [ ] Documentation accurately reflects the changes
|
|
||||||
- [ ] Examples are correct and working
|
|
||||||
- [ ] No important details are missing
|
|
||||||
- [ ] Style is consistent with existing documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
*This PR was automatically generated by Claude Code GitHub Action*" \
|
|
||||||
--base next \
|
|
||||||
--head ${{ steps.create-branch.outputs.branch_name }} \
|
|
||||||
--label "documentation" \
|
|
||||||
--label "automated"
|
|
||||||
107
.github/workflows/claude-issue-triage.yml
vendored
107
.github/workflows/claude-issue-triage.yml
vendored
@@ -1,107 +0,0 @@
|
|||||||
name: Claude Issue Triage
|
|
||||||
# description: Automatically triage GitHub issues using Claude Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage-issue:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Create triage prompt
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/claude-prompts
|
|
||||||
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
|
|
||||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
|
||||||
|
|
||||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
|
||||||
|
|
||||||
Issue Information:
|
|
||||||
- REPO: ${{ github.repository }}
|
|
||||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
|
|
||||||
TASK OVERVIEW:
|
|
||||||
|
|
||||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
|
||||||
|
|
||||||
2. Next, use the GitHub tools to get context about the issue:
|
|
||||||
- You have access to these tools:
|
|
||||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
|
||||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
|
||||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
|
||||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
|
||||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
|
||||||
- Start by using mcp__github__get_issue to get the issue details
|
|
||||||
|
|
||||||
3. Analyze the issue content, considering:
|
|
||||||
- The issue title and description
|
|
||||||
- The type of issue (bug report, feature request, question, etc.)
|
|
||||||
- Technical areas mentioned
|
|
||||||
- Severity or priority indicators
|
|
||||||
- User impact
|
|
||||||
- Components affected
|
|
||||||
|
|
||||||
4. Select appropriate labels from the available labels list provided above:
|
|
||||||
- Choose labels that accurately reflect the issue's nature
|
|
||||||
- Be specific but comprehensive
|
|
||||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
|
||||||
- Consider platform labels (android, ios) if applicable
|
|
||||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
|
||||||
|
|
||||||
5. Apply the selected labels:
|
|
||||||
- Use mcp__github__update_issue to apply your selected labels
|
|
||||||
- DO NOT post any comments explaining your decision
|
|
||||||
- DO NOT communicate directly with users
|
|
||||||
- If no labels are clearly applicable, do not apply any labels
|
|
||||||
|
|
||||||
IMPORTANT GUIDELINES:
|
|
||||||
- Be thorough in your analysis
|
|
||||||
- Only select labels from the provided list above
|
|
||||||
- DO NOT post any comments to the issue
|
|
||||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
|
||||||
- It's okay to not add any labels if none are clearly applicable
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Setup GitHub MCP Server
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/mcp-config
|
|
||||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"github": {
|
|
||||||
"command": "docker",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"-i",
|
|
||||||
"--rm",
|
|
||||||
"-e",
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
||||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Run Claude Code for Issue Triage
|
|
||||||
uses: anthropics/claude-code-base-action@beta
|
|
||||||
with:
|
|
||||||
prompt_file: /tmp/claude-prompts/triage-prompt.txt
|
|
||||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
|
||||||
timeout_minutes: "5"
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
|
||||||
claude_env: |
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
36
.github/workflows/claude.yml
vendored
36
.github/workflows/claude.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: Claude Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
issues:
|
|
||||||
types: [opened, assigned]
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude:
|
|
||||||
if: |
|
|
||||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
||||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
||||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code
|
|
||||||
id: claude
|
|
||||||
uses: anthropics/claude-code-action@beta
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
110
.github/workflows/extension-pre-release.yml
vendored
Normal file
110
.github/workflows/extension-pre-release.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
name: Extension Pre-Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "extension-rc@*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency: extension-pre-release-${{ github.ref }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-extension-rc:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: extension-release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
node_modules
|
||||||
|
*/*/node_modules
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
- name: Install Extension Dependencies
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: npm ci
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
- name: Type Check Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: npm run check-types
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Build Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Package Extension
|
||||||
|
working-directory: apps/extension
|
||||||
|
run: npm run package
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Create VSIX Package (Pre-Release)
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: npx vsce package --no-dependencies --pre-release
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Get VSIX filename
|
||||||
|
id: vsix-info
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: |
|
||||||
|
VSIX_FILE=$(find . -maxdepth 1 -name "*.vsix" -type f | head -n1 | xargs basename)
|
||||||
|
if [ -z "$VSIX_FILE" ]; then
|
||||||
|
echo "Error: No VSIX file found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "vsix-filename=$VSIX_FILE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Found VSIX: $VSIX_FILE"
|
||||||
|
|
||||||
|
- name: Publish to VS Code Marketplace (Pre-Release)
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: npx vsce publish --packagePath "${{ steps.vsix-info.outputs.vsix-filename }}" --pre-release
|
||||||
|
env:
|
||||||
|
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Install Open VSX CLI
|
||||||
|
run: npm install -g ovsx
|
||||||
|
|
||||||
|
- name: Publish to Open VSX Registry (Pre-Release)
|
||||||
|
working-directory: apps/extension/vsix-build
|
||||||
|
run: ovsx publish "${{ steps.vsix-info.outputs.vsix-filename }}" --pre-release
|
||||||
|
env:
|
||||||
|
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Upload Build Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: extension-pre-release-${{ github.ref_name }}
|
||||||
|
path: |
|
||||||
|
apps/extension/vsix-build/*.vsix
|
||||||
|
apps/extension/dist/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
notify-success:
|
||||||
|
needs: publish-extension-rc
|
||||||
|
if: success()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Success Notification
|
||||||
|
run: |
|
||||||
|
echo "🚀 Extension ${{ github.ref_name }} successfully published as pre-release!"
|
||||||
|
echo "📦 Available on VS Code Marketplace (Pre-Release)"
|
||||||
|
echo "🌍 Available on Open VSX Registry (Pre-Release)"
|
||||||
176
.github/workflows/log-issue-events.yml
vendored
176
.github/workflows/log-issue-events.yml
vendored
@@ -1,176 +0,0 @@
|
|||||||
name: Log GitHub Issue Events
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, closed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
log-issue-created:
|
|
||||||
if: github.event.action == 'opened'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Log issue creation to Statsig
|
|
||||||
env:
|
|
||||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
|
||||||
run: |
|
|
||||||
ISSUE_NUMBER=${{ github.event.issue.number }}
|
|
||||||
REPO=${{ github.repository }}
|
|
||||||
ISSUE_TITLE=$(echo '${{ github.event.issue.title }}' | sed "s/'/'\\\\''/g")
|
|
||||||
AUTHOR="${{ github.event.issue.user.login }}"
|
|
||||||
CREATED_AT="${{ github.event.issue.created_at }}"
|
|
||||||
|
|
||||||
if [ -z "$STATSIG_API_KEY" ]; then
|
|
||||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prepare the event payload
|
|
||||||
EVENT_PAYLOAD=$(jq -n \
|
|
||||||
--arg issue_number "$ISSUE_NUMBER" \
|
|
||||||
--arg repo "$REPO" \
|
|
||||||
--arg title "$ISSUE_TITLE" \
|
|
||||||
--arg author "$AUTHOR" \
|
|
||||||
--arg created_at "$CREATED_AT" \
|
|
||||||
'{
|
|
||||||
events: [{
|
|
||||||
eventName: "github_issue_created",
|
|
||||||
value: 1,
|
|
||||||
metadata: {
|
|
||||||
repository: $repo,
|
|
||||||
issue_number: ($issue_number | tonumber),
|
|
||||||
issue_title: $title,
|
|
||||||
issue_author: $author,
|
|
||||||
created_at: $created_at
|
|
||||||
},
|
|
||||||
time: (now | floor | tostring)
|
|
||||||
}]
|
|
||||||
}')
|
|
||||||
|
|
||||||
# Send to Statsig API
|
|
||||||
echo "Logging issue creation to Statsig for issue #${ISSUE_NUMBER}"
|
|
||||||
|
|
||||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
|
|
||||||
-d "$EVENT_PAYLOAD")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
||||||
BODY=$(echo "$RESPONSE" | head -n-1)
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
|
|
||||||
echo "Successfully logged issue creation for issue #${ISSUE_NUMBER}"
|
|
||||||
else
|
|
||||||
echo "Failed to log issue creation for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log-issue-closed:
|
|
||||||
if: github.event.action == 'closed'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Log issue closure to Statsig
|
|
||||||
env:
|
|
||||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
ISSUE_NUMBER=${{ github.event.issue.number }}
|
|
||||||
REPO=${{ github.repository }}
|
|
||||||
ISSUE_TITLE=$(echo '${{ github.event.issue.title }}' | sed "s/'/'\\\\''/g")
|
|
||||||
CLOSED_BY="${{ github.event.issue.closed_by.login }}"
|
|
||||||
CLOSED_AT="${{ github.event.issue.closed_at }}"
|
|
||||||
STATE_REASON="${{ github.event.issue.state_reason }}"
|
|
||||||
|
|
||||||
if [ -z "$STATSIG_API_KEY" ]; then
|
|
||||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get additional issue data via GitHub API
|
|
||||||
echo "Fetching additional issue data for #${ISSUE_NUMBER}"
|
|
||||||
ISSUE_DATA=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
"https://api.github.com/repos/${REPO}/issues/${ISSUE_NUMBER}")
|
|
||||||
|
|
||||||
COMMENTS_COUNT=$(echo "$ISSUE_DATA" | jq -r '.comments')
|
|
||||||
|
|
||||||
# Get reactions data
|
|
||||||
REACTIONS_DATA=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
"https://api.github.com/repos/${REPO}/issues/${ISSUE_NUMBER}/reactions")
|
|
||||||
|
|
||||||
REACTIONS_COUNT=$(echo "$REACTIONS_DATA" | jq '. | length')
|
|
||||||
|
|
||||||
# Check if issue was closed automatically (by checking if closed_by is a bot)
|
|
||||||
CLOSED_AUTOMATICALLY="false"
|
|
||||||
if [[ "$CLOSED_BY" == *"[bot]"* ]]; then
|
|
||||||
CLOSED_AUTOMATICALLY="true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if closed as duplicate by state_reason
|
|
||||||
CLOSED_AS_DUPLICATE="false"
|
|
||||||
if [ "$STATE_REASON" = "duplicate" ]; then
|
|
||||||
CLOSED_AS_DUPLICATE="true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prepare the event payload
|
|
||||||
EVENT_PAYLOAD=$(jq -n \
|
|
||||||
--arg issue_number "$ISSUE_NUMBER" \
|
|
||||||
--arg repo "$REPO" \
|
|
||||||
--arg title "$ISSUE_TITLE" \
|
|
||||||
--arg closed_by "$CLOSED_BY" \
|
|
||||||
--arg closed_at "$CLOSED_AT" \
|
|
||||||
--arg state_reason "$STATE_REASON" \
|
|
||||||
--arg comments_count "$COMMENTS_COUNT" \
|
|
||||||
--arg reactions_count "$REACTIONS_COUNT" \
|
|
||||||
--arg closed_automatically "$CLOSED_AUTOMATICALLY" \
|
|
||||||
--arg closed_as_duplicate "$CLOSED_AS_DUPLICATE" \
|
|
||||||
'{
|
|
||||||
events: [{
|
|
||||||
eventName: "github_issue_closed",
|
|
||||||
value: 1,
|
|
||||||
metadata: {
|
|
||||||
repository: $repo,
|
|
||||||
issue_number: ($issue_number | tonumber),
|
|
||||||
issue_title: $title,
|
|
||||||
closed_by: $closed_by,
|
|
||||||
closed_at: $closed_at,
|
|
||||||
state_reason: $state_reason,
|
|
||||||
comments_count: ($comments_count | tonumber),
|
|
||||||
reactions_count: ($reactions_count | tonumber),
|
|
||||||
closed_automatically: ($closed_automatically | test("true")),
|
|
||||||
closed_as_duplicate: ($closed_as_duplicate | test("true"))
|
|
||||||
},
|
|
||||||
time: (now | floor | tostring)
|
|
||||||
}]
|
|
||||||
}')
|
|
||||||
|
|
||||||
# Send to Statsig API
|
|
||||||
echo "Logging issue closure to Statsig for issue #${ISSUE_NUMBER}"
|
|
||||||
|
|
||||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
|
|
||||||
-d "$EVENT_PAYLOAD")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
||||||
BODY=$(echo "$RESPONSE" | head -n-1)
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
|
|
||||||
echo "Successfully logged issue closure for issue #${ISSUE_NUMBER}"
|
|
||||||
echo "Closed by: $CLOSED_BY"
|
|
||||||
echo "Comments: $COMMENTS_COUNT"
|
|
||||||
echo "Reactions: $REACTIONS_COUNT"
|
|
||||||
echo "Closed automatically: $CLOSED_AUTOMATICALLY"
|
|
||||||
echo "Closed as duplicate: $CLOSED_AS_DUPLICATE"
|
|
||||||
else
|
|
||||||
echo "Failed to log issue closure for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
|
|
||||||
fi
|
|
||||||
15
.github/workflows/pre-release.yml
vendored
15
.github/workflows/pre-release.yml
vendored
@@ -65,24 +65,15 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Run format
|
|
||||||
run: npm run format
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
- name: Build packages
|
|
||||||
run: npm run turbo:build
|
|
||||||
env:
|
|
||||||
NODE_ENV: production
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
publish: npx changeset publish
|
publish: node ./.github/scripts/pre-release.mjs
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||||
|
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||||
|
|
||||||
- name: Commit & Push changes
|
- name: Commit & Push changes
|
||||||
uses: actions-js/push@master
|
uses: actions-js/push@master
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -41,12 +41,6 @@ jobs:
|
|||||||
- name: Check pre-release mode
|
- name: Check pre-release mode
|
||||||
run: node ./.github/scripts/check-pre-release-mode.mjs "main"
|
run: node ./.github/scripts/check-pre-release-mode.mjs "main"
|
||||||
|
|
||||||
- name: Build packages
|
|
||||||
run: npm run turbo:build
|
|
||||||
env:
|
|
||||||
NODE_ENV: production
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
- name: Create Release Pull Request or Publish to npm
|
- name: Create Release Pull Request or Publish to npm
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
96
.github/workflows/weekly-metrics-discord.yml
vendored
96
.github/workflows/weekly-metrics-discord.yml
vendored
@@ -1,96 +0,0 @@
|
|||||||
name: Weekly Metrics to Discord
|
|
||||||
# description: Sends weekly metrics summary to Discord channel
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 9 * * 1" # Every Monday at 9 AM
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
weekly-metrics:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
|
|
||||||
steps:
|
|
||||||
- name: Get dates for last week
|
|
||||||
run: |
|
|
||||||
# Last 7 days
|
|
||||||
first_day=$(date -d "7 days ago" +%Y-%m-%d)
|
|
||||||
last_day=$(date +%Y-%m-%d)
|
|
||||||
|
|
||||||
echo "first_day=$first_day" >> $GITHUB_ENV
|
|
||||||
echo "last_day=$last_day" >> $GITHUB_ENV
|
|
||||||
echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Generate issue metrics
|
|
||||||
uses: github/issue-metrics@v3
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
|
|
||||||
HIDE_TIME_TO_ANSWER: true
|
|
||||||
HIDE_LABEL_METRICS: false
|
|
||||||
|
|
||||||
- name: Generate PR metrics
|
|
||||||
uses: github/issue-metrics@v3
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}"
|
|
||||||
OUTPUT_FILE: pr_metrics.md
|
|
||||||
|
|
||||||
- name: Parse metrics
|
|
||||||
id: metrics
|
|
||||||
run: |
|
|
||||||
# Parse the metrics from the generated markdown files
|
|
||||||
if [ -f "issue_metrics.md" ]; then
|
|
||||||
# Extract key metrics using grep/awk
|
|
||||||
AVG_TIME_TO_FIRST_RESPONSE=$(grep -A 1 "Average time to first response" issue_metrics.md | tail -1 | xargs || echo "N/A")
|
|
||||||
AVG_TIME_TO_CLOSE=$(grep -A 1 "Average time to close" issue_metrics.md | tail -1 | xargs || echo "N/A")
|
|
||||||
NUM_ISSUES_CREATED=$(grep -oP '\d+(?= issues created)' issue_metrics.md || echo "0")
|
|
||||||
NUM_ISSUES_CLOSED=$(grep -oP '\d+(?= issues closed)' issue_metrics.md || echo "0")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "pr_metrics.md" ]; then
|
|
||||||
PR_AVG_TIME_TO_MERGE=$(grep -A 1 "Average time to close" pr_metrics.md | tail -1 | xargs || echo "N/A")
|
|
||||||
NUM_PRS_CREATED=$(grep -oP '\d+(?= pull requests created)' pr_metrics.md || echo "0")
|
|
||||||
NUM_PRS_MERGED=$(grep -oP '\d+(?= pull requests closed)' pr_metrics.md || echo "0")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set outputs for Discord action
|
|
||||||
echo "issues_created=${NUM_ISSUES_CREATED:-0}" >> $GITHUB_OUTPUT
|
|
||||||
echo "issues_closed=${NUM_ISSUES_CLOSED:-0}" >> $GITHUB_OUTPUT
|
|
||||||
echo "prs_created=${NUM_PRS_CREATED:-0}" >> $GITHUB_OUTPUT
|
|
||||||
echo "prs_merged=${NUM_PRS_MERGED:-0}" >> $GITHUB_OUTPUT
|
|
||||||
echo "avg_first_response=${AVG_TIME_TO_FIRST_RESPONSE:-N/A}" >> $GITHUB_OUTPUT
|
|
||||||
echo "avg_time_to_close=${AVG_TIME_TO_CLOSE:-N/A}" >> $GITHUB_OUTPUT
|
|
||||||
echo "pr_avg_merge_time=${PR_AVG_TIME_TO_MERGE:-N/A}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Send to Discord
|
|
||||||
uses: sarisia/actions-status-discord@v1
|
|
||||||
if: env.DISCORD_WEBHOOK != ''
|
|
||||||
with:
|
|
||||||
webhook: ${{ env.DISCORD_WEBHOOK }}
|
|
||||||
status: Success
|
|
||||||
title: "📊 Weekly Metrics Report"
|
|
||||||
description: |
|
|
||||||
**${{ env.week_of }}**
|
|
||||||
|
|
||||||
**🎯 Issues**
|
|
||||||
• Created: ${{ steps.metrics.outputs.issues_created }}
|
|
||||||
• Closed: ${{ steps.metrics.outputs.issues_closed }}
|
|
||||||
|
|
||||||
**🔀 Pull Requests**
|
|
||||||
• Created: ${{ steps.metrics.outputs.prs_created }}
|
|
||||||
• Merged: ${{ steps.metrics.outputs.prs_merged }}
|
|
||||||
|
|
||||||
**⏱️ Response Times**
|
|
||||||
• First Response: ${{ steps.metrics.outputs.avg_first_response }}
|
|
||||||
• Time to Close: ${{ steps.metrics.outputs.avg_time_to_close }}
|
|
||||||
• PR Merge Time: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
|
||||||
color: 0x58AFFF
|
|
||||||
username: Task Master Metrics Bot
|
|
||||||
avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -93,7 +93,4 @@ dev-debug.log
|
|||||||
apps/extension/.vscode-test/
|
apps/extension/.vscode-test/
|
||||||
|
|
||||||
# apps/extension
|
# apps/extension
|
||||||
apps/extension/vsix-build/
|
apps/extension/vsix-build/
|
||||||
|
|
||||||
# turbo
|
|
||||||
.turbo
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
# Task Master Migration Roadmap
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Gradual migration from scripts-based architecture to a clean monorepo with separated concerns.
|
|
||||||
|
|
||||||
## Architecture Vision
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ User Interfaces │
|
|
||||||
├──────────┬──────────┬──────────┬────────────────┤
|
|
||||||
│ @tm/cli │ @tm/mcp │ @tm/ext │ @tm/web │
|
|
||||||
│ (CLI) │ (MCP) │ (VSCode)│ (Future) │
|
|
||||||
└──────────┴──────────┴──────────┴────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌──────────────────────┐
|
|
||||||
│ @tm/core │
|
|
||||||
│ (Business Logic) │
|
|
||||||
└──────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Phases
|
|
||||||
|
|
||||||
### Phase 1: Core Extraction ✅ (In Progress)
|
|
||||||
**Goal**: Move all business logic to @tm/core
|
|
||||||
|
|
||||||
- [x] Create @tm/core package structure
|
|
||||||
- [x] Move types and interfaces
|
|
||||||
- [x] Implement TaskMasterCore facade
|
|
||||||
- [x] Move storage adapters
|
|
||||||
- [x] Move task services
|
|
||||||
- [ ] Move AI providers
|
|
||||||
- [ ] Move parser logic
|
|
||||||
- [ ] Complete test coverage
|
|
||||||
|
|
||||||
### Phase 2: CLI Package Creation 🚧 (Started)
|
|
||||||
**Goal**: Create @tm/cli as a thin presentation layer
|
|
||||||
|
|
||||||
- [x] Create @tm/cli package structure
|
|
||||||
- [x] Implement Command interface pattern
|
|
||||||
- [x] Create CommandRegistry
|
|
||||||
- [x] Build legacy bridge/adapter
|
|
||||||
- [x] Migrate list-tasks command
|
|
||||||
- [ ] Migrate remaining commands one by one
|
|
||||||
- [ ] Remove UI logic from core
|
|
||||||
|
|
||||||
### Phase 3: Transitional Integration
|
|
||||||
**Goal**: Use new packages in existing scripts without breaking changes
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// scripts/modules/commands.js gradually adopts new commands
|
|
||||||
import { ListTasksCommand } from '@tm/cli';
|
|
||||||
const listCommand = new ListTasksCommand();
|
|
||||||
|
|
||||||
// Old interface remains the same
|
|
||||||
programInstance
|
|
||||||
.command('list')
|
|
||||||
.action(async (options) => {
|
|
||||||
// Use new command internally
|
|
||||||
const result = await listCommand.execute(convertOptions(options));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: MCP Package
|
|
||||||
**Goal**: Separate MCP server as its own package
|
|
||||||
|
|
||||||
- [ ] Create @tm/mcp package
|
|
||||||
- [ ] Move MCP server code
|
|
||||||
- [ ] Use @tm/core for all logic
|
|
||||||
- [ ] MCP becomes a thin RPC layer
|
|
||||||
|
|
||||||
### Phase 5: Complete Migration
|
|
||||||
**Goal**: Remove old scripts, pure monorepo
|
|
||||||
|
|
||||||
- [ ] All commands migrated to @tm/cli
|
|
||||||
- [ ] Remove scripts/modules/task-manager/*
|
|
||||||
- [ ] Remove scripts/modules/commands.js
|
|
||||||
- [ ] Update bin/task-master.js to use @tm/cli
|
|
||||||
- [ ] Clean up dependencies
|
|
||||||
|
|
||||||
## Current Transitional Strategy
|
|
||||||
|
|
||||||
### 1. Adapter Pattern (commands-adapter.js)
|
|
||||||
```javascript
|
|
||||||
// Checks if new CLI is available and uses it
|
|
||||||
// Falls back to legacy implementation if not
|
|
||||||
export async function listTasksAdapter(...args) {
|
|
||||||
if (cliAvailable) {
|
|
||||||
return useNewImplementation(...args);
|
|
||||||
}
|
|
||||||
return useLegacyImplementation(...args);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Command Bridge Pattern
|
|
||||||
```javascript
|
|
||||||
// Allows new commands to work in old code
|
|
||||||
const bridge = new CommandBridge(new ListTasksCommand());
|
|
||||||
const data = await bridge.run(legacyOptions); // Legacy style
|
|
||||||
const result = await bridge.execute(newOptions); // New style
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Gradual File Migration
|
|
||||||
Instead of big-bang refactoring:
|
|
||||||
1. Create new implementation in @tm/cli
|
|
||||||
2. Add adapter in commands-adapter.js
|
|
||||||
3. Update commands.js to use adapter
|
|
||||||
4. Test both paths work
|
|
||||||
5. Eventually remove adapter when all migrated
|
|
||||||
|
|
||||||
## Benefits of This Approach
|
|
||||||
|
|
||||||
1. **No Breaking Changes**: Existing CLI continues to work
|
|
||||||
2. **Incremental PRs**: Each command can be migrated separately
|
|
||||||
3. **Parallel Development**: New features can use new architecture
|
|
||||||
4. **Easy Rollback**: Can disable new implementation if issues
|
|
||||||
5. **Clear Separation**: Business logic (core) vs presentation (cli/mcp/etc)
|
|
||||||
|
|
||||||
## Example PR Sequence
|
|
||||||
|
|
||||||
### PR 1: Core Package Setup ✅
|
|
||||||
- Create @tm/core
|
|
||||||
- Move types and interfaces
|
|
||||||
- Basic TaskMasterCore implementation
|
|
||||||
|
|
||||||
### PR 2: CLI Package Foundation ✅
|
|
||||||
- Create @tm/cli
|
|
||||||
- Command interface and registry
|
|
||||||
- Legacy bridge utilities
|
|
||||||
|
|
||||||
### PR 3: First Command Migration
|
|
||||||
- Migrate list-tasks to new system
|
|
||||||
- Add adapter in scripts
|
|
||||||
- Test both implementations
|
|
||||||
|
|
||||||
### PR 4-N: Migrate Commands One by One
|
|
||||||
- Each PR migrates 1-2 related commands
|
|
||||||
- Small, reviewable changes
|
|
||||||
- Continuous delivery
|
|
||||||
|
|
||||||
### Final PR: Cleanup
|
|
||||||
- Remove legacy implementations
|
|
||||||
- Remove adapters
|
|
||||||
- Update documentation
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Dual Testing During Migration
|
|
||||||
```javascript
|
|
||||||
describe('List Tasks', () => {
|
|
||||||
it('works with legacy implementation', async () => {
|
|
||||||
// Force legacy
|
|
||||||
const result = await legacyListTasks(...);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works with new implementation', async () => {
|
|
||||||
// Force new
|
|
||||||
const command = new ListTasksCommand();
|
|
||||||
const result = await command.execute(...);
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adapter chooses correctly', async () => {
|
|
||||||
// Let adapter decide
|
|
||||||
const result = await listTasksAdapter(...);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
- [ ] All commands migrated without breaking changes
|
|
||||||
- [ ] Test coverage maintained or improved
|
|
||||||
- [ ] Performance maintained or improved
|
|
||||||
- [ ] Cleaner, more maintainable codebase
|
|
||||||
- [ ] Easy to add new interfaces (web, desktop, etc.)
|
|
||||||
|
|
||||||
## Notes for Contributors
|
|
||||||
|
|
||||||
1. **Keep PRs Small**: Migrate one command at a time
|
|
||||||
2. **Test Both Paths**: Ensure legacy and new both work
|
|
||||||
3. **Document Changes**: Update this roadmap as you go
|
|
||||||
4. **Communicate**: Discuss in PRs if architecture needs adjustment
|
|
||||||
|
|
||||||
This is a living document - update as the migration progresses!
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Simple Todo App PRD
|
|
||||||
|
|
||||||
Create a basic todo list application with the following features:
|
|
||||||
1. Add new todos
|
|
||||||
2. Mark todos as complete
|
|
||||||
3. Delete todos
|
|
||||||
|
|
||||||
That's it. Keep it simple.
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"generatedAt": "2025-08-06T12:39:03.250Z",
|
|
||||||
"tasksAnalyzed": 8,
|
|
||||||
"totalTasks": 11,
|
|
||||||
"analysisCount": 8,
|
|
||||||
"thresholdScore": 5,
|
|
||||||
"projectName": "Taskmaster",
|
|
||||||
"usedResearch": false
|
|
||||||
},
|
|
||||||
"complexityAnalysis": [
|
|
||||||
{
|
|
||||||
"taskId": 118,
|
|
||||||
"taskTitle": "Create AI Provider Base Architecture",
|
|
||||||
"complexityScore": 7,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Break down the implementation of BaseProvider abstract TypeScript class into subtasks focusing on: 1) Converting existing JavaScript base-provider.js to TypeScript with proper interface definitions, 2) Implementing the Template Method pattern with abstract methods, 3) Adding comprehensive error handling and retry logic with exponential backoff, 4) Creating proper TypeScript types for all method signatures and options, 5) Setting up comprehensive unit tests with MockProvider. Consider that the existing codebase uses JavaScript ES modules and Vercel AI SDK, so the TypeScript implementation needs to maintain compatibility while adding type safety.",
|
|
||||||
"reasoning": "This task requires significant architectural work including converting existing JavaScript code to TypeScript, creating new interfaces, implementing design patterns, and ensuring backward compatibility. The existing base-provider.js already implements a sophisticated provider pattern using Vercel AI SDK, so the TypeScript conversion needs careful consideration of type definitions and maintaining existing functionality."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 119,
|
|
||||||
"taskTitle": "Implement Provider Factory with Dynamic Imports",
|
|
||||||
"complexityScore": 5,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Break down the Provider Factory implementation into: 1) Creating the ProviderFactory class structure with proper TypeScript typing, 2) Implementing the switch statement for provider selection logic, 3) Adding dynamic imports for each provider to enable tree-shaking, 4) Handling provider instantiation with configuration passing, 5) Implementing comprehensive error handling for module loading failures. Note that the existing codebase already has a provider selection mechanism in the JavaScript files, so ensure the factory pattern integrates smoothly with existing infrastructure.",
|
|
||||||
"reasoning": "This is a moderate complexity task that involves creating a factory pattern with dynamic imports. The existing codebase already has provider management logic, so the main complexity is in creating a clean TypeScript implementation with proper dynamic imports while maintaining compatibility with the existing JavaScript module system."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 120,
|
|
||||||
"taskTitle": "Implement Anthropic Provider",
|
|
||||||
"complexityScore": 6,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Implement the AnthropicProvider class in stages: 1) Set up the class structure extending BaseProvider with proper TypeScript imports and type definitions, 2) Implement constructor with Anthropic SDK client initialization and configuration handling, 3) Implement generateCompletion method with proper message format transformation and error handling, 4) Add token calculation methods and utility functions (getName, getModel, getDefaultModel), 5) Implement comprehensive error handling with custom error wrapping and type exports. The existing anthropic.js provider can serve as a reference but needs to be reimplemented to extend the new TypeScript BaseProvider.",
|
|
||||||
"reasoning": "This task involves integrating with an external SDK (@anthropic-ai/sdk) and implementing all abstract methods from BaseProvider. The existing JavaScript implementation provides a good reference, but the TypeScript version needs proper type definitions, error handling, and must work with the new abstract base class architecture."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 121,
|
|
||||||
"taskTitle": "Create Prompt Builder and Task Parser",
|
|
||||||
"complexityScore": 8,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Implement PromptBuilder and TaskParser with focus on: 1) Creating PromptBuilder class with template methods for building structured prompts with JSON format instructions, 2) Implementing TaskParser class structure with dependency injection of IAIProvider and IConfiguration, 3) Implementing parsePRD method with file reading, prompt generation, and AI provider integration, 4) Adding task enrichment logic with metadata, validation, and structure verification, 5) Implementing comprehensive error handling for all failure scenarios including file I/O, AI provider errors, and JSON parsing. The existing parse-prd.js provides complex logic that needs to be reimplemented with proper TypeScript types and cleaner architecture.",
|
|
||||||
"reasoning": "This is a complex task that involves multiple components working together: file I/O, AI provider integration, JSON parsing, and data validation. The existing parse-prd.js implementation is quite sophisticated with Zod schemas and complex task processing logic that needs to be reimplemented in TypeScript with proper separation of concerns."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 122,
|
|
||||||
"taskTitle": "Implement Configuration Management",
|
|
||||||
"complexityScore": 6,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Create ConfigManager implementation focusing on: 1) Setting up Zod validation schema that matches the IConfiguration interface structure, 2) Implementing ConfigManager constructor with default values merging and storage initialization, 3) Creating validate method with Zod schema parsing and user-friendly error transformation, 4) Implementing type-safe get method using TypeScript generics and keyof operator, 5) Adding getAll method and ensuring proper immutability and module exports. The existing config-manager.js has complex configuration loading logic that can inform the TypeScript implementation but needs cleaner architecture.",
|
|
||||||
"reasoning": "This task involves creating a configuration management system with validation using Zod. The existing JavaScript config-manager.js is quite complex with multiple configuration sources, defaults, and validation logic. The TypeScript version needs to provide a cleaner API while maintaining the flexibility of the current system."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 123,
|
|
||||||
"taskTitle": "Create Utility Functions and Error Handling",
|
|
||||||
"complexityScore": 4,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Implement utilities and error handling in stages: 1) Create ID generation module with generateTaskId and generateSubtaskId functions using proper random generation, 2) Implement base TaskMasterError class extending Error with proper TypeScript typing, 3) Add error sanitization methods to prevent sensitive data exposure in production, 4) Implement development-only logging with environment detection, 5) Create specialized error subclasses (FileNotFoundError, ParseError, ValidationError, APIError) with appropriate error codes and formatting.",
|
|
||||||
"reasoning": "This is a relatively straightforward task involving utility functions and error class hierarchies. The main complexity is in ensuring proper error sanitization for production use and creating a well-structured error hierarchy that can be used throughout the application."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 124,
|
|
||||||
"taskTitle": "Implement TaskMasterCore Facade",
|
|
||||||
"complexityScore": 7,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Build TaskMasterCore facade implementation: 1) Create class structure with proper TypeScript imports and type definitions for all subsystem interfaces, 2) Implement initialize method for lazy loading AI provider and parser instances based on configuration, 3) Create parsePRD method that coordinates parser, AI provider, and storage subsystems, 4) Implement getTasks and other facade methods for task retrieval and management, 5) Create createTaskMaster factory function and set up all module exports including type re-exports. Ensure proper ESM compatibility with .js extensions in imports.",
|
|
||||||
"reasoning": "This is a complex integration task that brings together all the other components into a cohesive facade. It requires understanding of the facade pattern, proper dependency management, lazy initialization, and careful module export structure for the public API."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 125,
|
|
||||||
"taskTitle": "Create Placeholder Providers and Complete Testing",
|
|
||||||
"complexityScore": 5,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Complete the implementation with placeholders and testing: 1) Create OpenAIProvider placeholder class extending BaseProvider with 'not yet implemented' errors, 2) Create GoogleProvider placeholder class with similar structure, 3) Implement MockProvider in tests/mocks directory with configurable responses and behavior simulation, 4) Write comprehensive unit tests for TaskParser covering all methods and edge cases, 5) Create integration tests for the complete parse-prd workflow ensuring 80% code coverage. Follow kebab-case naming convention for test files.",
|
|
||||||
"reasoning": "This task involves creating placeholder implementations and a comprehensive test suite. While the placeholder providers are simple, creating a good MockProvider and comprehensive tests requires understanding the entire system architecture and ensuring all edge cases are covered."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"generatedAt": "2025-08-06T12:15:01.327Z",
|
|
||||||
"tasksAnalyzed": 8,
|
|
||||||
"totalTasks": 11,
|
|
||||||
"analysisCount": 8,
|
|
||||||
"thresholdScore": 5,
|
|
||||||
"projectName": "Taskmaster",
|
|
||||||
"usedResearch": false
|
|
||||||
},
|
|
||||||
"complexityAnalysis": [
|
|
||||||
{
|
|
||||||
"taskId": 118,
|
|
||||||
"taskTitle": "Create AI Provider Base Architecture",
|
|
||||||
"complexityScore": 4,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Break down the conversion of base-provider.js to TypeScript BaseProvider class: 1) Convert to TypeScript and define IAIProvider interface, 2) Implement abstract class with core properties, 3) Define abstract methods and Template Method pattern, 4) Add retry logic with exponential backoff, 5) Implement validation and logging. Focus on maintaining compatibility with existing provider pattern while adding type safety.",
|
|
||||||
"reasoning": "The codebase already has a well-established BaseAIProvider class in JavaScript. Converting to TypeScript mainly involves adding type definitions and ensuring the existing pattern is preserved. The complexity is moderate because the pattern is already proven in the codebase."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 119,
|
|
||||||
"taskTitle": "Implement Provider Factory with Dynamic Imports",
|
|
||||||
"complexityScore": 3,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Create ProviderFactory implementation: 1) Set up class structure and types, 2) Implement provider selection switch statement, 3) Add dynamic imports for tree-shaking, 4) Handle provider instantiation with config, 5) Add comprehensive error handling. The existing PROVIDERS registry pattern should guide the implementation.",
|
|
||||||
"reasoning": "The codebase already uses a dual registry pattern (static PROVIDERS and dynamic ProviderRegistry). Creating a factory is straightforward as the provider registration patterns are well-established. Dynamic imports are already used in the codebase."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 120,
|
|
||||||
"taskTitle": "Implement Anthropic Provider",
|
|
||||||
"complexityScore": 3,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Implement AnthropicProvider following existing patterns: 1) Create class structure with imports, 2) Implement constructor and client initialization, 3) Add generateCompletion with Claude API integration, 4) Implement token calculation and utility methods, 5) Add error handling and exports. Use the existing anthropic.js provider as reference.",
|
|
||||||
"reasoning": "AnthropicProvider already exists in the codebase with full implementation. This task essentially involves adapting the existing implementation to match the new TypeScript architecture, making it relatively straightforward."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 121,
|
|
||||||
"taskTitle": "Create Prompt Builder and Task Parser",
|
|
||||||
"complexityScore": 6,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Build prompt system and parser: 1) Create PromptBuilder with template methods, 2) Implement TaskParser with dependency injection, 3) Add parsePRD core logic with file reading, 4) Implement task enrichment and metadata, 5) Add comprehensive error handling. Leverage the existing prompt management system in src/prompts/.",
|
|
||||||
"reasoning": "While the codebase has a sophisticated prompt management system, creating a new PromptBuilder and TaskParser requires understanding the existing prompt templates, JSON schema validation, and integration with the AI provider system. The task involves significant new code."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 122,
|
|
||||||
"taskTitle": "Implement Configuration Management",
|
|
||||||
"complexityScore": 5,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Create ConfigManager with validation: 1) Define Zod schema for IConfiguration, 2) Implement constructor with defaults, 3) Add validate method with error handling, 4) Create type-safe get method with generics, 5) Implement getAll and finalize exports. Reference existing config-manager.js for patterns.",
|
|
||||||
"reasoning": "The codebase has an existing config-manager.js with sophisticated configuration handling. Adding Zod validation and TypeScript generics adds complexity, but the existing patterns provide a solid foundation."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 123,
|
|
||||||
"taskTitle": "Create Utility Functions and Error Handling",
|
|
||||||
"complexityScore": 2,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Implement utilities and error handling: 1) Create ID generation module with unique formats, 2) Build TaskMasterError base class, 3) Add error sanitization for security, 4) Implement development-only logging, 5) Create specialized error subclasses. Keep implementation simple and focused.",
|
|
||||||
"reasoning": "This is a straightforward utility implementation task. The codebase already has error handling patterns, and ID generation is a simple algorithmic task. The main work is creating clean, reusable utilities."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 124,
|
|
||||||
"taskTitle": "Implement TaskMasterCore Facade",
|
|
||||||
"complexityScore": 7,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Create main facade class: 1) Set up TaskMasterCore structure with imports, 2) Implement lazy initialization logic, 3) Add parsePRD coordination method, 4) Implement getTasks and other facade methods, 5) Create factory function and exports. This ties together all other components into a cohesive API.",
|
|
||||||
"reasoning": "This is the most complex task as it requires understanding and integrating all other components. The facade must coordinate between configuration, providers, storage, and parsing while maintaining a clean API. It's the architectural keystone of the system."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"taskId": 125,
|
|
||||||
"taskTitle": "Create Placeholder Providers and Complete Testing",
|
|
||||||
"complexityScore": 5,
|
|
||||||
"recommendedSubtasks": 5,
|
|
||||||
"expansionPrompt": "Implement testing infrastructure: 1) Create OpenAIProvider placeholder, 2) Create GoogleProvider placeholder, 3) Build MockProvider for testing, 4) Write TaskParser unit tests, 5) Create integration tests for parse-prd flow. Follow the existing test patterns in tests/ directory.",
|
|
||||||
"reasoning": "While creating placeholder providers is simple, the testing infrastructure requires understanding Jest with ES modules, mocking patterns, and comprehensive test coverage. The existing test structure provides good examples to follow."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"currentTag": "master",
|
"currentTag": "master",
|
||||||
"lastSwitched": "2025-08-27T21:03:20.550Z",
|
"lastSwitched": "2025-08-01T14:09:25.838Z",
|
||||||
"branchTagMapping": {
|
"branchTagMapping": {
|
||||||
"v017-adds": "v017-adds",
|
"v017-adds": "v017-adds",
|
||||||
"next": "next"
|
"next": "next"
|
||||||
|
|||||||
@@ -7297,763 +7297,5 @@
|
|||||||
"updated": "2025-07-22T09:38:19.341Z",
|
"updated": "2025-07-22T09:38:19.341Z",
|
||||||
"description": "Tasks for cc-kiro-hooks context"
|
"description": "Tasks for cc-kiro-hooks context"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"tm-core-phase-1": {
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"id": 115,
|
|
||||||
"title": "Initialize tm-core Package Structure",
|
|
||||||
"description": "Create the initial package structure for tm-core with all required directories and configuration files",
|
|
||||||
"details": "Create the packages/tm-core directory structure with all subdirectories as specified: src/, tests/, and all nested folders. Set up package.json with proper ESM/CJS configuration, tsconfig.json with strict TypeScript settings, tsup.config.js for dual format builds, and jest.config.js for testing. Ensure all barrel export files (index.ts) are created in each directory for clean imports.",
|
|
||||||
"testStrategy": "Verify directory structure matches specification exactly, ensure all configuration files are valid JSON/JS, run 'npm install' to verify package.json is correct, run 'tsc --noEmit' to verify TypeScript configuration",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [],
|
|
||||||
"status": "done",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create tm-core directory structure and base configuration files",
|
|
||||||
"description": "Set up the packages/tm-core directory with all required subdirectories and initialize core configuration files",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create packages/tm-core directory with subdirectories: src/, src/types/, src/interfaces/, src/providers/, src/parsers/, src/builders/, src/utils/, src/errors/, tests/, tests/unit/, tests/integration/, tests/mocks/. Create package.json with name '@task-master/tm-core', version '1.0.0', type 'module', main/module/types fields for dual ESM/CJS support, and necessary dependencies (typescript, tsup, jest, @types/node). Set up tsconfig.json with strict mode, ES2022 target, module resolution, and proper include/exclude patterns.\n<info added on 2025-08-06T10:49:59.891Z>\nImplementation completed as specified. Directory structure verified with all paths created correctly. Package.json configured with dual ESM/CJS support using tsup build tool, exports field properly set for both formats. TypeScript configuration established with strict mode enabled, ES2022 target for modern JavaScript features, and path mappings configured for clean imports like '@/types' and '@/utils'. All configuration files are valid and ready for development.\n</info added on 2025-08-06T10:49:59.891Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Verify directory structure with fs.existsSync() checks, validate package.json structure with JSON.parse(), ensure tsconfig.json compiles without errors"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Configure build and test infrastructure",
|
|
||||||
"description": "Set up tsup build configuration for dual format support and Jest testing configuration",
|
|
||||||
"dependencies": [
|
|
||||||
"115.1"
|
|
||||||
],
|
|
||||||
"details": "Create tsup.config.js with dual format configuration (ESM and CJS), entry points from src/index.ts, declaration files generation, and sourcemaps. Configure jest.config.js with TypeScript preset, ESM support, proper module name mapping, coverage thresholds (80%), and test environment setup. Create .gitignore for node_modules, dist, and coverage directories. Add npm scripts in package.json for build, test, test:watch, and test:coverage commands.\n<info added on 2025-08-06T10:50:49.396Z>\nBuild process successfully configured with tsup.config.ts (TypeScript configuration file instead of JavaScript) supporting dual format output and multiple entry points including submodules. Jest configuration established with comprehensive ESM support and path alias mapping. Created tests/setup.ts for centralized test environment configuration. Added ES2022 compilation target for modern JavaScript features. Enhanced .gitignore to exclude additional development-specific files beyond the basic directories.\n</info added on 2025-08-06T10:50:49.396Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Run 'npm run build' to verify tsup configuration works, execute 'npm test' with a simple test file to confirm Jest setup, check that both .mjs and .cjs files are generated in dist/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Create barrel export files for all directories",
|
|
||||||
"description": "Implement index.ts files in each directory to enable clean imports throughout the package",
|
|
||||||
"dependencies": [
|
|
||||||
"115.1"
|
|
||||||
],
|
|
||||||
"details": "Create index.ts in src/ that exports from all subdirectories. Create index.ts in each subdirectory (types/, interfaces/, providers/, parsers/, builders/, utils/, errors/) with appropriate exports. For now, add placeholder comments indicating what will be exported from each module. Ensure proper export syntax for TypeScript types and interfaces using 'export type' where appropriate. Structure exports to allow consumers to import like '@task-master/tm-core/types' or from the main entry point.\n<info added on 2025-08-06T10:51:56.837Z>\nImplementation complete. All barrel export files have been created successfully with:\n\n- Main src/index.ts exporting from all subdirectories with proper TypeScript syntax\n- Individual index.ts files in types/, providers/, storage/, parser/, utils/, and errors/ directories\n- Proper ES module syntax with .js extensions for TypeScript compatibility\n- Placeholder exports with @deprecated JSDoc tags to indicate future implementation\n- Clean module structure supporting both root imports and submodule imports like '@task-master/tm-core/types'\n- All files include appropriate documentation comments explaining their purpose\n</info added on 2025-08-06T10:51:56.837Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Compile with TypeScript to ensure all index.ts files are valid, verify no circular dependencies exist, check that imports from package root work correctly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Add development tooling and documentation",
|
|
||||||
"description": "Set up development tools, linting, and initial documentation structure",
|
|
||||||
"dependencies": [
|
|
||||||
"115.1",
|
|
||||||
"115.2"
|
|
||||||
],
|
|
||||||
"details": "Create .eslintrc.js with TypeScript plugin and recommended rules for consistent code style. Add prettier configuration for code formatting. Create README.md with package overview, installation instructions, and usage examples (marked as 'coming soon'). Add CHANGELOG.md to track version changes. Create npm scripts for linting and formatting. Add pre-commit hooks configuration if needed. Document the dual ESM/CJS support in README.\n<info added on 2025-08-06T10:53:45.056Z>\nI'll analyze the user's request and the context to determine what new information should be added to the subtask's details.Successfully completed development tooling and documentation setup. Created .eslintrc.js with TypeScript plugin and comprehensive rules including no-explicit-any, consistent-type-imports, and proper TypeScript checks. Added .prettierrc.json with sensible defaults for consistent code formatting. Created comprehensive README.md with package overview, installation instructions, usage examples for both ESM and CommonJS, modular imports, architecture description, development setup, and detailed roadmap for tasks 116-125. Added CHANGELOG.md following Keep a Changelog format with current package status and planned features. All development tooling is configured and ready for use.\n</info added on 2025-08-06T10:53:45.056Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Run eslint on sample TypeScript files, verify prettier formats code consistently, ensure all npm scripts execute without errors"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Validate package structure and prepare for development",
|
|
||||||
"description": "Perform final validation of the package structure and ensure it's ready for implementation",
|
|
||||||
"dependencies": [
|
|
||||||
"115.1",
|
|
||||||
"115.2",
|
|
||||||
"115.3",
|
|
||||||
"115.4"
|
|
||||||
],
|
|
||||||
"details": "Run 'npm install' to ensure all dependencies are properly resolved. Execute 'tsc --noEmit' to verify TypeScript configuration is correct. Create a simple smoke test in tests/ that imports from the package to verify module resolution works. Ensure the package can be linked locally for testing in other projects. Verify that both CommonJS and ESM imports work correctly. Create a checklist in README for remaining implementation tasks based on tasks 116-125.\n<info added on 2025-08-06T11:02:21.457Z>\nSuccessfully validated package structure with comprehensive testing. All validations passed: npm install resolved dependencies without issues, TypeScript compilation (tsc --noEmit) showed no errors, and dual-format build (npm run build) successfully generated both ESM and CJS outputs with proper TypeScript declarations. Created and executed comprehensive smoke test suite covering all module imports, placeholder functionality, and type definitions - all 8 tests passing. Code quality tools (ESLint, Prettier) are properly configured and show no issues. Package is confirmed ready for local linking and supports both CommonJS and ESM import patterns. README updated with implementation checklist marking Task 115 as complete and clearly outlining remaining implementation tasks 116-125. Package structure validation is complete and development environment is fully prepared for core implementation phase.\n</info added on 2025-08-06T11:02:21.457Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Successfully run all build and test commands, verify package can be imported in both ESM and CJS test files, ensure TypeScript compilation produces no errors, confirm all directories contain appropriate index.ts files"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 116,
|
|
||||||
"title": "Define Core TypeScript Types and Interfaces",
|
|
||||||
"description": "Create all TypeScript type definitions and interfaces for the tm-core package",
|
|
||||||
"details": "Create types/index.ts with Task, Subtask, TaskMetadata interfaces and type literals (TaskStatus, TaskPriority, TaskComplexity). Create all interface files: storage.interface.ts with IStorage methods, ai-provider.interface.ts with IAIProvider and AIOptions, configuration.interface.ts with IConfiguration. Use strict typing throughout, no 'any' types allowed. Follow naming conventions: interfaces prefixed with 'I', type literals in PascalCase.",
|
|
||||||
"testStrategy": "Compile with TypeScript to ensure no type errors, create mock implementations to verify interfaces are complete, use type checking in IDE to confirm all required properties are defined",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [
|
|
||||||
115
|
|
||||||
],
|
|
||||||
"status": "done",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create Core Task and Subtask Type Definitions",
|
|
||||||
"description": "Create types/index.ts with fundamental Task and Subtask interfaces including all required properties",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create types/index.ts file. Define Task interface with properties: id (string), title (string), description (string), status (TaskStatus), priority (TaskPriority), dependencies (string[]), details (string), testStrategy (string), subtasks (Subtask[]). Define Subtask interface extending Task but with numeric id. Define TaskMetadata interface with version (string), lastModified (string), taskCount (number), completedCount (number). Export all interfaces.\n<info added on 2025-08-06T11:03:44.220Z>\nImplementation completed with comprehensive type system. Created all required interfaces with strict typing, added optional properties for enhanced functionality (createdAt, updatedAt, effort, actualEffort, tags). Implemented utility types for create/update operations with proper type constraints. Added filter interfaces for advanced querying. Included runtime type guards for safe type narrowing. Successfully compiled without TypeScript errors, ready for integration with storage and AI provider implementations.\n</info added on 2025-08-06T11:03:44.220Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Compile TypeScript files to ensure no type errors, create sample objects conforming to interfaces to verify completeness"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Define Type Literals and Enums",
|
|
||||||
"description": "Create all type literal definitions for TaskStatus, TaskPriority, and TaskComplexity in the types file",
|
|
||||||
"dependencies": [
|
|
||||||
"116.1"
|
|
||||||
],
|
|
||||||
"details": "In types/index.ts, define type literals: TaskStatus = 'pending' | 'in-progress' | 'done' | 'deferred' | 'cancelled' | 'blocked'; TaskPriority = 'low' | 'medium' | 'high' | 'critical'; TaskComplexity = 'simple' | 'moderate' | 'complex' | 'very-complex'. Consider using const assertions for better type inference. Export all type literals.\n<info added on 2025-08-06T11:04:04.675Z>\nType literals were already implemented in subtask 116.1 as part of the comprehensive type system. The types/index.ts file includes all required type literals: TaskStatus with values 'pending' | 'in-progress' | 'done' | 'deferred' | 'cancelled' | 'blocked' | 'review', TaskPriority with values 'low' | 'medium' | 'high' | 'critical', and TaskComplexity with values 'simple' | 'moderate' | 'complex' | 'very-complex'. All type literals are properly exported and include comprehensive JSDoc documentation. TypeScript compilation verified the types work correctly.\n</info added on 2025-08-06T11:04:04.675Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Use TypeScript compiler to verify type literals work correctly, test with invalid values to ensure type checking catches errors"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Create Storage Interface Definition",
|
|
||||||
"description": "Create storage.interface.ts with IStorage interface defining all storage operation methods",
|
|
||||||
"dependencies": [
|
|
||||||
"116.1"
|
|
||||||
],
|
|
||||||
"details": "Create interfaces/storage.interface.ts file. Define IStorage interface with methods: loadTasks(tag?: string): Promise<Task[]>; saveTasks(tasks: Task[], tag?: string): Promise<void>; appendTasks(tasks: Task[], tag?: string): Promise<void>; updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>; deleteTask(taskId: string, tag?: string): Promise<void>; exists(tag?: string): Promise<boolean>. Import Task type from types/index.ts.\n<info added on 2025-08-06T11:05:00.573Z>\nImplementation completed successfully. Extended IStorage interface beyond original specification to include metadata operations (loadMetadata, saveMetadata), tag management (getAllTags, deleteTag, renameTag, copyTag), and lifecycle methods (initialize, close, getStats). Added StorageStats interface for monitoring storage metrics and StorageConfig interface for configuration options. Implemented BaseStorage abstract class that provides common functionality including task validation using validateTask method, tag sanitization with sanitizeTag to ensure valid filenames, and backup path generation through getBackupPath for data safety. The abstract class serves as a foundation for concrete storage implementations, reducing code duplication and ensuring consistent behavior across different storage backends. All methods properly typed with async/await patterns and comprehensive error handling considerations.\n</info added on 2025-08-06T11:05:00.573Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Create mock implementation of IStorage to verify all methods are properly typed, ensure Promise return types are correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Create AI Provider Interface Definition",
|
|
||||||
"description": "Create ai-provider.interface.ts with IAIProvider interface and AIOptions type",
|
|
||||||
"dependencies": [
|
|
||||||
"116.1"
|
|
||||||
],
|
|
||||||
"details": "Create interfaces/ai-provider.interface.ts file. Define AIOptions interface with properties: temperature (number), maxTokens (number), stream (boolean), topP (number), frequencyPenalty (number). Define IAIProvider interface with methods: generateCompletion(prompt: string, options?: AIOptions): Promise<string>; calculateTokens(text: string): number; getName(): string; getModel(): string; getDefaultModel(): string; isAvailable(): Promise<boolean>.\n<info added on 2025-08-06T11:06:15.795Z>\nFile successfully updated with expanded interface implementation details including comprehensive method signatures and supporting interfaces: AIOptions with full parameter set (temperature, maxTokens, stream, model, topP, topK, frequencyPenalty, presencePenalty, stopSequences, systemPrompt), AIResponse structure with content and usage metadata, AIModel interface for model information, ProviderInfo for capability tracking, ProviderUsageStats for usage monitoring, AIProviderConfig for initialization, and additional interfaces for streaming support. Documented BaseAIProvider abstract class implementation with validation, usage tracking, and common utility methods. All interfaces properly typed with strict TypeScript patterns and async/await support. No compilation errors.\n</info added on 2025-08-06T11:06:15.795Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Create stub implementation to verify interface completeness, test optional parameters work correctly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Create Configuration Interface Definition",
|
|
||||||
"description": "Create configuration.interface.ts with IConfiguration interface for all config options",
|
|
||||||
"dependencies": [
|
|
||||||
"116.1",
|
|
||||||
"116.2"
|
|
||||||
],
|
|
||||||
"details": "Create interfaces/configuration.interface.ts file. Define IConfiguration interface with properties: projectPath (string), aiProvider (string), apiKeys (Record<string, string>), models (object with main, research, fallback as strings), enableTags (boolean), defaultTag (string), maxConcurrentTasks (number), retryAttempts (number), retryDelay (number). Import necessary types from types/index.ts. Ensure all properties have appropriate types with no 'any' usage.\n<info added on 2025-08-06T11:07:43.367Z>\nImplementation completed successfully. Created comprehensive configuration system with:\n\n- Core IConfiguration interface with all required properties: projectPath, aiProvider, apiKeys, models configuration, providers settings, tasks management, tags configuration, storage options, retry behavior, logging preferences, and security settings\n- Supporting interfaces for each configuration section: ModelConfig for AI model selection, ProviderConfig for API provider settings, TaskSettings for task management options, TagSettings for tag-based organization, StorageSettings for persistence configuration, RetrySettings for error handling, LoggingSettings for debugging options, SecuritySettings for API key management\n- Configuration management system with IConfigurationFactory for creating configs from various sources (file, environment, defaults) and IConfigurationManager for runtime config operations including loading, saving, validation, watching for changes, and merging configurations\n- Validation system with ConfigValidationResult interface for detailed error reporting, ConfigSchema for JSON schema validation, and EnvironmentConfig for environment variable mapping\n- DEFAULT_CONFIG_VALUES constant providing sensible defaults for all configuration options\n- All interfaces properly typed with strict TypeScript typing, no 'any' usage, proper imports from types/index\n- Successfully exported all interfaces through main index.ts for package consumers\n- TypeScript compilation confirmed passing without any type errors\n</info added on 2025-08-06T11:07:43.367Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Create sample configuration objects to verify interface covers all needed options, test with partial configs to ensure optional properties work"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 117,
|
|
||||||
"title": "Implement Storage Layer with Repository Pattern",
|
|
||||||
"description": "Create FileStorage class implementing IStorage interface for task persistence",
|
|
||||||
"details": "Implement FileStorage class in storage/file-storage.ts following Repository pattern. Constructor accepts projectPath, private basePath property set to {projectPath}/.taskmaster. Implement all IStorage methods: loadTasks, saveTasks, appendTasks, updateTask, deleteTask, exists. Handle file operations with proper error handling (ENOENT returns empty arrays). Use JSON format with tasks array and metadata object containing version and lastModified. Create getTasksPath method to handle tag-based file paths.",
|
|
||||||
"testStrategy": "Unit test all FileStorage methods with mock file system, test error scenarios (missing files, invalid JSON), verify tag-based path generation, test concurrent operations, ensure proper directory creation",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [
|
|
||||||
116
|
|
||||||
],
|
|
||||||
"status": "done",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create FileStorage class structure and constructor",
|
|
||||||
"description": "Set up the FileStorage class skeleton with proper TypeScript typing and implement the constructor that accepts projectPath parameter",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create storage/file-storage.ts file. Import necessary Node.js modules (fs/promises, path). Import IStorage interface and Task type from types. Define FileStorage class implementing IStorage. Create constructor accepting projectPath string parameter. Initialize private basePath property as `${projectPath}/.taskmaster`. Add private property for managing file locks if needed for concurrent operations.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Unit test constructor initialization, verify basePath is correctly set, test with various projectPath inputs including edge cases"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement file path management and helper methods",
|
|
||||||
"description": "Create internal helper methods for managing file paths and ensuring directory structure exists",
|
|
||||||
"dependencies": [
|
|
||||||
"117.1"
|
|
||||||
],
|
|
||||||
"details": "Implement private getTasksPath(tag?: string) method that returns path to tasks file based on optional tag parameter. If tag provided, return `${basePath}/tasks/${tag}.json`, otherwise `${basePath}/tasks/tasks.json`. Create private ensureDirectoryExists() method that creates .taskmaster and tasks directories if they don't exist using fs.mkdir with recursive option. Add private method for safe JSON parsing with error handling.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test getTasksPath with and without tags, verify directory creation works recursively, test JSON parsing with valid and invalid data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement read operations: loadTasks and exists",
|
|
||||||
"description": "Implement methods for reading tasks from the file system with proper error handling",
|
|
||||||
"dependencies": [
|
|
||||||
"117.2"
|
|
||||||
],
|
|
||||||
"details": "Implement loadTasks(tag?: string) method: use getTasksPath to get file path, read file using fs.readFile, parse JSON content, return tasks array from parsed data. Handle ENOENT error by returning empty array. Handle JSON parse errors appropriately. Implement exists(tag?: string) method: use fs.access to check if file exists at getTasksPath location, return boolean result.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test loadTasks with existing files, missing files (ENOENT), corrupted JSON files. Test exists method with present and absent files"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Implement write operations: saveTasks and appendTasks",
|
|
||||||
"description": "Implement methods for persisting tasks to the file system with metadata",
|
|
||||||
"dependencies": [
|
|
||||||
"117.3"
|
|
||||||
],
|
|
||||||
"details": "Implement saveTasks(tasks: Task[], tag?: string) method: ensure directory exists, create data object with tasks array and metadata object containing version (e.g., '1.0.0') and lastModified (ISO timestamp). Write to file using fs.writeFile with JSON.stringify and proper formatting. Implement appendTasks(tasks: Task[], tag?: string) method: load existing tasks, merge with new tasks (avoiding duplicates by ID), call saveTasks with merged array.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test saveTasks creates files with correct structure, verify metadata is included, test appendTasks merges correctly without duplicates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Implement update and delete operations",
|
|
||||||
"description": "Implement methods for modifying and removing individual tasks with atomic operations",
|
|
||||||
"dependencies": [
|
|
||||||
"117.4"
|
|
||||||
],
|
|
||||||
"details": "Implement updateTask(taskId: string, updates: Partial<Task>, tag?: string) method: load tasks, find task by ID, merge updates using object spread, save updated tasks array. Return boolean indicating success. Implement deleteTask(taskId: string, tag?: string) method: load tasks, filter out task with matching ID, save filtered array. Return boolean indicating if task was found and deleted. Ensure both operations are atomic using temporary files if needed.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test updateTask with existing and non-existing tasks, verify partial updates work correctly. Test deleteTask removes correct task, handles missing tasks gracefully"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 118,
|
|
||||||
"title": "Create AI Provider Base Architecture",
|
|
||||||
"description": "Implement abstract BaseProvider class and provider interfaces using Template Method pattern",
|
|
||||||
"details": "Convert existing base-provider.js to TypeScript abstract class BaseProvider implementing IAIProvider. Add protected properties for apiKey and model. Create abstract methods: generateCompletion, calculateTokens, getName, getModel, getDefaultModel. Apply Template Method pattern for common provider logic like error handling and retry logic. Ensure proper type safety throughout.",
|
|
||||||
"testStrategy": "Create MockProvider extending BaseProvider to test abstract class functionality, verify all abstract methods are properly defined, test error handling and common logic",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [
|
|
||||||
116
|
|
||||||
],
|
|
||||||
"status": "done",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Convert base-provider.js to TypeScript and define IAIProvider interface",
|
|
||||||
"description": "Create the IAIProvider interface with all required method signatures and convert the existing base-provider.js file to a TypeScript file with proper type definitions",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/types/providers.ts with IAIProvider interface containing methods: generateCompletion(prompt: string, options?: CompletionOptions): Promise<CompletionResult>, calculateTokens(text: string): number, getName(): string, getModel(): string, getDefaultModel(): string. Move base-provider.js to src/providers/base-provider.ts and add initial TypeScript types.\n<info added on 2025-08-06T12:16:45.893Z>\nSince the IAIProvider interface already exists in src/interfaces/ai-provider.interface.ts with all required methods and type definitions, update the subtask to focus on converting base-provider.js to TypeScript and implementing the BaseAIProvider abstract class. The conversion should extend the existing BaseAIProvider from src/interfaces/ai-provider.interface.ts rather than creating duplicate interfaces. Ensure the implementation aligns with the comprehensive interface that includes AIOptions, AIResponse, AIModel, ProviderInfo types and methods for streaming, validation, and usage tracking.\n</info added on 2025-08-06T12:16:45.893Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Verify that the interface is properly defined and that TypeScript compilation succeeds without errors"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement BaseProvider abstract class with core properties",
|
|
||||||
"description": "Create the abstract BaseProvider class implementing IAIProvider with protected properties for apiKey and model configuration",
|
|
||||||
"dependencies": [
|
|
||||||
"118.1"
|
|
||||||
],
|
|
||||||
"details": "In base-provider.ts, define abstract class BaseProvider implements IAIProvider with protected properties: apiKey: string, model: string, maxRetries: number = 3, retryDelay: number = 1000. Add constructor that accepts BaseProviderConfig interface with apiKey and optional model. Implement getModel() method to return current model.\n<info added on 2025-08-06T12:28:45.485Z>\nI've reviewed the existing BaseAIProvider interface in the interfaces file. The task requires creating a separate BaseProvider abstract class in base-provider.ts that implements the IAIProvider interface, with specific protected properties and configuration. This appears to be a deliberate architectural decision to have a more concrete base class with built-in retry logic and configuration management that all provider implementations will extend.\n</info added on 2025-08-06T12:28:45.485Z>\n<info added on 2025-08-06T13:14:24.539Z>\nSuccessfully implemented BaseProvider abstract class:\n\nIMPLEMENTED FILES:\n✅ packages/tm-core/src/providers/base-provider.ts - Created new BaseProvider abstract class\n✅ packages/tm-core/src/providers/index.ts - Updated to export BaseProvider\n\nIMPLEMENTATION DETAILS:\n- Created BaseProviderConfig interface with required apiKey and optional model\n- BaseProvider abstract class implements IAIProvider interface\n- Protected properties implemented as specified:\n - apiKey: string \n - model: string\n - maxRetries: number = 3\n - retryDelay: number = 1000\n- Constructor accepts BaseProviderConfig and sets apiKey and model (using getDefaultModel() if not provided)\n- Implemented getModel() method that returns current model\n- All IAIProvider methods declared as abstract (to be implemented by concrete providers)\n- Uses .js extension for ESM import compatibility\n- TypeScript compilation verified successful\n\nThe BaseProvider provides the foundation for concrete provider implementations with shared retry logic properties and standardized configuration.\n</info added on 2025-08-06T13:14:24.539Z>\n<info added on 2025-08-20T17:16:14.037Z>\nREFACTORING REQUIRED: The BaseProvider implementation needs to be relocated from packages/tm-core/src/providers/base-provider.ts to packages/tm-core/src/providers/ai/base-provider.ts following the new directory structure. The class must implement the Template Method pattern with the following structure:\n\n1. Keep constructor concise (under 10 lines) - only initialize apiKey and model properties\n2. Remove maxRetries and retryDelay from constructor - these should be class-level constants or configurable separately\n3. Implement all abstract methods from IAIProvider: generateCompletion, calculateTokens, getName, getModel, getDefaultModel\n4. Add protected template methods for extensibility:\n - validateInput(input: string): void - for input validation with early returns\n - prepareRequest(input: string, options?: any): any - for request preparation\n - handleResponse(response: any): string - for response processing\n - handleError(error: any): never - for consistent error handling\n5. Apply clean code principles: extract complex logic into small focused methods, use early returns to reduce nesting, ensure each method has single responsibility\n\nThe refactored BaseProvider will serve as a robust foundation using Template Method pattern, allowing concrete providers to override specific behaviors while maintaining consistent structure and error handling across all AI provider implementations.\n</info added on 2025-08-20T17:16:14.037Z>\n<info added on 2025-08-21T15:57:30.467Z>\nREFACTORING UPDATE: The BaseProvider implementation in packages/tm-core/src/providers/base-provider.ts is now affected by the core/ folder removal and needs its import paths updated. Since base-provider.ts imports from '../interfaces/provider.interface.js', this import remains valid as both providers/ and interfaces/ are at the same level. No changes needed to BaseProvider imports due to the flattening. The file structure reorganization maintains the relative path relationship between providers/ and interfaces/ directories.\n</info added on 2025-08-21T15:57:30.467Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Create a test file that attempts to instantiate BaseProvider directly (should fail) and verify that protected properties are accessible in child classes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Define abstract methods and implement Template Method pattern",
|
|
||||||
"description": "Add all abstract methods to BaseProvider and implement the Template Method pattern for common provider operations",
|
|
||||||
"dependencies": [
|
|
||||||
"118.2"
|
|
||||||
],
|
|
||||||
"details": "Add abstract methods: protected abstract generateCompletionInternal(prompt: string, options?: CompletionOptions): Promise<CompletionResult>, abstract calculateTokens(text: string): number, abstract getName(): string, abstract getDefaultModel(): string. Implement public generateCompletion() as template method that calls generateCompletionInternal() with error handling and retry logic.\n<info added on 2025-08-20T17:16:38.315Z>\nApply Template Method pattern following clean code principles:\n\nDefine abstract methods:\n- protected abstract generateCompletionInternal(prompt: string, options?: CompletionOptions): Promise<CompletionResult>\n- protected abstract calculateTokens(text: string): number\n- protected abstract getName(): string\n- protected abstract getDefaultModel(): string\n- protected abstract getMaxRetries(): number\n- protected abstract getRetryDelay(): number\n\nImplement template method generateCompletion():\n- Call validateInput() with early returns for invalid prompt/options\n- Call prepareRequest() to format the request\n- Execute generateCompletionInternal() with retry logic\n- Call handleResponse() to process the result\n- Call handleError() in catch blocks\n\nAdd protected helper methods:\n- validateInput(prompt: string, options?: CompletionOptions): ValidationResult - Check prompt length, validate options, return early on errors\n- prepareRequest(prompt: string, options?: CompletionOptions): PreparedRequest - Format prompt, merge with defaults, add metadata\n- handleResponse(result: CompletionResult): ProcessedResult - Validate response format, extract completion text, add usage metrics\n- handleError(error: unknown, attempt: number): void - Log error details, determine if retryable, throw TaskMasterError\n\nExtract retry logic helpers:\n- shouldRetry(error: unknown, attempt: number): boolean - Check error type and attempt count\n- calculateBackoffDelay(attempt: number): number - Use exponential backoff with jitter\n- isRateLimitError(error: unknown): boolean - Detect rate limit responses\n- isTimeoutError(error: unknown): boolean - Detect timeout errors\n\nUse named constants:\n- DEFAULT_MAX_RETRIES = 3\n- BASE_RETRY_DELAY_MS = 1000\n- MAX_RETRY_DELAY_MS = 32000\n- BACKOFF_MULTIPLIER = 2\n- JITTER_FACTOR = 0.1\n\nEnsure each method stays under 30 lines by extracting complex logic into focused helper methods.\n</info added on 2025-08-20T17:16:38.315Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Create MockProvider extending BaseProvider to verify all abstract methods must be implemented and template method properly delegates to internal methods"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Implement error handling and retry logic with exponential backoff",
|
|
||||||
"description": "Add comprehensive error handling and retry mechanism with exponential backoff for API calls in the template method",
|
|
||||||
"dependencies": [
|
|
||||||
"118.3"
|
|
||||||
],
|
|
||||||
"details": "In generateCompletion() template method, wrap generateCompletionInternal() in try-catch with retry logic. Implement exponential backoff: delay * Math.pow(2, attempt). Add error types: ProviderError, RateLimitError, AuthenticationError extending Error. Log errors in development mode only. Handle specific error cases like rate limits (429), authentication errors (401), and network timeouts.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test retry logic with MockProvider that fails N times then succeeds, verify exponential backoff timing, test different error scenarios and their handling"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Add validation, logging, and completion options handling",
|
|
||||||
"description": "Implement input validation, debug logging for development, and proper handling of completion options like temperature and max tokens",
|
|
||||||
"dependencies": [
|
|
||||||
"118.4"
|
|
||||||
],
|
|
||||||
"details": "Add validatePrompt() method to check for empty/invalid prompts. Add validateOptions() to ensure temperature is between 0-2, maxTokens is positive. Implement debug logging using console.log only when NODE_ENV !== 'production'. Create CompletionOptions interface with optional temperature, maxTokens, topP, frequencyPenalty, presencePenalty. Ensure all validation errors throw descriptive ProviderError instances.",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test validation with invalid inputs (empty prompts, negative maxTokens, temperature > 2), verify logging only occurs in development, test option merging with defaults"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 119,
|
|
||||||
"title": "Implement Provider Factory with Dynamic Imports",
|
|
||||||
"description": "Create ProviderFactory class using Factory pattern for AI provider instantiation",
|
|
||||||
"details": "Implement ProviderFactory class in ai/provider-factory.ts with static create method. Use switch statement for provider selection ('anthropic', 'openai', 'google'). Implement dynamic imports for each provider to enable tree-shaking. Return Promise<IAIProvider> from create method. Handle unknown providers with meaningful error messages. Ensure proper typing for configuration object.",
|
|
||||||
"testStrategy": "Test factory with each provider type, verify dynamic imports work correctly, test error handling for unknown providers, mock dynamic imports for unit testing",
|
|
||||||
"priority": "medium",
|
|
||||||
"dependencies": [
|
|
||||||
118
|
|
||||||
],
|
|
||||||
"status": "pending",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create ProviderFactory class structure and types",
|
|
||||||
"description": "Set up the ProviderFactory class file with proper TypeScript types and interfaces",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create ai/provider-factory.ts file. Define ProviderFactory class with static create method signature. Import IAIProvider interface from base provider. Define ProviderType as union type ('anthropic' | 'openai' | 'google'). Set up proper return type as Promise<IAIProvider> for the create method to support dynamic imports.\n<info added on 2025-08-20T17:16:56.506Z>\nClean code architecture implementation: Move to src/providers/ai/provider-factory.ts. Follow Single Responsibility Principle - factory only creates providers, no other responsibilities. Create validateProviderConfig() method for provider configuration validation. Define PROVIDER_NAMES constant object with provider string values. Implement create() method with early returns pattern for better readability. Apply Dependency Inversion - factory depends on IAIProvider interface abstraction, not concrete implementations. Keep method under 40 lines following clean code practices.\n</info added on 2025-08-20T17:16:56.506Z>",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Verify file structure and type definitions compile correctly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement provider selection logic with switch statement",
|
|
||||||
"description": "Add the core switch statement logic to handle different provider types",
|
|
||||||
"dependencies": [
|
|
||||||
"119.1"
|
|
||||||
],
|
|
||||||
"details": "Inside the static create method, implement switch statement on provider type parameter. Add cases for 'anthropic', 'openai', and 'google'. Add default case that throws a descriptive error for unknown providers (e.g., throw new Error(`Unknown provider: ${providerType}`)). Structure each case to prepare for dynamic imports.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test switch statement with valid and invalid provider types, verify error messages"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Add dynamic imports for each provider",
|
|
||||||
"description": "Implement dynamic import() statements for lazy loading provider modules",
|
|
||||||
"dependencies": [
|
|
||||||
"119.2"
|
|
||||||
],
|
|
||||||
"details": "In each switch case, use dynamic import() to load the provider module: for 'anthropic' case use await import('./providers/anthropic-provider'), similar for OpenAI and Google providers. Extract the default export or specific class from each dynamic import. This enables tree-shaking by only loading the selected provider.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Mock dynamic imports in tests, verify only requested provider is loaded"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Instantiate providers with configuration",
|
|
||||||
"description": "Create provider instances with proper configuration passing",
|
|
||||||
"dependencies": [
|
|
||||||
"119.3"
|
|
||||||
],
|
|
||||||
"details": "After each dynamic import, instantiate the provider class with the configuration object passed to create method. Ensure configuration object is properly typed (use IConfiguration or relevant subset). Return the instantiated provider instance. Handle any instantiation errors and wrap them with context about which provider failed.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test provider instantiation with various configuration objects, verify configuration is passed correctly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Add error handling and validation",
|
|
||||||
"description": "Implement comprehensive error handling for all failure scenarios",
|
|
||||||
"dependencies": [
|
|
||||||
"119.4"
|
|
||||||
],
|
|
||||||
"details": "Wrap dynamic imports in try-catch blocks to handle module loading failures. Add validation for configuration object before passing to providers. Create custom error messages that include the provider type and specific failure reason. Consider adding a ProviderFactoryError custom error class. Ensure all errors bubble up properly while maintaining async/await chain.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test various error scenarios: missing provider modules, invalid configurations, network failures during dynamic import"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 120,
|
|
||||||
"title": "Implement Anthropic Provider",
|
|
||||||
"description": "Create AnthropicProvider class extending BaseProvider with full Anthropic SDK integration",
|
|
||||||
"details": "Create AnthropicProvider class in ai/providers/anthropic-provider.ts extending BaseProvider. Import and use @anthropic-ai/sdk. Initialize private client property in constructor. Implement all abstract methods: generateCompletion using Claude API, calculateTokens using appropriate tokenizer, getName returning 'anthropic', getModel returning current model, getDefaultModel returning 'claude-3-sonnet-20240229'. Wrap API errors with context.",
|
|
||||||
"testStrategy": "Mock Anthropic SDK for unit tests, test API error handling, verify token calculation accuracy, test with different model configurations",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [
|
|
||||||
118
|
|
||||||
],
|
|
||||||
"status": "pending",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Set up AnthropicProvider class structure and dependencies",
|
|
||||||
"description": "Create the AnthropicProvider class file with proper imports and class structure extending BaseProvider",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create ai/providers/anthropic-provider.ts file. Import BaseProvider from base-provider.ts and import Anthropic from @anthropic-ai/sdk. Import necessary types including IAIProvider, ChatMessage, and ChatCompletion. Set up the class declaration extending BaseProvider with proper TypeScript typing. Add private client property declaration of type Anthropic.\n<info added on 2025-08-20T17:17:15.019Z>\nFile should be created at src/providers/ai/adapters/anthropic-provider.ts instead of ai/providers/anthropic-provider.ts. Follow clean code principles: keep constructor minimal (under 10 lines) with only client initialization. Extract API call logic into separate small methods (each under 20 lines). Use early returns in generateCompletionInternal() for better readability. Extract error mapping logic to a dedicated mapAnthropicError() method. Avoid magic strings - define constants for model names and API parameters.\n</info added on 2025-08-20T17:17:15.019Z>",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Verify file structure and imports compile without errors, ensure class properly extends BaseProvider"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement constructor and client initialization",
|
|
||||||
"description": "Create the constructor that accepts configuration and initializes the Anthropic SDK client",
|
|
||||||
"dependencies": [
|
|
||||||
"120.1"
|
|
||||||
],
|
|
||||||
"details": "Implement constructor accepting IConfiguration parameter. Call super(config) to initialize BaseProvider. Initialize the private client property by creating new Anthropic instance with apiKey from config.apiKeys.anthropic. Add validation to ensure API key exists, throwing meaningful error if missing. Store the model configuration from config.model or use default.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test constructor with valid and invalid configurations, verify client initialization, test API key validation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement generateCompletion method with Claude API",
|
|
||||||
"description": "Implement the main generateCompletion method that calls Anthropic's Claude API and handles responses",
|
|
||||||
"dependencies": [
|
|
||||||
"120.2"
|
|
||||||
],
|
|
||||||
"details": "Implement async generateCompletion method accepting ChatMessage array. Map ChatMessage format to Anthropic's expected format (role and content). Use client.messages.create() with appropriate parameters including model, max_tokens, and messages. Transform Anthropic response format to ChatCompletion interface. Handle streaming vs non-streaming responses. Implement proper error handling wrapping API errors with context.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Mock Anthropic SDK client.messages.create, test with various message formats, verify response transformation, test error scenarios"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Implement token calculation and utility methods",
|
|
||||||
"description": "Implement calculateTokens method and other required abstract methods from BaseProvider",
|
|
||||||
"dependencies": [
|
|
||||||
"120.3"
|
|
||||||
],
|
|
||||||
"details": "Implement calculateTokens method using appropriate tokenizer (tiktoken or claude-tokenizer if available). Implement getName method returning 'anthropic' string constant. Implement getModel method returning current model from configuration. Implement getDefaultModel method returning 'claude-3-sonnet-20240229'. Add any additional helper methods for token counting or message formatting.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test token calculation accuracy with various input strings, verify utility methods return correct values"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Add comprehensive error handling and type exports",
|
|
||||||
"description": "Implement robust error handling throughout the class and ensure proper TypeScript exports",
|
|
||||||
"dependencies": [
|
|
||||||
"120.4"
|
|
||||||
],
|
|
||||||
"details": "Wrap all Anthropic API calls in try-catch blocks. Create custom error messages that include context about the operation being performed. Handle rate limiting errors specifically. Ensure all methods have proper TypeScript return types. Export the AnthropicProvider class as default export. Add JSDoc comments for all public methods. Ensure proper error propagation maintaining stack traces.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test various API error scenarios, verify error messages include context, test rate limit handling, ensure TypeScript types are correctly exported"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 121,
|
|
||||||
"title": "Create Prompt Builder and Task Parser",
|
|
||||||
"description": "Implement PromptBuilder class and TaskParser with Dependency Injection",
|
|
||||||
"details": "Create PromptBuilder class with buildParsePrompt and buildExpandPrompt methods using template literals. Include specific JSON format instructions. Create TaskParser class accepting IAIProvider and IConfiguration via constructor (Dependency Injection). Implement parsePRD method to read PRD file, use PromptBuilder to create prompt, call AI provider, extract tasks from response, and enrich with metadata. Handle parsing errors gracefully.",
|
|
||||||
"testStrategy": "Unit test prompt building with various inputs, mock AI provider responses, test JSON extraction logic, verify error handling for malformed responses, integration test with real PRD files",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [
|
|
||||||
119,
|
|
||||||
120
|
|
||||||
],
|
|
||||||
"status": "pending",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create PromptBuilder Class Structure",
|
|
||||||
"description": "Implement the PromptBuilder class with template methods for generating AI prompts",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/services/prompt-builder.ts. Define PromptBuilder class with two public methods: buildParsePrompt(prdContent: string): string and buildExpandPrompt(task: Task): string. Use template literals to construct prompts with clear JSON format instructions. Include system instructions for AI to follow specific output formats. Add private helper methods for common prompt sections like JSON schema definitions and response format examples.\n<info added on 2025-08-20T17:17:31.467Z>\nRefactor to src/services/prompts/prompt-builder.ts to separate concerns. Implement buildTaskPrompt() method. Define prompt template constants: PARSE_PROMPT_TEMPLATE, EXPAND_PROMPT_TEMPLATE, TASK_PROMPT_TEMPLATE, JSON_FORMAT_INSTRUCTIONS. Move JSON schema definitions and format instructions to constants. Ensure each template uses template literals with ${} placeholders. Keep all methods under 40 lines by extracting logic into focused helper methods. Use descriptive constant names for all repeated strings or instruction blocks.\n</info added on 2025-08-20T17:17:31.467Z>",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Unit test both prompt methods with sample inputs. Verify prompt contains required JSON structure instructions. Test edge cases like empty PRD content or minimal task objects."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement TaskParser Class with DI",
|
|
||||||
"description": "Create TaskParser class accepting IAIProvider and IConfiguration through constructor injection",
|
|
||||||
"dependencies": [
|
|
||||||
"121.1"
|
|
||||||
],
|
|
||||||
"details": "Create src/services/task-parser.ts. Define TaskParser class with constructor(private aiProvider: IAIProvider, private config: IConfiguration). Add private promptBuilder property initialized in constructor. Implement basic class structure with placeholder methods. Ensure proper TypeScript typing for all parameters and properties. Follow dependency injection pattern for testability.\n<info added on 2025-08-20T17:17:49.624Z>\nUpdate file location to src/services/tasks/task-parser.ts instead of src/services/task-parser.ts. Refactor parsePRD() method to stay under 40 lines by extracting logic into helper methods: readPRD(), validatePRD(), extractTasksFromResponse(), and enrichTasksWithMetadata(). Each helper method should be under 20 lines. Implement early returns in validation methods for cleaner code flow. Remove any file I/O operations from the parser class - delegate all storage operations to injected dependencies. Ensure clean separation of concerns with parser focused only on task parsing logic.\n</info added on 2025-08-20T17:17:49.624Z>",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test constructor properly stores injected dependencies. Verify class instantiation with mock providers. Test TypeScript compilation with proper interface implementations."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement parsePRD Method Core Logic",
|
|
||||||
"description": "Create the main parsePRD method that orchestrates the PRD parsing workflow",
|
|
||||||
"dependencies": [
|
|
||||||
"121.2"
|
|
||||||
],
|
|
||||||
"details": "Implement parsePRD(filePath: string): Promise<ParsedTask[]> method in TaskParser. Read PRD file using fs.promises.readFile. Use promptBuilder.buildParsePrompt() to create AI prompt. Call aiProvider.generateResponse() with constructed prompt. Extract JSON array from AI response using regex or JSON.parse. Handle potential parsing errors with try-catch blocks. Return empty array on errors after logging.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test with mock AI provider returning valid JSON. Test file reading with various file paths. Mock file system for controlled testing. Verify proper error logging without throwing."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Add Task Enrichment and Metadata",
|
|
||||||
"description": "Enhance parsed tasks with additional metadata and validation",
|
|
||||||
"dependencies": [
|
|
||||||
"121.3"
|
|
||||||
],
|
|
||||||
"details": "After extracting tasks from AI response, enrich each task with metadata: add createdAt timestamp, set initial status to 'pending', validate required fields (id, title, description). Add priority field with default 'medium' if not provided. Ensure all tasks have valid structure before returning. Create private enrichTask(task: any): ParsedTask method for this logic. Handle missing or malformed task data gracefully.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test enrichment adds all required metadata. Verify validation catches malformed tasks. Test default values are applied correctly. Ensure timestamps are properly formatted."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Implement Comprehensive Error Handling",
|
|
||||||
"description": "Add robust error handling throughout the TaskParser implementation",
|
|
||||||
"dependencies": [
|
|
||||||
"121.4"
|
|
||||||
],
|
|
||||||
"details": "Wrap file reading in try-catch to handle FILE_NOT_FOUND errors. Catch AI provider errors and wrap in appropriate TaskMasterError. Handle JSON parsing errors when extracting from AI response. Add specific error handling for network timeouts, rate limits, and malformed responses. Log errors with context in development mode only. Return meaningful error messages without exposing internals. Ensure all errors are properly typed as TaskMasterError instances.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test each error scenario separately: missing files, AI provider failures, malformed JSON, network errors. Verify proper error codes are used. Test that errors don't expose sensitive information."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 122,
|
|
||||||
"title": "Implement Configuration Management",
|
|
||||||
"description": "Create ConfigManager class with Zod validation for configuration",
|
|
||||||
"details": "Implement ConfigManager in config/config-manager.ts accepting Partial<IConfiguration> in constructor. Use Zod to create validation schema matching IConfiguration interface. Implement get method with TypeScript generics for type-safe access, getAll returning full config, validate method for validation. Set defaults: projectPath = process.cwd(), aiProvider = 'anthropic', enableTags = true. Handle validation errors with clear messages.",
|
|
||||||
"testStrategy": "Test with various configuration combinations, verify Zod validation catches invalid configs, test default values, ensure type safety of get method",
|
|
||||||
"priority": "medium",
|
|
||||||
"dependencies": [
|
|
||||||
116
|
|
||||||
],
|
|
||||||
"status": "in-progress",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create Zod validation schema for IConfiguration",
|
|
||||||
"description": "Define a Zod schema that matches the IConfiguration interface structure with proper validation rules",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create configSchema in config/config-manager.ts using z.object() to define validation for all IConfiguration properties. Include string validations for projectPath, enum validation for aiProvider ('anthropic', 'openai', etc.), boolean for enableTags, and any other configuration fields. Use z.string().min(1) for required strings, z.enum() for provider types, and appropriate validators for other fields.\n<info added on 2025-08-06T13:14:58.822Z>\nCompleted Zod validation schema implementation in packages/tm-core/src/config/validation.ts\n\nIMPLEMENTATION DETAILS:\n- Created comprehensive Zod schemas matching IConfiguration interface structure exactly\n- All required schemas exported as expected by config-schema.ts:\n * configurationSchema - Main configuration validation with custom refinements\n * partialConfigurationSchema - For partial updates (using base schema without refinements)\n * modelConfigSchema - Model configuration validation\n * providerConfigSchema - AI provider configuration validation \n * taskSettingsSchema - Task management settings validation\n * loggingSettingsSchema/loggingConfigSchema - Logging configuration (with legacy alias)\n * tagSettingsSchema - Tag management settings validation\n * storageSettingsSchema - Storage configuration validation\n * retrySettingsSchema - Retry/resilience settings validation\n * securitySettingsSchema - Security settings validation\n * cacheConfigSchema - Cache configuration stub (for consistency)\n\nKEY FEATURES:\n- Proper Zod validation rules applied (string lengths, number ranges, enums)\n- Custom refinements for business logic (maxRetryDelay >= retryDelay)\n- Comprehensive enum schemas for all union types\n- Legacy alias support for backwards compatibility\n- All 13 nested interface schemas implemented with appropriate constraints\n- Type exports for runtime validation\n\nVALIDATION INCLUDES:\n- String validations with min lengths for required fields\n- Enum validation for providers, priorities, complexities, log levels, etc.\n- Number range validations (min/max constraints)\n- URL validation for baseUrl fields\n- Array validations with proper item types\n- Record validations for dynamic key-value pairs\n- Optional field handling with appropriate defaults\n\nTESTED AND VERIFIED:\n- All schemas compile correctly with TypeScript\n- Import/export chain works properly through config-schema.ts\n- Basic validation tests pass for key schemas\n- No conflicts with existing IConfiguration interface structure\n</info added on 2025-08-06T13:14:58.822Z>\n<info added on 2025-08-20T17:18:12.343Z>\nCreated ConfigManager class at src/config/config-manager.ts with the following implementation:\n\nSTRUCTURE:\n- DEFAULT_CONFIG constant defined with complete default values for all configuration properties\n- Constructor validates config using validate() method (follows Fail-Fast principle)\n- Constructor kept under 15 lines as required\n- Type-safe get<K>() method using TypeScript generics for accessing specific config properties\n- getAll() method returns complete validated configuration\n- validate() method extracted for configuration validation using Zod schema\n- mergeWithDefaults() helper extracted for merging partial config with defaults\n\nKEY IMPLEMENTATION DETAILS:\n- Imports configurationSchema from src/config/schemas/config.schema.ts\n- Uses z.infer<typeof configurationSchema> for type safety\n- Validates on construction with clear error messages\n- No nested ternaries used\n- Proper error handling with ConfigValidationError\n- Type-safe property access with keyof IConfiguration constraint\n\nMETHODS:\n- constructor(config?: Partial<IConfiguration>) - Validates and stores config\n- get<K extends keyof IConfiguration>(key: K): IConfiguration[K] - Type-safe getter\n- getAll(): IConfiguration - Returns full config\n- private validate(config: unknown): IConfiguration - Validates using Zod\n- private mergeWithDefaults(config: Partial<IConfiguration>): IConfiguration - Merges with defaults\n\nAlso created src/config/schemas/config.schema.ts importing the configurationSchema from validation.ts for cleaner organization.\n</info added on 2025-08-20T17:18:12.343Z>",
|
|
||||||
"status": "review",
|
|
||||||
"testStrategy": "Test schema validation with valid and invalid configurations, ensure all IConfiguration fields are covered"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement ConfigManager class constructor and storage",
|
|
||||||
"description": "Create ConfigManager class with constructor that accepts Partial<IConfiguration> and initializes configuration with defaults",
|
|
||||||
"dependencies": [
|
|
||||||
"122.1"
|
|
||||||
],
|
|
||||||
"details": "Define ConfigManager class with private config property. In constructor, merge provided partial config with defaults (projectPath = process.cwd(), aiProvider = 'anthropic', enableTags = true). Store the merged configuration internally. Ensure the class is properly typed with IConfiguration interface.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test constructor with various partial configs, verify defaults are applied correctly, test with empty config"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement validate method with error handling",
|
|
||||||
"description": "Create validate method that uses Zod schema to validate configuration and provides clear error messages",
|
|
||||||
"dependencies": [
|
|
||||||
"122.1",
|
|
||||||
"122.2"
|
|
||||||
],
|
|
||||||
"details": "Implement validate(): void method that runs configSchema.parse(this.config) within try-catch block. On ZodError, transform the error into user-friendly messages that clearly indicate which fields are invalid and why. Consider creating a custom error class for configuration validation errors. The method should throw if validation fails.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test with invalid configs to ensure proper error messages, verify all validation rules work correctly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Implement type-safe get method with generics",
|
|
||||||
"description": "Create generic get method for retrieving individual configuration values with TypeScript type inference",
|
|
||||||
"dependencies": [
|
|
||||||
"122.2"
|
|
||||||
],
|
|
||||||
"details": "Implement get<K extends keyof IConfiguration>(key: K): IConfiguration[K] method that returns the value for a specific configuration key. Use TypeScript generics and keyof operator to ensure type safety. The method should provide proper type inference so consumers get the correct type based on the key they request.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test type inference with different keys, verify TypeScript catches invalid keys at compile time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Implement getAll method and finalize class",
|
|
||||||
"description": "Create getAll method to return full configuration and ensure proper exports",
|
|
||||||
"dependencies": [
|
|
||||||
"122.2",
|
|
||||||
"122.3",
|
|
||||||
"122.4"
|
|
||||||
],
|
|
||||||
"details": "Implement getAll(): IConfiguration method that returns a deep copy of the entire configuration object to prevent external mutations. Add JSDoc comments to all public methods. Export the ConfigManager class and ensure it's properly integrated with the module structure. Consider adding a static factory method if needed.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test getAll returns complete config, verify returned object is immutable, test integration with other modules"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"title": "Create Utility Functions and Error Handling",
|
|
||||||
"description": "Implement ID generation utilities and custom error classes",
|
|
||||||
"details": "Create id-generator.ts with generateTaskId and generateSubtaskId functions using specified formats with timestamp and random components. Create TaskMasterError class extending Error in errors/task-master-error.ts with error codes (FILE_NOT_FOUND, PARSE_ERROR, etc.). Ensure errors don't expose internal details. Add development-only logging.",
|
|
||||||
"testStrategy": "Test ID generation for uniqueness and format compliance, verify error classes properly extend Error, test error message formatting, ensure no sensitive data in errors",
|
|
||||||
"priority": "low",
|
|
||||||
"dependencies": [
|
|
||||||
116
|
|
||||||
],
|
|
||||||
"status": "in-progress",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create ID generation utilities module",
|
|
||||||
"description": "Implement the id-generator.ts module with functions for generating unique task and subtask IDs",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/utils/id-generator.ts file. Implement generateTaskId() function that returns format 'TASK-{timestamp}-{random}' where timestamp is Date.now() and random is 4-character alphanumeric string. Implement generateSubtaskId(parentId) function that returns format '{parentId}.{sequential}' where sequential increments based on existing subtasks. Use crypto.randomBytes or Math.random for randomness. Export both functions as named exports.\n<info added on 2025-08-06T12:42:22.203Z>\nThe ID generator module has been successfully implemented with the following completed features:\n- generateTaskId() function that creates unique IDs in 'TASK-{timestamp}-{random}' format\n- generateSubtaskId() function that generates sequential subtask IDs in '{parentId}.{sequential}' format\n- Input validation functions to ensure ID integrity\n- Proper TypeScript type definitions and interfaces\n- Comprehensive JSDoc documentation with usage examples\n- All functions exported as named exports from src/utils/id-generator.ts\n</info added on 2025-08-06T12:42:22.203Z>",
|
|
||||||
"status": "done",
|
|
||||||
"testStrategy": "Test uniqueness by generating 1000 IDs and checking for duplicates, verify format compliance with regex, test subtask ID sequential numbering"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Create base error class structure",
|
|
||||||
"description": "Implement the TaskMasterError base class that extends Error with proper error handling capabilities",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/errors/task-master-error.ts file. Define TaskMasterError class extending Error. Add constructor accepting (message: string, code: string, details?: any). Set this.name = 'TaskMasterError'. Create error code constants: FILE_NOT_FOUND = 'FILE_NOT_FOUND', PARSE_ERROR = 'PARSE_ERROR', VALIDATION_ERROR = 'VALIDATION_ERROR', API_ERROR = 'API_ERROR'. Override toString() to format errors appropriately. Ensure stack trace is preserved.\n<info added on 2025-08-06T13:13:11.635Z>\nCompleted TaskMasterError base class implementation:\n\nIMPLEMENTATION DETAILS:\n- TaskMasterError class fully implemented extending Error\n- Added proper prototype chain fix with Object.setPrototypeOf(this, TaskMasterError.prototype)\n- Includes all required properties: code (from ERROR_CODES), timestamp, context, cause\n- toJSON method implemented for full serialization support\n- Error sanitization implemented via getSanitizedDetails() and containsSensitiveInfo() methods\n- Error chaining with cause property fully supported\n- Additional utility methods: getUserMessage(), toString(), is(), hasCode(), withContext(), wrap()\n\nSUCCESS CRITERIA VERIFIED:\n✅ TaskMasterError class fully implemented\n✅ Extends Error with proper prototype chain fix (Object.setPrototypeOf)\n✅ Includes all required properties and methods\n✅ toJSON method for serialization\n✅ Error sanitization logic for production (containsSensitiveInfo method)\n✅ Comprehensive error context and metadata support\n\nFILE MODIFIED: packages/tm-core/src/errors/task-master-error.ts\n</info added on 2025-08-06T13:13:11.635Z>\n<info added on 2025-08-20T17:18:38.499Z>\nRefactored to follow clean code principles:\n\nCLEAN CODE IMPROVEMENTS:\n- Moved TaskMasterError class to be under 40 lines by extracting methods\n- Created separate error-codes.ts file with ERROR_CODES constant object\n- Extracted sanitizeMessage() method to handle message sanitization\n- Extracted addContext() method for adding error context\n- Extracted toJSON() method for serialization\n- Added static factory methods: fromError(), notFound(), parseError(), validationError(), apiError()\n- Improved error chaining with proper 'cause' property handling\n- Ensured user-friendly messages that hide implementation details\n- Maintained all existing functionality while improving code organization\n\nFILES CREATED/MODIFIED:\n- packages/tm-core/src/errors/error-codes.ts (new file with ERROR_CODES)\n- packages/tm-core/src/errors/task-master-error.ts (refactored to under 40 lines)\n</info added on 2025-08-20T17:18:38.499Z>",
|
|
||||||
"status": "review",
|
|
||||||
"testStrategy": "Test that error extends Error properly, verify error.name is set correctly, test toString() output format, ensure stack trace exists"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement error sanitization and security features",
|
|
||||||
"description": "Add security features to prevent exposure of sensitive internal details in error messages",
|
|
||||||
"dependencies": [
|
|
||||||
"123.2"
|
|
||||||
],
|
|
||||||
"details": "In TaskMasterError class, add private sanitizeDetails() method that removes sensitive data like API keys, file paths beyond project root, and internal state. Implement toJSON() method that returns sanitized error object for external consumption. Add static isSafeForProduction() method to validate error messages don't contain patterns like absolute paths, environment variables, or API credentials. Store original details in private property for debugging.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test sanitization removes absolute paths, API keys, and sensitive patterns, verify toJSON returns safe object, test original details are preserved internally"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Add development-only logging functionality",
|
|
||||||
"description": "Implement conditional logging that only operates in development environment",
|
|
||||||
"dependencies": [
|
|
||||||
"123.2",
|
|
||||||
"123.3"
|
|
||||||
],
|
|
||||||
"details": "In task-master-error.ts, add static enableDevLogging property defaulting to process.env.NODE_ENV !== 'production'. Add logError() method that console.error's full error details only when enableDevLogging is true. Include timestamp, error code, sanitized message, and full stack trace in dev logs. In production, log only error code and safe message. Create static setDevLogging(enabled: boolean) to control logging.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test logging output in dev vs production modes, verify sensitive data isn't logged in production, test log format includes all required fields"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Create specialized error subclasses",
|
|
||||||
"description": "Implement specific error classes for different error scenarios inheriting from TaskMasterError",
|
|
||||||
"dependencies": [
|
|
||||||
"123.2",
|
|
||||||
"123.3",
|
|
||||||
"123.4"
|
|
||||||
],
|
|
||||||
"details": "Create FileNotFoundError extending TaskMasterError with code FILE_NOT_FOUND, accepting filePath parameter. Create ParseError with code PARSE_ERROR for parsing failures, accepting source and line number. Create ValidationError with code VALIDATION_ERROR for data validation, accepting field and value. Create APIError with code API_ERROR for external API failures, accepting statusCode and provider. Each should format appropriate user-friendly messages while storing technical details internally.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test each error class constructor and message formatting, verify inheritance chain, test that each error type has correct code, ensure specialized errors work with logging system"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 124,
|
|
||||||
"title": "Implement TaskMasterCore Facade",
|
|
||||||
"description": "Create main TaskMasterCore class as Facade pattern entry point",
|
|
||||||
"details": "Create TaskMasterCore class in index.ts with private properties for config, storage, aiProvider, and parser. Implement initialize method for lazy loading of AI provider. Implement parsePRD method that coordinates parser, storage, and configuration. Implement getTasks for retrieving stored tasks. Apply Facade pattern to hide complexity. Export createTaskMaster factory function, all types and interfaces. Use proper import paths with .js extensions for ESM.",
|
|
||||||
"testStrategy": "Integration test full parse flow, test lazy initialization, verify facade properly delegates to subsystems, test with different configurations, ensure exports are correct",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [
|
|
||||||
117,
|
|
||||||
121,
|
|
||||||
122
|
|
||||||
],
|
|
||||||
"status": "pending",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create TaskMasterCore class structure with type definitions",
|
|
||||||
"description": "Set up the main TaskMasterCore class in src/index.ts with all necessary imports, type definitions, and class structure following the Facade pattern",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/index.ts file. Import IConfiguration, ITaskStorage, IAIProvider, and IPRDParser interfaces. Define TaskMasterCore class with private properties: _config (ConfigManager), _storage (ITaskStorage), _aiProvider (IAIProvider | null), _parser (IPRDParser | null). Add constructor accepting options parameter of type Partial<IConfiguration>. Initialize _config with ConfigManager, set other properties to null for lazy loading. Import all necessary types from their respective modules using .js extensions for ESM compatibility.\n<info added on 2025-08-20T17:18:56.625Z>\nApply Facade pattern principles: simple public interface, hide subsystem complexity. Keep all methods under 30 lines by extracting logic. Implement lazy initialization pattern in initialize() method - only create dependencies when first needed. Extract createDependencies() private helper method to handle creation of storage, AI provider, and parser instances. Add createTaskMaster() factory function for convenient instance creation. Use barrel exports pattern - export all public types and interfaces that clients need (IConfiguration, ITaskStorage, IAIProvider, IPRDParser, TaskMasterCore). Follow Interface Segregation Principle - only expose methods and types that clients actually need, hide internal implementation details.\n</info added on 2025-08-20T17:18:56.625Z>",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test class instantiation with various configuration options, verify private properties are correctly initialized, ensure TypeScript types are properly enforced"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Implement initialize method for lazy loading",
|
|
||||||
"description": "Create the initialize method that handles lazy loading of AI provider and parser instances based on configuration",
|
|
||||||
"dependencies": [
|
|
||||||
"124.1"
|
|
||||||
],
|
|
||||||
"details": "Implement async initialize() method in TaskMasterCore. Check if _aiProvider is null, if so create appropriate provider based on config.aiProvider value using a factory pattern or switch statement. Similarly initialize _parser if null. Store instances in private properties for reuse. Handle provider initialization errors gracefully. Ensure method is idempotent - calling multiple times should not recreate instances. Use dynamic imports if needed for code splitting.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test lazy initialization occurs only once, verify correct provider is instantiated based on config, test error handling for invalid providers, ensure idempotency"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement parsePRD method with coordination logic",
|
|
||||||
"description": "Create parsePRD method that coordinates the parser, AI provider, and storage to parse PRD content and store results",
|
|
||||||
"dependencies": [
|
|
||||||
"124.1",
|
|
||||||
"124.2"
|
|
||||||
],
|
|
||||||
"details": "Implement async parsePRD(content: string) method. First call initialize() to ensure components are loaded. Use _parser.parse() to parse the PRD content, passing the AI provider for task generation. Take the parsed tasks and use _storage.saveTasks() to persist them. Handle errors from parser or storage operations. Return the parsed tasks array. Implement proper error context and logging for debugging.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Integration test with mock parser and storage, verify coordination between components, test error propagation from subsystems, ensure tasks are properly stored"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Implement getTasks method and other facade methods",
|
|
||||||
"description": "Create getTasks method and any other necessary facade methods to retrieve and manage tasks",
|
|
||||||
"dependencies": [
|
|
||||||
"124.1"
|
|
||||||
],
|
|
||||||
"details": "Implement async getTasks() method that calls _storage.loadTasks() and returns the tasks array. Add getTask(id: string) for retrieving single task. Consider adding updateTask, deleteTask methods if needed. All methods should follow facade pattern - simple interface hiding complex operations. Add proper TypeScript return types for all methods. Handle storage not initialized scenarios.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test task retrieval with various scenarios, verify proper delegation to storage, test edge cases like empty task lists or invalid IDs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Create factory function and module exports",
|
|
||||||
"description": "Implement createTaskMaster factory function and set up all module exports including types and interfaces",
|
|
||||||
"dependencies": [
|
|
||||||
"124.1",
|
|
||||||
"124.2",
|
|
||||||
"124.3",
|
|
||||||
"124.4"
|
|
||||||
],
|
|
||||||
"details": "Create createTaskMaster(options?: Partial<IConfiguration>) factory function that returns a new TaskMasterCore instance. Export this as the primary entry point. Re-export all types and interfaces from submodules: ITask, IConfiguration, IAIProvider, ITaskStorage, IPRDParser, etc. Use 'export type' for type-only exports. Ensure all imports use .js extensions for ESM. Create index.d.ts if needed for better TypeScript support. Add JSDoc comments for public API.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test factory function creates proper instances, verify all exports are accessible, test TypeScript type inference works correctly, ensure ESM imports resolve properly"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 125,
|
|
||||||
"title": "Create Placeholder Providers and Complete Testing",
|
|
||||||
"description": "Implement placeholder providers for OpenAI and Google, create comprehensive test suite",
|
|
||||||
"details": "Create OpenAIProvider and GoogleProvider classes extending BaseProvider, throwing 'not yet implemented' errors. Create MockProvider in tests/mocks for testing without API calls. Write unit tests for TaskParser, integration tests for parse-prd flow, ensure 80% code coverage. Follow kebab-case naming for test files. Test error scenarios comprehensively.",
|
|
||||||
"testStrategy": "Run full test suite with coverage report, verify all edge cases are tested, ensure mock provider behaves like real providers, test both success and failure paths",
|
|
||||||
"priority": "medium",
|
|
||||||
"dependencies": [
|
|
||||||
120,
|
|
||||||
124
|
|
||||||
],
|
|
||||||
"status": "pending",
|
|
||||||
"subtasks": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Create OpenAIProvider placeholder class",
|
|
||||||
"description": "Implement OpenAIProvider class that extends BaseProvider with all required methods throwing 'not yet implemented' errors",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/providers/openai-provider.ts file. Import BaseProvider from base-provider.ts. Implement class OpenAIProvider extends BaseProvider. Override parseText() method to throw new Error('OpenAI provider not yet implemented'). Add proper TypeScript types and JSDoc comments. Export the class as default.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Write unit test to verify OpenAIProvider extends BaseProvider correctly and throws expected error when parseText is called"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Create GoogleProvider placeholder class",
|
|
||||||
"description": "Implement GoogleProvider class that extends BaseProvider with all required methods throwing 'not yet implemented' errors",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create src/providers/google-provider.ts file. Import BaseProvider from base-provider.ts. Implement class GoogleProvider extends BaseProvider. Override parseText() method to throw new Error('Google provider not yet implemented'). Add proper TypeScript types and JSDoc comments. Export the class as default.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Write unit test to verify GoogleProvider extends BaseProvider correctly and throws expected error when parseText is called"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Create MockProvider for testing",
|
|
||||||
"description": "Implement MockProvider class in tests/mocks directory that simulates provider behavior without making actual API calls",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create tests/mocks/mock-provider.ts file. Extend BaseProvider class. Implement parseText() to return predefined mock task data based on input. Add methods to configure mock responses, simulate errors, and track method calls. Include delay simulation for realistic testing. Export class and helper functions for test setup.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Test MockProvider returns consistent mock data, can simulate different scenarios (success/failure), and properly tracks method invocations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "Write unit tests for TaskParser",
|
|
||||||
"description": "Create comprehensive unit tests for TaskParser class covering all methods and edge cases",
|
|
||||||
"dependencies": [
|
|
||||||
"125.3"
|
|
||||||
],
|
|
||||||
"details": "Create tests/unit/task-parser.test.ts file. Test TaskParser constructor with different providers. Test parseFromText method with valid/invalid inputs. Test error handling for malformed responses. Use MockProvider to simulate API responses. Test task ID generation and structure validation. Ensure all public methods are covered.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Achieve 100% code coverage for TaskParser class, test both success and failure paths, verify error messages are appropriate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Write integration tests for parse-prd flow",
|
|
||||||
"description": "Create end-to-end integration tests for the complete PRD parsing workflow",
|
|
||||||
"dependencies": [
|
|
||||||
"125.3",
|
|
||||||
"125.4"
|
|
||||||
],
|
|
||||||
"details": "Create tests/integration/parse-prd-flow.test.ts file. Test full flow from PRD input to task output. Test with MockProvider simulating successful parsing. Test error scenarios (file not found, parse errors, network failures). Test task dependency resolution. Verify output format matches expected structure. Test with different PRD formats and sizes.",
|
|
||||||
"status": "pending",
|
|
||||||
"testStrategy": "Run coverage report to ensure 80% overall coverage, verify all critical paths are tested, ensure tests are deterministic and don't depend on external services"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"created": "2025-08-06T08:51:19.649Z",
|
|
||||||
"updated": "2025-08-20T21:32:21.837Z",
|
|
||||||
"description": "Tasks for tm-core-phase-1 context"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@@ -10,18 +10,5 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"json.format.enable": true,
|
"json.format.enable": true,
|
||||||
"json.validate.enable": true,
|
"json.validate.enable": true
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
206
CHANGELOG.md
206
CHANGELOG.md
@@ -1,211 +1,5 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
## 0.26.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1133](https://github.com/eyaltoledano/claude-task-master/pull/1133) [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144) Thanks [@neonwatty](https://github.com/neonwatty)! - Restore Taskmaster claude-code commands and move clear commands under /remove to avoid collision with the claude-code /clear command.
|
|
||||||
|
|
||||||
- [#1163](https://github.com/eyaltoledano/claude-task-master/pull/1163) [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Gemini CLI provider with codebase-aware task generation
|
|
||||||
|
|
||||||
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
|
|
||||||
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
|
|
||||||
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
|
|
||||||
|
|
||||||
- [#1165](https://github.com/eyaltoledano/claude-task-master/pull/1165) [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add configurable codebase analysis feature flag with multiple configuration sources
|
|
||||||
|
|
||||||
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
|
|
||||||
|
|
||||||
Priority order: .env > MCP session env > .taskmaster/config.json.
|
|
||||||
|
|
||||||
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.
|
|
||||||
|
|
||||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - feat(move): improve cross-tag move UX and safety
|
|
||||||
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
|
|
||||||
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
|
|
||||||
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
|
|
||||||
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
|
|
||||||
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
- [#1162](https://github.com/eyaltoledano/claude-task-master/pull/1162) [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
|
|
||||||
|
|
||||||
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
|
|
||||||
|
|
||||||
Commands contextualised:
|
|
||||||
- add-task
|
|
||||||
- update-subtask
|
|
||||||
- update-task
|
|
||||||
- update
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
|
||||||
|
|
||||||
## 0.26.0-rc.1
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1165](https://github.com/eyaltoledano/claude-task-master/pull/1165) [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add configurable codebase analysis feature flag with multiple configuration sources
|
|
||||||
|
|
||||||
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
|
|
||||||
|
|
||||||
Priority order: .env > MCP session env > .taskmaster/config.json.
|
|
||||||
|
|
||||||
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.
|
|
||||||
|
|
||||||
## 0.26.0-rc.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1163](https://github.com/eyaltoledano/claude-task-master/pull/1163) [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Gemini CLI provider with codebase-aware task generation
|
|
||||||
|
|
||||||
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
|
|
||||||
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
|
|
||||||
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
|
|
||||||
|
|
||||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - feat(move): improve cross-tag move UX and safety
|
|
||||||
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
|
|
||||||
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
|
|
||||||
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
|
|
||||||
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
|
|
||||||
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
- [#1162](https://github.com/eyaltoledano/claude-task-master/pull/1162) [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
|
|
||||||
|
|
||||||
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
|
|
||||||
|
|
||||||
Commands contextualised:
|
|
||||||
- add-task
|
|
||||||
- update-subtask
|
|
||||||
- update-task
|
|
||||||
- update
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
|
||||||
|
|
||||||
## 0.25.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1152](https://github.com/eyaltoledano/claude-task-master/pull/1152) [`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b) Thanks [@ben-vargas](https://github.com/ben-vargas)! - fix(claude-code): prevent crash/hang when the optional `@anthropic-ai/claude-code` SDK is missing by guarding `AbortError instanceof` checks and adding explicit SDK presence checks in `doGenerate`/`doStream`. Also bump the optional dependency to `^1.0.88` for improved export consistency.
|
|
||||||
|
|
||||||
Related to JSON truncation handling in #920; this change addresses a separate error-path crash reported in #1142.
|
|
||||||
|
|
||||||
- [#1151](https://github.com/eyaltoledano/claude-task-master/pull/1151) [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Temporarily disable streaming for improved model compatibility - will be re-enabled in upcoming release
|
|
||||||
|
|
||||||
## 0.25.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Add cross-tag task movement functionality for organizing tasks across different contexts.
|
|
||||||
|
|
||||||
This feature enables moving tasks between different tags (contexts) in your project, making it easier to organize work across different branches, environments, or project phases.
|
|
||||||
|
|
||||||
## CLI Usage Examples
|
|
||||||
|
|
||||||
Move a single task from one tag to another:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Move task 5 from backlog tag to in-progress tag
|
|
||||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-1
|
|
||||||
|
|
||||||
# Move task with its dependencies
|
|
||||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-2 --with-dependencies
|
|
||||||
|
|
||||||
# Move task without checking dependencies
|
|
||||||
task-master move --from=5 --from-tag=backlog --to-tag=bug-3 --ignore-dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
Move multiple tasks at once:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Move multiple tasks between tags
|
|
||||||
task-master move --from=5,6,7 --from-tag=backlog --to-tag=bug-4 --with-dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
- [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
|
|
||||||
|
|
||||||
- [#1054](https://github.com/eyaltoledano/claude-task-master/pull/1054) [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573) Thanks [@martincik](https://github.com/martincik)! - Add compact mode --compact / -c flag to the `tm list` CLI command
|
|
||||||
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
|
||||||
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
|
||||||
- Color-coded status, priority, and dependencies
|
|
||||||
- Smart title truncation and dependency abbreviation
|
|
||||||
- Subtask support with indentation
|
|
||||||
- Full backward compatibility with existing list options
|
|
||||||
|
|
||||||
- [#1048](https://github.com/eyaltoledano/claude-task-master/pull/1048) [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d) Thanks [@joedanz](https://github.com/joedanz)! - Add CLI & MCP progress tracking for parse-prd command.
|
|
||||||
|
|
||||||
- [#1124](https://github.com/eyaltoledano/claude-task-master/pull/1124) [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add support for ollama `gpt-oss:20b` and `gpt-oss:120b`
|
|
||||||
|
|
||||||
- [#1123](https://github.com/eyaltoledano/claude-task-master/pull/1123) [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1131](https://github.com/eyaltoledano/claude-task-master/pull/1131) [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e) Thanks [@joedanz](https://github.com/joedanz)! - Update Cursor one-click install link to new URL format
|
|
||||||
|
|
||||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Fix `add-tag --from-branch` command error where `projectRoot` was not properly referenced
|
|
||||||
|
|
||||||
The command was failing with "projectRoot is not defined" error because the code was directly referencing `projectRoot` instead of `context.projectRoot` in the git repository checks. This fix corrects the variable references to use the proper context object.
|
|
||||||
|
|
||||||
## 0.25.0-rc.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Add cross-tag task movement functionality for organizing tasks across different contexts.
|
|
||||||
|
|
||||||
This feature enables moving tasks between different tags (contexts) in your project, making it easier to organize work across different branches, environments, or project phases.
|
|
||||||
|
|
||||||
## CLI Usage Examples
|
|
||||||
|
|
||||||
Move a single task from one tag to another:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Move task 5 from backlog tag to in-progress tag
|
|
||||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-1
|
|
||||||
|
|
||||||
# Move task with its dependencies
|
|
||||||
task-master move --from=5 --from-tag=backlog --to-tag=feature-2 --with-dependencies
|
|
||||||
|
|
||||||
# Move task without checking dependencies
|
|
||||||
task-master move --from=5 --from-tag=backlog --to-tag=bug-3 --ignore-dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
Move multiple tasks at once:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Move multiple tasks between tags
|
|
||||||
task-master move --from=5,6,7 --from-tag=backlog --to-tag=bug-4 --with-dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
- [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
|
|
||||||
|
|
||||||
- [#1054](https://github.com/eyaltoledano/claude-task-master/pull/1054) [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573) Thanks [@martincik](https://github.com/martincik)! - Add compact mode --compact / -c flag to the `tm list` CLI command
|
|
||||||
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
|
||||||
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
|
||||||
- Color-coded status, priority, and dependencies
|
|
||||||
- Smart title truncation and dependency abbreviation
|
|
||||||
- Subtask support with indentation
|
|
||||||
- Full backward compatibility with existing list options
|
|
||||||
|
|
||||||
- [#1048](https://github.com/eyaltoledano/claude-task-master/pull/1048) [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d) Thanks [@joedanz](https://github.com/joedanz)! - Add CLI & MCP progress tracking for parse-prd command.
|
|
||||||
|
|
||||||
- [#1124](https://github.com/eyaltoledano/claude-task-master/pull/1124) [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add support for ollama `gpt-oss:20b` and `gpt-oss:120b`
|
|
||||||
|
|
||||||
- [#1123](https://github.com/eyaltoledano/claude-task-master/pull/1123) [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1131](https://github.com/eyaltoledano/claude-task-master/pull/1131) [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e) Thanks [@joedanz](https://github.com/joedanz)! - Update Cursor one-click install link to new URL format
|
|
||||||
|
|
||||||
- [#1088](https://github.com/eyaltoledano/claude-task-master/pull/1088) [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8) Thanks [@mm-parthy](https://github.com/mm-parthy)! - Fix `add-tag --from-branch` command error where `projectRoot` was not properly referenced
|
|
||||||
|
|
||||||
The command was failing with "projectRoot is not defined" error because the code was directly referencing `projectRoot` instead of `context.projectRoot` in the git repository checks. This fix corrects the variable references to use the proper context object.
|
|
||||||
|
|
||||||
## 0.24.0
|
## 0.24.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ The following documentation is also available in the `docs` directory:
|
|||||||
|
|
||||||
#### Quick Install for Cursor 1.0+ (One-Click)
|
#### Quick Install for Cursor 1.0+ (One-Click)
|
||||||
|
|
||||||
[](https://cursor.com/en/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D)
|
[](https://cursor.com/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D)
|
||||||
|
|
||||||
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# @tm/cli
|
|
||||||
|
|
||||||
## 1.1.0-rc.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`cd90b4d`](https://github.com/eyaltoledano/claude-task-master/commit/cd90b4d65fc2f04bdad9fb73aba320b58a124240) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@tm/cli",
|
|
||||||
"version": "0.26.0",
|
|
||||||
"description": "Task Master CLI - Command line interface for task management",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"types": "./src/index.ts",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"files": ["dist", "README.md"],
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"lint": "biome check src",
|
|
||||||
"format": "biome format --write src",
|
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"test:coverage": "vitest run --coverage",
|
|
||||||
"test:unit": "vitest run -t unit",
|
|
||||||
"test:integration": "vitest run -t integration",
|
|
||||||
"test:e2e": "vitest run --dir tests/e2e",
|
|
||||||
"test:ci": "vitest run --coverage --reporter=dot"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tm/core": "*",
|
|
||||||
"boxen": "^7.1.1",
|
|
||||||
"chalk": "5.6.2",
|
|
||||||
"cli-table3": "^0.6.5",
|
|
||||||
"commander": "^12.1.0",
|
|
||||||
"inquirer": "^9.2.10",
|
|
||||||
"ora": "^8.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^1.9.4",
|
|
||||||
"@types/inquirer": "^9.0.3",
|
|
||||||
"@types/node": "^22.10.5",
|
|
||||||
"tsx": "^4.20.4",
|
|
||||||
"typescript": "^5.7.3",
|
|
||||||
"vitest": "^2.1.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"keywords": ["task-master", "cli", "task-management", "productivity"],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"typesVersions": {
|
|
||||||
"*": {
|
|
||||||
"*": ["src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Auth command using Commander's native class pattern
|
|
||||||
* Extends Commander.Command for better integration with the framework
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import inquirer from 'inquirer';
|
|
||||||
import ora, { type Ora } from 'ora';
|
|
||||||
import open from 'open';
|
|
||||||
import {
|
|
||||||
AuthManager,
|
|
||||||
AuthenticationError,
|
|
||||||
type AuthCredentials
|
|
||||||
} from '@tm/core/auth';
|
|
||||||
import * as ui from '../utils/ui.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type from auth command
|
|
||||||
*/
|
|
||||||
export interface AuthResult {
|
|
||||||
success: boolean;
|
|
||||||
action: 'login' | 'logout' | 'status' | 'refresh';
|
|
||||||
credentials?: AuthCredentials;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthCommand extending Commander's Command class
|
|
||||||
* This is a thin presentation layer over @tm/core's AuthManager
|
|
||||||
*/
|
|
||||||
export class AuthCommand extends Command {
|
|
||||||
private authManager: AuthManager;
|
|
||||||
private lastResult?: AuthResult;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name || 'auth');
|
|
||||||
|
|
||||||
// Initialize auth manager
|
|
||||||
this.authManager = AuthManager.getInstance();
|
|
||||||
|
|
||||||
// Configure the command with subcommands
|
|
||||||
this.description('Manage authentication with tryhamster.com');
|
|
||||||
|
|
||||||
// Add subcommands
|
|
||||||
this.addLoginCommand();
|
|
||||||
this.addLogoutCommand();
|
|
||||||
this.addStatusCommand();
|
|
||||||
this.addRefreshCommand();
|
|
||||||
|
|
||||||
// Default action shows help
|
|
||||||
this.action(() => {
|
|
||||||
this.help();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add login subcommand
|
|
||||||
*/
|
|
||||||
private addLoginCommand(): void {
|
|
||||||
this.command('login')
|
|
||||||
.description('Authenticate with tryhamster.com')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeLogin();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add logout subcommand
|
|
||||||
*/
|
|
||||||
private addLogoutCommand(): void {
|
|
||||||
this.command('logout')
|
|
||||||
.description('Logout and clear credentials')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeLogout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add status subcommand
|
|
||||||
*/
|
|
||||||
private addStatusCommand(): void {
|
|
||||||
this.command('status')
|
|
||||||
.description('Display authentication status')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeStatus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add refresh subcommand
|
|
||||||
*/
|
|
||||||
private addRefreshCommand(): void {
|
|
||||||
this.command('refresh')
|
|
||||||
.description('Refresh authentication token')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeRefresh();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute login command
|
|
||||||
*/
|
|
||||||
private async executeLogin(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const result = await this.performInteractiveAuth();
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit cleanly after successful authentication
|
|
||||||
// Small delay to ensure all output is flushed
|
|
||||||
setTimeout(() => {
|
|
||||||
process.exit(0);
|
|
||||||
}, 100);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute logout command
|
|
||||||
*/
|
|
||||||
private async executeLogout(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const result = await this.performLogout();
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute status command
|
|
||||||
*/
|
|
||||||
private async executeStatus(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const result = this.displayStatus();
|
|
||||||
this.setLastResult(result);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute refresh command
|
|
||||||
*/
|
|
||||||
private async executeRefresh(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const result = await this.refreshToken();
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display authentication status
|
|
||||||
*/
|
|
||||||
private displayStatus(): AuthResult {
|
|
||||||
const credentials = this.authManager.getCredentials();
|
|
||||||
|
|
||||||
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
|
||||||
|
|
||||||
if (credentials) {
|
|
||||||
console.log(chalk.green('✓ Authenticated'));
|
|
||||||
console.log(chalk.gray(` Email: ${credentials.email || 'N/A'}`));
|
|
||||||
console.log(chalk.gray(` User ID: ${credentials.userId}`));
|
|
||||||
console.log(
|
|
||||||
chalk.gray(` Token Type: ${credentials.tokenType || 'standard'}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (credentials.expiresAt) {
|
|
||||||
const expiresAt = new Date(credentials.expiresAt);
|
|
||||||
const now = new Date();
|
|
||||||
const hoursRemaining = Math.floor(
|
|
||||||
(expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hoursRemaining > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(
|
|
||||||
` Expires: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(` Token expired at: ${expiresAt.toLocaleString()}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(chalk.gray(' Expires: Never (API key)'));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.gray(` Saved: ${new Date(credentials.savedAt).toLocaleString()}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'status',
|
|
||||||
credentials,
|
|
||||||
message: 'Authenticated'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log(chalk.yellow('✗ Not authenticated'));
|
|
||||||
console.log(
|
|
||||||
chalk.gray('\n Run "task-master auth login" to authenticate')
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'status',
|
|
||||||
message: 'Not authenticated'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform logout
|
|
||||||
*/
|
|
||||||
private async performLogout(): Promise<AuthResult> {
|
|
||||||
try {
|
|
||||||
await this.authManager.logout();
|
|
||||||
ui.displaySuccess('Successfully logged out');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'logout',
|
|
||||||
message: 'Successfully logged out'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const message = `Failed to logout: ${(error as Error).message}`;
|
|
||||||
ui.displayError(message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'logout',
|
|
||||||
message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh authentication token
|
|
||||||
*/
|
|
||||||
private async refreshToken(): Promise<AuthResult> {
|
|
||||||
const spinner = ora('Refreshing authentication token...').start();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const credentials = await this.authManager.refreshToken();
|
|
||||||
spinner.succeed('Token refreshed successfully');
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.gray(
|
|
||||||
` New expiration: ${credentials.expiresAt ? new Date(credentials.expiresAt).toLocaleString() : 'Never'}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'refresh',
|
|
||||||
credentials,
|
|
||||||
message: 'Token refreshed successfully'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
spinner.fail('Failed to refresh token');
|
|
||||||
|
|
||||||
if ((error as AuthenticationError).code === 'NO_REFRESH_TOKEN') {
|
|
||||||
ui.displayWarning(
|
|
||||||
'No refresh token available. Please re-authenticate.'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ui.displayError(`Refresh failed: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'refresh',
|
|
||||||
message: `Failed to refresh: ${(error as Error).message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform interactive authentication
|
|
||||||
*/
|
|
||||||
private async performInteractiveAuth(): Promise<AuthResult> {
|
|
||||||
ui.displayBanner('Task Master Authentication');
|
|
||||||
|
|
||||||
// Check if already authenticated
|
|
||||||
if (this.authManager.isAuthenticated()) {
|
|
||||||
const { continueAuth } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'continueAuth',
|
|
||||||
message:
|
|
||||||
'You are already authenticated. Do you want to re-authenticate?',
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!continueAuth) {
|
|
||||||
const credentials = this.authManager.getCredentials();
|
|
||||||
ui.displaySuccess('Using existing authentication');
|
|
||||||
|
|
||||||
if (credentials) {
|
|
||||||
console.log(chalk.gray(` Email: ${credentials.email || 'N/A'}`));
|
|
||||||
console.log(chalk.gray(` User ID: ${credentials.userId}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'login',
|
|
||||||
credentials: credentials || undefined,
|
|
||||||
message: 'Using existing authentication'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Direct browser authentication - no menu needed
|
|
||||||
const credentials = await this.authenticateWithBrowser();
|
|
||||||
|
|
||||||
ui.displaySuccess('Authentication successful!');
|
|
||||||
console.log(
|
|
||||||
chalk.gray(` Logged in as: ${credentials.email || credentials.userId}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'login',
|
|
||||||
credentials,
|
|
||||||
message: 'Authentication successful'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.handleAuthError(error as AuthenticationError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'login',
|
|
||||||
message: `Authentication failed: ${(error as Error).message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate with browser using OAuth 2.0 with PKCE
|
|
||||||
*/
|
|
||||||
private async authenticateWithBrowser(): Promise<AuthCredentials> {
|
|
||||||
let authSpinner: Ora | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use AuthManager's new unified OAuth flow method with callbacks
|
|
||||||
const credentials = await this.authManager.authenticateWithOAuth({
|
|
||||||
// Callback to handle browser opening
|
|
||||||
openBrowser: async (authUrl) => {
|
|
||||||
await open(authUrl);
|
|
||||||
},
|
|
||||||
timeout: 5 * 60 * 1000, // 5 minutes
|
|
||||||
|
|
||||||
// Callback when auth URL is ready
|
|
||||||
onAuthUrl: (authUrl) => {
|
|
||||||
// Display authentication instructions
|
|
||||||
console.log(chalk.blue.bold('\n🔐 Browser Authentication\n'));
|
|
||||||
console.log(chalk.white(' Opening your browser to authenticate...'));
|
|
||||||
console.log(chalk.gray(" If the browser doesn't open, visit:"));
|
|
||||||
console.log(chalk.cyan.underline(` ${authUrl}\n`));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Callback when waiting for authentication
|
|
||||||
onWaitingForAuth: () => {
|
|
||||||
authSpinner = ora({
|
|
||||||
text: 'Waiting for authentication...',
|
|
||||||
spinner: 'dots'
|
|
||||||
}).start();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Callback on success
|
|
||||||
onSuccess: () => {
|
|
||||||
if (authSpinner) {
|
|
||||||
authSpinner.succeed('Authentication successful!');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Callback on error
|
|
||||||
onError: () => {
|
|
||||||
if (authSpinner) {
|
|
||||||
authSpinner.fail('Authentication failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return credentials;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle authentication errors
|
|
||||||
*/
|
|
||||||
private handleAuthError(error: AuthenticationError): void {
|
|
||||||
console.error(chalk.red(`\n✗ ${error.message}`));
|
|
||||||
|
|
||||||
switch (error.code) {
|
|
||||||
case 'NETWORK_ERROR':
|
|
||||||
ui.displayWarning(
|
|
||||||
'Please check your internet connection and try again.'
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'INVALID_CREDENTIALS':
|
|
||||||
ui.displayWarning('Please check your credentials and try again.');
|
|
||||||
break;
|
|
||||||
case 'AUTH_EXPIRED':
|
|
||||||
ui.displayWarning(
|
|
||||||
'Your session has expired. Please authenticate again.'
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (process.env.DEBUG) {
|
|
||||||
console.error(chalk.gray(error.stack || ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle general errors
|
|
||||||
*/
|
|
||||||
private handleError(error: any): void {
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
this.handleAuthError(error);
|
|
||||||
} else {
|
|
||||||
const msg = error?.getSanitizedDetails?.() ?? {
|
|
||||||
message: error?.message ?? String(error)
|
|
||||||
};
|
|
||||||
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
|
||||||
|
|
||||||
if (error.stack && process.env.DEBUG) {
|
|
||||||
console.error(chalk.gray(error.stack));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last result for programmatic access
|
|
||||||
*/
|
|
||||||
private setLastResult(result: AuthResult): void {
|
|
||||||
this.lastResult = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last result (for programmatic usage)
|
|
||||||
*/
|
|
||||||
getLastResult(): AuthResult | undefined {
|
|
||||||
return this.lastResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current authentication status (for programmatic usage)
|
|
||||||
*/
|
|
||||||
isAuthenticated(): boolean {
|
|
||||||
return this.authManager.isAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current credentials (for programmatic usage)
|
|
||||||
*/
|
|
||||||
getCredentials(): AuthCredentials | null {
|
|
||||||
return this.authManager.getCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up resources
|
|
||||||
*/
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
// No resources to clean up for auth command
|
|
||||||
// But keeping method for consistency with other commands
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static method to register this command on an existing program
|
|
||||||
* This is for gradual migration - allows commands.js to use this
|
|
||||||
*/
|
|
||||||
static registerOn(program: Command): Command {
|
|
||||||
const authCommand = new AuthCommand();
|
|
||||||
program.addCommand(authCommand);
|
|
||||||
return authCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative registration that returns the command for chaining
|
|
||||||
* Can also configure the command name if needed
|
|
||||||
*/
|
|
||||||
static register(program: Command, name?: string): AuthCommand {
|
|
||||||
const authCommand = new AuthCommand(name);
|
|
||||||
program.addCommand(authCommand);
|
|
||||||
return authCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Context command for managing org/brief selection
|
|
||||||
* Provides a clean interface for workspace context management
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import inquirer from 'inquirer';
|
|
||||||
import ora from 'ora';
|
|
||||||
import {
|
|
||||||
AuthManager,
|
|
||||||
AuthenticationError,
|
|
||||||
type UserContext
|
|
||||||
} from '@tm/core/auth';
|
|
||||||
import * as ui from '../utils/ui.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type from context command
|
|
||||||
*/
|
|
||||||
export interface ContextResult {
|
|
||||||
success: boolean;
|
|
||||||
action: 'show' | 'select-org' | 'select-brief' | 'clear' | 'set';
|
|
||||||
context?: UserContext;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ContextCommand extending Commander's Command class
|
|
||||||
* Manages user's workspace context (org/brief selection)
|
|
||||||
*/
|
|
||||||
export class ContextCommand extends Command {
|
|
||||||
private authManager: AuthManager;
|
|
||||||
private lastResult?: ContextResult;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name || 'context');
|
|
||||||
|
|
||||||
// Initialize auth manager
|
|
||||||
this.authManager = AuthManager.getInstance();
|
|
||||||
|
|
||||||
// Configure the command
|
|
||||||
this.description(
|
|
||||||
'Manage workspace context (organization and brief selection)'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add subcommands
|
|
||||||
this.addOrgCommand();
|
|
||||||
this.addBriefCommand();
|
|
||||||
this.addClearCommand();
|
|
||||||
this.addSetCommand();
|
|
||||||
|
|
||||||
// Default action shows current context
|
|
||||||
this.action(async () => {
|
|
||||||
await this.executeShow();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add org selection subcommand
|
|
||||||
*/
|
|
||||||
private addOrgCommand(): void {
|
|
||||||
this.command('org')
|
|
||||||
.description('Select an organization')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeSelectOrg();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add brief selection subcommand
|
|
||||||
*/
|
|
||||||
private addBriefCommand(): void {
|
|
||||||
this.command('brief')
|
|
||||||
.description('Select a brief within the current organization')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeSelectBrief();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add clear subcommand
|
|
||||||
*/
|
|
||||||
private addClearCommand(): void {
|
|
||||||
this.command('clear')
|
|
||||||
.description('Clear all context selections')
|
|
||||||
.action(async () => {
|
|
||||||
await this.executeClear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add set subcommand for direct context setting
|
|
||||||
*/
|
|
||||||
private addSetCommand(): void {
|
|
||||||
this.command('set')
|
|
||||||
.description('Set context directly')
|
|
||||||
.option('--org <id>', 'Organization ID')
|
|
||||||
.option('--org-name <name>', 'Organization name')
|
|
||||||
.option('--brief <id>', 'Brief ID')
|
|
||||||
.option('--brief-name <name>', 'Brief name')
|
|
||||||
.action(async (options) => {
|
|
||||||
await this.executeSet(options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute show current context
|
|
||||||
*/
|
|
||||||
private async executeShow(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const result = this.displayContext();
|
|
||||||
this.setLastResult(result);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display current context
|
|
||||||
*/
|
|
||||||
private displayContext(): ContextResult {
|
|
||||||
// Check authentication first
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
console.log(chalk.yellow('✗ Not authenticated'));
|
|
||||||
console.log(chalk.gray('\n Run "tm auth login" to authenticate first'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'show',
|
|
||||||
message: 'Not authenticated'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = this.authManager.getContext();
|
|
||||||
|
|
||||||
console.log(chalk.cyan('\n🌍 Workspace Context\n'));
|
|
||||||
|
|
||||||
if (context && (context.orgId || context.briefId)) {
|
|
||||||
if (context.orgName || context.orgId) {
|
|
||||||
console.log(chalk.green('✓ Organization'));
|
|
||||||
if (context.orgName) {
|
|
||||||
console.log(chalk.white(` ${context.orgName}`));
|
|
||||||
}
|
|
||||||
if (context.orgId) {
|
|
||||||
console.log(chalk.gray(` ID: ${context.orgId}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.briefName || context.briefId) {
|
|
||||||
console.log(chalk.green('\n✓ Brief'));
|
|
||||||
if (context.briefName) {
|
|
||||||
console.log(chalk.white(` ${context.briefName}`));
|
|
||||||
}
|
|
||||||
if (context.briefId) {
|
|
||||||
console.log(chalk.gray(` ID: ${context.briefId}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.updatedAt) {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(
|
|
||||||
`\n Last updated: ${new Date(context.updatedAt).toLocaleString()}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'show',
|
|
||||||
context,
|
|
||||||
message: 'Context loaded'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log(chalk.yellow('✗ No context selected'));
|
|
||||||
console.log(
|
|
||||||
chalk.gray('\n Run "tm context org" to select an organization')
|
|
||||||
);
|
|
||||||
console.log(chalk.gray(' Run "tm context brief" to select a brief'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'show',
|
|
||||||
message: 'No context selected'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute org selection
|
|
||||||
*/
|
|
||||||
private async executeSelectOrg(): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Check authentication
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.selectOrganization();
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select an organization interactively
|
|
||||||
*/
|
|
||||||
private async selectOrganization(): Promise<ContextResult> {
|
|
||||||
const spinner = ora('Fetching organizations...').start();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch organizations from API
|
|
||||||
const organizations = await this.authManager.getOrganizations();
|
|
||||||
spinner.stop();
|
|
||||||
|
|
||||||
if (organizations.length === 0) {
|
|
||||||
ui.displayWarning('No organizations available');
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'select-org',
|
|
||||||
message: 'No organizations available'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for selection
|
|
||||||
const { selectedOrg } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: 'selectedOrg',
|
|
||||||
message: 'Select an organization:',
|
|
||||||
choices: organizations.map((org) => ({
|
|
||||||
name: org.name,
|
|
||||||
value: org
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update context
|
|
||||||
await this.authManager.updateContext({
|
|
||||||
orgId: selectedOrg.id,
|
|
||||||
orgName: selectedOrg.name,
|
|
||||||
// Clear brief when changing org
|
|
||||||
briefId: undefined,
|
|
||||||
briefName: undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.displaySuccess(`Selected organization: ${selectedOrg.name}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'select-org',
|
|
||||||
context: this.authManager.getContext() || undefined,
|
|
||||||
message: `Selected organization: ${selectedOrg.name}`
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
spinner.fail('Failed to fetch organizations');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute brief selection
|
|
||||||
*/
|
|
||||||
private async executeSelectBrief(): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Check authentication
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if org is selected
|
|
||||||
const context = this.authManager.getContext();
|
|
||||||
if (!context?.orgId) {
|
|
||||||
ui.displayError(
|
|
||||||
'No organization selected. Run "tm context org" first.'
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.selectBrief(context.orgId);
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a brief within the current organization
|
|
||||||
*/
|
|
||||||
private async selectBrief(orgId: string): Promise<ContextResult> {
|
|
||||||
const spinner = ora('Fetching briefs...').start();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch briefs from API
|
|
||||||
const briefs = await this.authManager.getBriefs(orgId);
|
|
||||||
spinner.stop();
|
|
||||||
|
|
||||||
if (briefs.length === 0) {
|
|
||||||
ui.displayWarning('No briefs available in this organization');
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'select-brief',
|
|
||||||
message: 'No briefs available'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for selection
|
|
||||||
const { selectedBrief } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: 'selectedBrief',
|
|
||||||
message: 'Select a brief:',
|
|
||||||
choices: [
|
|
||||||
{ name: '(No brief - organization level)', value: null },
|
|
||||||
...briefs.map((brief) => ({
|
|
||||||
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
|
||||||
value: brief
|
|
||||||
}))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (selectedBrief) {
|
|
||||||
// Update context with brief
|
|
||||||
const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`;
|
|
||||||
await this.authManager.updateContext({
|
|
||||||
briefId: selectedBrief.id,
|
|
||||||
briefName: briefName
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.displaySuccess(`Selected brief: ${briefName}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'select-brief',
|
|
||||||
context: this.authManager.getContext() || undefined,
|
|
||||||
message: `Selected brief: ${selectedBrief.name}`
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Clear brief selection
|
|
||||||
await this.authManager.updateContext({
|
|
||||||
briefId: undefined,
|
|
||||||
briefName: undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.displaySuccess('Cleared brief selection (organization level)');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'select-brief',
|
|
||||||
context: this.authManager.getContext() || undefined,
|
|
||||||
message: 'Cleared brief selection'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
spinner.fail('Failed to fetch briefs');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute clear context
|
|
||||||
*/
|
|
||||||
private async executeClear(): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Check authentication
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.clearContext();
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all context selections
|
|
||||||
*/
|
|
||||||
private async clearContext(): Promise<ContextResult> {
|
|
||||||
try {
|
|
||||||
await this.authManager.clearContext();
|
|
||||||
ui.displaySuccess('Context cleared');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'clear',
|
|
||||||
message: 'Context cleared'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ui.displayError(`Failed to clear context: ${(error as Error).message}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'clear',
|
|
||||||
message: `Failed to clear context: ${(error as Error).message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute set context with options
|
|
||||||
*/
|
|
||||||
private async executeSet(options: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Check authentication
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.setContext(options);
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set context directly from options
|
|
||||||
*/
|
|
||||||
private async setContext(options: any): Promise<ContextResult> {
|
|
||||||
try {
|
|
||||||
const context: Partial<UserContext> = {};
|
|
||||||
|
|
||||||
if (options.org) {
|
|
||||||
context.orgId = options.org;
|
|
||||||
}
|
|
||||||
if (options.orgName) {
|
|
||||||
context.orgName = options.orgName;
|
|
||||||
}
|
|
||||||
if (options.brief) {
|
|
||||||
context.briefId = options.brief;
|
|
||||||
}
|
|
||||||
if (options.briefName) {
|
|
||||||
context.briefName = options.briefName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(context).length === 0) {
|
|
||||||
ui.displayWarning('No context options provided');
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'set',
|
|
||||||
message: 'No context options provided'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.authManager.updateContext(context);
|
|
||||||
ui.displaySuccess('Context updated');
|
|
||||||
|
|
||||||
// Display what was set
|
|
||||||
if (context.orgName || context.orgId) {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(` Organization: ${context.orgName || context.orgId}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (context.briefName || context.briefId) {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(` Brief: ${context.briefName || context.briefId}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: 'set',
|
|
||||||
context: this.authManager.getContext() || undefined,
|
|
||||||
message: 'Context updated'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ui.displayError(`Failed to set context: ${(error as Error).message}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
action: 'set',
|
|
||||||
message: `Failed to set context: ${(error as Error).message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle errors
|
|
||||||
*/
|
|
||||||
private handleError(error: any): void {
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
console.error(chalk.red(`\n✗ ${error.message}`));
|
|
||||||
|
|
||||||
if (error.code === 'NOT_AUTHENTICATED') {
|
|
||||||
ui.displayWarning('Please authenticate first: tm auth login');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const msg = error?.message ?? String(error);
|
|
||||||
console.error(chalk.red(`Error: ${msg}`));
|
|
||||||
|
|
||||||
if (error.stack && process.env.DEBUG) {
|
|
||||||
console.error(chalk.gray(error.stack));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last result for programmatic access
|
|
||||||
*/
|
|
||||||
private setLastResult(result: ContextResult): void {
|
|
||||||
this.lastResult = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last result (for programmatic usage)
|
|
||||||
*/
|
|
||||||
getLastResult(): ContextResult | undefined {
|
|
||||||
return this.lastResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current context (for programmatic usage)
|
|
||||||
*/
|
|
||||||
getContext(): UserContext | null {
|
|
||||||
return this.authManager.getContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up resources
|
|
||||||
*/
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
// No resources to clean up for context command
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static method to register this command on an existing program
|
|
||||||
*/
|
|
||||||
static registerOn(program: Command): Command {
|
|
||||||
const contextCommand = new ContextCommand();
|
|
||||||
program.addCommand(contextCommand);
|
|
||||||
return contextCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative registration that returns the command for chaining
|
|
||||||
*/
|
|
||||||
static register(program: Command, name?: string): ContextCommand {
|
|
||||||
const contextCommand = new ContextCommand(name);
|
|
||||||
program.addCommand(contextCommand);
|
|
||||||
return contextCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,488 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview ListTasks command using Commander's native class pattern
|
|
||||||
* Extends Commander.Command for better integration with the framework
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import {
|
|
||||||
createTaskMasterCore,
|
|
||||||
type Task,
|
|
||||||
type TaskStatus,
|
|
||||||
type TaskMasterCore,
|
|
||||||
TASK_STATUSES,
|
|
||||||
OUTPUT_FORMATS,
|
|
||||||
STATUS_ICONS,
|
|
||||||
type OutputFormat
|
|
||||||
} from '@tm/core';
|
|
||||||
import type { StorageType } from '@tm/core/types';
|
|
||||||
import * as ui from '../utils/ui.js';
|
|
||||||
import {
|
|
||||||
displayHeader,
|
|
||||||
displayDashboards,
|
|
||||||
calculateTaskStatistics,
|
|
||||||
calculateSubtaskStatistics,
|
|
||||||
calculateDependencyStatistics,
|
|
||||||
getPriorityBreakdown,
|
|
||||||
displayRecommendedNextTask,
|
|
||||||
getTaskDescription,
|
|
||||||
displaySuggestedNextSteps,
|
|
||||||
type NextTaskInfo
|
|
||||||
} from '../ui/index.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options interface for the list command
|
|
||||||
*/
|
|
||||||
export interface ListCommandOptions {
|
|
||||||
status?: string;
|
|
||||||
tag?: string;
|
|
||||||
withSubtasks?: boolean;
|
|
||||||
format?: OutputFormat;
|
|
||||||
silent?: boolean;
|
|
||||||
project?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type from list command
|
|
||||||
*/
|
|
||||||
export interface ListTasksResult {
|
|
||||||
tasks: Task[];
|
|
||||||
total: number;
|
|
||||||
filtered: number;
|
|
||||||
tag?: string;
|
|
||||||
storageType: Exclude<StorageType, 'auto'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ListTasksCommand extending Commander's Command class
|
|
||||||
* This is a thin presentation layer over @tm/core
|
|
||||||
*/
|
|
||||||
export class ListTasksCommand extends Command {
|
|
||||||
private tmCore?: TaskMasterCore;
|
|
||||||
private lastResult?: ListTasksResult;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name || 'list');
|
|
||||||
|
|
||||||
// Configure the command
|
|
||||||
this.description('List tasks with optional filtering')
|
|
||||||
.alias('ls')
|
|
||||||
.option('-s, --status <status>', 'Filter by status (comma-separated)')
|
|
||||||
.option('-t, --tag <tag>', 'Filter by tag')
|
|
||||||
.option('--with-subtasks', 'Include subtasks in the output')
|
|
||||||
.option(
|
|
||||||
'-f, --format <format>',
|
|
||||||
'Output format (text, json, compact)',
|
|
||||||
'text'
|
|
||||||
)
|
|
||||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
|
||||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
|
||||||
.action(async (options: ListCommandOptions) => {
|
|
||||||
await this.executeCommand(options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the list command
|
|
||||||
*/
|
|
||||||
private async executeCommand(options: ListCommandOptions): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Validate options
|
|
||||||
if (!this.validateOptions(options)) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize tm-core
|
|
||||||
await this.initializeCore(options.project || process.cwd());
|
|
||||||
|
|
||||||
// Get tasks from core
|
|
||||||
const result = await this.getTasks(options);
|
|
||||||
|
|
||||||
// Store result for programmatic access
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
if (!options.silent) {
|
|
||||||
this.displayResults(result, options);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
const msg = error?.getSanitizedDetails?.() ?? {
|
|
||||||
message: error?.message ?? String(error)
|
|
||||||
};
|
|
||||||
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
|
||||||
if (error.stack && process.env.DEBUG) {
|
|
||||||
console.error(chalk.gray(error.stack));
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate command options
|
|
||||||
*/
|
|
||||||
private validateOptions(options: ListCommandOptions): boolean {
|
|
||||||
// Validate format
|
|
||||||
if (
|
|
||||||
options.format &&
|
|
||||||
!OUTPUT_FORMATS.includes(options.format as OutputFormat)
|
|
||||||
) {
|
|
||||||
console.error(chalk.red(`Invalid format: ${options.format}`));
|
|
||||||
console.error(chalk.gray(`Valid formats: ${OUTPUT_FORMATS.join(', ')}`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate status
|
|
||||||
if (options.status) {
|
|
||||||
const statuses = options.status.split(',').map((s: string) => s.trim());
|
|
||||||
|
|
||||||
for (const status of statuses) {
|
|
||||||
if (status !== 'all' && !TASK_STATUSES.includes(status as TaskStatus)) {
|
|
||||||
console.error(chalk.red(`Invalid status: ${status}`));
|
|
||||||
console.error(
|
|
||||||
chalk.gray(`Valid statuses: ${TASK_STATUSES.join(', ')}`)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize TaskMasterCore
|
|
||||||
*/
|
|
||||||
private async initializeCore(projectRoot: string): Promise<void> {
|
|
||||||
if (!this.tmCore) {
|
|
||||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get tasks from tm-core
|
|
||||||
*/
|
|
||||||
private async getTasks(
|
|
||||||
options: ListCommandOptions
|
|
||||||
): Promise<ListTasksResult> {
|
|
||||||
if (!this.tmCore) {
|
|
||||||
throw new Error('TaskMasterCore not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build filter
|
|
||||||
const filter =
|
|
||||||
options.status && options.status !== 'all'
|
|
||||||
? {
|
|
||||||
status: options.status
|
|
||||||
.split(',')
|
|
||||||
.map((s: string) => s.trim() as TaskStatus)
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// Call tm-core
|
|
||||||
const result = await this.tmCore.getTaskList({
|
|
||||||
tag: options.tag,
|
|
||||||
filter,
|
|
||||||
includeSubtasks: options.withSubtasks
|
|
||||||
});
|
|
||||||
|
|
||||||
return result as ListTasksResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display results based on format
|
|
||||||
*/
|
|
||||||
private displayResults(
|
|
||||||
result: ListTasksResult,
|
|
||||||
options: ListCommandOptions
|
|
||||||
): void {
|
|
||||||
const format = (options.format || 'text') as OutputFormat | 'text';
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case 'json':
|
|
||||||
this.displayJson(result);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'compact':
|
|
||||||
this.displayCompact(result.tasks, options.withSubtasks);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
default:
|
|
||||||
this.displayText(result, options.withSubtasks);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display in JSON format
|
|
||||||
*/
|
|
||||||
private displayJson(data: ListTasksResult): void {
|
|
||||||
console.log(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
tasks: data.tasks,
|
|
||||||
metadata: {
|
|
||||||
total: data.total,
|
|
||||||
filtered: data.filtered,
|
|
||||||
tag: data.tag,
|
|
||||||
storageType: data.storageType
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display in compact format
|
|
||||||
*/
|
|
||||||
private displayCompact(tasks: Task[], withSubtasks?: boolean): void {
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
const icon = STATUS_ICONS[task.status];
|
|
||||||
console.log(`${chalk.cyan(task.id)} ${icon} ${task.title}`);
|
|
||||||
|
|
||||||
if (withSubtasks && task.subtasks?.length) {
|
|
||||||
task.subtasks.forEach((subtask) => {
|
|
||||||
const subIcon = STATUS_ICONS[subtask.status];
|
|
||||||
console.log(
|
|
||||||
` ${chalk.gray(`${task.id}.${subtask.id}`)} ${subIcon} ${chalk.gray(subtask.title)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display in text format with tables
|
|
||||||
*/
|
|
||||||
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
|
|
||||||
const { tasks, tag } = data;
|
|
||||||
|
|
||||||
// Get file path for display
|
|
||||||
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
|
|
||||||
|
|
||||||
// Display header without banner (banner already shown by main CLI)
|
|
||||||
displayHeader({
|
|
||||||
tag: tag || 'master',
|
|
||||||
filePath: filePath
|
|
||||||
});
|
|
||||||
|
|
||||||
// No tasks message
|
|
||||||
if (tasks.length === 0) {
|
|
||||||
ui.displayWarning('No tasks found matching the criteria.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const taskStats = calculateTaskStatistics(tasks);
|
|
||||||
const subtaskStats = calculateSubtaskStatistics(tasks);
|
|
||||||
const depStats = calculateDependencyStatistics(tasks);
|
|
||||||
const priorityBreakdown = getPriorityBreakdown(tasks);
|
|
||||||
|
|
||||||
// Find next task following the same logic as findNextTask
|
|
||||||
const nextTask = this.findNextTask(tasks);
|
|
||||||
|
|
||||||
// Display dashboard boxes
|
|
||||||
displayDashboards(
|
|
||||||
taskStats,
|
|
||||||
subtaskStats,
|
|
||||||
priorityBreakdown,
|
|
||||||
depStats,
|
|
||||||
nextTask
|
|
||||||
);
|
|
||||||
|
|
||||||
// Task table - no title, just show the table directly
|
|
||||||
console.log(
|
|
||||||
ui.createTaskTable(tasks, {
|
|
||||||
showSubtasks: withSubtasks,
|
|
||||||
showDependencies: true,
|
|
||||||
showComplexity: true // Enable complexity column
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Display recommended next task section immediately after table
|
|
||||||
if (nextTask) {
|
|
||||||
// Find the full task object to get description
|
|
||||||
const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id));
|
|
||||||
const description = fullTask ? getTaskDescription(fullTask) : undefined;
|
|
||||||
|
|
||||||
displayRecommendedNextTask({
|
|
||||||
...nextTask,
|
|
||||||
status: 'pending', // Next task is typically pending
|
|
||||||
description
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
displayRecommendedNextTask(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display suggested next steps at the end
|
|
||||||
displaySuggestedNextSteps();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last result for programmatic access
|
|
||||||
*/
|
|
||||||
private setLastResult(result: ListTasksResult): void {
|
|
||||||
this.lastResult = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the next task to work on
|
|
||||||
* Implements the same logic as scripts/modules/task-manager/find-next-task.js
|
|
||||||
*/
|
|
||||||
private findNextTask(tasks: Task[]): NextTaskInfo | undefined {
|
|
||||||
const priorityValues: Record<string, number> = {
|
|
||||||
critical: 4,
|
|
||||||
high: 3,
|
|
||||||
medium: 2,
|
|
||||||
low: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build set of completed task IDs (including subtasks)
|
|
||||||
const completedIds = new Set<string>();
|
|
||||||
tasks.forEach((t) => {
|
|
||||||
if (t.status === 'done' || t.status === 'completed') {
|
|
||||||
completedIds.add(String(t.id));
|
|
||||||
}
|
|
||||||
if (t.subtasks) {
|
|
||||||
t.subtasks.forEach((st) => {
|
|
||||||
if (st.status === 'done' || st.status === 'completed') {
|
|
||||||
completedIds.add(`${t.id}.${st.id}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// First, look for eligible subtasks in in-progress parent tasks
|
|
||||||
const candidateSubtasks: NextTaskInfo[] = [];
|
|
||||||
|
|
||||||
tasks
|
|
||||||
.filter(
|
|
||||||
(t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0
|
|
||||||
)
|
|
||||||
.forEach((parent) => {
|
|
||||||
parent.subtasks!.forEach((st) => {
|
|
||||||
const stStatus = (st.status || 'pending').toLowerCase();
|
|
||||||
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
|
|
||||||
|
|
||||||
// Check if dependencies are satisfied
|
|
||||||
const fullDeps =
|
|
||||||
st.dependencies?.map((d) => {
|
|
||||||
// Handle both numeric and string IDs
|
|
||||||
if (typeof d === 'string' && d.includes('.')) {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
return `${parent.id}.${d}`;
|
|
||||||
}) ?? [];
|
|
||||||
|
|
||||||
const depsSatisfied =
|
|
||||||
fullDeps.length === 0 ||
|
|
||||||
fullDeps.every((depId) => completedIds.has(String(depId)));
|
|
||||||
|
|
||||||
if (depsSatisfied) {
|
|
||||||
candidateSubtasks.push({
|
|
||||||
id: `${parent.id}.${st.id}`,
|
|
||||||
title: st.title || `Subtask ${st.id}`,
|
|
||||||
priority: st.priority || parent.priority || 'medium',
|
|
||||||
dependencies: fullDeps.map((d) => String(d))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (candidateSubtasks.length > 0) {
|
|
||||||
// Sort by priority, then by dependencies count, then by ID
|
|
||||||
candidateSubtasks.sort((a, b) => {
|
|
||||||
const pa = priorityValues[a.priority || 'medium'] ?? 2;
|
|
||||||
const pb = priorityValues[b.priority || 'medium'] ?? 2;
|
|
||||||
if (pb !== pa) return pb - pa;
|
|
||||||
|
|
||||||
const depCountA = a.dependencies?.length || 0;
|
|
||||||
const depCountB = b.dependencies?.length || 0;
|
|
||||||
if (depCountA !== depCountB) return depCountA - depCountB;
|
|
||||||
|
|
||||||
return String(a.id).localeCompare(String(b.id));
|
|
||||||
});
|
|
||||||
return candidateSubtasks[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to finding eligible top-level tasks
|
|
||||||
const eligibleTasks = tasks.filter((task) => {
|
|
||||||
// Skip non-eligible statuses
|
|
||||||
const status = (task.status || 'pending').toLowerCase();
|
|
||||||
if (status !== 'pending' && status !== 'in-progress') return false;
|
|
||||||
|
|
||||||
// Check dependencies
|
|
||||||
const deps = task.dependencies || [];
|
|
||||||
const depsSatisfied =
|
|
||||||
deps.length === 0 ||
|
|
||||||
deps.every((depId) => completedIds.has(String(depId)));
|
|
||||||
|
|
||||||
return depsSatisfied;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (eligibleTasks.length === 0) return undefined;
|
|
||||||
|
|
||||||
// Sort eligible tasks
|
|
||||||
eligibleTasks.sort((a, b) => {
|
|
||||||
// Priority (higher first)
|
|
||||||
const pa = priorityValues[a.priority || 'medium'] ?? 2;
|
|
||||||
const pb = priorityValues[b.priority || 'medium'] ?? 2;
|
|
||||||
if (pb !== pa) return pb - pa;
|
|
||||||
|
|
||||||
// Dependencies count (fewer first)
|
|
||||||
const depCountA = a.dependencies?.length || 0;
|
|
||||||
const depCountB = b.dependencies?.length || 0;
|
|
||||||
if (depCountA !== depCountB) return depCountA - depCountB;
|
|
||||||
|
|
||||||
// ID (lower first)
|
|
||||||
return Number(a.id) - Number(b.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextTask = eligibleTasks[0];
|
|
||||||
return {
|
|
||||||
id: nextTask.id,
|
|
||||||
title: nextTask.title,
|
|
||||||
priority: nextTask.priority,
|
|
||||||
dependencies: nextTask.dependencies?.map((d) => String(d))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last result (for programmatic usage)
|
|
||||||
*/
|
|
||||||
getLastResult(): ListTasksResult | undefined {
|
|
||||||
return this.lastResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up resources
|
|
||||||
*/
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
if (this.tmCore) {
|
|
||||||
await this.tmCore.close();
|
|
||||||
this.tmCore = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static method to register this command on an existing program
|
|
||||||
* This is for gradual migration - allows commands.js to use this
|
|
||||||
*/
|
|
||||||
static registerOn(program: Command): Command {
|
|
||||||
const listCommand = new ListTasksCommand();
|
|
||||||
program.addCommand(listCommand);
|
|
||||||
return listCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative registration that returns the command for chaining
|
|
||||||
* Can also configure the command name if needed
|
|
||||||
*/
|
|
||||||
static register(program: Command, name?: string): ListTasksCommand {
|
|
||||||
const listCommand = new ListTasksCommand(name);
|
|
||||||
program.addCommand(listCommand);
|
|
||||||
return listCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview SetStatusCommand using Commander's native class pattern
|
|
||||||
* Extends Commander.Command for better integration with the framework
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
import {
|
|
||||||
createTaskMasterCore,
|
|
||||||
type TaskMasterCore,
|
|
||||||
type TaskStatus
|
|
||||||
} from '@tm/core';
|
|
||||||
import type { StorageType } from '@tm/core/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valid task status values for validation
|
|
||||||
*/
|
|
||||||
const VALID_TASK_STATUSES: TaskStatus[] = [
|
|
||||||
'pending',
|
|
||||||
'in-progress',
|
|
||||||
'done',
|
|
||||||
'deferred',
|
|
||||||
'cancelled',
|
|
||||||
'blocked',
|
|
||||||
'review'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options interface for the set-status command
|
|
||||||
*/
|
|
||||||
export interface SetStatusCommandOptions {
|
|
||||||
id?: string;
|
|
||||||
status?: TaskStatus;
|
|
||||||
format?: 'text' | 'json';
|
|
||||||
silent?: boolean;
|
|
||||||
project?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type from set-status command
|
|
||||||
*/
|
|
||||||
export interface SetStatusResult {
|
|
||||||
success: boolean;
|
|
||||||
updatedTasks: Array<{
|
|
||||||
taskId: string;
|
|
||||||
oldStatus: TaskStatus;
|
|
||||||
newStatus: TaskStatus;
|
|
||||||
}>;
|
|
||||||
storageType: Exclude<StorageType, 'auto'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SetStatusCommand extending Commander's Command class
|
|
||||||
* This is a thin presentation layer over @tm/core
|
|
||||||
*/
|
|
||||||
export class SetStatusCommand extends Command {
|
|
||||||
private tmCore?: TaskMasterCore;
|
|
||||||
private lastResult?: SetStatusResult;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name || 'set-status');
|
|
||||||
|
|
||||||
// Configure the command
|
|
||||||
this.description('Update the status of one or more tasks')
|
|
||||||
.requiredOption(
|
|
||||||
'-i, --id <id>',
|
|
||||||
'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)'
|
|
||||||
)
|
|
||||||
.requiredOption(
|
|
||||||
'-s, --status <status>',
|
|
||||||
`New status (${VALID_TASK_STATUSES.join(', ')})`
|
|
||||||
)
|
|
||||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
||||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
|
||||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
|
||||||
.action(async (options: SetStatusCommandOptions) => {
|
|
||||||
await this.executeCommand(options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the set-status command
|
|
||||||
*/
|
|
||||||
private async executeCommand(
|
|
||||||
options: SetStatusCommandOptions
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Validate required options
|
|
||||||
if (!options.id) {
|
|
||||||
console.error(chalk.red('Error: Task ID is required. Use -i or --id'));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.status) {
|
|
||||||
console.error(
|
|
||||||
chalk.red('Error: Status is required. Use -s or --status')
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate status
|
|
||||||
if (!VALID_TASK_STATUSES.includes(options.status)) {
|
|
||||||
console.error(
|
|
||||||
chalk.red(
|
|
||||||
`Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize TaskMaster core
|
|
||||||
this.tmCore = await createTaskMasterCore({
|
|
||||||
projectPath: options.project || process.cwd()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse task IDs (handle comma-separated values)
|
|
||||||
const taskIds = options.id.split(',').map((id) => id.trim());
|
|
||||||
|
|
||||||
// Update each task
|
|
||||||
const updatedTasks: Array<{
|
|
||||||
taskId: string;
|
|
||||||
oldStatus: TaskStatus;
|
|
||||||
newStatus: TaskStatus;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
for (const taskId of taskIds) {
|
|
||||||
try {
|
|
||||||
const result = await this.tmCore.updateTaskStatus(
|
|
||||||
taskId,
|
|
||||||
options.status
|
|
||||||
);
|
|
||||||
updatedTasks.push({
|
|
||||||
taskId: result.taskId,
|
|
||||||
oldStatus: result.oldStatus,
|
|
||||||
newStatus: result.newStatus
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
if (!options.silent) {
|
|
||||||
console.error(
|
|
||||||
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.format === 'json') {
|
|
||||||
console.log(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: errorMessage,
|
|
||||||
taskId,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store result for potential reuse
|
|
||||||
this.lastResult = {
|
|
||||||
success: true,
|
|
||||||
updatedTasks,
|
|
||||||
storageType: this.tmCore.getStorageType() as Exclude<
|
|
||||||
StorageType,
|
|
||||||
'auto'
|
|
||||||
>
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
this.displayResults(this.lastResult, options);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
|
||||||
|
|
||||||
if (!options.silent) {
|
|
||||||
console.error(chalk.red(`Error: ${errorMessage}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.format === 'json') {
|
|
||||||
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
// Clean up resources
|
|
||||||
if (this.tmCore) {
|
|
||||||
await this.tmCore.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display results based on format
|
|
||||||
*/
|
|
||||||
private displayResults(
|
|
||||||
result: SetStatusResult,
|
|
||||||
options: SetStatusCommandOptions
|
|
||||||
): void {
|
|
||||||
const format = options.format || 'text';
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case 'json':
|
|
||||||
console.log(JSON.stringify(result, null, 2));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
default:
|
|
||||||
if (!options.silent) {
|
|
||||||
this.displayTextResults(result);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display results in text format
|
|
||||||
*/
|
|
||||||
private displayTextResults(result: SetStatusResult): void {
|
|
||||||
if (result.updatedTasks.length === 1) {
|
|
||||||
// Single task update
|
|
||||||
const update = result.updatedTasks[0];
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) +
|
|
||||||
'\n\n' +
|
|
||||||
`${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` +
|
|
||||||
`${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`,
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'green',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 1 }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Multiple task updates
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.white.bold(
|
|
||||||
`✅ Successfully updated ${result.updatedTasks.length} tasks`
|
|
||||||
) +
|
|
||||||
'\n\n' +
|
|
||||||
result.updatedTasks
|
|
||||||
.map(
|
|
||||||
(update) =>
|
|
||||||
`${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}`
|
|
||||||
)
|
|
||||||
.join('\n'),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'green',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 1 }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show storage info
|
|
||||||
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get colored status display
|
|
||||||
*/
|
|
||||||
private getStatusDisplay(status: TaskStatus): string {
|
|
||||||
const statusColors: Record<TaskStatus, (text: string) => string> = {
|
|
||||||
pending: chalk.yellow,
|
|
||||||
'in-progress': chalk.blue,
|
|
||||||
done: chalk.green,
|
|
||||||
deferred: chalk.gray,
|
|
||||||
cancelled: chalk.red,
|
|
||||||
blocked: chalk.red,
|
|
||||||
review: chalk.magenta,
|
|
||||||
completed: chalk.green
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorFn = statusColors[status] || chalk.white;
|
|
||||||
return colorFn(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last command result (useful for testing or chaining)
|
|
||||||
*/
|
|
||||||
getLastResult(): SetStatusResult | undefined {
|
|
||||||
return this.lastResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static method to register this command on an existing program
|
|
||||||
* This is for gradual migration - allows commands.js to use this
|
|
||||||
*/
|
|
||||||
static registerOn(program: Command): Command {
|
|
||||||
const setStatusCommand = new SetStatusCommand();
|
|
||||||
program.addCommand(setStatusCommand);
|
|
||||||
return setStatusCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative registration that returns the command for chaining
|
|
||||||
* Can also configure the command name if needed
|
|
||||||
*/
|
|
||||||
static register(program: Command, name?: string): SetStatusCommand {
|
|
||||||
const setStatusCommand = new SetStatusCommand(name);
|
|
||||||
program.addCommand(setStatusCommand);
|
|
||||||
return setStatusCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory function to create and configure the set-status command
|
|
||||||
*/
|
|
||||||
export function createSetStatusCommand(): SetStatusCommand {
|
|
||||||
return new SetStatusCommand();
|
|
||||||
}
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview ShowCommand using Commander's native class pattern
|
|
||||||
* Extends Commander.Command for better integration with the framework
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
|
||||||
import type { StorageType } from '@tm/core/types';
|
|
||||||
import * as ui from '../utils/ui.js';
|
|
||||||
import {
|
|
||||||
displayTaskHeader,
|
|
||||||
displayTaskProperties,
|
|
||||||
displayImplementationDetails,
|
|
||||||
displayTestStrategy,
|
|
||||||
displaySubtasks,
|
|
||||||
displaySuggestedActions
|
|
||||||
} from '../ui/components/task-detail.component.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options interface for the show command
|
|
||||||
*/
|
|
||||||
export interface ShowCommandOptions {
|
|
||||||
id?: string;
|
|
||||||
status?: string;
|
|
||||||
format?: 'text' | 'json';
|
|
||||||
silent?: boolean;
|
|
||||||
project?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type from show command
|
|
||||||
*/
|
|
||||||
export interface ShowTaskResult {
|
|
||||||
task: Task | null;
|
|
||||||
found: boolean;
|
|
||||||
storageType: Exclude<StorageType, 'auto'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type for multiple tasks
|
|
||||||
*/
|
|
||||||
export interface ShowMultipleTasksResult {
|
|
||||||
tasks: Task[];
|
|
||||||
notFound: string[];
|
|
||||||
storageType: Exclude<StorageType, 'auto'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ShowCommand extending Commander's Command class
|
|
||||||
* This is a thin presentation layer over @tm/core
|
|
||||||
*/
|
|
||||||
export class ShowCommand extends Command {
|
|
||||||
private tmCore?: TaskMasterCore;
|
|
||||||
private lastResult?: ShowTaskResult | ShowMultipleTasksResult;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name || 'show');
|
|
||||||
|
|
||||||
// Configure the command
|
|
||||||
this.description('Display detailed information about one or more tasks')
|
|
||||||
.argument('[id]', 'Task ID(s) to show (comma-separated for multiple)')
|
|
||||||
.option(
|
|
||||||
'-i, --id <id>',
|
|
||||||
'Task ID(s) to show (comma-separated for multiple)'
|
|
||||||
)
|
|
||||||
.option('-s, --status <status>', 'Filter subtasks by status')
|
|
||||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
||||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
|
||||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
|
||||||
.action(
|
|
||||||
async (taskId: string | undefined, options: ShowCommandOptions) => {
|
|
||||||
await this.executeCommand(taskId, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the show command
|
|
||||||
*/
|
|
||||||
private async executeCommand(
|
|
||||||
taskId: string | undefined,
|
|
||||||
options: ShowCommandOptions
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Validate options
|
|
||||||
if (!this.validateOptions(options)) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize tm-core
|
|
||||||
await this.initializeCore(options.project || process.cwd());
|
|
||||||
|
|
||||||
// Get the task ID from argument or option
|
|
||||||
const idArg = taskId || options.id;
|
|
||||||
if (!idArg) {
|
|
||||||
console.error(chalk.red('Error: Please provide a task ID'));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if multiple IDs are provided (comma-separated)
|
|
||||||
const taskIds = idArg
|
|
||||||
.split(',')
|
|
||||||
.map((id) => id.trim())
|
|
||||||
.filter((id) => id.length > 0);
|
|
||||||
|
|
||||||
// Get tasks from core
|
|
||||||
const result =
|
|
||||||
taskIds.length > 1
|
|
||||||
? await this.getMultipleTasks(taskIds, options)
|
|
||||||
: await this.getSingleTask(taskIds[0], options);
|
|
||||||
|
|
||||||
// Store result for programmatic access
|
|
||||||
this.setLastResult(result);
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
if (!options.silent) {
|
|
||||||
this.displayResults(result, options);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
const msg = error?.getSanitizedDetails?.() ?? {
|
|
||||||
message: error?.message ?? String(error)
|
|
||||||
};
|
|
||||||
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
|
||||||
if (error.stack && process.env.DEBUG) {
|
|
||||||
console.error(chalk.gray(error.stack));
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate command options
|
|
||||||
*/
|
|
||||||
private validateOptions(options: ShowCommandOptions): boolean {
|
|
||||||
// Validate format
|
|
||||||
if (options.format && !['text', 'json'].includes(options.format)) {
|
|
||||||
console.error(chalk.red(`Invalid format: ${options.format}`));
|
|
||||||
console.error(chalk.gray(`Valid formats: text, json`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize TaskMasterCore
|
|
||||||
*/
|
|
||||||
private async initializeCore(projectRoot: string): Promise<void> {
|
|
||||||
if (!this.tmCore) {
|
|
||||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single task from tm-core
|
|
||||||
*/
|
|
||||||
private async getSingleTask(
|
|
||||||
taskId: string,
|
|
||||||
_options: ShowCommandOptions
|
|
||||||
): Promise<ShowTaskResult> {
|
|
||||||
if (!this.tmCore) {
|
|
||||||
throw new Error('TaskMasterCore not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the task
|
|
||||||
const task = await this.tmCore.getTask(taskId);
|
|
||||||
|
|
||||||
// Get storage type
|
|
||||||
const storageType = this.tmCore.getStorageType();
|
|
||||||
|
|
||||||
return {
|
|
||||||
task,
|
|
||||||
found: task !== null,
|
|
||||||
storageType: storageType as Exclude<StorageType, 'auto'>
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get multiple tasks from tm-core
|
|
||||||
*/
|
|
||||||
private async getMultipleTasks(
|
|
||||||
taskIds: string[],
|
|
||||||
_options: ShowCommandOptions
|
|
||||||
): Promise<ShowMultipleTasksResult> {
|
|
||||||
if (!this.tmCore) {
|
|
||||||
throw new Error('TaskMasterCore not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tasks: Task[] = [];
|
|
||||||
const notFound: string[] = [];
|
|
||||||
|
|
||||||
// Get each task individually
|
|
||||||
for (const taskId of taskIds) {
|
|
||||||
const task = await this.tmCore.getTask(taskId);
|
|
||||||
if (task) {
|
|
||||||
tasks.push(task);
|
|
||||||
} else {
|
|
||||||
notFound.push(taskId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get storage type
|
|
||||||
const storageType = this.tmCore.getStorageType();
|
|
||||||
|
|
||||||
return {
|
|
||||||
tasks,
|
|
||||||
notFound,
|
|
||||||
storageType: storageType as Exclude<StorageType, 'auto'>
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display results based on format
|
|
||||||
*/
|
|
||||||
private displayResults(
|
|
||||||
result: ShowTaskResult | ShowMultipleTasksResult,
|
|
||||||
options: ShowCommandOptions
|
|
||||||
): void {
|
|
||||||
const format = options.format || 'text';
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case 'json':
|
|
||||||
this.displayJson(result);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
default:
|
|
||||||
if ('task' in result) {
|
|
||||||
// Single task result
|
|
||||||
this.displaySingleTask(result, options);
|
|
||||||
} else {
|
|
||||||
// Multiple tasks result
|
|
||||||
this.displayMultipleTasks(result, options);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display in JSON format
|
|
||||||
*/
|
|
||||||
private displayJson(result: ShowTaskResult | ShowMultipleTasksResult): void {
|
|
||||||
console.log(JSON.stringify(result, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a single task in text format
|
|
||||||
*/
|
|
||||||
private displaySingleTask(
|
|
||||||
result: ShowTaskResult,
|
|
||||||
options: ShowCommandOptions
|
|
||||||
): void {
|
|
||||||
if (!result.found || !result.task) {
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.yellow(`Task not found!`), {
|
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
||||||
borderColor: 'yellow',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 1 }
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = result.task;
|
|
||||||
|
|
||||||
// Display header with tag
|
|
||||||
displayTaskHeader(task.id, task.title);
|
|
||||||
|
|
||||||
// Display task properties in table format
|
|
||||||
displayTaskProperties(task);
|
|
||||||
|
|
||||||
// Display implementation details if available
|
|
||||||
if (task.details) {
|
|
||||||
console.log(); // Empty line for spacing
|
|
||||||
displayImplementationDetails(task.details);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display test strategy if available
|
|
||||||
if ('testStrategy' in task && task.testStrategy) {
|
|
||||||
console.log(); // Empty line for spacing
|
|
||||||
displayTestStrategy(task.testStrategy as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display subtasks if available
|
|
||||||
if (task.subtasks && task.subtasks.length > 0) {
|
|
||||||
// Filter subtasks by status if provided
|
|
||||||
const filteredSubtasks = options.status
|
|
||||||
? task.subtasks.filter((sub) => sub.status === options.status)
|
|
||||||
: task.subtasks;
|
|
||||||
|
|
||||||
if (filteredSubtasks.length === 0 && options.status) {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(` No subtasks with status '${options.status}'`)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
displaySubtasks(filteredSubtasks, task.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display suggested actions
|
|
||||||
displaySuggestedActions(task.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display multiple tasks in text format
|
|
||||||
*/
|
|
||||||
private displayMultipleTasks(
|
|
||||||
result: ShowMultipleTasksResult,
|
|
||||||
_options: ShowCommandOptions
|
|
||||||
): void {
|
|
||||||
// Header
|
|
||||||
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
|
|
||||||
|
|
||||||
if (result.notFound.length > 0) {
|
|
||||||
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.tasks.length === 0) {
|
|
||||||
ui.displayWarning('No tasks found matching the criteria.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task table
|
|
||||||
console.log(chalk.blue.bold(`\n📋 Tasks:\n`));
|
|
||||||
console.log(
|
|
||||||
ui.createTaskTable(result.tasks, {
|
|
||||||
showSubtasks: true,
|
|
||||||
showDependencies: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last result for programmatic access
|
|
||||||
*/
|
|
||||||
private setLastResult(
|
|
||||||
result: ShowTaskResult | ShowMultipleTasksResult
|
|
||||||
): void {
|
|
||||||
this.lastResult = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last result (for programmatic usage)
|
|
||||||
*/
|
|
||||||
getLastResult(): ShowTaskResult | ShowMultipleTasksResult | undefined {
|
|
||||||
return this.lastResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up resources
|
|
||||||
*/
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
if (this.tmCore) {
|
|
||||||
await this.tmCore.close();
|
|
||||||
this.tmCore = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static method to register this command on an existing program
|
|
||||||
* This is for gradual migration - allows commands.js to use this
|
|
||||||
*/
|
|
||||||
static registerOn(program: Command): Command {
|
|
||||||
const showCommand = new ShowCommand();
|
|
||||||
program.addCommand(showCommand);
|
|
||||||
return showCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative registration that returns the command for chaining
|
|
||||||
* Can also configure the command name if needed
|
|
||||||
*/
|
|
||||||
static register(program: Command, name?: string): ShowCommand {
|
|
||||||
const showCommand = new ShowCommand(name);
|
|
||||||
program.addCommand(showCommand);
|
|
||||||
return showCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Main entry point for @tm/cli package
|
|
||||||
* Exports all public APIs for the CLI presentation layer
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
export { ListTasksCommand } from './commands/list.command.js';
|
|
||||||
export { ShowCommand } from './commands/show.command.js';
|
|
||||||
export { AuthCommand } from './commands/auth.command.js';
|
|
||||||
export { ContextCommand } from './commands/context.command.js';
|
|
||||||
export { SetStatusCommand } from './commands/set-status.command.js';
|
|
||||||
|
|
||||||
// UI utilities (for other commands to use)
|
|
||||||
export * as ui from './utils/ui.js';
|
|
||||||
|
|
||||||
// Re-export commonly used types from tm-core
|
|
||||||
export type {
|
|
||||||
Task,
|
|
||||||
TaskStatus,
|
|
||||||
TaskPriority,
|
|
||||||
TaskMasterCore
|
|
||||||
} from '@tm/core';
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Dashboard components for Task Master CLI
|
|
||||||
* Displays project statistics and dependency information
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
import type { Task, TaskPriority } from '@tm/core/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Statistics for task collection
|
|
||||||
*/
|
|
||||||
export interface TaskStatistics {
|
|
||||||
total: number;
|
|
||||||
done: number;
|
|
||||||
inProgress: number;
|
|
||||||
pending: number;
|
|
||||||
blocked: number;
|
|
||||||
deferred: number;
|
|
||||||
cancelled: number;
|
|
||||||
review?: number;
|
|
||||||
completionPercentage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Statistics for dependencies
|
|
||||||
*/
|
|
||||||
export interface DependencyStatistics {
|
|
||||||
tasksWithNoDeps: number;
|
|
||||||
tasksReadyToWork: number;
|
|
||||||
tasksBlockedByDeps: number;
|
|
||||||
mostDependedOnTaskId?: number;
|
|
||||||
mostDependedOnCount?: number;
|
|
||||||
avgDependenciesPerTask: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Next task information
|
|
||||||
*/
|
|
||||||
export interface NextTaskInfo {
|
|
||||||
id: string | number;
|
|
||||||
title: string;
|
|
||||||
priority?: TaskPriority;
|
|
||||||
dependencies?: (string | number)[];
|
|
||||||
complexity?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status breakdown for progress bars
|
|
||||||
*/
|
|
||||||
export interface StatusBreakdown {
|
|
||||||
'in-progress'?: number;
|
|
||||||
pending?: number;
|
|
||||||
blocked?: number;
|
|
||||||
deferred?: number;
|
|
||||||
cancelled?: number;
|
|
||||||
review?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a progress bar with color-coded status segments
|
|
||||||
*/
|
|
||||||
function createProgressBar(
|
|
||||||
completionPercentage: number,
|
|
||||||
width: number = 30,
|
|
||||||
statusBreakdown?: StatusBreakdown
|
|
||||||
): string {
|
|
||||||
// If no breakdown provided, use simple green bar
|
|
||||||
if (!statusBreakdown) {
|
|
||||||
const filled = Math.round((completionPercentage / 100) * width);
|
|
||||||
const empty = width - filled;
|
|
||||||
return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the bar with different colored sections
|
|
||||||
// Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
|
|
||||||
let bar = '';
|
|
||||||
let charsUsed = 0;
|
|
||||||
|
|
||||||
// 1. Green filled blocks for completed tasks (done)
|
|
||||||
const completedChars = Math.round((completionPercentage / 100) * width);
|
|
||||||
if (completedChars > 0) {
|
|
||||||
bar += chalk.green('█').repeat(completedChars);
|
|
||||||
charsUsed += completedChars;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Gray filled blocks for cancelled (won't be done)
|
|
||||||
if (statusBreakdown.cancelled && charsUsed < width) {
|
|
||||||
const cancelledChars = Math.round(
|
|
||||||
(statusBreakdown.cancelled / 100) * width
|
|
||||||
);
|
|
||||||
const actualChars = Math.min(cancelledChars, width - charsUsed);
|
|
||||||
if (actualChars > 0) {
|
|
||||||
bar += chalk.gray('█').repeat(actualChars);
|
|
||||||
charsUsed += actualChars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Gray filled blocks for deferred (won't be done now)
|
|
||||||
if (statusBreakdown.deferred && charsUsed < width) {
|
|
||||||
const deferredChars = Math.round((statusBreakdown.deferred / 100) * width);
|
|
||||||
const actualChars = Math.min(deferredChars, width - charsUsed);
|
|
||||||
if (actualChars > 0) {
|
|
||||||
bar += chalk.gray('█').repeat(actualChars);
|
|
||||||
charsUsed += actualChars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Blue filled blocks for in-progress (actively working)
|
|
||||||
if (statusBreakdown['in-progress'] && charsUsed < width) {
|
|
||||||
const inProgressChars = Math.round(
|
|
||||||
(statusBreakdown['in-progress'] / 100) * width
|
|
||||||
);
|
|
||||||
const actualChars = Math.min(inProgressChars, width - charsUsed);
|
|
||||||
if (actualChars > 0) {
|
|
||||||
bar += chalk.blue('█').repeat(actualChars);
|
|
||||||
charsUsed += actualChars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Magenta empty blocks for review (almost done)
|
|
||||||
if (statusBreakdown.review && charsUsed < width) {
|
|
||||||
const reviewChars = Math.round((statusBreakdown.review / 100) * width);
|
|
||||||
const actualChars = Math.min(reviewChars, width - charsUsed);
|
|
||||||
if (actualChars > 0) {
|
|
||||||
bar += chalk.magenta('░').repeat(actualChars);
|
|
||||||
charsUsed += actualChars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Yellow empty blocks for pending (ready to start)
|
|
||||||
if (statusBreakdown.pending && charsUsed < width) {
|
|
||||||
const pendingChars = Math.round((statusBreakdown.pending / 100) * width);
|
|
||||||
const actualChars = Math.min(pendingChars, width - charsUsed);
|
|
||||||
if (actualChars > 0) {
|
|
||||||
bar += chalk.yellow('░').repeat(actualChars);
|
|
||||||
charsUsed += actualChars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Red empty blocks for blocked (can't start yet)
|
|
||||||
if (statusBreakdown.blocked && charsUsed < width) {
|
|
||||||
const blockedChars = Math.round((statusBreakdown.blocked / 100) * width);
|
|
||||||
const actualChars = Math.min(blockedChars, width - charsUsed);
|
|
||||||
if (actualChars > 0) {
|
|
||||||
bar += chalk.red('░').repeat(actualChars);
|
|
||||||
charsUsed += actualChars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill any remaining space with gray empty yellow blocks
|
|
||||||
if (charsUsed < width) {
|
|
||||||
bar += chalk.yellow('░').repeat(width - charsUsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate task statistics from a list of tasks
|
|
||||||
*/
|
|
||||||
export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
|
|
||||||
const stats: TaskStatistics = {
|
|
||||||
total: tasks.length,
|
|
||||||
done: 0,
|
|
||||||
inProgress: 0,
|
|
||||||
pending: 0,
|
|
||||||
blocked: 0,
|
|
||||||
deferred: 0,
|
|
||||||
cancelled: 0,
|
|
||||||
review: 0,
|
|
||||||
completionPercentage: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
switch (task.status) {
|
|
||||||
case 'done':
|
|
||||||
stats.done++;
|
|
||||||
break;
|
|
||||||
case 'in-progress':
|
|
||||||
stats.inProgress++;
|
|
||||||
break;
|
|
||||||
case 'pending':
|
|
||||||
stats.pending++;
|
|
||||||
break;
|
|
||||||
case 'blocked':
|
|
||||||
stats.blocked++;
|
|
||||||
break;
|
|
||||||
case 'deferred':
|
|
||||||
stats.deferred++;
|
|
||||||
break;
|
|
||||||
case 'cancelled':
|
|
||||||
stats.cancelled++;
|
|
||||||
break;
|
|
||||||
case 'review':
|
|
||||||
stats.review = (stats.review || 0) + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stats.completionPercentage =
|
|
||||||
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate subtask statistics from tasks
|
|
||||||
*/
|
|
||||||
export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
|
|
||||||
const stats: TaskStatistics = {
|
|
||||||
total: 0,
|
|
||||||
done: 0,
|
|
||||||
inProgress: 0,
|
|
||||||
pending: 0,
|
|
||||||
blocked: 0,
|
|
||||||
deferred: 0,
|
|
||||||
cancelled: 0,
|
|
||||||
review: 0,
|
|
||||||
completionPercentage: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
if (task.subtasks && task.subtasks.length > 0) {
|
|
||||||
task.subtasks.forEach((subtask) => {
|
|
||||||
stats.total++;
|
|
||||||
switch (subtask.status) {
|
|
||||||
case 'done':
|
|
||||||
stats.done++;
|
|
||||||
break;
|
|
||||||
case 'in-progress':
|
|
||||||
stats.inProgress++;
|
|
||||||
break;
|
|
||||||
case 'pending':
|
|
||||||
stats.pending++;
|
|
||||||
break;
|
|
||||||
case 'blocked':
|
|
||||||
stats.blocked++;
|
|
||||||
break;
|
|
||||||
case 'deferred':
|
|
||||||
stats.deferred++;
|
|
||||||
break;
|
|
||||||
case 'cancelled':
|
|
||||||
stats.cancelled++;
|
|
||||||
break;
|
|
||||||
case 'review':
|
|
||||||
stats.review = (stats.review || 0) + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stats.completionPercentage =
|
|
||||||
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate dependency statistics
|
|
||||||
*/
|
|
||||||
export function calculateDependencyStatistics(
|
|
||||||
tasks: Task[]
|
|
||||||
): DependencyStatistics {
|
|
||||||
const completedTaskIds = new Set(
|
|
||||||
tasks.filter((t) => t.status === 'done').map((t) => t.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const tasksWithNoDeps = tasks.filter(
|
|
||||||
(t) =>
|
|
||||||
t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0)
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const tasksWithAllDepsSatisfied = tasks.filter(
|
|
||||||
(t) =>
|
|
||||||
t.status !== 'done' &&
|
|
||||||
t.dependencies &&
|
|
||||||
t.dependencies.length > 0 &&
|
|
||||||
t.dependencies.every((depId) => completedTaskIds.has(depId))
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const tasksBlockedByDeps = tasks.filter(
|
|
||||||
(t) =>
|
|
||||||
t.status !== 'done' &&
|
|
||||||
t.dependencies &&
|
|
||||||
t.dependencies.length > 0 &&
|
|
||||||
!t.dependencies.every((depId) => completedTaskIds.has(depId))
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Calculate most depended-on task
|
|
||||||
const dependencyCount: Record<string, number> = {};
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
if (task.dependencies && task.dependencies.length > 0) {
|
|
||||||
task.dependencies.forEach((depId) => {
|
|
||||||
const key = String(depId);
|
|
||||||
dependencyCount[key] = (dependencyCount[key] || 0) + 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mostDependedOnTaskId: number | undefined;
|
|
||||||
let mostDependedOnCount = 0;
|
|
||||||
|
|
||||||
for (const [taskId, count] of Object.entries(dependencyCount)) {
|
|
||||||
if (count > mostDependedOnCount) {
|
|
||||||
mostDependedOnCount = count;
|
|
||||||
mostDependedOnTaskId = parseInt(taskId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate average dependencies
|
|
||||||
const totalDependencies = tasks.reduce(
|
|
||||||
(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const avgDependenciesPerTask =
|
|
||||||
tasks.length > 0 ? totalDependencies / tasks.length : 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
tasksWithNoDeps,
|
|
||||||
tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied,
|
|
||||||
tasksBlockedByDeps,
|
|
||||||
mostDependedOnTaskId,
|
|
||||||
mostDependedOnCount,
|
|
||||||
avgDependenciesPerTask
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get priority counts
|
|
||||||
*/
|
|
||||||
export function getPriorityBreakdown(
|
|
||||||
tasks: Task[]
|
|
||||||
): Record<TaskPriority, number> {
|
|
||||||
const breakdown: Record<TaskPriority, number> = {
|
|
||||||
critical: 0,
|
|
||||||
high: 0,
|
|
||||||
medium: 0,
|
|
||||||
low: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
const priority = task.priority || 'medium';
|
|
||||||
breakdown[priority]++;
|
|
||||||
});
|
|
||||||
|
|
||||||
return breakdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate status breakdown as percentages
|
|
||||||
*/
|
|
||||||
function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
|
|
||||||
if (stats.total === 0) return {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
'in-progress': (stats.inProgress / stats.total) * 100,
|
|
||||||
pending: (stats.pending / stats.total) * 100,
|
|
||||||
blocked: (stats.blocked / stats.total) * 100,
|
|
||||||
deferred: (stats.deferred / stats.total) * 100,
|
|
||||||
cancelled: (stats.cancelled / stats.total) * 100,
|
|
||||||
review: ((stats.review || 0) / stats.total) * 100
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format status counts in the correct order with colors
|
|
||||||
* @param stats - The statistics object containing counts
|
|
||||||
* @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label)
|
|
||||||
*/
|
|
||||||
function formatStatusLine(
|
|
||||||
stats: TaskStatistics,
|
|
||||||
isSubtask: boolean = false
|
|
||||||
): string {
|
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
|
|
||||||
if (isSubtask) {
|
|
||||||
parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
|
|
||||||
} else {
|
|
||||||
parts.push(`Done: ${chalk.green(stats.done)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`);
|
|
||||||
parts.push(`Deferred: ${chalk.gray(stats.deferred)}`);
|
|
||||||
|
|
||||||
// Add line break for second row
|
|
||||||
const firstLine = parts.join(' ');
|
|
||||||
parts.length = 0;
|
|
||||||
|
|
||||||
parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`);
|
|
||||||
parts.push(`Review: ${chalk.magenta(stats.review || 0)}`);
|
|
||||||
parts.push(`Pending: ${chalk.yellow(stats.pending)}`);
|
|
||||||
parts.push(`Blocked: ${chalk.red(stats.blocked)}`);
|
|
||||||
|
|
||||||
const secondLine = parts.join(' ');
|
|
||||||
|
|
||||||
return firstLine + '\n' + secondLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the project dashboard box
|
|
||||||
*/
|
|
||||||
export function displayProjectDashboard(
|
|
||||||
taskStats: TaskStatistics,
|
|
||||||
subtaskStats: TaskStatistics,
|
|
||||||
priorityBreakdown: Record<TaskPriority, number>
|
|
||||||
): string {
|
|
||||||
// Calculate status breakdowns using the helper function
|
|
||||||
const taskStatusBreakdown = calculateStatusBreakdown(taskStats);
|
|
||||||
const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats);
|
|
||||||
|
|
||||||
// Create progress bars with the breakdowns
|
|
||||||
const taskProgressBar = createProgressBar(
|
|
||||||
taskStats.completionPercentage,
|
|
||||||
30,
|
|
||||||
taskStatusBreakdown
|
|
||||||
);
|
|
||||||
const subtaskProgressBar = createProgressBar(
|
|
||||||
subtaskStats.completionPercentage,
|
|
||||||
30,
|
|
||||||
subtaskStatusBreakdown
|
|
||||||
);
|
|
||||||
|
|
||||||
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
|
|
||||||
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
|
|
||||||
|
|
||||||
const content =
|
|
||||||
chalk.white.bold('Project Dashboard') +
|
|
||||||
'\n' +
|
|
||||||
`Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` +
|
|
||||||
formatStatusLine(taskStats, false) +
|
|
||||||
'\n\n' +
|
|
||||||
`Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` +
|
|
||||||
formatStatusLine(subtaskStats, true) +
|
|
||||||
'\n\n' +
|
|
||||||
chalk.cyan.bold('Priority Breakdown:') +
|
|
||||||
'\n' +
|
|
||||||
`${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` +
|
|
||||||
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` +
|
|
||||||
`${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`;
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the dependency dashboard box
|
|
||||||
*/
|
|
||||||
export function displayDependencyDashboard(
|
|
||||||
depStats: DependencyStatistics,
|
|
||||||
nextTask?: NextTaskInfo
|
|
||||||
): string {
|
|
||||||
const content =
|
|
||||||
chalk.white.bold('Dependency Status & Next Task') +
|
|
||||||
'\n' +
|
|
||||||
chalk.cyan.bold('Dependency Metrics:') +
|
|
||||||
'\n' +
|
|
||||||
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` +
|
|
||||||
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` +
|
|
||||||
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` +
|
|
||||||
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${
|
|
||||||
depStats.mostDependedOnTaskId
|
|
||||||
? chalk.cyan(
|
|
||||||
`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`
|
|
||||||
)
|
|
||||||
: chalk.gray('None')
|
|
||||||
}\n` +
|
|
||||||
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` +
|
|
||||||
chalk.cyan.bold('Next Task to Work On:') +
|
|
||||||
'\n' +
|
|
||||||
`ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${
|
|
||||||
nextTask
|
|
||||||
? chalk.white.bold(nextTask.title)
|
|
||||||
: chalk.yellow('No task available')
|
|
||||||
}\n` +
|
|
||||||
`Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${
|
|
||||||
nextTask?.dependencies?.length
|
|
||||||
? chalk.cyan(nextTask.dependencies.join(', '))
|
|
||||||
: chalk.gray('None')
|
|
||||||
}\n` +
|
|
||||||
`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display dashboard boxes side by side or stacked
|
|
||||||
*/
|
|
||||||
export function displayDashboards(
|
|
||||||
taskStats: TaskStatistics,
|
|
||||||
subtaskStats: TaskStatistics,
|
|
||||||
priorityBreakdown: Record<TaskPriority, number>,
|
|
||||||
depStats: DependencyStatistics,
|
|
||||||
nextTask?: NextTaskInfo
|
|
||||||
): void {
|
|
||||||
const projectDashboardContent = displayProjectDashboard(
|
|
||||||
taskStats,
|
|
||||||
subtaskStats,
|
|
||||||
priorityBreakdown
|
|
||||||
);
|
|
||||||
const dependencyDashboardContent = displayDependencyDashboard(
|
|
||||||
depStats,
|
|
||||||
nextTask
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get terminal width
|
|
||||||
const terminalWidth = process.stdout.columns || 80;
|
|
||||||
const minDashboardWidth = 50;
|
|
||||||
const minDependencyWidth = 50;
|
|
||||||
const totalMinWidth = minDashboardWidth + minDependencyWidth + 4;
|
|
||||||
|
|
||||||
// If terminal is wide enough, show side by side
|
|
||||||
if (terminalWidth >= totalMinWidth) {
|
|
||||||
const halfWidth = Math.floor(terminalWidth / 2);
|
|
||||||
const boxContentWidth = halfWidth - 4;
|
|
||||||
|
|
||||||
const dashboardBox = boxen(projectDashboardContent, {
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'blue',
|
|
||||||
borderStyle: 'round',
|
|
||||||
width: boxContentWidth,
|
|
||||||
dimBorder: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const dependencyBox = boxen(dependencyDashboardContent, {
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'magenta',
|
|
||||||
borderStyle: 'round',
|
|
||||||
width: boxContentWidth,
|
|
||||||
dimBorder: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create side-by-side layout
|
|
||||||
const dashboardLines = dashboardBox.split('\n');
|
|
||||||
const dependencyLines = dependencyBox.split('\n');
|
|
||||||
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
|
|
||||||
|
|
||||||
const combinedLines = [];
|
|
||||||
for (let i = 0; i < maxHeight; i++) {
|
|
||||||
const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
|
|
||||||
const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
|
|
||||||
const paddedDashLine = dashLine.padEnd(halfWidth, ' ');
|
|
||||||
combinedLines.push(paddedDashLine + depLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(combinedLines.join('\n'));
|
|
||||||
} else {
|
|
||||||
// Show stacked vertically
|
|
||||||
const dashboardBox = boxen(projectDashboardContent, {
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'blue',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 0, bottom: 1 }
|
|
||||||
});
|
|
||||||
|
|
||||||
const dependencyBox = boxen(dependencyDashboardContent, {
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'magenta',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 0, bottom: 1 }
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(dashboardBox);
|
|
||||||
console.log(dependencyBox);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Task Master header component
|
|
||||||
* Displays the banner, version, project info, and file path
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Header configuration options
|
|
||||||
*/
|
|
||||||
export interface HeaderOptions {
|
|
||||||
title?: string;
|
|
||||||
tag?: string;
|
|
||||||
filePath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the Task Master header with project info
|
|
||||||
*/
|
|
||||||
export function displayHeader(options: HeaderOptions = {}): void {
|
|
||||||
const { filePath, tag } = options;
|
|
||||||
|
|
||||||
// Display tag and file path info
|
|
||||||
if (tag) {
|
|
||||||
let tagInfo = '';
|
|
||||||
|
|
||||||
if (tag && tag !== 'master') {
|
|
||||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
|
||||||
} else {
|
|
||||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(tagInfo);
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
// Convert to absolute path if it's relative
|
|
||||||
const absolutePath = filePath.startsWith('/')
|
|
||||||
? filePath
|
|
||||||
: `${process.cwd()}/${filePath}`;
|
|
||||||
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(); // Empty line for spacing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview UI components exports
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './header.component.js';
|
|
||||||
export * from './dashboard.component.js';
|
|
||||||
export * from './next-task.component.js';
|
|
||||||
export * from './suggested-steps.component.js';
|
|
||||||
export * from './task-detail.component.js';
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Next task recommendation component
|
|
||||||
* Displays detailed information about the recommended next task
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
import type { Task } from '@tm/core/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Next task display options
|
|
||||||
*/
|
|
||||||
export interface NextTaskDisplayOptions {
|
|
||||||
id: string | number;
|
|
||||||
title: string;
|
|
||||||
priority?: string;
|
|
||||||
status?: string;
|
|
||||||
dependencies?: (string | number)[];
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the recommended next task section
|
|
||||||
*/
|
|
||||||
export function displayRecommendedNextTask(
|
|
||||||
task: NextTaskDisplayOptions | undefined
|
|
||||||
): void {
|
|
||||||
if (!task) {
|
|
||||||
// If no task available, show a message
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.yellow(
|
|
||||||
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
|
|
||||||
),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'yellow',
|
|
||||||
title: '⚠ NO TASKS AVAILABLE ⚠',
|
|
||||||
titleAlignment: 'center'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the content for the next task box
|
|
||||||
const content = [];
|
|
||||||
|
|
||||||
// Task header with ID and title
|
|
||||||
content.push(
|
|
||||||
`🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}`
|
|
||||||
);
|
|
||||||
content.push('');
|
|
||||||
|
|
||||||
// Priority and Status line
|
|
||||||
const statusLine = [];
|
|
||||||
if (task.priority) {
|
|
||||||
const priorityColor =
|
|
||||||
task.priority === 'high'
|
|
||||||
? chalk.red
|
|
||||||
: task.priority === 'medium'
|
|
||||||
? chalk.yellow
|
|
||||||
: chalk.gray;
|
|
||||||
statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`);
|
|
||||||
}
|
|
||||||
if (task.status) {
|
|
||||||
const statusDisplay =
|
|
||||||
task.status === 'pending'
|
|
||||||
? chalk.yellow('○ pending')
|
|
||||||
: task.status === 'in-progress'
|
|
||||||
? chalk.blue('▶ in-progress')
|
|
||||||
: chalk.gray(task.status);
|
|
||||||
statusLine.push(`Status: ${statusDisplay}`);
|
|
||||||
}
|
|
||||||
content.push(statusLine.join(' '));
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
const depsDisplay =
|
|
||||||
!task.dependencies || task.dependencies.length === 0
|
|
||||||
? chalk.gray('None')
|
|
||||||
: chalk.cyan(task.dependencies.join(', '));
|
|
||||||
content.push(`Dependencies: ${depsDisplay}`);
|
|
||||||
|
|
||||||
// Description if available
|
|
||||||
if (task.description) {
|
|
||||||
content.push('');
|
|
||||||
content.push(`Description: ${chalk.white(task.description)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action commands
|
|
||||||
content.push('');
|
|
||||||
content.push(
|
|
||||||
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}`
|
|
||||||
);
|
|
||||||
content.push(
|
|
||||||
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Display in a styled box with orange border
|
|
||||||
console.log(
|
|
||||||
boxen(content.join('\n'), {
|
|
||||||
padding: 1,
|
|
||||||
margin: { top: 1, bottom: 1 },
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: '#FFA500', // Orange color
|
|
||||||
title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'),
|
|
||||||
titleAlignment: 'center',
|
|
||||||
width: process.stdout.columns * 0.97,
|
|
||||||
fullscreen: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get task description from the full task object
|
|
||||||
*/
|
|
||||||
export function getTaskDescription(task: Task): string | undefined {
|
|
||||||
// Try to get description from the task
|
|
||||||
// This could be from task.description or the first line of task.details
|
|
||||||
if ('description' in task && task.description) {
|
|
||||||
return task.description as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('details' in task && task.details) {
|
|
||||||
// Take first sentence or line from details
|
|
||||||
const details = task.details as string;
|
|
||||||
const firstLine = details.split('\n')[0];
|
|
||||||
const firstSentence = firstLine.split('.')[0];
|
|
||||||
return firstSentence;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Suggested next steps component
|
|
||||||
* Displays helpful command suggestions at the end of the list
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display suggested next steps section
|
|
||||||
*/
|
|
||||||
export function displaySuggestedNextSteps(): void {
|
|
||||||
const steps = [
|
|
||||||
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next`,
|
|
||||||
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
|
|
||||||
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.white.bold('Suggested Next Steps:') + '\n\n' + steps.join('\n'),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
margin: { top: 0, bottom: 1 },
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'gray',
|
|
||||||
width: process.stdout.columns * 0.97
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Task detail component for show command
|
|
||||||
* Displays detailed task information in a structured format
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
import Table from 'cli-table3';
|
|
||||||
import { marked, MarkedExtension } from 'marked';
|
|
||||||
import { markedTerminal } from 'marked-terminal';
|
|
||||||
import type { Task } from '@tm/core/types';
|
|
||||||
import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js';
|
|
||||||
|
|
||||||
// Configure marked to use terminal renderer with subtle colors
|
|
||||||
marked.use(
|
|
||||||
markedTerminal({
|
|
||||||
// More subtle colors that match the overall design
|
|
||||||
code: (code: string) => {
|
|
||||||
// Custom code block handler to preserve formatting
|
|
||||||
return code
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => ' ' + chalk.cyan(line))
|
|
||||||
.join('\n');
|
|
||||||
},
|
|
||||||
blockquote: chalk.gray.italic,
|
|
||||||
html: chalk.gray,
|
|
||||||
heading: chalk.white.bold, // White bold for headings
|
|
||||||
hr: chalk.gray,
|
|
||||||
listitem: chalk.white, // White for list items
|
|
||||||
paragraph: chalk.white, // White for paragraphs (default text color)
|
|
||||||
strong: chalk.white.bold, // White bold for strong text
|
|
||||||
em: chalk.white.italic, // White italic for emphasis
|
|
||||||
codespan: chalk.cyan, // Cyan for inline code (no background)
|
|
||||||
del: chalk.dim.strikethrough,
|
|
||||||
link: chalk.blue,
|
|
||||||
href: chalk.blue.underline,
|
|
||||||
// Add more explicit code block handling
|
|
||||||
showSectionPrefix: false,
|
|
||||||
unescape: true,
|
|
||||||
emoji: false,
|
|
||||||
// Try to preserve whitespace in code blocks
|
|
||||||
tab: 4,
|
|
||||||
width: 120
|
|
||||||
}) as MarkedExtension
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also set marked options to preserve whitespace
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the task header with tag
|
|
||||||
*/
|
|
||||||
export function displayTaskHeader(
|
|
||||||
taskId: string | number,
|
|
||||||
title: string
|
|
||||||
): void {
|
|
||||||
// Display task header box
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
|
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
||||||
borderColor: 'blue',
|
|
||||||
borderStyle: 'round'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display task properties in a table format
|
|
||||||
*/
|
|
||||||
export function displayTaskProperties(task: Task): void {
|
|
||||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
|
||||||
// Create table for task properties - simple 2-column layout
|
|
||||||
const table = new Table({
|
|
||||||
head: [],
|
|
||||||
style: {
|
|
||||||
head: [],
|
|
||||||
border: ['grey']
|
|
||||||
},
|
|
||||||
colWidths: [
|
|
||||||
Math.floor(terminalWidth * 0.2),
|
|
||||||
Math.floor(terminalWidth * 0.8)
|
|
||||||
],
|
|
||||||
wordWrap: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const deps =
|
|
||||||
task.dependencies && task.dependencies.length > 0
|
|
||||||
? task.dependencies.map((d) => String(d)).join(', ')
|
|
||||||
: 'None';
|
|
||||||
|
|
||||||
// Build the left column (labels) and right column (values)
|
|
||||||
const labels = [
|
|
||||||
chalk.cyan('ID:'),
|
|
||||||
chalk.cyan('Title:'),
|
|
||||||
chalk.cyan('Status:'),
|
|
||||||
chalk.cyan('Priority:'),
|
|
||||||
chalk.cyan('Dependencies:'),
|
|
||||||
chalk.cyan('Complexity:'),
|
|
||||||
chalk.cyan('Description:')
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const values = [
|
|
||||||
String(task.id),
|
|
||||||
task.title,
|
|
||||||
getStatusWithColor(task.status),
|
|
||||||
getPriorityWithColor(task.priority),
|
|
||||||
deps,
|
|
||||||
'N/A',
|
|
||||||
task.description || ''
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
table.push([labels, values]);
|
|
||||||
|
|
||||||
console.log(table.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display implementation details in a box
|
|
||||||
*/
|
|
||||||
export function displayImplementationDetails(details: string): void {
|
|
||||||
// Handle all escaped characters properly
|
|
||||||
const cleanDetails = details
|
|
||||||
.replace(/\\n/g, '\n') // Convert \n to actual newlines
|
|
||||||
.replace(/\\t/g, '\t') // Convert \t to actual tabs
|
|
||||||
.replace(/\\"/g, '"') // Convert \" to actual quotes
|
|
||||||
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
|
|
||||||
|
|
||||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
|
||||||
|
|
||||||
// Parse markdown to terminal-friendly format
|
|
||||||
const markdownResult = marked(cleanDetails);
|
|
||||||
const formattedDetails =
|
|
||||||
typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'cyan', // Changed to cyan to match the original
|
|
||||||
width: terminalWidth // Fixed width to match the original
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display test strategy in a box
|
|
||||||
*/
|
|
||||||
export function displayTestStrategy(testStrategy: string): void {
|
|
||||||
// Handle all escaped characters properly (same as implementation details)
|
|
||||||
const cleanStrategy = testStrategy
|
|
||||||
.replace(/\\n/g, '\n') // Convert \n to actual newlines
|
|
||||||
.replace(/\\t/g, '\t') // Convert \t to actual tabs
|
|
||||||
.replace(/\\"/g, '"') // Convert \" to actual quotes
|
|
||||||
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
|
|
||||||
|
|
||||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
|
||||||
|
|
||||||
// Parse markdown to terminal-friendly format (same as implementation details)
|
|
||||||
const markdownResult = marked(cleanStrategy);
|
|
||||||
const formattedStrategy =
|
|
||||||
typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'cyan', // Changed to cyan to match implementation details
|
|
||||||
width: terminalWidth
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display subtasks in a table format
|
|
||||||
*/
|
|
||||||
export function displaySubtasks(
|
|
||||||
subtasks: Array<{
|
|
||||||
id: string | number;
|
|
||||||
title: string;
|
|
||||||
status: any;
|
|
||||||
description?: string;
|
|
||||||
dependencies?: string[];
|
|
||||||
}>,
|
|
||||||
parentId: string | number
|
|
||||||
): void {
|
|
||||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
|
||||||
// Display subtasks header
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.magenta.bold('Subtasks'), {
|
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
||||||
borderColor: 'magenta',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 1, bottom: 0 }
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create subtasks table
|
|
||||||
const table = new Table({
|
|
||||||
head: [
|
|
||||||
chalk.magenta.bold('ID'),
|
|
||||||
chalk.magenta.bold('Status'),
|
|
||||||
chalk.magenta.bold('Title'),
|
|
||||||
chalk.magenta.bold('Deps')
|
|
||||||
],
|
|
||||||
style: {
|
|
||||||
head: [],
|
|
||||||
border: ['grey']
|
|
||||||
},
|
|
||||||
colWidths: [
|
|
||||||
Math.floor(terminalWidth * 0.1),
|
|
||||||
Math.floor(terminalWidth * 0.15),
|
|
||||||
Math.floor(terminalWidth * 0.6),
|
|
||||||
Math.floor(terminalWidth * 0.15)
|
|
||||||
],
|
|
||||||
wordWrap: true
|
|
||||||
});
|
|
||||||
|
|
||||||
subtasks.forEach((subtask) => {
|
|
||||||
const subtaskId = `${parentId}.${subtask.id}`;
|
|
||||||
|
|
||||||
// Format dependencies
|
|
||||||
const deps =
|
|
||||||
subtask.dependencies && subtask.dependencies.length > 0
|
|
||||||
? subtask.dependencies.join(', ')
|
|
||||||
: 'None';
|
|
||||||
|
|
||||||
table.push([
|
|
||||||
subtaskId,
|
|
||||||
getStatusWithColor(subtask.status),
|
|
||||||
subtask.title,
|
|
||||||
deps
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(table.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display suggested actions
|
|
||||||
*/
|
|
||||||
export function displaySuggestedActions(taskId: string | number): void {
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.white.bold('Suggested Actions:') +
|
|
||||||
'\n\n' +
|
|
||||||
`${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
|
|
||||||
`${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
|
|
||||||
`${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
margin: { top: 1 },
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'green',
|
|
||||||
width: process.stdout.columns * 0.95 || 100
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Main UI exports
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Export all components
|
|
||||||
export * from './components/index.js';
|
|
||||||
|
|
||||||
// Re-export existing UI utilities
|
|
||||||
export * from '../utils/ui.js';
|
|
||||||
@@ -1,362 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview UI utilities for Task Master CLI
|
|
||||||
* Provides formatting, display, and visual components for the command line interface
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import boxen from 'boxen';
|
|
||||||
import Table from 'cli-table3';
|
|
||||||
import type { Task, TaskStatus, TaskPriority } from '@tm/core/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get colored status display with ASCII icons (matches scripts/modules/ui.js style)
|
|
||||||
*/
|
|
||||||
export function getStatusWithColor(
|
|
||||||
status: TaskStatus,
|
|
||||||
forTable: boolean = false
|
|
||||||
): string {
|
|
||||||
const statusConfig = {
|
|
||||||
done: {
|
|
||||||
color: chalk.green,
|
|
||||||
icon: '✓',
|
|
||||||
tableIcon: '✓'
|
|
||||||
},
|
|
||||||
pending: {
|
|
||||||
color: chalk.yellow,
|
|
||||||
icon: '○',
|
|
||||||
tableIcon: '○'
|
|
||||||
},
|
|
||||||
'in-progress': {
|
|
||||||
color: chalk.hex('#FFA500'),
|
|
||||||
icon: '▶',
|
|
||||||
tableIcon: '▶'
|
|
||||||
},
|
|
||||||
deferred: {
|
|
||||||
color: chalk.gray,
|
|
||||||
icon: 'x',
|
|
||||||
tableIcon: 'x'
|
|
||||||
},
|
|
||||||
review: {
|
|
||||||
color: chalk.magenta,
|
|
||||||
icon: '?',
|
|
||||||
tableIcon: '?'
|
|
||||||
},
|
|
||||||
cancelled: {
|
|
||||||
color: chalk.gray,
|
|
||||||
icon: 'x',
|
|
||||||
tableIcon: 'x'
|
|
||||||
},
|
|
||||||
blocked: {
|
|
||||||
color: chalk.red,
|
|
||||||
icon: '!',
|
|
||||||
tableIcon: '!'
|
|
||||||
},
|
|
||||||
completed: {
|
|
||||||
color: chalk.green,
|
|
||||||
icon: '✓',
|
|
||||||
tableIcon: '✓'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = statusConfig[status] || {
|
|
||||||
color: chalk.red,
|
|
||||||
icon: 'X',
|
|
||||||
tableIcon: 'X'
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = forTable ? config.tableIcon : config.icon;
|
|
||||||
return config.color(`${icon} ${status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get colored priority display
|
|
||||||
*/
|
|
||||||
export function getPriorityWithColor(priority: TaskPriority): string {
|
|
||||||
const priorityColors: Record<TaskPriority, (text: string) => string> = {
|
|
||||||
critical: chalk.red.bold,
|
|
||||||
high: chalk.red,
|
|
||||||
medium: chalk.yellow,
|
|
||||||
low: chalk.gray
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorFn = priorityColors[priority] || chalk.white;
|
|
||||||
return colorFn(priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get colored complexity display
|
|
||||||
*/
|
|
||||||
export function getComplexityWithColor(complexity: number | string): string {
|
|
||||||
const score =
|
|
||||||
typeof complexity === 'string' ? parseInt(complexity, 10) : complexity;
|
|
||||||
|
|
||||||
if (isNaN(score)) {
|
|
||||||
return chalk.gray('N/A');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score >= 8) {
|
|
||||||
return chalk.red.bold(`${score} (High)`);
|
|
||||||
} else if (score >= 5) {
|
|
||||||
return chalk.yellow(`${score} (Medium)`);
|
|
||||||
} else {
|
|
||||||
return chalk.green(`${score} (Low)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Truncate text to specified length
|
|
||||||
*/
|
|
||||||
export function truncate(text: string, maxLength: number): string {
|
|
||||||
if (text.length <= maxLength) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
return text.substring(0, maxLength - 3) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a progress bar
|
|
||||||
*/
|
|
||||||
export function createProgressBar(
|
|
||||||
completed: number,
|
|
||||||
total: number,
|
|
||||||
width: number = 30
|
|
||||||
): string {
|
|
||||||
if (total === 0) {
|
|
||||||
return chalk.gray('No tasks');
|
|
||||||
}
|
|
||||||
|
|
||||||
const percentage = Math.round((completed / total) * 100);
|
|
||||||
const filled = Math.round((completed / total) * width);
|
|
||||||
const empty = width - filled;
|
|
||||||
|
|
||||||
const bar = chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
|
|
||||||
|
|
||||||
return `${bar} ${chalk.cyan(`${percentage}%`)} (${completed}/${total})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a fancy banner
|
|
||||||
*/
|
|
||||||
export function displayBanner(title: string = 'Task Master'): void {
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.white.bold(title), {
|
|
||||||
padding: 1,
|
|
||||||
margin: { top: 1, bottom: 1 },
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'blue',
|
|
||||||
textAlignment: 'center'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display an error message (matches scripts/modules/ui.js style)
|
|
||||||
*/
|
|
||||||
export function displayError(message: string, details?: string): void {
|
|
||||||
console.error(
|
|
||||||
boxen(
|
|
||||||
chalk.red.bold('X Error: ') +
|
|
||||||
chalk.white(message) +
|
|
||||||
(details ? '\n\n' + chalk.gray(details) : ''),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'red'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a success message
|
|
||||||
*/
|
|
||||||
export function displaySuccess(message: string): void {
|
|
||||||
console.log(
|
|
||||||
boxen(
|
|
||||||
chalk.green.bold(String.fromCharCode(8730) + ' ') + chalk.white(message),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'green'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a warning message
|
|
||||||
*/
|
|
||||||
export function displayWarning(message: string): void {
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.yellow.bold('⚠ ') + chalk.white(message), {
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'yellow'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display info message
|
|
||||||
*/
|
|
||||||
export function displayInfo(message: string): void {
|
|
||||||
console.log(
|
|
||||||
boxen(chalk.blue.bold('i ') + chalk.white(message), {
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'blue'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format dependencies with their status
|
|
||||||
*/
|
|
||||||
export function formatDependenciesWithStatus(
|
|
||||||
dependencies: string[] | number[],
|
|
||||||
tasks: Task[]
|
|
||||||
): string {
|
|
||||||
if (!dependencies || dependencies.length === 0) {
|
|
||||||
return chalk.gray('none');
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskMap = new Map(tasks.map((t) => [t.id.toString(), t]));
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
.map((depId) => {
|
|
||||||
const task = taskMap.get(depId.toString());
|
|
||||||
if (!task) {
|
|
||||||
return chalk.red(`${depId} (not found)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusIcon =
|
|
||||||
task.status === 'done'
|
|
||||||
? '✓'
|
|
||||||
: task.status === 'in-progress'
|
|
||||||
? '►'
|
|
||||||
: '○';
|
|
||||||
|
|
||||||
return `${depId}${statusIcon}`;
|
|
||||||
})
|
|
||||||
.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a task table for display
|
|
||||||
*/
|
|
||||||
export function createTaskTable(
|
|
||||||
tasks: Task[],
|
|
||||||
options?: {
|
|
||||||
showSubtasks?: boolean;
|
|
||||||
showComplexity?: boolean;
|
|
||||||
showDependencies?: boolean;
|
|
||||||
}
|
|
||||||
): string {
|
|
||||||
const {
|
|
||||||
showSubtasks = false,
|
|
||||||
showComplexity = false,
|
|
||||||
showDependencies = true
|
|
||||||
} = options || {};
|
|
||||||
|
|
||||||
// Calculate dynamic column widths based on terminal width
|
|
||||||
const terminalWidth = process.stdout.columns * 0.9 || 100;
|
|
||||||
// Adjust column widths to better match the original layout
|
|
||||||
const baseColWidths = showComplexity
|
|
||||||
? [
|
|
||||||
Math.floor(terminalWidth * 0.06),
|
|
||||||
Math.floor(terminalWidth * 0.4),
|
|
||||||
Math.floor(terminalWidth * 0.15),
|
|
||||||
Math.floor(terminalWidth * 0.12),
|
|
||||||
Math.floor(terminalWidth * 0.2),
|
|
||||||
Math.floor(terminalWidth * 0.12)
|
|
||||||
] // ID, Title, Status, Priority, Dependencies, Complexity
|
|
||||||
: [
|
|
||||||
Math.floor(terminalWidth * 0.08),
|
|
||||||
Math.floor(terminalWidth * 0.4),
|
|
||||||
Math.floor(terminalWidth * 0.18),
|
|
||||||
Math.floor(terminalWidth * 0.12),
|
|
||||||
Math.floor(terminalWidth * 0.2)
|
|
||||||
]; // ID, Title, Status, Priority, Dependencies
|
|
||||||
|
|
||||||
const headers = [
|
|
||||||
chalk.blue.bold('ID'),
|
|
||||||
chalk.blue.bold('Title'),
|
|
||||||
chalk.blue.bold('Status'),
|
|
||||||
chalk.blue.bold('Priority')
|
|
||||||
];
|
|
||||||
const colWidths = baseColWidths.slice(0, 4);
|
|
||||||
|
|
||||||
if (showDependencies) {
|
|
||||||
headers.push(chalk.blue.bold('Dependencies'));
|
|
||||||
colWidths.push(baseColWidths[4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showComplexity) {
|
|
||||||
headers.push(chalk.blue.bold('Complexity'));
|
|
||||||
colWidths.push(baseColWidths[5] || 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
const table = new Table({
|
|
||||||
head: headers,
|
|
||||||
style: { head: [], border: [] },
|
|
||||||
colWidths,
|
|
||||||
wordWrap: true
|
|
||||||
});
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
const row: string[] = [
|
|
||||||
chalk.cyan(task.id.toString()),
|
|
||||||
truncate(task.title, colWidths[1] - 3),
|
|
||||||
getStatusWithColor(task.status, true), // Use table version
|
|
||||||
getPriorityWithColor(task.priority)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (showDependencies) {
|
|
||||||
// For table display, show simple format without status icons
|
|
||||||
if (!task.dependencies || task.dependencies.length === 0) {
|
|
||||||
row.push(chalk.gray('None'));
|
|
||||||
} else {
|
|
||||||
row.push(
|
|
||||||
chalk.cyan(task.dependencies.map((d) => String(d)).join(', '))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showComplexity) {
|
|
||||||
// Show N/A if no complexity score
|
|
||||||
row.push(chalk.gray('N/A'));
|
|
||||||
}
|
|
||||||
|
|
||||||
table.push(row);
|
|
||||||
|
|
||||||
// Add subtasks if requested
|
|
||||||
if (showSubtasks && task.subtasks && task.subtasks.length > 0) {
|
|
||||||
task.subtasks.forEach((subtask) => {
|
|
||||||
const subRow: string[] = [
|
|
||||||
chalk.gray(` └─ ${subtask.id}`),
|
|
||||||
chalk.gray(truncate(subtask.title, colWidths[1] - 6)),
|
|
||||||
chalk.gray(getStatusWithColor(subtask.status, true)),
|
|
||||||
chalk.gray(subtask.priority || 'medium')
|
|
||||||
];
|
|
||||||
|
|
||||||
if (showDependencies) {
|
|
||||||
subRow.push(
|
|
||||||
chalk.gray(
|
|
||||||
subtask.dependencies && subtask.dependencies.length > 0
|
|
||||||
? subtask.dependencies.map((dep) => String(dep)).join(', ')
|
|
||||||
: 'None'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showComplexity) {
|
|
||||||
subRow.push(chalk.gray('--'));
|
|
||||||
}
|
|
||||||
|
|
||||||
table.push(subRow);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return table.toString();
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"lib": ["ES2022"],
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"strictFunctionTypes": true,
|
|
||||||
"strictBindCallApply": true,
|
|
||||||
"strictPropertyInitialization": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": ["node"],
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"allowImportingTsExtensions": false
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# docs
|
|
||||||
|
|
||||||
## 0.0.2
|
|
||||||
|
|
||||||
## 0.0.1
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "docs",
|
"name": "docs",
|
||||||
"version": "0.0.2",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Task Master documentation powered by Mintlify",
|
"description": "Task Master documentation powered by Mintlify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "mintlify dev",
|
"dev": "mintlify dev",
|
||||||
|
"build": "mintlify build",
|
||||||
"preview": "mintlify preview"
|
"preview": "mintlify preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mintlify": "^4.2.111"
|
"mintlify": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,5 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## 0.24.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
|
|
||||||
- task-master-ai@0.26.0
|
|
||||||
|
|
||||||
## 0.24.2-rc.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e)]:
|
|
||||||
- task-master-ai@0.26.0-rc.1
|
|
||||||
|
|
||||||
## 0.24.2-rc.0
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
|
|
||||||
- task-master-ai@0.26.0-rc.0
|
|
||||||
|
|
||||||
## 0.24.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b), [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de)]:
|
|
||||||
- task-master-ai@0.25.1
|
|
||||||
|
|
||||||
## 0.24.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]:
|
|
||||||
- task-master-ai@0.25.0
|
|
||||||
|
|
||||||
## 0.24.0-rc.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
|
|
||||||
|
|
||||||
- [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]:
|
|
||||||
- task-master-ai@0.25.0-rc.0
|
|
||||||
|
|
||||||
## 0.23.1
|
## 0.23.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ async function main() {
|
|||||||
// This prevents the multiple React instances issue
|
// This prevents the multiple React instances issue
|
||||||
// Ensure React is resolved from the workspace root to avoid duplicates
|
// Ensure React is resolved from the workspace root to avoid duplicates
|
||||||
alias: {
|
alias: {
|
||||||
react: path.resolve(__dirname, '../../node_modules/react'),
|
react: path.resolve(__dirname, 'node_modules/react'),
|
||||||
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
|
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
||||||
@@ -135,8 +135,8 @@ async function main() {
|
|||||||
jsxImportSource: 'react',
|
jsxImportSource: 'react',
|
||||||
external: ['*.css'],
|
external: ['*.css'],
|
||||||
alias: {
|
alias: {
|
||||||
react: path.resolve(__dirname, '../../node_modules/react'),
|
react: path.resolve(__dirname, 'node_modules/react'),
|
||||||
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
|
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
||||||
|
|||||||
@@ -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.24.2",
|
"version": "0.23.1",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -229,7 +229,6 @@
|
|||||||
"build": "npm run build:js && npm run build:css",
|
"build": "npm run build:js && npm run build:css",
|
||||||
"build:js": "node ./esbuild.js --production",
|
"build:js": "node ./esbuild.js --production",
|
||||||
"build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
|
"build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
|
||||||
"dev": "npm run watch",
|
|
||||||
"package": "npm exec node ./package.mjs",
|
"package": "npm exec node ./package.mjs",
|
||||||
"package:direct": "node ./package.mjs",
|
"package:direct": "node ./package.mjs",
|
||||||
"debug:env": "node ./debug-env.mjs",
|
"debug:env": "node ./debug-env.mjs",
|
||||||
@@ -240,7 +239,7 @@
|
|||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "*"
|
"task-master-ai": "0.24.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Loader2, Play } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { PriorityBadge } from './PriorityBadge';
|
import { PriorityBadge } from './PriorityBadge';
|
||||||
import type { TaskMasterTask } from '../../webview/types';
|
import type { TaskMasterTask } from '../../webview/types';
|
||||||
import { useVSCodeContext } from '../../webview/contexts/VSCodeContext';
|
|
||||||
|
|
||||||
interface TaskMetadataSidebarProps {
|
interface TaskMetadataSidebarProps {
|
||||||
currentTask: TaskMasterTask;
|
currentTask: TaskMasterTask;
|
||||||
@@ -29,12 +28,10 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
|||||||
isRegenerating = false,
|
isRegenerating = false,
|
||||||
isAppending = false
|
isAppending = false
|
||||||
}) => {
|
}) => {
|
||||||
const { vscode } = useVSCodeContext();
|
|
||||||
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
|
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
|
||||||
const [mcpComplexityScore, setMcpComplexityScore] = useState<
|
const [mcpComplexityScore, setMcpComplexityScore] = useState<
|
||||||
number | undefined
|
number | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [isStartingTask, setIsStartingTask] = useState(false);
|
|
||||||
|
|
||||||
// Get complexity score from task
|
// Get complexity score from task
|
||||||
const currentComplexityScore = complexity?.score;
|
const currentComplexityScore = complexity?.score;
|
||||||
@@ -100,29 +97,6 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle starting a task
|
|
||||||
const handleStartTask = () => {
|
|
||||||
if (!currentTask || isStartingTask) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsStartingTask(true);
|
|
||||||
|
|
||||||
// Send message to extension to open terminal
|
|
||||||
if (vscode) {
|
|
||||||
vscode.postMessage({
|
|
||||||
type: 'openTerminal',
|
|
||||||
taskId: currentTask.id,
|
|
||||||
taskTitle: currentTask.title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset loading state after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsStartingTask(false);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Effect to handle complexity on task change
|
// Effect to handle complexity on task change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentTask?.id) {
|
if (currentTask?.id) {
|
||||||
@@ -310,30 +284,6 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
|||||||
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
|
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
|
||||||
<div className="border-b border-textSeparator-foreground" />
|
<div className="border-b border-textSeparator-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Start Task Button */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<Button
|
|
||||||
onClick={handleStartTask}
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="w-full text-xs"
|
|
||||||
disabled={
|
|
||||||
isRegenerating ||
|
|
||||||
isAppending ||
|
|
||||||
isStartingTask ||
|
|
||||||
currentTask?.status === 'done' ||
|
|
||||||
currentTask?.status === 'in-progress'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isStartingTask ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Play className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{isStartingTask ? 'Starting...' : 'Start Task'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -361,30 +361,6 @@ export class WebviewManager {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case 'openTerminal':
|
|
||||||
// Open VS Code terminal for task execution
|
|
||||||
this.logger.log(
|
|
||||||
`Opening terminal for task ${data.taskId}: ${data.taskTitle}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const terminal = vscode.window.createTerminal({
|
|
||||||
name: `Task ${data.taskId}: ${data.taskTitle}`,
|
|
||||||
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
|
||||||
});
|
|
||||||
terminal.show();
|
|
||||||
|
|
||||||
this.logger.log('Terminal created and shown successfully');
|
|
||||||
response = { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to create terminal:', error);
|
|
||||||
response = {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown message type: ${type}`);
|
throw new Error(`Unknown message type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
Clear all subtasks from all tasks globally.
|
|
||||||
|
|
||||||
## Global Subtask Clearing
|
|
||||||
|
|
||||||
Remove all subtasks across the entire project. Use with extreme caution.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
task-master clear-subtasks --all
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-Clear Analysis
|
|
||||||
|
|
||||||
1. **Project-Wide Summary**
|
|
||||||
```
|
|
||||||
Global Subtask Summary
|
|
||||||
━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total parent tasks: 12
|
|
||||||
Total subtasks: 47
|
|
||||||
- Completed: 15
|
|
||||||
- In-progress: 8
|
|
||||||
- Pending: 24
|
|
||||||
|
|
||||||
Work at risk: ~120 hours
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Critical Warnings**
|
|
||||||
- In-progress subtasks that will lose work
|
|
||||||
- Completed subtasks with valuable history
|
|
||||||
- Complex dependency chains
|
|
||||||
- Integration test results
|
|
||||||
|
|
||||||
## Double Confirmation
|
|
||||||
|
|
||||||
```
|
|
||||||
⚠️ DESTRUCTIVE OPERATION WARNING ⚠️
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
This will remove ALL 47 subtasks from your project
|
|
||||||
Including 8 in-progress and 15 completed subtasks
|
|
||||||
|
|
||||||
This action CANNOT be undone
|
|
||||||
|
|
||||||
Type 'CLEAR ALL SUBTASKS' to confirm:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smart Safeguards
|
|
||||||
|
|
||||||
- Require explicit confirmation phrase
|
|
||||||
- Create automatic backup
|
|
||||||
- Log all removed data
|
|
||||||
- Option to export first
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
Valid reasons for global clear:
|
|
||||||
- Project restructuring
|
|
||||||
- Major pivot in approach
|
|
||||||
- Starting fresh breakdown
|
|
||||||
- Switching to different task organization
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Full project analysis
|
|
||||||
2. Create backup file
|
|
||||||
3. Show detailed impact
|
|
||||||
4. Require confirmation
|
|
||||||
5. Execute removal
|
|
||||||
6. Generate summary report
|
|
||||||
|
|
||||||
## Alternative Suggestions
|
|
||||||
|
|
||||||
Before clearing all:
|
|
||||||
- Export subtasks to file
|
|
||||||
- Clear only pending subtasks
|
|
||||||
- Clear by task category
|
|
||||||
- Archive instead of delete
|
|
||||||
|
|
||||||
## Post-Clear Report
|
|
||||||
|
|
||||||
```
|
|
||||||
Global Subtask Clear Complete
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Removed: 47 subtasks from 12 tasks
|
|
||||||
Backup saved: .taskmaster/backup/subtasks-20240115.json
|
|
||||||
Parent tasks updated: 12
|
|
||||||
Time estimates adjusted: Yes
|
|
||||||
|
|
||||||
Next steps:
|
|
||||||
- Review updated task list
|
|
||||||
- Re-expand complex tasks as needed
|
|
||||||
- Check project timeline
|
|
||||||
```
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
Clear all subtasks from a specific task.
|
|
||||||
|
|
||||||
Arguments: $ARGUMENTS (task ID)
|
|
||||||
|
|
||||||
Remove all subtasks from a parent task at once.
|
|
||||||
|
|
||||||
## Clearing Subtasks
|
|
||||||
|
|
||||||
Bulk removal of all subtasks from a parent task.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
task-master clear-subtasks --id=<task-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-Clear Analysis
|
|
||||||
|
|
||||||
1. **Subtask Summary**
|
|
||||||
- Number of subtasks
|
|
||||||
- Completion status of each
|
|
||||||
- Work already done
|
|
||||||
- Dependencies affected
|
|
||||||
|
|
||||||
2. **Impact Assessment**
|
|
||||||
- Data that will be lost
|
|
||||||
- Dependencies to be removed
|
|
||||||
- Effect on project timeline
|
|
||||||
- Parent task implications
|
|
||||||
|
|
||||||
## Confirmation Required
|
|
||||||
|
|
||||||
```
|
|
||||||
Clear Subtasks Confirmation
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Parent Task: #5 "Implement user authentication"
|
|
||||||
Subtasks to remove: 4
|
|
||||||
- #5.1 "Setup auth framework" (done)
|
|
||||||
- #5.2 "Create login form" (in-progress)
|
|
||||||
- #5.3 "Add validation" (pending)
|
|
||||||
- #5.4 "Write tests" (pending)
|
|
||||||
|
|
||||||
⚠️ This will permanently delete all subtask data
|
|
||||||
Continue? (y/n)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smart Features
|
|
||||||
|
|
||||||
- Option to convert to standalone tasks
|
|
||||||
- Backup task data before clearing
|
|
||||||
- Preserve completed work history
|
|
||||||
- Update parent task appropriately
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. List all subtasks for confirmation
|
|
||||||
2. Check for in-progress work
|
|
||||||
3. Remove all subtasks
|
|
||||||
4. Update parent task
|
|
||||||
5. Clean up dependencies
|
|
||||||
|
|
||||||
## Alternative Options
|
|
||||||
|
|
||||||
Suggest alternatives:
|
|
||||||
- Convert important subtasks to tasks
|
|
||||||
- Keep completed subtasks
|
|
||||||
- Archive instead of delete
|
|
||||||
- Export subtask data first
|
|
||||||
|
|
||||||
## Post-Clear
|
|
||||||
|
|
||||||
- Show updated parent task
|
|
||||||
- Recalculate time estimates
|
|
||||||
- Update task complexity
|
|
||||||
- Suggest next steps
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```
|
|
||||||
/project:tm/clear-subtasks 5
|
|
||||||
→ Found 4 subtasks to remove
|
|
||||||
→ Warning: Subtask #5.2 is in-progress
|
|
||||||
→ Cleared all subtasks from task #5
|
|
||||||
→ Updated parent task estimates
|
|
||||||
→ Suggestion: Consider re-expanding with better breakdown
|
|
||||||
```
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user