mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat(auth): add shared org-selection utility and mandatory org selection post-login
- Create apps/cli/src/utils/org-selection.ts with reusable ensureOrgSelected() - Update context.command.ts to use shared utility for mandatory org selection - Update briefs.command.ts to use shared utility - Org selection is now mandatory after login (brief selection remains optional)
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
selectBriefFromInput,
|
||||
selectBriefInteractive
|
||||
} from '../utils/brief-selection.js';
|
||||
import { ensureOrgSelected } from '../utils/org-selection.js';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
@@ -180,7 +181,7 @@ Note: Briefs must be created through the Hamster Studio web interface.
|
||||
}
|
||||
|
||||
// Ensure org is selected - prompt if not
|
||||
const orgId = await this.ensureOrgSelected();
|
||||
const orgId = await this.ensureOrgSelectedLocal();
|
||||
if (!orgId) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -248,63 +249,11 @@ Note: Briefs must be created through the Hamster Studio web interface.
|
||||
|
||||
/**
|
||||
* Ensure an organization is selected, prompting if necessary
|
||||
* Uses the shared org-selection utility
|
||||
*/
|
||||
private async ensureOrgSelected(): Promise<string | null> {
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
// If org is already selected, return it
|
||||
if (context?.orgId) {
|
||||
return context.orgId;
|
||||
}
|
||||
|
||||
// No org selected - check if we can auto-select
|
||||
const orgs = await this.authManager.getOrganizations();
|
||||
|
||||
if (orgs.length === 0) {
|
||||
ui.displayError(
|
||||
'No organizations available. Please create or join an organization first.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (orgs.length === 1) {
|
||||
// Auto-select the only org
|
||||
await this.authManager.updateContext({
|
||||
orgId: orgs[0].id,
|
||||
orgName: orgs[0].name,
|
||||
orgSlug: orgs[0].slug
|
||||
});
|
||||
console.log(chalk.gray(` Auto-selected organization: ${orgs[0].name}`));
|
||||
return orgs[0].id;
|
||||
}
|
||||
|
||||
// Multiple orgs - prompt for selection
|
||||
console.log(chalk.yellow('No organization selected.'));
|
||||
|
||||
const response = await inquirer.prompt<{ orgId: string }>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'orgId',
|
||||
message: 'Select an organization:',
|
||||
choices: orgs.map((org) => ({
|
||||
name: org.name,
|
||||
value: org.id
|
||||
}))
|
||||
}
|
||||
]);
|
||||
|
||||
const selectedOrg = orgs.find((o) => o.id === response.orgId);
|
||||
if (selectedOrg) {
|
||||
await this.authManager.updateContext({
|
||||
orgId: selectedOrg.id,
|
||||
orgName: selectedOrg.name,
|
||||
orgSlug: selectedOrg.slug
|
||||
});
|
||||
ui.displaySuccess(`Selected organization: ${selectedOrg.name}`);
|
||||
return selectedOrg.id;
|
||||
}
|
||||
|
||||
return null;
|
||||
private async ensureOrgSelectedLocal(): Promise<string | null> {
|
||||
const result = await ensureOrgSelected(this.authManager);
|
||||
return result.success ? result.orgId || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
selectBriefFromInput,
|
||||
selectBriefInteractive
|
||||
} from '../utils/brief-selection.js';
|
||||
import { ensureOrgSelected } from '../utils/org-selection.js';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
@@ -622,7 +623,8 @@ export class ContextCommand extends Command {
|
||||
|
||||
/**
|
||||
* Interactive context setup (for post-auth flow)
|
||||
* Prompts user to select org and brief
|
||||
* Organization selection is MANDATORY - you cannot proceed without an org.
|
||||
* Brief selection is optional.
|
||||
*/
|
||||
async setupContextInteractive(): Promise<{
|
||||
success: boolean;
|
||||
@@ -630,30 +632,35 @@ export class ContextCommand extends Command {
|
||||
briefSelected: boolean;
|
||||
}> {
|
||||
try {
|
||||
// Ask if user wants to set up workspace context
|
||||
const { setupContext } = await inquirer.prompt([
|
||||
// Organization selection is REQUIRED - use the shared utility
|
||||
// It will auto-select if only one org, or prompt if multiple
|
||||
const orgResult = await ensureOrgSelected(this.authManager, {
|
||||
promptMessage: 'Select an organization:'
|
||||
});
|
||||
|
||||
if (!orgResult.success || !orgResult.orgId) {
|
||||
// This should rarely happen (only if user has no orgs)
|
||||
return { success: false, orgSelected: false, briefSelected: false };
|
||||
}
|
||||
|
||||
// Brief selection is optional - ask if they want to select one
|
||||
const { selectBrief } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'setupContext',
|
||||
message: 'Would you like to set up your workspace context now?',
|
||||
name: 'selectBrief',
|
||||
message: 'Would you like to select a brief now?',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
if (!setupContext) {
|
||||
return { success: true, orgSelected: false, briefSelected: false };
|
||||
}
|
||||
|
||||
// Select organization
|
||||
const orgResult = await this.selectOrganization();
|
||||
if (!orgResult.success || !orgResult.context?.orgId) {
|
||||
return { success: false, orgSelected: false, briefSelected: false };
|
||||
if (!selectBrief) {
|
||||
return { success: true, orgSelected: true, briefSelected: false };
|
||||
}
|
||||
|
||||
// Select brief using shared utility
|
||||
const briefResult = await selectBriefInteractive(
|
||||
this.authManager,
|
||||
orgResult.context.orgId
|
||||
orgResult.orgId
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
@@ -663,7 +670,7 @@ export class ContextCommand extends Command {
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.yellow(
|
||||
'\nContext setup skipped due to error. You can set it up later with "tm context"'
|
||||
'\nContext setup encountered an error. You can set it up later with "tm context"'
|
||||
)
|
||||
);
|
||||
return { success: false, orgSelected: false, briefSelected: false };
|
||||
|
||||
172
apps/cli/src/utils/org-selection.ts
Normal file
172
apps/cli/src/utils/org-selection.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* @fileoverview Shared Organization Selection Utility
|
||||
* Provides reusable org selection flow for commands that require org context.
|
||||
*/
|
||||
|
||||
import { AuthManager } from '@tm/core';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import * as ui from './ui.js';
|
||||
|
||||
/**
|
||||
* Result of org selection
|
||||
*/
|
||||
export interface OrgSelectionResult {
|
||||
success: boolean;
|
||||
orgId?: string;
|
||||
orgName?: string;
|
||||
orgSlug?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for ensureOrgSelected
|
||||
*/
|
||||
export interface EnsureOrgOptions {
|
||||
/** If true, suppress informational messages */
|
||||
silent?: boolean;
|
||||
/** Custom message to show when prompting */
|
||||
promptMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure an organization is selected, prompting if necessary.
|
||||
*
|
||||
* This is a shared utility that can be used by any command that requires
|
||||
* an organization context. It will:
|
||||
* 1. Check if org is already selected in context
|
||||
* 2. If not, fetch orgs and auto-select if only one
|
||||
* 3. If multiple, prompt user to select
|
||||
*
|
||||
* @param authManager - The AuthManager instance
|
||||
* @param options - Optional configuration
|
||||
* @returns OrgSelectionResult with orgId if successful
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await ensureOrgSelected(authManager);
|
||||
* if (!result.success) {
|
||||
* process.exit(1);
|
||||
* }
|
||||
* // Now we have result.orgId
|
||||
* ```
|
||||
*/
|
||||
export async function ensureOrgSelected(
|
||||
authManager: AuthManager,
|
||||
options: EnsureOrgOptions = {}
|
||||
): Promise<OrgSelectionResult> {
|
||||
const { silent = false, promptMessage } = options;
|
||||
|
||||
try {
|
||||
const context = authManager.getContext();
|
||||
|
||||
// If org is already selected, return it
|
||||
if (context?.orgId) {
|
||||
return {
|
||||
success: true,
|
||||
orgId: context.orgId,
|
||||
orgName: context.orgName,
|
||||
orgSlug: context.orgSlug
|
||||
};
|
||||
}
|
||||
|
||||
// No org selected - check if we can auto-select
|
||||
const orgs = await authManager.getOrganizations();
|
||||
|
||||
if (orgs.length === 0) {
|
||||
ui.displayError(
|
||||
'No organizations available. Please create or join an organization first.'
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
message: 'No organizations available'
|
||||
};
|
||||
}
|
||||
|
||||
if (orgs.length === 1) {
|
||||
// Auto-select the only org
|
||||
await authManager.updateContext({
|
||||
orgId: orgs[0].id,
|
||||
orgName: orgs[0].name,
|
||||
orgSlug: orgs[0].slug
|
||||
});
|
||||
if (!silent) {
|
||||
console.log(chalk.gray(` Auto-selected organization: ${orgs[0].name}`));
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
orgId: orgs[0].id,
|
||||
orgName: orgs[0].name,
|
||||
orgSlug: orgs[0].slug
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple orgs - prompt for selection
|
||||
if (!silent) {
|
||||
console.log(chalk.yellow('No organization selected.'));
|
||||
}
|
||||
|
||||
const response = await inquirer.prompt<{ orgId: string }>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'orgId',
|
||||
message: promptMessage || 'Select an organization:',
|
||||
choices: orgs.map((org) => ({
|
||||
name: org.name,
|
||||
value: org.id
|
||||
}))
|
||||
}
|
||||
]);
|
||||
|
||||
const selectedOrg = orgs.find((o) => o.id === response.orgId);
|
||||
if (selectedOrg) {
|
||||
await authManager.updateContext({
|
||||
orgId: selectedOrg.id,
|
||||
orgName: selectedOrg.name,
|
||||
orgSlug: selectedOrg.slug
|
||||
});
|
||||
ui.displaySuccess(`Selected organization: ${selectedOrg.name}`);
|
||||
return {
|
||||
success: true,
|
||||
orgId: selectedOrg.id,
|
||||
orgName: selectedOrg.name,
|
||||
orgSlug: selectedOrg.slug
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to select organization'
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
ui.displayError(`Failed to select organization: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if org is selected, returning the current org info without prompting.
|
||||
* Use this for non-interactive checks.
|
||||
*/
|
||||
export function getSelectedOrg(authManager: AuthManager): OrgSelectionResult {
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (context?.orgId) {
|
||||
return {
|
||||
success: true,
|
||||
orgId: context.orgId,
|
||||
orgName: context.orgName,
|
||||
orgSlug: context.orgSlug
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'No organization selected'
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user