Merge twenty-cli into twenty-sdk (#16150)
- Moves twenty-cli content into twenty-sdk - add a new twenty-sdk:0.1.0 version - this new twenty-sdk exports a cli command called 'twenty' (like twenty-cli before) - deprecates twenty-cli - simplify app init command base-project - use `twenty-sdk:0.1.0` in base project - move the "twenty-sdk/application" barrel to "twenty-sdk" - add `create-twenty-app` package <img width="1512" height="919" alt="image" src="https://github.com/user-attachments/assets/007bef45-4e71-419a-9213-cebed376adbf" /> <img width="1506" height="929" alt="image" src="https://github.com/user-attachments/assets/3de2fec6-1624-4923-ae13-f4e1cf165eb5" />
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import { join } from 'path';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { getObjectDecoratedClass } from '../utils/get-object-decorated-class';
|
||||
import { getFunctionBaseFile } from '../utils/get-function-base-file';
|
||||
import { convertToLabel } from '../utils/convert-to-label';
|
||||
|
||||
export enum SyncableEntity {
|
||||
AGENT = 'agent',
|
||||
OBJECT = 'object',
|
||||
FUNCTION = 'function',
|
||||
}
|
||||
|
||||
export const isSyncableEntity = (value: string): value is SyncableEntity => {
|
||||
return Object.values(SyncableEntity).includes(value as SyncableEntity);
|
||||
};
|
||||
|
||||
export class AppAddCommand {
|
||||
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
|
||||
try {
|
||||
const appPath = join(CURRENT_EXECUTION_DIRECTORY, path ?? '');
|
||||
|
||||
await fs.ensureDir(appPath);
|
||||
|
||||
const entity = entityType ?? (await this.getEntity());
|
||||
|
||||
if (entity === SyncableEntity.OBJECT) {
|
||||
const entityData = await this.getObjectData();
|
||||
|
||||
const name = entityData.nameSingular;
|
||||
|
||||
const objectFileName = `${camelcase(name)}.ts`;
|
||||
|
||||
const decoratedObject = getObjectDecoratedClass({
|
||||
data: entityData,
|
||||
name,
|
||||
});
|
||||
|
||||
await fs.writeFile(join(appPath, objectFileName), decoratedObject);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity === SyncableEntity.FUNCTION) {
|
||||
const entityName = await this.getEntityName(entity);
|
||||
|
||||
const objectFileName = `${camelcase(entityName)}.ts`;
|
||||
|
||||
const decoratedServerlessFunction = getFunctionBaseFile({
|
||||
name: entityName,
|
||||
});
|
||||
|
||||
await fs.writeFile(
|
||||
join(appPath, objectFileName),
|
||||
decoratedServerlessFunction,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(`Add new entity failed:`),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntity() {
|
||||
const { entity } = await inquirer.prompt<{ entity: SyncableEntity }>([
|
||||
{
|
||||
type: 'select',
|
||||
name: 'entity',
|
||||
message: `What entity do you want to create?`,
|
||||
default: '',
|
||||
choices: [SyncableEntity.FUNCTION, SyncableEntity.OBJECT],
|
||||
},
|
||||
]);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private async getEntityName(entity: SyncableEntity) {
|
||||
const { name } = await inquirer.prompt<{ name: string }>([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: `Enter a name for your new ${entity}:`,
|
||||
default: '',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) {
|
||||
return `${entity} name is required`;
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9-]+$/.test(input)) {
|
||||
return 'Name must contain only lowercase letters, numbers, and hyphens';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private async getObjectData() {
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'nameSingular',
|
||||
message: 'Enter a name singular for your object (eg: company):',
|
||||
default: '',
|
||||
validate: (input: string) => {
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'namePlural',
|
||||
message: 'Enter a name plural for your object (eg: companies):',
|
||||
default: '',
|
||||
validate: (input: string, answers?: any) => {
|
||||
if (input.trim() === answers?.nameSingular.trim()) {
|
||||
return 'Name plural must be different from name singular';
|
||||
}
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'labelSingular',
|
||||
message: 'Enter a label singular for your object:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers.nameSingular);
|
||||
},
|
||||
validate: (input: string) => {
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'labelPlural',
|
||||
message: 'Enter a label plural for your object:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers.namePlural);
|
||||
},
|
||||
validate: (input: string) => {
|
||||
if (!input || input.trim().length === 0) {
|
||||
return 'Please enter a non empty string';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user