2093 extensibility add twenty auth switch command (#17286)
add auth:switch and auth:list commands
This commit is contained in:
@@ -195,6 +195,8 @@ const createPackageJson = async ({
|
||||
'auth:login': 'twenty auth:login',
|
||||
'auth:logout': 'twenty auth:logout',
|
||||
'auth:status': 'twenty auth:status',
|
||||
'auth:switch': 'twenty auth:switch',
|
||||
'auth:list': 'twenty auth:list',
|
||||
'app:dev': 'twenty app:dev',
|
||||
'app:sync': 'twenty app:sync',
|
||||
'entity:add': 'twenty entity:add',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -173,18 +173,32 @@ The first time you run `yarn auth:login`, you'll be prompted for:
|
||||
- API URL (defaults to http://localhost:3000 or your current workspace profile)
|
||||
- API key
|
||||
|
||||
Your credentials are stored per-user in `~/.twenty/config.json`. You can maintain multiple profiles and switch using `--workspace <name>`.
|
||||
Your credentials are stored per-user in `~/.twenty/config.json`. You can maintain multiple profiles and switch between them.
|
||||
|
||||
Examples:
|
||||
### Managing workspaces
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn auth:login
|
||||
|
||||
# Use a specific workspace profile
|
||||
# Login to a specific workspace profile
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
|
||||
## Use the SDK resources (types & config)
|
||||
|
||||
The twenty-sdk provides typed building blocks and helper functions you use inside your app. Below are the key pieces you'll touch most often.
|
||||
|
||||
@@ -68,6 +68,14 @@ Authenticate the CLI against your Twenty workspace.
|
||||
|
||||
- `twenty auth:status` — Print the current authentication status (API URL, masked API key, validity).
|
||||
|
||||
- `twenty auth:list` — List all configured workspaces.
|
||||
- Behavior: Displays all available workspaces with their authentication status and API URLs. Shows which workspace is the current default.
|
||||
|
||||
- `twenty auth:switch [workspace]` — Switch the default workspace for authentication.
|
||||
- Arguments:
|
||||
- `workspace` (optional): Name of the workspace to switch to. If omitted, shows an interactive selection.
|
||||
- Behavior: Sets the specified workspace as the default, so subsequent commands use it without needing `--workspace`.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
@@ -85,6 +93,15 @@ twenty auth:status
|
||||
|
||||
# Logout current profile
|
||||
twenty auth:logout
|
||||
|
||||
# List all configured workspaces
|
||||
twenty auth:list
|
||||
|
||||
# Switch default workspace interactively
|
||||
twenty auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
twenty auth:switch production
|
||||
```
|
||||
|
||||
### App
|
||||
@@ -166,12 +183,13 @@ twenty function:execute -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf -p '{"key": "val
|
||||
The CLI stores configuration per user in a JSON file:
|
||||
|
||||
- Location: `~/.twenty/config.json`
|
||||
- Structure: Profiles keyed by workspace name. The active profile is selected with `--workspace <name>`.
|
||||
- Structure: Profiles keyed by workspace name. The active profile is selected with `--workspace <name>` or by the `defaultWorkspace` setting.
|
||||
|
||||
Example configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultWorkspace": "prod",
|
||||
"profiles": {
|
||||
"default": {
|
||||
"apiUrl": "http://localhost:3000",
|
||||
@@ -188,8 +206,10 @@ Example configuration file:
|
||||
Notes:
|
||||
|
||||
- If a profile is missing, `apiUrl` defaults to `http://localhost:3000` until set.
|
||||
- `twenty auth:login` writes the `apiUrl` and `apiKey` for the default profile.
|
||||
- `twenty auth:login` writes the `apiUrl` and `apiKey` for the active workspace profile.
|
||||
- `twenty auth:login --workspace custom-workspace` writes the `apiUrl` and `apiKey` for a custom `custom-workspace` profile.
|
||||
- `twenty auth:switch` sets the `defaultWorkspace` field, which is used when `--workspace` is not specified.
|
||||
- `twenty auth:list` shows all configured workspaces and their authentication status.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -17,15 +17,21 @@ program
|
||||
|
||||
program.option(
|
||||
'--workspace <name>',
|
||||
'Use a specific workspace configuration',
|
||||
'default',
|
||||
'Use a specific workspace configuration (overrides the default set by auth:switch)',
|
||||
);
|
||||
|
||||
program.hook('preAction', (thisCommand) => {
|
||||
program.hook('preAction', async (thisCommand) => {
|
||||
const opts = (thisCommand as any).optsWithGlobals
|
||||
? (thisCommand as any).optsWithGlobals()
|
||||
: thisCommand.opts();
|
||||
const workspace = opts.workspace;
|
||||
|
||||
// If --workspace is provided, use it; otherwise, read the persisted default
|
||||
let workspace = opts.workspace;
|
||||
if (!workspace) {
|
||||
const configService = new ConfigService();
|
||||
workspace = await configService.getDefaultWorkspace();
|
||||
}
|
||||
|
||||
ConfigService.setActiveWorkspace(workspace);
|
||||
console.log(
|
||||
chalk.gray(`👩💻 Workspace - ${ConfigService.getActiveWorkspace()}`),
|
||||
|
||||
@@ -6,11 +6,13 @@ import { AppDevCommand } from './app/app-dev';
|
||||
import { AppGenerateCommand } from './app/app-generate';
|
||||
import { AppSyncCommand } from './app/app-sync';
|
||||
import { AppUninstallCommand } from './app/app-uninstall';
|
||||
import { AuthListCommand } from './auth/auth-list';
|
||||
import { AuthLoginCommand } from './auth/auth-login';
|
||||
import { AuthLogoutCommand } from './auth/auth-logout';
|
||||
import { AuthStatusCommand } from './auth/auth-status';
|
||||
import { FunctionExecuteCommand } from './function/function-execute';
|
||||
import { FunctionLogsCommand } from './function/function-logs';
|
||||
import { AuthSwitchCommand } from './auth/auth-switch';
|
||||
import {
|
||||
EntityAddCommand,
|
||||
isSyncableEntity,
|
||||
@@ -19,9 +21,11 @@ import {
|
||||
|
||||
export const registerCommands = (program: Command): void => {
|
||||
// Auth commands
|
||||
const listCommand = new AuthListCommand();
|
||||
const loginCommand = new AuthLoginCommand();
|
||||
const logoutCommand = new AuthLogoutCommand();
|
||||
const statusCommand = new AuthStatusCommand();
|
||||
const switchCommand = new AuthSwitchCommand();
|
||||
|
||||
program
|
||||
.command('auth:login')
|
||||
@@ -46,6 +50,20 @@ export const registerCommands = (program: Command): void => {
|
||||
await statusCommand.execute();
|
||||
});
|
||||
|
||||
program
|
||||
.command('auth:switch [workspace]')
|
||||
.description('Switch the default workspace for authentication')
|
||||
.action(async (workspace?: string) => {
|
||||
await switchCommand.execute({ workspace });
|
||||
});
|
||||
|
||||
program
|
||||
.command('auth:list')
|
||||
.description('List all configured workspaces')
|
||||
.action(async () => {
|
||||
await listCommand.execute();
|
||||
});
|
||||
|
||||
// App commands
|
||||
const devCommand = new AppDevCommand();
|
||||
const syncCommand = new AppSyncCommand();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import chalk from 'chalk';
|
||||
import { ConfigService } from '@/cli/utilities/config/services/config.service';
|
||||
|
||||
export class AuthListCommand {
|
||||
private configService = new ConfigService();
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
const availableWorkspaces =
|
||||
await this.configService.getAvailableWorkspaces();
|
||||
const currentDefault = await this.configService.getDefaultWorkspace();
|
||||
|
||||
if (availableWorkspaces.length === 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠ No workspaces configured. Use `twenty auth:login` to create one.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue('Available workspaces:\n'));
|
||||
|
||||
for (const workspace of availableWorkspaces) {
|
||||
const config = await this.configService.getConfigForWorkspace(workspace);
|
||||
const hasCredentials = !!config.apiKey;
|
||||
const isDefault = workspace === currentDefault;
|
||||
|
||||
const defaultIndicator = isDefault ? chalk.green(' (default)') : '';
|
||||
const credentialStatus = hasCredentials
|
||||
? chalk.green('●')
|
||||
: chalk.gray('○');
|
||||
|
||||
console.log(` ${credentialStatus} ${workspace}${defaultIndicator}`);
|
||||
console.log(chalk.gray(` API URL: ${config.apiUrl}`));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.gray('● = authenticated, ○ = no credentials'));
|
||||
console.log(
|
||||
chalk.gray('Use `twenty auth:switch <workspace>` to change default'),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('List failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,106 @@
|
||||
// Placeholder for auth switch command
|
||||
// TODO: Implement workspace switching functionality
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { ApiService } from '@/cli/utilities/api/services/api.service';
|
||||
import { ConfigService } from '@/cli/utilities/config/services/config.service';
|
||||
|
||||
export class AuthSwitchCommand {
|
||||
async execute(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
private configService = new ConfigService();
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: { workspace?: string }): Promise<void> {
|
||||
try {
|
||||
let { workspace } = options;
|
||||
|
||||
const availableWorkspaces =
|
||||
await this.configService.getAvailableWorkspaces();
|
||||
const currentDefault = await this.configService.getDefaultWorkspace();
|
||||
|
||||
if (availableWorkspaces.length === 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠ No workspaces configured. Use `twenty auth:login` to create one.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If workspace is not provided, show interactive selection
|
||||
if (!workspace) {
|
||||
// Build choices with indicators for current default
|
||||
const choices = availableWorkspaces.map((ws) => ({
|
||||
name: ws === currentDefault ? `${ws} (current default)` : ws,
|
||||
value: ws,
|
||||
}));
|
||||
|
||||
const answer = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'workspace',
|
||||
message: 'Select a workspace to set as default:',
|
||||
choices,
|
||||
default: currentDefault,
|
||||
},
|
||||
]);
|
||||
|
||||
workspace = answer.workspace as string;
|
||||
}
|
||||
|
||||
// Validate that the workspace exists (workspace is guaranteed to be defined here)
|
||||
if (!availableWorkspaces.includes(workspace!)) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`✗ Workspace "${workspace}" not found. Available workspaces: ${availableWorkspaces.join(', ')}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If already the default, inform and exit
|
||||
if (workspace === currentDefault) {
|
||||
console.log(
|
||||
chalk.blue(`ℹ "${workspace}" is already the default workspace.`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new default workspace
|
||||
await this.configService.setDefaultWorkspace(workspace!);
|
||||
|
||||
// Also set it as active for the current session to validate
|
||||
ConfigService.setActiveWorkspace(workspace);
|
||||
|
||||
// Check authentication status for the new workspace
|
||||
const config = await this.configService.getConfig();
|
||||
const hasCredentials = !!config.apiKey;
|
||||
|
||||
console.log(
|
||||
chalk.green(`✓ Switched default workspace to "${workspace}"`),
|
||||
);
|
||||
|
||||
if (hasCredentials) {
|
||||
const isValid = await this.apiService.validateAuth();
|
||||
if (isValid) {
|
||||
console.log(chalk.green('✓ Authentication is valid'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠ Authentication credentials exist but are invalid. Run `twenty auth:login` to re-authenticate.',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠ No credentials configured for this workspace. Run `twenty auth:login` to authenticate.',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Switch failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type TwentyConfig = {
|
||||
|
||||
type PersistedConfig = TwentyConfig & {
|
||||
profiles?: Record<string, TwentyConfig>;
|
||||
defaultWorkspace?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_WORKSPACE_NAME = 'default';
|
||||
@@ -40,16 +41,19 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
async getConfig(): Promise<TwentyConfig> {
|
||||
return this.getConfigForWorkspace(this.getActiveWorkspaceName());
|
||||
}
|
||||
|
||||
async getConfigForWorkspace(workspaceName: string): Promise<TwentyConfig> {
|
||||
const defaultConfig = this.getDefaultConfig();
|
||||
try {
|
||||
const raw = await this.readRawConfig();
|
||||
const profile = this.getActiveWorkspaceName();
|
||||
|
||||
const profileConfig =
|
||||
profile === DEFAULT_WORKSPACE_NAME &&
|
||||
workspaceName === DEFAULT_WORKSPACE_NAME &&
|
||||
!raw.profiles?.[DEFAULT_WORKSPACE_NAME]
|
||||
? raw
|
||||
: raw.profiles?.[profile];
|
||||
: raw.profiles?.[workspaceName];
|
||||
|
||||
// Fallback to legacy top-level values if profile value is missing
|
||||
const apiUrl = profileConfig?.apiUrl ?? defaultConfig.apiUrl;
|
||||
@@ -110,4 +114,39 @@ export class ConfigService {
|
||||
apiUrl: 'http://localhost:3000',
|
||||
};
|
||||
}
|
||||
|
||||
async getAvailableWorkspaces(): Promise<string[]> {
|
||||
try {
|
||||
const raw = await this.readRawConfig();
|
||||
const workspaces = new Set<string>();
|
||||
|
||||
// Always include the default workspace
|
||||
workspaces.add(DEFAULT_WORKSPACE_NAME);
|
||||
|
||||
// Add all profiles
|
||||
if (raw.profiles) {
|
||||
Object.keys(raw.profiles).forEach((name) => workspaces.add(name));
|
||||
}
|
||||
|
||||
return Array.from(workspaces).sort();
|
||||
} catch {
|
||||
return [DEFAULT_WORKSPACE_NAME];
|
||||
}
|
||||
}
|
||||
|
||||
async getDefaultWorkspace(): Promise<string> {
|
||||
try {
|
||||
const raw = await this.readRawConfig();
|
||||
return raw.defaultWorkspace ?? DEFAULT_WORKSPACE_NAME;
|
||||
} catch {
|
||||
return DEFAULT_WORKSPACE_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
async setDefaultWorkspace(name: string): Promise<void> {
|
||||
const raw = await this.readRawConfig();
|
||||
raw.defaultWorkspace = name;
|
||||
await fs.ensureDir(path.dirname(this.configPath));
|
||||
await fs.writeFile(this.configPath, JSON.stringify(raw, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user