First Application POC (#14382)
Quick proof of concept for twenty-apps + twenty-cli, with local development / hot reload Let's discuss it! https://github.com/user-attachments/assets/c6789936-cd5f-4110-a265-863a6ac1af2d
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { resolveAppPath } from '../utils/app-path-resolver';
|
||||
import { syncApp } from '../utils/app-sync';
|
||||
|
||||
export class AppDeployCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: { path?: string }): Promise<void> {
|
||||
try {
|
||||
const appPath = await resolveAppPath(options.path);
|
||||
|
||||
console.log(chalk.blue('🚀 Deploying Twenty Application'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await syncApp(appPath, this.apiService);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('❌ Deployment failed:'), result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ Application deployed successfully'));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Deployment failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import chalk from 'chalk';
|
||||
import * as chokidar from 'chokidar';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { resolveAppPath } from '../utils/app-path-resolver';
|
||||
import { syncApp } from '../utils/app-sync';
|
||||
|
||||
export class AppDevCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: {
|
||||
path?: string;
|
||||
debounce: string;
|
||||
verbose?: boolean;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const appPath = await resolveAppPath(options.path, options.verbose);
|
||||
const debounceMs = parseInt(options.debounce, 10);
|
||||
|
||||
this.logStartupInfo(appPath, debounceMs, options.verbose);
|
||||
|
||||
await syncApp(appPath, this.apiService);
|
||||
|
||||
const watcher = this.setupFileWatcher(
|
||||
appPath,
|
||||
debounceMs,
|
||||
options.verbose,
|
||||
);
|
||||
|
||||
this.setupGracefulShutdown(watcher);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Development mode failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private logStartupInfo(
|
||||
appPath: string,
|
||||
debounceMs: number,
|
||||
verbose?: boolean,
|
||||
): void {
|
||||
console.log(chalk.blue('🚀 Starting Twenty Application Development Mode'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log(chalk.gray(`⏱️ Debounce: ${debounceMs}ms`));
|
||||
console.log(chalk.gray(`🔧 Verbose: ${verbose ? 'On' : 'Off'}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private setupFileWatcher(
|
||||
appPath: string,
|
||||
debounceMs: number,
|
||||
verbose?: boolean,
|
||||
): chokidar.FSWatcher {
|
||||
const watcher = chokidar.watch(appPath, {
|
||||
ignored: /node_modules|\.git/,
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const debouncedSync = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(async () => {
|
||||
console.log(chalk.blue('🔄 Changes detected, syncing...'));
|
||||
await syncApp(appPath, this.apiService);
|
||||
console.log(
|
||||
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
|
||||
);
|
||||
}, debounceMs);
|
||||
};
|
||||
|
||||
watcher.on('change', (filePath) => {
|
||||
if (verbose) {
|
||||
console.log(chalk.gray(`📝 ${filePath} changed`));
|
||||
}
|
||||
debouncedSync();
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
|
||||
);
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
private setupGracefulShutdown(watcher: chokidar.FSWatcher): void {
|
||||
process.on('SIGINT', () => {
|
||||
console.log(chalk.yellow('\n🛑 Stopping development mode...'));
|
||||
watcher.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
createAgentManifest,
|
||||
createManifest,
|
||||
createReadmeContent,
|
||||
} from '../utils/app-template';
|
||||
import { writeJsoncFile } from '../utils/jsonc-parser';
|
||||
|
||||
export class AppInitCommand {
|
||||
async execute(options: { path?: string; name?: string }): Promise<void> {
|
||||
try {
|
||||
const appName = await this.getAppName(options.name);
|
||||
const appDir = this.determineAppDirectory(options.path, appName);
|
||||
|
||||
await this.validateDirectory(appDir);
|
||||
|
||||
this.logCreationInfo(appDir, appName);
|
||||
|
||||
await this.createAppStructure(appDir, appName);
|
||||
|
||||
this.logSuccess(appDir);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Initialization failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppName(providedName?: string): Promise<string> {
|
||||
if (providedName) {
|
||||
return providedName;
|
||||
}
|
||||
|
||||
const nameAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'appName',
|
||||
message: 'Application name:',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
if (!/^[a-z0-9-]+$/.test(input))
|
||||
return 'Name must contain only lowercase letters, numbers, and hyphens';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return nameAnswer.appName;
|
||||
}
|
||||
|
||||
private determineAppDirectory(
|
||||
providedPath?: string,
|
||||
appName?: string,
|
||||
): string {
|
||||
if (providedPath) {
|
||||
return path.resolve(providedPath);
|
||||
}
|
||||
|
||||
return path.join(process.cwd(), appName!);
|
||||
}
|
||||
|
||||
private async validateDirectory(appDir: string): Promise<void> {
|
||||
if (!(await fs.pathExists(appDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await fs.readdir(appDir);
|
||||
if (files.length > 0) {
|
||||
throw new Error(`Directory ${appDir} already exists and is not empty`);
|
||||
}
|
||||
}
|
||||
|
||||
private logCreationInfo(appDir: string, appName: string): void {
|
||||
console.log(chalk.blue('🎯 Creating Twenty Application'));
|
||||
console.log(chalk.gray(`📁 Directory: ${appDir}`));
|
||||
console.log(chalk.gray(`📝 Name: ${appName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private async createAppStructure(
|
||||
appDir: string,
|
||||
appName: string,
|
||||
): Promise<void> {
|
||||
await fs.ensureDir(appDir);
|
||||
|
||||
// Create agents directory
|
||||
const agentsDir = path.join(appDir, 'agents');
|
||||
await fs.ensureDir(agentsDir);
|
||||
|
||||
// Create main manifest with agent references
|
||||
const manifest = createManifest(appName);
|
||||
const manifestPath = path.join(appDir, 'twenty-app.jsonc');
|
||||
await writeJsoncFile(manifestPath, manifest);
|
||||
|
||||
// Create agent manifest file
|
||||
const agentManifest = createAgentManifest(appName);
|
||||
const agentFileName = `${appName}-agent`;
|
||||
const agentPath = path.join(agentsDir, `${agentFileName}.jsonc`);
|
||||
await writeJsoncFile(agentPath, agentManifest);
|
||||
|
||||
// Create README
|
||||
const readmeContent = createReadmeContent(appName, appDir);
|
||||
await fs.writeFile(path.join(appDir, 'README.md'), readmeContent);
|
||||
}
|
||||
|
||||
private logSuccess(appDir: string): void {
|
||||
console.log(chalk.green('✅ Application created successfully!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(` cd ${appDir}`);
|
||||
console.log(' twenty app dev');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { ApiService } from '../services/api.service';
|
||||
|
||||
export class AppInstallCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: { source?: string; type: string }): Promise<void> {
|
||||
try {
|
||||
const source = await this.getSource(options.source);
|
||||
|
||||
this.logInstallInfo(source, options.type);
|
||||
|
||||
const result = await this.apiService.installApplication(
|
||||
source,
|
||||
options.type as 'local' | 'git' | 'marketplace',
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('❌ Installation failed:'), result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ Application installed successfully'));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Installation failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getSource(providedSource?: string): Promise<string> {
|
||||
if (providedSource) {
|
||||
return providedSource;
|
||||
}
|
||||
|
||||
const answer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'source',
|
||||
message: 'Application source (URL, path, or ID):',
|
||||
validate: (input) => input.length > 0 || 'Source is required',
|
||||
},
|
||||
]);
|
||||
|
||||
return answer.source;
|
||||
}
|
||||
|
||||
private logInstallInfo(source: string, type: string): void {
|
||||
console.log(chalk.blue('📦 Installing Twenty Application'));
|
||||
console.log(chalk.gray(`📍 Source: ${source}`));
|
||||
console.log(chalk.gray(`🔧 Type: ${type}`));
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '../services/api.service';
|
||||
|
||||
export class AppListCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
console.log(chalk.blue('📋 Listing Twenty Applications'));
|
||||
console.log('');
|
||||
|
||||
const result = await this.apiService.listApplications();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
console.error(
|
||||
chalk.red('❌ Failed to list applications:'),
|
||||
result.error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.displayApplications(result.data);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('List failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private displayApplications(apps: any[]): void {
|
||||
if (apps.length === 0) {
|
||||
console.log(chalk.yellow('No applications found'));
|
||||
return;
|
||||
}
|
||||
|
||||
apps.forEach((app: any, index: number) => {
|
||||
console.log(`${index + 1}. ${chalk.bold(app.name)}`);
|
||||
console.log(` ${chalk.gray(app.description || 'No description')}`);
|
||||
console.log(` ${chalk.gray(`Version: ${app.version || 'N/A'}`)}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Command } from 'commander';
|
||||
import { AppDeployCommand } from './app-deploy.command';
|
||||
import { AppDevCommand } from './app-dev.command';
|
||||
import { AppInitCommand } from './app-init.command';
|
||||
import { AppInstallCommand } from './app-install.command';
|
||||
import { AppListCommand } from './app-list.command';
|
||||
|
||||
export class AppCommand {
|
||||
private devCommand = new AppDevCommand();
|
||||
private deployCommand = new AppDeployCommand();
|
||||
private installCommand = new AppInstallCommand();
|
||||
private listCommand = new AppListCommand();
|
||||
private initCommand = new AppInitCommand();
|
||||
|
||||
getCommand(): Command {
|
||||
const appCommand = new Command('app');
|
||||
appCommand.description('Application development commands');
|
||||
|
||||
appCommand
|
||||
.command('dev')
|
||||
.description('Watch and sync local application changes')
|
||||
.option(
|
||||
'-p, --path <path>',
|
||||
'Application directory path (auto-detected if not specified)',
|
||||
)
|
||||
.option('-d, --debounce <ms>', 'Debounce delay in milliseconds', '1000')
|
||||
.option('--verbose', 'Enable verbose logging')
|
||||
.action(async (options) => {
|
||||
await this.devCommand.execute(options);
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('deploy')
|
||||
.description('Deploy application to Twenty')
|
||||
.option(
|
||||
'-p, --path <path>',
|
||||
'Application directory path (auto-detected if not specified)',
|
||||
)
|
||||
.action(async (options) => {
|
||||
await this.deployCommand.execute(options);
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('install')
|
||||
.description('Install application from source')
|
||||
.option(
|
||||
'-s, --source <source>',
|
||||
'Application source (git URL, local path, or marketplace ID)',
|
||||
)
|
||||
.option(
|
||||
'-t, --type <type>',
|
||||
'Source type (git, local, marketplace)',
|
||||
'local',
|
||||
)
|
||||
.action(async (options) => {
|
||||
await this.installCommand.execute(options);
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('list')
|
||||
.description('List installed applications')
|
||||
.action(async () => {
|
||||
await this.listCommand.execute();
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('init')
|
||||
.description('Initialize a new Twenty application')
|
||||
.option('-p, --path <path>', 'Directory to create the application in')
|
||||
.option('-n, --name <name>', 'Application name')
|
||||
.action(async (options) => {
|
||||
await this.initCommand.execute(options);
|
||||
});
|
||||
|
||||
return appCommand;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import inquirer from 'inquirer';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { ConfigService } from '../services/config.service';
|
||||
|
||||
export class AuthCommand {
|
||||
private configService = new ConfigService();
|
||||
private apiService = new ApiService();
|
||||
|
||||
getCommand(): Command {
|
||||
const authCommand = new Command('auth');
|
||||
authCommand.description('Authentication commands');
|
||||
|
||||
authCommand
|
||||
.command('login')
|
||||
.description('Authenticate with Twenty')
|
||||
.option('--api-key <key>', 'API key for authentication')
|
||||
.option('--api-url <url>', 'Twenty API URL')
|
||||
.action(async (options) => {
|
||||
await this.login(options);
|
||||
});
|
||||
|
||||
authCommand
|
||||
.command('logout')
|
||||
.description('Remove authentication credentials')
|
||||
.action(async () => {
|
||||
await this.logout();
|
||||
});
|
||||
|
||||
authCommand
|
||||
.command('status')
|
||||
.description('Check authentication status')
|
||||
.action(async () => {
|
||||
await this.status();
|
||||
});
|
||||
|
||||
return authCommand;
|
||||
}
|
||||
|
||||
private async login(options: {
|
||||
apiKey?: string;
|
||||
apiUrl?: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
let { apiKey, apiUrl } = options;
|
||||
|
||||
// Get current config
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
// Prompt for missing values
|
||||
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;
|
||||
}
|
||||
|
||||
// Update config
|
||||
await this.configService.setConfig({
|
||||
apiUrl,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
// Validate authentication
|
||||
const isValid = await this.apiService.validateAuth();
|
||||
|
||||
if (isValid) {
|
||||
console.log(chalk.green('✓ Successfully authenticated with Twenty'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.red('✗ Authentication failed. Please check your credentials.'),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Login failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async logout(): Promise<void> {
|
||||
try {
|
||||
await this.configService.clearConfig();
|
||||
console.log(chalk.green('✓ Successfully logged out'));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Logout failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async status(): Promise<void> {
|
||||
try {
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
console.log(chalk.blue('Authentication Status:'));
|
||||
console.log(`API URL: ${config.apiUrl}`);
|
||||
console.log(
|
||||
`API Key: ${config.apiKey ? '***' + config.apiKey.slice(-4) : 'Not set'}`,
|
||||
);
|
||||
|
||||
if (config.apiKey) {
|
||||
const isValid = await this.apiService.validateAuth();
|
||||
console.log(
|
||||
`Status: ${isValid ? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import { ConfigService } from '../services/config.service';
|
||||
import { TwentyConfig } from '../types/config.types';
|
||||
|
||||
export class ConfigCommand {
|
||||
private configService = new ConfigService();
|
||||
|
||||
getCommand(): Command {
|
||||
const configCommand = new Command('config');
|
||||
configCommand.description('Configuration management');
|
||||
|
||||
configCommand
|
||||
.command('get [key]')
|
||||
.description('Get configuration value(s)')
|
||||
.action(async (key) => {
|
||||
await this.get(key);
|
||||
});
|
||||
|
||||
configCommand
|
||||
.command('set <key> <value>')
|
||||
.description('Set configuration value')
|
||||
.action(async (key, value) => {
|
||||
await this.set(key, value);
|
||||
});
|
||||
|
||||
configCommand
|
||||
.command('unset <key>')
|
||||
.description('Remove configuration value')
|
||||
.action(async (key) => {
|
||||
await this.unset(key);
|
||||
});
|
||||
|
||||
configCommand
|
||||
.command('list')
|
||||
.description('List all configuration values')
|
||||
.action(async () => {
|
||||
await this.list();
|
||||
});
|
||||
|
||||
return configCommand;
|
||||
}
|
||||
|
||||
private async get(key: string | undefined): Promise<void> {
|
||||
try {
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
if (key) {
|
||||
const value = (config as any)[key];
|
||||
if (value !== undefined) {
|
||||
console.log(value);
|
||||
} else {
|
||||
console.log(chalk.gray('(not set)'));
|
||||
}
|
||||
} else {
|
||||
this.printConfig('Configuration', config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Failed to get configuration:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async set(key: keyof TwentyConfig, value: string): Promise<void> {
|
||||
try {
|
||||
const config = await this.configService.getConfig();
|
||||
config[key] = value;
|
||||
await this.configService.setConfig(config);
|
||||
console.log(chalk.green(`✓ Set ${key} in configuration`));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Failed to set configuration:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async unset(key: string): Promise<void> {
|
||||
try {
|
||||
const config = await this.configService.getConfig();
|
||||
delete (config as any)[key];
|
||||
await this.configService.setConfig(config);
|
||||
console.log(chalk.green(`✓ Removed ${key} from configuration`));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Failed to unset configuration:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async list(): Promise<void> {
|
||||
try {
|
||||
const config = await this.configService.getConfig();
|
||||
this.printConfig('Configuration', config);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Failed to list configuration:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private printConfig(title: string, config: Record<string, any>): void {
|
||||
console.log(chalk.blue(title + ':'));
|
||||
|
||||
const keys = Object.keys(config);
|
||||
if (keys.length === 0) {
|
||||
console.log(chalk.gray(' (empty)'));
|
||||
return;
|
||||
}
|
||||
|
||||
keys.forEach((key) => {
|
||||
const value = config[key];
|
||||
const displayValue =
|
||||
key.toLowerCase().includes('key') && value
|
||||
? '***' + value.slice(-4)
|
||||
: value;
|
||||
|
||||
// Show if value is overridden by environment variable
|
||||
const envVarName = `TWENTY_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
|
||||
const isOverridden = process.env[envVarName] !== undefined;
|
||||
const suffix = isOverridden ? chalk.gray(' (from env)') : '';
|
||||
|
||||
console.log(` ${key}: ${displayValue}${suffix}`);
|
||||
});
|
||||
|
||||
// Show available environment variables
|
||||
console.log(chalk.gray('\nEnvironment variables:'));
|
||||
console.log(chalk.gray(' TWENTY_API_URL - Override API URL'));
|
||||
console.log(chalk.gray(' TWENTY_API_KEY - Override API key'));
|
||||
console.log(chalk.gray(' TWENTY_DEFAULT_APP - Override default app'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user