Compare commits

..
8 Commits
Author SHA1 Message Date
Charles Bochet 8d43ed4515 Fix message import scheduled 2025-11-25 19:24:00 +01:00
Baptiste DevessierandCharles Bochet 3d1d3d9f04 [Side Panel V2] Bring back old container (#16065)
## Before

<img width="1446" height="780" alt="image"
src="https://github.com/user-attachments/assets/fa25f22d-b979-4a8f-9989-004d07f862d7"
/>

## After

<img width="3456" height="2160" alt="CleanShot 2025-11-25 at 17 35
59@2x"
src="https://github.com/user-attachments/assets/a87880c4-3faa-47c3-9215-34afc36270a3"
/>
2025-11-25 18:16:32 +01:00
EtienneandCharles Bochet b19dc703cc Security - disable gql introspection for non-auth user (#16047)
closes https://github.com/twentyhq/private-issues/issues/351
closes https://github.com/twentyhq/private-issues/issues/350

Before, introspection query works without token. After, fails.

```

query IntrospectionQuery {
  __schema {
    queryType {
      name
    }
    mutationType {
      name
    }
    subscriptionType {
      name
    }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args {
        ...InputValue
      }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description
  fields(includeDeprecated: true) {
    name
    description
    args {
      ...InputValue
    }
    type {
      ...TypeRef
    }
    isDeprecated
    deprecationReason
  }
  inputFields {
    ...InputValue
  }
  interfaces {
    ...TypeRef
  }
  enumValues(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
  }
  possibleTypes {
    ...TypeRef
  }
}

fragment InputValue on __InputValue {
  name
  description
  type {
    ...TypeRef
  }
  defaultValue
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}
```
2025-11-25 18:16:07 +01:00
neo773andCharles Bochet 9b668619c5 Update channel sync service to immediately enqueue jobs (#16064) 2025-11-25 18:15:57 +01:00
neo773andCharles Bochet e7c83dc04f move folder cron to message-list-fetch (#16062) 2025-11-25 18:15:49 +01:00
Charles BochetandCharles Bochet 693fe01fa4 Fix upgrade command messaging (#16067) 2025-11-25 18:15:42 +01:00
Charles BochetandCharles Bochet c616713435 Add import scheduled status to messaging sync (#16058)
We have introduced to new syncStage statuses:
`messageChannel.MESSAGES_IMPORT_SCHEDULED` and
`calendarChannel.CALENDAR_EVENTS_IMPORT_SCHEDULED`

We need to make sure all existing workspaces have it
2025-11-25 15:28:48 +01:00
Raphaël Bosiandprastoin 859c948d01 Fix page layout widget deletion (#16035)
With the new side panel, we are able to delete a widget with the side
panel still open. This caused the app to crash because we threw when the
widget id wasn't defined.

This PR fixes this by closing the side panel in the delete action and by
returning null instead of throwing.

## Before



https://github.com/user-attachments/assets/092bfe62-82dc-4d83-9967-1cc753ecf55e



## After


https://github.com/user-attachments/assets/8bed6cc5-961b-4112-8cf5-e587865d14da
2025-11-25 14:08:39 +01:00
2085 changed files with 42298 additions and 83833 deletions
+1 -2
View File
@@ -12,7 +12,6 @@ This directory contains Twenty's development guidelines and best practices in th
### Core Guidelines
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
### Code Quality
- **typescript-guidelines.mdc** - TypeScript best practices and conventions (Auto-attached to .ts/.tsx files)
@@ -41,7 +40,7 @@ You can manually reference any rule using the `@ruleName` syntax:
- `@testing-guidelines` - Get testing recommendations
### Rule Types Used
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
- **Auto Attached** - Loaded when matching file patterns are referenced
- **Agent Requested** - Available for AI to include when relevant
- **Manual** - Only included when explicitly mentioned
-5
View File
@@ -89,8 +89,6 @@ Use the AI codebase search to find:
### 1. Setup Git Branch
**IMPORTANT**: Always start from an up-to-date main branch to avoid merge conflicts and ensure the changelog is based on the latest code.
```bash
cd /Users/thomascolasdesfrancs/code/twenty
git checkout main
@@ -100,8 +98,6 @@ git checkout -b {VERSION}
Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
⚠️ **Do this first** before making any file changes. This ensures your branch is based on the latest main.
### 2. Create File Structure
**Create changelog file:**
@@ -178,7 +174,6 @@ Description of the third feature.
- Focus on user benefits, not technical implementation
- Use active voice
- Start with what the user can now do
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
**Reference Previous Changelogs:**
- Check `packages/twenty-website/src/content/releases/` for examples
-29
View File
@@ -1,29 +0,0 @@
---
description: Guidelines for generating and managing TypeORM migrations in twenty-server
globs: [
"packages/twenty-server/src/**/*.entity.ts",
"packages/twenty-server/src/database/typeorm/**/*.ts"
]
alwaysApply: false
---
## Server Migrations (twenty-server)
- **When changing an entity, always generate a migration**
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
- Use the Nx + TypeORM command from the project root:
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
```
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
- **Prefer generated migrations over manual edits**
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
- **Keep migrations consistent and reversible**
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
+1 -1
View File
@@ -63,4 +63,4 @@ git push origin your-branch-name
## Reporting Issues
If you face any issues or have suggestions, please feel free to [create an issue on Twenty's GitHub repository](https://github.com/twentyhq/twenty/issues/new). Please provide as much detail as possible.
If you face any issues or have suggestions, please feel free to (create an issue on Twenty's GitHub repository)[https://github.com/twentyhq/twenty/issues/new]. Please provide as much detail as possible.
@@ -1,4 +1,4 @@
name: CI SDK
name: CI CLI
on:
push:
@@ -19,8 +19,9 @@ jobs:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-sdk/**
sdk-test:
packages/twenty-cli/**
packages/twenty-server/**
cli-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
@@ -42,12 +43,12 @@ jobs:
- name: Run ${{ matrix.task }} task
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:sdk
tag: scope:cli
tasks: ${{ matrix.task }}
sdk-e2e-test:
cli-e2e-test:
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
needs: [changed-files-check, sdk-test]
needs: [changed-files-check, cli-test]
if: needs.changed-files-check.outputs.any_changed == 'true'
services:
postgres:
@@ -89,13 +90,13 @@ jobs:
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: SDK / Run E2E Tests
run: npx nx test:e2e twenty-sdk
ci-sdk-status-check:
- name: CLI / Run E2E Tests
run: npx nx test:e2e twenty-cli
ci-cli-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, sdk-test, sdk-e2e-test]
needs: [changed-files-check, cli-test, cli-e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
-55
View File
@@ -1,55 +0,0 @@
name: CI Create App
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
create-app-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test, build]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:create-app
tasks: ${{ matrix.task }}
ci-create-app-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
@@ -1,8 +1,4 @@
name: 'Test Docker Compose'
permissions:
contents: read
on:
pull_request:
+1 -3
View File
@@ -124,9 +124,7 @@ jobs:
exit 0
fi
git commit -m "chore: sync docs artifacts"
git push origin "HEAD:$HEAD_REF"
env:
HEAD_REF: ${{ github.head_ref }}
git push origin HEAD:${{ github.head_ref }}
- name: Check for changes and commit
if: github.event_name != 'pull_request'
+7 -2
View File
@@ -9,6 +9,8 @@
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@radix-ui/colors": "^3.0.0",
"@sentry/profiling-node": "^9.26.0",
"@sentry/react": "^9.26.0",
"@sniptt/guards": "^0.2.0",
"@tabler/icons-react": "^3.31.0",
"@wyw-in-js/vite": "^0.7.0",
@@ -187,7 +189,11 @@
"ts-node": "10.9.1",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"vite": "^7.0.0"
"vite": "^7.0.0",
"vite-plugin-checker": "^0.10.2",
"vite-plugin-cjs-interop": "^2.2.0",
"vite-plugin-dts": "3.8.1",
"vite-plugin-svgr": "^4.2.0"
},
"engines": {
"node": "^24.5.0",
@@ -227,7 +233,6 @@
"packages/twenty-sdk",
"packages/twenty-apps",
"packages/twenty-cli",
"packages/create-twenty-app",
"tools/eslint-rules"
]
}
-97
View File
@@ -1,97 +0,0 @@
<div align="center">
<a href="https://twenty.com">
<picture>
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/public/images/core/logo.svg" height="128">
</picture>
</a>
<h1>Create Twenty App</h1>
<a href="https://www.npmjs.com/package/create-twenty-app"><img alt="NPM version" src="https://img.shields.io/npm/v/create-twenty-app.svg?style=for-the-badge&labelColor=000000"></a>
<a href="https://github.com/twentyhq/twenty/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/npm/l/next.svg?style=for-the-badge&labelColor=000000"></a>
<a href="https://discord.gg/cx5n4Jzs57"><img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&logo=Twenty&labelColor=000000&logoWidth=20"></a>
</div>
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a readytorun project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
- Zeroconfig project bootstrap
- Preconfigured scripts for auth, generate, dev sync, oneoff sync, uninstall
- Strong TypeScript support and typed client generation
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
## Quick start
```bash
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Authenticate using your API key (you'll be prompted)
yarn auth
# Add a new entity to your application (guided)
yarn create-entity
# Generate a typed Twenty client and workspace entity types
yarn generate
# Start dev mode: automatically syncs local changes to your workspace
yarn dev
# Or run a onetime sync
yarn sync
# Watch your application's functions logs
yarn logs
# Uninstall the application from the current workspace
yarn uninstall
# Display commands' help
yarn help
```
## What gets scaffolded
- A minimal app structure ready for Twenty
- TypeScript configuration
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
- 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 uptodate using `yarn generate`.
- Use `yarn dev` while you iterate to see changes instantly in your workspace.
## Publish your application
Applications are currently stored in `twenty/packages/twenty-apps`.
You can share your application with all Twenty users:
```bash
# pull the 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.
## 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 restart `yarn dev`.
## Contributing
- See our [GitHub](https://github.com/twentyhq/twenty)
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
-54
View File
@@ -1,54 +0,0 @@
{
"name": "create-twenty-app",
"version": "0.1.3",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
"files": [
"dist/**/*"
],
"scripts": {
"build": "npx rimraf dist && npx vite build"
},
"keywords": [
"twenty",
"cli",
"crm",
"application",
"development"
],
"exports": {
".": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.mjs",
"require": "./dist/cli.cjs"
}
},
"license": "AGPL-3.0",
"dependencies": {
"@genql/cli": "^3.0.3",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"inquirer": "^10.0.0",
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": "^24.5.0",
"yarn": "^4.0.2"
}
}
-56
View File
@@ -1,56 +0,0 @@
{
"name": "create-twenty-app",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"tags": ["scope:create-app"],
"targets": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/create-twenty-app",
"command": "tsx src/cli.ts"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/create-twenty-app",
"command": "node dist/cli.cjs"
}
},
"typecheck": {},
"lint": {
"options": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"maxWarnings": 0
},
"configurations": {
"ci": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"maxWarnings": 0
},
"fix": {}
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "{projectRoot}/jest.config.mjs"
},
"configurations": {
"ci": {
"ci": true,
"coverage": true,
"watchAll": false
}
}
}
}
}
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env node
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { CreateAppCommand } from '@/create-app.command';
import packageJson from '../package.json';
const program = new Command(packageJson.name)
.description('CLI tool to initialize a new Twenty application')
.version(
packageJson.version,
'-v, --version',
'Output the current version of create-twenty-app.',
)
.argument('[directory]')
.helpOption('-h, --help', 'Display this help message.')
.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 new CreateAppCommand().execute(directory);
});
program.exitOverride();
try {
program.parse();
} catch (error) {
if (error instanceof CommanderError) {
process.exit(error.exitCode);
}
if (error instanceof Error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}
}
@@ -1 +0,0 @@
24.5.0
@@ -1,27 +0,0 @@
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
First, authenticate to your workspace:
```bash
yarn auth
```
Then, install this app to your workspace:
```bash
yarn sync
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Learn More
To learn more about Twenty applications, take a look at the following resources:
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
@@ -1,137 +0,0 @@
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
import prettierPlugin from 'eslint-plugin-prettier';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
export default [
// Base JS rules
js.configs.recommended,
// Global ignores
{
ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**'],
},
// Base config for TS/JS files
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: {
prettier: prettierPlugin,
import: importPlugin,
'prefer-arrow': preferArrowPlugin,
'unused-imports': unusedImportsPlugin,
},
rules: {
// General rules (aligned with main project)
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': [
'warn',
{ allow: ['group', 'groupCollapsed', 'groupEnd'] },
],
'no-control-regex': 0,
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-undef': 'off',
'no-unused-vars': 'off',
// Import rules
'import/no-relative-packages': 'error',
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { considerQueryString: true }],
// Prefer arrow functions
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
// Unused imports
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
// Prettier (formatting as lint errors if you want)
'prettier/prettier': 'error',
},
},
// TypeScript-specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
},
rules: {
// Turn off base rule and use TS-aware versions
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
// Test files (Jest)
{
files: ['**/*.spec.@(ts|tsx|js|jsx)', '**/*.test.@(ts|tsx|js|jsx)'],
languageOptions: {
globals: {
jest: true,
describe: true,
it: true,
expect: true,
beforeEach: true,
afterEach: true,
beforeAll: true,
afterAll: true,
},
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
];
@@ -1,10 +0,0 @@
import { convertToLabel } from '@/utils/convert-to-label';
describe('convertToLabel', () => {
it('should convert to label', () => {
expect(convertToLabel('toto')).toBe('Toto');
expect(convertToLabel('totoTata')).toBe('Toto tata');
expect(convertToLabel('totoTataTiti')).toBe('Toto tata titi');
expect(convertToLabel('toto-tata-titi')).toBe('Toto tata titi');
});
});
@@ -1,143 +0,0 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { v4 } from 'uuid';
export const copyBaseApplicationProject = async ({
appName,
appDisplayName,
appDescription,
appDirectory,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}) => {
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
await createPackageJson({ appName, appDirectory });
await createGitignore(appDirectory);
await createYarnLock(appDirectory);
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
appDirectory,
});
};
const createYarnLock = async (appDirectory: string) => {
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
};
const createGitignore = async (appDirectory: string) => {
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# testing
/coverage
# dev
/dist/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
`;
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
};
const createApplicationConfig = async ({
displayName,
description,
appDirectory,
}: {
displayName: string;
description?: string;
appDirectory: string;
}) => {
const content = `import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = {
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
};
export default config;
`;
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
};
const createPackageJson = async ({
appName,
appDirectory,
}: {
appName: string;
appDirectory: string;
}) => {
const packageJson = {
name: appName,
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
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',
help: 'twenty help',
auth: 'twenty auth login',
},
dependencies: {
'twenty-sdk': '0.1.3',
},
devDependencies: {
'@types/node': '^24.7.2',
typescript: '^5.9.3',
},
};
await fs.writeFile(
join(appDirectory, 'package.json'),
JSON.stringify(packageJson, null, 2),
'utf8',
);
};
@@ -1,13 +0,0 @@
import chalk from 'chalk';
import { promisify } from 'util';
import { exec } from 'child_process';
const execPromise = promisify(exec);
export const install = async (root: string) => {
try {
await execPromise('yarn', { cwd: root });
} catch (error: any) {
console.error(chalk.red('yarn install failed:'), error.stdout);
}
};
@@ -1,70 +0,0 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { promisify } from 'util';
import { exec } from 'child_process';
const execPromise = promisify(exec);
const isInGitRepository = async (root: string): Promise<boolean> => {
try {
await execPromise('git rev-parse --is-inside-work-tree', { cwd: root });
return true;
} catch {
// Empty catch block
}
return false;
};
const isInMercurialRepository = async (root: string): Promise<boolean> => {
try {
await execPromise('hg --cwd . root', { cwd: root });
return true;
} catch {
// Empty catch block
}
return false;
};
const isDefaultBranchSet = async (root: string): Promise<boolean> => {
try {
await execPromise('git config init.defaultBranch', { cwd: root });
return true;
} catch {
// Empty catch block
}
return false;
};
export const tryGitInit = async (root: string): Promise<boolean> => {
try {
await execPromise('git --version', { cwd: root });
if (
(await isInGitRepository(root)) ||
(await isInMercurialRepository(root))
) {
return false;
}
await execPromise('git init', { cwd: root });
try {
if (!(await isDefaultBranchSet(root))) {
await execPromise('git checkout -b main', { cwd: root });
}
await execPromise('git add -A', { cwd: root });
await execPromise(
'git commit -m "Initial commit from Create Twenty App"',
{
cwd: root,
},
);
return true;
} catch {
fs.rm(join(root, '.git'), { recursive: true, force: true });
return false;
}
} catch {
return false;
}
};
-26
View File
@@ -1,26 +0,0 @@
{
"compilerOptions": {
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
@@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": [
"**/__mocks__/**/*",
"vite.config.ts",
"jest.config.mjs",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/**/*.test.ts",
"src/**/*.test.tsx"
]
}
-96
View File
@@ -1,96 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';
const moduleEntries = Object.keys((packageJson as any).exports || {})
.filter(
(key) => key !== './style.css' && key !== '.' && !key.startsWith('./src/'),
)
.map((module) => `src/${module.replace(/^\.\//, '')}/index.ts`);
const entries = ['src/cli.ts', ...moduleEntries];
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
if (!chunk.isEntry) {
throw new Error(
`Should never occurs, encountered a non entry chunk ${chunk.facadeModuleId}`,
);
}
const splitFaceModuleId = chunk.facadeModuleId?.split('/');
if (splitFaceModuleId === undefined) {
throw new Error(
`Should never occurs splitFaceModuleId is undefined ${chunk.facadeModuleId}`,
);
}
const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2];
if (moduleDirectory === 'src') {
return `${chunk.name}.${extension}`;
}
return `${moduleDirectory}.${extension}`;
};
const copyAssetPlugin = (targets: { src: string; dest: string }[]) => {
return {
name: 'copy-assets',
closeBundle: async () => {
for (const target of targets) {
await fs.copy(
path.resolve(__dirname, target.src),
path.resolve(__dirname, target.dest),
);
}
},
};
};
export default defineConfig(() => {
const tsConfigPath = path.resolve(__dirname, './tsconfig.lib.json');
return {
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/create-twenty-app',
plugins: [
tsconfigPaths({
root: __dirname,
}),
dts({ entryRoot: './src', tsconfigPath: tsConfigPath }),
copyAssetPlugin([
{
src: 'src/constants/base-application',
dest: 'dist/constants/base-application',
},
]),
],
build: {
outDir: 'dist',
lib: { entry: entries, name: 'create-twenty-app' },
rollupOptions: {
external: [
...Object.keys((packageJson as any).dependencies || {}),
'path',
'fs',
'child_process',
],
output: [
{
format: 'es',
entryFileNames: (chunk) => entryFileNames(chunk, 'mjs'),
},
{
format: 'cjs',
interop: 'auto',
esModule: true,
exports: 'named',
entryFileNames: (chunk) => entryFileNames(chunk, 'cjs'),
},
],
},
},
logLevel: 'warn',
};
});
@@ -2056,8 +2056,8 @@ __metadata:
linkType: hard
"glob@npm:^10.2.2":
version: 10.5.0
resolution: "glob@npm:10.5.0"
version: 10.4.5
resolution: "glob@npm:10.4.5"
dependencies:
foreground-child: "npm:^3.1.0"
jackspeak: "npm:^3.1.2"
@@ -2067,7 +2067,7 @@ __metadata:
path-scurry: "npm:^1.11.1"
bin:
glob: dist/esm/bin.mjs
checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
languageName: node
linkType: hard
@@ -239,15 +239,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^24.7.2":
version: 24.10.1
resolution: "@types/node@npm:24.10.1"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/d6bca7a78f550fbb376f236f92b405d676003a8a09a1b411f55920ef34286ee3ee51f566203920e835478784df52662b5b2af89159d9d319352e9ea21801c002
languageName: node
linkType: hard
"abbrev@npm:^3.0.0":
version: 3.0.1
resolution: "abbrev@npm:3.0.1"
@@ -581,8 +572,8 @@ __metadata:
linkType: hard
"glob@npm:^10.2.2":
version: 10.5.0
resolution: "glob@npm:10.5.0"
version: 10.4.5
resolution: "glob@npm:10.4.5"
dependencies:
foreground-child: "npm:^3.1.0"
jackspeak: "npm:^3.1.2"
@@ -592,7 +583,7 @@ __metadata:
path-scurry: "npm:^1.11.1"
bin:
glob: dist/esm/bin.mjs
checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
languageName: node
linkType: hard
@@ -916,10 +907,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "rollup-engine@workspace:."
dependencies:
"@types/node": "npm:^24.7.2"
dotenv: "npm:^16.4.5"
tsx: "npm:^4.20.6"
twenty-sdk: "npm:0.0.3"
languageName: unknown
linkType: soft
@@ -1078,20 +1067,6 @@ __metadata:
languageName: node
linkType: hard
"twenty-sdk@npm:0.0.3":
version: 0.0.3
resolution: "twenty-sdk@npm:0.0.3"
checksum: 10c0/0a3c85c27edb22fb50f7eb0da4f9770e85729fce05e9e0118ad0cdfc36e42425c93340a6cd1c276daf30aeeaa612db0cd905831c0a8287a31bff3da5be9b0562
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
"unique-filename@npm:^4.0.0":
version: 4.0.0
resolution: "unique-filename@npm:4.0.0"
@@ -50,7 +50,7 @@ const returnWorkspaceMemberObjectId = async (): Promise<string> => {
).id;
};
const createUpdatedByFieldMetadata = async (
const createUpdatedByField = async (
sourceObjectId: string,
workspaceMemberObjectId: string,
objectName: string,
@@ -124,7 +124,7 @@ const createUpdatedByFieldMetadata = async (
}
};
const updateUpdatedByFieldValue = async (
const updateUpdatedByField = async (
objectName: string,
workspaceMemberId: string,
recordId: string,
@@ -184,7 +184,7 @@ export const main = async (params: {
const workspaceMemberObjectId: string =
await returnWorkspaceMemberObjectId();
const isFieldCreated: boolean | undefined = await createUpdatedByFieldMetadata(
const isFieldCreated: boolean | undefined = await createUpdatedByField(
objectMetadata.id,
workspaceMemberObjectId,
objectMetadata.namePlural,
@@ -198,7 +198,7 @@ export const main = async (params: {
}
}
const isObjectUpdated: boolean | undefined = await updateUpdatedByFieldValue(
const isObjectUpdated: boolean | undefined = await updateUpdatedByField(
objectMetadata.namePlural,
workspaceMemberId,
recordId,
@@ -1,4 +1,4 @@
import { type ApplicationConfig } from 'twenty-sdk';
import { type ApplicationConfig } from 'twenty-sdk/application';
const config: ApplicationConfig = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
+2 -10
View File
@@ -1,6 +1,6 @@
{
"name": "hello-world",
"version": "0.1.0",
"version": "0.0.2",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -8,16 +8,8 @@
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"scripts": {
"create-entity": "twenty app add",
"dev": "twenty app dev",
"generate": "twenty app generate",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
"auth": "twenty auth login"
},
"dependencies": {
"twenty-sdk": "0.1.2"
"twenty-sdk": "0.0.6"
},
"devDependencies": {
"@types/node": "^24.7.2"
@@ -1,4 +1,4 @@
import { type FunctionConfig } from 'twenty-sdk';
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
import { createClient } from '../../generated';
export const main = async (params: { recipient?: string }) => {
@@ -10,7 +10,6 @@ export const main = async (params: { recipient?: string }) => {
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
});
const createPostCard = await client.mutation({
createPostCard: {
__args: {
@@ -22,9 +21,6 @@ export const main = async (params: { recipient?: string }) => {
id: true,
},
});
console.log('createPostCard result', createPostCard);
return createPostCard;
} catch (error) {
console.error(error);
@@ -32,7 +28,7 @@ export const main = async (params: { recipient?: string }) => {
}
};
export const config: FunctionConfig = {
export const config: ServerlessFunctionConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -10,7 +10,7 @@ import {
Relation,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
} from 'twenty-sdk/application';
enum PostCardStatus {
DRAFT = 'DRAFT',
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -1,16 +1,13 @@
# Deprecated: twenty-cli
# Why Twenty CLI?
This package is deprecated. Please install and use twenty-sdk instead:
A command-line interface to easily scaffold, develop, and publish applications that extend Twenty CRM
## Installation
```bash
npm uninstall twenty-cli
npm install -g twenty-sdk
npm install -g twenty-cli
```
The command name remains the same: twenty.
A command-line interface to easily scaffold, develop, and publish applications that extend Twenty CRM (now provided by twenty-sdk).
## Requirements
- yarn >= 4.9.2
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env node
const message = `\nTwenty CLI (twenty-cli) is deprecated.\n\nPlease install and use the new package instead:\n npm install -g twenty-sdk\n\nThe command name remains the same: \"twenty\".\nMore info: https://www.npmjs.com/package/twenty-sdk\n`;
console.error(message);
process.exitCode = 1;
@@ -4,7 +4,7 @@ const jestConfig: JestConfigWithTsJest = {
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
// Prettier v3 should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
prettierPath: null,
displayName: 'twenty-sdk-e2e',
displayName: 'twenty-cli-e2e',
silent: false,
errorOnDeprecated: true,
maxConcurrency: 1,
@@ -13,8 +13,8 @@ const jestConfig: JestConfigWithTsJest = {
testEnvironment: 'node',
testRegex: '\\.e2e-spec\\.ts$',
modulePathIgnorePatterns: ['<rootDir>/dist'],
globalTeardown: '<rootDir>/src/cli/__tests__/e2e/teardown.ts',
setupFilesAfterEnv: ['<rootDir>/src/cli/__tests__/e2e/setupTest.ts'],
globalTeardown: '<rootDir>/src/__tests__/e2e/teardown.ts',
setupFilesAfterEnv: ['<rootDir>/src/__tests__/e2e/setupTest.ts'],
testTimeout: 30000, // 30 seconds timeout for e2e tests
maxWorkers: 1,
transform: {
+58 -4
View File
@@ -1,10 +1,64 @@
{
"name": "twenty-cli",
"version": "0.3.0",
"description": "[DEPRECATED] Use twenty-sdk instead: https://www.npmjs.com/package/twenty-sdk",
"version": "0.2.4",
"description": "Command-line interface for Twenty application development",
"main": "dist/cli.js",
"bin": {
"twenty": "dist/cli.js"
},
"files": [
"dist/**/*",
"!dist/**/*.e2e-spec.*",
"!dist/**/__tests__/**"
],
"scripts": {
"build": "echo 'use npx nx build'",
"start": "echo 'deprecated'"
"dev": "tsx src/cli.ts",
"start": "node dist/cli.js"
},
"license": "AGPL-3.0"
"keywords": [
"twenty",
"cli",
"crm",
"application",
"development"
],
"license": "AGPL-3.0",
"dependencies": {
"@genql/cli": "^3.0.3",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"axios": "^1.6.0",
"chalk": "^5.3.0",
"chokidar": "^4.0.0",
"commander": "^12.0.0",
"dotenv": "^16.4.0",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
"inquirer": "^10.0.0",
"jsonc-parser": "^3.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.capitalize": "^4.2.1",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
"typescript": "^5.9.2",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/jest": "^29.5.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.capitalize": "^4",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"jest": "^29.5.0",
"tsx": "^4.7.0",
"wait-on": "^7.2.0"
},
"engines": {
"node": "^24.5.0",
"yarn": "^4.0.2"
}
}
+94
View File
@@ -0,0 +1,94 @@
{
"name": "twenty-cli",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"tags": ["scope:cli"],
"targets": {
"before-build": {
"executor": "nx:run-commands",
"cache": true,
"options": {
"cwd": "packages/twenty-cli",
"commands": ["rimraf dist", "tsc --project tsconfig.lib.json"]
},
"dependsOn": ["^build", "typecheck"]
},
"build": {
"executor": "nx:run-commands",
"cache": true,
"options": {
"cwd": "packages/twenty-cli",
"commands": [
"cp -R src/constants/base-application-project dist/constants"
]
},
"dependsOn": ["before-build"]
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-cli",
"command": "tsx src/cli.ts"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-cli",
"command": "node dist/cli.js"
}
},
"typecheck": {},
"lint": {
"options": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"maxWarnings": 0
},
"configurations": {
"ci": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"maxWarnings": 0
},
"fix": {}
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "{projectRoot}/jest.config.mjs"
},
"configurations": {
"ci": {
"ci": true,
"coverage": true,
"watchAll": false
}
}
},
"test:e2e": {
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"cwd": "packages/twenty-cli",
"commands": [
"npx wait-on http://localhost:3000/healthz --timeout 600000 --interval 1000 --log && NODE_ENV=test npx jest --config ./jest.e2e.config.ts"
]
},
"parallel": false,
"dependsOn": [
"build",
{
"target": "database:reset",
"projects": "twenty-server"
},
{
"target": "start:ci-if-needed",
"projects": "twenty-server"
}
]
}
}
}
@@ -1,4 +1,4 @@
import { type TwentyConfig } from '../../../types/config.types';
import { TwentyConfig } from '../../../types/config.types';
export const testConfig: TwentyConfig = {
apiUrl: 'http://localhost:3000',
@@ -1,7 +1,7 @@
import { exec } from 'child_process';
export default async () =>
new Promise<void>((resolve) => {
export default async function globalTeardown() {
return new Promise<void>((resolve) => {
exec('pkill -f "nest start" || true', (error: unknown) => {
if (error) {
console.log('No server processes to kill');
@@ -11,3 +11,4 @@ export default async () =>
resolve();
});
});
}
@@ -1,9 +1,11 @@
import path from 'path';
export const getTestedApplicationPath = (relativePath: string): string => {
const currentFileDir = __dirname;
const twentyAppsPath = path.resolve(
__dirname,
'../../../../../../twenty-apps',
currentFileDir,
'../../../../../twenty-apps',
);
return path.join(twentyAppsPath, relativePath);
@@ -1,10 +1,15 @@
#!/usr/bin/env node
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { readFileSync } from 'fs';
import { join } from 'path';
import { AppCommand } from './commands/app.command';
import { AuthCommand } from './commands/auth.command';
import { ConfigService } from './services/config.service';
import packageJson from '../../package.json';
const packageJson = JSON.parse(
readFileSync(join(__dirname, '../package.json'), 'utf-8'),
);
const program = new Command();
@@ -5,13 +5,13 @@ 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 { getServerlessFunctionBaseFile } from '../utils/get-serverless-function-base-file';
import { convertToLabel } from '../utils/convert-to-label';
export enum SyncableEntity {
AGENT = 'agent',
OBJECT = 'object',
FUNCTION = 'function',
SERVERLESS_FUNCTION = 'serverlessFunction',
}
export const isSyncableEntity = (value: string): value is SyncableEntity => {
@@ -44,12 +44,12 @@ export class AppAddCommand {
return;
}
if (entity === SyncableEntity.FUNCTION) {
if (entity === SyncableEntity.SERVERLESS_FUNCTION) {
const entityName = await this.getEntityName(entity);
const objectFileName = `${camelcase(entityName)}.ts`;
const decoratedServerlessFunction = getFunctionBaseFile({
const decoratedServerlessFunction = getServerlessFunctionBaseFile({
name: entityName,
});
@@ -76,7 +76,7 @@ export class AppAddCommand {
name: 'entity',
message: `What entity do you want to create?`,
default: '',
choices: [SyncableEntity.FUNCTION, SyncableEntity.OBJECT],
choices: [SyncableEntity.SERVERLESS_FUNCTION, SyncableEntity.OBJECT],
},
]);
@@ -1,11 +1,10 @@
import chalk from 'chalk';
import * as chokidar from 'chokidar';
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
import { ApiService } from '@/cli/services/api.service';
import { loadManifest } from '@/cli/utils/load-manifest';
import { AppSyncCommand } from './app-sync.command';
export class AppDevCommand {
private apiService = new ApiService();
private syncCommand = new AppSyncCommand();
async execute(options: {
appPath?: string;
@@ -18,7 +17,7 @@ export class AppDevCommand {
this.logStartupInfo(appPath, debounceMs);
await this.synchronize(appPath);
await this.syncCommand.execute(appPath);
const watcher = this.setupFileWatcher(appPath, debounceMs);
@@ -32,16 +31,6 @@ export class AppDevCommand {
}
}
private async synchronize(appPath: string) {
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
await this.apiService.syncApplication({
manifest,
packageJson,
yarnLock,
});
}
private logStartupInfo(appPath: string, debounceMs: number): void {
console.log(chalk.blue('🚀 Starting Twenty Application Development Mode'));
console.log(chalk.gray(`📁 App Path: ${appPath}`));
@@ -68,7 +57,7 @@ export class AppDevCommand {
timeout = setTimeout(async () => {
console.log(chalk.blue('🔄 Changes detected, syncing...'));
await this.synchronize(appPath);
await this.syncCommand.execute(appPath);
console.log(
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
@@ -2,15 +2,11 @@ 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 { 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 { convertToLabel } from '../utils/convert-to-label';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
export class CreateAppCommand {
export class AppInitCommand {
async execute(directory?: string): Promise<void> {
try {
const { appName, appDisplayName, appDirectory, appDescription } =
@@ -29,10 +25,6 @@ export class CreateAppCommand {
appDirectory,
});
await install(appDirectory);
await tryGitInit(appDirectory);
this.logSuccess(appDirectory);
} catch (error) {
console.error(
@@ -86,8 +78,8 @@ export class CreateAppCommand {
const appDescription = description.trim();
const appDirectory = directory
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
? path.join(process.cwd(), kebabCase(directory))
: path.join(process.cwd(), kebabCase(appName));
return { appName, appDisplayName, appDirectory, appDescription };
}
@@ -119,10 +111,10 @@ export class CreateAppCommand {
}
private logSuccess(appDirectory: string): void {
console.log(chalk.green('✅ Application created!'));
console.log(chalk.green('✅ Application created successfully!'));
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(`cd ${appDirectory.split('/').reverse()[0] ?? ''}`);
console.log('yarn auth');
console.log(` cd ${appDirectory}`);
console.log(' twenty app dev');
}
}
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
import { ApiService } from '../services/api.service';
import { GenerateService } from '../services/generate.service';
import { type ApiResponse } from '../types/config.types';
import { ApiResponse } from '../types/config.types';
import { loadManifest } from '../utils/load-manifest';
export class AppSyncCommand {
@@ -49,7 +49,7 @@ export class AppSyncCommand {
});
}
if (serverlessSyncResult.success === false) {
if (!serverlessSyncResult.success) {
console.error(
chalk.red('❌ Serverless functions Sync failed:'),
serverlessSyncResult.error,
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import inquirer from 'inquirer';
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
import { ApiService } from '../services/api.service';
import { type ApiResponse } from '../types/config.types';
import { ApiResponse } from '../types/config.types';
import { loadManifest } from '../utils/load-manifest';
export class AppUninstallCommand {
@@ -31,7 +31,7 @@ export class AppUninstallCommand {
manifest.application.universalIdentifier,
);
if (result.success === false) {
if (!result.success) {
console.error(chalk.red('❌ Uninstall failed:'), result.error);
} else {
console.log(chalk.green('✅ Application uninstalled successfully'));
@@ -7,18 +7,18 @@ import {
} from './app-add.command';
import { AppUninstallCommand } from './app-uninstall.command';
import { AppDevCommand } from './app-dev.command';
import { AppInitCommand } from './app-init.command';
import { AppSyncCommand } from './app-sync.command';
import { formatPath } from '../utils/format-path';
import { AppGenerateCommand } from './app-generate.command';
import { AppLogsCommand } from '@/cli/commands/app-logs.command';
export class AppCommand {
private devCommand = new AppDevCommand();
private syncCommand = new AppSyncCommand();
private uninstallCommand = new AppUninstallCommand();
private initCommand = new AppInitCommand();
private addCommand = new AppAddCommand();
private generateCommand = new AppGenerateCommand();
private logsCommand = new AppLogsCommand();
getCommand(): Command {
const appCommand = new Command('app');
@@ -84,6 +84,21 @@ export class AppCommand {
}
});
appCommand
.command('init [directory]')
.description('Initialize a new Twenty application')
.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 [entityType]')
.option('--path <path>', 'Path in which the entity should be created.')
@@ -112,32 +127,6 @@ export class AppCommand {
await this.generateCommand.execute(formatPath(appPath));
});
appCommand
.command('logs [appPath]')
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Only show logs for the function with this universal ID',
)
.option(
'-n, --functionName <functionName>',
'Only show logs for the function with this name',
)
.description('Watch application function logs')
.action(
async (
appPath?: string,
options?: {
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
await this.logsCommand.execute({
...options,
appPath: formatPath(appPath),
});
},
);
return appCommand;
}
}
@@ -0,0 +1,26 @@
# Set environment values for your application here.
# Use the format: KEY=value
#
# These variables are automatically loaded when running your serverless functions.
# You can access them directly in your code using:
# const myValue = process.env.KEY;
#
# To make these variables available to your application, add them in application.config.ts file
#
# const config: ApplicationConfig = {
# ...
# applicationVariables: {
# KEY: {
# universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
# description: 'Description',
# isSecret: true,
# },
# },
# };
#
# Those environment variables will be provided to your serverless
# functions at runtime.
#
# Example:
# API_TOKEN=your-api-token
# TIMEOUT_MS=3000
@@ -0,0 +1,5 @@
# Duplicated with ./gitignore because npm publish does not include .gitignore
# https://github.com/npm/npm/issues/3763
.yarn/install-state.gz
.env
@@ -0,0 +1,15 @@
# {title}
{description}
## 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 auth login
twenty app sync
```
@@ -0,0 +1,2 @@
.yarn/install-state.gz
.env
@@ -0,0 +1,17 @@
{
"name": "my-application",
"version": "0.0.1",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"dependencies": {
"twenty-sdk": "0.0.6"
},
"devDependencies": {
"@types/node": "^24.7.2"
}
}
@@ -11,7 +11,7 @@
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"strictNullChecks": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
@@ -22,5 +22,6 @@
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
@@ -0,0 +1,38 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10c0
"@types/node@npm:^24.7.2":
version: 24.9.1
resolution: "@types/node@npm:24.9.1"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/c52f8168080ef9a7c3dc23d8ac6061fab5371aad89231a0f6f4c075869bc3de7e89b075b1f3e3171d9e5143d0dda1807c3dab8e32eac6d68f02e7480e7e78576
languageName: node
linkType: hard
"root-workspace-0b6124@workspace:.":
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
"@types/node": "npm:^24.7.2"
twenty-sdk: "npm:^0.0.2"
languageName: unknown
linkType: soft
"twenty-sdk@npm:^0.0.2":
version: 0.0.2
resolution: "twenty-sdk@npm:0.0.2"
checksum: 10c0/99e6fe86059d847b548c1f03e0f0c59a4d540caf1d28dd4500f1f5f0094196985ded955801274de9e72ff03e3d1f41e9a509b4c2c5a02ffc8a027277b1e35d8e
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
@@ -0,0 +1,8 @@
import { join } from 'path';
const BASE_PATH = join(__dirname, '../constants');
export const BASE_APPLICATION_PROJECT_PATH = join(
BASE_PATH,
'base-application-project',
);
@@ -5,7 +5,6 @@ import {
getIntrospectionQuery,
printSchema,
} from 'graphql/index';
import { createClient } from 'graphql-sse';
import {
type ApiResponse,
type AppManifest,
@@ -236,53 +235,4 @@ export class ApiService {
throw error;
}
}
async subscribeToLogs({
applicationUniversalIdentifier,
functionUniversalIdentifier,
functionName,
}: {
applicationUniversalIdentifier: string;
functionUniversalIdentifier?: string;
functionName?: string;
}) {
const twentyConfig = await this.configService.getConfig();
const wsClient = createClient({
url: twentyConfig.apiUrl + '/graphql',
headers: {
Authorization: `Bearer ${twentyConfig.apiKey}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
});
const query = `
subscription SubscribeToLogs($input: ServerlessFunctionLogsInput!) {
serverlessFunctionLogs(input: $input) {
logs
}
}
`;
const variables = {
input: {
applicationUniversalIdentifier,
universalIdentifier: functionUniversalIdentifier,
name: functionName,
},
};
wsClient.subscribe<{ serverlessFunctionLogs: { logs: string } }>(
{
query,
variables,
},
{
next: ({ data }) => console.log(data?.serverlessFunctionLogs.logs),
error: (err: unknown) => console.error(err),
complete: () => console.log('Completed'),
},
);
}
}
@@ -1,7 +1,7 @@
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { type TwentyConfig } from '../types/config.types';
import { TwentyConfig } from '../types/config.types';
type PersistedConfig = TwentyConfig & {
profiles?: Record<string, TwentyConfig>;
@@ -69,7 +69,7 @@ export class ConfigService {
raw.profiles = {};
}
const currentProfile = raw.profiles[profile] || { apiUrl: '' };
const currentProfile = raw.profiles[profile] || {};
raw.profiles[profile] = { ...currentProfile, ...config };
@@ -14,7 +14,7 @@ describe('getObjectDecoratedClass', () => {
name: 'MyNewObject',
}),
).toBe(
`import { Object } from 'twenty-sdk';
`import { Object } from 'twenty-sdk/application';
@Object({
universalIdentifier: '4122a047-260f-4cf1-bf4f-a268579d7ddf',
@@ -1,13 +1,14 @@
import { getFunctionBaseFile } from '../get-function-base-file';
import { getServerlessFunctionBaseFile } from '../get-serverless-function-base-file';
describe('getFunctionBaseFile', () => {
describe('getServerlessFunctionBaseFile', () => {
it('should render proper file', () => {
expect(
getFunctionBaseFile({
getServerlessFunctionBaseFile({
name: 'serverless-function-name',
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
}),
).toBe(`import { type FunctionConfig } from 'twenty-sdk';
)
.toBe(`import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
export const main = async (params: {
a: string;
@@ -22,7 +23,7 @@ export const main = async (params: {
return { message };
};
export const config: FunctionConfig = {
export const config: ServerlessFunctionConfig = {
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
name: 'serverless-function-name',
timeoutSeconds: 5,
@@ -1,8 +1,8 @@
import { ensureDirSync, writeFileSync, removeSync } from 'fs-extra';
import { ensureDirSync, removeSync, writeFileSync } from 'fs-extra';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { copyBaseApplicationProject } from '../app-template';
import { loadManifest } from '../load-manifest';
import { v4 } from 'uuid';
const write = (root: string, file: string, content: string) => {
const abs = join(root, file);
@@ -22,7 +22,7 @@ const tsLibMock = `declare module 'tslib' {
}`;
const twentySdkTypesMock = `
declare module 'twenty-sdk' {
declare module 'twenty-sdk/application' {
export type SyncableEntityOptions = { universalIdentifier: string };
type ApplicationVariable = SyncableEntityOptions & {
@@ -58,7 +58,7 @@ declare module 'twenty-sdk' {
type ServerlessFunctionTrigger = SyncableEntityOptions &
(RouteTrigger | CronTrigger | DatabaseEventTrigger);
export type FunctionConfig = SyncableEntityOptions & {
export type ServerlessFunctionConfig = SyncableEntityOptions & {
name?: string;
description?: string;
timeoutSeconds?: number;
@@ -93,13 +93,13 @@ declare module 'twenty-sdk' {
`;
const serverlessFunctionMock = `
import { type FunctionConfig } from 'twenty-sdk';
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
export const main = async (params: any): Promise<any> => {
return {};
}
export const config: FunctionConfig = {
export const config: ServerlessFunctionConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'hello',
timeoutSeconds: 2,
@@ -129,7 +129,7 @@ const objectMock = `import {
BaseObjectMetadata,
FieldMetadata,
FieldMetadataType
} from 'twenty-sdk';
} from 'twenty-sdk/application';
enum PostCardStatus {
DRAFT = 'DRAFT',
@@ -215,97 +215,19 @@ export class PostCard extends BaseObjectMetadata {
}
`;
const packageJsonMock = {
name: 'my-app',
version: '0.0.1',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'create-entity': 'twenty app add',
dev: 'twenty app dev',
generate: 'twenty app generate',
sync: 'twenty app sync',
uninstall: 'twenty app uninstall',
auth: 'twenty auth login',
},
dependencies: {
'twenty-sdk': '0.1.0',
},
devDependencies: {
'@types/node': '^24.7.2',
typescript: '^5.9.3',
},
};
const tsConfigJsonMock = {
compileOnSave: false,
compilerOptions: {
sourceMap: true,
declaration: true,
outDir: './dist',
rootDir: '.',
moduleResolution: 'node',
allowSyntheticDefaultImports: true,
emitDecoratorMetadata: true,
experimentalDecorators: true,
importHelpers: true,
allowUnreachableCode: false,
strictNullChecks: true,
alwaysStrict: true,
noImplicitAny: true,
strictBindCallApply: false,
target: 'es2018',
module: 'esnext',
lib: ['es2020', 'dom'],
skipLibCheck: true,
skipDefaultLibCheck: true,
resolveJsonModule: true,
},
exclude: ['node_modules', 'dist', '**/*.test.ts', '**/*.spec.ts'],
};
const yarnLockMock = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
const applicationConfigMock = `import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = {
universalIdentifier: '${v4()}',
displayName: 'My App',
description: 'My app description',
};
export default config;
`;
describe('loadManifest (integration)', () => {
const appDirectory = join(tmpdir(), 'test-app');
const appName = 'my-app';
const appDisplayName = 'My App';
const appDescription = 'My app description';
const appDirectory = join(tmpdir(), 'twenty-manifest-');
beforeEach(async () => {
await ensureDirSync(appDirectory);
write(appDirectory, 'yarn.lock', yarnLockMock);
write(appDirectory, 'application.config.ts', applicationConfigMock);
write(
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
'tsconfig.json',
JSON.stringify(tsConfigJsonMock, null, 2),
);
write(
appDirectory,
'package.json',
JSON.stringify(packageJsonMock, null, 2),
);
});
write(appDirectory, 'src/Account.ts', objectMock);
@@ -336,11 +258,10 @@ describe('loadManifest (integration)', () => {
expect(packageJson.name).toBe('my-app');
expect(packageJson.version).toBe('0.0.1');
expect(packageJson.license).toBe('MIT');
expect(yarnLock).toContain(
'# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
);
expect(yarnLock).toContain('# This file is generated by running ');
// application
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { universalIdentifier: _, ...otherInfo } = manifest.application;
expect(otherInfo).toEqual({
displayName: 'My App',
@@ -350,6 +271,7 @@ describe('loadManifest (integration)', () => {
expect(manifest.objects.length).toBe(1);
for (const object of manifest.objects) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { universalIdentifier: _, fields, ...otherInfo } = object;
expect(otherInfo).toEqual({
description: ' A post card object',
@@ -419,7 +341,9 @@ describe('loadManifest (integration)', () => {
// serverless functions
for (const serverlessFunction of manifest.serverlessFunctions) {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
universalIdentifier: _,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handlerPath: __,
triggers,
...otherInfo
@@ -432,6 +356,7 @@ describe('loadManifest (integration)', () => {
});
for (const trigger of triggers) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { universalIdentifier: _, ...otherInfo } = trigger;
switch (trigger.type) {
case 'route':
@@ -0,0 +1,107 @@
import * as fs from 'fs-extra';
import { BASE_APPLICATION_PROJECT_PATH } from '../constants/constants-path';
import { writeJsoncFile } from '../utils/jsonc-parser';
import { join } from 'path';
import path from 'path';
import { v4 } from 'uuid';
export const copyBaseApplicationProject = async ({
appName,
appDisplayName,
appDescription,
appDirectory,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}) => {
await fs.copy(BASE_APPLICATION_PROJECT_PATH, appDirectory);
await fs.rename(
join(appDirectory, 'gitignore'),
join(appDirectory, '.gitignore'),
);
await fs.copy(join(appDirectory, '.env.example'), join(appDirectory, '.env'));
await createBasePackageJson({
appName,
appDirectory,
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
appDirectory,
});
await createReadmeContent({
displayName: appDisplayName,
appDescription,
appDirectory,
});
};
const createApplicationConfig = async ({
displayName,
description,
appDirectory,
}: {
displayName: string;
description?: string;
appDirectory: string;
}) => {
const content = `import { type ApplicationConfig } from 'twenty-sdk/application';
const config: ApplicationConfig = {
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
};
export default config;
`;
await fs.writeFile(path.join(appDirectory, 'application.config.ts'), content);
};
const createBasePackageJson = async ({
appName,
appDirectory,
}: {
appName: string;
appDirectory: string;
}) => {
const base = JSON.parse(await readBaseApplicationProjectFile('package.json'));
base['universalIdentifier'] = v4();
base['name'] = appName;
await writeJsoncFile(join(appDirectory, 'package.json'), base);
};
const createReadmeContent = async ({
displayName,
appDescription,
appDirectory,
}: {
displayName: string;
appDescription: string;
appDirectory: string;
}) => {
let readmeContent = await readBaseApplicationProjectFile('README.md');
readmeContent = readmeContent.replace(/\{title}/g, displayName);
readmeContent = readmeContent.replace(/\{description}/g, appDescription);
await fs.writeFile(path.join(appDirectory, 'README.md'), readmeContent);
};
const readBaseApplicationProjectFile = async (fileName: string) => {
return await fs.readFile(
join(BASE_APPLICATION_PROJECT_PATH, fileName),
'utf-8',
);
};
@@ -1,13 +1,9 @@
import {
type Diagnostic,
formatDiagnosticsWithColorAndContext,
sys,
} from 'typescript';
import ts, { formatDiagnosticsWithColorAndContext, sys } from 'typescript';
export const formatAndWarnTsDiagnostics = ({
diagnostics,
}: {
diagnostics: Diagnostic[];
diagnostics: ts.Diagnostic[];
}) => {
if (diagnostics.length > 0) {
const formattedDiagnostics = formatDiagnosticsWithColorAndContext(
@@ -15,7 +15,7 @@ export const getObjectDecoratedClass = ({
const className = camelCaseName[0].toUpperCase() + camelCaseName.slice(1);
return `import { Object } from 'twenty-sdk';
return `import { Object } from 'twenty-sdk/application';
@Object({
${decoratorOptions}
@@ -1,7 +1,7 @@
import kebabCase from 'lodash.kebabcase';
import { v4 } from 'uuid';
export const getFunctionBaseFile = ({
export const getServerlessFunctionBaseFile = ({
name,
universalIdentifier = v4(),
}: {
@@ -10,7 +10,7 @@ export const getFunctionBaseFile = ({
}) => {
const kebabCaseName = kebabCase(name);
return `import { type FunctionConfig } from 'twenty-sdk';
return `import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
export const main = async (params: {
a: string;
@@ -25,7 +25,7 @@ export const main = async (params: {
return { message };
};
export const config: FunctionConfig = {
export const config: ServerlessFunctionConfig = {
universalIdentifier: '${universalIdentifier}',
name: '${kebabCaseName}',
timeoutSeconds: 5,
@@ -1,3 +1,4 @@
import ts from 'typescript';
import { join } from 'path';
import {
createProgram,
@@ -5,8 +6,6 @@ import {
parseJsonConfigFileContent,
readConfigFile,
sys,
type Program,
type Diagnostic,
} from 'typescript';
const getProgramFromTsconfig = ({
@@ -42,7 +41,7 @@ export const getTsProgramAndDiagnostics = async ({
appPath,
}: {
appPath: string;
}): Promise<{ program: Program; diagnostics: Diagnostic[] }> => {
}): Promise<{ program: ts.Program; diagnostics: ts.Diagnostic[] }> => {
const program = getProgramFromTsconfig({
appPath,
tsconfigPath: 'tsconfig.json',
@@ -1,5 +1,5 @@
import * as fs from 'fs-extra';
import { type ParseError, parse as parseJsonc } from 'jsonc-parser';
import { ParseError, parse as parseJsonc } from 'jsonc-parser';
export interface JsoncParseOptions {
allowTrailingComma?: boolean;
@@ -1,15 +1,15 @@
import * as fs from 'fs-extra';
import { posix, relative, sep } from 'path';
import {
type Decorator,
type Expression,
type FunctionDeclaration,
type Modifier,
type Node,
type Program,
type SourceFile,
Decorator,
Expression,
FunctionDeclaration,
Modifier,
Node,
Program,
SourceFile,
SyntaxKind,
type VariableDeclaration,
VariableDeclaration,
forEachChild,
getDecorators,
isArrayLiteralExpression,
@@ -34,13 +34,13 @@ import {
} from 'typescript';
import { GENERATED_FOLDER_NAME } from '../services/generate.service';
import {
type AppManifest,
type Application,
type FieldMetadata,
type ObjectManifest,
type PackageJson,
type ServerlessFunctionManifest,
type Sources,
AppManifest,
Application,
FieldMetadata,
ObjectManifest,
PackageJson,
ServerlessFunctionManifest,
Sources,
} from '../types/config.types';
import { findPathFile } from '../utils/find-path-file';
import { getTsProgramAndDiagnostics } from '../utils/get-ts-program-and-diagnostics';
@@ -210,7 +210,7 @@ const hasExportModifier = (st: any) =>
/**
* Finds (and validates) the new serverless file shape:
* - exactly 2 exported bindings
* - one must be `config` (typed FunctionConfig)
* - one must be `config` (typed ServerlessFunctionConfig)
* - the other must be a function (exported function declaration, or const initialized with arrow/function expression)
*/
const findHandlerAndConfig = (
@@ -290,13 +290,13 @@ const findHandlerAndConfig = (
`"config" in ${sf.fileName} must be initialized to an object literal.`,
);
}
// (Light) type guard: ensure declared type mentions FunctionConfig if present
// (Light) type guard: ensure declared type mentions ServerlessFunctionConfig if present
const maybeVarDecl = configExport.declNode as VariableDeclaration;
if ('type' in maybeVarDecl && maybeVarDecl.type) {
const typeText = maybeVarDecl.type.getText(sf);
if (!/\bFunctionConfig\b/.test(typeText)) {
if (!/\bServerlessFunctionConfig\b/.test(typeText)) {
throw new Error(
`"config" in ${sf.fileName} must be typed as FunctionConfig (got: ${typeText}).`,
`"config" in ${sf.fileName} must be typed as ServerlessFunctionConfig (got: ${typeText}).`,
);
}
}
@@ -1,23 +1,31 @@
{
"extends": "./tsconfig.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"outDir": "./dist-e2e",
"rootDir": "./src",
"strict": true,
"module": "commonjs",
"target": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"sourceMap": true
"declarationMap": false,
"sourceMap": true,
"types": ["jest", "node"]
},
"include": ["src"],
"include": [
"src/**/*",
"src/**/__tests__/**/*.e2e-spec.ts"
],
"exclude": [
"node_modules",
"dist",
"dist-e2e",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**"
"**/*.spec.ts"
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"composite": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.e2e-spec.ts", "**/__tests__/**"]
}
@@ -1,24 +1,21 @@
{
"extends": "./tsconfig.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"module": "commonjs",
"target": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src"],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.e2e-spec.ts",
"**/__tests__/**"
]
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.e2e-spec.ts", "**/__tests__/**"]
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": [
"src/**/*",
"src/**/__tests__/**/*.spec.ts"
],
"exclude": [
"node_modules",
"dist",
]
}
@@ -39,12 +39,6 @@ Deploy Twenty on Railway with the community maintained template below.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/nAL3hA)
### Twenty on Sealos
Deploy Twenty on Sealos with the community maintained template below.
[![Deploy on Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/twenty)
## Others
Please feel free to Open a PR to add more Cloud Provider options.
@@ -39,12 +39,6 @@ crwdns60692:0crwdne60692:0
crwdns60694:0https://railway.com/button.svgcrwdnd60694:0https://railway.com/deploy/nAL3hAcrwdne60694:0
### crwdns64878:0crwdne64878:0
crwdns64880:0crwdne64880:0
crwdns64882:0https://sealos.io/Deploy-on-Sealos.svgcrwdnd64882:0https://sealos.io/products/app-store/twentycrwdne64882:0
## crwdns60696:0crwdne60696:0
crwdns60698:0crwdne60698:0
@@ -39,12 +39,6 @@ Stel Twenty op Railway in met die gemeenskap-onderhoudene sjabloon hieronder.
[![Stel op Railway](https://railway.com/button.svg)](https://railway.com/deploy/nAL3hA)
### Ander
Stel Twenty op Sealos in met die deur die gemeenskap onderhoue sjabloon hieronder.
[![Stel op Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/twenty)
## Ander
Voel vry om 'n PR te open om meer Wolkverskaffer opsies by te voeg.
@@ -57,7 +57,7 @@ sectionInfo: أتمتة العمليات والاندماج مع الأدوات
**المشكلة**: الوصول إلى حد 100 سير عمل متزامن لكل مساحة عمل.
<Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning>لا يمكنك تشغيل أكثر من 100 سير عمل بالتوازي في أي وقت لكل مساحة عمل.</Warning></Warning>
<Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning>لا يمكنك تشغيل أكثر من 100 سير عمل بالتوازي في أي وقت لكل مساحة عمل.</Warning></Warning>
**حلول**:
@@ -39,12 +39,6 @@ Nasazení Twenty na Railway s komunitně udržovanou šablonou níže.
[![Nasadit na Railway](https://railway.com/button.svg)](https://railway.com/deploy/nAL3hA)
### Ostatní
Nasazení Twenty na Sealos s komunitně udržovanou šablonou níže.
[![Nasadit na Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/twenty)
## Ostatní
Neváhejte otevřít PR pro přidání dalších možností poskytovatele cloudových služeb.
@@ -39,12 +39,6 @@ Implementer Twenty på Railway med den fællesskabsvedligeholdte skabelon nedenf
[![Implementer på Railway](https://railway.com/button.svg)](https://railway.com/deploy/nAL3hA)
### Twenty på Sealos
Implementer Twenty på Sealos med den fællesskabsvedligeholdte skabelon nedenfor.
[![Implementer på Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/twenty)
## Andre
Du er velkommen til at åbne en PR for at tilføje flere Cloud Provider-muligheder.
@@ -39,12 +39,6 @@ image: /images/user-guide/notes/notes_header.png
[![Ανάπτυξη στο Railway](https://railway.com/button.svg)](https://railway.com/deploy/nAL3hA)
### Twenty στο Sealos
Αναπτύξτε το Twenty στο Sealos με το υποστηριζόμενο από την κοινότητα πρότυπο παρακάτω.
[![Ανάπτυξη στο Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/twenty)
## Άλλες επιλογές
Παρακαλώ μη διστάσετε να ανοίξετε ένα PR για να προσθέσετε περισσότερες επιλογές Παρόχου Cloud.
@@ -18,7 +18,7 @@ Absolutamente. Puedes crear un nuevo espacio de trabajo haciendo clic en el men
<Accordion title="I accidentally created multiple workspaces but only need one. What should I do?"><Accordion title="How do I change my workspace appearance and regional settings?">Ve a `Configuración → Experiencia` para personalizar:
<Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning>No elimines tu cuenta (accesible en Configuración → Configuración de perfil): tu cuenta se comparte entre los diferentes espacios de trabajo.</Warning></Warning>
<Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning>No elimines tu cuenta (accesible en Configuración → Configuración de perfil): tu cuenta se comparte entre los diferentes espacios de trabajo.</Warning></Warning>
</Accordion>
<Accordion title="How can I disable my workspace?"><Accordion title="How can I disable my workspace?"><Accordion title="How can I disable my workspace?"><Accordion title="How can I disable my workspace?"><Accordion title="How can I disable my workspace?">Si solo deseas desactivar tu espacio de trabajo (no eliminarlo), ve a `Configuración → Facturación` y haz clic en `Cancelar plan`.</Accordion></Accordion>
@@ -57,7 +57,7 @@ Los formularios están diseñados actualmente solo para disparadores manuales. P
**Problema**: Se alcanza el límite de 100 flujos de trabajo concurrentes por espacio de trabajo.
<Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning>No puedes ejecutar más de 100 flujos de trabajo en paralelo en cualquier momento por espacio de trabajo.</Warning></Warning>
<Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning><Warning>No puedes ejecutar más de 100 flujos de trabajo en paralelo en cualquier momento por espacio de trabajo.</Warning></Warning>
**Soluciones**:

Some files were not shown because too many files have changed in this diff Show More