Improve readme (#14858)
This commit is contained in:
+61
-102
@@ -1,127 +1,86 @@
|
||||
# Twenty CLI (WIP WIP WIP - DO NOT USE)
|
||||
# Why Twenty CLI?
|
||||
|
||||
A command-line interface for Twenty application development. Build, deploy, and manage Twenty applications with ease.
|
||||
A command-line interface to easily scaffold, develop, and publish applications that extend Twenty CRM
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g twenty-cli
|
||||
|
||||
# Or use npx
|
||||
npx twenty-cli --help
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## Requirements
|
||||
- yarn >= 4.9.2
|
||||
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
|
||||
|
||||
## Quick example project
|
||||
|
||||
```bash
|
||||
# Authenticate with Twenty
|
||||
# Authenticate using your apiKey (CLI will prompt for your <apiKey>)
|
||||
twenty auth login
|
||||
|
||||
# Create a new application
|
||||
twenty app init my-app
|
||||
cd my-app
|
||||
# Init a new application called hello-world
|
||||
twenty app init hello-world
|
||||
|
||||
# Start development mode (watches for changes and syncs automatically)
|
||||
# Go to your app
|
||||
cd hello-world
|
||||
|
||||
# Add a serverless function to your application
|
||||
twenty app add serverlessFunction
|
||||
|
||||
# Add a trigger to your serverless function
|
||||
twenty app add trigger
|
||||
|
||||
# Add axios to your application
|
||||
yarn add axios
|
||||
|
||||
# Start dev mode: automatically syncs changes to your Twenty workspace, so you can test new functions/objects instantly.
|
||||
twenty app dev
|
||||
|
||||
# Or use one time sync
|
||||
twenty app sync
|
||||
|
||||
# List all available commands
|
||||
twenty help
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
# Login to Twenty
|
||||
twenty auth login
|
||||
|
||||
# Check authentication status
|
||||
twenty auth status
|
||||
|
||||
# Logout
|
||||
twenty auth logout
|
||||
```
|
||||
|
||||
### Application Development
|
||||
|
||||
```bash
|
||||
# Initialize a new application
|
||||
twenty app init [name]
|
||||
|
||||
# Add a new core entity to your application
|
||||
twenty app add [options]
|
||||
-p, --path <path> Application directory path (default: current directory)
|
||||
|
||||
# Start development mode with file watching
|
||||
twenty app dev [options]
|
||||
-p, --path <path> Application directory path (default: current directory)
|
||||
-w, --workspace-id <id> Workspace ID
|
||||
-d, --debounce <ms> Debounce delay in milliseconds (default: 1000)
|
||||
|
||||
# Deploy application
|
||||
twenty app deploy [options]
|
||||
-p, --path <path> Application directory path (default: current directory)
|
||||
-w, --workspace-id <id> Workspace ID
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Get configuration value
|
||||
twenty config get [key]
|
||||
--global Show global configuration
|
||||
--project Show project configuration
|
||||
|
||||
# Set configuration value
|
||||
twenty config set <key> <value>
|
||||
--global Set in global configuration
|
||||
--project Set in project configuration
|
||||
|
||||
# Remove configuration value
|
||||
twenty config unset <key>
|
||||
--global Remove from global configuration
|
||||
--project Remove from project configuration
|
||||
|
||||
# List all configuration
|
||||
twenty config list
|
||||
--global Show only global configuration
|
||||
--project Show only project configuration
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI supports both global and project-level configuration:
|
||||
|
||||
- **Global config**: `~/.twenty/config.json`
|
||||
- **Project config**: `.twenty.json` in your project directory
|
||||
|
||||
Project configuration takes precedence over global configuration.
|
||||
|
||||
### Configuration Keys
|
||||
|
||||
- `apiUrl`: Twenty API URL (default: http://localhost:3000)
|
||||
- `apiKey`: Your Twenty API key
|
||||
- `workspaceId`: Default workspace ID for operations
|
||||
|
||||
## Application Structure
|
||||
|
||||
Each application in this package follows the standard Twenty application structure:
|
||||
Each application in this package follows the standard application structure:
|
||||
|
||||
```
|
||||
app-name/
|
||||
├── twenty-app.json # Application manifest
|
||||
├── README.md # Application documentation
|
||||
├── DEVELOPMENT.md # Development guide (optional)
|
||||
├── functions/ # Serverless functions (optional)
|
||||
│ ├── function1.ts
|
||||
│ └── function2.ts
|
||||
└── assets/ # Static assets (optional)
|
||||
├── icons/
|
||||
└── screenshots/
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── serverlessFunctions # Custom backend logic (runs on demand)
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
## Publish your application
|
||||
|
||||
1. **Initialize**: Create a new application with `twenty app init`
|
||||
2. **Develop**: Use `twenty app dev` to watch for changes and auto-sync
|
||||
Applications are currently stored in twenty/packages/twenty-apps.
|
||||
|
||||
The development mode watches your application directory and automatically syncs changes to your Twenty workspace, providing a smooth development experience similar to Vercel or Heroku CLI.
|
||||
You can share your application with all twenty users.
|
||||
|
||||
```bash
|
||||
# pull twenty project
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twenty
|
||||
|
||||
# create a new branch
|
||||
git checkout -b feature/my-awesome-app
|
||||
```
|
||||
|
||||
- copy your app folder into twenty/packages/twenty-apps
|
||||
- commit your changes and open a pull request on https://github.com/twentyhq/twenty
|
||||
|
||||
```bash
|
||||
git commit -m "Add new application"
|
||||
git push
|
||||
```
|
||||
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Contributing
|
||||
|
||||
- see our [Hacktoberfest 2025 notion page](https://twentycrm.notion.site/Hacktoberfest-27711d8417038037a149d4638a9cc510)
|
||||
- our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"examples": [
|
||||
{
|
||||
"standardId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"label": "Customer Support App",
|
||||
"name": "Customer Support App",
|
||||
"description": "Comprehensive customer support application with AI agents",
|
||||
"icon": "🎧",
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -3,11 +3,11 @@ import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import path from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { resolveAppPath } from '../utils/app-path-resolver';
|
||||
import { parseJsoncFile, writeJsoncFile } from '../utils/jsonc-parser';
|
||||
import { getSchemaUrls } from '../utils/schema-validator';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
|
||||
enum SyncableEntity {
|
||||
export enum SyncableEntity {
|
||||
AGENT = 'agent',
|
||||
OBJECT = 'object',
|
||||
SERVERLESS_FUNCTION = 'serverlessFunction',
|
||||
@@ -27,12 +27,16 @@ const getFolderName = (entity: SyncableEntity) => {
|
||||
}
|
||||
};
|
||||
|
||||
export class AppAddCommand {
|
||||
async execute(options: { path?: string }): Promise<void> {
|
||||
try {
|
||||
const appPath = await resolveAppPath(options.path);
|
||||
export const isSyncableEntity = (value: string): value is SyncableEntity => {
|
||||
return Object.values(SyncableEntity).includes(value as SyncableEntity);
|
||||
};
|
||||
|
||||
const entity = await this.getEntity();
|
||||
export class AppAddCommand {
|
||||
async execute(entityType?: SyncableEntity): Promise<void> {
|
||||
try {
|
||||
const appPath = CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
const entity = entityType ?? (await this.getEntity());
|
||||
|
||||
const appExists = await fs.pathExists(appPath);
|
||||
|
||||
@@ -99,12 +103,7 @@ export class AppAddCommand {
|
||||
name: 'entity',
|
||||
message: `What entity do you want to create?`,
|
||||
default: '',
|
||||
choices: [
|
||||
SyncableEntity.AGENT,
|
||||
SyncableEntity.OBJECT,
|
||||
SyncableEntity.SERVERLESS_FUNCTION,
|
||||
SyncableEntity.TRIGGER,
|
||||
],
|
||||
choices: [SyncableEntity.SERVERLESS_FUNCTION, SyncableEntity.TRIGGER],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -261,7 +260,8 @@ export class AppAddCommand {
|
||||
{
|
||||
type: 'input',
|
||||
name: 'eventName',
|
||||
message: 'Enter the database event name (e.g., company.created):',
|
||||
message:
|
||||
'Enter the database event name (e.g. company.created, *.updated, person.*):',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) {
|
||||
return 'Event name is required';
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import chalk from 'chalk';
|
||||
import * as chokidar from 'chokidar';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { resolveAppPath } from '../utils/app-path-resolver';
|
||||
import { syncApp } from '../utils/app-sync';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
|
||||
export class AppDevCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: { path?: string; debounce: string }): Promise<void> {
|
||||
async execute(options: { debounce: string }): Promise<void> {
|
||||
try {
|
||||
const appPath = await resolveAppPath(options.path);
|
||||
const appPath = CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
const debounceMs = parseInt(options.debounce, 10);
|
||||
|
||||
this.logStartupInfo(appPath, debounceMs);
|
||||
|
||||
@@ -6,10 +6,10 @@ import { copyBaseApplicationProject } from '../utils/app-template';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
|
||||
export class AppInitCommand {
|
||||
async execute(options: { path?: string }): Promise<void> {
|
||||
async execute(directory?: string): Promise<void> {
|
||||
try {
|
||||
const { appName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(options);
|
||||
await this.getAppInfos(directory);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AppInitCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppInfos(options: { path?: string }): Promise<{
|
||||
private async getAppInfos(directory?: string): Promise<{
|
||||
appName: string;
|
||||
appDirectory: string;
|
||||
appDescription: string;
|
||||
@@ -60,9 +60,9 @@ export class AppInitCommand {
|
||||
|
||||
const appDescription = description.trim();
|
||||
|
||||
const appDirectory = options.path
|
||||
? path.resolve(options.path, kebabCase(appName))
|
||||
: path.join(process.cwd(), kebabCase(appName)!);
|
||||
const appDirectory = directory
|
||||
? path.join(process.cwd(), kebabCase(directory))
|
||||
: path.join(process.cwd(), kebabCase(appName));
|
||||
|
||||
return { appName, appDirectory, appDescription };
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { resolveAppPath } from '../utils/app-path-resolver';
|
||||
import { syncApp } from '../utils/app-sync';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
|
||||
export class AppSyncCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: { path?: string }): Promise<void> {
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
const appPath = await resolveAppPath(options.path);
|
||||
const appPath = CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('🚀 Syncing Twenty Application'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Command } from 'commander';
|
||||
import { AppSyncCommand } from './app-sync.command';
|
||||
import { AppDevCommand } from './app-dev.command';
|
||||
import { AppInitCommand } from './app-init.command';
|
||||
import { AppAddCommand } from './app-add.command';
|
||||
import {
|
||||
AppAddCommand,
|
||||
isSyncableEntity,
|
||||
SyncableEntity,
|
||||
} from './app-add.command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export class AppCommand {
|
||||
private devCommand = new AppDevCommand();
|
||||
@@ -17,10 +22,6 @@ export class AppCommand {
|
||||
appCommand
|
||||
.command('dev')
|
||||
.description('Watch and sync local application changes')
|
||||
.option(
|
||||
'-p, --path <path>',
|
||||
'Application directory path (auto-detected if not specified)',
|
||||
)
|
||||
.option('-d, --debounce <ms>', 'Debounce delay in milliseconds', '1000')
|
||||
.action(async (options) => {
|
||||
await this.devCommand.execute(options);
|
||||
@@ -29,31 +30,40 @@ export class AppCommand {
|
||||
appCommand
|
||||
.command('sync')
|
||||
.description('Sync application to Twenty')
|
||||
.option(
|
||||
'-p, --path <path>',
|
||||
'Application directory path (auto-detected if not specified)',
|
||||
)
|
||||
.action(async (options) => {
|
||||
await this.syncCommand.execute(options);
|
||||
.action(async () => {
|
||||
await this.syncCommand.execute();
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('init')
|
||||
.command('init [directory]')
|
||||
.description('Initialize a new Twenty application')
|
||||
.option('-p, --path <path>', 'Directory to create the application in')
|
||||
.action(async (options) => {
|
||||
await this.initCommand.execute(options);
|
||||
.action(async (directory?: string) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await this.initCommand.execute(directory);
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('add')
|
||||
.description('Add a new entity to your application')
|
||||
.option(
|
||||
'-p, --path <path>',
|
||||
'Application directory path (auto-detected if not specified)',
|
||||
.command('add [entityType]')
|
||||
.description(
|
||||
`Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`,
|
||||
)
|
||||
.action(async (options) => {
|
||||
await this.addCommand.execute(options);
|
||||
.action(async (entityType?: 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);
|
||||
});
|
||||
|
||||
return appCommand;
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
{description}
|
||||
|
||||
## Development
|
||||
## Requirements
|
||||
- twenty-cli `npm install -g twenty-cli`
|
||||
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
|
||||
|
||||
|
||||
## Install to your Twenty workspace
|
||||
|
||||
```bash
|
||||
twenty app dev --path {appDir}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
twenty app sync --path {appDir}
|
||||
twenty auth login
|
||||
twenty app sync
|
||||
```
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const CURRENT_EXECUTION_DIRECTORY =
|
||||
process.env.INIT_CWD || process.cwd();
|
||||
@@ -131,7 +131,7 @@ export class ApiService {
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data.syncApplication,
|
||||
message: `Successfully synced application: ${manifest.label}`,
|
||||
message: `Successfully synced application: ${manifest.name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface TwentyConfig {
|
||||
export type PackageJson = {
|
||||
$schema?: string;
|
||||
universalIdentifier: string;
|
||||
label: string;
|
||||
name: string;
|
||||
license: string;
|
||||
description?: string;
|
||||
engines: {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { APP_MANIFEST_SCHEMA_URL } from '../constants/schemas';
|
||||
|
||||
export const findProjectRoot = async (): Promise<string | null> => {
|
||||
let currentDir = process.cwd();
|
||||
const maxDepth = 10;
|
||||
let depth = 0;
|
||||
|
||||
while (depth < maxDepth) {
|
||||
const nxConfig = path.join(currentDir, 'nx.json');
|
||||
const packageJson = path.join(currentDir, 'package.json');
|
||||
|
||||
if (await fs.pathExists(nxConfig)) {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
if (await fs.pathExists(packageJson)) {
|
||||
try {
|
||||
const pkg = await fs.readJson(packageJson);
|
||||
if (pkg.workspaces || pkg.name === 'twenty') {
|
||||
return currentDir;
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) break;
|
||||
|
||||
currentDir = parentDir;
|
||||
depth++;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const findNearbyApps = async (startDir: string): Promise<string[]> => {
|
||||
const apps: string[] = [];
|
||||
|
||||
try {
|
||||
const searchPaths = [
|
||||
startDir,
|
||||
path.join(startDir, '..'),
|
||||
path.join(startDir, '../..'),
|
||||
path.join(startDir, 'packages/twenty-apps'),
|
||||
path.join(startDir, '../../packages/twenty-apps'),
|
||||
];
|
||||
|
||||
for (const searchPath of searchPaths) {
|
||||
if (await fs.pathExists(searchPath)) {
|
||||
const items = await fs.readdir(searchPath, { withFileTypes: true });
|
||||
|
||||
for (const item of items) {
|
||||
if (item.isDirectory()) {
|
||||
const itemPath = path.join(searchPath, item.name);
|
||||
if (await isValidAppPath(itemPath)) {
|
||||
apps.push(itemPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during search
|
||||
}
|
||||
|
||||
return apps.slice(0, 5);
|
||||
};
|
||||
|
||||
export const isValidAppPath = async (appPath: string): Promise<boolean> => {
|
||||
const packageJsonPath = path.join(appPath, 'package.json');
|
||||
|
||||
if (!(await fs.pathExists(packageJsonPath))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
// Check if this is a Twenty app by looking for the exact $schema URL
|
||||
return packageJson.$schema === APP_MANIFEST_SCHEMA_URL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
findNearbyApps,
|
||||
findProjectRoot,
|
||||
isValidAppPath,
|
||||
} from './app-discovery';
|
||||
|
||||
export const resolveAppPath = async (
|
||||
providedPath?: string,
|
||||
): Promise<string> => {
|
||||
if (providedPath && path.isAbsolute(providedPath)) {
|
||||
return validateAppPath(providedPath);
|
||||
}
|
||||
|
||||
if (providedPath) {
|
||||
return resolveRelativePath(providedPath);
|
||||
}
|
||||
|
||||
return autoDetectAppPath();
|
||||
};
|
||||
|
||||
const resolveRelativePath = async (providedPath: string): Promise<string> => {
|
||||
const fromCwd = path.resolve(process.cwd(), providedPath);
|
||||
if (await isValidAppPath(fromCwd)) {
|
||||
return fromCwd;
|
||||
}
|
||||
|
||||
const projectRoot = await findProjectRoot();
|
||||
if (projectRoot) {
|
||||
const fromProjectRoot = path.resolve(projectRoot, providedPath);
|
||||
if (await isValidAppPath(fromProjectRoot)) {
|
||||
return fromProjectRoot;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Cannot find Twenty app package.json at any of these locations:
|
||||
- ${fromCwd}
|
||||
- ${projectRoot ? path.resolve(projectRoot, providedPath) : 'N/A (no project root found)'}
|
||||
|
||||
Please check the path or run from the correct directory.`);
|
||||
};
|
||||
|
||||
const autoDetectAppPath = async (): Promise<string> => {
|
||||
let currentDir = process.cwd();
|
||||
const maxDepth = 10;
|
||||
let depth = 0;
|
||||
|
||||
while (depth < maxDepth) {
|
||||
if (await isValidAppPath(currentDir)) {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) break;
|
||||
|
||||
currentDir = parentDir;
|
||||
depth++;
|
||||
}
|
||||
|
||||
const suggestions = await findNearbyApps(process.cwd());
|
||||
let errorMessage =
|
||||
'No Twenty app found in current directory or parent directories.';
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
errorMessage += '\n\nFound Twenty applications nearby:';
|
||||
suggestions.forEach((suggestion: string, i: number) => {
|
||||
errorMessage += `\n ${i + 1}. ${suggestion}`;
|
||||
});
|
||||
errorMessage +=
|
||||
'\n\nTry running from one of these directories or use --path option.';
|
||||
} else {
|
||||
errorMessage += '\n\nRun `twenty app init` to create a new application.';
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
};
|
||||
|
||||
const validateAppPath = async (appPath: string): Promise<string> => {
|
||||
if (!(await fs.pathExists(appPath))) {
|
||||
throw new Error(`Directory does not exist: ${appPath}`);
|
||||
}
|
||||
|
||||
if (!(await isValidAppPath(appPath))) {
|
||||
let errorMessage = `Not a valid Twenty app: ${appPath}`;
|
||||
|
||||
const packageJsonPath = path.join(appPath, 'package.json');
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
errorMessage +=
|
||||
'\n\npackage.json found but missing required $schema field for Twenty apps.';
|
||||
} else {
|
||||
errorMessage += '\n\npackage.json not found.';
|
||||
}
|
||||
|
||||
errorMessage += '\n\nRun `twenty app init` to create a new application.';
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return appPath;
|
||||
};
|
||||
@@ -69,8 +69,6 @@ const createReadmeContent = async ({
|
||||
|
||||
readmeContent = readmeContent.replace(/\{description}/g, appDescription);
|
||||
|
||||
readmeContent = readmeContent.replace(/\{appDir}/g, appDirectory);
|
||||
|
||||
await fs.writeFile(path.join(appDirectory, 'README.md'), readmeContent);
|
||||
};
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameApplicationColumn1759433496458
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'RenameApplicationColumn1759433496458';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" RENAME COLUMN "label" TO "name"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" RENAME COLUMN "name" TO "label"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -105,7 +105,7 @@ export class ApplicationSyncService {
|
||||
|
||||
return await this.applicationService.create({
|
||||
universalIdentifier: manifest.universalIdentifier,
|
||||
label: manifest.label,
|
||||
name: manifest.name,
|
||||
description: manifest.description,
|
||||
version: manifest.version,
|
||||
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
|
||||
@@ -123,7 +123,7 @@ export class ApplicationSyncService {
|
||||
);
|
||||
|
||||
await this.applicationService.update(application.id, {
|
||||
label: manifest.label,
|
||||
name: manifest.name,
|
||||
description: manifest.description,
|
||||
version: manifest.version,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ApplicationEntity {
|
||||
universalIdentifier?: string;
|
||||
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
label: string;
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ApplicationService {
|
||||
|
||||
async create(data: {
|
||||
universalIdentifier?: string;
|
||||
label: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
serverlessFunctionLayerId: string;
|
||||
@@ -51,7 +51,7 @@ export class ApplicationService {
|
||||
async update(
|
||||
id: string,
|
||||
data: {
|
||||
label?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
sourcePath?: string;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { type ServerlessFunctionCode } from 'src/engine/metadata-modules/serverl
|
||||
export type PackageJson = {
|
||||
$schema?: string;
|
||||
universalIdentifier: string;
|
||||
label: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
engines: {
|
||||
node: string;
|
||||
|
||||
Reference in New Issue
Block a user