Twenty sdk cli oauth (#18638)
<img width="1418" height="804" alt="image" src="https://github.com/user-attachments/assets/de6c8222-6496-4a71-bc21-7e5e1269d5cb" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -1,78 +1,31 @@
|
||||
import { formatPath } from '@/cli/utilities/file/file-path';
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import { AppBuildCommand } from './app/app-build';
|
||||
import { AppDevCommand } from './app/app-dev';
|
||||
import { AppPublishCommand } from './app/app-publish';
|
||||
import { AppTypecheckCommand } from './app/app-typecheck';
|
||||
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 { LogicFunctionExecuteCommand } from './logic-function/logic-function-execute';
|
||||
import { LogicFunctionLogsCommand } from './logic-function/logic-function-logs';
|
||||
import { AuthSwitchCommand } from './auth/auth-switch';
|
||||
import { EntityAddCommand } from './entity/entity-add';
|
||||
import { AppBuildCommand } from './build';
|
||||
import { AppDevCommand } from './dev';
|
||||
import { AppPublishCommand } from './publish';
|
||||
import { AppTypecheckCommand } from './typecheck';
|
||||
import { AppUninstallCommand } from './uninstall';
|
||||
import { DeployCommand } from './deploy';
|
||||
import { LogicFunctionExecuteCommand } from './exec';
|
||||
import { LogicFunctionLogsCommand } from './logs';
|
||||
import { EntityAddCommand } from './add';
|
||||
import { registerRemoteCommands } from './remote';
|
||||
import { SyncableEntity } from 'twenty-shared/application';
|
||||
|
||||
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')
|
||||
.description('Authenticate with Twenty')
|
||||
.option('--api-key <key>', 'API key for authentication')
|
||||
.option('--api-url <url>', 'Twenty API URL')
|
||||
.action(async (options) => {
|
||||
await loginCommand.execute(options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('auth:logout')
|
||||
.description('Remove authentication credentials')
|
||||
.action(async () => {
|
||||
await logoutCommand.execute();
|
||||
});
|
||||
|
||||
program
|
||||
.command('auth:status')
|
||||
.description('Check authentication status')
|
||||
.action(async () => {
|
||||
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 buildCommand = new AppBuildCommand();
|
||||
const devCommand = new AppDevCommand();
|
||||
const publishCommand = new AppPublishCommand();
|
||||
const typecheckCommand = new AppTypecheckCommand();
|
||||
const uninstallCommand = new AppUninstallCommand();
|
||||
const deployCommand = new DeployCommand();
|
||||
const addCommand = new EntityAddCommand();
|
||||
const logsCommand = new LogicFunctionLogsCommand();
|
||||
const executeCommand = new LogicFunctionExecuteCommand();
|
||||
|
||||
program
|
||||
.command('app:dev [appPath]')
|
||||
.command('dev [appPath]')
|
||||
.description('Watch and sync local application changes')
|
||||
.action(async (appPath) => {
|
||||
await devCommand.execute({
|
||||
@@ -81,7 +34,7 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:build [appPath]')
|
||||
.command('build [appPath]')
|
||||
.description('Build, sync, and generate API client into .twenty/output/')
|
||||
.option('--tarball', 'Also pack into a .tgz tarball')
|
||||
.action(async (appPath, options) => {
|
||||
@@ -92,24 +45,29 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:publish [appPath]')
|
||||
.description(
|
||||
'Build and publish to npm, or to a Twenty server with --server',
|
||||
)
|
||||
.option('--server <url>', 'Publish to a Twenty server instead of npm')
|
||||
.option('--token <token>', 'Auth token for the server')
|
||||
.command('deploy [appPath]')
|
||||
.description('Build and deploy to a Twenty server')
|
||||
.option('-r, --remote <name>', 'Deploy to a specific remote')
|
||||
.action(async (appPath, options) => {
|
||||
await deployCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
remote: options.remote,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('publish [appPath]')
|
||||
.description('Build and publish to npm')
|
||||
.option('--tag <tag>', 'npm dist-tag (e.g. beta, next)')
|
||||
.action(async (appPath, options) => {
|
||||
await publishCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
server: options.server,
|
||||
token: options.token,
|
||||
tag: options.tag,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:typecheck [appPath]')
|
||||
.command('typecheck [appPath]')
|
||||
.description('Run TypeScript type checking on the application')
|
||||
.action(async (appPath) => {
|
||||
await typecheckCommand.execute({
|
||||
@@ -118,7 +76,7 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:uninstall [appPath]')
|
||||
.command('uninstall [appPath]')
|
||||
.description('Uninstall application from Twenty')
|
||||
.option('-y, --yes', 'Skip confirmation prompt')
|
||||
.action(async (appPath?: string, options?: { yes?: boolean }) => {
|
||||
@@ -133,8 +91,10 @@ export const registerCommands = (program: Command): void => {
|
||||
}
|
||||
});
|
||||
|
||||
registerRemoteCommands(program);
|
||||
|
||||
program
|
||||
.command('entity:add [entityType]')
|
||||
.command('add [entityType]')
|
||||
.option('--path <path>', 'Path in which the entity should be created.')
|
||||
.description(
|
||||
`Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`,
|
||||
@@ -143,35 +103,8 @@ export const registerCommands = (program: Command): void => {
|
||||
await addCommand.execute(entityType as SyncableEntity, options?.path);
|
||||
});
|
||||
|
||||
// Function commands
|
||||
program
|
||||
.command('function:logs [appPath]')
|
||||
.option(
|
||||
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
|
||||
'Only show logs for the function with this universal ID',
|
||||
)
|
||||
.option(
|
||||
'-n, --functionName <functionName>',
|
||||
'Only show logs for the function with this name',
|
||||
)
|
||||
.description('Watch application function logs')
|
||||
.action(
|
||||
async (
|
||||
appPath?: string,
|
||||
options?: {
|
||||
functionUniversalIdentifier?: string;
|
||||
functionName?: string;
|
||||
},
|
||||
) => {
|
||||
await logsCommand.execute({
|
||||
...options,
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
program
|
||||
.command('function:execute [appPath]')
|
||||
.command('exec [appPath]')
|
||||
.option('--postInstall', 'Execute post-install logic function if defined')
|
||||
.option(
|
||||
'-p, --payload <payload>',
|
||||
@@ -216,4 +149,30 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
program
|
||||
.command('logs [appPath]')
|
||||
.option(
|
||||
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
|
||||
'Only show logs for the function with this universal ID',
|
||||
)
|
||||
.option(
|
||||
'-n, --functionName <functionName>',
|
||||
'Only show logs for the function with this name',
|
||||
)
|
||||
.description('Watch application function logs')
|
||||
.action(
|
||||
async (
|
||||
appPath?: string,
|
||||
options?: {
|
||||
functionUniversalIdentifier?: string;
|
||||
functionName?: string;
|
||||
},
|
||||
) => {
|
||||
await logsCommand.execute({
|
||||
...options,
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
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 workspaceName of availableWorkspaces) {
|
||||
const config =
|
||||
await this.configService.getConfigForWorkspace(workspaceName);
|
||||
const hasCredentials = !!config.apiKey;
|
||||
const isDefault = workspaceName === currentDefault;
|
||||
|
||||
const defaultIndicator = isDefault ? chalk.green(' (default)') : '';
|
||||
const credentialStatus = hasCredentials
|
||||
? chalk.green('●')
|
||||
: chalk.gray('○');
|
||||
|
||||
console.log(
|
||||
` ${credentialStatus} ${workspaceName}${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,63 +0,0 @@
|
||||
import { authLogin } from '@/cli/public-operations/auth-login';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
export class AuthLoginCommand {
|
||||
private configService = new ConfigService();
|
||||
|
||||
async execute(options: { apiKey?: string; apiUrl?: string }): Promise<void> {
|
||||
let { apiKey, apiUrl } = options;
|
||||
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
if (!apiUrl) {
|
||||
const urlAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'apiUrl',
|
||||
message: 'Twenty API URL:',
|
||||
default: config.apiUrl,
|
||||
validate: (input) => {
|
||||
try {
|
||||
new URL(input);
|
||||
return true;
|
||||
} catch {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
apiUrl = urlAnswer.apiUrl;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
const keyAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'apiKey',
|
||||
message: 'API Key:',
|
||||
mask: '*',
|
||||
validate: (input) => input.length > 0 || 'API key is required',
|
||||
},
|
||||
]);
|
||||
apiKey = keyAnswer.apiKey;
|
||||
}
|
||||
|
||||
const result = await authLogin({ apiKey: apiKey!, apiUrl: apiUrl! });
|
||||
|
||||
if (result.success) {
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Successfully authenticated with Twenty (workspace: ${activeWorkspace})`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.red('✗ Authentication failed. Please check your credentials.'),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { authLogout } from '@/cli/public-operations/auth-logout';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export class AuthLogoutCommand {
|
||||
async execute(): Promise<void> {
|
||||
const result = await authLogout();
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('Logout failed:'), result.error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
console.log(
|
||||
chalk.green(`✓ Successfully logged out (workspace: ${activeWorkspace})`),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export class AuthStatusCommand {
|
||||
private configService = new ConfigService();
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
console.log(chalk.blue('Authentication Status:'));
|
||||
console.log(`Workspace: ${activeWorkspace}`);
|
||||
console.log(`API URL: ${config.apiUrl}`);
|
||||
console.log(
|
||||
`API Key: ${config.apiKey ? '***' + config.apiKey.slice(-4) : 'Not set'}`,
|
||||
);
|
||||
|
||||
if (config.apiKey) {
|
||||
const validateAuth = await this.apiService.validateAuth();
|
||||
console.log(
|
||||
`Status: ${validateAuth.authValid ? chalk.green('✓ Valid') : chalk.red('✗ Invalid')}`,
|
||||
);
|
||||
} else {
|
||||
console.log(`Status: ${chalk.yellow('⚠ Not authenticated')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Status check failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
export class AuthSwitchCommand {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!availableWorkspaces.includes(workspace!)) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`✗ Workspace "${workspace}" not found. Available workspaces: ${availableWorkspaces.join(', ')}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (workspace === currentDefault) {
|
||||
console.log(
|
||||
chalk.blue(`ℹ "${workspace}" is already the default workspace.`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.configService.setDefaultWorkspace(workspace!);
|
||||
ConfigService.setActiveWorkspace(workspace);
|
||||
|
||||
const config = await this.configService.getConfig();
|
||||
const hasCredentials = !!config.apiKey;
|
||||
|
||||
console.log(
|
||||
chalk.green(`✓ Switched default workspace to "${workspace}"`),
|
||||
);
|
||||
|
||||
if (hasCredentials) {
|
||||
const validateAuth = await this.apiService.validateAuth();
|
||||
|
||||
if (validateAuth.authValid) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { appBuild } from '@/cli/public-operations/app-build';
|
||||
import { appBuild } from '@/cli/operations/build';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import chalk from 'chalk';
|
||||
@@ -0,0 +1,56 @@
|
||||
import { appDeploy } from '@/cli/operations/deploy';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type DeployCommandOptions = {
|
||||
appPath?: string;
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
export class DeployCommand {
|
||||
async execute(options: DeployCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
|
||||
const configService = new ConfigService();
|
||||
let serverUrl: string;
|
||||
let token: string | undefined;
|
||||
|
||||
if (options.remote) {
|
||||
const remoteConfig = await configService.getConfigForRemote(
|
||||
options.remote,
|
||||
);
|
||||
|
||||
serverUrl = remoteConfig.apiUrl;
|
||||
token = remoteConfig.accessToken ?? remoteConfig.apiKey;
|
||||
} else {
|
||||
const config = await configService.getConfig();
|
||||
|
||||
serverUrl = config.apiUrl;
|
||||
token = config.accessToken ?? config.apiKey;
|
||||
}
|
||||
|
||||
const remoteName = options.remote ?? ConfigService.getActiveRemote();
|
||||
|
||||
console.log(chalk.blue(`Deploying to ${remoteName} (${serverUrl})...`));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appDeploy({
|
||||
appPath,
|
||||
serverUrl,
|
||||
token,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(result.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Deployed successfully'));
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -1,8 +1,5 @@
|
||||
import { functionExecute } from '@/cli/public-operations/function-execute';
|
||||
import {
|
||||
APP_ERROR_CODES,
|
||||
FUNCTION_ERROR_CODES,
|
||||
} from '@/cli/public-operations/types';
|
||||
import { functionExecute } from '@/cli/operations/execute';
|
||||
import { APP_ERROR_CODES, FUNCTION_ERROR_CODES } from '@/cli/types';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -80,7 +77,7 @@ export class LogicFunctionExecuteCommand {
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'No functions found for this application. Have you synced your app with `yarn app:dev`?',
|
||||
'No functions found for this application. Have you synced your app with `twenty dev`?',
|
||||
),
|
||||
);
|
||||
}
|
||||
+3
-21
@@ -1,12 +1,10 @@
|
||||
import { appPublish } from '@/cli/public-operations/app-publish';
|
||||
import { appPublish } from '@/cli/operations/publish';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppPublishCommandOptions = {
|
||||
appPath?: string;
|
||||
server?: string;
|
||||
token?: string;
|
||||
tag?: string;
|
||||
};
|
||||
|
||||
@@ -16,22 +14,12 @@ export class AppPublishCommand {
|
||||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
|
||||
const isServerPublish = !!options.server;
|
||||
|
||||
console.log(
|
||||
chalk.blue(
|
||||
isServerPublish
|
||||
? `Publishing to server ${options.server}...`
|
||||
: 'Publishing to npm...',
|
||||
),
|
||||
);
|
||||
console.log(chalk.blue('Publishing to npm...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appPublish({
|
||||
appPath,
|
||||
server: options.server,
|
||||
token: options.token,
|
||||
npmTag: options.tag,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
@@ -41,12 +29,6 @@ export class AppPublishCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.data.target === 'npm') {
|
||||
console.log(chalk.green('✓ Published to npm successfully'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green('✓ Published to server and installed successfully'),
|
||||
);
|
||||
}
|
||||
console.log(chalk.green('✓ Published to npm successfully'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import { authLogin } from '@/cli/operations/login';
|
||||
import { authLoginOAuth } from '@/cli/operations/login-oauth';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
const deriveRemoteName = (url: string): string => {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
return hostname.replace(/\./g, '-');
|
||||
} catch {
|
||||
return 'remote';
|
||||
}
|
||||
};
|
||||
|
||||
const authenticate = async (apiUrl: string, token?: string): Promise<void> => {
|
||||
const result = token
|
||||
? await authLogin({ apiKey: token, apiUrl })
|
||||
: await runOAuthWithApiKeyFallback(apiUrl);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('✗ Authentication failed.'));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const runOAuthWithApiKeyFallback = async (
|
||||
apiUrl: string,
|
||||
): Promise<{ success: boolean }> => {
|
||||
await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirm',
|
||||
message: 'Press Enter to open the browser for authentication...',
|
||||
},
|
||||
]);
|
||||
|
||||
const oauthResult = await authLoginOAuth({ apiUrl });
|
||||
|
||||
if (oauthResult.success) {
|
||||
return oauthResult;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(oauthResult.error.message));
|
||||
|
||||
const keyAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'apiKey',
|
||||
message: 'API Key:',
|
||||
mask: '*',
|
||||
validate: (input: string) => input.length > 0 || 'API key is required',
|
||||
},
|
||||
]);
|
||||
|
||||
return authLogin({ apiKey: keyAnswer.apiKey, apiUrl });
|
||||
};
|
||||
|
||||
export const registerRemoteCommands = (program: Command): void => {
|
||||
const remote = program
|
||||
.command('remote')
|
||||
.description('Manage remote Twenty servers');
|
||||
|
||||
remote
|
||||
.command('add [nameOrUrl]')
|
||||
.description('Add a new remote or re-authenticate an existing one')
|
||||
.option('--as <name>', 'Name for this remote')
|
||||
.option('--local', 'Connect to local development server')
|
||||
.option('--token <token>', 'API key for non-interactive auth')
|
||||
.option('--url <url>', 'Server URL (alternative to positional arg)')
|
||||
.action(
|
||||
async (
|
||||
nameOrUrl: string | undefined,
|
||||
options: {
|
||||
as?: string;
|
||||
local?: boolean;
|
||||
token?: string;
|
||||
url?: string;
|
||||
},
|
||||
) => {
|
||||
const configService = new ConfigService();
|
||||
const existingRemotes = await configService.getRemotes();
|
||||
|
||||
if (options.local) {
|
||||
const remoteName = options.as ?? 'local';
|
||||
const token =
|
||||
options.token ??
|
||||
(
|
||||
await inquirer.prompt<{ apiKey: string }>([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'apiKey',
|
||||
message: 'API Key for local server:',
|
||||
mask: '*',
|
||||
validate: (input: string) =>
|
||||
input.length > 0 || 'API key is required',
|
||||
},
|
||||
])
|
||||
).apiKey;
|
||||
|
||||
ConfigService.setActiveRemote(remoteName);
|
||||
await authenticate('http://localhost:3000', token);
|
||||
console.log(chalk.green(`✓ Authenticated remote "${remoteName}".`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-authenticate an existing remote by name
|
||||
const isExistingRemote =
|
||||
nameOrUrl !== undefined && existingRemotes.includes(nameOrUrl);
|
||||
|
||||
if (isExistingRemote) {
|
||||
const config = await configService.getConfigForRemote(nameOrUrl);
|
||||
|
||||
ConfigService.setActiveRemote(nameOrUrl);
|
||||
await authenticate(config.apiUrl, options.token);
|
||||
console.log(chalk.green(`✓ Re-authenticated remote "${nameOrUrl}".`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the URL — from args, flags, or interactive prompt
|
||||
const apiUrl =
|
||||
nameOrUrl ??
|
||||
options.url ??
|
||||
(options.token
|
||||
? 'http://localhost:3000'
|
||||
: (
|
||||
await inquirer.prompt<{ apiUrl: string }>([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'apiUrl',
|
||||
message: 'Twenty server URL:',
|
||||
validate: (input: string) => {
|
||||
try {
|
||||
new URL(input);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
).apiUrl);
|
||||
|
||||
const name = options.as ?? deriveRemoteName(apiUrl);
|
||||
|
||||
ConfigService.setActiveRemote(name);
|
||||
await authenticate(apiUrl, options.token);
|
||||
|
||||
const defaultRemote = await configService.getDefaultRemote();
|
||||
|
||||
if (defaultRemote === 'local') {
|
||||
await configService.setDefaultRemote(name);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ Authenticated remote "${name}".`));
|
||||
},
|
||||
);
|
||||
|
||||
remote
|
||||
.command('list')
|
||||
.description('List all configured remotes')
|
||||
.action(async () => {
|
||||
const configService = new ConfigService();
|
||||
const remotes = await configService.getRemotes();
|
||||
const defaultRemote = await configService.getDefaultRemote();
|
||||
|
||||
if (remotes.length === 0) {
|
||||
console.log('No remotes configured.');
|
||||
console.log("Use 'twenty remote add <url>' to add one.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
for (const remoteName of remotes) {
|
||||
const config = await configService.getConfigForRemote(remoteName);
|
||||
|
||||
const authMethod = config.accessToken
|
||||
? 'oauth'
|
||||
: config.apiKey
|
||||
? 'api-key'
|
||||
: 'none';
|
||||
|
||||
const isDefault = remoteName === defaultRemote;
|
||||
const marker = isDefault ? '* ' : ' ';
|
||||
const nameText = isDefault ? chalk.bold(remoteName) : remoteName;
|
||||
|
||||
console.log(
|
||||
`${marker}${nameText} ${chalk.gray(config.apiUrl)} [${authMethod}]`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
chalk.gray("Use 'twenty remote switch <name>' to change default"),
|
||||
);
|
||||
});
|
||||
|
||||
remote
|
||||
.command('switch [name]')
|
||||
.description('Set the default remote')
|
||||
.action(async (nameArg?: string) => {
|
||||
const configService = new ConfigService();
|
||||
|
||||
const remoteName =
|
||||
nameArg ??
|
||||
(
|
||||
await inquirer.prompt<{ remote: string }>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'remote',
|
||||
message: 'Select default remote:',
|
||||
choices: await configService.getRemotes(),
|
||||
},
|
||||
])
|
||||
).remote;
|
||||
|
||||
const remotes = await configService.getRemotes();
|
||||
|
||||
if (!remotes.includes(remoteName)) {
|
||||
console.error(chalk.red(`Remote "${remoteName}" not found.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await configService.setDefaultRemote(remoteName);
|
||||
console.log(chalk.green(`✓ Default remote set to "${remoteName}".`));
|
||||
});
|
||||
|
||||
remote
|
||||
.command('status')
|
||||
.description('Show active remote and authentication status')
|
||||
.action(async () => {
|
||||
const configService = new ConfigService();
|
||||
const apiService = new ApiService();
|
||||
const activeRemote = ConfigService.getActiveRemote();
|
||||
const config = await configService.getConfig();
|
||||
|
||||
const authMethod = config.accessToken
|
||||
? 'oauth'
|
||||
: config.apiKey
|
||||
? 'api-key'
|
||||
: 'none';
|
||||
|
||||
console.log(` Remote: ${chalk.bold(activeRemote)}`);
|
||||
console.log(` Server: ${config.apiUrl}`);
|
||||
|
||||
if (authMethod === 'none') {
|
||||
console.log(` Auth: ${chalk.yellow('not configured')}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { authValid } = await apiService.validateAuth();
|
||||
|
||||
const statusText = authValid
|
||||
? chalk.green(`${authMethod} (valid)`)
|
||||
: chalk.red(`${authMethod} (invalid)`);
|
||||
|
||||
console.log(` Auth: ${statusText}`);
|
||||
});
|
||||
|
||||
remote
|
||||
.command('remove <name>')
|
||||
.description('Remove a remote')
|
||||
.action(async (name: string) => {
|
||||
const configService = new ConfigService();
|
||||
const remotes = await configService.getRemotes();
|
||||
|
||||
if (!remotes.includes(name)) {
|
||||
console.error(chalk.red(`Remote "${name}" not found.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
ConfigService.setActiveRemote(name);
|
||||
await configService.clearConfig();
|
||||
|
||||
console.log(chalk.green(`✓ Remote "${name}" removed.`));
|
||||
});
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { appUninstall } from '@/cli/public-operations/app-uninstall';
|
||||
import { appUninstall } from '@/cli/operations/uninstall';
|
||||
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
Reference in New Issue
Block a user