Compare commits
5 Commits
task-maste
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9acb900153 | ||
|
|
c4f5d89e72 | ||
|
|
e308cf4f46 | ||
|
|
11b7354010 | ||
|
|
4c1ef2ca94 |
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add changelog highlights to auto-update notifications
|
|
||||||
|
|
||||||
When the CLI auto-updates to a new version, it now displays a "What's New" section.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add Claude Code plugin with marketplace distribution
|
|
||||||
|
|
||||||
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
|
|
||||||
|
|
||||||
## 🎉 New: Claude Code Plugin
|
|
||||||
|
|
||||||
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
|
|
||||||
|
|
||||||
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
|
|
||||||
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
|
|
||||||
- **MCP server integration** for deep Claude Code integration
|
|
||||||
|
|
||||||
**Installation:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/plugin marketplace add eyaltoledano/claude-task-master
|
|
||||||
/plugin install taskmaster@taskmaster
|
|
||||||
```
|
|
||||||
|
|
||||||
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
|
|
||||||
|
|
||||||
- Shows plugin installation instructions
|
|
||||||
- Only manages CLAUDE.md imports for agent instructions
|
|
||||||
- Directs users to install the official plugin
|
|
||||||
|
|
||||||
**Migration for Existing Users:**
|
|
||||||
|
|
||||||
If you previously used `rules add claude`:
|
|
||||||
|
|
||||||
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
|
|
||||||
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
|
|
||||||
3. remove old `.claude/commands/` and `.claude/agents/` directories
|
|
||||||
|
|
||||||
**Why This Change?**
|
|
||||||
|
|
||||||
Claude Code plugins provide:
|
|
||||||
|
|
||||||
- ✅ Automatic updates when we release new features
|
|
||||||
- ✅ Better command organization and naming
|
|
||||||
- ✅ Seamless integration with Claude Code
|
|
||||||
- ✅ No manual file copying or management
|
|
||||||
|
|
||||||
The plugin system is the future of Task Master AI integration with Claude Code!
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
|
|
||||||
|
|
||||||
Key features:
|
|
||||||
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
|
|
||||||
- Inline instructions at decision points guide AI through each section
|
|
||||||
- Good/bad examples for immediate pattern matching
|
|
||||||
- Flexible plain-text format with XML-style tags for parseability
|
|
||||||
- Critical dependency-graph section ensures correct task ordering
|
|
||||||
- Automatic inclusion during `task-master init`
|
|
||||||
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
|
|
||||||
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
|
|
||||||
|
|
||||||
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix cross-level task dependencies not being saved
|
|
||||||
|
|
||||||
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"mode": "exit",
|
|
||||||
"tag": "rc",
|
|
||||||
"initialVersions": {
|
|
||||||
"task-master-ai": "0.28.0",
|
|
||||||
"@tm/cli": "",
|
|
||||||
"docs": "0.0.5",
|
|
||||||
"extension": "0.25.5",
|
|
||||||
"@tm/ai-sdk-provider-grok-cli": "",
|
|
||||||
"@tm/build-config": "",
|
|
||||||
"@tm/claude-code-plugin": "0.0.1",
|
|
||||||
"@tm/core": ""
|
|
||||||
},
|
|
||||||
"changesets": [
|
|
||||||
"auto-update-changelog-highlights",
|
|
||||||
"mean-planes-wave",
|
|
||||||
"nice-ways-hope",
|
|
||||||
"plain-falcons-serve",
|
|
||||||
"smart-owls-relax"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Improve refresh token when authenticating
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
|
|
||||||
|
|
||||||
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
|
|
||||||
|
|
||||||
Key improvements:
|
|
||||||
- Automatic integration with complexity analysis reports
|
|
||||||
- Tag-aware complexity report path resolution
|
|
||||||
- Intelligent subtask count determination based on task complexity
|
|
||||||
- Falls back to defaults when complexity analysis is unavailable
|
|
||||||
- Enhanced logging for better visibility into expansion decisions
|
|
||||||
|
|
||||||
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
|
|
||||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -1,5 +1,94 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
|
## 0.29.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#1286](https://github.com/eyaltoledano/claude-task-master/pull/1286) [`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add changelog highlights to auto-update notifications
|
||||||
|
|
||||||
|
When the CLI auto-updates to a new version, it now displays a "What's New" section.
|
||||||
|
|
||||||
|
- [#1293](https://github.com/eyaltoledano/claude-task-master/pull/1293) [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add Claude Code plugin with marketplace distribution
|
||||||
|
|
||||||
|
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
|
||||||
|
|
||||||
|
## 🎉 New: Claude Code Plugin
|
||||||
|
|
||||||
|
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
|
||||||
|
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
|
||||||
|
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
|
||||||
|
- **MCP server integration** for deep Claude Code integration
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/plugin marketplace add eyaltoledano/claude-task-master
|
||||||
|
/plugin install taskmaster@taskmaster
|
||||||
|
```
|
||||||
|
|
||||||
|
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
|
||||||
|
- Shows plugin installation instructions
|
||||||
|
- Only manages CLAUDE.md imports for agent instructions
|
||||||
|
- Directs users to install the official plugin
|
||||||
|
|
||||||
|
**Migration for Existing Users:**
|
||||||
|
|
||||||
|
If you previously used `rules add claude`:
|
||||||
|
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
|
||||||
|
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
|
||||||
|
3. remove old `.claude/commands/` and `.claude/agents/` directories
|
||||||
|
|
||||||
|
**Why This Change?**
|
||||||
|
|
||||||
|
Claude Code plugins provide:
|
||||||
|
- ✅ Automatic updates when we release new features
|
||||||
|
- ✅ Better command organization and naming
|
||||||
|
- ✅ Seamless integration with Claude Code
|
||||||
|
- ✅ No manual file copying or management
|
||||||
|
|
||||||
|
The plugin system is the future of Task Master AI integration with Claude Code!
|
||||||
|
|
||||||
|
- [#1285](https://github.com/eyaltoledano/claude-task-master/pull/1285) [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
|
||||||
|
- Inline instructions at decision points guide AI through each section
|
||||||
|
- Good/bad examples for immediate pattern matching
|
||||||
|
- Flexible plain-text format with XML-style tags for parseability
|
||||||
|
- Critical dependency-graph section ensures correct task ordering
|
||||||
|
- Automatic inclusion during `task-master init`
|
||||||
|
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
|
||||||
|
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
|
||||||
|
|
||||||
|
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
|
||||||
|
|
||||||
|
- [#1287](https://github.com/eyaltoledano/claude-task-master/pull/1287) [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
|
||||||
|
|
||||||
|
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
|
||||||
|
|
||||||
|
Key improvements:
|
||||||
|
- Automatic integration with complexity analysis reports
|
||||||
|
- Tag-aware complexity report path resolution
|
||||||
|
- Intelligent subtask count determination based on task complexity
|
||||||
|
- Falls back to defaults when complexity analysis is unavailable
|
||||||
|
- Enhanced logging for better visibility into expansion decisions
|
||||||
|
|
||||||
|
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1191](https://github.com/eyaltoledano/claude-task-master/pull/1191) [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix cross-level task dependencies not being saved
|
||||||
|
|
||||||
|
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
|
||||||
|
|
||||||
|
- [#1299](https://github.com/eyaltoledano/claude-task-master/pull/1299) [`4c1ef2c`](https://github.com/eyaltoledano/claude-task-master/commit/4c1ef2ca94411c53bcd2a78ec710b06c500236dd) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve refresh token when authenticating
|
||||||
|
|
||||||
|
## 0.29.0-rc.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1299](https://github.com/eyaltoledano/claude-task-master/pull/1299) [`a6c5152`](https://github.com/eyaltoledano/claude-task-master/commit/a6c5152f20edd8717cf1aea34e7c178b1261aa99) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve refresh token when authenticating
|
||||||
|
|
||||||
## 0.29.0-rc.0
|
## 0.29.0-rc.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @tm/core@null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
- Updated dependencies []:
|
- Updated dependencies []:
|
||||||
- @tm/core@null
|
- @tm/core@null
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# docs
|
# docs
|
||||||
|
|
||||||
|
## 0.0.6
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
## 0.0.4
|
## 0.0.4
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "docs",
|
"name": "docs",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Task Master documentation powered by Mintlify",
|
"description": "Task Master documentation powered by Mintlify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.25.6
|
||||||
|
|
||||||
## 0.25.6-rc.0
|
## 0.25.6-rc.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.6-rc.0",
|
"version": "0.25.6",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0-rc.0",
|
"version": "0.29.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0-rc.0",
|
"version": "0.29.0",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -125,13 +125,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/docs": {
|
"apps/docs": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mintlify": "^4.2.111"
|
"mintlify": "^4.2.111"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.6-rc.0",
|
"version": "0.25.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
@@ -27136,7 +27136,7 @@
|
|||||||
},
|
},
|
||||||
"packages/claude-code-plugin": {
|
"packages/claude-code-plugin": {
|
||||||
"name": "@tm/claude-code-plugin",
|
"name": "@tm/claude-code-plugin",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"license": "MIT WITH Commons-Clause"
|
"license": "MIT WITH Commons-Clause"
|
||||||
},
|
},
|
||||||
"packages/tm-core": {
|
"packages/tm-core": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0-rc.0",
|
"version": "0.29.0",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# @tm/ai-sdk-provider-grok-cli
|
# @tm/ai-sdk-provider-grok-cli
|
||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|||||||
@@ -4,4 +4,6 @@
|
|||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|||||||
3
packages/claude-code-plugin/CHANGELOG.md
Normal file
3
packages/claude-code-plugin/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# @tm/claude-code-plugin
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/claude-code-plugin",
|
"name": "@tm/claude-code-plugin",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
|
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|
||||||
## 0.26.1
|
## 0.26.1
|
||||||
|
|
||||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||||
|
|||||||
@@ -21,11 +21,16 @@ const CredentialStoreSpy = vi.fn();
|
|||||||
vi.mock('./credential-store.js', () => {
|
vi.mock('./credential-store.js', () => {
|
||||||
return {
|
return {
|
||||||
CredentialStore: class {
|
CredentialStore: class {
|
||||||
|
static getInstance(config?: any) {
|
||||||
|
return new (this as any)(config);
|
||||||
|
}
|
||||||
|
static resetInstance() {
|
||||||
|
// Mock reset instance method
|
||||||
|
}
|
||||||
constructor(config: any) {
|
constructor(config: any) {
|
||||||
CredentialStoreSpy(config);
|
CredentialStoreSpy(config);
|
||||||
this.getCredentials = vi.fn(() => null);
|
|
||||||
}
|
}
|
||||||
getCredentials() {
|
getCredentials(_options?: any) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
saveCredentials() {}
|
saveCredentials() {}
|
||||||
@@ -85,7 +90,7 @@ describe('AuthManager Singleton', () => {
|
|||||||
expect(instance1).toBe(instance2);
|
expect(instance1).toBe(instance2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use config on first call', () => {
|
it('should use config on first call', async () => {
|
||||||
const config = {
|
const config = {
|
||||||
baseUrl: 'https://test.auth.com',
|
baseUrl: 'https://test.auth.com',
|
||||||
configDir: '/test/config',
|
configDir: '/test/config',
|
||||||
@@ -101,7 +106,7 @@ describe('AuthManager Singleton', () => {
|
|||||||
|
|
||||||
// Verify the config is passed to internal components through observable behavior
|
// Verify the config is passed to internal components through observable behavior
|
||||||
// getCredentials would look in the configured file path
|
// getCredentials would look in the configured file path
|
||||||
const credentials = instance.getCredentials();
|
const credentials = await instance.getCredentials();
|
||||||
expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly
|
expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ export class AuthManager {
|
|||||||
private supabaseClient: SupabaseAuthClient;
|
private supabaseClient: SupabaseAuthClient;
|
||||||
private organizationService?: OrganizationService;
|
private organizationService?: OrganizationService;
|
||||||
private logger = getLogger('AuthManager');
|
private logger = getLogger('AuthManager');
|
||||||
|
private refreshPromise: Promise<AuthCredentials> | null = null;
|
||||||
|
|
||||||
private constructor(config?: Partial<AuthConfig>) {
|
private constructor(config?: Partial<AuthConfig>) {
|
||||||
this.credentialStore = CredentialStore.getInstance(config);
|
this.credentialStore = CredentialStore.getInstance(config);
|
||||||
@@ -37,7 +38,10 @@ export class AuthManager {
|
|||||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||||
|
|
||||||
// Initialize Supabase client with session restoration
|
// Initialize Supabase client with session restoration
|
||||||
this.initializeSupabaseSession();
|
// Fire-and-forget with catch handler to prevent unhandled rejections
|
||||||
|
this.initializeSupabaseSession().catch(() => {
|
||||||
|
// Errors are already logged in initializeSupabaseSession
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,38 +86,49 @@ export class AuthManager {
|
|||||||
* Automatically refreshes the token if expired
|
* Automatically refreshes the token if expired
|
||||||
*/
|
*/
|
||||||
async getCredentials(): Promise<AuthCredentials | null> {
|
async getCredentials(): Promise<AuthCredentials | null> {
|
||||||
const credentials = this.credentialStore.getCredentials();
|
const credentials = this.credentialStore.getCredentials({
|
||||||
|
allowExpired: true
|
||||||
|
});
|
||||||
|
|
||||||
// If credentials exist but are expired, try to refresh
|
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
const expiredCredentials = this.credentialStore.getCredentials({
|
return null;
|
||||||
allowExpired: true
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Check if we have any credentials at all
|
// Check if credentials are expired (with 30-second clock skew buffer)
|
||||||
if (!expiredCredentials) {
|
const CLOCK_SKEW_MS = 30_000;
|
||||||
// No credentials found
|
const isExpired = credentials.expiresAt
|
||||||
return null;
|
? new Date(credentials.expiresAt).getTime() <= Date.now() + CLOCK_SKEW_MS
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// If expired and we have a refresh token, attempt refresh
|
||||||
|
if (isExpired && credentials.refreshToken) {
|
||||||
|
// Return existing refresh promise if one is in progress
|
||||||
|
if (this.refreshPromise) {
|
||||||
|
try {
|
||||||
|
return await this.refreshPromise;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if refresh token is available
|
|
||||||
if (!expiredCredentials.refreshToken) {
|
|
||||||
this.logger.warn(
|
|
||||||
'Token expired but no refresh token available. Please re-authenticate.'
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt refresh
|
|
||||||
try {
|
try {
|
||||||
this.logger.info('Token expired, attempting automatic refresh...');
|
this.logger.info('Token expired, attempting automatic refresh...');
|
||||||
return await this.refreshToken();
|
this.refreshPromise = this.refreshToken();
|
||||||
|
const result = await this.refreshPromise;
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn('Automatic token refresh failed:', error);
|
this.logger.warn('Automatic token refresh failed:', error);
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.refreshPromise = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return null if expired and no refresh token
|
||||||
|
if (isExpired) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export class ExportService {
|
|||||||
|
|
||||||
if (useAPIEndpoint) {
|
if (useAPIEndpoint) {
|
||||||
// Use the new bulk import API endpoint
|
// Use the new bulk import API endpoint
|
||||||
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
|
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks`;
|
||||||
|
|
||||||
// Transform tasks to flat structure for API
|
// Transform tasks to flat structure for API
|
||||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
const flatTasks = this.transformTasksForBulkImport(tasks);
|
||||||
@@ -370,11 +370,11 @@ export class ExportService {
|
|||||||
// Prepare request body
|
// Prepare request body
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
source: 'task-master-cli',
|
source: 'task-master-cli',
|
||||||
accountId: orgId,
|
|
||||||
options: {
|
options: {
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
stopOnError: false
|
stopOnError: false
|
||||||
},
|
},
|
||||||
|
accountId: orgId,
|
||||||
tasks: flatTasks
|
tasks: flatTasks
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
176
packages/tm-core/tests/auth/auth-refresh.test.ts
Normal file
176
packages/tm-core/tests/auth/auth-refresh.test.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import type { Session } from '@supabase/supabase-js';
|
||||||
|
import { AuthManager } from '../../src/auth/auth-manager';
|
||||||
|
import { CredentialStore } from '../../src/auth/credential-store';
|
||||||
|
import type { AuthCredentials } from '../../src/auth/types';
|
||||||
|
|
||||||
|
describe('AuthManager Token Refresh', () => {
|
||||||
|
let authManager: AuthManager;
|
||||||
|
let credentialStore: CredentialStore;
|
||||||
|
let tmpDir: string;
|
||||||
|
let authFile: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset singletons
|
||||||
|
AuthManager.resetInstance();
|
||||||
|
CredentialStore.resetInstance();
|
||||||
|
|
||||||
|
// Create temporary directory for test isolation
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-refresh-'));
|
||||||
|
authFile = path.join(tmpDir, 'auth.json');
|
||||||
|
|
||||||
|
// Initialize AuthManager with test config (this will create CredentialStore internally)
|
||||||
|
authManager = AuthManager.getInstance({
|
||||||
|
configDir: tmpDir,
|
||||||
|
configFile: authFile
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the CredentialStore instance that AuthManager created
|
||||||
|
credentialStore = CredentialStore.getInstance();
|
||||||
|
credentialStore.clearCredentials();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up
|
||||||
|
try {
|
||||||
|
credentialStore.clearCredentials();
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
AuthManager.resetInstance();
|
||||||
|
CredentialStore.resetInstance();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
|
||||||
|
// Remove temporary directory
|
||||||
|
if (tmpDir && fs.existsSync(tmpDir)) {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not make concurrent refresh requests', async () => {
|
||||||
|
// Set up expired credentials with refresh token
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired_access_token',
|
||||||
|
refreshToken: 'valid_refresh_token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
// Mock the refreshToken method to track calls
|
||||||
|
const refreshTokenSpy = vi.spyOn(authManager as any, 'refreshToken');
|
||||||
|
const mockSession: Session = {
|
||||||
|
access_token: 'new_access_token',
|
||||||
|
refresh_token: 'new_refresh_token',
|
||||||
|
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||||
|
user: {
|
||||||
|
id: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
app_metadata: {},
|
||||||
|
user_metadata: {},
|
||||||
|
aud: 'authenticated',
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshTokenSpy.mockResolvedValue({
|
||||||
|
token: mockSession.access_token,
|
||||||
|
refreshToken: mockSession.refresh_token,
|
||||||
|
userId: mockSession.user.id,
|
||||||
|
email: mockSession.user.email,
|
||||||
|
expiresAt: new Date(mockSession.expires_at! * 1000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make multiple concurrent calls to getCredentials
|
||||||
|
const promises = [
|
||||||
|
authManager.getCredentials(),
|
||||||
|
authManager.getCredentials(),
|
||||||
|
authManager.getCredentials()
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
// Verify all calls returned the same new credentials
|
||||||
|
expect(results[0]?.token).toBe('new_access_token');
|
||||||
|
expect(results[1]?.token).toBe('new_access_token');
|
||||||
|
expect(results[2]?.token).toBe('new_access_token');
|
||||||
|
|
||||||
|
// Verify refreshToken was only called once, not three times
|
||||||
|
expect(refreshTokenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return valid credentials without attempting refresh', async () => {
|
||||||
|
// Set up valid (non-expired) credentials
|
||||||
|
const validCredentials: AuthCredentials = {
|
||||||
|
token: 'valid_access_token',
|
||||||
|
refreshToken: 'valid_refresh_token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 3600000).toISOString(), // Expires in 1 hour
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(validCredentials);
|
||||||
|
|
||||||
|
// Spy on refreshToken to ensure it's not called
|
||||||
|
const refreshTokenSpy = vi.spyOn(authManager as any, 'refreshToken');
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(credentials?.token).toBe('valid_access_token');
|
||||||
|
expect(refreshTokenSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if credentials are expired with no refresh token', async () => {
|
||||||
|
// Set up expired credentials WITHOUT refresh token
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired_access_token',
|
||||||
|
refreshToken: undefined,
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if no credentials exist', async () => {
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle refresh failures gracefully', async () => {
|
||||||
|
// Set up expired credentials with refresh token
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired_access_token',
|
||||||
|
refreshToken: 'invalid_refresh_token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 1000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
// Mock refreshToken to throw an error
|
||||||
|
const refreshTokenSpy = vi.spyOn(authManager as any, 'refreshToken');
|
||||||
|
refreshTokenSpy.mockRejectedValue(new Error('Refresh failed'));
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
expect(refreshTokenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
import type { Session } from '@supabase/supabase-js';
|
import type { Session } from '@supabase/supabase-js';
|
||||||
import { AuthManager } from '../../src/auth/auth-manager';
|
import { AuthManager } from '../../src/auth/auth-manager';
|
||||||
import { CredentialStore } from '../../src/auth/credential-store';
|
import { CredentialStore } from '../../src/auth/credential-store';
|
||||||
@@ -14,6 +17,8 @@ import type { AuthCredentials } from '../../src/auth/types';
|
|||||||
describe('AuthManager - Token Auto-Refresh Integration', () => {
|
describe('AuthManager - Token Auto-Refresh Integration', () => {
|
||||||
let authManager: AuthManager;
|
let authManager: AuthManager;
|
||||||
let credentialStore: CredentialStore;
|
let credentialStore: CredentialStore;
|
||||||
|
let tmpDir: string;
|
||||||
|
let authFile: string;
|
||||||
|
|
||||||
// Mock Supabase session that will be returned on refresh
|
// Mock Supabase session that will be returned on refresh
|
||||||
const mockRefreshedSession: Session = {
|
const mockRefreshedSession: Session = {
|
||||||
@@ -34,10 +39,21 @@ describe('AuthManager - Token Auto-Refresh Integration', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset AuthManager singleton
|
// Reset singletons
|
||||||
AuthManager.resetInstance();
|
AuthManager.resetInstance();
|
||||||
|
CredentialStore.resetInstance();
|
||||||
|
|
||||||
// Clear any existing credentials
|
// Create temporary directory for test isolation
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-integration-'));
|
||||||
|
authFile = path.join(tmpDir, 'auth.json');
|
||||||
|
|
||||||
|
// Initialize AuthManager with test config (this will create CredentialStore internally)
|
||||||
|
authManager = AuthManager.getInstance({
|
||||||
|
configDir: tmpDir,
|
||||||
|
configFile: authFile
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the CredentialStore instance that AuthManager created
|
||||||
credentialStore = CredentialStore.getInstance();
|
credentialStore = CredentialStore.getInstance();
|
||||||
credentialStore.clearCredentials();
|
credentialStore.clearCredentials();
|
||||||
});
|
});
|
||||||
@@ -50,7 +66,13 @@ describe('AuthManager - Token Auto-Refresh Integration', () => {
|
|||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
}
|
}
|
||||||
AuthManager.resetInstance();
|
AuthManager.resetInstance();
|
||||||
|
CredentialStore.resetInstance();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
|
||||||
|
// Remove temporary directory
|
||||||
|
if (tmpDir && fs.existsSync(tmpDir)) {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Expired Token Detection', () => {
|
describe('Expired Token Detection', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user