Twenty SDK command renaming and dev mode (#17245)
## Description This PR improves the developer experience for the `twenty-sdk` and `create-twenty-app` packages by reorganizing commands, adding development tooling, and improving documentation. ## Changes ### twenty-sdk #### Command Refactoring - **Renamed `app-watch` → `app-dev`**: Renamed `AppWatchCommand` to `AppDevCommand` and moved to `app-dev.ts` for consistent naming with the CLI command `app:dev` - **Moved `app-add` → `entity-add`**: Relocated entity creation logic from `app/app-add.ts` to `entity/entity-add.ts` and renamed `AppAddCommand` to `EntityAddCommand` for better separation of concerns #### Development Tooling - **Added `dev` target**: New Nx target `npx nx run twenty-sdk:dev` that runs the build in watch mode for faster development iteration ### create-twenty-app #### Improved Scaffolded Project - **Enhanced base-application README** with: - Updated Getting Started to recommend `yarn dev` for development - Added "Available Commands" section listing all available scripts #### Better Onboarding - **Improved success message** after app creation with formatted next steps:
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 <key>`: API key for authentication.
|
||||
- `--api-url <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 <ms>`: 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 <id>`: Only show logs for a specific function universal ID.
|
||||
- `-n, --functionName <name>`: 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 -- <command>
|
||||
# Example: npx nx run twenty-sdk:start -- auth:status
|
||||
```
|
||||
|
||||
Or run the built CLI directly:
|
||||
|
||||
```bash
|
||||
node packages/twenty-sdk/dist/cli.cjs <command>
|
||||
```
|
||||
|
||||
### Resources
|
||||
- See our [GitHub](https://github.com/twentyhq/twenty)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 <ms>', 'Debounce delay in milliseconds', '1000')
|
||||
.action(async (appPath, options) => {
|
||||
await watchCommand.execute({
|
||||
await devCommand.execute({
|
||||
...options,
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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: {
|
||||
@@ -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<void> {
|
||||
throw new Error('Not implemented');
|
||||
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
|
||||
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;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user