Rework folder structure (#17229)

<img width="700" height="771" alt="image"
src="https://github.com/user-attachments/assets/509b95b5-9c4f-474d-a47f-f950dee189ea"
/>
This commit is contained in:
Charles Bochet
2026-01-19 10:36:40 +01:00
committed by GitHub
parent 72e89d5c6b
commit 7c713577f1
27 changed files with 394 additions and 320 deletions
@@ -1,7 +1,7 @@
import { existsSync } from 'fs';
import { AppSyncCommand } from '@/cli/commands/app-sync.command';
import { AppUninstallCommand } from '@/cli/commands/app-uninstall.command';
import { getTestedApplicationPath } from '@/cli/__tests__/e2e/utils/get-tested-application-path.util';
import { AppSyncCommand } from '@/cli/commands/app/app-sync';
import { AppUninstallCommand } from '@/cli/commands/app/app-uninstall';
import { existsSync } from 'fs';
import { inspect } from 'util';
inspect.defaultOptions.depth = 10;
+4 -6
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env node
import { inspect } from 'util';
import { registerCommands } from '@/cli/commands/app.command';
import { ConfigService } from '@/cli/services/config.service';
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { AppCommand } from '@/cli/commands/app.command';
import { AuthCommand } from '@/cli/commands/auth.command';
import { ConfigService } from '@/cli/services/config.service';
import { inspect } from 'util';
import packageJson from '../../package.json';
inspect.defaultOptions.depth = 10;
@@ -33,8 +32,7 @@ program.hook('preAction', (thisCommand) => {
);
});
program.addCommand(new AuthCommand().getCommand());
program.addCommand(new AppCommand().getCommand());
registerCommands(program);
program.exitOverride();
@@ -1,164 +1,186 @@
import { formatPath } from '@/cli/utils/format-path';
import chalk from 'chalk';
import { Command } from 'commander';
import type { Command } from 'commander';
import {
AppAddCommand,
isSyncableEntity,
SyncableEntity,
} from './app-add.command';
import { AppUninstallCommand } from '@/cli/commands/app-uninstall.command';
import { AppDevCommand } from '@/cli/commands/app-dev.command';
import { AppSyncCommand } from '@/cli/commands/app-sync.command';
import { formatPath } from '@/cli/utils/format-path';
import { AppGenerateCommand } from '@/cli/commands/app-generate.command';
import { AppLogsCommand } from '@/cli/commands/app-logs.command';
import { AppBuildCommand } from '@/cli/commands/app-build.command';
} from './app/app-add';
import { AppBuildCommand } from './app/app-build';
import { AppGenerateCommand } from './app/app-generate';
import { AppLogsCommand } from './app/app-logs';
import { AppSyncCommand } from './app/app-sync';
import { AppUninstallCommand } from './app/app-uninstall';
import { AppWatchCommand } from './app/app-watch';
import { AuthLoginCommand } from './auth/auth-login';
import { AuthLogoutCommand } from './auth/auth-logout';
import { AuthStatusCommand } from './auth/auth-status';
export class AppCommand {
private devCommand = new AppDevCommand();
private syncCommand = new AppSyncCommand();
private uninstallCommand = new AppUninstallCommand();
private addCommand = new AppAddCommand();
private generateCommand = new AppGenerateCommand();
private logsCommand = new AppLogsCommand();
private buildCommand = new AppBuildCommand();
export const registerCommands = (program: Command): void => {
// Auth commands
const loginCommand = new AuthLoginCommand();
const logoutCommand = new AuthLogoutCommand();
const statusCommand = new AuthStatusCommand();
getCommand(): Command {
const appCommand = new Command('app');
appCommand.description('Application development commands');
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);
});
appCommand
.command('dev [appPath]')
.description('Watch and sync local application changes')
.option('-d, --debounce <ms>', 'Debounce delay in milliseconds', '1000')
.action(async (appPath, options) => {
await this.devCommand.execute({
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();
});
// App commands
const watchCommand = new AppWatchCommand();
const syncCommand = new AppSyncCommand();
const uninstallCommand = new AppUninstallCommand();
const addCommand = new AppAddCommand();
const generateCommand = new AppGenerateCommand();
const logsCommand = new AppLogsCommand();
const buildCommand = new AppBuildCommand();
program
.command('app:dev [appPath]')
.description('Watch and sync local application changes')
.option('-d, --debounce <ms>', 'Debounce delay in milliseconds', '1000')
.action(async (appPath, options) => {
await watchCommand.execute({
...options,
appPath: formatPath(appPath),
});
});
program
.command('app:build [appPath]')
.description('Build application for deployment')
.option('-w, --watch', 'Watch for changes and rebuild')
.option('-t, --tarball', 'Create a tarball after build')
.action(async (appPath, options) => {
try {
const result = await buildCommand.execute({
...options,
appPath: formatPath(appPath),
});
});
appCommand
.command('build [appPath]')
.description('Build application for deployment')
.option('-w, --watch', 'Watch for changes and rebuild')
.option('-t, --tarball', 'Create a tarball after build')
.action(async (appPath, options) => {
try {
const result = await this.buildCommand.execute({
...options,
appPath: formatPath(appPath),
});
if (!result.success) {
process.exit(1);
}
} catch {
if (!result.success) {
process.exit(1);
}
});
} catch {
process.exit(1);
}
});
appCommand
.command('sync [appPath]')
.description('Sync application to Twenty')
.action(async (appPath?: string) => {
try {
const result = await this.syncCommand.execute(formatPath(appPath));
if (!result.success) {
process.exit(1);
}
} catch {
program
.command('app:sync [appPath]')
.description('Sync application to Twenty')
.action(async (appPath?: string) => {
try {
const result = await syncCommand.execute(formatPath(appPath));
if (!result.success) {
process.exit(1);
}
});
} catch {
process.exit(1);
}
});
appCommand
.command('uninstall [appPath]')
.description('Uninstall application from Twenty')
.action(async (appPath?: string) => {
try {
const result = await this.uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: true,
});
if (!result.success) {
process.exit(1);
}
} catch {
program
.command('app:uninstall [appPath]')
.description('Uninstall application from Twenty')
.action(async (appPath?: string) => {
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: true,
});
if (!result.success) {
process.exit(1);
}
});
} catch {
process.exit(1);
}
});
// Keeping to avoid breaking changes
appCommand
.command('delete [appPath]', { hidden: true })
.description('Delete application from Twenty')
.action(async (appPath?: string) => {
try {
const result = await this.uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: true,
});
if (!result.success) {
process.exit(1);
}
} catch {
// Keeping to avoid breaking changes
program
.command('app:delete [appPath]', { hidden: true })
.description('Delete application from Twenty')
.action(async (appPath?: string) => {
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: true,
});
if (!result.success) {
process.exit(1);
}
});
} catch {
process.exit(1);
}
});
appCommand
.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('|')})`,
)
.action(async (entityType?: string, options?: { path?: string }) => {
if (entityType && !isSyncableEntity(entityType)) {
console.error(
chalk.red(
`Invalid entity type "${entityType}". Must be one of: ${Object.values(SyncableEntity).join('|')}`,
),
);
process.exit(1);
}
await this.addCommand.execute(
entityType as SyncableEntity,
options?.path,
program
.command('app: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('|')})`,
)
.action(async (entityType?: string, options?: { path?: string }) => {
if (entityType && !isSyncableEntity(entityType)) {
console.error(
chalk.red(
`Invalid entity type "${entityType}". Must be one of: ${Object.values(SyncableEntity).join('|')}`,
),
);
});
process.exit(1);
}
await addCommand.execute(entityType as SyncableEntity, options?.path);
});
appCommand
.command('generate [appPath]')
.description('Generate Twenty client')
.action(async (appPath?: string) => {
await this.generateCommand.execute(formatPath(appPath));
});
program
.command('app:generate [appPath]')
.description('Generate Twenty client')
.action(async (appPath?: string) => {
await generateCommand.execute(formatPath(appPath));
});
appCommand
.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 this.logsCommand.execute({
...options,
appPath: formatPath(appPath),
});
program
.command('app: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;
},
);
return appCommand;
}
}
) => {
await logsCommand.execute({
...options,
appPath: formatPath(appPath),
});
},
);
};
@@ -9,17 +9,6 @@ export type BuildCommandOptions = {
tarball?: boolean;
};
/**
* AppBuildCommand handles the `twenty app build` CLI command.
*
* This command transpiles TypeScript applications into distributable
* JavaScript bundles using Vite.
*
* Usage:
* - `npx twenty app build [appPath]` - One-time build
* - `npx twenty app build --watch [appPath]` - Watch mode with incremental rebuilds
* - `npx twenty app build --tarball [appPath]` - Build + create .tar.gz
*/
export class AppBuildCommand {
private buildService = new BuildService();
@@ -0,0 +1,8 @@
// Placeholder for app publish command
// TODO: Implement application publishing functionality
export class AppPublishCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1,8 @@
// Placeholder for app test command
// TODO: Implement application testing functionality
export class AppTestCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -8,7 +8,7 @@ import { loadManifest } from '@/cli/utils/load-manifest';
import { displayWarnings } from '@/cli/utils/display-warnings';
import { displayErrors } from '@/cli/utils/display-errors';
export class AppDevCommand {
export class AppWatchCommand {
private apiService = new ApiService();
async execute(options: {
@@ -0,0 +1,9 @@
export { AppWatchCommand } from './app-watch';
export { AppBuildCommand, type BuildCommandOptions } from './app-build';
export { AppSyncCommand } from './app-sync';
export { AppUninstallCommand } from './app-uninstall';
export { AppLogsCommand } from './app-logs';
export { AppTestCommand } from './app-test';
export { AppPublishCommand } from './app-publish';
export { AppAddCommand, SyncableEntity, isSyncableEntity } from './app-add';
export { AppGenerateCommand } from './app-generate';
@@ -1,162 +0,0 @@
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer';
import { ApiService } from '@/cli/services/api.service';
import { ConfigService } from '@/cli/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) {
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);
}
} 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();
const activeWorkspace = ConfigService.getActiveWorkspace();
console.log(
chalk.green(
`✓ Successfully logged out (workspace: ${activeWorkspace})`,
),
);
} catch (error) {
console.error(
chalk.red('Logout failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
private async status(): 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 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,84 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { ApiService } from '@/cli/services/api.service';
import { ConfigService } from '@/cli/services/config.service';
export class AuthLoginCommand {
private configService = new ConfigService();
private apiService = new ApiService();
async execute(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) {
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);
}
} catch (error) {
console.error(
chalk.red('Login failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
}
@@ -0,0 +1,24 @@
import chalk from 'chalk';
import { ConfigService } from '@/cli/services/config.service';
export class AuthLogoutCommand {
private configService = new ConfigService();
async execute(): Promise<void> {
try {
await this.configService.clearConfig();
const activeWorkspace = ConfigService.getActiveWorkspace();
console.log(
chalk.green(
`✓ Successfully logged out (workspace: ${activeWorkspace})`,
),
);
} catch (error) {
console.error(
chalk.red('Logout failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
}
@@ -0,0 +1,37 @@
import chalk from 'chalk';
import { ApiService } from '@/cli/services/api.service';
import { ConfigService } from '@/cli/services/config.service';
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 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,8 @@
// Placeholder for auth switch command
// TODO: Implement workspace switching functionality
export class AuthSwitchCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1,4 @@
export { AuthLoginCommand } from './auth-login';
export { AuthLogoutCommand } from './auth-logout';
export { AuthStatusCommand } from './auth-status';
export { AuthSwitchCommand } from './auth-switch';
@@ -0,0 +1,8 @@
// Placeholder for entity add command
// TODO: Implement entity add functionality
export class EntityAddCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1 @@
export { EntityAddCommand } from './entity-add';
@@ -0,0 +1,3 @@
export { InstanceSetupCommand } from './instance-setup';
export { InstanceUpgradeCommand } from './instance-upgrade';
export { InstanceRunCommand } from './instance-run';
@@ -0,0 +1,8 @@
// Placeholder for instance run command
// TODO: Implement instance run functionality
export class InstanceRunCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1,8 @@
// Placeholder for instance setup command
// TODO: Implement instance setup functionality
export class InstanceSetupCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1,8 @@
// Placeholder for instance upgrade command
// TODO: Implement instance upgrade functionality
export class InstanceUpgradeCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}
@@ -0,0 +1 @@
export { SdkGenerateCommand } from './sdk-generate';
@@ -0,0 +1,8 @@
// Placeholder for sdk generate command
// TODO: Implement SDK generation functionality
export class SdkGenerateCommand {
async execute(): Promise<void> {
throw new Error('Not implemented');
}
}