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:
martmull
2025-12-01 11:44:35 +01:00
committed by GitHub
parent 3f08a0c901
commit e498367e2f
85 changed files with 1077 additions and 1560 deletions
@@ -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;
},
},
]);
}
}