Compare commits
1 Commits
task-maste
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80929711be |
@@ -6,14 +6,13 @@
|
||||
"repo": "eyaltoledano/claude-task-master"
|
||||
}
|
||||
],
|
||||
"commit": false,
|
||||
"commit": true,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"docs",
|
||||
"@tm/cli",
|
||||
"@tm/core",
|
||||
"@tm/build-config"
|
||||
"docs"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Testing one more pre-release iteration
|
||||
@@ -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
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "rc",
|
||||
"initialVersions": {
|
||||
"task-master-ai": "0.26.0",
|
||||
"@tm/cli": "0.26.0",
|
||||
"docs": "0.0.2",
|
||||
"extension": "0.24.2",
|
||||
"@tm/build-config": "1.0.0",
|
||||
"@tm/core": "0.26.0"
|
||||
},
|
||||
"changesets": [
|
||||
"easy-deer-heal",
|
||||
"moody-oranges-slide",
|
||||
"odd-otters-tan",
|
||||
"wild-ears-look"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
@tm/cli: add auto-update functionality to every command
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -92,9 +92,6 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
14
.github/workflows/pre-release.yml
vendored
14
.github/workflows/pre-release.yml
vendored
@@ -65,20 +65,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_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
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
@@ -41,15 +41,6 @@ jobs:
|
||||
- name: Check pre-release mode
|
||||
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
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,28 +1,5 @@
|
||||
# task-master-ai
|
||||
|
||||
## 1.0.0-rc.2
|
||||
|
||||
### Major Changes
|
||||
|
||||
- [#1217](https://github.com/eyaltoledano/claude-task-master/pull/1217) [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - @tm/cli: add auto-update functionality to every command
|
||||
|
||||
## 0.27.0-rc.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`255b9f0`](https://github.com/eyaltoledano/claude-task-master/commit/255b9f0334555b0063280abde701445cd62fa11b) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Testing one more pre-release iteration
|
||||
|
||||
## 0.27.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Test out the RC
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
|
||||
- @tm/cli@0.27.0-rc.0
|
||||
|
||||
## 0.26.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# @tm/cli
|
||||
|
||||
## 0.27.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
||||
|
||||
## 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,9 +1,8 @@
|
||||
{
|
||||
"name": "@tm/cli",
|
||||
"version": "0.27.0-rc.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Task Master CLI - Command line interface for task management",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora, { Ora } from 'ora';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
AuthManager,
|
||||
AuthenticationError,
|
||||
@@ -49,15 +49,8 @@ export class ContextCommand extends Command {
|
||||
this.addClearCommand();
|
||||
this.addSetCommand();
|
||||
|
||||
// Accept optional positional argument for brief ID or Hamster URL
|
||||
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
|
||||
|
||||
// Default action: if an argument is provided, resolve and set context; else show
|
||||
this.action(async (briefOrUrl?: string) => {
|
||||
if (briefOrUrl && briefOrUrl.trim().length > 0) {
|
||||
await this.executeSetFromBriefInput(briefOrUrl.trim());
|
||||
return;
|
||||
}
|
||||
// Default action shows current context
|
||||
this.action(async () => {
|
||||
await this.executeShow();
|
||||
});
|
||||
}
|
||||
@@ -333,7 +326,7 @@ export class ContextCommand extends Command {
|
||||
choices: [
|
||||
{ name: '(No brief - organization level)', value: null },
|
||||
...briefs.map((brief) => ({
|
||||
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||
name: `Brief ${brief.id.slice(0, 8)} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||
value: brief
|
||||
}))
|
||||
]
|
||||
@@ -448,142 +441,6 @@ export class ContextCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute setting context from a brief ID or Hamster URL
|
||||
*/
|
||||
private async executeSetFromBriefInput(briefOrUrl: string): Promise<void> {
|
||||
let spinner: Ora | undefined;
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner = ora('Resolving brief...');
|
||||
spinner.start();
|
||||
|
||||
// Extract brief ID
|
||||
const briefId = this.extractBriefId(briefOrUrl);
|
||||
if (!briefId) {
|
||||
spinner.fail('Could not extract a brief ID from the provided input');
|
||||
ui.displayError(
|
||||
`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fetch brief and resolve its organization
|
||||
const brief = await this.authManager.getBrief(briefId);
|
||||
if (!brief) {
|
||||
spinner.fail('Brief not found or you do not have access');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fetch org to get a friendly name (optional)
|
||||
let orgName: string | undefined;
|
||||
try {
|
||||
const org = await this.authManager.getOrganization(brief.accountId);
|
||||
orgName = org?.name;
|
||||
} catch {
|
||||
// Non-fatal if org lookup fails
|
||||
}
|
||||
|
||||
// Update context: set org and brief
|
||||
const briefName = `Brief ${brief.id.slice(0, 8)}`;
|
||||
await this.authManager.updateContext({
|
||||
orgId: brief.accountId,
|
||||
orgName,
|
||||
briefId: brief.id,
|
||||
briefName
|
||||
});
|
||||
|
||||
spinner.succeed('Context set from brief');
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}`
|
||||
)
|
||||
);
|
||||
|
||||
this.setLastResult({
|
||||
success: true,
|
||||
action: 'set',
|
||||
context: this.authManager.getContext() || undefined,
|
||||
message: 'Context set from brief'
|
||||
});
|
||||
} catch (error: any) {
|
||||
try {
|
||||
if (spinner?.isSpinning) spinner.stop();
|
||||
} catch {}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a brief ID from raw input (ID or Hamster URL)
|
||||
*/
|
||||
private extractBriefId(input: string): string | null {
|
||||
const raw = input?.trim() ?? '';
|
||||
if (!raw) return null;
|
||||
|
||||
const parseUrl = (s: string): URL | null => {
|
||||
try {
|
||||
return new URL(s);
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(`https://${s}`);
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fromParts = (path: string): string | null => {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const briefsIdx = parts.lastIndexOf('briefs');
|
||||
const candidate =
|
||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
||||
? parts[briefsIdx + 1]
|
||||
: parts[parts.length - 1];
|
||||
return candidate?.trim() || null;
|
||||
};
|
||||
|
||||
// 1) URL (absolute or scheme‑less)
|
||||
const url = parseUrl(raw);
|
||||
if (url) {
|
||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
||||
if (candidate) {
|
||||
// Light sanity check; let API be the final validator
|
||||
if (this.isLikelyId(candidate) || candidate.length >= 8)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Looks like a path without scheme
|
||||
if (raw.includes('/')) {
|
||||
const candidate = fromParts(raw);
|
||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: raw token
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic to check if a string looks like a brief ID (UUID-like)
|
||||
*/
|
||||
private isLikelyId(value: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; // ULID
|
||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/; // general token
|
||||
return (
|
||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context directly from options
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -8,18 +8,10 @@ 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';
|
||||
|
||||
// Auto-update utilities
|
||||
export {
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification
|
||||
} from './utils/auto-update.js';
|
||||
|
||||
// Re-export commonly used types from tm-core
|
||||
export type {
|
||||
Task,
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Auto-update utilities for task-master-ai CLI
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import https from 'https';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import boxen from 'boxen';
|
||||
import packageJson from '../../../../package.json' with { type: 'json' };
|
||||
|
||||
export interface UpdateInfo {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
needsUpdate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version from package.json
|
||||
*/
|
||||
function getCurrentVersion(): string {
|
||||
try {
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
console.warn('Could not read package.json for version info');
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semantic versions with proper pre-release handling
|
||||
* @param v1 - First version
|
||||
* @param v2 - Second version
|
||||
* @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
|
||||
*/
|
||||
function compareVersions(v1: string, v2: string): number {
|
||||
const toParts = (v: string) => {
|
||||
const [core, pre = ''] = v.split('-', 2);
|
||||
const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
||||
return { nums, pre };
|
||||
};
|
||||
|
||||
const a = toParts(v1);
|
||||
const b = toParts(v2);
|
||||
const len = Math.max(a.nums.length, b.nums.length);
|
||||
|
||||
// Compare numeric parts
|
||||
for (let i = 0; i < len; i++) {
|
||||
const d = (a.nums[i] || 0) - (b.nums[i] || 0);
|
||||
if (d !== 0) return d < 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Handle pre-release comparison
|
||||
if (a.pre && !b.pre) return -1; // prerelease < release
|
||||
if (!a.pre && b.pre) return 1; // release > prerelease
|
||||
if (a.pre === b.pre) return 0; // same or both empty
|
||||
return a.pre < b.pre ? -1 : 1; // basic prerelease tie-break
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for newer version of task-master-ai
|
||||
*/
|
||||
export async function checkForUpdate(
|
||||
currentVersionOverride?: string
|
||||
): Promise<UpdateInfo> {
|
||||
const currentVersion = currentVersionOverride || getCurrentVersion();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
hostname: 'registry.npmjs.org',
|
||||
path: '/task-master-ai',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.npm.install-v1+json',
|
||||
'User-Agent': `task-master-ai/${currentVersion}`
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode !== 200)
|
||||
throw new Error(`npm registry status ${res.statusCode}`);
|
||||
const npmData = JSON.parse(data);
|
||||
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
|
||||
|
||||
const needsUpdate =
|
||||
compareVersions(currentVersion, latestVersion) < 0;
|
||||
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
needsUpdate
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.setTimeout(3000, () => {
|
||||
req.destroy();
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display upgrade notification message
|
||||
*/
|
||||
export function displayUpgradeNotification(
|
||||
currentVersion: string,
|
||||
latestVersion: string
|
||||
) {
|
||||
const message = boxen(
|
||||
`${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` +
|
||||
`Auto-updating to the latest version with new features and bug fixes...`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
);
|
||||
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically update task-master-ai to the latest version
|
||||
*/
|
||||
export async function performAutoUpdate(
|
||||
latestVersion: string
|
||||
): Promise<boolean> {
|
||||
if (process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' || process.env.CI) {
|
||||
console.log(
|
||||
chalk.dim('Skipping auto-update (TASKMASTER_SKIP_AUTO_UPDATE/CI).')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const spinner = ora({
|
||||
text: chalk.blue(
|
||||
`Updating task-master-ai to version ${chalk.green(latestVersion)}`
|
||||
),
|
||||
spinner: 'dots',
|
||||
color: 'blue'
|
||||
}).start();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const updateProcess = spawn(
|
||||
'npm',
|
||||
[
|
||||
'install',
|
||||
'-g',
|
||||
`task-master-ai@${latestVersion}`,
|
||||
'--no-fund',
|
||||
'--no-audit',
|
||||
'--loglevel=warn'
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
}
|
||||
);
|
||||
|
||||
let errorOutput = '';
|
||||
|
||||
updateProcess.stdout.on('data', () => {
|
||||
// Update spinner text with progress
|
||||
spinner.text = chalk.blue(
|
||||
`Installing task-master-ai@${latestVersion}...`
|
||||
);
|
||||
});
|
||||
|
||||
updateProcess.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
updateProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
spinner.succeed(
|
||||
chalk.green(
|
||||
`Successfully updated to version ${chalk.bold(latestVersion)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.dim('Please restart your command to use the new version.')
|
||||
);
|
||||
resolve(true);
|
||||
} else {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Please run manually: npm install -g task-master-ai@${latestVersion}`
|
||||
)
|
||||
);
|
||||
if (errorOutput) {
|
||||
console.log(chalk.dim(`Error: ${errorOutput.trim()}`));
|
||||
}
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
updateProcess.on('error', (error) => {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
console.log(chalk.red('Error:'), error.message);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Please run manually: npm install -g task-master-ai@${latestVersion}`
|
||||
)
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -128,6 +128,23 @@ You can reorganize tasks in various ways:
|
||||
- Moving a task to a new ID position: `--from=5 --to=25` (even if task 25 doesn't exist yet)
|
||||
- Moving multiple tasks at once: `--from=10,11,12 --to=16,17,18` (must have same number of IDs, Taskmaster will look through each position)
|
||||
|
||||
#### Cross-Tag Move Safety (v0.26.0+)
|
||||
|
||||
For moves between different tags, use the enhanced dependency handling:
|
||||
|
||||
```bash
|
||||
# Move with all dependencies (safest)
|
||||
task-master move --from=5 --to=3 --with-dependencies --tag-from=feature --tag-to=main
|
||||
|
||||
# Move ignoring dependencies (shows safety tips)
|
||||
task-master move --from=5 --to=3 --ignore-dependencies --tag-from=feature --tag-to=main
|
||||
```
|
||||
|
||||
**Enhanced Safety Features:**
|
||||
- **Next Steps Guidance**: After cross-tag moves with `--ignore-dependencies`, the CLI provides "Next Steps" tips with validation and fix guidance
|
||||
- **ID Collision Help**: Dedicated help blocks appear when destination tags already have the target ID, with structured suggestions
|
||||
- **Better Error Messages**: Task collision errors now include actionable suggestions for resolution
|
||||
|
||||
When moving tasks to new IDs:
|
||||
|
||||
- The system automatically creates placeholder tasks for non-existent destination IDs
|
||||
|
||||
@@ -83,11 +83,40 @@ Taskmaster uses two primary methods for configuration:
|
||||
- `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
|
||||
- `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'.
|
||||
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI).
|
||||
- **Optional Auto-Update Control:**
|
||||
- `TASKMASTER_SKIP_AUTO_UPDATE`: Set to '1' to disable automatic updates. Also automatically disabled in CI environments (when `CI` environment variable is set).
|
||||
- `TASKMASTER_ENABLE_CODEBASE_ANALYSIS`: Control codebase analysis features (Claude Code and Gemini CLI integration). Set to 'false' to disable. Default: enabled.
|
||||
|
||||
**Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables.
|
||||
|
||||
## Codebase Analysis Configuration
|
||||
|
||||
Task Master includes advanced codebase analysis features that enhance task generation by analyzing your project structure, existing implementations, and patterns. This is particularly useful when integrated with Claude Code or using Gemini CLI provider.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
**Environment Variable Control (Priority Order: `.env` > MCP session env > `.taskmaster/config.json`)**
|
||||
|
||||
```bash
|
||||
# In .env file
|
||||
TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false # Disable codebase analysis features
|
||||
```
|
||||
|
||||
When enabled, the following commands benefit from codebase analysis:
|
||||
|
||||
- `parse-prd`: Generates tasks informed by project structure
|
||||
- `analyze-complexity`: Considers existing codebase complexity
|
||||
- `add-task`: Creates tasks aligned with project patterns
|
||||
- `update-task`: Updates tasks with relevant codebase context
|
||||
- `update-subtask`: Enhances subtasks based on implementation details
|
||||
- `expand`: Generates subtasks informed by existing code patterns
|
||||
|
||||
### Integration with Claude Code
|
||||
|
||||
When Task Master detects it's running within Claude Code, it automatically provides codebase context to generate more accurate and relevant task details. This feature can be disabled using the environment variable above.
|
||||
|
||||
### Gemini CLI Provider Enhancement
|
||||
|
||||
The Gemini CLI provider includes automatic codebase analysis when generating or updating tasks, resulting in more contextually accurate outputs aligned with your project's architecture.
|
||||
|
||||
## Tagged Task Lists Configuration (v0.17+)
|
||||
|
||||
Taskmaster includes a tagged task lists system for multi-context task management.
|
||||
@@ -149,6 +178,9 @@ PERPLEXITY_API_KEY=pplx-your-key-here
|
||||
|
||||
# Google Vertex AI Configuration (Required if using 'vertex' provider)
|
||||
# VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
|
||||
# Feature Control
|
||||
# TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false # Disable codebase analysis features
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -206,4 +206,25 @@ sidebarTitle: "CLI Commands"
|
||||
task-master init
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Move Tasks">
|
||||
```bash
|
||||
# Move a task or subtask to a new position
|
||||
task-master move --from=<id> --to=<id>
|
||||
|
||||
# Move multiple tasks at once (same number of IDs required)
|
||||
task-master move --from=1,2,3 --to=7,8,9
|
||||
|
||||
# Cross-tag moves with dependency handling
|
||||
task-master move --from=5 --to=3 --with-dependencies --tag-from=feature --tag-to=main
|
||||
task-master move --from=5 --to=3 --ignore-dependencies --tag-from=feature --tag-to=main
|
||||
|
||||
# Note: --force flag is deprecated, use --with-dependencies or --ignore-dependencies instead
|
||||
```
|
||||
|
||||
**Cross-tag Move Safety Features:**
|
||||
- CLI shows "Next Steps" guidance after moves that ignore dependencies
|
||||
- Dedicated help is displayed for ID collisions with structured suggestions
|
||||
- Better error handling with actionable suggestions for resolution
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## 0.25.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
|
||||
- task-master-ai@0.27.0-rc.0
|
||||
|
||||
## 0.24.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.25.0-rc.0",
|
||||
"version": "0.24.2",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
@@ -240,7 +240,7 @@
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"task-master-ai": "*"
|
||||
"task-master-ai": "0.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
73
output.txt
Normal file
73
output.txt
Normal file
File diff suppressed because one or more lines are too long
7271
package-lock.json
generated
7271
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.27.0-rc.1",
|
||||
"version": "0.26.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -12,7 +12,7 @@
|
||||
"workspaces": ["apps/*", "packages/*", "."],
|
||||
"scripts": {
|
||||
"build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"dev": "tsdown --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'",
|
||||
"turbo:dev": "turbo dev",
|
||||
"turbo:build": "turbo build",
|
||||
"turbo:typecheck": "turbo typecheck",
|
||||
@@ -65,6 +65,7 @@
|
||||
"@inquirer/search": "^3.0.15",
|
||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@tm/cli": "*",
|
||||
"ai": "^4.3.10",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
@@ -120,7 +121,6 @@
|
||||
"whatwg-url": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tm/cli": "*",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.28.1",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Shared build configuration for Task Master monorepo",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./dist/tsdown.base.js",
|
||||
"types": "./src/tsdown.base.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tm/core",
|
||||
"version": "0.26.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Core library for Task Master - TypeScript task management system",
|
||||
"type": "module",
|
||||
|
||||
@@ -17,14 +17,6 @@ export interface IStorage {
|
||||
*/
|
||||
loadTasks(tag?: string): Promise<Task[]>;
|
||||
|
||||
/**
|
||||
* Load a single task by ID
|
||||
* @param taskId - ID of the task to load
|
||||
* @param tag - Optional tag context for the task
|
||||
* @returns Promise that resolves to the task or null if not found
|
||||
*/
|
||||
loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||
|
||||
/**
|
||||
* Save tasks to storage, replacing existing tasks
|
||||
* @param tasks - Array of tasks to save
|
||||
@@ -183,7 +175,6 @@ export abstract class BaseStorage implements IStorage {
|
||||
|
||||
// Abstract methods that must be implemented by concrete classes
|
||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract updateTask(
|
||||
|
||||
@@ -127,7 +127,7 @@ export class TaskMapper {
|
||||
/**
|
||||
* Maps database status to internal status
|
||||
*/
|
||||
static mapStatus(
|
||||
private static mapStatus(
|
||||
status: Database['public']['Enums']['task_status']
|
||||
): Task['status'] {
|
||||
switch (status) {
|
||||
|
||||
@@ -3,30 +3,6 @@ import { Task } from '../types/index.js';
|
||||
import { Database } from '../types/database.types.js';
|
||||
import { TaskMapper } from '../mappers/TaskMapper.js';
|
||||
import { AuthManager } from '../auth/auth-manager.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Zod schema for task status validation
|
||||
const TaskStatusSchema = z.enum([
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'review',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked'
|
||||
]);
|
||||
|
||||
// Zod schema for task updates
|
||||
const TaskUpdateSchema = z
|
||||
.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
status: TaskStatusSchema.optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||
details: z.string().optional(),
|
||||
testStrategy: z.string().optional()
|
||||
})
|
||||
.partial();
|
||||
|
||||
export class SupabaseTaskRepository {
|
||||
constructor(private supabase: SupabaseClient<Database>) {}
|
||||
@@ -84,22 +60,12 @@ export class SupabaseTaskRepository {
|
||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||
}
|
||||
|
||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||
// Get the current context to determine briefId (projectId not used in Supabase context)
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
async getTask(accountId: string, taskId: string): Promise<Task | null> {
|
||||
const { data, error } = await this.supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.eq('brief_id', context.briefId)
|
||||
.eq('display_id', taskId.toUpperCase())
|
||||
.eq('account_id', accountId)
|
||||
.eq('id', taskId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
@@ -141,84 +107,4 @@ export class SupabaseTaskRepository {
|
||||
dependenciesByTaskId
|
||||
);
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
updates: Partial<Task>
|
||||
): Promise<Task> {
|
||||
// Get the current context to determine briefId
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
// Validate updates using Zod schema
|
||||
try {
|
||||
TaskUpdateSchema.parse(updates);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errorMessages = error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Convert Task fields to database fields - only include fields that actually exist in the database
|
||||
const dbUpdates: any = {};
|
||||
|
||||
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||
if (updates.description !== undefined)
|
||||
dbUpdates.description = updates.description;
|
||||
if (updates.status !== undefined)
|
||||
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
|
||||
// Skip fields that don't exist in database schema: details, testStrategy, etc.
|
||||
|
||||
// Update the task
|
||||
const { error } = await this.supabase
|
||||
.from('tasks')
|
||||
.update(dbUpdates)
|
||||
.eq('brief_id', context.briefId)
|
||||
.eq('display_id', taskId.toUpperCase());
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to update task: ${error.message}`);
|
||||
}
|
||||
|
||||
// Return the updated task by fetching it
|
||||
const updatedTask = await this.getTask(projectId, taskId);
|
||||
if (!updatedTask) {
|
||||
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
||||
}
|
||||
|
||||
return updatedTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps internal status to database status
|
||||
*/
|
||||
private mapStatusToDatabase(
|
||||
status: string
|
||||
): Database['public']['Enums']['task_status'] {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'todo';
|
||||
case 'in-progress':
|
||||
case 'in_progress': // Accept both formats
|
||||
return 'in_progress';
|
||||
case 'done':
|
||||
return 'done';
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,73 +360,4 @@ export class TaskService {
|
||||
async setActiveTag(tag: string): Promise<void> {
|
||||
await this.configManager.setActiveTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status
|
||||
*/
|
||||
async updateTaskStatus(
|
||||
taskId: string | number,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
taskId: string;
|
||||
}> {
|
||||
// Ensure we have storage
|
||||
if (!this.storage) {
|
||||
throw new TaskMasterError(
|
||||
'Storage not initialized',
|
||||
ERROR_CODES.STORAGE_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// Use provided tag or get active tag
|
||||
const activeTag = tag || this.getActiveTag();
|
||||
|
||||
const taskIdStr = String(taskId);
|
||||
|
||||
// TODO: For now, assume it's a regular task and just try to update directly
|
||||
// In the future, we can add subtask support if needed
|
||||
if (taskIdStr.includes('.')) {
|
||||
throw new TaskMasterError(
|
||||
'Subtask status updates not yet supported in API storage',
|
||||
ERROR_CODES.NOT_IMPLEMENTED
|
||||
);
|
||||
}
|
||||
|
||||
// Get the current task to get old status (simple, direct approach)
|
||||
let currentTask: Task | null;
|
||||
try {
|
||||
// Try to get the task directly
|
||||
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
`Failed to load task ${taskIdStr}`,
|
||||
ERROR_CODES.TASK_NOT_FOUND,
|
||||
{ taskId: taskIdStr },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentTask) {
|
||||
throw new TaskMasterError(
|
||||
`Task ${taskIdStr} not found`,
|
||||
ERROR_CODES.TASK_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
const oldStatus = currentTask.status;
|
||||
|
||||
// Simple, direct update - just change the status
|
||||
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId: taskIdStr
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +223,14 @@ export class ApiStorage implements IStorage {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
if (tag) {
|
||||
// Check if task is in tag
|
||||
const tagData = this.tagsCache.get(tag);
|
||||
if (!tagData || !tagData.tasks.includes(taskId)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.retryOperation(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
);
|
||||
|
||||
@@ -102,14 +102,6 @@ export class FileStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single task by ID from the tasks.json file
|
||||
*/
|
||||
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
return tasks.find((task) => task.id === taskId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tasks for a specific tag in the single tasks.json file
|
||||
*/
|
||||
|
||||
@@ -175,22 +175,6 @@ export class TaskMasterCore {
|
||||
await this.configManager.setActiveTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status
|
||||
*/
|
||||
async updateTaskStatus(
|
||||
taskId: string | number,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
taskId: string;
|
||||
}> {
|
||||
return this.taskService.updateTaskStatus(taskId, newStatus, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and cleanup resources
|
||||
*/
|
||||
|
||||
@@ -20,17 +20,14 @@ import {
|
||||
ListTasksCommand,
|
||||
ShowCommand,
|
||||
AuthCommand,
|
||||
ContextCommand,
|
||||
SetStatusCommand,
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification
|
||||
ContextCommand
|
||||
} from '@tm/cli';
|
||||
|
||||
import {
|
||||
parsePRD,
|
||||
updateTasks,
|
||||
generateTaskFiles,
|
||||
setTaskStatus,
|
||||
listTasks,
|
||||
expandTask,
|
||||
expandAllTasks,
|
||||
@@ -85,7 +82,8 @@ import {
|
||||
isConfigFilePresent,
|
||||
getAvailableModels,
|
||||
getBaseUrlForRole,
|
||||
getDefaultNumTasks
|
||||
getDefaultNumTasks,
|
||||
getDefaultSubtasks
|
||||
} from './config-manager.js';
|
||||
|
||||
import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js';
|
||||
@@ -1686,9 +1684,63 @@ function registerCommands(programInstance) {
|
||||
});
|
||||
});
|
||||
|
||||
// Register the set-status command from @tm/cli
|
||||
// Handles task status updates with proper error handling and validation
|
||||
SetStatusCommand.registerOn(programInstance);
|
||||
// set-status command
|
||||
programInstance
|
||||
.command('set-status')
|
||||
.alias('mark')
|
||||
.alias('set')
|
||||
.description('Set the status of a task')
|
||||
.option(
|
||||
'-i, --id <id>',
|
||||
'Task ID (can be comma-separated for multiple tasks)'
|
||||
)
|
||||
.option(
|
||||
'-s, --status <status>',
|
||||
`New status (one of: ${TASK_STATUS_OPTIONS.join(', ')})`
|
||||
)
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option('--tag <tag>', 'Specify tag context for task operations')
|
||||
.action(async (options) => {
|
||||
// Initialize TaskMaster
|
||||
const taskMaster = initTaskMaster({
|
||||
tasksPath: options.file || true,
|
||||
tag: options.tag
|
||||
});
|
||||
|
||||
const taskId = options.id;
|
||||
const status = options.status;
|
||||
|
||||
if (!taskId || !status) {
|
||||
console.error(chalk.red('Error: Both --id and --status are required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isValidTaskStatus(status)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
const tag = taskMaster.getCurrentTag();
|
||||
|
||||
displayCurrentTagIndicator(tag);
|
||||
|
||||
console.log(
|
||||
chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`)
|
||||
);
|
||||
|
||||
await setTaskStatus(taskMaster.getTasksPath(), taskId, status, {
|
||||
projectRoot: taskMaster.getProjectRoot(),
|
||||
tag
|
||||
});
|
||||
});
|
||||
|
||||
// NEW: Register the new list command from @tm/cli
|
||||
// This command handles all its own configuration and logic
|
||||
@@ -5115,6 +5167,122 @@ function setupCLI() {
|
||||
return programInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for newer version of task-master-ai
|
||||
* @returns {Promise<{currentVersion: string, latestVersion: string, needsUpdate: boolean}>}
|
||||
*/
|
||||
async function checkForUpdate() {
|
||||
// Get current version from package.json ONLY
|
||||
const currentVersion = getTaskMasterVersion();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Get the latest version from npm registry
|
||||
const options = {
|
||||
hostname: 'registry.npmjs.org',
|
||||
path: '/task-master-ai',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.npm.install-v1+json' // Lightweight response
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const npmData = JSON.parse(data);
|
||||
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
|
||||
|
||||
// Compare versions
|
||||
const needsUpdate =
|
||||
compareVersions(currentVersion, latestVersion) < 0;
|
||||
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
needsUpdate
|
||||
});
|
||||
} catch (error) {
|
||||
log('debug', `Error parsing npm response: ${error.message}`);
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
log('debug', `Error checking for updates: ${error.message}`);
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
// Set a timeout to avoid hanging if npm is slow
|
||||
req.setTimeout(3000, () => {
|
||||
req.abort();
|
||||
log('debug', 'Update check timed out');
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semantic versions
|
||||
* @param {string} v1 - First version
|
||||
* @param {string} v2 - Second version
|
||||
* @returns {number} -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
|
||||
*/
|
||||
function compareVersions(v1, v2) {
|
||||
const v1Parts = v1.split('.').map((p) => parseInt(p, 10));
|
||||
const v2Parts = v2.split('.').map((p) => parseInt(p, 10));
|
||||
|
||||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||
const v1Part = v1Parts[i] || 0;
|
||||
const v2Part = v2Parts[i] || 0;
|
||||
|
||||
if (v1Part < v2Part) return -1;
|
||||
if (v1Part > v2Part) return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display upgrade notification message
|
||||
* @param {string} currentVersion - Current version
|
||||
* @param {string} latestVersion - Latest version
|
||||
*/
|
||||
function displayUpgradeNotification(currentVersion, latestVersion) {
|
||||
const message = boxen(
|
||||
`${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` +
|
||||
`Run ${chalk.cyan('npm i task-master-ai@latest -g')} to update to the latest version with new features and bug fixes.`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
);
|
||||
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arguments and run the CLI
|
||||
* @param {Array} argv - Command-line arguments
|
||||
@@ -5133,8 +5301,7 @@ async function runCLI(argv = process.argv) {
|
||||
}
|
||||
|
||||
// Start the update check in the background - don't await yet
|
||||
const currentVersion = getTaskMasterVersion();
|
||||
const updateCheckPromise = checkForUpdate(currentVersion);
|
||||
const updateCheckPromise = checkForUpdate();
|
||||
|
||||
// Setup and parse
|
||||
// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
|
||||
@@ -5145,18 +5312,10 @@ async function runCLI(argv = process.argv) {
|
||||
// After command execution, check if an update is available
|
||||
const updateInfo = await updateCheckPromise;
|
||||
if (updateInfo.needsUpdate) {
|
||||
// Display the upgrade notification first
|
||||
displayUpgradeNotification(
|
||||
updateInfo.currentVersion,
|
||||
updateInfo.latestVersion
|
||||
);
|
||||
|
||||
// Then automatically perform the update
|
||||
const updateSuccess = await performAutoUpdate(updateInfo.latestVersion);
|
||||
if (updateSuccess) {
|
||||
// Exit gracefully after successful update
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if migration has occurred and show FYI notice once
|
||||
@@ -5280,4 +5439,11 @@ export function resolveComplexityReportPath({
|
||||
return tag !== 'master' ? base.replace('.json', `_${tag}.json`) : base;
|
||||
}
|
||||
|
||||
export { registerCommands, setupCLI, runCLI };
|
||||
export {
|
||||
registerCommands,
|
||||
setupCLI,
|
||||
runCLI,
|
||||
checkForUpdate,
|
||||
compareVersions,
|
||||
displayUpgradeNotification
|
||||
};
|
||||
|
||||
@@ -25,8 +25,6 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
// Create test directory
|
||||
testDir = fs.mkdtempSync(path.join(__dirname, 'test-'));
|
||||
process.chdir(testDir);
|
||||
// Keep integration timings deterministic
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
|
||||
|
||||
// Initialize task-master
|
||||
execSync(`node ${binPath} init --yes`, {
|
||||
@@ -139,7 +137,6 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
|
||||
});
|
||||
|
||||
describe('Circular Dependency Detection', () => {
|
||||
@@ -372,7 +369,7 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(largeTaskSet, null, 2));
|
||||
// Should complete within reasonable time
|
||||
const timeout = process.env.CI ? 11000 : 6000;
|
||||
const timeout = process.env.CI ? 10000 : 5000;
|
||||
const startTime = Date.now();
|
||||
execSync(
|
||||
`node ${binPath} move --from=50 --from-tag=master --to-tag=in-progress --with-dependencies`,
|
||||
|
||||
@@ -280,26 +280,8 @@ describe('Version comparison utility', () => {
|
||||
let compareVersions;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Import from @tm/cli instead of commands.js
|
||||
const { compareVersions: cv } = await import(
|
||||
'../../apps/cli/src/utils/auto-update.js'
|
||||
);
|
||||
|
||||
// Create a local compareVersions function for testing
|
||||
compareVersions = (v1, v2) => {
|
||||
const v1Parts = v1.split('.').map((p) => parseInt(p, 10));
|
||||
const v2Parts = v2.split('.').map((p) => parseInt(p, 10));
|
||||
|
||||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||
const v1Part = v1Parts[i] || 0;
|
||||
const v2Part = v2Parts[i] || 0;
|
||||
|
||||
if (v1Part < v2Part) return -1;
|
||||
if (v1Part > v2Part) return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
const commandsModule = await import('../../scripts/modules/commands.js');
|
||||
compareVersions = commandsModule.compareVersions;
|
||||
});
|
||||
|
||||
test('compareVersions correctly compares semantic versions', () => {
|
||||
@@ -321,9 +303,8 @@ describe('Update check functionality', () => {
|
||||
let consoleLogSpy;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Import from @tm/cli instead of commands.js
|
||||
const cliModule = await import('../../apps/cli/src/utils/auto-update.js');
|
||||
displayUpgradeNotification = cliModule.displayUpgradeNotification;
|
||||
const commandsModule = await import('../../scripts/modules/commands.js');
|
||||
displayUpgradeNotification = commandsModule.displayUpgradeNotification;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -24,7 +24,6 @@ export default defineConfig(
|
||||
},
|
||||
outDir: 'dist',
|
||||
copy: ['public'],
|
||||
ignoreWatch: ['node_modules', 'dist', 'tests'],
|
||||
// Bundle only our workspace packages, keep npm dependencies external
|
||||
noExternal: [/^@tm\//],
|
||||
env: getBuildTimeEnvs()
|
||||
|
||||
Reference in New Issue
Block a user