Compare commits
7 Commits
docs/auto-
...
extension@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9acb900153 | ||
|
|
c4f5d89e72 | ||
|
|
e308cf4f46 | ||
|
|
11b7354010 | ||
|
|
4c1ef2ca94 | ||
|
|
663aa2dfe9 | ||
|
|
8f60a0561e |
@@ -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.
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"docs"
|
"docs",
|
||||||
|
"@tm/claude-code-plugin"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -310,6 +310,12 @@ cd claude-task-master
|
|||||||
node scripts/init.js
|
node scripts/init.js
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Join Our Team
|
||||||
|
|
||||||
|
<a href="https://tryhamster.com" target="_blank">
|
||||||
|
<img src="./images/hamster-hiring.png" alt="Join Hamster's founding team" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
<a href="https://github.com/eyaltoledano/claude-task-master/graphs/contributors">
|
<a href="https://github.com/eyaltoledano/claude-task-master/graphs/contributors">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class AuthCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
private async executeStatus(): Promise<void> {
|
private async executeStatus(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = this.displayStatus();
|
const result = await this.displayStatus();
|
||||||
this.setLastResult(result);
|
this.setLastResult(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
@@ -171,8 +171,8 @@ export class AuthCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Display authentication status
|
* Display authentication status
|
||||||
*/
|
*/
|
||||||
private displayStatus(): AuthResult {
|
private async displayStatus(): Promise<AuthResult> {
|
||||||
const credentials = this.authManager.getCredentials();
|
const credentials = await this.authManager.getCredentials();
|
||||||
|
|
||||||
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
||||||
|
|
||||||
@@ -187,19 +187,29 @@ export class AuthCommand extends Command {
|
|||||||
if (credentials.expiresAt) {
|
if (credentials.expiresAt) {
|
||||||
const expiresAt = new Date(credentials.expiresAt);
|
const expiresAt = new Date(credentials.expiresAt);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const hoursRemaining = Math.floor(
|
const timeRemaining = expiresAt.getTime() - now.getTime();
|
||||||
(expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)
|
const hoursRemaining = Math.floor(timeRemaining / (1000 * 60 * 60));
|
||||||
);
|
const minutesRemaining = Math.floor(timeRemaining / (1000 * 60));
|
||||||
|
|
||||||
|
if (timeRemaining > 0) {
|
||||||
|
// Token is still valid
|
||||||
if (hoursRemaining > 0) {
|
if (hoursRemaining > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.gray(
|
chalk.gray(
|
||||||
` Expires: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
|
` Expires at: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow(` Token expired at: ${expiresAt.toLocaleString()}`)
|
chalk.gray(
|
||||||
|
` Expires at: ${expiresAt.toLocaleString()} (${minutesRemaining} minutes remaining)`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Token has expired
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(` Expired at: ${expiresAt.toLocaleString()}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -315,7 +325,7 @@ export class AuthCommand extends Command {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!continueAuth) {
|
if (!continueAuth) {
|
||||||
const credentials = this.authManager.getCredentials();
|
const credentials = await this.authManager.getCredentials();
|
||||||
ui.displaySuccess('Using existing authentication');
|
ui.displaySuccess('Using existing authentication');
|
||||||
|
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
@@ -480,7 +490,7 @@ export class AuthCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Get current credentials (for programmatic usage)
|
* Get current credentials (for programmatic usage)
|
||||||
*/
|
*/
|
||||||
getCredentials(): AuthCredentials | null {
|
getCredentials(): Promise<AuthCredentials | null> {
|
||||||
return this.authManager.getCredentials();
|
return this.authManager.getCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export class ContextCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
private async executeShow(): Promise<void> {
|
private async executeShow(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = this.displayContext();
|
const result = await this.displayContext();
|
||||||
this.setLastResult(result);
|
this.setLastResult(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
@@ -126,7 +126,7 @@ export class ContextCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Display current context
|
* Display current context
|
||||||
*/
|
*/
|
||||||
private displayContext(): ContextResult {
|
private async displayContext(): Promise<ContextResult> {
|
||||||
// Check authentication first
|
// Check authentication first
|
||||||
if (!this.authManager.isAuthenticated()) {
|
if (!this.authManager.isAuthenticated()) {
|
||||||
console.log(chalk.yellow('✗ Not authenticated'));
|
console.log(chalk.yellow('✗ Not authenticated'));
|
||||||
@@ -139,7 +139,7 @@ export class ContextCommand extends Command {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = this.authManager.getContext();
|
const context = await this.authManager.getContext();
|
||||||
|
|
||||||
console.log(chalk.cyan('\n🌍 Workspace Context\n'));
|
console.log(chalk.cyan('\n🌍 Workspace Context\n'));
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'select-org',
|
action: 'select-org',
|
||||||
context: this.authManager.getContext() || undefined,
|
context: (await this.authManager.getContext()) || undefined,
|
||||||
message: `Selected organization: ${selectedOrg.name}`
|
message: `Selected organization: ${selectedOrg.name}`
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -284,7 +284,7 @@ export class ContextCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if org is selected
|
// Check if org is selected
|
||||||
const context = this.authManager.getContext();
|
const context = await this.authManager.getContext();
|
||||||
if (!context?.orgId) {
|
if (!context?.orgId) {
|
||||||
ui.displayError(
|
ui.displayError(
|
||||||
'No organization selected. Run "tm context org" first.'
|
'No organization selected. Run "tm context org" first.'
|
||||||
@@ -353,7 +353,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'select-brief',
|
action: 'select-brief',
|
||||||
context: this.authManager.getContext() || undefined,
|
context: (await this.authManager.getContext()) || undefined,
|
||||||
message: `Selected brief: ${selectedBrief.name}`
|
message: `Selected brief: ${selectedBrief.name}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -368,7 +368,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'select-brief',
|
action: 'select-brief',
|
||||||
context: this.authManager.getContext() || undefined,
|
context: (await this.authManager.getContext()) || undefined,
|
||||||
message: 'Cleared brief selection'
|
message: 'Cleared brief selection'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ export class ContextCommand extends Command {
|
|||||||
this.setLastResult({
|
this.setLastResult({
|
||||||
success: true,
|
success: true,
|
||||||
action: 'set',
|
action: 'set',
|
||||||
context: this.authManager.getContext() || undefined,
|
context: (await this.authManager.getContext()) || undefined,
|
||||||
message: 'Context set from brief'
|
message: 'Context set from brief'
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -631,7 +631,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'set',
|
action: 'set',
|
||||||
context: this.authManager.getContext() || undefined,
|
context: (await this.authManager.getContext()) || undefined,
|
||||||
message: 'Context updated'
|
message: 'Context updated'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -682,7 +682,7 @@ export class ContextCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Get current context (for programmatic usage)
|
* Get current context (for programmatic usage)
|
||||||
*/
|
*/
|
||||||
getContext(): UserContext | null {
|
getContext(): Promise<UserContext | null> {
|
||||||
return this.authManager.getContext();
|
return this.authManager.getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class ExportCommand extends Command {
|
|||||||
await this.initializeServices();
|
await this.initializeServices();
|
||||||
|
|
||||||
// Get current context
|
// Get current context
|
||||||
const context = this.authManager.getContext();
|
const context = await this.authManager.getContext();
|
||||||
|
|
||||||
// Determine org and brief IDs
|
// Determine org and brief IDs
|
||||||
let orgId = options?.org || context?.orgId;
|
let orgId = options?.org || context?.orgId;
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -239,9 +239,6 @@
|
|||||||
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
|
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"task-master-ai": "0.29.0-rc.0"
|
|
||||||
},
|
|
||||||
"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",
|
||||||
@@ -277,7 +274,8 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"@tm/core": "*"
|
"@tm/core": "*",
|
||||||
|
"task-master-ai": "*"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"glob@<8": "^10.4.5",
|
"glob@<8": "^10.4.5",
|
||||||
|
|||||||
BIN
images/hamster-hiring.png
Normal file
BIN
images/hamster-hiring.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
143
package-lock.json
generated
143
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "npm:task-master-ai@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,58 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/docs": {
|
"apps/docs": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mintlify": "^4.2.111"
|
"mintlify": "^4.2.111"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"apps/extension": {
|
||||||
|
"version": "0.25.6",
|
||||||
|
"devDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@modelcontextprotocol/sdk": "1.13.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-portal": "^1.1.9",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"@tm/core": "*",
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@types/react": "19.1.8",
|
||||||
|
"@types/react-dom": "19.1.6",
|
||||||
|
"@types/vscode": "^1.101.0",
|
||||||
|
"@vscode/test-cli": "^0.0.11",
|
||||||
|
"@vscode/test-electron": "^2.5.2",
|
||||||
|
"@vscode/vsce": "^2.32.0",
|
||||||
|
"autoprefixer": "10.4.21",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"esbuild": "^0.25.3",
|
||||||
|
"esbuild-postcss": "^0.0.4",
|
||||||
|
"fs-extra": "^11.3.0",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"postcss": "8.5.6",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss": "4.1.11",
|
||||||
|
"task-master-ai": "*",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.93.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/amazon-bedrock": {
|
"apps/extension/node_modules/@ai-sdk/amazon-bedrock": {
|
||||||
"version": "2.2.12",
|
"version": "2.2.12",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -149,6 +194,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/anthropic": {
|
"apps/extension/node_modules/@ai-sdk/anthropic": {
|
||||||
"version": "1.2.12",
|
"version": "1.2.12",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -163,6 +209,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/azure": {
|
"apps/extension/node_modules/@ai-sdk/azure": {
|
||||||
"version": "1.3.25",
|
"version": "1.3.25",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "1.3.24",
|
"@ai-sdk/openai": "1.3.24",
|
||||||
@@ -178,6 +225,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/google": {
|
"apps/extension/node_modules/@ai-sdk/google": {
|
||||||
"version": "1.2.22",
|
"version": "1.2.22",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -192,6 +240,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/google-vertex": {
|
"apps/extension/node_modules/@ai-sdk/google-vertex": {
|
||||||
"version": "2.2.27",
|
"version": "2.2.27",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "1.2.12",
|
"@ai-sdk/anthropic": "1.2.12",
|
||||||
@@ -209,6 +258,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/groq": {
|
"apps/extension/node_modules/@ai-sdk/groq": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.9",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -223,6 +273,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/mistral": {
|
"apps/extension/node_modules/@ai-sdk/mistral": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -237,6 +288,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/openai": {
|
"apps/extension/node_modules/@ai-sdk/openai": {
|
||||||
"version": "1.3.24",
|
"version": "1.3.24",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -251,6 +303,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/openai-compatible": {
|
"apps/extension/node_modules/@ai-sdk/openai-compatible": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.16",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -265,6 +318,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/perplexity": {
|
"apps/extension/node_modules/@ai-sdk/perplexity": {
|
||||||
"version": "1.1.9",
|
"version": "1.1.9",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -279,6 +333,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/provider": {
|
"apps/extension/node_modules/@ai-sdk/provider": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-schema": "^0.4.0"
|
"json-schema": "^0.4.0"
|
||||||
@@ -289,6 +344,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/provider-utils": {
|
"apps/extension/node_modules/@ai-sdk/provider-utils": {
|
||||||
"version": "2.2.8",
|
"version": "2.2.8",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -304,6 +360,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/react": {
|
"apps/extension/node_modules/@ai-sdk/react": {
|
||||||
"version": "1.2.12",
|
"version": "1.2.12",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider-utils": "2.2.8",
|
"@ai-sdk/provider-utils": "2.2.8",
|
||||||
@@ -326,6 +383,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/ui-utils": {
|
"apps/extension/node_modules/@ai-sdk/ui-utils": {
|
||||||
"version": "1.2.11",
|
"version": "1.2.11",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -341,6 +399,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/xai": {
|
"apps/extension/node_modules/@ai-sdk/xai": {
|
||||||
"version": "1.2.18",
|
"version": "1.2.18",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai-compatible": "0.2.16",
|
"@ai-sdk/openai-compatible": "0.2.16",
|
||||||
@@ -356,6 +415,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@openrouter/ai-sdk-provider": {
|
"apps/extension/node_modules/@openrouter/ai-sdk-provider": {
|
||||||
"version": "0.4.6",
|
"version": "0.4.6",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.0.9",
|
"@ai-sdk/provider": "1.0.9",
|
||||||
@@ -370,6 +430,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider": {
|
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-schema": "^0.4.0"
|
"json-schema": "^0.4.0"
|
||||||
@@ -380,6 +441,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider-utils": {
|
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider-utils": {
|
||||||
"version": "2.1.10",
|
"version": "2.1.10",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.0.9",
|
"@ai-sdk/provider": "1.0.9",
|
||||||
@@ -401,6 +463,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/ai": {
|
"apps/extension/node_modules/ai": {
|
||||||
"version": "4.3.19",
|
"version": "4.3.19",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -425,6 +488,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/ai-sdk-provider-gemini-cli": {
|
"apps/extension/node_modules/ai-sdk-provider-gemini-cli": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -458,6 +522,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/ollama-ai-provider": {
|
"apps/extension/node_modules/ollama-ai-provider": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "^1.0.0",
|
"@ai-sdk/provider": "^1.0.0",
|
||||||
@@ -478,6 +543,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/openai": {
|
"apps/extension/node_modules/openai": {
|
||||||
"version": "4.104.0",
|
"version": "4.104.0",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
@@ -506,6 +572,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/openai/node_modules/@types/node": {
|
"apps/extension/node_modules/openai/node_modules/@types/node": {
|
||||||
"version": "18.19.127",
|
"version": "18.19.127",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@@ -513,10 +580,12 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/openai/node_modules/undici-types": {
|
"apps/extension/node_modules/openai/node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"apps/extension/node_modules/task-master-ai": {
|
"apps/extension/node_modules/task-master-ai": {
|
||||||
"version": "0.27.1",
|
"version": "0.27.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -588,6 +657,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/zod": {
|
"apps/extension/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
@@ -595,6 +665,7 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/zod-to-json-schema": {
|
"apps/extension/node_modules/zod-to-json-schema": {
|
||||||
"version": "3.24.6",
|
"version": "3.24.6",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
@@ -883,6 +954,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
"version": "0.39.0",
|
"version": "0.39.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
@@ -896,6 +968,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||||
"version": "18.19.127",
|
"version": "18.19.127",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@@ -903,6 +976,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
|
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@ark/schema": {
|
"node_modules/@ark/schema": {
|
||||||
@@ -8362,6 +8436,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/diff-match-patch": {
|
"node_modules/@types/diff-match-patch": {
|
||||||
"version": "1.0.36",
|
"version": "1.0.36",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/es-aggregate-error": {
|
"node_modules/@types/es-aggregate-error": {
|
||||||
@@ -8532,6 +8607,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
"node_modules/@types/node-fetch": {
|
||||||
"version": "2.6.13",
|
"version": "2.6.13",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -8977,6 +9053,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/abort-controller": {
|
"node_modules/abort-controller": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"event-target-shim": "^5.0.0"
|
"event-target-shim": "^5.0.0"
|
||||||
@@ -9038,6 +9115,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/agentkeepalive": {
|
"node_modules/agentkeepalive": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"humanize-ms": "^1.2.1"
|
"humanize-ms": "^1.2.1"
|
||||||
@@ -9620,6 +9698,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/auto-bind": {
|
"node_modules/auto-bind": {
|
||||||
@@ -11397,6 +11476,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@@ -12035,6 +12115,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -12057,6 +12138,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -12176,6 +12258,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/diff-match-patch": {
|
"node_modules/diff-match-patch": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/diff-sequences": {
|
"node_modules/diff-sequences": {
|
||||||
@@ -12675,6 +12758,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/es-set-tostringtag": {
|
"node_modules/es-set-tostringtag": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -12963,6 +13047,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/event-target-shim": {
|
"node_modules/event-target-shim": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -14001,6 +14086,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -14015,6 +14101,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/form-data-encoder": {
|
"node_modules/form-data-encoder": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/format": {
|
"node_modules/format": {
|
||||||
@@ -14026,6 +14113,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/formdata-node": {
|
"node_modules/formdata-node": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-domexception": "1.0.0",
|
"node-domexception": "1.0.0",
|
||||||
@@ -14686,6 +14774,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/has-tostringtag": {
|
"node_modules/has-tostringtag": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -15260,6 +15349,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/humanize-ms": {
|
"node_modules/humanize-ms": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.0.0"
|
"ms": "^2.0.0"
|
||||||
@@ -18046,6 +18136,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/jsondiffpatch": {
|
"node_modules/jsondiffpatch": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/diff-match-patch": "^1.0.36",
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
@@ -18309,6 +18400,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -20226,6 +20318,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -20334,6 +20427,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/node-domexception": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -21198,6 +21292,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/partial-json": {
|
"node_modules/partial-json": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/patch-console": {
|
"node_modules/patch-console": {
|
||||||
@@ -21970,6 +22065,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.1",
|
"version": "19.1.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -23096,6 +23192,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/secure-json-parse": {
|
"node_modules/secure-json-parse": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/selderee": {
|
"node_modules/selderee": {
|
||||||
@@ -24143,6 +24240,26 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-literal": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^9.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-literal/node_modules/js-tokens": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/strnum": {
|
"node_modules/strnum": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"funding": [
|
"funding": [
|
||||||
@@ -24320,6 +24437,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/swr": {
|
"node_modules/swr": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
@@ -24512,6 +24630,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/throttleit": {
|
"node_modules/throttleit": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -25614,6 +25733,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -25866,6 +25986,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -26021,6 +26142,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.3",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
@@ -27014,19 +27136,9 @@
|
|||||||
},
|
},
|
||||||
"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/claude-code-plugin/node_modules/@types/node": {
|
|
||||||
"version": "20.19.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
|
|
||||||
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
|
|
||||||
"extraneous": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~6.21.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/tm-core": {
|
"packages/tm-core": {
|
||||||
"name": "@tm/core",
|
"name": "@tm/core",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -27037,6 +27149,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
|
"strip-literal": "3.1.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
@@ -27346,6 +27459,8 @@
|
|||||||
},
|
},
|
||||||
"packages/tm-core/node_modules/vitest": {
|
"packages/tm-core/node_modules/vitest": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4",
|
||||||
|
"strip-literal": "3.1.0"
|
||||||
},
|
},
|
||||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,6 +29,8 @@ export class AuthManager {
|
|||||||
private oauthService: OAuthService;
|
private oauthService: OAuthService;
|
||||||
private supabaseClient: SupabaseAuthClient;
|
private supabaseClient: SupabaseAuthClient;
|
||||||
private organizationService?: OrganizationService;
|
private organizationService?: OrganizationService;
|
||||||
|
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);
|
||||||
@@ -36,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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,8 +83,60 @@ export class AuthManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get stored authentication credentials
|
* Get stored authentication credentials
|
||||||
|
* Automatically refreshes the token if expired
|
||||||
*/
|
*/
|
||||||
getCredentials(): AuthCredentials | null {
|
async getCredentials(): Promise<AuthCredentials | null> {
|
||||||
|
const credentials = this.credentialStore.getCredentials({
|
||||||
|
allowExpired: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if credentials are expired (with 30-second clock skew buffer)
|
||||||
|
const CLOCK_SKEW_MS = 30_000;
|
||||||
|
const isExpired = credentials.expiresAt
|
||||||
|
? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.info('Token expired, attempting automatic refresh...');
|
||||||
|
this.refreshPromise = this.refreshToken();
|
||||||
|
const result = await this.refreshPromise;
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Automatic token refresh failed:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.refreshPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null if expired and no refresh token
|
||||||
|
if (isExpired) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored authentication credentials (synchronous version)
|
||||||
|
* Does not attempt automatic refresh
|
||||||
|
*/
|
||||||
|
getCredentialsSync(): AuthCredentials | null {
|
||||||
return this.credentialStore.getCredentials();
|
return this.credentialStore.getCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +228,8 @@ export class AuthManager {
|
|||||||
/**
|
/**
|
||||||
* Get the current user context (org/brief selection)
|
* Get the current user context (org/brief selection)
|
||||||
*/
|
*/
|
||||||
getContext(): UserContext | null {
|
async getContext(): Promise<UserContext | null> {
|
||||||
const credentials = this.getCredentials();
|
const credentials = await this.getCredentials();
|
||||||
return credentials?.selectedContext || null;
|
return credentials?.selectedContext || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +237,7 @@ export class AuthManager {
|
|||||||
* Update the user context (org/brief selection)
|
* Update the user context (org/brief selection)
|
||||||
*/
|
*/
|
||||||
async updateContext(context: Partial<UserContext>): Promise<void> {
|
async updateContext(context: Partial<UserContext>): Promise<void> {
|
||||||
const credentials = this.getCredentials();
|
const credentials = await this.getCredentials();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||||
}
|
}
|
||||||
@@ -206,7 +263,7 @@ export class AuthManager {
|
|||||||
* Clear the user context
|
* Clear the user context
|
||||||
*/
|
*/
|
||||||
async clearContext(): Promise<void> {
|
async clearContext(): Promise<void> {
|
||||||
const credentials = this.getCredentials();
|
const credentials = await this.getCredentials();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||||
}
|
}
|
||||||
@@ -223,7 +280,7 @@ export class AuthManager {
|
|||||||
private async getOrganizationService(): Promise<OrganizationService> {
|
private async getOrganizationService(): Promise<OrganizationService> {
|
||||||
if (!this.organizationService) {
|
if (!this.organizationService) {
|
||||||
// First check if we have credentials with a token
|
// First check if we have credentials with a token
|
||||||
const credentials = this.getCredentials();
|
const credentials = await this.getCredentials();
|
||||||
if (!credentials || !credentials.token) {
|
if (!credentials || !credentials.token) {
|
||||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||||
}
|
}
|
||||||
|
|||||||
289
packages/tm-core/src/auth/credential-store.spec.ts
Normal file
289
packages/tm-core/src/auth/credential-store.spec.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit tests for CredentialStore token expiration handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { CredentialStore } from './credential-store';
|
||||||
|
import type { AuthCredentials } from './types';
|
||||||
|
|
||||||
|
describe('CredentialStore - Token Expiration', () => {
|
||||||
|
let credentialStore: CredentialStore;
|
||||||
|
let tmpDir: string;
|
||||||
|
let authFile: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temp directory for test credentials
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-cred-test-'));
|
||||||
|
authFile = path.join(tmpDir, 'auth.json');
|
||||||
|
|
||||||
|
// Create instance with test config
|
||||||
|
CredentialStore.resetInstance();
|
||||||
|
credentialStore = CredentialStore.getInstance({
|
||||||
|
configDir: tmpDir,
|
||||||
|
configFile: authFile
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(tmpDir)) {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
CredentialStore.resetInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Expiration Detection', () => {
|
||||||
|
it('should return null for expired token', () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return credentials for valid token', () => {
|
||||||
|
const validCredentials: AuthCredentials = {
|
||||||
|
token: 'valid-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(validCredentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).not.toBeNull();
|
||||||
|
expect(retrieved?.token).toBe('valid-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return expired token when allowExpired is true', () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials({ allowExpired: true });
|
||||||
|
|
||||||
|
expect(retrieved).not.toBeNull();
|
||||||
|
expect(retrieved?.token).toBe('expired-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Clock Skew Tolerance', () => {
|
||||||
|
it('should reject token expiring within 30-second buffer', () => {
|
||||||
|
// Token expires in 15 seconds (within 30-second buffer)
|
||||||
|
const almostExpiredCredentials: AuthCredentials = {
|
||||||
|
token: 'almost-expired-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 15000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(almostExpiredCredentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept token expiring outside 30-second buffer', () => {
|
||||||
|
// Token expires in 60 seconds (outside 30-second buffer)
|
||||||
|
const validCredentials: AuthCredentials = {
|
||||||
|
token: 'valid-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(validCredentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).not.toBeNull();
|
||||||
|
expect(retrieved?.token).toBe('valid-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Timestamp Format Handling', () => {
|
||||||
|
it('should handle ISO string timestamps', () => {
|
||||||
|
const credentials: AuthCredentials = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(credentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).not.toBeNull();
|
||||||
|
expect(typeof retrieved?.expiresAt).toBe('number'); // Normalized to number
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle numeric timestamps', () => {
|
||||||
|
const credentials: AuthCredentials = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(credentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).not.toBeNull();
|
||||||
|
expect(typeof retrieved?.expiresAt).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for invalid timestamp format', () => {
|
||||||
|
// Manually write invalid timestamp to file
|
||||||
|
const invalidCredentials = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: 'invalid-date',
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(authFile, JSON.stringify(invalidCredentials), {
|
||||||
|
mode: 0o600
|
||||||
|
});
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for missing expiresAt', () => {
|
||||||
|
const credentialsWithoutExpiry = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(authFile, JSON.stringify(credentialsWithoutExpiry), {
|
||||||
|
mode: 0o600
|
||||||
|
});
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
expect(retrieved).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Storage Persistence', () => {
|
||||||
|
it('should persist expiresAt as ISO string', () => {
|
||||||
|
const expiryTime = Date.now() + 3600000;
|
||||||
|
const credentials: AuthCredentials = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: expiryTime,
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(credentials);
|
||||||
|
|
||||||
|
// Read raw file to verify format
|
||||||
|
const fileContent = fs.readFileSync(authFile, 'utf-8');
|
||||||
|
const parsed = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
// Should be stored as ISO string
|
||||||
|
expect(typeof parsed.expiresAt).toBe('string');
|
||||||
|
expect(parsed.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO format
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize timestamp on retrieval', () => {
|
||||||
|
const credentials: AuthCredentials = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(credentials);
|
||||||
|
|
||||||
|
const retrieved = credentialStore.getCredentials();
|
||||||
|
|
||||||
|
// Should be normalized to number for runtime use
|
||||||
|
expect(typeof retrieved?.expiresAt).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasValidCredentials', () => {
|
||||||
|
it('should return false for expired credentials', () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
expect(credentialStore.hasValidCredentials()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for valid credentials', () => {
|
||||||
|
const validCredentials: AuthCredentials = {
|
||||||
|
token: 'valid-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(validCredentials);
|
||||||
|
|
||||||
|
expect(credentialStore.hasValidCredentials()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when no credentials exist', () => {
|
||||||
|
expect(credentialStore.hasValidCredentials()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -47,8 +47,8 @@ export class SupabaseTaskRepository {
|
|||||||
* Gets the current brief ID from auth context
|
* Gets the current brief ID from auth context
|
||||||
* @throws {Error} If no brief is selected
|
* @throws {Error} If no brief is selected
|
||||||
*/
|
*/
|
||||||
private getBriefIdOrThrow(): string {
|
private async getBriefIdOrThrow(): Promise<string> {
|
||||||
const context = this.authManager.getContext();
|
const context = await this.authManager.getContext();
|
||||||
if (!context?.briefId) {
|
if (!context?.briefId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No brief selected. Please select a brief first using: tm context brief'
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
@@ -61,7 +61,7 @@ export class SupabaseTaskRepository {
|
|||||||
_projectId?: string,
|
_projectId?: string,
|
||||||
options?: LoadTasksOptions
|
options?: LoadTasksOptions
|
||||||
): Promise<Task[]> {
|
): Promise<Task[]> {
|
||||||
const briefId = this.getBriefIdOrThrow();
|
const briefId = await this.getBriefIdOrThrow();
|
||||||
|
|
||||||
// Build query with filters
|
// Build query with filters
|
||||||
let query = this.supabase
|
let query = this.supabase
|
||||||
@@ -114,7 +114,7 @@ export class SupabaseTaskRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||||
const briefId = this.getBriefIdOrThrow();
|
const briefId = await this.getBriefIdOrThrow();
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
@@ -157,7 +157,7 @@ export class SupabaseTaskRepository {
|
|||||||
taskId: string,
|
taskId: string,
|
||||||
updates: Partial<Task>
|
updates: Partial<Task>
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
const briefId = this.getBriefIdOrThrow();
|
const briefId = await this.getBriefIdOrThrow();
|
||||||
|
|
||||||
// Validate updates using Zod schema
|
// Validate updates using Zod schema
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get current context
|
// Get current context
|
||||||
const context = this.authManager.getContext();
|
const context = await this.authManager.getContext();
|
||||||
|
|
||||||
// Determine org and brief IDs
|
// Determine org and brief IDs
|
||||||
let orgId = options.orgId || context?.orgId;
|
let orgId = options.orgId || context?.orgId;
|
||||||
@@ -232,7 +232,7 @@ export class ExportService {
|
|||||||
hasBrief: boolean;
|
hasBrief: boolean;
|
||||||
context: UserContext | null;
|
context: UserContext | null;
|
||||||
}> {
|
}> {
|
||||||
const context = this.authManager.getContext();
|
const context = await this.authManager.getContext();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasOrg: !!context?.orgId,
|
hasOrg: !!context?.orgId,
|
||||||
@@ -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,16 +370,16 @@ 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
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get auth token
|
// Get auth token
|
||||||
const credentials = this.authManager.getCredentials();
|
const credentials = await this.authManager.getCredentials();
|
||||||
if (!credentials || !credentials.token) {
|
if (!credentials || !credentials.token) {
|
||||||
throw new Error('Not authenticated');
|
throw new Error('Not authenticated');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export class ApiStorage implements IStorage {
|
|||||||
private async loadTagsIntoCache(): Promise<void> {
|
private async loadTagsIntoCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const context = authManager.getContext();
|
const context = await authManager.getContext();
|
||||||
|
|
||||||
// If we have a selected brief, create a virtual "tag" for it
|
// If we have a selected brief, create a virtual "tag" for it
|
||||||
if (context?.briefId) {
|
if (context?.briefId) {
|
||||||
@@ -152,7 +152,7 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const context = authManager.getContext();
|
const context = await authManager.getContext();
|
||||||
|
|
||||||
// If no brief is selected in context, throw an error
|
// If no brief is selected in context, throw an error
|
||||||
if (!context?.briefId) {
|
if (!context?.briefId) {
|
||||||
@@ -318,7 +318,7 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const context = authManager.getContext();
|
const context = await authManager.getContext();
|
||||||
|
|
||||||
// In our API-based system, we only have one "tag" at a time - the current brief
|
// In our API-based system, we only have one "tag" at a time - the current brief
|
||||||
if (context?.briefId) {
|
if (context?.briefId) {
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export class StorageFactory {
|
|||||||
{ storageType: 'api', missing }
|
{ storageType: 'api', missing }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Use auth token from AuthManager
|
// Use auth token from AuthManager (synchronous - no auto-refresh here)
|
||||||
const credentials = authManager.getCredentials();
|
const credentials = authManager.getCredentialsSync();
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
// Merge with existing storage config, ensuring required fields
|
// Merge with existing storage config, ensuring required fields
|
||||||
const nextStorage: StorageSettings = {
|
const nextStorage: StorageSettings = {
|
||||||
@@ -103,7 +103,7 @@ export class StorageFactory {
|
|||||||
|
|
||||||
// Then check if authenticated via AuthManager
|
// Then check if authenticated via AuthManager
|
||||||
if (authManager.isAuthenticated()) {
|
if (authManager.isAuthenticated()) {
|
||||||
const credentials = authManager.getCredentials();
|
const credentials = authManager.getCredentialsSync();
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
// Configure API storage with auth credentials
|
// Configure API storage with auth credentials
|
||||||
const nextStorage: StorageSettings = {
|
const nextStorage: StorageSettings = {
|
||||||
|
|||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
392
packages/tm-core/tests/integration/auth-token-refresh.test.ts
Normal file
392
packages/tm-core/tests/integration/auth-token-refresh.test.ts
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Integration tests for JWT token auto-refresh functionality
|
||||||
|
*
|
||||||
|
* These tests verify that expired tokens are automatically refreshed
|
||||||
|
* when making API calls through AuthManager.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 Auto-Refresh Integration', () => {
|
||||||
|
let authManager: AuthManager;
|
||||||
|
let credentialStore: CredentialStore;
|
||||||
|
let tmpDir: string;
|
||||||
|
let authFile: string;
|
||||||
|
|
||||||
|
// Mock Supabase session that will be returned on refresh
|
||||||
|
const mockRefreshedSession: Session = {
|
||||||
|
access_token: 'new-access-token-xyz',
|
||||||
|
refresh_token: 'new-refresh-token-xyz',
|
||||||
|
token_type: 'bearer',
|
||||||
|
expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
||||||
|
expires_in: 3600,
|
||||||
|
user: {
|
||||||
|
id: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
aud: 'authenticated',
|
||||||
|
role: 'authenticated',
|
||||||
|
app_metadata: {},
|
||||||
|
user_metadata: {},
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset singletons
|
||||||
|
AuthManager.resetInstance();
|
||||||
|
CredentialStore.resetInstance();
|
||||||
|
|
||||||
|
// 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.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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Expired Token Detection', () => {
|
||||||
|
it('should detect expired token', async () => {
|
||||||
|
// Set up expired credentials
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
// Mock the Supabase refreshSession to return new tokens
|
||||||
|
const mockRefreshSession = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockRefreshedSession);
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockImplementation(mockRefreshSession);
|
||||||
|
|
||||||
|
// Get credentials should trigger refresh
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(mockRefreshSession).toHaveBeenCalledTimes(1);
|
||||||
|
expect(credentials).not.toBeNull();
|
||||||
|
expect(credentials?.token).toBe('new-access-token-xyz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not refresh valid token', async () => {
|
||||||
|
// Set up valid credentials
|
||||||
|
const validCredentials: AuthCredentials = {
|
||||||
|
token: 'valid-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(validCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
// Mock refresh to ensure it's not called
|
||||||
|
const mockRefreshSession = vi.fn();
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockImplementation(mockRefreshSession);
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(mockRefreshSession).not.toHaveBeenCalled();
|
||||||
|
expect(credentials?.token).toBe('valid-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Token Refresh Flow', () => {
|
||||||
|
it('should refresh expired token and save new credentials', async () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'old-token',
|
||||||
|
refreshToken: 'old-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date(Date.now() - 3600000).toISOString(),
|
||||||
|
selectedContext: {
|
||||||
|
orgId: 'test-org',
|
||||||
|
briefId: 'test-brief',
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockResolvedValue(mockRefreshedSession);
|
||||||
|
|
||||||
|
const refreshedCredentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(refreshedCredentials).not.toBeNull();
|
||||||
|
expect(refreshedCredentials?.token).toBe('new-access-token-xyz');
|
||||||
|
expect(refreshedCredentials?.refreshToken).toBe('new-refresh-token-xyz');
|
||||||
|
|
||||||
|
// Verify context was preserved
|
||||||
|
expect(refreshedCredentials?.selectedContext?.orgId).toBe('test-org');
|
||||||
|
expect(refreshedCredentials?.selectedContext?.briefId).toBe('test-brief');
|
||||||
|
|
||||||
|
// Verify new expiration is in the future
|
||||||
|
const newExpiry = new Date(refreshedCredentials!.expiresAt!).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
expect(newExpiry).toBeGreaterThan(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if refresh fails', async () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'invalid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
// Mock refresh to fail
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockRejectedValue(new Error('Refresh token expired'));
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if no refresh token available', async () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
// No refresh token
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if credentials missing expiresAt', async () => {
|
||||||
|
const credentialsWithoutExpiry: AuthCredentials = {
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
// Missing expiresAt
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(credentialsWithoutExpiry);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
// Should return null because no valid expiration
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Clock Skew Tolerance', () => {
|
||||||
|
it('should refresh token within 30-second expiry window', async () => {
|
||||||
|
// Token expires in 15 seconds (within 30-second buffer)
|
||||||
|
const almostExpiredCredentials: AuthCredentials = {
|
||||||
|
token: 'almost-expired-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 15000).toISOString(), // 15 seconds from now
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(almostExpiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
const mockRefreshSession = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockRefreshedSession);
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockImplementation(mockRefreshSession);
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
// Should trigger refresh due to 30-second buffer
|
||||||
|
expect(mockRefreshSession).toHaveBeenCalledTimes(1);
|
||||||
|
expect(credentials?.token).toBe('new-access-token-xyz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not refresh token well before expiry', async () => {
|
||||||
|
// Token expires in 5 minutes (well outside 30-second buffer)
|
||||||
|
const validCredentials: AuthCredentials = {
|
||||||
|
token: 'valid-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() + 300000).toISOString(), // 5 minutes
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(validCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
const mockRefreshSession = vi.fn();
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockImplementation(mockRefreshSession);
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(mockRefreshSession).not.toHaveBeenCalled();
|
||||||
|
expect(credentials?.token).toBe('valid-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Synchronous vs Async Methods', () => {
|
||||||
|
it('getCredentialsSync should not trigger refresh', () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
// Synchronous call should return null without refresh
|
||||||
|
const credentials = authManager.getCredentialsSync();
|
||||||
|
|
||||||
|
expect(credentials).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCredentials async should trigger refresh', async () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockResolvedValue(mockRefreshedSession);
|
||||||
|
|
||||||
|
const credentials = await authManager.getCredentials();
|
||||||
|
|
||||||
|
expect(credentials).not.toBeNull();
|
||||||
|
expect(credentials?.token).toBe('new-access-token-xyz');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multiple Concurrent Calls', () => {
|
||||||
|
it('should handle concurrent getCredentials calls gracefully', async () => {
|
||||||
|
const expiredCredentials: AuthCredentials = {
|
||||||
|
token: 'expired-token',
|
||||||
|
refreshToken: 'valid-refresh-token',
|
||||||
|
userId: 'test-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
expiresAt: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialStore.saveCredentials(expiredCredentials);
|
||||||
|
|
||||||
|
authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
const mockRefreshSession = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockRefreshedSession);
|
||||||
|
vi.spyOn(
|
||||||
|
authManager['supabaseClient'],
|
||||||
|
'refreshSession'
|
||||||
|
).mockImplementation(mockRefreshSession);
|
||||||
|
|
||||||
|
// Make multiple concurrent calls
|
||||||
|
const [creds1, creds2, creds3] = await Promise.all([
|
||||||
|
authManager.getCredentials(),
|
||||||
|
authManager.getCredentials(),
|
||||||
|
authManager.getCredentials()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// All should get the refreshed token
|
||||||
|
expect(creds1?.token).toBe('new-access-token-xyz');
|
||||||
|
expect(creds2?.token).toBe('new-access-token-xyz');
|
||||||
|
expect(creds3?.token).toBe('new-access-token-xyz');
|
||||||
|
|
||||||
|
// Refresh might be called multiple times, but that's okay
|
||||||
|
// (ideally we'd debounce, but this is acceptable behavior)
|
||||||
|
expect(mockRefreshSession).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user