mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
Merge pull request #1354 from eyaltoledano/next Release 0.30.2
This commit is contained in:
@@ -62,8 +62,22 @@ export class AuthCommand extends Command {
|
||||
private addLoginCommand(): void {
|
||||
this.command('login')
|
||||
.description('Authenticate with tryhamster.com')
|
||||
.action(async () => {
|
||||
await this.executeLogin();
|
||||
.argument(
|
||||
'[token]',
|
||||
'Authentication token (optional, for SSH/remote environments)'
|
||||
)
|
||||
.option('-y, --yes', 'Skip interactive prompts')
|
||||
.addHelpText(
|
||||
'after',
|
||||
`
|
||||
Examples:
|
||||
$ tm auth login # Browser-based OAuth flow (interactive)
|
||||
$ tm auth login <token> # Token-based authentication
|
||||
$ tm auth login <token> -y # Non-interactive token auth (for scripts)
|
||||
`
|
||||
)
|
||||
.action(async (token?: string, options?: { yes?: boolean }) => {
|
||||
await this.executeLogin(token, options?.yes);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,9 +117,11 @@ export class AuthCommand extends Command {
|
||||
/**
|
||||
* Execute login command
|
||||
*/
|
||||
private async executeLogin(): Promise<void> {
|
||||
private async executeLogin(token?: string, yes?: boolean): Promise<void> {
|
||||
try {
|
||||
const result = await this.performInteractiveAuth();
|
||||
const result = token
|
||||
? await this.performTokenAuth(token, yes)
|
||||
: await this.performInteractiveAuth(yes);
|
||||
this.setLastResult(result);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -143,7 +159,7 @@ export class AuthCommand extends Command {
|
||||
*/
|
||||
private async executeStatus(): Promise<void> {
|
||||
try {
|
||||
const result = this.displayStatus();
|
||||
const result = await this.displayStatus();
|
||||
this.setLastResult(result);
|
||||
} catch (error: any) {
|
||||
displayError(error);
|
||||
@@ -169,21 +185,28 @@ export class AuthCommand extends Command {
|
||||
/**
|
||||
* Display authentication status
|
||||
*/
|
||||
private displayStatus(): AuthResult {
|
||||
const credentials = this.authManager.getCredentials();
|
||||
|
||||
private async displayStatus(): Promise<AuthResult> {
|
||||
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
||||
|
||||
if (credentials) {
|
||||
console.log(chalk.green('✓ Authenticated'));
|
||||
console.log(chalk.gray(` Email: ${credentials.email || 'N/A'}`));
|
||||
console.log(chalk.gray(` User ID: ${credentials.userId}`));
|
||||
console.log(
|
||||
chalk.gray(` Token Type: ${credentials.tokenType || 'standard'}`)
|
||||
);
|
||||
// Check if user has valid session
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
|
||||
if (credentials.expiresAt) {
|
||||
const expiresAt = new Date(credentials.expiresAt);
|
||||
if (hasSession) {
|
||||
// Get session from Supabase (has tokens and expiry)
|
||||
const session = await this.authManager.getSession();
|
||||
|
||||
// Get user context (has email, userId, org/brief selection)
|
||||
const context = this.authManager.getContext();
|
||||
const contextStore = this.authManager.getStoredContext();
|
||||
|
||||
console.log(chalk.green('✓ Authenticated'));
|
||||
console.log(chalk.gray(` Email: ${contextStore?.email || 'N/A'}`));
|
||||
console.log(chalk.gray(` User ID: ${contextStore?.userId || 'N/A'}`));
|
||||
console.log(chalk.gray(` Token Type: standard`));
|
||||
|
||||
// Display expiration info
|
||||
if (session?.expires_at) {
|
||||
const expiresAt = new Date(session.expires_at * 1000);
|
||||
const now = new Date();
|
||||
const timeRemaining = expiresAt.getTime() - now.getTime();
|
||||
const hoursRemaining = Math.floor(timeRemaining / (1000 * 60 * 60));
|
||||
@@ -210,13 +233,32 @@ export class AuthCommand extends Command {
|
||||
chalk.yellow(` Expired at: ${expiresAt.toLocaleString()}`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(' Expires: Never (API key)'));
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.gray(` Saved: ${new Date(credentials.savedAt).toLocaleString()}`)
|
||||
);
|
||||
// Display context if available
|
||||
if (context) {
|
||||
console.log(chalk.gray('\n Context:'));
|
||||
if (context.orgName) {
|
||||
console.log(chalk.gray(` Organization: ${context.orgName}`));
|
||||
}
|
||||
if (context.briefName) {
|
||||
console.log(chalk.gray(` Brief: ${context.briefName}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Build credentials for backward compatibility
|
||||
const credentials = {
|
||||
token: session?.access_token || '',
|
||||
refreshToken: session?.refresh_token,
|
||||
userId: contextStore?.userId || '',
|
||||
email: contextStore?.email,
|
||||
expiresAt: session?.expires_at
|
||||
? new Date(session.expires_at * 1000).toISOString()
|
||||
: undefined,
|
||||
tokenType: 'standard' as const,
|
||||
savedAt: contextStore?.lastUpdated || new Date().toISOString(),
|
||||
selectedContext: context || undefined
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -307,11 +349,12 @@ export class AuthCommand extends Command {
|
||||
/**
|
||||
* Perform interactive authentication
|
||||
*/
|
||||
private async performInteractiveAuth(): Promise<AuthResult> {
|
||||
private async performInteractiveAuth(yes?: boolean): Promise<AuthResult> {
|
||||
ui.displayBanner('Task Master Authentication');
|
||||
const isAuthenticated = await this.authManager.hasValidSession();
|
||||
|
||||
// Check if already authenticated
|
||||
if (this.authManager.isAuthenticated()) {
|
||||
// Check if already authenticated (skip if --yes is used)
|
||||
if (isAuthenticated && !yes) {
|
||||
const { continueAuth } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
@@ -323,7 +366,7 @@ export class AuthCommand extends Command {
|
||||
]);
|
||||
|
||||
if (!continueAuth) {
|
||||
const credentials = this.authManager.getCredentials();
|
||||
const credentials = await this.authManager.getAuthCredentials();
|
||||
ui.displaySuccess('Using existing authentication');
|
||||
|
||||
if (credentials) {
|
||||
@@ -349,35 +392,43 @@ export class AuthCommand extends Command {
|
||||
chalk.gray(` Logged in as: ${credentials.email || credentials.userId}`)
|
||||
);
|
||||
|
||||
// Post-auth: Set up workspace context
|
||||
console.log(); // Add spacing
|
||||
try {
|
||||
const contextCommand = new ContextCommand();
|
||||
const contextResult = await contextCommand.setupContextInteractive();
|
||||
if (contextResult.success) {
|
||||
if (contextResult.orgSelected && contextResult.briefSelected) {
|
||||
// Post-auth: Set up workspace context (skip if --yes flag is used)
|
||||
if (!yes) {
|
||||
console.log(); // Add spacing
|
||||
try {
|
||||
const contextCommand = new ContextCommand();
|
||||
const contextResult = await contextCommand.setupContextInteractive();
|
||||
if (contextResult.success) {
|
||||
if (contextResult.orgSelected && contextResult.briefSelected) {
|
||||
console.log(
|
||||
chalk.green('✓ Workspace context configured successfully')
|
||||
);
|
||||
} else if (contextResult.orgSelected) {
|
||||
console.log(chalk.green('✓ Organization selected'));
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green('✓ Workspace context configured successfully')
|
||||
chalk.yellow('⚠ Context setup was skipped or encountered issues')
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(' You can set up context later with "tm context"')
|
||||
);
|
||||
} else if (contextResult.orgSelected) {
|
||||
console.log(chalk.green('✓ Organization selected'));
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow('⚠ Context setup was skipped or encountered issues')
|
||||
);
|
||||
} catch (contextError) {
|
||||
console.log(chalk.yellow('⚠ Context setup encountered an error'));
|
||||
console.log(
|
||||
chalk.gray(' You can set up context later with "tm context"')
|
||||
);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(chalk.gray((contextError as Error).message));
|
||||
}
|
||||
}
|
||||
} catch (contextError) {
|
||||
console.log(chalk.yellow('⚠ Context setup encountered an error'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.gray(' You can set up context later with "tm context"')
|
||||
chalk.gray(
|
||||
'\n Skipped interactive setup. Use "tm context" to configure later.'
|
||||
)
|
||||
);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(chalk.gray((contextError as Error).message));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -450,6 +501,96 @@ export class AuthCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with token
|
||||
*/
|
||||
private async authenticateWithToken(token: string): Promise<AuthCredentials> {
|
||||
const spinner = ora('Verifying authentication token...').start();
|
||||
|
||||
try {
|
||||
const credentials = await this.authManager.authenticateWithCode(token);
|
||||
spinner.succeed('Successfully authenticated!');
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
spinner.fail('Authentication failed');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform token-based authentication flow
|
||||
*/
|
||||
private async performTokenAuth(
|
||||
token: string,
|
||||
yes?: boolean
|
||||
): Promise<AuthResult> {
|
||||
ui.displayBanner('Task Master Authentication');
|
||||
|
||||
try {
|
||||
// Authenticate with the token
|
||||
const credentials = await this.authenticateWithToken(token);
|
||||
|
||||
ui.displaySuccess('Authentication successful!');
|
||||
console.log(
|
||||
chalk.gray(` Logged in as: ${credentials.email || credentials.userId}`)
|
||||
);
|
||||
|
||||
// Post-auth: Set up workspace context (skip if --yes flag is used)
|
||||
if (!yes) {
|
||||
console.log(); // Add spacing
|
||||
try {
|
||||
const contextCommand = new ContextCommand();
|
||||
const contextResult = await contextCommand.setupContextInteractive();
|
||||
if (contextResult.success) {
|
||||
if (contextResult.orgSelected && contextResult.briefSelected) {
|
||||
console.log(
|
||||
chalk.green('✓ Workspace context configured successfully')
|
||||
);
|
||||
} else if (contextResult.orgSelected) {
|
||||
console.log(chalk.green('✓ Organization selected'));
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow('⚠ Context setup was skipped or encountered issues')
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(' You can set up context later with "tm context"')
|
||||
);
|
||||
}
|
||||
} catch (contextError) {
|
||||
console.log(chalk.yellow('⚠ Context setup encountered an error'));
|
||||
console.log(
|
||||
chalk.gray(' You can set up context later with "tm context"')
|
||||
);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(chalk.gray((contextError as Error).message));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'\n Skipped interactive setup. Use "tm context" to configure later.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'login',
|
||||
credentials,
|
||||
message: 'Authentication successful'
|
||||
};
|
||||
} catch (error) {
|
||||
displayError(error, { skipExit: true });
|
||||
|
||||
return {
|
||||
success: false,
|
||||
action: 'login',
|
||||
message: `Authentication failed: ${(error as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last result for programmatic access
|
||||
*/
|
||||
@@ -464,18 +605,11 @@ export class AuthCommand extends Command {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authentication status (for programmatic usage)
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.authManager.isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current credentials (for programmatic usage)
|
||||
*/
|
||||
getCredentials(): AuthCredentials | null {
|
||||
return this.authManager.getCredentials();
|
||||
async getCredentials(): Promise<AuthCredentials | null> {
|
||||
return this.authManager.getAuthCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,7 +113,7 @@ export class ContextCommand extends Command {
|
||||
*/
|
||||
private async executeShow(): Promise<void> {
|
||||
try {
|
||||
const result = this.displayContext();
|
||||
const result = await this.displayContext();
|
||||
this.setLastResult(result);
|
||||
} catch (error: any) {
|
||||
displayError(error);
|
||||
@@ -123,9 +123,10 @@ export class ContextCommand extends Command {
|
||||
/**
|
||||
* Display current context
|
||||
*/
|
||||
private displayContext(): ContextResult {
|
||||
private async displayContext(): Promise<ContextResult> {
|
||||
// Check authentication first
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
console.log(chalk.yellow('✗ Not authenticated'));
|
||||
console.log(chalk.gray('\n Run "tm auth login" to authenticate first'));
|
||||
|
||||
@@ -200,7 +201,8 @@ export class ContextCommand extends Command {
|
||||
private async executeSelectOrg(): Promise<void> {
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -250,7 +252,7 @@ export class ContextCommand extends Command {
|
||||
]);
|
||||
|
||||
// Update context
|
||||
this.authManager.updateContext({
|
||||
await this.authManager.updateContext({
|
||||
orgId: selectedOrg.id,
|
||||
orgName: selectedOrg.name,
|
||||
orgSlug: selectedOrg.slug,
|
||||
@@ -279,7 +281,8 @@ export class ContextCommand extends Command {
|
||||
private async executeSelectBrief(): Promise<void> {
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -371,7 +374,7 @@ export class ContextCommand extends Command {
|
||||
const briefName =
|
||||
selectedBrief.document?.title ||
|
||||
`Brief ${selectedBrief.id.slice(0, 8)}`;
|
||||
this.authManager.updateContext({
|
||||
await this.authManager.updateContext({
|
||||
briefId: selectedBrief.id,
|
||||
briefName: briefName
|
||||
});
|
||||
@@ -386,7 +389,7 @@ export class ContextCommand extends Command {
|
||||
};
|
||||
} else {
|
||||
// Clear brief selection
|
||||
this.authManager.updateContext({
|
||||
await this.authManager.updateContext({
|
||||
briefId: undefined,
|
||||
briefName: undefined
|
||||
});
|
||||
@@ -412,7 +415,8 @@ export class ContextCommand extends Command {
|
||||
private async executeClear(): Promise<void> {
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -458,7 +462,8 @@ export class ContextCommand extends Command {
|
||||
private async executeSet(options: any): Promise<void> {
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -481,7 +486,8 @@ export class ContextCommand extends Command {
|
||||
let spinner: Ora | undefined;
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -520,7 +526,7 @@ export class ContextCommand extends Command {
|
||||
// Update context: set org and brief
|
||||
const briefName =
|
||||
brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
|
||||
this.authManager.updateContext({
|
||||
await this.authManager.updateContext({
|
||||
orgId: brief.accountId,
|
||||
orgName,
|
||||
orgSlug,
|
||||
@@ -642,7 +648,7 @@ export class ContextCommand extends Command {
|
||||
};
|
||||
}
|
||||
|
||||
this.authManager.updateContext(context);
|
||||
await this.authManager.updateContext(context);
|
||||
ui.displaySuccess('Context updated');
|
||||
|
||||
// Display what was set
|
||||
|
||||
@@ -96,7 +96,8 @@ export class ExportCommand extends Command {
|
||||
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
const hasSession = await this.authManager.hasValidSession();
|
||||
if (!hasSession) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user