diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index c2cc49ad8b..3533dd4243 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -28,29 +28,29 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [T npx create-twenty-app@latest my-twenty-app cd my-twenty-app +# Get Help +yarn run help + # Authenticate using your API key (you'll be prompted) -yarn auth +yarn auth:login # Add a new entity to your application (guided) -yarn create-entity +yarn app:add # Generate a typed Twenty client and workspace entity types -yarn generate +yarn app:generate # Start dev mode: automatically syncs local changes to your workspace -yarn dev +yarn app:dev # Or run a one‑time sync -yarn sync +yarn app:sync # Watch your application's functions logs -yarn logs +yarn app:logs # Uninstall the application from the current workspace -yarn uninstall - -# Display commands' help -yarn help +yarn app:uninstall ``` ## What gets scaffolded @@ -60,9 +60,9 @@ yarn help - Example placeholders to help you add entities, actions, and sync logic ## Next steps -- Explore the generated project and add your first entity with `yarn create-entity`. -- Keep your types up‑to‑date using `yarn generate`. -- Use `yarn dev` while you iterate to see changes instantly in your workspace. +- Explore the generated project and add your first entity with `yarn app:add`. +- Keep your types up‑to‑date using `yarn app:generate`. +- Use `yarn app:dev` while you iterate to see changes instantly in your workspace. ## Publish your application @@ -90,8 +90,8 @@ git push Our team reviews contributions for quality, security, and reusability before merging. ## Troubleshooting -- Auth prompts not appearing: run `yarn auth` again and verify the API key permissions. -- Types not generated: ensure `yarn generate` runs without errors, then re‑start `yarn dev`. +- Auth prompts not appearing: run `yarn auth:login` again and verify the API key permissions. +- Types not generated: ensure `yarn app:generate` runs without errors, then re‑start `yarn app:dev`. ## Contributing - See our [GitHub](https://github.com/twentyhq/twenty) diff --git a/packages/create-twenty-app/src/constants/base-application/README.md b/packages/create-twenty-app/src/constants/base-application/README.md index 6f50cba69c..08c036ac60 100644 --- a/packages/create-twenty-app/src/constants/base-application/README.md +++ b/packages/create-twenty-app/src/constants/base-application/README.md @@ -5,17 +5,40 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c First, authenticate to your workspace: ```bash -yarn auth +yarn auth:login ``` -Then, install this app to your workspace: +Then, start development mode to sync your app and watch for changes: ```bash -yarn sync +yarn app:dev +``` + +Or run a one-time sync: + +```bash +yarn app:sync ``` Open your Twenty instance and go to `/settings/applications` section to see the result. +## Available Commands + +```bash +# Authentication +yarn auth:login # Authenticate with Twenty +yarn auth:logout # Remove credentials +yarn auth:status # Check auth status + +# Application +yarn app:dev # Start dev mode (sync + watch) +yarn app:sync # One-time sync +yarn app:add # Add a new entity (function, object, role) +yarn app:generate # Generate typed Twenty client +yarn app:logs # Stream function logs +yarn app:uninstall # Uninstall app from workspace +``` + ## Learn More To learn more about Twenty applications, take a look at the following resources: diff --git a/packages/create-twenty-app/src/create-app.command.ts b/packages/create-twenty-app/src/create-app.command.ts index 5c5ff4a7b1..ce8f947581 100644 --- a/packages/create-twenty-app/src/create-app.command.ts +++ b/packages/create-twenty-app/src/create-app.command.ts @@ -1,12 +1,12 @@ +import { copyBaseApplicationProject } from '@/utils/app-template'; +import { convertToLabel } from '@/utils/convert-to-label'; +import { install } from '@/utils/install'; +import { tryGitInit } from '@/utils/try-git-init'; import chalk from 'chalk'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; -import * as path from 'path'; -import { copyBaseApplicationProject } from '@/utils/app-template'; import kebabCase from 'lodash.kebabcase'; -import { convertToLabel } from '@/utils/convert-to-label'; -import { tryGitInit } from '@/utils/try-git-init'; -import { install } from '@/utils/install'; +import * as path from 'path'; const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd(); @@ -119,10 +119,13 @@ export class CreateAppCommand { } private logSuccess(appDirectory: string): void { + const dirName = appDirectory.split('/').reverse()[0] ?? ''; + console.log(chalk.green('✅ Application created!')); console.log(''); console.log(chalk.blue('Next steps:')); - console.log(`cd ${appDirectory.split('/').reverse()[0] ?? ''}`); - console.log('yarn auth'); + console.log(chalk.gray(` cd ${dirName}`)); + console.log(chalk.gray(' yarn auth:login # Authenticate with Twenty')); + console.log(chalk.gray(' yarn app:dev # Start dev mode')); } } diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index 7eee983d67..4bd1d24fa4 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -68,8 +68,8 @@ describe('copyBaseApplicationProject', () => { expect(packageJson.name).toBe('my-test-app'); expect(packageJson.version).toBe('0.1.0'); expect(packageJson.dependencies['twenty-sdk']).toBe('0.3.1'); - expect(packageJson.scripts.sync).toBe('twenty app sync'); - expect(packageJson.scripts.dev).toBe('twenty app dev'); + expect(packageJson.scripts['app:sync']).toBe('twenty app:sync'); + expect(packageJson.scripts['app:dev']).toBe('twenty app:dev'); }); it('should create .gitignore file', async () => { diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index 9e6c1a8add..8251a8ca5a 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -156,16 +156,18 @@ const createPackageJson = async ({ }, packageManager: 'yarn@4.9.2', scripts: { - 'create-entity': 'twenty app add', - dev: 'twenty app dev', - generate: 'twenty app generate', - sync: 'twenty app sync', - logs: 'twenty app logs', - uninstall: 'twenty app uninstall', + 'auth:login': 'twenty auth:login', + 'auth:logout': 'twenty auth:logout', + 'auth:status': 'twenty auth:status', + 'app:dev': 'twenty app:dev', + 'app:sync': 'twenty app:sync', + 'app:add': 'twenty app:add', + 'app:generate': 'twenty app:generate', + 'app:logs': 'twenty app:logs', + 'app:uninstall': 'twenty app:uninstall', help: 'twenty help', - auth: 'twenty auth login', lint: 'eslint', - 'lint-fix': 'eslint --fix', + 'lint:fix': 'eslint --fix', }, dependencies: { 'twenty-sdk': '0.3.1', diff --git a/packages/twenty-sdk/README.md b/packages/twenty-sdk/README.md index fcf6f69e4d..8b87904f3a 100644 --- a/packages/twenty-sdk/README.md +++ b/packages/twenty-sdk/README.md @@ -58,51 +58,51 @@ Commands: Authenticate the CLI against your Twenty workspace. -- `twenty auth login` — Authenticate with Twenty. +- `twenty auth:login` — Authenticate with Twenty. - Options: - `--api-key `: API key for authentication. - `--api-url `: Twenty API URL (defaults to your current profile's value or `http://localhost:3000`). - Behavior: Prompts for any missing values, persists them to the active workspace profile, and validates the credentials. -- `twenty auth logout` — Remove authentication credentials for the active workspace profile. +- `twenty auth:logout` — Remove authentication credentials for the active workspace profile. -- `twenty auth status` — Print the current authentication status (API URL, masked API key, validity). +- `twenty auth:status` — Print the current authentication status (API URL, masked API key, validity). Examples: ```bash # Login interactively (recommended) -twenty auth login +twenty auth:login # Provide values in flags -twenty auth login --api-key $TWENTY_API_KEY --api-url https://api.twenty.com +twenty auth:login --api-key $TWENTY_API_KEY --api-url https://api.twenty.com # Login interactively for a specific workspace profile -twenty auth login --workspace my-custom-workspace +twenty auth:login --workspace my-custom-workspace # Check status -twenty auth status +twenty auth:status # Logout current profile -twenty auth logout +twenty auth:logout ``` ### App Application development commands. -- `twenty app sync [appPath]` — One-time sync of the application to your Twenty workspace. -- Behavior: Compute your application's manifest and send it to your workspace to sync your application +- `twenty app:sync [appPath]` — One-time sync of the application to your Twenty workspace. + - Behavior: Compute your application's manifest and send it to your workspace to sync your application -- `twenty app dev [appPath]` — Watch and sync local application changes. +- `twenty app:dev [appPath]` — Start development mode: sync local application changes. - Options: - `-d, --debounce `: Debounce delay in milliseconds (default: `1000`). - Behavior: Performs an initial sync, then watches the directory for changes and re-syncs after debounced edits. Press Ctrl+C to stop. -- `twenty app uninstall [appPath]` — Uninstall the application from the current workspace. - - Note: `twenty app delete` exists as a hidden alias for backward compatibility. +- `twenty app:uninstall [appPath]` — Uninstall the application from the current workspace. + - Note: `twenty app:delete` exists as a hidden alias for backward compatibility. -- `twenty app add [entityType]` — Add a new entity to your application. +- `twenty app:add [entityType]` — Add a new entity to your application. - Arguments: - `entityType`: one of `function` or `object`. If omitted, an interactive prompt is shown. - Options: @@ -111,9 +111,9 @@ Application development commands. - `object`: prompts for singular/plural names and labels, then creates a new object definition file. - `function`: prompts for a name and scaffolds a serverless function file. -- `twenty app generate [appPath]` — Generate the typed Twenty client for your application. +- `twenty app:generate [appPath]` — Generate the typed Twenty client for your application. -- `twenty app logs [appPath]` — Stream application function logs. +- `twenty app:logs [appPath]` — Stream application function logs. - Options: - `-u, --functionUniversalIdentifier `: Only show logs for a specific function universal ID. - `-n, --functionName `: Only show logs for a specific function name. @@ -122,28 +122,28 @@ Examples: ```bash # Start dev mode with default debounce -twenty app dev +twenty app:dev # Start dev mode with custom workspace profile -twenty app dev --workspace my-custom-workspace +twenty app:dev --workspace my-custom-workspace # Dev mode with custom debounce -twenty app dev --debounce 1500 +twenty app:dev --debounce 1500 # One-time sync of the current directory -twenty app sync +twenty app:sync # Add a new object interactively -twenty app add +twenty app:add # Generate client types -twenty app generate +twenty app:generate # Watch all function logs -twenty app logs +twenty app:logs # Watch logs for a specific function by name -twenty app logs -n my-function +twenty app:logs -n my-function ``` ## Configuration @@ -173,15 +173,60 @@ Example configuration file: Notes: - If a profile is missing, `apiUrl` defaults to `http://localhost:3000` until set. -- `twenty auth login` writes the `apiUrl` and `apiKey` for the default profile. -- `twenty auth login --workspace custom-workspace` writes the `apiUrl` and `apiKey` for a custom `custom-workspace` profile. +- `twenty auth:login` writes the `apiUrl` and `apiKey` for the default profile. +- `twenty auth:login --workspace custom-workspace` writes the `apiUrl` and `apiKey` for a custom `custom-workspace` profile. ## Troubleshooting -- Auth errors: run `twenty auth login` again and ensure the API key has the required permissions. -- Typings out of date: run `twenty app generate` to refresh the client and types. -- Not seeing changes in dev: make sure dev mode is running (`twenty app dev`). +- Auth errors: run `twenty auth:login` again and ensure the API key has the required permissions. +- Typings out of date: run `twenty app:generate` to refresh the client and types. +- Not seeing changes in dev: make sure dev mode is running (`twenty app:dev`). ## Contributing + +### Development Setup + +To contribute to the twenty-sdk package, clone the repository and install dependencies: + +```bash +git clone https://github.com/twentyhq/twenty.git +cd twenty +yarn install +``` + +### Development Mode + +Run the SDK build in watch mode to automatically rebuild on file changes: + +```bash +npx nx run twenty-sdk:dev +``` + +This will watch for changes and rebuild the `dist` folder automatically. + +### Production Build + +Build the SDK for production: + +```bash +npx nx run twenty-sdk:build +``` + +### Running the CLI Locally + +After building, you can run the CLI directly: + +```bash +npx nx run twenty-sdk:start -- +# Example: npx nx run twenty-sdk:start -- auth:status +``` + +Or run the built CLI directly: + +```bash +node packages/twenty-sdk/dist/cli.cjs +``` + +### Resources - See our [GitHub](https://github.com/twentyhq/twenty) - Join our [Discord](https://discord.gg/cx5n4Jzs57) diff --git a/packages/twenty-sdk/project.json b/packages/twenty-sdk/project.json index 1a36ebc442..2fa328444f 100644 --- a/packages/twenty-sdk/project.json +++ b/packages/twenty-sdk/project.json @@ -16,6 +16,17 @@ "{projectRoot}/dist" ] }, + "dev": { + "executor": "nx:run-commands", + "dependsOn": [ + "generateBarrels", + "^build" + ], + "options": { + "cwd": "packages/twenty-sdk", + "command": "npx rimraf dist && npx vite build --watch" + } + }, "start": { "executor": "nx:run-commands", "dependsOn": [ diff --git a/packages/twenty-sdk/src/cli/commands/app.command.ts b/packages/twenty-sdk/src/cli/commands/app.command.ts index 30a9b7398e..79b3324d77 100644 --- a/packages/twenty-sdk/src/cli/commands/app.command.ts +++ b/packages/twenty-sdk/src/cli/commands/app.command.ts @@ -1,20 +1,20 @@ import { formatPath } from '@/cli/utilities/file/utils/file-path'; import chalk from 'chalk'; import type { Command } from 'commander'; -import { - AppAddCommand, - isSyncableEntity, - SyncableEntity, -} from './app/app-add'; import { AppBuildCommand } from './app/app-build'; +import { AppDevCommand } from './app/app-dev'; 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'; +import { + EntityAddCommand, + isSyncableEntity, + SyncableEntity, +} from './entity/entity-add'; export const registerCommands = (program: Command): void => { // Auth commands @@ -46,20 +46,20 @@ export const registerCommands = (program: Command): void => { }); // App commands - const watchCommand = new AppWatchCommand(); + const devCommand = new AppDevCommand(); const syncCommand = new AppSyncCommand(); const uninstallCommand = new AppUninstallCommand(); - const addCommand = new AppAddCommand(); + const addCommand = new EntityAddCommand(); const generateCommand = new AppGenerateCommand(); const logsCommand = new AppLogsCommand(); const buildCommand = new AppBuildCommand(); program .command('app:dev [appPath]') - .description('Watch and sync local application changes') + .description('Start development mode: sync local application changes') .option('-d, --debounce ', 'Debounce delay in milliseconds', '1000') .action(async (appPath, options) => { - await watchCommand.execute({ + await devCommand.execute({ ...options, appPath: formatPath(appPath), }); diff --git a/packages/twenty-sdk/src/cli/commands/app/app-add.ts b/packages/twenty-sdk/src/cli/commands/app/app-add.ts deleted file mode 100644 index e17698a0e6..0000000000 --- a/packages/twenty-sdk/src/cli/commands/app/app-add.ts +++ /dev/null @@ -1,212 +0,0 @@ -import chalk from 'chalk'; -import * as fs from 'fs-extra'; -import inquirer from 'inquirer'; -import { join } from 'path'; -import camelcase from 'lodash.camelcase'; -import kebabcase from 'lodash.kebabcase'; -import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; -import { getNewObjectFileContent } from '@/cli/utilities/entity/utils/entity-object-template'; -import { getFunctionBaseFile } from '@/cli/utilities/entity/utils/entity-function-template'; -import { getRoleBaseFile } from '@/cli/utilities/entity/utils/entity-role-template'; -import { convertToLabel } from '@/cli/utilities/entity/utils/entity-label'; - -const APP_FOLDER = 'src/app'; - -export enum SyncableEntity { - AGENT = 'agent', - OBJECT = 'object', - FUNCTION = 'function', - ROLE = 'role', -} - -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 { - try { - // Default to src/app/ folder, allow override with path parameter - const appPath = path - ? join(CURRENT_EXECUTION_DIRECTORY, path) - : join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER); - - await fs.ensureDir(appPath); - - const entity = entityType ?? (await this.getEntity()); - - if (entity === SyncableEntity.OBJECT) { - const entityData = await this.getObjectData(); - - const name = entityData.nameSingular; - - // Use *.object.ts naming convention - const objectFileName = `${camelcase(name)}.object.ts`; - - const decoratedObject = getNewObjectFileContent({ - data: entityData, - name, - }); - - const filePath = join(appPath, objectFileName); - - await fs.writeFile(filePath, decoratedObject); - - console.log( - chalk.green(`✓ Created object:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - return; - } - - if (entity === SyncableEntity.FUNCTION) { - const entityName = await this.getEntityName(entity); - - // Use *.function.ts naming convention - const functionFileName = `${kebabcase(entityName)}.function.ts`; - - const decoratedServerlessFunction = getFunctionBaseFile({ - name: entityName, - }); - - const filePath = join(appPath, functionFileName); - - await fs.writeFile(filePath, decoratedServerlessFunction); - - console.log( - chalk.green(`✓ Created function:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - return; - } - - if (entity === SyncableEntity.ROLE) { - const entityName = await this.getEntityName(entity); - - // Use *.role.ts naming convention - const roleFileName = `${kebabcase(entityName)}.role.ts`; - - const roleFileContent = getRoleBaseFile({ - name: entityName, - }); - - const filePath = join(appPath, roleFileName); - - await fs.writeFile(filePath, roleFileContent); - - console.log( - chalk.green(`✓ Created role:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - 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, SyncableEntity.ROLE], - }, - ]); - - 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; - }, - }, - ]); - } -} diff --git a/packages/twenty-sdk/src/cli/commands/app/app-watch.ts b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts similarity index 98% rename from packages/twenty-sdk/src/cli/commands/app/app-watch.ts rename to packages/twenty-sdk/src/cli/commands/app/app-dev.ts index 3d67c3e138..2dbb42db04 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-watch.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts @@ -10,7 +10,7 @@ import { loadManifest } from '@/cli/utilities/manifest/utils/manifest-load'; import chalk from 'chalk'; import * as chokidar from 'chokidar'; -export class AppWatchCommand { +export class AppDevCommand { private apiService = new ApiService(); async execute(options: { diff --git a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts index 851c934096..b12e117259 100644 --- a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts +++ b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts @@ -1,8 +1,212 @@ -// Placeholder for entity add command -// TODO: Implement entity add functionality +import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory'; +import { getFunctionBaseFile } from '@/cli/utilities/entity/utils/entity-function-template'; +import { convertToLabel } from '@/cli/utilities/entity/utils/entity-label'; +import { getNewObjectFileContent } from '@/cli/utilities/entity/utils/entity-object-template'; +import { getRoleBaseFile } from '@/cli/utilities/entity/utils/entity-role-template'; +import chalk from 'chalk'; +import * as fs from 'fs-extra'; +import inquirer from 'inquirer'; +import camelcase from 'lodash.camelcase'; +import kebabcase from 'lodash.kebabcase'; +import { join } from 'path'; + +const APP_FOLDER = 'src/app'; + +export enum SyncableEntity { + AGENT = 'agent', + OBJECT = 'object', + FUNCTION = 'function', + ROLE = 'role', +} + +export const isSyncableEntity = (value: string): value is SyncableEntity => { + return Object.values(SyncableEntity).includes(value as SyncableEntity); +}; export class EntityAddCommand { - async execute(): Promise { - throw new Error('Not implemented'); + async execute(entityType?: SyncableEntity, path?: string): Promise { + try { + // Default to src/app/ folder, allow override with path parameter + const appPath = path + ? join(CURRENT_EXECUTION_DIRECTORY, path) + : join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER); + + await fs.ensureDir(appPath); + + const entity = entityType ?? (await this.getEntity()); + + if (entity === SyncableEntity.OBJECT) { + const entityData = await this.getObjectData(); + + const name = entityData.nameSingular; + + // Use *.object.ts naming convention + const objectFileName = `${camelcase(name)}.object.ts`; + + const decoratedObject = getNewObjectFileContent({ + data: entityData, + name, + }); + + const filePath = join(appPath, objectFileName); + + await fs.writeFile(filePath, decoratedObject); + + console.log( + chalk.green(`✓ Created object:`), + chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), + ); + + return; + } + + if (entity === SyncableEntity.FUNCTION) { + const entityName = await this.getEntityName(entity); + + // Use *.function.ts naming convention + const functionFileName = `${kebabcase(entityName)}.function.ts`; + + const decoratedServerlessFunction = getFunctionBaseFile({ + name: entityName, + }); + + const filePath = join(appPath, functionFileName); + + await fs.writeFile(filePath, decoratedServerlessFunction); + + console.log( + chalk.green(`✓ Created function:`), + chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), + ); + + return; + } + + if (entity === SyncableEntity.ROLE) { + const entityName = await this.getEntityName(entity); + + // Use *.role.ts naming convention + const roleFileName = `${kebabcase(entityName)}.role.ts`; + + const roleFileContent = getRoleBaseFile({ + name: entityName, + }); + + const filePath = join(appPath, roleFileName); + + await fs.writeFile(filePath, roleFileContent); + + console.log( + chalk.green(`✓ Created role:`), + chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), + ); + + 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, SyncableEntity.ROLE], + }, + ]); + + 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; + }, + }, + ]); } }