Compare commits

...
Author SHA1 Message Date
Thomas Trompette ccdaa31273 WIP¨ 2026-03-20 15:25:35 +01:00
Thomas Trompette f4f7ccd0e1 Improve workflow metrics with faillure reason 2026-03-19 15:30:54 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
0d27a255a9 chore(deps): bump nodemailer from 7.0.11 to 7.0.13 (#18754)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.11
to 7.0.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/releases">nodemailer's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.13</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2>v7.0.12</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md">nodemailer's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/893119505aa25723dd9a7d90c8dfd223d28a0cc4"><code>8931195</code></a>
chore(master): release 7.0.13 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1798">#1798</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/9398d633d2c1bf5bf08a0e258ff17d5d7a45f0e6"><code>9398d63</code></a>
Bumped deps</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f"><code>4c041db</code></a>
fix: downgrade transient connection error logs to warn level</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/a208a0bc86a315d037d5be8849fab5862c488baa"><code>a208a0b</code></a>
chore(master): release 7.0.12 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1785">#1785</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84"><code>053ce6a</code></a>
fix: added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b"><code>adf8611</code></a>
fix: use 8bit encoding for message/rfc822 attachments</li>
<li>See full diff in <a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.13">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nodemailer&package-manager=npm_and_yarn&previous-version=7.0.11&new-version=7.0.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 09:22:17 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ef2a113a16 chore(deps): bump @dagrejs/dagre from 1.1.3 to 1.1.8 (#18753)
Bumps [@dagrejs/dagre](https://github.com/dagrejs/dagre) from 1.1.3 to
1.1.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dagrejs/dagre/commit/7e4d15f191678f7f05f3c86d9071a193230e7e00"><code>7e4d15f</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/d3908e2c13148c9143db585accc10ae0b6634657"><code>d3908e2</code></a>
Bumping version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/ce295f8e073c4fe96c9e36ecf08ae2940e5e6a10"><code>ce295f8</code></a>
Build for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/b64b9057726eee17f24f73579ba0668527276448"><code>b64b905</code></a>
Bumping the version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/de169d24c13d06c1e9c560f4f4f8f98650109b94"><code>de169d2</code></a>
Merge pull request <a
href="https://redirect.github.com/dagrejs/dagre/issues/481">#481</a>
from Nathan-Fenner/nf/improve-network-simplex-perform...</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/065e0d8374f4c1c35a7cb4b84df37aaa31598d86"><code>065e0d8</code></a>
improve performance of graph node ranking</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/00d3178d671e49de9c032e3abd281dc9f2739e73"><code>00d3178</code></a>
Typo</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/3982a69d2b323b06aa969a4ec09829d37fe6e7bd"><code>3982a69</code></a>
Bump version and set as pre-release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/1339f5516508dba0cbcc4ef1c0587e7384bec23d"><code>1339f55</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/9459f01bc815f16b87db727821d8401acbad2cd3"><code>9459f01</code></a>
Bumping the version</li>
<li>Additional commits viewable in <a
href="https://github.com/dagrejs/dagre/compare/v1.1.3...v1.1.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@dagrejs/dagre&package-manager=npm_and_yarn&previous-version=1.1.3&new-version=1.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:11:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0e25eac2b chore(deps): bump @ai-sdk/mistral from 3.0.20 to 3.0.25 (#18755)
Bumps [@ai-sdk/mistral](https://github.com/vercel/ai) from 3.0.20 to
3.0.25.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/ai/commit/4c1613ac8ff9f638da3a25fa6a1aa40362eb0e7c"><code>4c1613a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13131">#13131</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/64ac0fdd80da66d1e920a8a6309ebe05b9d686ad"><code>64ac0fd</code></a>
Backport: fix(security): validate redirect targets in download functions
to p...</li>
<li><a
href="https://github.com/vercel/ai/commit/f622bf85c114f810b00dd2825b56e044bce578a7"><code>f622bf8</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13126">#13126</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/e2a59ef9273ef2ad97c8aec5bf5c0a7b9e8c436c"><code>e2a59ef</code></a>
Backport: fix(provider/google): preserve groundingMetadata when streamed
befo...</li>
<li><a
href="https://github.com/vercel/ai/commit/ebf43a6765e6fa337e8d5b6cf065625a1794c21c"><code>ebf43a6</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13120">#13120</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/258900473214256af40b035b66f15362cbc53287"><code>2589004</code></a>
Backport: feat(openai): add GPT-5.4 model support (<a
href="https://redirect.github.com/vercel/ai/issues/13117">#13117</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/d23121fd71036611e0142a9233de4d1a1f5d54c0"><code>d23121f</code></a>
Backport: chore(ai): add optional ChatRequestOptions to
`addToolApprovalRespo...</li>
<li><a
href="https://github.com/vercel/ai/commit/55a2acf625c78d5c68ac2abc02bc26c3cd0ffa6e"><code>55a2acf</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13101">#13101</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/45d71c381b756e03310f81bdd322aa7460cb5a15"><code>45d71c3</code></a>
Backport: fix(google): use VALIDATED function calling mode when any tool
has ...</li>
<li><a
href="https://github.com/vercel/ai/commit/ee92dc766422fffde3bae06437510296c4e860a8"><code>ee92dc7</code></a>
ci(release): add <code>--tag ai-v6</code> to <code>ci:release</code>
script (<a
href="https://redirect.github.com/vercel/ai/issues/13096">#13096</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/mistral@3.0.20...@ai-sdk/mistral@3.0.25">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@ai-sdk/mistral&package-manager=npm_and_yarn&previous-version=3.0.20&new-version=3.0.25)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:10:58 +00:00
Charles BochetandGitHub 8ab8f80687 fix: use unique concurrency group per merge queue entry (#18756)
## Summary

- Fixes merge queue PRs blocking each other by changing the concurrency
group in `ci-merge-queue.yaml`
- The old concurrency group used `merge_group.base_ref` which resolves
to `refs/heads/main` for every PR, causing all merge queue entries to
serialize behind a single concurrency slot
- Now uses `github.ref` (unique per entry:
`refs/heads/gh-readonly-queue/main/pr-NUMBER-SHA`), matching what all
other CI workflows already do

## Recommended ruleset changes (in GitHub Settings > Rules > Rulesets >
"CI Status Checks")

- **Grouping strategy**: Switch `ALLGREEN` to `NONE` -- each PR is still
tested against the correct base (including all PRs ahead of it in the
queue), but failures only affect the failing PR instead of ejecting the
entire group. `max_entries_to_build: 5` still allows parallel
speculative testing.
- **`min_entries_to_merge_wait_minutes`**: Reduce from 5 to 1 -- the
5-minute wait adds unnecessary latency to every merge.

## Test plan

- [ ] Enqueue 2+ PRs in the merge queue and verify both trigger e2e
tests in parallel instead of one blocking the other
2026-03-19 10:10:47 +01:00
a370a26b79 i18n - docs translations (#18749)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-19 09:36:37 +01:00
martmullandGitHub 2f095c8903 Scaffold light twenty app dev container (#18734)
as title
2026-03-18 20:10:54 +01:00
65 changed files with 1314 additions and 456 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
.git
.env
node_modules
**/node_modules
.nx/cache
packages/twenty-server/.env
+2 -2
View File
@@ -10,8 +10,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
e2e-test:
+27 -12
View File
@@ -25,25 +25,25 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
## 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)
- Docker (for the local Twenty dev server)
## Quick start
```bash
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Get help and list all available commands
yarn twenty help
# The scaffolder can automatically:
# 1. Start a local Twenty server (Docker)
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
# 3. Authenticate your app via OAuth
# Authenticate with your Twenty server
yarn twenty remote add --local
# Add a new entity to your application (guided)
yarn twenty add
# Or do it manually:
yarn twenty server start # Start local Twenty server
yarn twenty remote add --local # Authenticate via OAuth
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
yarn twenty dev
# Watch your application's function logs
@@ -107,10 +107,24 @@ npx create-twenty-app@latest my-app -m
- `skills/example-skill.ts` — Example AI agent skill definition
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## Local server
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
```bash
yarn twenty server start # Start (pulls image if needed)
yarn twenty server status # Check if it's healthy
yarn twenty server logs # Stream logs
yarn twenty server stop # Stop (data is preserved)
yarn twenty server reset # Wipe all data and start fresh
```
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
@@ -153,8 +167,9 @@ Our team reviews contributions for quality, security, and reusability before mer
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty remote add --local` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty dev` is running — it autogenerates the typed client.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.8.0-canary.0",
"version": "0.8.0-canary.1",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -1,60 +1,11 @@
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
Start development mode — it auto-connects to your local Twenty server at localhost:3000:
```bash
yarn twenty dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & authentication
yarn twenty remote add --local # Connect to local Twenty server
yarn twenty remote add <url> # Connect to a remote server (OAuth)
yarn twenty remote list # List all configured remotes
yarn twenty remote switch # Switch default remote
yarn twenty remote status # Check auth status
yarn twenty remote remove <name> # Remove a remote
# Development
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty build # Build the application
yarn twenty deploy # Deploy to a Twenty server
yarn twenty publish # Publish to npm
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty exec # Execute a function with JSON payload
yarn twenty logs # Stream function logs
yarn twenty uninstall # Uninstall app from server
```
## Integration Tests
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
```bash
# Make sure a Twenty server is running at http://localhost:3000
yarn test
```
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
Run `yarn twenty help` to list all available commands.
## 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!
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)
@@ -46,6 +46,7 @@ export class CreateAppCommand {
await fs.ensureDir(appDirectory);
console.log(chalk.gray(' Scaffolding project files...'));
await copyBaseApplicationProject({
appName,
appDisplayName,
@@ -54,29 +55,20 @@ export class CreateAppCommand {
exampleOptions,
});
console.log(chalk.gray(' Installing dependencies...'));
await install(appDirectory);
console.log(chalk.gray(' Initializing git repository...'));
await tryGitInit(appDirectory);
let localResult: LocalInstanceResult = { running: false };
if (!options.skipLocalInstance) {
const { needsLocalInstance } = await inquirer.prompt([
{
type: 'confirm',
name: 'needsLocalInstance',
message:
'Do you need a local instance of Twenty? Recommended if you not have one already.',
default: true,
},
]);
// Auto-detect a running server first
localResult = await setupLocalInstance(appDirectory);
if (needsLocalInstance) {
localResult = await setupLocalInstance();
}
if (isDefined(localResult.apiKey)) {
this.runAuthLogin(appDirectory, localResult.apiKey);
if (localResult.running && localResult.serverUrl) {
await this.connectToLocal(appDirectory, localResult.serverUrl);
}
}
@@ -201,23 +193,29 @@ export class CreateAppCommand {
appDirectory: string;
appName: string;
}): void {
console.log(chalk.blue('🎯 Creating Twenty Application'));
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
console.log(chalk.gray(`📝 Name: ${appName}`));
console.log(chalk.blue('Creating Twenty Application'));
console.log(chalk.gray(` Directory: ${appDirectory}`));
console.log(chalk.gray(` Name: ${appName}`));
console.log('');
}
private runAuthLogin(appDirectory: string, apiKey: string): void {
private async connectToLocal(
appDirectory: string,
serverUrl: string,
): Promise<void> {
try {
execSync(
`yarn twenty auth:login --api-key "${apiKey}" --api-url http://localhost:3000`,
{ cwd: appDirectory, stdio: 'inherit' },
`npx nx run twenty-sdk:start -- remote add ${serverUrl} --as local`,
{
cwd: appDirectory,
stdio: 'inherit',
},
);
console.log(chalk.green('Authenticated with local Twenty instance.'));
console.log(chalk.green('Authenticated with local Twenty instance.'));
} catch {
console.log(
chalk.yellow(
'⚠️ Auto auth:login failed. Run `yarn twenty auth:login` manually.',
'Authentication skipped. Run `npx nx run twenty-sdk:start -- remote add --local` manually.',
),
);
}
@@ -229,27 +227,21 @@ export class CreateAppCommand {
): void {
const dirName = appDirectory.split('/').reverse()[0] ?? '';
console.log(chalk.green('Application created!'));
console.log(chalk.green('Application created!'));
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(chalk.gray(` cd ${dirName}`));
if (localResult.apiKey) {
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else if (localResult.running) {
if (!localResult.running) {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
}
console.log(
chalk.gray(' yarn twenty dev # Start dev mode'),
);
}
}
@@ -1,194 +1,101 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { execSync } from 'node:child_process';
import { isDefined } from 'twenty-shared/utils';
import { platform } from 'node:os';
const INSTALL_SCRIPT_URL =
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh';
const DEFAULT_PORT = 2020;
const SERVER_CONTAINER = 'twenty-server-1';
const DB_CONTAINER = 'twenty-db-1';
// Minimal health check — the full implementation lives in twenty-sdk
const isServerReady = async (port: number): Promise<boolean> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const isDockerAvailable = (): boolean => {
try {
execSync('docker compose version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isDockerRunning = (): boolean => {
try {
execSync('docker info', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isTwentyServerRunning = async (): Promise<boolean> => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch('http://localhost:3000/healthz', {
const response = await fetch(`http://localhost:${port}/healthz`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
const body = await response.json();
return body.status === 'ok';
} catch {
return false;
}
};
const getActiveWorkspaceId = (): string | null => {
try {
const result = execSync(
`docker exec ${DB_CONTAINER} psql -U postgres -d default -t -c "SELECT id FROM core.workspace WHERE \\"activationStatus\\" = 'ACTIVE' LIMIT 1"`,
{ encoding: 'utf-8' },
).trim();
return result || null;
} catch {
return null;
}
};
const generateApiKeyToken = (workspaceId: string): string | null => {
try {
const output = execSync(
`docker exec -e NODE_ENV=development ${SERVER_CONTAINER} yarn command:prod workspace:generate-api-key -w ${workspaceId}`,
{ encoding: 'utf-8' },
);
const TOKEN_PREFIX = 'TOKEN:';
const tokenLine = output
.trim()
.split('\n')
.find((line) => line.includes(TOKEN_PREFIX));
if (!tokenLine) {
return null;
}
const tokenStartIndex =
tokenLine.indexOf(TOKEN_PREFIX) + TOKEN_PREFIX.length;
return tokenLine.slice(tokenStartIndex).trim();
} catch {
return null;
} finally {
clearTimeout(timeoutId);
}
};
export type LocalInstanceResult = {
running: boolean;
apiKey?: string;
serverUrl?: string;
};
export const setupLocalInstance = async (): Promise<LocalInstanceResult> => {
export const setupLocalInstance = async (
appDirectory: string,
): Promise<LocalInstanceResult> => {
console.log('');
console.log(chalk.blue('🐳 Setting up local Twenty instance...'));
console.log(chalk.blue('Setting up local Twenty instance...'));
if (await isTwentyServerRunning()) {
console.log(
chalk.green('✅ Twenty server is already running on localhost:3000.'),
);
} else {
if (!isDockerAvailable()) {
console.log(
chalk.yellow(
'⚠️ Docker Compose is not installed. Please install Docker first.',
),
);
console.log(chalk.gray(' See https://docs.docker.com/get-docker/'));
if (await isServerReady(DEFAULT_PORT)) {
const serverUrl = `http://localhost:${DEFAULT_PORT}`;
return { running: false };
}
console.log(chalk.green(`Twenty server detected on ${serverUrl}.`));
if (!isDockerRunning()) {
return { running: true, serverUrl };
}
// Delegate to `twenty server start` from the scaffolded app
console.log(chalk.gray('Starting local Twenty server...'));
try {
execSync('yarn twenty server start', {
cwd: appDirectory,
stdio: 'inherit',
});
} catch {
console.log(
chalk.yellow(
'Failed to start Twenty server. Run `yarn twenty server start` manually.',
),
);
return { running: false };
}
console.log(chalk.gray('Waiting for Twenty to be ready...'));
const startTime = Date.now();
const timeoutMs = 180 * 1000;
while (Date.now() - startTime < timeoutMs) {
if (await isServerReady(DEFAULT_PORT)) {
const serverUrl = `http://localhost:${DEFAULT_PORT}`;
console.log(chalk.green(`Twenty server is running on ${serverUrl}.`));
console.log(
chalk.yellow(
'⚠️ Docker is not running. Please start Docker and try again.',
chalk.gray(
'Workspace ready — login with tim@apple.dev / tim@apple.dev',
),
);
return { running: false };
}
const openCommand = platform() === 'darwin' ? 'open' : 'xdg-open';
try {
execSync(`bash <(curl -sL ${INSTALL_SCRIPT_URL})`, {
stdio: 'inherit',
shell: '/bin/bash',
});
} catch {
console.log(
chalk.yellow('⚠️ Local instance setup did not complete successfully.'),
);
try {
execSync(`${openCommand} ${serverUrl}`, { stdio: 'ignore' });
} catch {
// Ignore if browser can't be opened
}
return { running: false };
return { running: true, serverUrl };
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
console.log('');
console.log(
chalk.blue(
'👉 Please create your workspace in the browser before continuing.',
chalk.yellow(
'Twenty server did not become healthy in time. Check: yarn twenty server logs',
),
);
const { workspaceCreated } = await inquirer.prompt([
{
type: 'confirm',
name: 'workspaceCreated',
message: 'Have you finished creating your workspace?',
default: true,
},
]);
if (!workspaceCreated) {
console.log(
chalk.yellow(
'⚠️ Skipping API key generation. Run `yarn twenty remote add --local` manually after creating your workspace.',
),
);
return { running: true };
}
console.log(chalk.blue('🔑 Generating API key for your workspace...'));
const workspaceId = getActiveWorkspaceId();
if (!isDefined(workspaceId)) {
console.log(
chalk.yellow(
'⚠️ No active workspace found. Make sure you completed the signup flow, then run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
const apiKey = generateApiKeyToken(workspaceId);
if (!isDefined(apiKey)) {
console.log(
chalk.yellow(
'⚠️ Could not generate API key. Run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
console.log(chalk.green('✅ API key generated for your workspace.'));
return { running: true, apiKey };
return { running: false };
};
@@ -93,7 +93,7 @@ import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
const assertServerIsReachable = async () => {
@@ -0,0 +1,130 @@
ARG APP_VERSION
# === Stage 1: Common dependencies ===
FROM node:22-alpine AS common-deps
WORKDIR /app
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
RUN yarn && yarn cache clean && npx nx reset
# === Stage 2: Build server from source ===
FROM common-deps AS twenty-server-build
COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:build
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
# === Stage 3: Build frontend from source ===
# Requires ~10GB Docker memory. To skip, pre-build on host first:
# npx nx build twenty-front
# The COPY below will pick up packages/twenty-front/build/ if it exists.
FROM common-deps AS twenty-front-build
COPY --from=twenty-server-build /app/package.json /tmp/.build-sentinel
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
RUN if [ -d /app/packages/twenty-front/build ]; then \
echo "Using pre-built frontend from host"; \
else \
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
fi
# === Stage 4: s6-overlay ===
FROM alpine:3.20 AS s6-fetch
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
else echo "x86_64" > /tmp/s6arch; fi
RUN S6_ARCH=$(cat /tmp/s6arch) && \
wget -O /tmp/s6-overlay-noarch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
wget -O /tmp/s6-overlay-arch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
cd /tmp && \
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
# === Stage 5: Final all-in-one image ===
FROM node:22-alpine
# s6-overlay
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
&& rm /tmp/s6-overlay-*.tar.xz
# Infrastructure: Postgres, Redis, and utilities
RUN apk add --no-cache \
postgresql16 postgresql16-contrib \
redis \
curl jq su-exec
# tsx for database setup scripts
RUN npm install -g tsx
# Twenty application (both server and frontend built from source)
COPY --from=twenty-server-build /app /app
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# s6 service definitions
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
# Data directories
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
# Tell s6-overlay to preserve container environment variables
ENV S6_KEEP_ENV=1
# Environment defaults
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
SERVER_URL=http://localhost:2020 \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=3000 \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
EXPOSE 3000
VOLUME ["/data/postgres", "/app/.local-storage"]
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
ENTRYPOINT ["/init"]
@@ -0,0 +1 @@
/bin/sh /etc/s6-overlay/scripts/init-db.sh
@@ -0,0 +1,13 @@
#!/bin/sh
# Initialize PostgreSQL data directory if empty
if [ ! -f /data/postgres/PG_VERSION ]; then
echo "Initializing PostgreSQL data directory..."
su-exec postgres initdb -D /data/postgres --auth=trust --encoding=UTF8
# Allow local connections without password
echo "host all all 127.0.0.1/32 trust" >> /data/postgres/pg_hba.conf
echo "host all all ::1/128 trust" >> /data/postgres/pg_hba.conf
fi
exec su-exec postgres postgres -D /data/postgres \
-c listen_addresses=localhost \
-c unix_socket_directories=/tmp
@@ -0,0 +1,6 @@
#!/bin/sh
exec redis-server \
--dir /data/redis \
--maxmemory-policy noeviction \
--bind 127.0.0.1 \
--protected-mode yes
@@ -0,0 +1,3 @@
#!/bin/sh
cd /app/packages/twenty-server
exec yarn start:prod
@@ -0,0 +1,3 @@
#!/bin/sh
cd /app/packages/twenty-server
exec yarn worker:prod
@@ -0,0 +1,56 @@
#!/bin/sh
set -e
# Wait for PostgreSQL to be ready (timeout after 60s)
echo "Waiting for PostgreSQL..."
TRIES=0
until su-exec postgres pg_isready -h localhost; do
TRIES=$((TRIES + 1))
if [ "$TRIES" -ge 120 ]; then
echo "ERROR: PostgreSQL did not become ready within 60s"
exit 1
fi
sleep 0.5
done
echo "PostgreSQL is ready."
# Create role if it doesn't exist
su-exec postgres psql -h localhost -tc \
"SELECT 1 FROM pg_roles WHERE rolname='twenty'" | grep -q 1 \
|| su-exec postgres psql -h localhost -c "CREATE ROLE twenty WITH LOGIN PASSWORD 'twenty' SUPERUSER"
# Create database if it doesn't exist
su-exec postgres psql -h localhost -tc \
"SELECT 1 FROM pg_database WHERE datname='default'" | grep -q 1 \
|| su-exec postgres createdb -h localhost -O twenty default
# Run Twenty database setup and migrations
cd /app/packages/twenty-server
has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running initial setup..."
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
fi
# Always run migrations (idempotent — skips already-applied ones)
yarn database:migrate:prod
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod cache:flush
# Only seed on first boot — check if the dev workspace already exists
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
if [ "$has_workspace" = "f" ]; then
echo "Seeding app dev data..."
yarn command:prod workspace:seed:dev --light || true
else
echo "Dev workspace already seeded, skipping."
fi
echo "Database initialization complete."
@@ -20,19 +20,51 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
## Prerequisites
- Node.js 24+ and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
- Docker (for the local Twenty dev server)
## Getting Started
Create a new app using the official scaffolder, then authenticate and start developing:
Create a new app using the official scaffolder. It can automatically start a local Twenty instance for you:
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
yarn twenty dev
```
### Local Server Management
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
The local server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`), so you can start developing immediately without any manual setup.
### Authentication
Connect your app to the local server using OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
The scaffolder supports two modes for controlling which example files are included:
@@ -21,19 +21,51 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
* Docker (لخادم تطوير Twenty المحلي)
## البدء
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
أنشئ تطبيقًا جديدًا باستخدام المولّد الرسمي. يمكنه بدء مثيل محلي من Twenty تلقائيًا لك:
```bash filename="Terminal"
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
# إنشاء تطبيق جديد — ستعرض واجهة سطر الأوامر خيار بدء خادم Twenty محلي
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
yarn twenty app:dev
yarn twenty dev
```
### إدارة الخادم المحلي
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
```bash filename="Terminal"
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
yarn twenty server start
# تحقّق من حالة الخادم
yarn twenty server status
# بثّ سجلات الخادم
yarn twenty server logs
# أوقف الخادم
yarn twenty server stop
# أعد ضبط جميع البيانات وابدأ من جديد
yarn twenty server reset
```
يأتي الخادم المحلي مهيأً مسبقًا بمساحة عمل ومستخدم (`tim@apple.dev` / `tim@apple.dev`)، بحيث يمكنك البدء في التطوير فورًا دون أي إعداد يدوي.
### المصادقة
وصّل تطبيقك بالخادم المحلي باستخدام OAuth:
```bash filename="Terminal"
# المصادقة عبر OAuth (يفتح المتصفح)
yarn twenty remote add --local
```
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
@@ -21,19 +21,51 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
* Docker (für den lokalen Twenty-Dev-Server)
## Erste Schritte
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
Erstelle eine neue App mit dem offiziellen Scaffolder. Der Scaffolder kann für dich automatisch eine lokale Twenty-Instanz starten:
```bash filename="Terminal"
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
# Eine neue App erstellen — die CLI bietet an, einen lokalen Twenty-Server zu starten
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
yarn twenty app:dev
yarn twenty dev
```
### Lokale Serververwaltung
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
```bash filename="Terminal"
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
yarn twenty server start
# Serverstatus prüfen
yarn twenty server status
# Serverprotokolle streamen
yarn twenty server logs
# Server stoppen
yarn twenty server stop
# Alle Daten zurücksetzen und neu starten
yarn twenty server reset
```
Der lokale Server ist bereits mit einem Arbeitsbereich und einem Benutzer (`tim@apple.dev` / `tim@apple.dev`) vorbefüllt, sodass Sie ohne manuelle Einrichtung sofort mit der Entwicklung beginnen können.
### Authentifizierung
Verbinden Sie Ihre App mithilfe von OAuth mit dem lokalen Server:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
@@ -21,19 +21,51 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
## Prerequisiti
* Node.js 24+ e Yarn 4
* Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks)
* Docker (per il server di sviluppo locale di Twenty)
## Per iniziare
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
Crea una nuova app utilizzando lo scaffolder ufficiale. Può avviare automaticamente un'istanza locale di Twenty per te:
```bash filename="Terminal"
# Crea lo scaffold di una nuova app (include tutti gli esempi per impostazione predefinita)
# Crea lo scaffold di una nuova app — la CLI offrirà di avviare un server locale di Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
yarn twenty app:dev
yarn twenty dev
```
### Gestione del server locale
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
```bash filename="Terminal"
# Avvia il server locale (scarica l'immagine se necessario)
yarn twenty server start
# Verifica lo stato del server
yarn twenty server status
# Segui i log del server
yarn twenty server logs
# Arresta il server
yarn twenty server stop
# Reimposta tutti i dati e riparti da zero
yarn twenty server reset
```
Il server locale è preconfigurato con uno spazio di lavoro e un utente (`tim@apple.dev` / `tim@apple.dev`), così puoi iniziare a sviluppare immediatamente senza alcuna configurazione manuale.
### Autenticazione
Collega la tua app al server locale tramite OAuth:
```bash filename="Terminal"
# Autenticati tramite OAuth (apre il browser)
yarn twenty remote add --local
```
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
@@ -21,19 +21,51 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
## Pré-requisitos
* Node.js 24+ e Yarn 4
* Um espaço de trabalho do Twenty e uma chave de API (crie uma em https://app.twenty.com/settings/api-webhooks)
* Docker (para o servidor de desenvolvimento local do Twenty)
## Primeiros passos
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
Crie um novo app usando o gerador oficial de estrutura. Ele pode iniciar automaticamente uma instância local do Twenty para você:
```bash filename="Terminal"
# Criar a estrutura de um novo app (inclui todos os exemplos por padrão)
# Criar a estrutura de um novo app — a CLI oferecerá iniciar um servidor local do Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
yarn twenty app:dev
yarn twenty dev
```
### Gerenciamento do Servidor Local
O SDK inclui comandos para gerenciar um servidor de desenvolvimento local do Twenty (imagem Docker all-in-one com PostgreSQL, Redis, servidor e worker):
```bash filename="Terminal"
# Iniciar o servidor local (faz pull da imagem se necessário)
yarn twenty server start
# Verificar o status do servidor
yarn twenty server status
# Transmitir os logs do servidor
yarn twenty server logs
# Parar o servidor
yarn twenty server stop
# Redefinir todos os dados e começar do zero
yarn twenty server reset
```
O servidor local já vem pré-configurado com um espaço de trabalho e um usuário (`tim@apple.dev` / `tim@apple.dev`), para que você possa começar a desenvolver imediatamente, sem qualquer configuração manual.
### Autenticação
Conecte seu aplicativo ao servidor local usando OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
@@ -21,19 +21,51 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
## Cerințe
* Node.js 24+ și Yarn 4
* Un spațiu de lucru Twenty și o cheie API (creați una la https://app.twenty.com/settings/api-webhooks)
* Docker (pentru serverul local de dezvoltare Twenty)
## Începeți
Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă și începeți să dezvoltați:
Creează o aplicație nouă folosind generatorul oficial. Poate porni automat o instanță Twenty locală pentru tine:
```bash filename="Terminal"
# Creează scheletul unei aplicații noi (include toate exemplele în mod implicit)
# Creează scheletul unei aplicații noi — CLI-ul îți va oferi opțiunea de a porni un server Twenty local
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Pornește modul de dezvoltare: sincronizează automat modificările locale cu spațiul tău de lucru
yarn twenty app:dev
yarn twenty dev
```
### Gestionarea serverului local
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker):
```bash filename="Terminal"
# Pornește serverul local (descarcă imaginea dacă este necesar)
yarn twenty server start
# Verifică starea serverului
yarn twenty server status
# Afișează în timp real jurnalele serverului
yarn twenty server logs
# Oprește serverul
yarn twenty server stop
# Resetează toate datele și pornește de la zero
yarn twenty server reset
```
Serverul local vine preconfigurat cu un spațiu de lucru și un utilizator (`tim@apple.dev` / `tim@apple.dev`), astfel încât să poți începe să dezvolți imediat, fără nicio configurare manuală.
### Autentificare
Conectează-ți aplicația la serverul local folosind OAuth:
```bash filename="Terminal"
# Autentifică-te prin OAuth (se deschide browserul)
yarn twenty remote add --local
```
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
@@ -21,19 +21,51 @@ description: Создавайте и управляйте настройками
## Требования
* Node.js 24+ и Yarn 4
* Рабочее пространство Twenty и ключ API (создайте его на https://app.twenty.com/settings/api-webhooks)
* Docker (для локального сервера разработки Twenty)
## Начало работы
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
Создайте новое приложение с помощью официального генератора каркаса. Он может автоматически запустить локальный экземпляр Twenty:
```bash filename="Terminal"
# Создать каркас нового приложения (по умолчанию включает все примеры)
# Создать каркас нового приложения — CLI предложит запустить локальный сервер Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
yarn twenty app:dev
yarn twenty dev
```
### Управление локальным сервером
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером):
```bash filename="Terminal"
# Запустить локальный сервер (при необходимости будет загружен образ)
yarn twenty server start
# Проверить статус сервера
yarn twenty server status
# Просмотр логов сервера в реальном времени
yarn twenty server logs
# Остановить сервер
yarn twenty server stop
# Сбросить все данные и начать с нуля
yarn twenty server reset
```
Локальный сервер уже содержит рабочее пространство и пользователя (`tim@apple.dev` / `tim@apple.dev`), так что вы можете сразу начать разработку без какой-либо ручной настройки.
### Аутентификация
Подключите своё приложение к локальному серверу с помощью OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
@@ -21,19 +21,51 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
## Ön Gereksinimler
* Node.js 24+ ve Yarn 4
* Bir Twenty çalışma alanı ve bir API anahtarı (https://app.twenty.com/settings/api-webhooks adresinde oluşturun)
* Docker (yerel Twenty geliştirme sunucusu için)
## Başlarken
Resmi scaffolder aracını kullanarak yeni bir uygulama oluşturun, ardından kimlik doğrulaması yapıp geliştirmeye başlayın:
Resmi iskelet oluşturucusunu kullanarak yeni bir uygulama oluşturun. Sizin için otomatik olarak yerel bir Twenty örneğini başlatabilir:
```bash filename="Terminal"
# Yeni bir uygulamanın iskeletini oluşturun (varsayılan olarak tüm örnekleri içerir)
# Yeni bir uygulamanın iskeletini oluşturun — CLI yerel bir Twenty sunucusunu başlatmayı önerecektir
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Geliştirme modunu başlatın: yerel değişiklikleri otomatik olarak çalışma alanınızla senkronize eder
yarn twenty app:dev
# Geliştirme modunu başlatın: yerel değişiklikleri çalışma alanınızla otomatik olarak senkronize eder
yarn twenty dev
```
### Yerel Sunucu Yönetimi
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı):
```bash filename="Terminal"
# Yerel sunucuyu başlatın (gerekirse imajı indirir)
yarn twenty server start
# Sunucu durumunu kontrol edin
yarn twenty server status
# Sunucu günlüklerini akış olarak görüntüleyin
yarn twenty server logs
# Sunucuyu durdurun
yarn twenty server stop
# Tüm verileri sıfırlayın ve temiz bir başlangıç yapın
yarn twenty server reset
```
Yerel sunucu, bir çalışma alanı ve kullanıcıyla (`tim@apple.dev` / `tim@apple.dev`) önceden yapılandırılmış olarak gelir; böylece herhangi bir manuel kurulum gerektirmeden hemen geliştirmeye başlayabilirsiniz.
### Kimlik Doğrulama
Uygulamanızı OAuth kullanarak yerel sunucuya bağlayın:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
@@ -21,19 +21,51 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
## 先决条件
* Node.js 24+ 和 Yarn 4
* 一个 Twenty 工作空间和一个 API 密钥(在 https://app.twenty.com/settings/api-webhooks 创建)
* Docker (用于本地 Twenty 开发服务器)
## 开始使用
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发
使用官方脚手架创建一个新应用。 它可以为你自动启动一个本地 Twenty 实例
```bash filename="Terminal"
# 创建一个新应用脚手架(默认包含所有示例)
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# 启动开发模式:会将本地更改自动同步到你的工作区
yarn twenty app:dev
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
### 本地服务器管理
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
本地服务器预置了一个工作区和用户 (`tim@apple.dev` / `tim@apple.dev`),因此你可以无需任何手动设置即可立即开始开发。
### 身份验证
使用 OAuth 将你的应用连接到本地服务器:
```bash filename="Terminal"
# 通过 OAuth 进行身份验证 (将打开浏览器)
yarn twenty remote add --local
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
+40 -4
View File
@@ -25,7 +25,7 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key. Generate one at https://app.twenty.com/settings/api-webhooks
- Docker (for the local Twenty dev server) or a remote Twenty workspace
## Installation
@@ -55,6 +55,7 @@ Commands:
typecheck [appPath] Run TypeScript type checking on the application
uninstall [appPath] Uninstall application from Twenty
remote Manage remote Twenty servers
server Manage a local Twenty server instance
add [entityType] Add a new entity to your application
exec [appPath] Execute a logic function with a JSON payload
logs [appPath] Watch application function logs
@@ -69,6 +70,41 @@ In a scaffolded project (via `create-twenty-app`), use `yarn twenty <command>` i
## Commands
### Server
Manage a local Twenty dev server (all-in-one Docker image).
- `twenty server start` — Start the local server (pulls image if needed).
- Options:
- `-p, --port <port>`: HTTP port (default: `2020`).
- `twenty server stop` — Stop the local server.
- `twenty server logs` — Stream server logs.
- Options:
- `-n, --lines <lines>`: Number of initial lines to show (default: `50`).
- `twenty server status` — Show server status (running/stopped/healthy).
- `twenty server reset` — Delete all data and start fresh.
The server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
Examples:
```bash
# Start the local server
twenty server start
# Check if it's ready
twenty server status
# Follow logs during first startup
twenty server logs
# Stop the server (data is preserved)
twenty server stop
# Wipe everything and start over
twenty server reset
```
### Remote
Manage remote server connections and authentication.
@@ -79,7 +115,7 @@ Manage remote server connections and authentication.
- `--token <token>`: API key for non-interactive auth.
- `--url <url>`: Server URL (alternative to positional arg).
- `--as <name>`: Name for this remote (otherwise derived from URL hostname).
- `--local`: Connect to local development server (`http://localhost:3000`).
- `--local`: Connect to local development server (`http://localhost:2020`) via OAuth.
- Behavior: If `nameOrUrl` matches an existing remote name, re-authenticates it. Otherwise, creates a new remote and authenticates via OAuth (with API key fallback).
- `twenty remote remove <name>` — Remove a remote and its credentials.
@@ -270,7 +306,7 @@ Example configuration file:
"defaultRemote": "production",
"remotes": {
"local": {
"apiUrl": "http://localhost:3000",
"apiUrl": "http://localhost:2020",
"apiKey": "<your-api-key>"
},
"production": {
@@ -285,7 +321,7 @@ Example configuration file:
Notes:
- If a remote is missing, `apiUrl` defaults to `http://localhost:3000`.
- If a remote is missing, `apiUrl` defaults to `http://localhost:2020`.
- `twenty remote add` writes credentials for the active remote (OAuth tokens or API key).
- `twenty remote add --as my-remote` saves under a custom name.
- `twenty remote switch` sets the `defaultRemote` field, used when `-r` is not specified.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.8.0-canary.0",
"version": "0.8.0-canary.1",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/sdk/index.d.ts",
@@ -1 +1 @@
export const SERVER_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
export const SERVER_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
@@ -11,6 +11,7 @@ import { LogicFunctionExecuteCommand } from './exec';
import { LogicFunctionLogsCommand } from './logs';
import { EntityAddCommand } from './add';
import { registerRemoteCommands } from './remote';
import { registerServerCommands } from './server';
import { SyncableEntity } from 'twenty-shared/application';
export const registerCommands = (program: Command): void => {
@@ -92,6 +93,7 @@ export const registerCommands = (program: Command): void => {
});
registerRemoteCommands(program);
registerServerCommands(program);
program
.command('add [entityType]')
+15 -16
View File
@@ -2,6 +2,7 @@ import { authLogin } from '@/cli/operations/login';
import { authLoginOAuth } from '@/cli/operations/login-oauth';
import { ApiService } from '@/cli/utilities/api/api-service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { detectLocalServer } from '@/cli/utilities/server/detect-local-server';
import chalk from 'chalk';
import type { Command } from 'commander';
import inquirer from 'inquirer';
@@ -86,23 +87,21 @@ export const registerRemoteCommands = (program: Command): void => {
if (options.local) {
const remoteName = options.as ?? 'local';
const token =
options.token ??
(
await inquirer.prompt<{ apiKey: string }>([
{
type: 'password',
name: 'apiKey',
message: 'API Key for local server:',
mask: '*',
validate: (input: string) =>
input.length > 0 || 'API key is required',
},
])
).apiKey;
const localUrl = await detectLocalServer();
if (!localUrl) {
console.error(
chalk.red(
'No local Twenty server found on ports 2020 or 3000.\n' +
'Start one with: yarn twenty server start',
),
);
process.exit(1);
}
console.log(chalk.gray(`Found server at ${localUrl}`));
ConfigService.setActiveRemote(remoteName);
await authenticate('http://localhost:3000', token);
await authenticate(localUrl, options.token);
console.log(chalk.green(`✓ Authenticated remote "${remoteName}".`));
return;
@@ -127,7 +126,7 @@ export const registerRemoteCommands = (program: Command): void => {
nameOrUrl ??
options.url ??
(options.token
? 'http://localhost:3000'
? ((await detectLocalServer()) ?? 'http://localhost:2020')
: (
await inquirer.prompt<{ apiUrl: string }>([
{
@@ -0,0 +1,243 @@
import { checkServerHealth } from '@/cli/utilities/server/detect-local-server';
import chalk from 'chalk';
import type { Command } from 'commander';
import { execSync, spawnSync } from 'node:child_process';
const CONTAINER_NAME = 'twenty-app-dev';
const IMAGE = 'twentycrm/twenty-app-dev:latest';
const DEFAULT_PORT = 2020;
const isContainerRunning = (): boolean => {
try {
const result = execSync(
`docker inspect -f '{{.State.Running}}' ${CONTAINER_NAME}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
).trim();
return result === 'true';
} catch {
return false;
}
};
const getContainerPort = (): number => {
try {
const result = execSync(
`docker inspect -f '{{(index (index .NetworkSettings.Ports "3000/tcp") 0).HostPort}}' ${CONTAINER_NAME}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
).trim();
return parseInt(result, 10) || DEFAULT_PORT;
} catch {
return DEFAULT_PORT;
}
};
const containerExists = (): boolean => {
try {
execSync(`docker inspect ${CONTAINER_NAME}`, {
stdio: ['pipe', 'pipe', 'ignore'],
});
return true;
} catch {
return false;
}
};
const validatePort = (value: string): number => {
const port = parseInt(value, 10);
if (isNaN(port) || port < 1 || port > 65535) {
console.error(chalk.red('Invalid port number.'));
process.exit(1);
}
return port;
};
export const registerServerCommands = (program: Command): void => {
const server = program
.command('server')
.description('Manage a local Twenty server instance');
server
.command('start')
.description('Start a local Twenty server')
.option('-p, --port <port>', 'HTTP port', String(DEFAULT_PORT))
.action(async (options: { port: string }) => {
let port = validatePort(options.port);
if (await checkServerHealth(port)) {
console.log(
chalk.green(`Twenty server is already running on localhost:${port}.`),
);
return;
}
if (isContainerRunning()) {
console.log(chalk.gray('Container is running but not healthy yet.'));
return;
}
if (containerExists()) {
const existingPort = getContainerPort();
if (existingPort !== port) {
console.log(
chalk.yellow(
`Existing container uses port ${existingPort}. Run 'yarn twenty server reset' first to change ports.`,
),
);
}
console.log(chalk.gray('Starting existing container...'));
execSync(`docker start ${CONTAINER_NAME}`, { stdio: 'ignore' });
port = existingPort;
} else {
try {
execSync('docker info', { stdio: 'ignore' });
} catch {
console.error(
chalk.red(
'Docker is not running. Please start Docker and try again.',
),
);
process.exit(1);
}
console.log(chalk.gray(`Pulling ${IMAGE}...`));
try {
execSync(`docker pull ${IMAGE}`, { stdio: 'inherit' });
} catch {
console.log(chalk.gray('Pull failed, trying local image...'));
}
console.log(chalk.gray('Starting Twenty container...'));
const runResult = spawnSync(
'docker',
[
'run',
'-d',
'--name',
CONTAINER_NAME,
'-p',
`${port}:3000`,
'-v',
'twenty-app-dev-data:/data/postgres',
'-v',
'twenty-app-dev-storage:/app/.local-storage',
IMAGE,
],
{ stdio: 'inherit' },
);
if (runResult.status !== 0) {
console.error(chalk.red('Failed to start Twenty container.'));
process.exit(runResult.status ?? 1);
}
}
console.log(
chalk.green(`Twenty server starting on http://localhost:${port}`),
);
console.log(
chalk.gray('Run `yarn twenty server logs` to follow startup progress.'),
);
});
server
.command('stop')
.description('Stop the local Twenty server')
.action(() => {
if (!containerExists()) {
console.log(chalk.yellow('No Twenty server container found.'));
return;
}
execSync(`docker stop ${CONTAINER_NAME}`, { stdio: 'ignore' });
console.log(chalk.green('Twenty server stopped.'));
});
server
.command('logs')
.description('Stream Twenty server logs')
.option('-n, --lines <lines>', 'Number of lines to show', '50')
.action((options: { lines: string }) => {
if (!containerExists()) {
console.log(chalk.yellow('No Twenty server container found.'));
return;
}
try {
spawnSync(
'docker',
['logs', '-f', '--tail', options.lines, CONTAINER_NAME],
{ stdio: 'inherit' },
);
} catch {
// User hit Ctrl-C
}
});
server
.command('status')
.description('Show Twenty server status')
.action(async () => {
if (!containerExists()) {
console.log(` Status: ${chalk.gray('not created')}`);
console.log(
chalk.gray(" Run 'yarn twenty server start' to create one."),
);
return;
}
const running = isContainerRunning();
const port = running ? getContainerPort() : DEFAULT_PORT;
const healthy = running ? await checkServerHealth(port) : false;
const statusText = healthy
? chalk.green('running (healthy)')
: running
? chalk.yellow('running (starting...)')
: chalk.gray('stopped');
console.log(` Status: ${statusText}`);
console.log(` URL: http://localhost:${port}`);
if (healthy) {
console.log(chalk.gray(' Login: tim@apple.dev / tim@apple.dev'));
}
});
server
.command('reset')
.description('Delete all data and start fresh')
.action(() => {
if (containerExists()) {
execSync(`docker rm -f ${CONTAINER_NAME}`, { stdio: 'ignore' });
}
try {
execSync(
'docker volume rm twenty-app-dev-data twenty-app-dev-storage',
{
stdio: 'ignore',
},
);
} catch {
// Volumes may not exist
}
console.log(chalk.green('Twenty server data reset.'));
console.log(
chalk.gray("Run 'yarn twenty server start' to start a fresh instance."),
);
});
};
@@ -196,7 +196,7 @@ export class ConfigService {
private getDefaultConfig(): RemoteConfig {
return {
apiUrl: 'http://localhost:3000',
apiUrl: 'http://localhost:2020',
};
}
@@ -217,6 +217,7 @@ export class DevModeOrchestrator {
message: 'Failed to create development application',
status: 'error',
},
{ message: JSON.stringify(createResult, null, 2), status: 'error' },
]);
this.state.updatePipeline({ status: 'error' });
@@ -36,12 +36,11 @@ export class CheckServerOrchestratorStep {
this.state.applyStepEvents([
{
message:
'Cannot reach Twenty at localhost:3000.\n\n' +
' Start a local server with Docker:\n' +
' curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml -o docker-compose.yml\n' +
' docker compose up -d\n\n' +
' Or from the monorepo:\n' +
' yarn start\n\n' +
'Cannot reach Twenty server.\n\n' +
' Start a local server:\n' +
' yarn twenty server start\n\n' +
' Check server status:\n' +
' yarn twenty server status\n\n' +
' Waiting for server...',
status: 'error',
},
@@ -0,0 +1,30 @@
const LOCAL_PORTS = [2020, 3000];
export const checkServerHealth = async (port: number): Promise<boolean> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
try {
const response = await fetch(`http://localhost:${port}/healthz`, {
signal: controller.signal,
});
const body = await response.json();
return body.status === 'ok';
} catch {
return false;
} finally {
clearTimeout(timeoutId);
}
};
export const detectLocalServer = async (): Promise<string | null> => {
for (const port of LOCAL_PORTS) {
if (await checkServerHealth(port)) {
return `http://localhost:${port}`;
}
}
return null;
};
@@ -1,32 +1,52 @@
import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { Command, CommandRunner, Option } from 'nest-commander';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
SeededWorkspacesIds,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
type DataSeedWorkspaceOptions = {
light?: boolean;
};
@Command({
name: 'workspace:seed:dev',
description:
'Seed workspace with initial data. This command is intended for development only.',
})
export class DataSeedWorkspaceCommand extends CommandRunner {
workspaceIds = [
SEED_APPLE_WORKSPACE_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
] as const;
private readonly logger = new Logger(DataSeedWorkspaceCommand.name);
constructor(private readonly devSeederService: DevSeederService) {
super();
}
async run(): Promise<void> {
@Option({
flags: '--light',
description:
'Light seed: skip demo custom objects (Pet, Survey, etc.) and limit records to 5 per object',
})
parseLight(): boolean {
return true;
}
async run(
_passedParams: string[],
options: DataSeedWorkspaceOptions,
): Promise<void> {
const workspaceIds: SeededWorkspacesIds[] = options.light
? [SEED_APPLE_WORKSPACE_ID]
: [SEED_APPLE_WORKSPACE_ID, SEED_YCOMBINATOR_WORKSPACE_ID];
try {
for (const workspaceId of this.workspaceIds) {
await this.devSeederService.seedDev(workspaceId);
for (const workspaceId of workspaceIds) {
await this.devSeederService.seedDev(workspaceId, {
light: options.light,
});
}
} catch (error) {
this.logger.error(error);
@@ -59,6 +59,12 @@ export const handler = async (event) => {
const builtCode = await fs.readFile(outFilePath, 'utf-8');
return { builtCode };
} catch (error) {
return {
success: false,
category: 'USER_ERROR',
errorMessage: error.message,
};
} finally {
await fs.rm(workDir, { recursive: true, force: true });
}
@@ -118,10 +118,17 @@ export const handler = async (event) => {
await copyYarnEngine(nodejsDir);
await runYarnInstall(nodejsDir);
await createZip(buildDir, zipPath);
await uploadToPresignedUrl(zipPath, presignedUploadUrl);
return { success: true };
} catch (error) {
const isYarnInstallError = error.message?.startsWith('yarn install failed');
return {
success: false,
category: isYarnInstallError ? 'USER_ERROR' : 'INFRA_ERROR',
errorMessage: error.message,
};
} finally {
await fs.rm(buildDir, { recursive: true, force: true });
await fs.rm(zipPath, { force: true });
@@ -81,6 +81,8 @@ type LambdaDriverExecutorPayload = {
handlerName: string;
};
export type LambdaErrorCategory = 'USER_ERROR' | 'INFRA_ERROR';
export type YarnInstallLambdaPayload = {
action: 'createLayer';
packageJson: string;
@@ -88,9 +90,13 @@ export type YarnInstallLambdaPayload = {
presignedUploadUrl: string;
};
export type YarnInstallLambdaResult = {
success: boolean;
};
export type YarnInstallLambdaResult =
| { success: true }
| {
success: false;
category: LambdaErrorCategory;
errorMessage: string;
};
export type BuilderLambdaPayload = {
action: 'transpile';
@@ -99,9 +105,13 @@ export type BuilderLambdaPayload = {
builtFileName: string;
};
export type BuilderLambdaResult = {
builtCode: string;
};
export type BuilderLambdaResult =
| { builtCode: string }
| {
success: false;
category: LambdaErrorCategory;
errorMessage: string;
};
export interface LambdaDriverOptions extends LambdaClientConfig {
logicFunctionResourceService: LogicFunctionResourceService;
@@ -395,10 +405,18 @@ export class LambdaDriver implements LogicFunctionDriver {
const parsedResult: YarnInstallLambdaResult = result.Payload
? JSON.parse(result.Payload.transformToString())
: {};
: { success: false, category: 'INFRA_ERROR', errorMessage: 'Empty payload' };
if (!parsedResult.success) {
throw new Error('Yarn install Lambda did not report success');
const exceptionCode =
parsedResult.category === 'USER_ERROR'
? LogicFunctionExceptionCode.LOGIC_FUNCTION_BUILD_USER_ERROR
: LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED;
throw new LogicFunctionException(
`Yarn install Lambda failed: ${parsedResult.errorMessage}`,
exceptionCode,
);
}
return parsedResult;
@@ -496,7 +514,19 @@ export class LambdaDriver implements LogicFunctionDriver {
? JSON.parse(result.Payload.transformToString())
: {};
if (!parsedResult.builtCode) {
if ('success' in parsedResult && parsedResult.success === false) {
const exceptionCode =
parsedResult.category === 'USER_ERROR'
? LogicFunctionExceptionCode.LOGIC_FUNCTION_BUILD_USER_ERROR
: LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED;
throw new LogicFunctionException(
`Builder Lambda failed: ${parsedResult.errorMessage}`,
exceptionCode,
);
}
if (!('builtCode' in parsedResult) || !parsedResult.builtCode) {
throw new Error('Builder Lambda did not return builtCode');
}
@@ -18,7 +18,8 @@ export enum MetricsKeys {
WorkflowRunStartedWebhookTrigger = 'workflow-run/started/webhook-trigger',
WorkflowRunStartedManualTrigger = 'workflow-run/started/manual-trigger',
WorkflowRunCompleted = 'workflow-run/completed',
WorkflowRunFailed = 'workflow-run/failed',
WorkflowRunFailedUserError = 'workflow-run/failed/user-error',
WorkflowRunFailedSystemError = 'workflow-run/failed/system-error',
WorkflowRunStopped = 'workflow-run/stopped',
WorkflowRunThrottled = 'workflow-run/throttled',
WorkflowRunFailedToEnqueue = 'workflow-run/failed/to-enqueue',
@@ -12,6 +12,7 @@ export enum LogicFunctionExceptionCode {
LOGIC_FUNCTION_CODE_UNCHANGED = 'LOGIC_FUNCTION_CODE_UNCHANGED',
LOGIC_FUNCTION_EXECUTION_LIMIT_REACHED = 'LOGIC_FUNCTION_EXECUTION_LIMIT_REACHED',
LOGIC_FUNCTION_CREATE_FAILED = 'LOGIC_FUNCTION_CREATE_FAILED',
LOGIC_FUNCTION_BUILD_USER_ERROR = 'LOGIC_FUNCTION_BUILD_USER_ERROR',
LOGIC_FUNCTION_EXECUTION_TIMEOUT = 'LOGIC_FUNCTION_EXECUTION_TIMEOUT',
LOGIC_FUNCTION_DISABLED = 'LOGIC_FUNCTION_DISABLED',
LOGIC_FUNCTION_INVALID_SEED_PROJECT = 'LOGIC_FUNCTION_INVALID_SEED_PROJECT',
@@ -35,6 +36,8 @@ const getLogicFunctionExceptionUserFriendlyMessage = (
return msg`Function execution limit reached.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED:
return msg`Failed to create function.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_BUILD_USER_ERROR:
return msg`Function build failed due to invalid code or dependencies.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_TIMEOUT:
return msg`Function execution timed out.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED:
@@ -46,10 +46,12 @@ export class DevSeederPermissionsService {
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
workspaceId,
light = false,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
workspaceCustomFlatApplication: FlatApplication;
light?: boolean;
}) {
const adminRole = await this.roleRepository.findOne({
where: {
@@ -80,35 +82,49 @@ export class DevSeederPermissionsService {
let guestUserWorkspaceId: string | undefined;
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.JANE;
limitedUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JONY,
...Object.values(RANDOM_USER_WORKSPACE_IDS),
];
guestUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.PHIL;
if (light) {
// In light mode, Tim is admin (prefilled login user needs full
// access for SDK development). No demo permission roles needed.
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JANE,
USER_WORKSPACE_DATA_SEED_IDS.JONY,
USER_WORKSPACE_DATA_SEED_IDS.PHIL,
...Object.values(RANDOM_USER_WORKSPACE_IDS),
];
} else {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.JANE;
limitedUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
guestUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.PHIL;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JONY,
...Object.values(RANDOM_USER_WORKSPACE_IDS),
];
const guestRole = await this.roleService.createGuestRole({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
const guestRole = await this.roleService.createGuestRole({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [guestUserWorkspaceId],
roleId: guestRole.id,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [guestUserWorkspaceId],
roleId: guestRole.id,
});
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
// The limited role restricts access to Pet and Rocket objects,
// which are only created in full (non-light) mode
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [limitedUserWorkspaceId],
roleId: limitedRole.id,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [limitedUserWorkspaceId],
roleId: limitedRole.id,
});
}
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM_ACME;
memberUserWorkspaceIds = [
@@ -300,10 +300,12 @@ export class DevSeederDataService {
schemaName,
workspaceId,
featureFlags,
light = false,
}: {
schemaName: string;
workspaceId: string;
featureFlags?: Record<FeatureFlagKey, boolean>;
light?: boolean;
}) {
const objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
@@ -328,19 +330,22 @@ export class DevSeederDataService {
attachmentSeeds,
featureFlags,
objectMetadataItems,
light,
});
await this.timelineActivitySeederService.seedTimelineActivities({
entityManager,
schemaName,
workspaceId,
});
if (!light) {
await this.timelineActivitySeederService.seedTimelineActivities({
entityManager,
schemaName,
workspaceId,
});
await this.seedAttachmentFiles(
workspaceId,
entityManager,
attachmentFileMeta,
);
await this.seedAttachmentFiles(
workspaceId,
entityManager,
attachmentFileMeta,
);
}
await prefillWorkflows(
entityManager,
@@ -359,6 +364,7 @@ export class DevSeederDataService {
attachmentSeeds,
featureFlags,
objectMetadataItems,
light = false,
}: {
entityManager: WorkspaceEntityManager;
schemaName: string;
@@ -366,6 +372,7 @@ export class DevSeederDataService {
attachmentSeeds: RecordSeedConfig['recordSeeds'];
featureFlags?: Record<FeatureFlagKey, boolean>;
objectMetadataItems: FlatObjectMetadata[];
light?: boolean;
}) {
const batches = getRecordSeedsBatches(
workspaceId,
@@ -378,6 +385,10 @@ export class DevSeederDataService {
for (const batch of batches) {
await Promise.all(
batch.map(async (recordSeedsConfig) => {
if (light && recordSeedsConfig.tableName.startsWith('_')) {
return;
}
const objectMetadata = objectMetadataItems.find(
(item) =>
computeTableName(item.nameSingular, item.isCustom) ===
@@ -51,6 +51,14 @@ type JunctionConfigSeed = {
label?: string;
};
type WorkspaceSeedConfig = {
objects: { seed: ObjectMetadataSeed; fields?: FieldMetadataSeed[] }[];
fields: { objectName: string; seeds: FieldMetadataSeed[] }[];
morphRelations?: { objectName: string; seeds: MorphRelationSeed[] }[];
junctionFields?: JunctionFieldSeed[];
junctionConfigs?: JunctionConfigSeed[];
};
type FlatMaps = {
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
@@ -65,18 +73,7 @@ export class DevSeederMetadataService {
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
private readonly workspaceConfigs: Record<
string,
{
objects: { seed: ObjectMetadataSeed; fields?: FieldMetadataSeed[] }[];
fields: { objectName: string; seeds: FieldMetadataSeed[] }[];
morphRelations?: { objectName: string; seeds: MorphRelationSeed[] }[];
// Junction fields create relations to junction objects (inverses auto-created)
junctionFields?: JunctionFieldSeed[];
// Configure junction settings on fields after all relations exist
junctionConfigs?: JunctionConfigSeed[];
}
> = {
private readonly workspaceConfigs: Record<string, WorkspaceSeedConfig> = {
[SEED_APPLE_WORKSPACE_ID]: {
objects: [
{ seed: ROCKET_CUSTOM_OBJECT_SEED },
@@ -178,13 +175,14 @@ export class DevSeederMetadataService {
},
};
public async seed({
dataSourceMetadata,
workspaceId,
}: {
dataSourceMetadata: DataSourceEntity;
workspaceId: string;
}) {
private getLightConfig(config: WorkspaceSeedConfig): WorkspaceSeedConfig {
return {
objects: [],
fields: [],
};
}
private getConfig(workspaceId: string, light: boolean): WorkspaceSeedConfig {
const config = this.workspaceConfigs[workspaceId];
if (!config) {
@@ -193,6 +191,20 @@ export class DevSeederMetadataService {
);
}
return light ? this.getLightConfig(config) : config;
}
public async seed({
dataSourceMetadata,
workspaceId,
light = false,
}: {
dataSourceMetadata: DataSourceEntity;
workspaceId: string;
light?: boolean;
}) {
const config = this.getConfig(workspaceId, light);
for (const obj of config.objects) {
await this.seedCustomObject({
dataSourceId: dataSourceMetadata.id,
@@ -266,14 +278,14 @@ export class DevSeederMetadataService {
});
}
public async seedRelations({ workspaceId }: { workspaceId: string }) {
const config = this.workspaceConfigs[workspaceId];
if (!config) {
throw new Error(
`Workspace configuration not found for workspaceId: ${workspaceId}`,
);
}
public async seedRelations({
workspaceId,
light = false,
}: {
workspaceId: string;
light?: boolean;
}) {
const config = this.getConfig(workspaceId, light);
// 1. Seed morph relations (creates inverses on target objects)
let maps = await this.getFreshFlatMaps(workspaceId);
@@ -287,7 +299,6 @@ export class DevSeederMetadataService {
}
// 2. Seed junction fields (creates relations + inverses on junction objects)
// Use same maps for all - matches original working behavior
maps = await this.getFreshFlatMaps(workspaceId);
for (const field of config.junctionFields ?? []) {
@@ -43,7 +43,11 @@ export class DevSeederService {
private readonly coreDataSource: DataSource,
) {}
public async seedDev(workspaceId: SeededWorkspacesIds): Promise<void> {
public async seedDev(
workspaceId: SeededWorkspacesIds,
options?: { light?: boolean },
): Promise<void> {
const light = options?.light ?? false;
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
const appVersion = this.twentyConfigService.get('APP_VERSION');
@@ -89,16 +93,19 @@ export class DevSeederService {
await this.devSeederMetadataService.seed({
dataSourceMetadata,
workspaceId,
light,
});
await this.devSeederMetadataService.seedRelations({
workspaceId,
light,
});
await this.devSeederPermissionsService.initPermissions({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
light,
});
await seedPageLayouts(
@@ -148,6 +155,7 @@ export class DevSeederService {
schemaName: dataSourceMetadata.schema,
workspaceId,
featureFlags: featureFlagsMap,
light,
});
await this.workspaceCacheStorageService.flush(workspaceId, undefined);
@@ -1,6 +1,9 @@
import { type WorkflowFailureReason } from 'src/modules/workflow/workflow-executor/types/workflow-failure-reason.type';
export type WorkflowActionOutput = {
result?: object;
error?: string;
failureReason?: WorkflowFailureReason;
pendingEvent?: boolean;
shouldEndWorkflowRun?: boolean;
shouldRemainRunning?: boolean;
@@ -0,0 +1 @@
export type WorkflowFailureReason = 'USER_ERROR' | 'SYSTEM_ERROR';
@@ -5,6 +5,10 @@ import { resolveInput } from 'twenty-shared/utils';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
@@ -46,16 +50,31 @@ export class CodeWorkflowAction implements WorkflowAction {
const { workspaceId } = runInfo;
const result = await this.logicFunctionExecutorService.execute({
logicFunctionId: workflowActionInput.logicFunctionId,
workspaceId,
payload: workflowActionInput.logicFunctionInput,
});
try {
const result = await this.logicFunctionExecutorService.execute({
logicFunctionId: workflowActionInput.logicFunctionId,
workspaceId,
payload: workflowActionInput.logicFunctionInput,
});
if (result.error) {
return { error: result.error.errorMessage };
if (result.error) {
return { error: result.error.errorMessage };
}
return { result: result.data || {} };
} catch (error) {
if (
error instanceof LogicFunctionException &&
error.code ===
LogicFunctionExceptionCode.LOGIC_FUNCTION_BUILD_USER_ERROR
) {
throw new WorkflowStepExecutorException(
error.message,
WorkflowStepExecutorExceptionCode.INVALID_STEP_INPUT,
);
}
throw error;
}
return { result: result.data || {} };
}
}
@@ -9,6 +9,10 @@ import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { type WorkflowSendEmailActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type';
@@ -39,13 +43,19 @@ export class ToolExecutorWorkflowAction implements WorkflowAction {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new Error('Step not found');
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
const tool = this.toolsByActionType.get(step.type);
if (!tool) {
throw new Error(`No tool found for workflow action type: ${step.type}`);
throw new WorkflowStepExecutorException(
`No tool found for workflow action type: ${step.type}`,
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
let toolInput = step.settings.input;
@@ -28,6 +28,7 @@ import {
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { type WorkflowFailureReason } from 'src/modules/workflow/workflow-executor/types/workflow-failure-reason.type';
import {
type WorkflowBranchExecutorInput,
type WorkflowExecutorInput,
@@ -68,9 +69,9 @@ export class WorkflowExecutorWorkspaceService {
shouldComputeWorkflowRunStatus = true,
executedStepsCount = 0,
}: WorkflowExecutorInput) {
await Promise.all(
const reasons = await Promise.all(
stepIds.map(async (stepIdToExecute) => {
await this.executeFromStep({
return this.executeFromStep({
stepId: stepIdToExecute,
workflowRunId,
workspaceId,
@@ -80,9 +81,16 @@ export class WorkflowExecutorWorkspaceService {
);
if (shouldComputeWorkflowRunStatus) {
const failureReason = reasons.some(
(reason) => reason === 'SYSTEM_ERROR',
)
? 'SYSTEM_ERROR'
: reasons.find((reason) => reason === 'USER_ERROR') ?? undefined;
await this.computeWorkflowRunStatus({
workflowRunId,
workspaceId,
failureReason,
});
}
}
@@ -92,7 +100,7 @@ export class WorkflowExecutorWorkspaceService {
workflowRunId,
workspaceId,
executedStepsCount,
}: WorkflowBranchExecutorInput) {
}: WorkflowBranchExecutorInput): Promise<WorkflowFailureReason | undefined> {
const workflowRun =
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
workflowRunId,
@@ -113,7 +121,7 @@ export class WorkflowExecutorWorkspaceService {
error: 'Step not found',
});
return;
return 'SYSTEM_ERROR';
}
let actionOutput: WorkflowActionOutput;
@@ -165,7 +173,7 @@ export class WorkflowExecutorWorkspaceService {
shouldSkipStepExecution: true,
};
} else {
return;
return undefined;
}
const isError =
@@ -183,7 +191,11 @@ export class WorkflowExecutorWorkspaceService {
});
if (!shouldProcessNextSteps) {
return;
if (isError) {
return actionOutput.failureReason ?? 'SYSTEM_ERROR';
}
return undefined;
}
const shouldRunAnotherJob =
@@ -196,7 +208,7 @@ export class WorkflowExecutorWorkspaceService {
workspaceId,
});
return;
return undefined;
}
const { nextStepIdsToExecute, nextStepIdsToSkip, nextStepIdsToFailSafely } =
@@ -225,6 +237,8 @@ export class WorkflowExecutorWorkspaceService {
executedStepsCount: (executedStepsCount ?? 0) + 1,
});
}
return undefined;
}
async getNextStepIdsToExecute({
@@ -300,9 +314,11 @@ export class WorkflowExecutorWorkspaceService {
private async computeWorkflowRunStatus({
workflowRunId,
workspaceId,
failureReason,
}: {
workflowRunId: string;
workspaceId: string;
failureReason?: WorkflowFailureReason;
}) {
const workflowRun =
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
@@ -332,6 +348,7 @@ export class WorkflowExecutorWorkspaceService {
workspaceId,
status: WorkflowRunStatus.FAILED,
error: 'WorkflowRun failed',
failureReason,
});
return;
@@ -502,8 +519,13 @@ export class WorkflowExecutorWorkspaceService {
});
}
const failureReason: WorkflowFailureReason = isUserError
? 'USER_ERROR'
: 'SYSTEM_ERROR';
return {
error: error.message ?? 'Execution result error, no data or error',
failureReason,
};
}
}
@@ -187,11 +187,13 @@ export class WorkflowRunWorkspaceService {
workspaceId,
status,
error,
failureReason,
}: {
workflowRunId: string;
workspaceId: string;
status: Extract<WorkflowRunStatus, 'COMPLETED' | 'FAILED' | 'STOPPED'>;
error?: string;
failureReason?: 'USER_ERROR' | 'SYSTEM_ERROR';
}) {
const workflowRunToUpdate = await this.getWorkflowRunOrFail({
workflowRunId,
@@ -222,8 +224,9 @@ export class WorkflowRunWorkspaceService {
? MetricsKeys.WorkflowRunCompleted
: status === WorkflowRunStatus.STOPPED
? MetricsKeys.WorkflowRunStopped
: MetricsKeys.WorkflowRunFailed,
: this.getMetricKeyFromFailureReason(failureReason),
eventId: workflowRunId,
attributes: { workspace_id: workspaceId },
});
}
@@ -250,7 +253,7 @@ export class WorkflowRunWorkspaceService {
stepInfos: {
...workflowRunToUpdate.state?.stepInfos,
[stepId]: {
...(workflowRunToUpdate.state?.stepInfos[stepId] || {}),
...workflowRunToUpdate.state?.stepInfos[stepId],
result: stepInfo?.result,
error: stepInfo?.error,
status: stepInfo.status,
@@ -283,7 +286,7 @@ export class WorkflowRunWorkspaceService {
for (const [stepId, info] of Object.entries(stepInfos)) {
mergedStepInfos[stepId] = {
...(existingStepInfos[stepId] || {}),
...existingStepInfos[stepId],
...info,
};
}
@@ -335,7 +338,7 @@ export class WorkflowRunWorkspaceService {
state: {
...workflowRunToUpdate.state,
flow: {
...(workflowRunToUpdate.state?.flow ?? {}),
...workflowRunToUpdate.state?.flow,
steps: updatedSteps,
},
},
@@ -492,4 +495,14 @@ export class WorkflowRunWorkspaceService {
};
}, {});
}
private getMetricKeyFromFailureReason(
failureReason: 'USER_ERROR' | 'SYSTEM_ERROR' | undefined,
): MetricsKeys {
if (failureReason === 'USER_ERROR') {
return MetricsKeys.WorkflowRunFailedUserError;
}
return MetricsKeys.WorkflowRunFailedSystemError;
}
}
+20 -23
View File
@@ -102,14 +102,14 @@ __metadata:
linkType: hard
"@ai-sdk/mistral@npm:^3.0.20":
version: 3.0.20
resolution: "@ai-sdk/mistral@npm:3.0.20"
version: 3.0.25
resolution: "@ai-sdk/mistral@npm:3.0.25"
dependencies:
"@ai-sdk/provider": "npm:3.0.8"
"@ai-sdk/provider-utils": "npm:4.0.15"
"@ai-sdk/provider-utils": "npm:4.0.20"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/32caf651dae8a0fb7b7faceb8706e711404a32bc21aea94fe6bcbbe6adb09c31fc6563efd28547bdcbbfe99165145ec73fe83f4b54bd75f290b6020cc9834bc7
checksum: 10c0/4ea732fc807e9e8f143ecbc3168e103dbb0c43f611ab9d9fdfe532510059c18aa8b44a61c7eafda4d9a91977e7d83fa94022c4aa39be21ad035b186f8471384f
languageName: node
linkType: hard
@@ -163,6 +163,19 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:4.0.20":
version: 4.0.20
resolution: "@ai-sdk/provider-utils@npm:4.0.20"
dependencies:
"@ai-sdk/provider": "npm:3.0.8"
"@standard-schema/spec": "npm:^1.1.0"
eventsource-parser: "npm:^3.0.6"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/749d9f91b2b098f716246bbb92a801d0f8a952fed4481e48b7d36a1c026cfe96b9d956f34b95270bc4628048ad57edd7b19026c167e8ded288e741aa289d4cdd
languageName: node
linkType: hard
"@ai-sdk/provider@npm:2.0.1":
version: 2.0.1
resolution: "@ai-sdk/provider@npm:2.0.1"
@@ -4117,16 +4130,7 @@ __metadata:
languageName: node
linkType: hard
"@dagrejs/dagre@npm:^1.1.2":
version: 1.1.3
resolution: "@dagrejs/dagre@npm:1.1.3"
dependencies:
"@dagrejs/graphlib": "npm:2.2.2"
checksum: 10c0/c81e142ed55d23bda02968624bd96987eeb545dbc860b0dc22aafd606f8bd8b1b18914e2b986c46e3250a6cfcc2bc16a944bb8df301da9adffd738936679db44
languageName: node
linkType: hard
"@dagrejs/dagre@npm:^1.1.8":
"@dagrejs/dagre@npm:^1.1.2, @dagrejs/dagre@npm:^1.1.8":
version: 1.1.8
resolution: "@dagrejs/dagre@npm:1.1.8"
dependencies:
@@ -4135,13 +4139,6 @@ __metadata:
languageName: node
linkType: hard
"@dagrejs/graphlib@npm:2.2.2":
version: 2.2.2
resolution: "@dagrejs/graphlib@npm:2.2.2"
checksum: 10c0/2e79a4f5c6c402054b7ef42e786459645495934476170999f13867a55a00072636a23914772cce6bc03ce51eef70de589058860b8f034c1d70804fb61e01fcfc
languageName: node
linkType: hard
"@dagrejs/graphlib@npm:2.2.4":
version: 2.2.4
resolution: "@dagrejs/graphlib@npm:2.2.4"
@@ -46875,14 +46872,14 @@ __metadata:
languageName: node
linkType: hard
"nodemailer@npm:7.0.11, nodemailer@npm:^7.0.11":
"nodemailer@npm:7.0.11":
version: 7.0.11
resolution: "nodemailer@npm:7.0.11"
checksum: 10c0/208f108fdb4c5dd0e3a2f013578d53dad505cf1b9c7a084f6d22fc9d6f3912daafb4a23793ca568ff848afc35f15f4eb24382d3f6f9fb8ede4a8410d4ca63618
languageName: node
linkType: hard
"nodemailer@npm:7.0.13":
"nodemailer@npm:7.0.13, nodemailer@npm:^7.0.11":
version: 7.0.13
resolution: "nodemailer@npm:7.0.13"
checksum: 10c0/b26aa5b9fa4a033bbc1e1c16ef75ee2a9c8641fd290c00a8361d6a251b3c1b8bad545a23efa627f59cb266340a448891ea8aa49d8a9307c767b8505219d95079