From c0cc0689d64a5b96c4b852e6b19647d83b53dd4c Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 17 Feb 2026 18:45:52 +0100 Subject: [PATCH] Add Client Api generation (#17961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Add API client generation to SDK dev mode and refactor orchestrator into step-based pipeline ### Why The SDK dev mode lacked typed API client generation, forcing developers to work without auto-generated GraphQL types when building applications. Additionally, the orchestrator was a monolithic class that mixed watcher management, token handling, and sync logic — making it difficult to extend with new steps like client generation. ### How - **Refactored the orchestrator** into a step-based pipeline with dedicated classes: `CheckServer`, `EnsureValidTokens`, `ResolveApplication`, `BuildManifest`, `UploadFiles`, `GenerateApiClient`, `SyncApplication`, and `StartWatchers`. Each step has typed input/output/status, managed by a new `OrchestratorState` class. - **Added `GenerateApiClientOrchestratorStep`** that detects object/field schema changes and regenerates a typed GraphQL client (via `@genql/cli`) into `node_modules/twenty-sdk/generated` for seamless imports. - **Replaced `checkApplicationExist`** with `findOneApplication` on both server resolver and SDK API service, returning the entity data instead of a boolean. - **Added application token pair mutations** (`generateApplicationToken`, `renewApplicationToken`) to the API service, with the server now returning `ApplicationTokenPairDTO` containing both access and refresh tokens. - **Restructured the dev UI** into `dev/ui/components/` with dedicated panel, section, and event log components. - **Simplified `AppDevCommand`** from ~180 lines of watcher management down to ~40 lines that delegate entirely to the orchestrator. --- packages/create-twenty-app/README.md | 10 +- .../src/constants/base-application/README.md | 3 +- .../last-email-interaction/package.json | 1 - .../mailchimp-synchronizer/package.json | 1 - .../stripe-synchronizer/package.json | 1 - packages/twenty-apps/hello-world/package.json | 1 - .../internal/self-hosting/package.json | 1 - .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 16 +- .../developers/extend/capabilities/apps.mdx | 16 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 16 +- .../developers/extend/capabilities/apps.mdx | 16 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../developers/extend/capabilities/apps.mdx | 13 +- .../src/generated-metadata/graphql.ts | 9 +- packages/twenty-sdk/.gitignore | 1 + packages/twenty-sdk/README.md | 10 +- packages/twenty-sdk/package.json | 10 + packages/twenty-sdk/project.json | 4 +- .../__tests__/apps/invalid-app/package.json | 1 - .../app-dev/expected-manifest.ts | 2 +- .../cli/__tests__/apps/rich-app/package.json | 1 - .../app-dev/expected-manifest.ts | 2 +- .../cli/__tests__/apps/root-app/package.json | 1 - .../integration/utils/setup-app-dev-mocks.ts | 30 +- .../src/cli/commands/app-command.ts | 9 - .../src/cli/commands/app/app-dev.ts | 163 +------ .../src/cli/commands/app/app-generate.ts | 19 - .../src/cli/utilities/api/api-service.ts | 169 ++++++- .../utilities/build/common/esbuild-watcher.ts | 26 +- .../build/common/file-upload-watcher.ts | 1 - .../build/manifest/manifest-watcher.ts | 39 +- .../cli/utilities/client/client-service.ts | 60 +-- .../cli/utilities/config/config-service.ts | 6 + .../utilities/dev/dev-mode-orchestrator.ts | 415 ------------------ .../cli/utilities/dev/dev-ui-state-manager.ts | 203 --------- .../src/cli/utilities/dev/dev-ui-state.ts | 38 -- .../src/cli/utilities/dev/dev-ui.tsx | 295 ------------- .../dev-mode-orchestrator-state.ts | 280 ++++++++++++ .../dev/orchestrator/dev-mode-orchestrator.ts | 188 ++++++++ .../steps/build-manifest-orchestrator-step.ts | 91 ++++ .../steps/check-server-orchestrator-step.ts | 64 +++ .../ensure-valid-tokens-orchestrator-step.ts | 134 ++++++ .../generate-api-client-orchestrator-step.ts | 55 +++ .../resolve-application-orchestrator-step.ts | 97 ++++ .../steps/start-watchers-orchestrator-step.ts | 210 +++++++++ .../sync-application-orchestrator-step.ts | 84 ++++ .../steps/upload-files-orchestrator-step.ts | 114 +++++ .../components/dev-ui-application-panel.tsx | 127 ++++++ .../ui/components/dev-ui-entity-section.tsx | 101 +++++ .../dev/ui/components/dev-ui-event-log.tsx | 24 + .../utilities/dev/ui/components/dev-ui.tsx | 54 +++ .../cli/utilities/dev/ui/dev-ui-constants.ts | 224 ++++++++++ .../src/cli/utilities/dev/ui/dev-ui-hooks.ts | 33 ++ .../utilities/dev/ui/dev-ui-ink-context.tsx | 22 + .../utilities/dev/ui/dev-ui-state-manager.ts | 29 ++ .../application-development.resolver.ts | 8 +- .../resolvers/application.resolver.ts | 16 - .../services/application.service.ts | 14 - .../webhook/jobs/webhook-job.module.ts | 2 + ...token-schema-filtering.integration-spec.ts | 3 +- ...rate-application-token.integration-spec.ts | 30 +- ...te-application-token-query-factory.util.ts | 10 +- .../utils/generate-application-token.util.ts | 4 +- yarn.lock | 97 +++- 72 files changed, 2419 insertions(+), 1422 deletions(-) delete mode 100644 packages/twenty-sdk/src/cli/commands/app/app-generate.ts delete mode 100644 packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts delete mode 100644 packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts delete mode 100644 packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state.ts delete mode 100644 packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-application-panel.tsx create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-entity-section.tsx create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-event-log.tsx create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui.tsx create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-constants.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-hooks.ts create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-ink-context.tsx create mode 100644 packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-state-manager.ts diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index ebcb4d262a..00b29fd23b 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -15,7 +15,7 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk). - Zero‑config project bootstrap -- Preconfigured scripts for auth, dev mode (watch & sync), generate, uninstall, and function management +- Preconfigured scripts for auth, dev mode (watch & sync), uninstall, and function management - Strong TypeScript support and typed client generation ## Documentation @@ -44,10 +44,8 @@ yarn twenty auth:login # Add a new entity to your application (guided) yarn twenty entity:add -# Generate a typed Twenty client and workspace entity types -yarn twenty app:generate - # Start dev mode: watches, builds, and syncs local changes to your workspace +# (also auto-generates a typed API client in node_modules/twenty-sdk/generated) yarn twenty app:dev # Watch your application's function logs @@ -74,7 +72,7 @@ yarn twenty app:uninstall - Use `yarn twenty auth:login` to authenticate with your Twenty workspace. - Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles). - Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time. -- Keep your types up‑to‑date using `yarn twenty app:generate`. +- Types are auto‑generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`. ## Publish your application @@ -103,7 +101,7 @@ Our team reviews contributions for quality, security, and reusability before mer ## Troubleshooting - Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions. -- Types not generated: ensure `yarn twenty app:generate` runs without errors, then re‑start `yarn twenty app:dev`. +- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client. ## Contributing - See our [GitHub](https://github.com/twentyhq/twenty) diff --git a/packages/create-twenty-app/src/constants/base-application/README.md b/packages/create-twenty-app/src/constants/base-application/README.md index a5a53337b6..26c135bbec 100644 --- a/packages/create-twenty-app/src/constants/base-application/README.md +++ b/packages/create-twenty-app/src/constants/base-application/README.md @@ -29,9 +29,8 @@ yarn twenty auth:switch # Switch default workspace yarn twenty auth:list # List all configured workspaces # Application -yarn twenty app:dev # Start dev mode (watch, build, and sync) +yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client) yarn twenty entity:add # Add a new entity (function, front-component, object, role) -yarn twenty app:generate # Generate typed Twenty client yarn twenty function:logs # Stream function logs yarn twenty function:execute # Execute a function with JSON payload yarn twenty app:uninstall # Uninstall app from workspace diff --git a/packages/twenty-apps/community/last-email-interaction/package.json b/packages/twenty-apps/community/last-email-interaction/package.json index fb164a4a4f..9eb0237fba 100644 --- a/packages/twenty-apps/community/last-email-interaction/package.json +++ b/packages/twenty-apps/community/last-email-interaction/package.json @@ -17,7 +17,6 @@ }, "scripts": { "auth": "twenty auth login", - "generate": "twenty app generate", "dev": "twenty app dev", "sync": "twenty app sync", "uninstall": "twenty app uninstall", diff --git a/packages/twenty-apps/community/mailchimp-synchronizer/package.json b/packages/twenty-apps/community/mailchimp-synchronizer/package.json index b1a6ce33f2..ae11b72ee7 100644 --- a/packages/twenty-apps/community/mailchimp-synchronizer/package.json +++ b/packages/twenty-apps/community/mailchimp-synchronizer/package.json @@ -17,7 +17,6 @@ }, "scripts": { "auth": "twenty auth login", - "generate": "twenty app generate", "dev": "twenty app dev", "sync": "twenty app sync", "uninstall": "twenty app uninstall", diff --git a/packages/twenty-apps/community/stripe-synchronizer/package.json b/packages/twenty-apps/community/stripe-synchronizer/package.json index e2a148318e..4bc5ecf5f6 100644 --- a/packages/twenty-apps/community/stripe-synchronizer/package.json +++ b/packages/twenty-apps/community/stripe-synchronizer/package.json @@ -17,7 +17,6 @@ }, "scripts": { "auth": "twenty auth login", - "generate": "twenty app generate", "dev": "twenty app dev", "sync": "twenty app sync", "uninstall": "twenty app uninstall", diff --git a/packages/twenty-apps/hello-world/package.json b/packages/twenty-apps/hello-world/package.json index 091d99b4dc..4bf61300c5 100644 --- a/packages/twenty-apps/hello-world/package.json +++ b/packages/twenty-apps/hello-world/package.json @@ -11,7 +11,6 @@ "scripts": { "create-entity": "twenty app add", "dev": "twenty app dev", - "generate": "twenty app generate", "sync": "twenty app sync", "uninstall": "twenty app uninstall", "auth": "twenty auth login" diff --git a/packages/twenty-apps/internal/self-hosting/package.json b/packages/twenty-apps/internal/self-hosting/package.json index 601128d434..4ceed7014f 100644 --- a/packages/twenty-apps/internal/self-hosting/package.json +++ b/packages/twenty-apps/internal/self-hosting/package.json @@ -17,7 +17,6 @@ "app:dev": "twenty app dev", "app:sync": "twenty app sync", "entity:add": "twenty entity add", - "app:generate": "twenty app generate", "function:logs": "twenty function logs", "function:execute": "twenty function execute", "app:uninstall": "twenty app uninstall", diff --git a/packages/twenty-docs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/developers/extend/capabilities/apps.mdx index 2a17c63b42..76786addfc 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apps.mdx @@ -47,9 +47,6 @@ From here you can: # Add a new entity to your application (guided) yarn twenty entity:add -# Generate a typed Twenty client and workspace entity types -yarn twenty app:generate - # Watch your application's function logs yarn twenty function:logs @@ -140,7 +137,7 @@ export default defineObject({ Later commands will add more files and folders: -- `yarn twenty app:generate` will create a `generated/` folder (typed Twenty client + workspace types). +- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types). - `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles. ## Authentication @@ -651,7 +648,7 @@ You can create new front components in two ways: ### Generated typed client -Run `yarn twenty app:generate` to create a local typed client in `generated/` based on your workspace schema. Use it in your functions: +The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions: ```typescript import Twenty from '~/generated'; @@ -660,7 +657,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -The client is re-generated by `yarn twenty app:generate`. Re-run after changing your objects or when onboarding to a new workspace. +The client is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change. #### Runtime credentials in logic functions @@ -697,13 +694,13 @@ Then add a `twenty` script: } ``` -Now you can run all commands via `yarn twenty `, e.g. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc. +Now you can run all commands via `yarn twenty `, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc. ## Troubleshooting - Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions. - Cannot connect to server: verify the API URL and that the Twenty server is reachable. -- Types or client missing/outdated: run `yarn twenty app:generate`. +- Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client. - Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment. Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx index d385460b10..8221d23e2f 100644 --- a/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ yarn twenty app:dev # أضف كيانًا جديدًا إلى تطبيقك (موجّه) yarn twenty entity:add -# ولِّد عميل Twenty مضبوط الأنواع وأنواع كيانات مساحة العمل -yarn twenty app:generate - # راقب سجلات وظائف تطبيقك yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات: -* `yarn twenty app:generate` سيُنشئ مجلدًا `generated/` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل). +* `yarn twenty app:dev` يولّد عميل Twenty مضبوط الأنواع تلقائيًا في `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار. ## المصادقة @@ -662,7 +659,7 @@ export default defineFrontComponent({ ### عميل مُولَّد مضبوط الأنواع -شغّل `yarn twenty app:generate` لإنشاء عميل محلي مضبوط الأنواع في `generated/` استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك: +يولّد `yarn twenty app:dev` عميل Twenty مضبوط الأنواع تلقائيًا في `node_modules/twenty-sdk/generated`. استخدمه في وظائفك: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -يُعاد توليد العميل بواسطة `yarn twenty app:generate`. أعِد التشغيل بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة. +يُعاد توليد العميل تلقائيًا أثناء تشغيل `app:dev`. أعد تشغيل `app:dev` بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة. #### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية @@ -708,13 +705,13 @@ yarn add -D twenty-sdk } ``` -الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty `، مثلًا: `yarn twenty app:dev`، `yarn twenty app:generate`، `yarn twenty help`، إلخ. +الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty `، مثلًا: `yarn twenty app:dev`، `yarn twenty help`، إلخ. ## استكشاف الأخطاء وإصلاحها * أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة. * يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة البرمجة وأن خادم Twenty قابل للوصول. -* الأنواع أو العميل مفقود/قديم: شغّل `yarn twenty app:generate`. +* الأنواع أو العميل مفقود/قديم: أعد تشغيل `yarn twenty app:dev`. * وضع التطوير لا يزامن: تأكد من أن `yarn twenty app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك. قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx index a2e0ebea6f..70460b2e08 100644 --- a/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ Odtud můžete: # Přidejte do vaší aplikace novou entitu (s průvodcem) yarn twenty entity:add -# Vygenerujte typovaného klienta Twenty a typy entit pracovního prostoru -yarn twenty app:generate - # Sledujte logy funkcí vaší aplikace yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ Pozdější příkazy přidají další soubory a složky: -* `yarn twenty app:generate` vytvoří složku `generated/` (typovaný klient Twenty + typy pracovního prostoru). +* `yarn twenty app:dev` automaticky generuje typovaného klienta v `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty nebo role. ## Ověření @@ -662,7 +659,7 @@ Nové frontendové komponenty můžete vytvořit dvěma způsoby: ### Generovaný typovaný klient -Spusťte `yarn twenty app:generate` a vytvořte lokálního typovaného klienta v `generated/` na základě schématu vašeho pracovního prostoru. Použijte jej ve svých funkcích: +Typovaný klient je automaticky generován příkazem `yarn twenty app:dev` a ukládán do `node_modules/twenty-sdk/generated`. Použijte jej ve svých funkcích: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -Klient je znovu generován příkazem `yarn twenty app:generate`. Spusťte znovu po změně vašich objektů nebo při připojování k novému pracovnímu prostoru. +Klient je automaticky znovu generován během `app:dev`, když změníte své objekty nebo se připojíte k novému pracovnímu prostoru. #### Běhové přihlašovací údaje v logických funkcích @@ -708,13 +705,13 @@ Poté přidejte skript `twenty`: } ``` -Nyní můžete spouštět všechny příkazy přes `yarn twenty `, např. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` atd. +Nyní můžete spouštět všechny příkazy přes `yarn twenty `, např. `yarn twenty app:dev`, `yarn twenty help` atd. ## Řešení potíží * Chyby ověření: spusťte `yarn twenty auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění. * Nelze se připojit k serveru: ověřte URL API a že je server Twenty dosažitelný. -* Typy nebo klient chybí nebo jsou zastaralé: spusťte `yarn twenty app:generate`. +* Typy nebo klient chybí nebo jsou zastaralé: restartujte `yarn twenty app:dev`. * Režim vývoje se nesynchronizuje: ujistěte se, že běží `yarn twenty app:dev` a že vaše prostředí změny neignoruje. Kanál podpory na Discordu: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx index 930754129e..a564db2a14 100644 --- a/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ Von hier aus können Sie: # Eine neue Entität zu deiner Anwendung hinzufügen (geführt) yarn twenty entity:add -# Einen typisierten Twenty-Client und Entitätstypen für den Arbeitsbereich generieren -yarn twenty app:generate - # Die Funktionsprotokolle deiner Anwendung überwachen yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ Spätere Befehle fügen weitere Dateien und Ordner hinzu: -* `yarn twenty app:generate` erstellt einen `generated/`-Ordner (typisierter Twenty-Client + Workspace-Typen). +* `yarn twenty app:dev` generiert den typisierten Client automatisch in `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für benutzerdefinierte Objekte, Funktionen, Frontend-Komponenten oder Rollen hinzu. ## Authentifizierung @@ -662,7 +659,7 @@ Sie können neue Frontend-Komponenten auf zwei Arten erstellen: ### Generierter typisierter Client -Führen Sie `yarn twenty app:generate` aus, um einen lokalen typisierten Client in `generated/` basierend auf Ihrem Arbeitsbereichs-Schema zu erstellen. Verwenden Sie ihn in Ihren Funktionen: +Der typisierte Client wird automatisch von `yarn twenty app:dev` generiert und in `node_modules/twenty-sdk/generated` gespeichert. Verwenden Sie ihn in Ihren Funktionen: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -Der Client wird durch `yarn twenty app:generate` erneut generiert. Führen Sie ihn nach Änderungen an Ihren Objekten oder beim Onboarding in einen neuen Workspace erneut aus. +Der Client wird während `app:dev` automatisch neu generiert, wenn Sie Ihre Objekte ändern oder einen neuen Workspace einbinden. #### Laufzeit-Anmeldedaten in Logikfunktionen @@ -708,13 +705,13 @@ Fügen Sie dann ein `twenty`-Skript hinzu: } ``` -Jetzt können Sie alle Befehle über `yarn twenty ` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` usw. +Jetzt können Sie alle Befehle über `yarn twenty ` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty help` usw. ## Fehlerbehebung * Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat. * Verbindung zum Server nicht möglich: Überprüfen Sie die API-URL und dass der Twenty-Server erreichbar ist. -* Typen oder Client fehlen/veraltet: Führen Sie `yarn twenty app:generate` aus. +* Typen oder Client fehlen/veraltet: Starten Sie `yarn twenty app:dev` neu. * Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn twenty app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden. Discord-Hilfekanal: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx index 5e75aa65ab..20ea9c7603 100644 --- a/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx @@ -52,9 +52,6 @@ Desde aquí usted puede: # Añade una nueva entidad a tu aplicación (guiado) yarn entity:add -# Genera un cliente tipado de Twenty y tipos de entidad del espacio de trabajo -yarn app:generate - # Supervisa los registros de funciones de tu aplicación yarn function:logs @@ -157,7 +154,7 @@ src/ A grandes rasgos: -* **package.json**: Declara el nombre de la aplicación, la versión, los entornos (Node 24+, Yarn 4) y agrega `twenty-sdk` además de scripts como `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` y `auth:login` que delegan en la CLI local `twenty`. +* **package.json**: Declara el nombre de la aplicación, la versión, los entornos (Node 24+, Yarn 4) y agrega `twenty-sdk` además de scripts como `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` y `auth:login` que delegan en la CLI local `twenty`. * **.gitignore**: Ignora artefactos comunes como `node_modules`, `.yarn`, `generated/` (cliente tipado), `dist/`, `build/`, carpetas de cobertura, archivos de registro y archivos `.env*`. * **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloquean y configuran la cadena de herramientas Yarn 4 utilizada por el proyecto. * **.nvmrc**: Fija la versión de Node.js esperada por el proyecto. @@ -173,7 +170,7 @@ A grandes rasgos: Comandos posteriores añadirán más archivos y carpetas: -* `yarn app:generate` creará una carpeta `generated/` (cliente tipado de Twenty + tipos del espacio de trabajo). +* `yarn app:dev` genera automáticamente el cliente Twenty tipado en `node_modules/twenty-sdk/generated`. * `yarn entity:add` añadirá archivos de definición de entidades en `src/` para tus objetos, funciones, componentes de interfaz o roles personalizados. ## Autenticación @@ -585,7 +582,7 @@ Puedes crear funciones nuevas de dos maneras: ### Cliente tipado generado -Ejecuta yarn app:generate para crear un cliente tipado local en generated/ basado en el esquema de tu espacio de trabajo. Úsalo en tus funciones: +`yarn app:dev` genera automáticamente el cliente Twenty tipado en `node_modules/twenty-sdk/generated`. Úsalo en tus funciones: ```typescript import Twenty from '~/generated'; @@ -594,7 +591,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -El cliente se vuelve a generar con `yarn app:generate`. Vuelve a ejecutarlo después de cambiar tus objetos o al incorporarte a un nuevo espacio de trabajo. +El cliente se regenera automáticamente durante la ejecución de `app:dev`. Reinicia `app:dev` después de cambiar tus objetos o al incorporarte a un nuevo espacio de trabajo. #### Credenciales en tiempo de ejecución en funciones de lógica @@ -632,7 +629,6 @@ Luego agrega scripts como estos: "auth:switch": "twenty auth:switch", "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", - "app:generate": "twenty app:generate", "app:uninstall": "twenty app:uninstall", "entity:add": "twenty entity:add", "function:logs": "twenty function:logs", @@ -642,13 +638,13 @@ Luego agrega scripts como estos: } ``` -Ahora puedes ejecutar los mismos comandos mediante Yarn, p. ej., `yarn app:dev`, `yarn app:generate`, etc. +Ahora puedes ejecutar los mismos comandos mediante Yarn, p. ej., `yarn app:dev`, etc. ## Solución de problemas * Errores de autenticación: ejecuta `yarn auth:login` y asegúrate de que tu clave de API tenga los permisos necesarios. * No se puede conectar al servidor: verifica la URL de la API y que el servidor de Twenty sea accesible. -* Tipos o cliente faltantes/obsoletos: ejecuta `yarn app:generate`. +* Tipos o cliente faltantes/obsoletos: reinicia `yarn app:dev`. * El modo de desarrollo no sincroniza: asegúrate de que `yarn app:dev` esté ejecutándose y de que los cambios no sean ignorados por tu entorno. Canal de ayuda en Discord: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx index 1f98c1397d..c196442021 100644 --- a/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx @@ -52,9 +52,6 @@ yarn app:dev # Ajouter une nouvelle entité à votre application (assisté) yarn entity:add -# Générer un client Twenty typé et les types d'entité de l'espace de travail -yarn app:generate - # Surveiller les journaux des fonctions de votre application yarn function:logs @@ -157,7 +154,7 @@ src/ Dans les grandes lignes : -* **package.json** : Déclare le nom de l’application, la version, les moteurs (Node 24+, Yarn 4), et ajoute `twenty-sdk` ainsi que des scripts comme `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` et `auth:login` qui délèguent à la CLI locale `twenty`. +* **package.json** : Déclare le nom de l’application, la version, les moteurs (Node 24+, Yarn 4), et ajoute `twenty-sdk` ainsi que des scripts comme `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` et `auth:login` qui délèguent à la CLI locale `twenty`. * **.gitignore** : Ignore les artefacts courants tels que `node_modules`, `.yarn`, `generated/` (client typé), `dist/`, `build/`, les dossiers de couverture, les fichiers journaux et les fichiers `.env*`. * **yarn.lock**, **.yarnrc.yml**, **.yarn/** : Verrouillent et configurent la chaîne d’outils Yarn 4 utilisée par le projet. * **.nvmrc** : Fige la version de Node.js attendue par le projet. @@ -173,7 +170,7 @@ Dans les grandes lignes : Des commandes ultérieures ajouteront d’autres fichiers et dossiers : -* `yarn app:generate` créera un dossier `generated/` (client Twenty typé + types de l’espace de travail). +* `yarn app:dev` génère automatiquement le client Twenty typé dans `node_modules/twenty-sdk/generated`. * `yarn entity:add` ajoutera des fichiers de définition d’entité sous `src/` pour vos objets, fonctions, composants front-end ou rôles personnalisés. ## Authentification @@ -585,7 +582,7 @@ Vous pouvez créer de nouvelles fonctions de deux façons : ### Client typé généré -Exécutez yarn app:generate pour créer un client typé local dans generated/ basé sur le schéma de votre espace de travail. Utilisez-le dans vos fonctions : +`yarn app:dev` génère automatiquement le client Twenty typé dans `node_modules/twenty-sdk/generated`. Utilisez-le dans vos fonctions : ```typescript import Twenty from '~/generated'; @@ -594,7 +591,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -Le client est régénéré par `yarn app:generate`. Relancez après avoir modifié vos objets ou lors de l’intégration à un nouvel espace de travail. +Le client est régénéré automatiquement pendant l'exécution de `app:dev`. Redémarrez `app:dev` après avoir modifié vos objets ou lors de l’intégration à un nouvel espace de travail. #### Identifiants d’exécution dans les fonctions logiques @@ -632,7 +629,6 @@ Ajoutez ensuite des scripts comme ceux-ci : "auth:switch": "twenty auth:switch", "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", - "app:generate": "twenty app:generate", "app:uninstall": "twenty app:uninstall", "entity:add": "twenty entity:add", "function:logs": "twenty function:logs", @@ -642,13 +638,13 @@ Ajoutez ensuite des scripts comme ceux-ci : } ``` -Vous pouvez désormais exécuter les mêmes commandes via Yarn, par exemple `yarn app:dev`, `yarn app:generate`, etc. +Vous pouvez désormais exécuter les mêmes commandes via Yarn, par exemple `yarn app:dev`, etc. ## Résolution des problèmes * Erreurs d’authentification : exécutez `yarn auth:login` et assurez-vous que votre clé API dispose des autorisations requises. * Impossible de se connecter au serveur : vérifiez l’URL de l’API et que le serveur Twenty est accessible. -* Types ou client manquants/obsolètes : exécutez `yarn app:generate`. +* Types ou client manquants/obsolètes : redémarrez `yarn app:dev`. * Le mode dev ne se synchronise pas : assurez-vous que `yarn app:dev` est en cours d’exécution et que les modifications ne sont pas ignorées par votre environnement. Canal d’aide Discord : https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx index 5c000efca8..923f28a968 100644 --- a/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ Da qui puoi: # Aggiungi una nuova entità alla tua applicazione (guidata) yarn twenty entity:add -# Genera un client Twenty tipizzato e i tipi di entità dell'area di lavoro -yarn twenty app:generate - # Monitora i log delle funzioni della tua applicazione yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ Comandi successivi aggiungeranno altri file e cartelle: -* `yarn twenty app:generate` creerà una cartella `generated/` (client Twenty tipizzato + tipi dello spazio di lavoro). +* `yarn twenty app:dev` genera automaticamente il client tipizzato in `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti, funzioni, componenti front-end o ruoli personalizzati. ## Autenticazione @@ -662,7 +659,7 @@ Puoi creare nuovi componenti front-end in due modi: ### Client tipizzato generato -Esegui `yarn twenty app:generate` per creare un client tipizzato locale in `generated/` basato sullo schema del tuo spazio di lavoro. Usalo nelle tue funzioni: +Il client tipizzato viene generato automaticamente da `yarn twenty app:dev` e memorizzato in `node_modules/twenty-sdk/generated`. Usalo nelle tue funzioni: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -Il client viene rigenerato da `yarn twenty app:generate`. Eseguilo nuovamente dopo aver modificato i tuoi oggetti oppure quando effettui l'onboarding su un nuovo spazio di lavoro. +Il client viene rigenerato automaticamente durante `app:dev` quando modifichi i tuoi oggetti o effettui l'onboarding su un nuovo spazio di lavoro. #### Credenziali di runtime nelle funzioni logiche @@ -708,13 +705,13 @@ Quindi aggiungi uno script `twenty`: } ``` -Ora puoi eseguire tutti i comandi tramite `yarn twenty `, ad es. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, ecc. +Ora puoi eseguire tutti i comandi tramite `yarn twenty `, ad es. `yarn twenty app:dev`, `yarn twenty help`, ecc. ## Risoluzione dei problemi * Errori di autenticazione: esegui `yarn twenty auth:login` e assicurati che la tua chiave API abbia i permessi richiesti. * Impossibile connettersi al server: verifica l'URL dell'API e che il server Twenty sia raggiungibile. -* Tipi o client mancanti/obsoleti: esegui `yarn twenty app:generate`. +* Tipi o client mancanti/obsoleti: riavvia `yarn twenty app:dev`. * Modalità di sviluppo non sincronizzata: assicurati che `yarn twenty app:dev` sia in esecuzione e che le modifiche non vengano ignorate dal tuo ambiente. Canale di supporto su Discord: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx index d858172d5e..7f1d8d3700 100644 --- a/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx @@ -52,9 +52,6 @@ yarn app:dev # アプリケーションに新しいエンティティを追加(ガイド付き) yarn entity:add -# 型付きの Twenty クライアントとワークスペースのエンティティ型を生成 -yarn app:generate - # アプリケーションの関数のログを監視 yarn function:logs @@ -156,7 +153,7 @@ src/ 概要: -* **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI. +* **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI. * **.gitignore**: `node_modules`、`.yarn`、`generated/`(型付きクライアント)、`dist/`、`build/`、カバレッジ用フォルダー、ログファイル、`.env*` ファイルなどの一般的な生成物を無視します。 * **yarn.lock**、**.yarnrc.yml**、**.yarn/**: プロジェクトで使用する Yarn 4 ツールチェーンをロックおよび構成します。 * **.nvmrc**: プロジェクトで想定する Node.js バージョンを固定します。 @@ -171,7 +168,7 @@ src/ 後続のコマンドにより、さらにファイルやフォルダーが追加されます: -* `yarn app:generate` は `generated/` フォルダー(型付きの Twenty クライアント + ワークスペースの型)を作成します。 +* `yarn app:dev` は `node_modules/twenty-sdk/generated` に型付き Twenty クライアントを自動生成します。 * `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles. ## 認証 @@ -583,7 +580,7 @@ const handler = async (event: RoutePayload) => { ### 生成された型付きクライアント -ワークスペースのスキーマに基づき、generated/ にローカルの型付きクライアントを作成するには yarn app:generate を実行します。 関数内で使用します: +`yarn app:dev` は `node_modules/twenty-sdk/generated` に型付き Twenty クライアントを自動生成します。 関数内で使用します: ```typescript import Twenty from '~/generated'; @@ -592,7 +589,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -このクライアントは `yarn app:generate` によって再生成されます。 Re-run after changing your objects or when onboarding to a new workspace. +このクライアントは `app:dev` 実行中に自動的に再生成されます。 オブジェクトを変更した後、または新しいワークスペースにオンボーディングする際は、`app:dev` を再起動してください。 #### Runtime credentials in logic functions @@ -630,7 +627,6 @@ yarn add -D twenty-sdk "auth:switch": "twenty auth:switch", "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", - "app:generate": "twenty app:generate", "app:uninstall": "twenty app:uninstall", "entity:add": "twenty entity:add", "function:logs": "twenty function:logs", @@ -640,13 +636,13 @@ yarn add -D twenty-sdk } ``` -Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc. +Now you can run the same commands via Yarn, e.g. `yarn app:dev`, etc. ## トラブルシューティング * 認証エラー: `yarn auth:login` を実行し、API キーに必要な権限があることを確認してください。 * サーバーに接続できません: API URL と、Twenty サーバーに到達可能であることを確認してください。 -* Types or client missing/outdated: run `yarn app:generate`. +* Types or client missing/outdated: restart `yarn app:dev`. * 開発モードで同期されない: `yarn app:dev` が実行中であり、環境によって変更が無視されていないことを確認してください。 Discord ヘルプチャンネル: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx index 666ae7652e..1b09d6f9c9 100644 --- a/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx @@ -52,9 +52,6 @@ yarn app:dev # Add a new entity to your application (guided) yarn entity:add -# Generate a typed Twenty client and workspace entity types -yarn app:generate - # Watch your application's function logs yarn function:logs @@ -157,7 +154,7 @@ src/ 개요: -* **package.json**: 앱 이름, 버전, 엔진(Node 24+, Yarn 4)을 선언하고, `twenty-sdk`와 함께 `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, `auth:login` 같은 스크립트를 추가합니다. 이 스크립트들은 로컬 `twenty` CLI에 위임됩니다. +* **package.json**: 앱 이름, 버전, 엔진(Node 24+, Yarn 4)을 선언하고, `twenty-sdk`와 함께 `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, `auth:login` 같은 스크립트를 추가합니다. 이 스크립트들은 로컬 `twenty` CLI에 위임됩니다. * **.gitignore**: `node_modules`, `.yarn`, `generated/`(타입드 클라이언트), `dist/`, `build/`, 커버리지 폴더, 로그 파일, `.env*` 파일 등의 일반 산출물을 무시합니다. * **yarn.lock**, **.yarnrc.yml**, **.yarn/**: 프로젝트에서 사용하는 Yarn 4 툴체인을 고정하고 구성합니다. * **.nvmrc**: 프로젝트에서 예상하는 Node.js 버전을 고정합니다. @@ -173,7 +170,7 @@ src/ 이후 명령을 실행하면 더 많은 파일과 폴더가 추가됩니다: -* `yarn app:generate`는 `generated/` 폴더를 생성합니다(타입드 Twenty 클라이언트 + 워크스페이스 타입). +* `yarn app:dev`는 `node_modules/twenty-sdk/generated`에 타입드 Twenty 클라이언트를 자동으로 생성합니다. * `yarn entity:add`는 사용자 정의 객체, 함수, 프런트 컴포넌트 또는 역할에 대한 엔티티 정의 파일을 `src/` 아래에 추가합니다. ## 인증 @@ -585,7 +582,7 @@ const handler = async (event: RoutePayload) => { ### 생성된 타입드 클라이언트 -워크스페이스 스키마를 기반으로 generated/에 로컬 타입드 클라이언트를 생성하려면 yarn app:generate를 실행하세요. 함수에서 사용하세요: +`yarn app:dev`는 `node_modules/twenty-sdk/generated`에 타입드 Twenty 클라이언트를 자동으로 생성합니다. 함수에서 사용하세요: ```typescript import Twenty from '~/generated'; @@ -594,7 +591,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -클라이언트는 `yarn app:generate`로 다시 생성됩니다. 객체를 변경한 후 또는 새 워크스페이스에 온보딩할 때 다시 실행하세요. +클라이언트는 `app:dev` 실행 중 자동으로 다시 생성됩니다. 객체를 변경한 후 또는 새 워크스페이스에 온보딩할 때 `app:dev`를 다시 시작하세요. #### 로직 함수의 런타임 자격 증명 @@ -632,7 +629,6 @@ yarn add -D twenty-sdk "auth:switch": "twenty auth:switch", "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", - "app:generate": "twenty app:generate", "app:uninstall": "twenty app:uninstall", "entity:add": "twenty entity:add", "function:logs": "twenty function:logs", @@ -642,13 +638,13 @@ yarn add -D twenty-sdk } ``` -이제 Yarn을 통해 동일한 명령을 실행할 수 있습니다. 예: `yarn app:dev`, `yarn app:generate` 등. +이제 Yarn을 통해 동일한 명령을 실행할 수 있습니다. 예: `yarn app:dev` 등. ## 문제 해결 * 인증 오류: `yarn auth:login`를 실행하고 API 키에 필요한 권한이 있는지 확인하세요. * 서버에 연결할 수 없음: API URL과 Twenty 서버에 접근 가능한지 확인하세요. -* 타입 또는 클라이언트가 없거나 오래된 경우: `yarn app:generate`를 실행하세요. +* 타입 또는 클라이언트가 없거나 오래된 경우: `yarn app:dev`를 다시 시작하세요. * 개발 모드가 동기화되지 않음: `yarn app:dev`가 실행 중인지, 환경에서 변경 사항을 무시하지 않는지 확인하세요. Discord 도움말 채널: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx index 35cf583205..1abe6045d9 100644 --- a/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ A partir daqui você pode: # Adicionar uma nova entidade à sua aplicação (assistido) yarn twenty entity:add -# Gerar um cliente Twenty tipado e tipos de entidades do espaço de trabalho -yarn twenty app:generate - # Acompanhar os logs das funções da sua aplicação yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ Comandos posteriores adicionarão mais arquivos e pastas: -* `yarn twenty app:generate` criará uma pasta `generated/` (cliente tipado do Twenty + tipos do espaço de trabalho). +* `yarn twenty app:dev` gera automaticamente o cliente tipado em `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` adicionará arquivos de definição de entidade em `src/` para seus objetos, funções, componentes de front-end ou papéis personalizados. ## Autenticação @@ -662,7 +659,7 @@ Você pode criar novos componentes de front-end de duas formas: ### Cliente tipado gerado -Execute `yarn twenty app:generate` para criar um cliente tipado local em `generated/` com base no esquema do seu espaço de trabalho. Use-o em suas funções: +O cliente tipado é gerado automaticamente por `yarn twenty app:dev` e armazenado em `node_modules/twenty-sdk/generated`. Use-o em suas funções: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -O cliente é regenerado pelo `yarn twenty app:generate`. Execute novamente após alterar seus objetos ou ao ingressar em um novo workspace. +O cliente é regenerado automaticamente durante `app:dev` quando você altera seus objetos ou ingressa em um novo workspace. #### Credenciais em tempo de execução em funções de lógica @@ -708,13 +705,13 @@ Em seguida, adicione um script `twenty`: } ``` -Agora você pode executar todos os comandos via `yarn twenty `, por exemplo, `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc. +Agora você pode executar todos os comandos via `yarn twenty `, por exemplo, `yarn twenty app:dev`, `yarn twenty help`, etc. ## Resolução de Problemas * Erros de autenticação: execute `yarn twenty auth:login` e certifique-se de que sua chave de API tenha as permissões necessárias. * Não é possível conectar ao servidor: verifique a URL da API e se o servidor do Twenty está acessível. -* Tipos ou cliente ausentes/desatualizados: execute `yarn twenty app:generate`. +* Tipos ou cliente ausentes/desatualizados: reinicie `yarn twenty app:dev`. * Modo de desenvolvimento não sincronizando: certifique-se de que `yarn twenty app:dev` esteja em execução e de que as alterações não estejam sendo ignoradas pelo seu ambiente. Canal de ajuda no Discord: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx index 2f887897c0..dbebfa20bc 100644 --- a/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ De aici puteți: # Adaugă o entitate nouă în aplicația ta (ghidat) yarn twenty entity:add -# Generează un client Twenty tipizat și tipurile de entități ale spațiului de lucru -yarn twenty app:generate - # Urmărește jurnalele funcțiilor aplicației tale yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ Comenzile ulterioare vor adăuga mai multe fișiere și foldere: -* `yarn twenty app:generate` va crea un folder `generated/` (client Twenty tipizat + tipuri pentru spațiul de lucru). +* `yarn twenty app:dev` generează automat clientul Twenty tipizat în `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end sau rolurile personalizate. ## Autentificare @@ -662,7 +659,7 @@ Puteți crea componente Front noi în două moduri: ### Client tipizat generat -Rulați `yarn twenty app:generate` pentru a crea un client tipizat local în `generated/`, pe baza schemei spațiului de lucru. Folosiți-l în funcțiile dvs.: +Clientul tipizat este generat automat de `yarn twenty app:dev` și stocat în `node_modules/twenty-sdk/generated`. Folosiți-l în funcțiile dvs.: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -Clientul este regenerat de `yarn twenty app:generate`. Rulați din nou după ce vă modificați obiectele sau când vă integrați într-un spațiu de lucru nou. +Clientul este regenerat automat când rulați `app:dev`. #### Acreditări la runtime în funcțiile de logică @@ -708,13 +705,13 @@ Apoi adăugați un script `twenty`: } ``` -Acum puteți rula toate comenzile prin `yarn twenty `, de ex. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc. +Acum puteți rula toate comenzile prin `yarn twenty `, de ex. `yarn twenty app:dev`, `yarn twenty help`, etc. ## Depanare * Erori de autentificare: rulați `yarn twenty auth:login` și asigurați-vă că cheia API are permisiunile necesare. * Nu se poate conecta la server: verificați URL-ul API și că serverul Twenty este accesibil. -* Tipuri sau client lipsă/învechite: rulați `yarn twenty app:generate`. +* Tipuri sau client lipsă/învechite: reporniți `yarn twenty app:dev`. * Modul dev nu sincronizează: asigurați-vă că `yarn twenty app:dev` rulează și că modificările nu sunt ignorate de mediul dvs. Canal de ajutor pe Discord: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx index 4611d43599..df5bce08a1 100644 --- a/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ yarn twenty app:dev # Добавить новую сущность в ваше приложение (с мастером) yarn twenty entity:add -# Сгенерировать типизированный клиент Twenty и типы сущностей рабочего пространства -yarn twenty app:generate - # Просматривать логи функций вашего приложения yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ Позднее команды добавят больше файлов и папок: -* `yarn twenty app:generate` создаст папку `generated/` (типизированный клиент Twenty + типы рабочего пространства). +* `yarn twenty app:dev` автоматически генерирует типизированный клиент Twenty в `node_modules/twenty-sdk/generated`. * `yarn twenty entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов или ролей. ## Аутентификация @@ -662,7 +659,7 @@ export default defineFrontComponent({ ### Сгенерированный типизированный клиент -Запустите `yarn twenty app:generate`, чтобы создать локальный типизированный клиент в `generated/` на основе схемы вашего рабочего пространства. Используйте его в своих функциях: +Типизированный клиент автоматически генерируется командой `yarn twenty app:dev` и сохраняется в `node_modules/twenty-sdk/generated`. Используйте его в своих функциях: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -Клиент повторно генерируется командой `yarn twenty app:generate`. Запускайте повторно после изменения ваших объектов или при подключении к новому рабочему пространству. +Клиент автоматически перегенерируется при запуске `app:dev`. #### Учётные данные времени выполнения в логических функциях @@ -708,13 +705,13 @@ yarn add -D twenty-sdk } ``` -Теперь вы можете запускать все команды через `yarn twenty `, например, `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` и т. д. +Теперь вы можете запускать все команды через `yarn twenty `, например, `yarn twenty app:dev`, `yarn twenty help` и т. д. ## Устранение неполадок * Ошибки аутентификации: выполните `yarn twenty auth:login` и убедитесь, что у вашего ключа API есть необходимые права. * Не удаётся подключиться к серверу: проверьте URL API и доступность сервера Twenty. -* Типы или клиент отсутствуют/устарели: выполните `yarn twenty app:generate`. +* Типы или клиент отсутствуют/устарели: перезапустите `yarn twenty app:dev`. * Режим разработки не синхронизируется: убедитесь, что запущен `yarn twenty app:dev`, и что ваша среда не игнорирует изменения. Канал помощи в Discord: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx index 7dfc6fdb7d..5089c51e4c 100644 --- a/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ Buradan şunları yapabilirsiniz: # Add a new entity to your application (guided) yarn twenty entity:add -# Generate a typed Twenty client and workspace entity types -yarn twenty app:generate - # Watch your application's function logs yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ İlerideki komutlar daha fazla dosya ve klasör ekleyecektir: -* `yarn twenty app:generate`, `generated/` klasörünü oluşturur (türlendirilmiş Twenty istemcisi + çalışma alanı türleri). +* `yarn twenty app:dev`, `node_modules/twenty-sdk/generated` içinde türlendirilmiş Twenty istemcisini otomatik olarak oluşturur. * `yarn twenty entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz veya rolleriniz için `src/` altında varlık tanım dosyaları ekler. ## Kimlik Doğrulama @@ -662,7 +659,7 @@ Yeni ön uç bileşenlerini iki şekilde oluşturabilirsiniz: ### Oluşturulmuş türlendirilmiş istemci -Çalışma alanı şemanıza göre `generated/` içinde yerel bir türlendirilmiş istemci oluşturmak için `yarn twenty app:generate` çalıştırın. Fonksiyonlarınızda kullanın: +Türlendirilmiş istemci `yarn twenty app:dev` tarafından otomatik olarak oluşturulur ve `node_modules/twenty-sdk/generated` içinde saklanır. Fonksiyonlarınızda kullanın: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -İstemci `yarn twenty app:generate` tarafından yeniden oluşturulur. Nesnelerinizi değiştirdikten sonra veya yeni bir çalışma alanına katılırken yeniden çalıştırın. +İstemci `app:dev` çalışırken otomatik olarak yeniden oluşturulur. #### Mantık fonksiyonlarında çalışma zamanı kimlik bilgileri @@ -708,13 +705,13 @@ Ardından bir `twenty` betiği ekleyin: } ``` -Artık tüm komutları `yarn twenty ` üzerinden çalıştırabilirsiniz; örn. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` vb. +Artık tüm komutları `yarn twenty ` üzerinden çalıştırabilirsiniz; örn. `yarn twenty app:dev`, `yarn twenty help` vb. ## Sorun Giderme * Kimlik doğrulama hataları: `yarn twenty auth:login` çalıştırın ve API anahtarınızın gerekli izinlere sahip olduğundan emin olun. * Sunucuya bağlanılamıyor: API URL’sini ve Twenty sunucusunun erişilebilir olduğunu doğrulayın. -* Türler veya istemci eksik/eski: `yarn twenty app:generate` çalıştırın. +* Türler veya istemci eksik/eski: `yarn twenty app:dev`'i yeniden başlatın. * Geliştirme modu eşitlenmiyor: `yarn twenty app:dev`'in çalıştığından ve değişikliklerin ortamınız tarafından yok sayılmadığından emin olun. Discord Yardım Kanalı: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx index 64e5cd2e15..7d41ae636d 100644 --- a/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx @@ -48,9 +48,6 @@ yarn twenty app:dev # 向你的应用添加一个新实体(引导式) yarn twenty entity:add -# 生成类型化的 Twenty 客户端和工作区实体类型 -yarn twenty app:generate - # 监听你的应用函数日志 yarn twenty function:logs @@ -142,7 +139,7 @@ export default defineObject({ 后续命令将添加更多文件和文件夹: -* `yarn twenty app:generate` 将创建一个 `generated/` 文件夹(类型化 Twenty 客户端 + 工作空间类型)。 +* `yarn twenty app:dev` 会在 `node_modules/twenty-sdk/generated` 中自动生成类型化 Twenty 客户端。 * `yarn twenty entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件或角色添加实体定义文件。 ## 身份验证 @@ -662,7 +659,7 @@ export default defineFrontComponent({ ### 生成的类型化客户端 -运行 `yarn twenty app:generate`,根据你的工作空间模式在 `generated/` 中创建本地类型化客户端。 在你的函数中使用它: +类型化客户端由 `yarn twenty app:dev` 自动生成,并存储在 `node_modules/twenty-sdk/generated` 中。 在你的函数中使用它: ```typescript import Twenty from '~/generated'; @@ -671,7 +668,7 @@ const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` -客户端会通过 `yarn twenty app:generate` 重新生成。 在更改对象之后或接入新工作空间时,请重新运行。 +客户端会在运行 `app:dev` 时自动重新生成。 #### 逻辑函数中的运行时凭据 @@ -708,13 +705,13 @@ yarn add -D twenty-sdk } ``` -现在你可以通过 `yarn twenty ` 运行所有命令,例如 `yarn twenty app:dev`、`yarn twenty app:generate`、`yarn twenty help` 等。 +现在你可以通过 `yarn twenty ` 运行所有命令,例如 `yarn twenty app:dev`、`yarn twenty help` 等。 ## 故障排除 * 身份验证错误:运行 `yarn twenty auth:login`,并确保你的 API 密钥具有所需权限。 * 无法连接到服务器:请验证 API URL,并确保 Twenty 服务器可达。 -* 类型或客户端缺失/过期:运行 `yarn twenty app:generate`。 +* 类型或客户端缺失/过期:重启 `yarn twenty app:dev`。 * 开发模式未同步:确保 `yarn twenty app:dev` 正在运行,并且你的环境不会忽略变更。 Discord 帮助频道:https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index bce2d699d6..3dfad2ab26 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2245,7 +2245,7 @@ export type Mutation = { evaluateAgentTurn: AgentTurnEvaluation; executeOneLogicFunction: LogicFunctionExecutionResult; generateApiKeyToken: ApiKeyToken; - generateApplicationToken: AuthToken; + generateApplicationToken: ApplicationTokenPair; generateTransientToken: TransientTokenOutput; getAuthTokensFromLoginToken: AuthTokens; getAuthTokensFromOTP: AuthTokens; @@ -3650,7 +3650,6 @@ export type Query = { chatMessages: Array; chatThread: AgentChatThread; chatThreads: Array; - checkApplicationExist: Scalars['Boolean']; checkUserExists: CheckUserExistOutput; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput; commandMenuItem?: Maybe; @@ -3761,12 +3760,6 @@ export type QueryChatThreadArgs = { }; -export type QueryCheckApplicationExistArgs = { - id?: InputMaybe; - universalIdentifier?: InputMaybe; -}; - - export type QueryCheckUserExistsArgs = { captchaToken?: InputMaybe; email: Scalars['String']; diff --git a/packages/twenty-sdk/.gitignore b/packages/twenty-sdk/.gitignore index 3d5681e1b6..2d8b0906ef 100644 --- a/packages/twenty-sdk/.gitignore +++ b/packages/twenty-sdk/.gitignore @@ -1,5 +1,6 @@ node_modules .twenty +generated storybook-static src/front-component-renderer/__stories__/example-sources-built src/front-component-renderer/__stories__/example-sources-built-preact diff --git a/packages/twenty-sdk/README.md b/packages/twenty-sdk/README.md index 26cf7ffc8d..4cb04ca94b 100644 --- a/packages/twenty-sdk/README.md +++ b/packages/twenty-sdk/README.md @@ -15,7 +15,7 @@ A CLI and SDK to develop, build, and publish applications that extend [Twenty CRM](https://twenty.com). - Type‑safe client and workspace entity typings -- Built‑in CLI for auth, dev mode (watch & sync), generate, uninstall, and function management +- Built‑in CLI for auth, dev mode (watch & sync), uninstall, and function management - Works great with the scaffolder: [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) ## Documentation @@ -52,7 +52,6 @@ Commands: auth:switch Switch the default workspace auth:list List all configured workspaces app:dev Watch and sync local application changes - app:generate Generate Twenty client app:uninstall Uninstall application from Twenty entity:add Add a new entity to your application function:logs Watch application function logs @@ -127,8 +126,6 @@ Application development commands. - `twenty app:uninstall [appPath]` — Uninstall the application from the current workspace. -- `twenty app:generate [appPath]` — Generate the typed Twenty client for your application. - ### Entity - `twenty entity:add [entityType]` — Add a new entity to your application. @@ -173,9 +170,6 @@ twenty entity:add function # Add a new front component twenty entity:add front-component -# Generate client types -twenty app:generate - # Uninstall the app from the workspace twenty app:uninstall @@ -231,7 +225,7 @@ Notes: ## Troubleshooting - Auth errors: run `twenty auth:login` again and ensure the API key has the required permissions. -- Typings out of date: run `twenty app:generate` to refresh the client and types. +- Typings out of date: restart `twenty app:dev` to refresh the client and types. - Not seeing changes in dev: make sure dev mode is running (`twenty app:dev`). ## Contributing diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 123e077ca1..0a3e63193c 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -9,6 +9,7 @@ }, "files": [ "dist", + "generated", "README.md", "package.json" ], @@ -38,6 +39,11 @@ "types": "./dist/front-component-renderer/index.d.ts", "import": "./dist/front-component-renderer/index.mjs", "require": "./dist/front-component-renderer/index.cjs" + }, + "./generated": { + "types": "./generated/index.ts", + "import": "./generated/index.ts", + "require": "./generated/index.ts" } }, "license": "AGPL-3.0", @@ -45,6 +51,7 @@ "@chakra-ui/react": "^3.33.0", "@emotion/react": "^11.14.0", "@genql/cli": "^3.0.3", + "@genql/runtime": "^2.10.0", "@quilted/threads": "^4.0.1", "@remote-dom/core": "^1.10.1", "@remote-dom/react": "^1.2.2", @@ -106,6 +113,9 @@ ], "front-component-renderer": [ "dist/front-component-renderer/index.d.ts" + ], + "generated": [ + "generated/index.ts" ] } } diff --git a/packages/twenty-sdk/project.json b/packages/twenty-sdk/project.json index 37fa59e766..263446d945 100644 --- a/packages/twenty-sdk/project.json +++ b/packages/twenty-sdk/project.json @@ -24,7 +24,7 @@ "options": { "cwd": "{projectRoot}", "commands": [ - "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts", + "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts && npx vite build -c vite.config.sdk.ts", "tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist" ], "parallel": false @@ -37,7 +37,7 @@ ], "options": { "cwd": "packages/twenty-sdk", - "command": "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist && npx vite build -c vite.config.node.ts --watch & npx vite build -c vite.config.browser.ts --watch" + "command": "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts && npx vite build -c vite.config.sdk.ts && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist && npx vite build -c vite.config.node.ts --watch & npx vite build -c vite.config.browser.ts --watch & npx vite build -c vite.config.sdk.ts --watch" } }, "start": { diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json index a8672907a6..b79736e590 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json @@ -16,7 +16,6 @@ "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", "entity:add": "twenty entity:add", - "app:generate": "twenty app:generate", "function:logs": "twenty function:logs", "function:execute": "twenty function:execute", "app:uninstall": "twenty app:uninstall", diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 84aaf7073b..3a48c46cd8 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -27,7 +27,7 @@ export const EXPECTED_MANIFEST: Manifest = { icon: 'IconWorld', universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e', - packageJsonChecksum: '42c3913415952d91ff1cf67ef6452872', + packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde', }, frontComponents: [ { diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json index 4e450bbef6..30996af1f7 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json @@ -16,7 +16,6 @@ "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", "entity:add": "twenty entity:add", - "app:generate": "twenty app:generate", "function:logs": "twenty function:logs", "function:execute": "twenty function:execute", "app:uninstall": "twenty app:uninstall", diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts index 43c333e72a..a99b529c91 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts @@ -9,7 +9,7 @@ export const EXPECTED_MANIFEST: Manifest = { description: 'An app with all entities at root level', icon: 'IconFolder', defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', - packageJsonChecksum: '351460efb13a6c1bee63e27cf87f6ece', + packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc', yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e', }, publicAssets: [], diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/package.json b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/package.json index 31ad7f945c..22bc2983be 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/package.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/package.json @@ -16,7 +16,6 @@ "auth:list": "twenty auth:list", "app:dev": "twenty app:dev", "entity:add": "twenty entity:add", - "app:generate": "twenty app:generate", "function:logs": "twenty function:logs", "function:execute": "twenty function:execute", "app:uninstall": "twenty app:uninstall", diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts index 3ea5fa3fef..dfd0d92bf2 100644 --- a/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts @@ -2,12 +2,30 @@ import { vi } from 'vitest'; const mockApiService = { validateAuth: vi.fn().mockResolvedValue({ authValid: true, serverUp: true }), - checkApplicationExist: vi - .fn() - .mockResolvedValue({ success: true, data: false }), + findOneApplication: vi.fn().mockResolvedValue({ success: true, data: null }), createApplication: vi .fn() .mockResolvedValue({ success: true, data: { id: 'mock-id' } }), + generateApplicationToken: vi.fn().mockResolvedValue({ + success: true, + data: { + applicationAccessToken: { token: 'mock-access-token', expiresAt: '' }, + applicationRefreshToken: { token: 'mock-refresh-token', expiresAt: '' }, + }, + }), + renewApplicationToken: vi.fn().mockResolvedValue({ + success: true, + data: { + applicationAccessToken: { + token: 'mock-renewed-access-token', + expiresAt: '', + }, + applicationRefreshToken: { + token: 'mock-renewed-refresh-token', + expiresAt: '', + }, + }, + }), syncApplication: vi.fn().mockResolvedValue({ success: true, data: true }), uploadFile: vi.fn().mockResolvedValue({ success: true, data: true }), }; @@ -15,8 +33,10 @@ const mockApiService = { vi.mock('@/cli/utilities/api/api-service', () => ({ ApiService: class { validateAuth = mockApiService.validateAuth; - checkApplicationExist = mockApiService.checkApplicationExist; + findOneApplication = mockApiService.findOneApplication; createApplication = mockApiService.createApplication; + generateApplicationToken = mockApiService.generateApplicationToken; + renewApplicationToken = mockApiService.renewApplicationToken; syncApplication = mockApiService.syncApplication; uploadFile = mockApiService.uploadFile; }, @@ -28,6 +48,6 @@ vi.mock('@/cli/utilities/file/file-uploader', () => ({ }, })); -vi.mock('@/cli/utilities/dev/dev-ui', () => ({ +vi.mock('@/cli/utilities/dev/ui/components/dev-ui', () => ({ renderDevUI: vi.fn().mockResolvedValue({ unmount: vi.fn() }), })); diff --git a/packages/twenty-sdk/src/cli/commands/app-command.ts b/packages/twenty-sdk/src/cli/commands/app-command.ts index 8404406058..095b17d400 100644 --- a/packages/twenty-sdk/src/cli/commands/app-command.ts +++ b/packages/twenty-sdk/src/cli/commands/app-command.ts @@ -2,7 +2,6 @@ import { formatPath } from '@/cli/utilities/file/file-path'; import chalk from 'chalk'; import type { Command } from 'commander'; import { AppDevCommand } from './app/app-dev'; -import { AppGenerateCommand } from './app/app-generate'; import { AppUninstallCommand } from './app/app-uninstall'; import { AuthListCommand } from './auth/auth-list'; import { AuthLoginCommand } from './auth/auth-login'; @@ -63,7 +62,6 @@ export const registerCommands = (program: Command): void => { const devCommand = new AppDevCommand(); const uninstallCommand = new AppUninstallCommand(); const addCommand = new EntityAddCommand(); - const generateCommand = new AppGenerateCommand(); const logsCommand = new LogicFunctionLogsCommand(); const executeCommand = new LogicFunctionExecuteCommand(); @@ -102,13 +100,6 @@ export const registerCommands = (program: Command): void => { await addCommand.execute(entityType as SyncableEntity, options?.path); }); - program - .command('app:generate [appPath]') - .description('Generate Twenty client') - .action(async (appPath?: string) => { - await generateCommand.execute(formatPath(appPath)); - }); - // Function commands program .command('function:logs [appPath]') diff --git a/packages/twenty-sdk/src/cli/commands/app/app-dev.ts b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts index 41ef6b256d..29eb19c30a 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-dev.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts @@ -1,179 +1,46 @@ -import { - createFrontComponentsWatcher, - createLogicFunctionsWatcher, - type EsbuildWatcher, -} from '@/cli/utilities/build/common/esbuild-watcher'; -import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums'; -import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; -import { DevModeOrchestrator } from '@/cli/utilities/dev/dev-mode-orchestrator'; -import path from 'path'; -import * as fs from 'fs-extra'; -import { DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager'; -import { renderDevUI } from '@/cli/utilities/dev/dev-ui'; -import { ASSETS_DIR, OUTPUT_DIR } from 'twenty-shared/application'; -import { FileUploadWatcher } from '@/cli/utilities/build/common/file-upload-watcher'; -import { FileFolder } from 'twenty-shared/types'; +import { renderDevUI } from '@/cli/utilities/dev/ui/components/dev-ui'; +import { DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager'; +import { DevModeOrchestrator } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator'; +import { OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; export type AppDevOptions = { appPath?: string; }; export class AppDevCommand { - private appPath = ''; private orchestrator: DevModeOrchestrator | null = null; - private manifestWatcher: ManifestWatcher | null = null; - private logicFunctionsWatcher: EsbuildWatcher | null = null; - private frontComponentsWatcher: EsbuildWatcher | null = null; - private assetWatcher: FileUploadWatcher | null = null; - private dependencyWatcher: FileUploadWatcher | null = null; - private watchersStarted = false; - private uiStateManager: DevUiStateManager | null = null; private unmountUI: (() => void) | null = null; async close(): Promise { this.unmountUI?.(); - - await Promise.all([ - this.manifestWatcher?.close(), - this.logicFunctionsWatcher?.close(), - this.frontComponentsWatcher?.close(), - this.assetWatcher?.close(), - this.dependencyWatcher?.close(), - ]); + await this.orchestrator?.close(); } async execute(options: AppDevOptions): Promise { - this.appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY; + const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY; - await this.cleanOutputDir(); - - this.uiStateManager = new DevUiStateManager({ - appPath: this.appPath, + const orchestratorState = new OrchestratorState({ + appPath, frontendUrl: process.env.FRONTEND_URL, }); - const { unmount } = await renderDevUI(this.uiStateManager); + const uiStateManager = new DevUiStateManager(orchestratorState); + + orchestratorState.onChange = () => uiStateManager.notify(); + + const { unmount } = await renderDevUI(uiStateManager); this.unmountUI = unmount; this.orchestrator = new DevModeOrchestrator({ - appPath: this.appPath, - handleManifestBuilt: this.handleWatcherRestarts.bind(this), - uiStateManager: this.uiStateManager, + state: orchestratorState, }); - await this.startManifestWatcher(); + await this.orchestrator.start(); this.setupGracefulShutdown(); } - private async cleanOutputDir() { - const outputDir = path.join(this.appPath, OUTPUT_DIR); - await fs.ensureDir(outputDir); - await fs.emptyDir(outputDir); - } - - private async startManifestWatcher(): Promise { - this.manifestWatcher = new ManifestWatcher({ - appPath: this.appPath, - handleChangeDetected: this.orchestrator!.handleChangeDetected.bind( - this.orchestrator, - ), - }); - - await this.manifestWatcher.start(); - } - - private async handleWatcherRestarts(result: ManifestBuildResult) { - const { logicFunctions, frontComponents } = result.filePaths; - - if (!this.watchersStarted) { - this.watchersStarted = true; - await this.startFileWatchers(logicFunctions, frontComponents); - return; - } - - if (this.logicFunctionsWatcher?.shouldRestart(logicFunctions)) { - await this.logicFunctionsWatcher.restart(logicFunctions); - } - - if (this.frontComponentsWatcher?.shouldRestart(frontComponents)) { - await this.frontComponentsWatcher.restart(frontComponents); - } - } - - private async startFileWatchers( - logicFunctions: string[], - frontComponents: string[], - ): Promise { - await Promise.all([ - this.startLogicFunctionsWatcher(logicFunctions), - this.startFrontComponentsWatcher(frontComponents), - this.startAssetWatcher(), - this.startDependencyWatcher(), - ]); - } - - private async startLogicFunctionsWatcher( - sourcePaths: string[], - ): Promise { - this.logicFunctionsWatcher = createLogicFunctionsWatcher({ - appPath: this.appPath, - sourcePaths, - handleBuildError: this.orchestrator!.handleFileBuildError.bind( - this.orchestrator, - ), - handleFileBuilt: this.orchestrator!.handleFileBuilt.bind( - this.orchestrator, - ), - }); - - await this.logicFunctionsWatcher.start(); - } - - private async startFrontComponentsWatcher( - sourcePaths: string[], - ): Promise { - this.frontComponentsWatcher = createFrontComponentsWatcher({ - appPath: this.appPath, - sourcePaths, - handleBuildError: this.orchestrator!.handleFileBuildError.bind( - this.orchestrator, - ), - handleFileBuilt: this.orchestrator!.handleFileBuilt.bind( - this.orchestrator, - ), - }); - - await this.frontComponentsWatcher.start(); - } - - private async startAssetWatcher(): Promise { - this.assetWatcher = new FileUploadWatcher({ - appPath: this.appPath, - fileFolder: FileFolder.PublicAsset, - watchPaths: [ASSETS_DIR], - handleFileBuilt: this.orchestrator!.handleFileBuilt.bind( - this.orchestrator, - ), - }); - - await this.assetWatcher.start(); - } - - private async startDependencyWatcher(): Promise { - this.dependencyWatcher = new FileUploadWatcher({ - appPath: this.appPath, - fileFolder: FileFolder.Dependencies, - watchPaths: ['package.json', 'yarn.lock'], - handleFileBuilt: this.orchestrator!.handleFileBuilt.bind( - this.orchestrator, - ), - }); - - this.dependencyWatcher.start(); - } - private setupGracefulShutdown(): void { const shutdown = () => void this.close().then(() => process.exit(0)); diff --git a/packages/twenty-sdk/src/cli/commands/app/app-generate.ts b/packages/twenty-sdk/src/cli/commands/app/app-generate.ts deleted file mode 100644 index 963ed2ad0f..0000000000 --- a/packages/twenty-sdk/src/cli/commands/app/app-generate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import chalk from 'chalk'; -import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; -import { ClientService } from '@/cli/utilities/client/client-service'; - -export class AppGenerateCommand { - private clientService = new ClientService(); - - async execute(appPath: string = CURRENT_EXECUTION_DIRECTORY) { - try { - await this.clientService.generate(appPath); - } catch (error) { - console.error( - chalk.red('Generate Twenty client failed:'), - error instanceof Error ? error.message : error, - ); - throw error; - } - } -} diff --git a/packages/twenty-sdk/src/cli/utilities/api/api-service.ts b/packages/twenty-sdk/src/cli/utilities/api/api-service.ts index a763861178..ef0f3573f7 100644 --- a/packages/twenty-sdk/src/cli/utilities/api/api-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/api/api-service.ts @@ -3,16 +3,9 @@ import axios, { type AxiosInstance, type AxiosResponse } from 'axios'; import chalk from 'chalk'; import * as fs from 'fs'; import { createClient } from 'graphql-sse'; -import { - buildClientSchema, - getIntrospectionQuery, - printSchema, -} from 'graphql/index'; +import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql'; import * as path from 'path'; -import { - type ApplicationManifest, - type Manifest, -} from 'twenty-shared/application'; +import { type Manifest } from 'twenty-shared/application'; import { type FileFolder } from 'twenty-shared/types'; import { type ApiResponse } from '@/cli/utilities/api/api-response-type'; import { pascalCase } from 'twenty-shared/utils'; @@ -31,7 +24,7 @@ export class ApiService { config.baseURL = twentyConfig.apiUrl; - if (twentyConfig.apiKey) { + if (!config.headers.Authorization && twentyConfig.apiKey) { config.headers.Authorization = `Bearer ${twentyConfig.apiKey}`; } @@ -102,15 +95,19 @@ export class ApiService { } } - async checkApplicationExist( + async findOneApplication( universalIdentifier: string, - ): Promise> { + ): Promise> { try { const query = ` - query CheckApplicationExist($universalIdentifier: UUID!) { - checkApplicationExist(universalIdentifier: $universalIdentifier) + query FindOneApplication($universalIdentifier: UUID!) { + findOneApplication(universalIdentifier: $universalIdentifier) { + id + universalIdentifier + } } `; + const response = await this.client.post( '/metadata', { @@ -125,6 +122,70 @@ export class ApiService { }, ); + if (response.data.errors) { + const isNotFound = response.data.errors.some( + (error: { extensions?: { code?: string } }) => + error.extensions?.code === 'NOT_FOUND', + ); + + if (isNotFound) { + return { success: true, data: null }; + } + + return { + success: false, + error: response.data.errors[0], + }; + } + + return { + success: true, + data: response.data.data.findOneApplication, + }; + } catch (error) { + return { + success: false, + error, + }; + } + } + + async generateApplicationToken(applicationId: string): Promise< + ApiResponse<{ + applicationAccessToken: { token: string; expiresAt: string }; + applicationRefreshToken: { token: string; expiresAt: string }; + }> + > { + try { + const mutation = ` + mutation GenerateApplicationToken($applicationId: UUID!) { + generateApplicationToken(applicationId: $applicationId) { + applicationAccessToken { + token + expiresAt + } + applicationRefreshToken { + token + expiresAt + } + } + } + `; + + const response: AxiosResponse = await this.client.post( + '/metadata', + { + query: mutation, + variables: { applicationId }, + }, + { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + }, + ); + if (response.data.errors) { return { success: false, @@ -134,8 +195,62 @@ export class ApiService { return { success: true, - data: response.data.data.checkApplicationExist, - message: `Successfully find application`, + data: response.data.data.generateApplicationToken, + }; + } catch (error) { + return { + success: false, + error, + }; + } + } + + async renewApplicationToken(applicationRefreshToken: string): Promise< + ApiResponse<{ + applicationAccessToken: { token: string; expiresAt: string }; + applicationRefreshToken: { token: string; expiresAt: string }; + }> + > { + try { + const mutation = ` + mutation RenewApplicationToken($applicationRefreshToken: String!) { + renewApplicationToken(applicationRefreshToken: $applicationRefreshToken) { + applicationAccessToken { + token + expiresAt + } + applicationRefreshToken { + token + expiresAt + } + } + } + `; + + const response: AxiosResponse = await this.client.post( + '/metadata', + { + query: mutation, + variables: { applicationRefreshToken }, + }, + { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + }, + ); + + if (response.data.errors) { + return { + success: false, + error: response.data.errors[0], + }; + } + + return { + success: true, + data: response.data.data.renewApplicationToken, }; } catch (error) { return { @@ -147,7 +262,7 @@ export class ApiService { async createApplication( manifest: Manifest, - ): Promise> { + ): Promise> { try { const mutation = ` mutation CreateOneApplication($input: CreateApplicationInput!) { @@ -298,21 +413,27 @@ export class ApiService { } } - async getSchema(): Promise> { + async getSchema(options?: { + authToken?: string; + }): Promise> { try { const introspectionQuery = getIntrospectionQuery(); + const headers: Record = { + 'Content-Type': 'application/json', + Accept: '*/*', + }; + + if (options?.authToken) { + headers.Authorization = `Bearer ${options.authToken}`; + } + const response = await this.client.post( '/graphql', { query: introspectionQuery, }, - { - headers: { - 'Content-Type': 'application/json', - Accept: '*/*', - }, - }, + { headers }, ); if (response.data.errors) { diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts index ca5a3a3860..daa0f595ad 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts @@ -188,15 +188,22 @@ export class EsbuildWatcher implements RestartableWatcher { } } -const externalPatternsPlugin: esbuild.Plugin = { - name: 'external-patterns', +// Resolves twenty-sdk/generated to the actual file path so esbuild +// bundles it instead of treating it as external (via twenty-sdk/*) +const createSdkGeneratedResolverPlugin = (appPath: string): esbuild.Plugin => ({ + name: 'sdk-generated-resolver', setup: (build) => { - build.onResolve({ filter: /(?:^|\/)generated(?:\/|$)/ }, (args) => ({ - path: args.path, - external: true, + build.onResolve({ filter: /^twenty-sdk\/generated/ }, () => ({ + path: path.join( + appPath, + 'node_modules', + 'twenty-sdk', + 'generated', + 'index.ts', + ), })); }, -}; +}); export const createLogicFunctionsWatcher = ( options: RestartableWatcherOptions, @@ -207,7 +214,7 @@ export const createLogicFunctionsWatcher = ( externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES, fileFolder: FileFolder.BuiltLogicFunction, platform: 'node', - extraPlugins: [externalPatternsPlugin], + extraPlugins: [createSdkGeneratedResolverPlugin(options.appPath)], banner: NODE_ESM_CJS_BANNER, }, }); @@ -221,6 +228,9 @@ export const createFrontComponentsWatcher = ( externalModules: FRONT_COMPONENT_EXTERNAL_MODULES, fileFolder: FileFolder.BuiltFrontComponent, jsx: 'automatic', - extraPlugins: getFrontComponentBuildPlugins(), + extraPlugins: [ + createSdkGeneratedResolverPlugin(options.appPath), + ...getFrontComponentBuildPlugins(), + ], }, }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/file-upload-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/file-upload-watcher.ts index cae5770ccf..37b26ed1a2 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/file-upload-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/file-upload-watcher.ts @@ -48,7 +48,6 @@ export class FileUploadWatcher { stabilityThreshold: 100, pollInterval: 50, }, - usePolling: true, }); this.watcher.on('all', (event, filePath) => { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts index f984e512db..6213662b83 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-watcher.ts @@ -1,4 +1,4 @@ -import { relative } from 'path'; +import path, { relative } from 'path'; import chokidar, { type FSWatcher } from 'chokidar'; import { type EventName } from 'chokidar/handler.js'; import { ASSETS_DIR } from 'twenty-shared/application'; @@ -8,6 +8,13 @@ export type ManifestWatcherOptions = { handleChangeDetected: (filePath: string) => void; }; +const IGNORED_DIRECTORY_NAMES = new Set([ + 'node_modules', + 'generated', + 'dist', + '.twenty', +]); + export class ManifestWatcher { private appPath: string; private handleChangeDetected: (filePath: string, event: EventName) => void; @@ -19,26 +26,36 @@ export class ManifestWatcher { } async start(): Promise { + const appPath = this.appPath; + this.watcher = chokidar.watch(this.appPath, { + ignored: (filePath: string) => { + const relativePath = relative(appPath, filePath); + + if (relativePath === '') { + return false; + } + + const firstSegment = relativePath.split(path.sep)[0]; + + return ( + IGNORED_DIRECTORY_NAMES.has(firstSegment) || + firstSegment.startsWith('.') + ); + }, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50, }, - usePolling: true, }); - this.watcher.on('all', async (event, filePath) => { + this.watcher.on('all', (event, filePath) => { if (event === 'addDir') { return; } const relativePath = relative(this.appPath, filePath); - const isInIgnoredDir = - relativePath.startsWith('node_modules') || - relativePath.startsWith('generated') || - relativePath.startsWith('dist'); - const isAssetFile = relativePath.startsWith(ASSETS_DIR); const isDependencyFile = ['package.json', 'yarn.lock'].includes( @@ -48,11 +65,7 @@ export class ManifestWatcher { const isTypeScriptFile = relativePath.endsWith('.ts') || relativePath.endsWith('.tsx'); - const isHiddenFile = relativePath.startsWith('.'); - - const shouldIgnore = isInIgnoredDir || !isTypeScriptFile || isHiddenFile; - - if (shouldIgnore && !isAssetFile && !isDependencyFile) { + if (!isTypeScriptFile && !isAssetFile && !isDependencyFile) { return; } diff --git a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts index 1316f2ae2f..f0b8489478 100644 --- a/packages/twenty-sdk/src/cli/utilities/client/client-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/client/client-service.ts @@ -1,61 +1,44 @@ import { ApiService } from '@/cli/utilities/api/api-service'; -import { ConfigService } from '@/cli/utilities/config/config-service'; import { generate } from '@genql/cli'; -import chalk from 'chalk'; import * as fs from 'fs-extra'; -import { join, resolve } from 'path'; +import { join } from 'path'; import { DEFAULT_API_KEY_NAME, DEFAULT_API_URL_NAME, } from 'twenty-shared/application'; -export const GENERATED_FOLDER_NAME = 'generated'; - export class ClientService { - private configService: ConfigService; private apiService: ApiService; constructor() { - this.configService = new ConfigService(); - this.apiService = new ApiService(); + this.apiService = new ApiService({ disableInterceptors: true }); } - async generate(appPath: string): Promise { - const outputPath = join(appPath, GENERATED_FOLDER_NAME); + async generate({ + appPath, + authToken, + }: { + appPath: string; + authToken?: string; + }): Promise { + const outputPath = this.resolveGeneratedPath(appPath); - console.log(chalk.blue('📦 Generating Twenty client...')); - console.log(chalk.gray(`📁 Output Path: ${outputPath}`)); - console.log(''); - const config = await this.configService.getConfig(); - - const url = config.apiUrl; - const token = config.apiKey; - - if (!url || !token) { - console.log( - chalk.yellow( - '⚠️ Skipping Client generation: API URL or token not configured', - ), - ); - return; - } - - console.log(chalk.gray(`API URL: ${url}`)); - console.log(chalk.gray(`Output: ${outputPath}`)); - - const getSchemaResponse = await this.apiService.getSchema(); + const getSchemaResponse = await this.apiService.getSchema({ authToken }); if (!getSchemaResponse.success) { - return; + throw new Error( + `Failed to introspect schema: ${JSON.stringify(getSchemaResponse.error)}`, + ); } const { data: schema } = getSchemaResponse; - const output = resolve(outputPath); + await fs.ensureDir(outputPath); + await fs.emptyDir(outputPath); await generate({ schema, - output, + output: outputPath, scalarTypes: { DateTime: 'string', JSON: 'Record', @@ -63,17 +46,18 @@ export class ClientService { }, }); - await this.injectTwentyClient(output); + await this.injectTwentyClient(outputPath); + } - console.log(chalk.green('✓ Client generated successfully!')); - console.log(chalk.gray(`Generated files at: ${outputPath}`)); + private resolveGeneratedPath(appPath: string): string { + return join(appPath, 'node_modules', 'twenty-sdk', 'generated'); } private async injectTwentyClient(output: string) { const twentyClientContent = ` // ---------------------------------------------------- -// ✨ Custom Twenty client (auto-injected) +// Custom Twenty client (auto-injected) // ---------------------------------------------------- const defaultOptions: ClientOptions = { diff --git a/packages/twenty-sdk/src/cli/utilities/config/config-service.ts b/packages/twenty-sdk/src/cli/utilities/config/config-service.ts index 8fa6bc3475..366c087e9d 100644 --- a/packages/twenty-sdk/src/cli/utilities/config/config-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/config/config-service.ts @@ -6,6 +6,8 @@ import { getConfigPath } from '@/cli/utilities/config/get-config-path'; export type TwentyConfig = { apiUrl: string; apiKey?: string; + applicationAccessToken?: string; + applicationRefreshToken?: string; }; type PersistedConfig = TwentyConfig & { @@ -59,10 +61,14 @@ export class ConfigService { // Fallback to legacy top-level values if profile value is missing const apiUrl = profileConfig?.apiUrl ?? defaultConfig.apiUrl; const apiKey = profileConfig?.apiKey; + const applicationAccessToken = profileConfig?.applicationAccessToken; + const applicationRefreshToken = profileConfig?.applicationRefreshToken; return { apiUrl, apiKey, + applicationAccessToken, + applicationRefreshToken, }; } catch { return defaultConfig; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts b/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts deleted file mode 100644 index 620b67534f..0000000000 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { - type ManifestBuildResult, - manifestUpdateChecksums, -} from '@/cli/utilities/build/manifest/manifest-update-checksums'; -import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer'; -import { ApiService } from '@/cli/utilities/api/api-service'; -import { FileUploader } from '@/cli/utilities/file/file-uploader'; -import { type FileFolder } from 'twenty-shared/types'; -import type { Location } from 'esbuild'; -import { type DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager'; -import { type EventName } from 'chokidar/handler.js'; -import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build'; -import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate'; - -export type DevModeOrchestratorOptions = { - appPath: string; - debounceMs?: number; - handleManifestBuilt: (result: ManifestBuildResult) => void | Promise; - uiStateManager: DevUiStateManager; -}; - -export class DevModeOrchestrator { - private appPath: string; - private debounceMs: number; - - private builtFileInfos = new Map< - string, - { - checksum: string; - builtPath: string; - sourcePath: string; - fileFolder: FileFolder; - } - >(); - - private fileUploader: FileUploader | null = null; - private apiService = new ApiService({ disableInterceptors: true }); - - private activeUploads = new Set>(); - - private syncTimer: NodeJS.Timeout | null = null; - private isSyncing = false; - private uiStateManager: DevUiStateManager; - private serverReady = false; - private serverErrorLogged = false; - - private handleManifestBuilt: ( - result: ManifestBuildResult, - ) => void | Promise; - - constructor(options: DevModeOrchestratorOptions) { - this.appPath = options.appPath; - this.debounceMs = options.debounceMs ?? 200; - this.handleManifestBuilt = options.handleManifestBuilt; - this.uiStateManager = options.uiStateManager; - } - - private async checkServer(): Promise { - const validateAuth = await this.apiService.validateAuth(); - - if (!validateAuth.serverUp) { - if (!this.serverErrorLogged) { - this.uiStateManager.addEvent({ - message: 'Cannot reach server', - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - error: 'Cannot connect to Twenty server. Is it running?', - }); - this.serverErrorLogged = true; - } - return; - } - if (!validateAuth.authValid) { - if (!this.serverErrorLogged) { - this.uiStateManager.addEvent({ - message: 'Authentication failed', - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - error: - 'Cannot authenticate. Check your credentials are correct with "yarn auth:login"', - }); - this.serverErrorLogged = true; - } - return; - } - this.serverErrorLogged = false; - this.serverReady = true; - } - - async handleChangeDetected(sourcePath: string, event: EventName) { - if (!this.serverReady) { - await this.checkServer(); - } - - if (!this.serverReady) { - return; - } - - this.uiStateManager.addEvent({ - message: `Change detected: ${sourcePath}`, - status: 'info', - }); - - if (event === 'unlink') { - this.uiStateManager.removeEntity(sourcePath); - } else { - this.uiStateManager.updateFileStatus(sourcePath, 'building'); - } - - this.scheduleSync(); - } - - handleFileBuildError( - errors: { error: string; location: Location | null }[], - ): void { - this.uiStateManager.addEvent({ - message: 'Build failed:', - status: 'error', - }); - for (const error of errors) { - this.uiStateManager.addEvent({ - message: error.error, - status: 'error', - }); - } - } - - handleFileBuilt({ - fileFolder, - builtPath, - sourcePath, - checksum, - }: { - fileFolder: FileFolder; - builtPath: string; - sourcePath: string; - checksum: string; - }): void { - this.uiStateManager.addEvent({ - message: `Successfully built ${builtPath}`, - status: 'success', - }); - - this.builtFileInfos.set(builtPath, { - checksum, - builtPath, - sourcePath, - fileFolder, - }); - - if (this.fileUploader) { - this.uploadFile(builtPath, sourcePath, fileFolder); - } - - this.scheduleSync(); - } - - private uploadFile( - builtPath: string, - sourcePath: string, - fileFolder: FileFolder, - ): void { - this.uiStateManager.addEvent({ - message: `Uploading ${builtPath}`, - status: 'info', - }); - this.uiStateManager.updateFileStatus(sourcePath, 'uploading'); - const uploadPromise = this.fileUploader!.uploadFile({ - builtPath, - fileFolder, - }) - .then((result) => { - if (result.success) { - this.uiStateManager.addEvent({ - message: `Successfully uploaded ${builtPath}`, - status: 'success', - }); - this.uiStateManager.updateFileStatus(sourcePath, 'success'); - } else { - this.uiStateManager.addEvent({ - message: `Failed to upload ${builtPath}: ${result.error}`, - status: 'error', - }); - } - }) - .catch((error) => { - this.uiStateManager.addEvent({ - message: `Upload failed for ${builtPath}: ${error}`, - status: 'error', - }); - }) - .finally(() => { - this.activeUploads.delete(uploadPromise); - }); - - this.activeUploads.add(uploadPromise); - } - - private cancelPendingSync(): void { - if (this.syncTimer) { - clearTimeout(this.syncTimer); - this.syncTimer = null; - } - } - - private scheduleSync(): void { - this.cancelPendingSync(); - - this.syncTimer = setTimeout(() => { - this.syncTimer = null; - void this.performSync(); - }, this.debounceMs); - } - - private async performSync(): Promise { - if (this.isSyncing) { - return; - } - - this.isSyncing = true; - - try { - this.uiStateManager.addEvent({ - message: 'Building manifest', - status: 'info', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'building', - }); - - const result = await buildManifest(this.appPath); - - if (result.errors.length > 0 || !result.manifest) { - for (const error of result.errors) { - this.uiStateManager.addEvent({ - message: error, - status: 'error', - }); - } - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - error: result.errors[result.errors.length - 1], - }); - return; - } - - const validation = manifestValidate(result.manifest); - - if (!validation.isValid) { - for (const e of validation.errors) { - this.uiStateManager.addEvent({ - message: e, - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - error: e, - }); - } - return; - } - - this.uiStateManager.updateManifestState({ - appName: result.manifest.application.displayName, - }); - - this.uiStateManager.updateAllFilesTypes({ - manifestFilePaths: result.filePaths, - }); - - if (validation.warnings.length > 0) { - for (const warning of validation.warnings) { - this.uiStateManager.addEvent({ - message: `⚠ ${warning}`, - status: 'warning', - }); - } - } - - this.uiStateManager.addEvent({ - message: 'Successfully built manifest', - status: 'success', - }); - - await this.handleManifestBuilt(result); - - if (!this.fileUploader) { - const checkApplicationExistResult = - await this.apiService.checkApplicationExist( - result.manifest.application.universalIdentifier, - ); - - if (!checkApplicationExistResult.success) { - this.uiStateManager.addEvent({ - message: `Failed to check if application ${result.manifest.application.universalIdentifier} already exists`, - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - error: `Failed to check if application already exists`, - }); - return; - } - - const applicationExists = checkApplicationExistResult.data; - - if (!applicationExists) { - this.uiStateManager.addEvent({ - message: 'Creating application', - status: 'info', - }); - - const createApplicationResult = - await this.apiService.createApplication(result.manifest); - - if (createApplicationResult.success) { - this.uiStateManager.addEvent({ - message: 'Application created', - status: 'success', - }); - } else { - this.uiStateManager.addEvent({ - message: `Application creation failed with error ${JSON.stringify(createApplicationResult.error, null, 2)}`, - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - error: `Application creation failed with error ${JSON.stringify(createApplicationResult.error, null, 2)}`, - }); - return; - } - } - - this.fileUploader = new FileUploader({ - appPath: this.appPath, - applicationUniversalIdentifier: - result.manifest.application.universalIdentifier, - }); - - for (const [ - builtPath, - { fileFolder, sourcePath }, - ] of this.builtFileInfos.entries()) { - this.uploadFile(builtPath, sourcePath, fileFolder); - } - } - - while (this.activeUploads.size > 0) { - await Promise.all(this.activeUploads); - } - - const manifest = manifestUpdateChecksums({ - manifest: result.manifest, - builtFileInfos: this.builtFileInfos, - }); - - this.uiStateManager.addEvent({ - message: 'Manifest checksums set', - status: 'info', - }); - - await writeManifestToOutput(this.appPath, manifest); - - this.uiStateManager.addEvent({ - message: 'Manifest saved to output directory', - status: 'info', - }); - - this.uiStateManager.addEvent({ - message: 'Syncing manifest', - status: 'info', - }); - - this.uiStateManager.updateManifestState({ - manifestStatus: 'syncing', - }); - - const syncResult = await this.apiService.syncApplication(manifest); - - this.uiStateManager.updateAllFilesStatus('success'); - - if (syncResult.success) { - this.uiStateManager.addEvent({ - message: '✓ Synced', - status: 'success', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'synced', - }); - } else { - this.uiStateManager.addEvent({ - message: `Sync failed with error ${JSON.stringify(syncResult.error, null, 2)}`, - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - }); - } - } catch (error) { - this.uiStateManager.addEvent({ - message: `Sync failed with error ${JSON.stringify(error, null, 2)}`, - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - }); - } finally { - this.isSyncing = false; - } - } -} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts deleted file mode 100644 index f1a0f6d480..0000000000 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { SyncableEntity } from 'twenty-shared/application'; -import { - type FileStatus, - type Listener, - type ManifestStatus, - type UiEvent, - type DevUiState, -} from '@/cli/utilities/dev/dev-ui-state'; -import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config'; - -const MAX_EVENT_NUMBER = 200; - -const FILE_STATUS_TRANSITION_MATRIX: Record = { - pending: ['building', 'uploading', 'success'], - building: ['pending', 'uploading', 'success'], - uploading: ['pending', 'success'], - success: ['pending', 'building', 'uploading'], -}; - -export class DevUiStateManager { - private state: DevUiState; - private eventIdCounter = 0; - private listeners = new Set(); - - constructor({ - appPath, - frontendUrl, - }: { - appPath: string; - frontendUrl?: string; - }) { - this.state = { - appPath, - frontendUrl, - appName: null, - appDescription: null, - appUniversalIdentifier: null, - manifestStatus: 'idle', - entities: new Map(), - events: [], - }; - } - - getSnapshot(): DevUiState { - return this.state; - } - - subscribe(listener: Listener): () => void { - this.listeners.add(listener); - listener(this.getSnapshot()); - return () => this.listeners.delete(listener); - } - - private notify(): void { - for (const listener of this.listeners) { - listener(this.state); - } - } - - addEvent({ - message, - status = 'info', - }: { - message: string; - status: UiEvent['status']; - }): void { - const event: UiEvent = { - id: ++this.eventIdCounter, - timestamp: new Date(), - message: message.slice(0, 5_000), - status, - }; - - this.state = { - ...this.state, - events: [...this.state.events.slice(-MAX_EVENT_NUMBER - 1), event], - }; - - this.notify(); - } - - updateManifestState({ - manifestStatus, - appName, - error, - }: { - manifestStatus?: ManifestStatus; - appName?: string; - error?: string; - }): void { - this.state = { - ...this.state, - ...(manifestStatus ? { manifestStatus } : {}), - ...(appName ? { appName } : {}), - ...(error ? { error: error.slice(0, 5_000) } : { error: undefined }), - }; - - this.notify(); - } - - convertEntityTypeToSyncableEntity( - entityType: string, - ): SyncableEntity | undefined { - switch (entityType) { - case 'objects': - return SyncableEntity.Object; - case 'fields': - return SyncableEntity.Field; - case 'logicFunctions': - return SyncableEntity.LogicFunction; - case 'frontComponents': - return SyncableEntity.FrontComponent; - case 'roles': - return SyncableEntity.Role; - default: - return; - } - } - - updateAllFilesTypes({ - manifestFilePaths, - }: { - manifestFilePaths: EntityFilePaths; - }): void { - const entityMaps = new Map(); - - (Object.entries(manifestFilePaths) as [SyncableEntity, string[]][]).forEach( - ([entityType, filePaths]) => { - filePaths.forEach((filePath) => { - const syncableEntity = - this.convertEntityTypeToSyncableEntity(entityType); - - if (!syncableEntity) { - return; - } - entityMaps.set(filePath, syncableEntity); - }); - }, - ); - - const entities = new Map(this.state.entities); - - for (const [filePath, entity] of entities) { - entities.set(filePath, { - ...entity, - type: entityMaps.get(filePath), - }); - } - this.state = { ...this.state, entities }; - - this.notify(); - } - - updateAllFilesStatus(status: FileStatus): void { - const entities = new Map(this.state.entities); - - for (const [filePath, entity] of entities) { - entities.set(filePath, { - ...entity, - status: status, - }); - } - this.state = { ...this.state, entities }; - - this.notify(); - } - - removeEntity(filePath: string) { - const entities = new Map(this.state.entities); - entities.delete(filePath); - this.state = { ...this.state, entities }; - } - - updateFileStatus(filePath: string, status: FileStatus): void { - const entities = new Map(this.state.entities); - - const entity = entities.get(filePath); - - if ( - entity?.status && - !FILE_STATUS_TRANSITION_MATRIX[entity.status].find( - (nextStatus) => nextStatus === status, - ) - ) { - return; - } - - entities.set( - filePath, - entity - ? { ...entity, status } - : { - name: filePath, - path: filePath, - status, - }, - ); - - this.state = { ...this.state, entities }; - - this.notify(); - } -} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state.ts b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state.ts deleted file mode 100644 index ed6f9830b4..0000000000 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type SyncableEntity } from 'twenty-shared/application'; - -export type UiEvent = { - id: number; - timestamp: Date; - message: string; - status: 'info' | 'success' | 'error' | 'warning'; -}; - -export type ManifestStatus = - | 'idle' - | 'building' - | 'syncing' - | 'synced' - | 'error'; - -export type FileStatus = 'pending' | 'building' | 'uploading' | 'success'; - -export type EntityInfo = { - name: string; - path: string; - type?: SyncableEntity; - status: FileStatus; -}; - -export type DevUiState = { - appPath: string; - appName: string | null; - appDescription: string | null; - appUniversalIdentifier: string | null; - frontendUrl?: string | null; - manifestStatus: ManifestStatus; - error?: string | null; - entities: Map; - events: UiEvent[]; -}; - -export type Listener = (state: DevUiState) => void; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx deleted file mode 100644 index 575a49cee9..0000000000 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { - type UiEvent, - type DevUiState, - type FileStatus, - type EntityInfo, -} from '@/cli/utilities/dev/dev-ui-state'; -import { SyncableEntity } from 'twenty-shared/application'; -import { type DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager'; - -const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; -const UPLOAD_FRAMES = ['↑', '⇡', '↟', '⤒']; - -const STATUS_ICONS: Record = { - pending: '○', - building: '◐', - uploading: '↑', - success: '✓', -}; - -const STATUS_COLORS: Record = { - pending: 'gray', - building: 'yellow', - uploading: 'cyan', - success: 'green', -}; - -const ENTITY_LABELS: Record = { - [SyncableEntity.Object]: 'Objects', - [SyncableEntity.Field]: 'Fields', - [SyncableEntity.LogicFunction]: 'Logic functions', - [SyncableEntity.FrontComponent]: 'Front components', - [SyncableEntity.Role]: 'Roles', -}; - -const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[]; - -const EVENT_COLORS: Record = { - info: 'gray', - success: 'green', - error: 'red', - warning: 'yellow', -}; - -const groupEntitiesByType = ( - entities: Map, -): Map => { - const grouped = new Map(); - - for (const type of ENTITY_ORDER) { - grouped.set(type, []); - } - - for (const entity of entities.values()) { - if (!entity.type) { - continue; - } - const list = grouped.get(entity.type) ?? []; - list.push(entity); - grouped.set(entity.type, list); - } - - return grouped; -}; - -const formatTime = (date: Date): string => { - return date.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); -}; - -const shortenPath = (path: string, maxLength = 40): string => { - if (path.length <= maxLength) return path; - const parts = path.split('/'); - if (parts.length <= 2) return path; - return `.../${parts.slice(-2).join('/')}`; -}; - -const getApplicationUrl = (snapshot: DevUiState): string | null => { - if (!snapshot.frontendUrl || !snapshot.appUniversalIdentifier) { - return null; - } - return `${snapshot.frontendUrl}/settings/applications`; -}; - -export const renderDevUI = async ( - uiStateManager: DevUiStateManager, -): Promise<{ unmount: () => void }> => { - const [React, ink] = await Promise.all([import('react'), import('ink')]); - - const { useState, useEffect } = React; - const { render, Box, Text, Static } = ink; - - const useSpinner = (frames: string[], interval = 80): string => { - const [frameIndex, setFrameIndex] = useState(0); - - useEffect(() => { - const timer = setInterval(() => { - setFrameIndex((prev) => (prev + 1) % frames.length); - }, interval); - - return () => clearInterval(timer); - }, [frames.length, interval]); - - return frames[frameIndex]; - }; - - const EventItem = ({ event }: { event: UiEvent }): React.ReactElement => { - const color = EVENT_COLORS[event.status]; - const time = formatTime(event.timestamp); - - return ( - - {time} - {event.message} - - ); - }; - - const StatusIcon = ({ - status, - }: { - status: FileStatus; - }): React.ReactElement => { - const buildingFrame = useSpinner(SPINNER_FRAMES, 200); - const uploadingFrame = useSpinner(UPLOAD_FRAMES, 200); - - const iconByStatus: Record = { - building: buildingFrame, - uploading: uploadingFrame, - pending: STATUS_ICONS.pending, - success: STATUS_ICONS.success, - }; - - return {iconByStatus[status]} ; - }; - - const EntityRow = ({ - entity, - }: { - entity: EntityInfo; - }): React.ReactElement => { - return ( - - - {entity.name} - {entity.path !== entity.name && ( - ({shortenPath(entity.path)}) - )} - - ); - }; - - const EntitySection = ({ - type, - entities, - }: { - type: SyncableEntity; - entities: EntityInfo[]; - }): React.ReactElement | null => { - if (entities.length === 0) return null; - - return ( - - - {ENTITY_LABELS[type]} - - {entities.map((entity) => ( - - ))} - - ); - }; - - const MANIFEST_STATUS_CONFIG = { - synced: { color: 'green', icon: '✓', text: 'Synced' }, - building: { color: 'yellow', icon: 'spinner', text: 'Building...' }, - syncing: { color: 'yellow', icon: 'spinner', text: 'Syncing...' }, - error: { color: 'red', icon: null, text: 'Error' }, - idle: { color: 'gray', icon: null, text: 'Idle' }, - } as const; - - const UnifiedStatusIndicator = ({ - snapshot, - }: { - snapshot: DevUiState; - }): React.ReactElement => { - const spinnerFrame = useSpinner(SPINNER_FRAMES, 80); - const config = MANIFEST_STATUS_CONFIG[snapshot.manifestStatus]; - const icon = config.icon === 'spinner' ? spinnerFrame : config.icon; - - return ( - - {icon ? `${icon} ` : ''} - {config.text} - {snapshot.error && `: ${snapshot.error}`} - - ); - }; - - const ApplicationPanel = ({ - snapshot, - }: { - snapshot: DevUiState; - }): React.ReactElement => { - const groupedEntities = groupEntitiesByType(snapshot.entities); - const appUrl = getApplicationUrl(snapshot); - - return ( - - - Application - - - - Name: - {snapshot.appName ?? 'Loading...'} - - {snapshot.appDescription && ( - - Description: - {snapshot.appDescription} - - )} - - Status: - - - {appUrl && ( - - Open: - - {' '} - {appUrl} - - - )} - - - - {ENTITY_ORDER.map((type) => { - const entities = groupedEntities.get(type) ?? []; - return ; - })} - - - ); - }; - - const Legend = (): React.ReactElement => ( - - - {STATUS_ICONS.pending}{' '} - pending {SPINNER_FRAMES[0]}{' '} - building {UPLOAD_FRAMES[0]}{' '} - uploading{' '} - {STATUS_ICONS.success}{' '} - success - - - ); - - const DevUI = (): React.ReactElement => { - const [snapshot, setSnapshot] = useState( - uiStateManager.getSnapshot(), - ); - - useEffect(() => { - return uiStateManager.subscribe(setSnapshot); - }, []); - - return ( - <> - - {(event: UiEvent) => } - - - - - - - - ); - }; - - const { unmount } = render(); - return { unmount }; -}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state.ts new file mode 100644 index 0000000000..ece1b0b0a1 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state.ts @@ -0,0 +1,280 @@ +import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config'; +import { type BuildManifestOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step'; +import { type CheckServerOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step'; +import { type ResolveApplicationOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step'; +import { type StartWatchersOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step'; +import { type SyncApplicationOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step'; +import { type UploadFilesOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step'; +import { type Manifest, SyncableEntity } from 'twenty-shared/application'; +import { type FileFolder } from 'twenty-shared/types'; + +export type OrchestratorStateStepEvent = { + message: string; + status: 'info' | 'success' | 'error' | 'warning'; +}; + +export type OrchestratorStateEvent = OrchestratorStateStepEvent & { + id: number; + timestamp: Date; +}; + +export type OrchestratorStateSyncStatus = + | 'idle' + | 'building' + | 'syncing' + | 'synced' + | 'error'; + +export type OrchestratorStateStepStatus = + | 'idle' + | 'in_progress' + | 'done' + | 'error'; + +export type OrchestratorStepState = { + output: TOutput; + status: OrchestratorStateStepStatus; +}; + +export type OrchestratorStateFileStatus = + | 'pending' + | 'building' + | 'uploading' + | 'success'; + +export type OrchestratorStateEntityInfo = { + name: string; + path: string; + type?: SyncableEntity; + status: OrchestratorStateFileStatus; +}; + +export type OrchestratorStateBuiltFileInfo = { + checksum: string; + builtPath: string; + sourcePath: string; + fileFolder: FileFolder; +}; + +export type OrchestratorStatePipeline = { + status: OrchestratorStateSyncStatus; + isSyncing: boolean; + error: string | null; + appName: string | null; +}; + +const ENTITY_TYPE_TO_SYNCABLE: Record = { + objects: SyncableEntity.Object, + fields: SyncableEntity.Field, + logicFunctions: SyncableEntity.LogicFunction, + frontComponents: SyncableEntity.FrontComponent, + roles: SyncableEntity.Role, +}; + +const MAX_EVENT_COUNT = 200; + +const FILE_STATUS_TRANSITION_MATRIX: Record< + OrchestratorStateFileStatus, + OrchestratorStateFileStatus[] +> = { + pending: ['building', 'uploading', 'success'], + building: ['pending', 'uploading', 'success'], + uploading: ['pending', 'success'], + success: ['pending', 'building', 'uploading'], +}; + +export class OrchestratorState { + appPath: string; + frontendUrl?: string; + + steps: { + checkServer: OrchestratorStepState; + ensureValidTokens: OrchestratorStepState>; + resolveApplication: OrchestratorStepState; + buildManifest: OrchestratorStepState; + uploadFiles: OrchestratorStepState; + generateApiClient: OrchestratorStepState>; + syncApplication: OrchestratorStepState; + startWatchers: OrchestratorStepState; + }; + + previousObjectsFieldsFingerprint: string | null; + + pipeline: OrchestratorStatePipeline; + + entities: Map; + events: OrchestratorStateEvent[]; + + private eventIdCounter = 0; + onChange?: () => void; + + constructor(options: { appPath: string; frontendUrl?: string }) { + this.appPath = options.appPath; + this.frontendUrl = options.frontendUrl; + + this.previousObjectsFieldsFingerprint = null; + + this.steps = { + checkServer: { + output: { isReady: false, errorLogged: false }, + status: 'idle', + }, + ensureValidTokens: { + output: {}, + status: 'idle', + }, + resolveApplication: { + output: { applicationId: null, universalIdentifier: null }, + status: 'idle', + }, + buildManifest: { + output: { result: null }, + status: 'idle', + }, + uploadFiles: { + output: { + fileUploader: null, + builtFileInfos: new Map(), + activeUploads: new Set(), + }, + status: 'idle', + }, + generateApiClient: { + output: {}, + status: 'idle', + }, + syncApplication: { + output: { syncStatus: 'idle', error: null }, + status: 'idle', + }, + startWatchers: { + output: { watchersStarted: false }, + status: 'idle', + }, + }; + + this.pipeline = { + status: 'idle', + isSyncing: false, + error: null, + appName: null, + }; + + this.entities = new Map(); + this.events = []; + } + + notify(): void { + this.onChange?.(); + } + + updatePipeline(update: Partial): void { + Object.assign(this.pipeline, update); + this.notify(); + } + + applyStepEvents(stepEvents: OrchestratorStateStepEvent[]): void { + const enrichedEvents: OrchestratorStateEvent[] = stepEvents.map( + (stepEvent) => { + this.eventIdCounter += 1; + + return { + ...stepEvent, + id: this.eventIdCounter, + timestamp: new Date(), + message: stepEvent.message.slice(0, 5_000), + }; + }, + ); + + this.events = [ + ...this.events.slice(-(MAX_EVENT_COUNT - enrichedEvents.length)), + ...enrichedEvents, + ]; + } + + addEvent(event: OrchestratorStateStepEvent): void { + this.applyStepEvents([event]); + } + + updateEntityStatus( + filePath: string, + status: OrchestratorStateFileStatus, + ): void { + const entities = new Map(this.entities); + const entity = entities.get(filePath); + + if ( + entity?.status && + !FILE_STATUS_TRANSITION_MATRIX[entity.status].includes(status) + ) { + return; + } + + entities.set( + filePath, + entity + ? { ...entity, status } + : { name: filePath, path: filePath, status }, + ); + + this.entities = entities; + } + + removeEntity(filePath: string): void { + const entities = new Map(this.entities); + + entities.delete(filePath); + this.entities = entities; + } + + updateAllEntitiesStatus(status: OrchestratorStateFileStatus): void { + const entities = new Map(this.entities); + + for (const [filePath, entity] of entities) { + entities.set(filePath, { ...entity, status }); + } + + this.entities = entities; + } + + updateEntitiesFromManifest(manifestFilePaths: EntityFilePaths): void { + const entityTypeMap = new Map(); + + for (const [entityType, filePaths] of Object.entries(manifestFilePaths)) { + const syncableEntity = ENTITY_TYPE_TO_SYNCABLE[entityType]; + + if (!syncableEntity) { + continue; + } + + for (const filePath of filePaths as string[]) { + entityTypeMap.set(filePath, syncableEntity); + } + } + + const entities = new Map(this.entities); + + for (const [filePath, entity] of entities) { + entities.set(filePath, { + ...entity, + type: entityTypeMap.get(filePath), + }); + } + + this.entities = entities; + } + + hasObjectsOrFieldsChanged(manifest: Manifest): boolean { + const fingerprint = JSON.stringify({ + objects: manifest.objects, + fields: manifest.fields, + }); + + const changed = fingerprint !== this.previousObjectsFieldsFingerprint; + + this.previousObjectsFieldsFingerprint = fingerprint; + + return changed; + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts new file mode 100644 index 0000000000..4dbfcaf5d3 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/dev-mode-orchestrator.ts @@ -0,0 +1,188 @@ +import { ApiService } from '@/cli/utilities/api/api-service'; +import { ClientService } from '@/cli/utilities/client/client-service'; +import { ConfigService } from '@/cli/utilities/config/config-service'; +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { BuildManifestOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step'; +import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step'; +import { EnsureValidTokensOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step'; +import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step'; +import { ResolveApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step'; +import { StartWatchersOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step'; +import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step'; +import { UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step'; +import * as fs from 'fs-extra'; +import path from 'path'; +import { OUTPUT_DIR, type Manifest } from 'twenty-shared/application'; + +export type DevModeOrchestratorOptions = { + state: OrchestratorState; + debounceMs?: number; +}; + +export class DevModeOrchestrator { + private state: OrchestratorState; + private debounceMs: number; + private syncTimer: NodeJS.Timeout | null = null; + + private checkServerStep: CheckServerOrchestratorStep; + private ensureValidTokensStep: EnsureValidTokensOrchestratorStep; + private buildManifestStep: BuildManifestOrchestratorStep; + private resolveApplicationStep: ResolveApplicationOrchestratorStep; + private uploadFilesStep: UploadFilesOrchestratorStep; + private generateApiClientStep: GenerateApiClientOrchestratorStep; + private syncApplicationStep: SyncApplicationOrchestratorStep; + private startWatchersStep: StartWatchersOrchestratorStep; + + constructor(options: DevModeOrchestratorOptions) { + this.debounceMs = options.debounceMs ?? 200; + this.state = options.state; + + const apiService = new ApiService({ disableInterceptors: true }); + const configService = new ConfigService(); + const clientService = new ClientService(); + const stepDeps = { state: this.state, notify: () => this.state.notify() }; + + this.checkServerStep = new CheckServerOrchestratorStep({ + ...stepDeps, + apiService, + }); + this.ensureValidTokensStep = new EnsureValidTokensOrchestratorStep({ + ...stepDeps, + apiService, + configService, + }); + this.buildManifestStep = new BuildManifestOrchestratorStep(stepDeps); + this.resolveApplicationStep = new ResolveApplicationOrchestratorStep({ + ...stepDeps, + apiService, + }); + this.uploadFilesStep = new UploadFilesOrchestratorStep(stepDeps); + this.generateApiClientStep = new GenerateApiClientOrchestratorStep({ + ...stepDeps, + clientService, + configService, + }); + this.syncApplicationStep = new SyncApplicationOrchestratorStep({ + ...stepDeps, + apiService, + }); + this.startWatchersStep = new StartWatchersOrchestratorStep({ + ...stepDeps, + scheduleSync: this.scheduleSync.bind(this), + uploadFilesStep: this.uploadFilesStep, + }); + } + + async start(): Promise { + const outputDir = path.join(this.state.appPath, OUTPUT_DIR); + + await fs.ensureDir(outputDir); + await fs.emptyDir(outputDir); + + await this.startWatchersStep.start(); + } + + async close(): Promise { + await this.startWatchersStep.close(); + } + + getState(): OrchestratorState { + return this.state; + } + + private scheduleSync(): void { + if (this.syncTimer) { + clearTimeout(this.syncTimer); + } + + this.syncTimer = setTimeout(() => { + this.syncTimer = null; + void this.performSync(); + }, this.debounceMs); + } + + private async performSync(): Promise { + if (this.state.pipeline.isSyncing) { + return; + } + + this.state.updatePipeline({ isSyncing: true }); + + try { + await this.runSyncPipeline(); + } catch (error) { + this.state.addEvent({ + message: `Sync failed with error ${JSON.stringify(error, null, 2)}`, + status: 'error', + }); + this.state.updatePipeline({ status: 'error' }); + } finally { + this.state.updatePipeline({ isSyncing: false }); + } + } + + private async runSyncPipeline(): Promise { + const isReady = await this.checkServerStep.execute(); + + if (!isReady) { + return; + } + + await this.ensureValidTokensStep.execute({ + applicationId: this.state.steps.resolveApplication.output.applicationId, + }); + + const buildResult = await this.buildManifestStep.execute({ + appPath: this.state.appPath, + }); + + if (!buildResult) { + return; + } + + await this.startWatchersStep.handleWatcherRestarts(buildResult); + + if (!this.uploadFilesStep.isInitialized) { + const initialized = await this.initializePipeline(buildResult.manifest!); + + if (!initialized) { + return; + } + } + + if (this.state.hasObjectsOrFieldsChanged(buildResult.manifest!)) { + await this.generateApiClientStep.execute({ + appPath: this.state.appPath, + }); + } + + await this.uploadFilesStep.waitForUploads(); + + await this.syncApplicationStep.execute({ + manifest: buildResult.manifest!, + builtFileInfos: this.state.steps.uploadFiles.output.builtFileInfos, + appPath: this.state.appPath, + }); + } + + private async initializePipeline(manifest: Manifest): Promise { + const resolveResult = await this.resolveApplicationStep.execute({ + manifest, + }); + + if (!resolveResult.applicationId) { + return false; + } + + await this.ensureValidTokensStep.exchangeTokens({ + applicationId: resolveResult.applicationId, + }); + + this.uploadFilesStep.initialize({ + appPath: this.state.appPath, + universalIdentifier: manifest.application.universalIdentifier, + }); + + return true; + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step.ts new file mode 100644 index 0000000000..f0ae94b5a3 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step.ts @@ -0,0 +1,91 @@ +import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build'; +import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums'; +import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate'; +import { + type OrchestratorState, + type OrchestratorStateStepEvent, +} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; + +export type BuildManifestOrchestratorStepOutput = { + result: ManifestBuildResult | null; +}; + +export class BuildManifestOrchestratorStep { + private state: OrchestratorState; + private notify: () => void; + + constructor({ + state, + notify, + }: { + state: OrchestratorState; + notify: () => void; + }) { + this.state = state; + this.notify = notify; + } + + async execute(input: { + appPath: string; + }): Promise { + const step = this.state.steps.buildManifest; + + step.status = 'in_progress'; + this.state.updatePipeline({ status: 'building' }); + + const events: OrchestratorStateStepEvent[] = [ + { message: 'Building manifest', status: 'info' }, + ]; + + const result = await buildManifest(input.appPath); + + if (result.errors.length > 0 || !result.manifest) { + for (const error of result.errors) { + events.push({ message: error, status: 'error' }); + } + + step.output = { result: null }; + step.status = 'error'; + this.state.updatePipeline({ status: 'error' }); + this.state.applyStepEvents(events); + + return null; + } + + const validation = manifestValidate(result.manifest); + + if (!validation.isValid) { + for (const validationError of validation.errors) { + events.push({ message: validationError, status: 'error' }); + } + + step.output = { result: null }; + step.status = 'error'; + this.state.updatePipeline({ status: 'error' }); + this.state.applyStepEvents(events); + + return null; + } + + if (validation.warnings.length > 0) { + for (const warning of validation.warnings) { + events.push({ message: `⚠ ${warning}`, status: 'warning' }); + } + } + + events.push({ + message: 'Successfully built manifest', + status: 'success', + }); + + step.output = { result }; + step.status = 'done'; + this.state.updatePipeline({ + appName: result.manifest.application.displayName, + }); + this.state.updateEntitiesFromManifest(result.filePaths); + this.state.applyStepEvents(events); + + return result; + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step.ts new file mode 100644 index 0000000000..eb874ccdfb --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step.ts @@ -0,0 +1,64 @@ +import { type ApiService } from '@/cli/utilities/api/api-service'; +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; + +export type CheckServerOrchestratorStepOutput = { + isReady: boolean; + errorLogged: boolean; +}; + +export class CheckServerOrchestratorStep { + private apiService: ApiService; + private state: OrchestratorState; + private notify: () => void; + + constructor({ + apiService, + state, + notify, + }: { + apiService: ApiService; + state: OrchestratorState; + notify: () => void; + }) { + this.apiService = apiService; + this.state = state; + this.notify = notify; + } + + async execute(): Promise { + const step = this.state.steps.checkServer; + const validateAuth = await this.apiService.validateAuth(); + + if (!validateAuth.serverUp) { + if (!step.output.errorLogged) { + step.output = { isReady: false, errorLogged: true }; + step.status = 'error'; + this.state.updatePipeline({ status: 'error' }); + this.state.applyStepEvents([ + { message: 'Cannot reach server', status: 'error' }, + ]); + } + + return false; + } + + if (!validateAuth.authValid) { + if (!step.output.errorLogged) { + step.output = { isReady: false, errorLogged: true }; + step.status = 'error'; + this.state.updatePipeline({ status: 'error' }); + this.state.applyStepEvents([ + { message: 'Authentication failed', status: 'error' }, + ]); + } + + return false; + } + + step.output = { isReady: true, errorLogged: false }; + step.status = 'done'; + this.notify(); + + return true; + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step.ts new file mode 100644 index 0000000000..141b6f0e48 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step.ts @@ -0,0 +1,134 @@ +import { type ApiService } from '@/cli/utilities/api/api-service'; +import { type ConfigService } from '@/cli/utilities/config/config-service'; +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; + +export class EnsureValidTokensOrchestratorStep { + private apiService: ApiService; + private configService: ConfigService; + private state: OrchestratorState; + private notify: () => void; + + constructor({ + apiService, + configService, + state, + notify, + }: { + apiService: ApiService; + configService: ConfigService; + state: OrchestratorState; + notify: () => void; + }) { + this.apiService = apiService; + this.configService = configService; + this.state = state; + this.notify = notify; + } + + async execute(input: { applicationId: string | null }): Promise { + if (!input.applicationId) { + return; + } + + const step = this.state.steps.ensureValidTokens; + + step.status = 'in_progress'; + this.notify(); + + const config = await this.configService.getConfig(); + + if ( + config.applicationAccessToken && + !this.isTokenExpired(config.applicationAccessToken) + ) { + step.status = 'done'; + this.notify(); + + return; + } + + if ( + config.applicationRefreshToken && + !this.isTokenExpired(config.applicationRefreshToken) + ) { + const renewResult = await this.apiService.renewApplicationToken( + config.applicationRefreshToken, + ); + + if (renewResult.success) { + await this.configService.setConfig({ + applicationAccessToken: renewResult.data.applicationAccessToken.token, + applicationRefreshToken: + renewResult.data.applicationRefreshToken.token, + }); + + this.state.applyStepEvents([ + { message: 'Renewing application tokens', status: 'info' }, + { message: 'Application tokens renewed', status: 'success' }, + ]); + step.status = 'done'; + this.notify(); + + return; + } + + this.state.applyStepEvents([ + { message: 'Renewing application tokens', status: 'info' }, + { + message: `Failed to renew application tokens: ${JSON.stringify(renewResult.error, null, 2)}`, + status: 'error', + }, + ]); + + await this.exchangeTokens({ applicationId: input.applicationId }); + + return; + } + + await this.exchangeTokens({ applicationId: input.applicationId }); + } + + async exchangeTokens(input: { applicationId: string }): Promise { + const tokenResult = await this.apiService.generateApplicationToken( + input.applicationId, + ); + + if (!tokenResult.success) { + this.state.applyStepEvents([ + { message: 'Generating application tokens', status: 'info' }, + { + message: `Failed to generate application tokens: ${JSON.stringify(tokenResult.error, null, 2)}`, + status: 'error', + }, + ]); + this.state.steps.ensureValidTokens.status = 'error'; + this.notify(); + + return; + } + + await this.configService.setConfig({ + applicationAccessToken: tokenResult.data.applicationAccessToken.token, + applicationRefreshToken: tokenResult.data.applicationRefreshToken.token, + }); + + this.state.applyStepEvents([ + { message: 'Generating application tokens', status: 'info' }, + { message: 'Application tokens stored in config', status: 'success' }, + ]); + this.state.steps.ensureValidTokens.status = 'done'; + this.notify(); + } + + private isTokenExpired(token: string): boolean { + try { + const payload = JSON.parse( + Buffer.from(token.split('.')[1], 'base64').toString(), + ); + + return Date.now() >= payload.exp * 1000 - 60_000; + } catch { + return true; + } + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step.ts new file mode 100644 index 0000000000..238ec2efca --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step.ts @@ -0,0 +1,55 @@ +import { type ClientService } from '@/cli/utilities/client/client-service'; +import { type ConfigService } from '@/cli/utilities/config/config-service'; +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; + +export class GenerateApiClientOrchestratorStep { + private clientService: ClientService; + private configService: ConfigService; + private state: OrchestratorState; + private notify: () => void; + + constructor({ + clientService, + configService, + state, + notify, + }: { + clientService: ClientService; + configService: ConfigService; + state: OrchestratorState; + notify: () => void; + }) { + this.clientService = clientService; + this.configService = configService; + this.state = state; + this.notify = notify; + } + + async execute(input: { appPath: string }): Promise { + const step = this.state.steps.generateApiClient; + + step.status = 'in_progress'; + this.notify(); + + try { + const config = await this.configService.getConfig(); + + await this.clientService.generate({ + appPath: input.appPath, + authToken: config.applicationAccessToken, + }); + + step.status = 'done'; + } catch (error) { + this.state.applyStepEvents([ + { + message: `Failed to generate API client: ${error instanceof Error ? error.message : String(error)}`, + status: 'error', + }, + ]); + step.status = 'error'; + } + + this.notify(); + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step.ts new file mode 100644 index 0000000000..81123c41fa --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step.ts @@ -0,0 +1,97 @@ +import { type ApiService } from '@/cli/utilities/api/api-service'; +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { type Manifest } from 'twenty-shared/application'; + +export type ResolveApplicationOrchestratorStepOutput = { + applicationId: string | null; + universalIdentifier: string | null; +}; + +export class ResolveApplicationOrchestratorStep { + private apiService: ApiService; + private state: OrchestratorState; + private notify: () => void; + + constructor({ + apiService, + state, + notify, + }: { + apiService: ApiService; + state: OrchestratorState; + notify: () => void; + }) { + this.apiService = apiService; + this.state = state; + this.notify = notify; + } + + async execute(input: { + manifest: Manifest; + }): Promise { + const step = this.state.steps.resolveApplication; + + step.status = 'in_progress'; + this.notify(); + + const universalIdentifier = input.manifest.application.universalIdentifier; + + const findResult = + await this.apiService.findOneApplication(universalIdentifier); + + if (!findResult.success) { + this.state.applyStepEvents([ + { + message: `Failed to find application ${universalIdentifier}`, + status: 'error', + }, + ]); + step.status = 'error'; + this.state.updatePipeline({ status: 'error' }); + + return step.output; + } + + if (findResult.data) { + step.output = { + applicationId: findResult.data.id, + universalIdentifier: findResult.data.universalIdentifier, + }; + step.status = 'done'; + this.notify(); + + return step.output; + } + + const createResult = await this.apiService.createApplication( + input.manifest, + ); + + if (!createResult.success) { + this.state.applyStepEvents([ + { message: 'Creating application', status: 'info' }, + { + message: `Application creation failed with error ${JSON.stringify(createResult.error, null, 2)}`, + status: 'error', + }, + ]); + step.status = 'error'; + this.state.updatePipeline({ status: 'error' }); + + return step.output; + } + + step.output = { + applicationId: createResult.data!.id, + universalIdentifier: createResult.data!.universalIdentifier, + }; + this.state.applyStepEvents([ + { message: 'Creating application', status: 'info' }, + { message: 'Application created', status: 'success' }, + ]); + step.status = 'done'; + this.notify(); + + return step.output; + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts new file mode 100644 index 0000000000..0f13224b38 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step.ts @@ -0,0 +1,210 @@ +import { + createFrontComponentsWatcher, + createLogicFunctionsWatcher, + type EsbuildWatcher, +} from '@/cli/utilities/build/common/esbuild-watcher'; +import { FileUploadWatcher } from '@/cli/utilities/build/common/file-upload-watcher'; +import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums'; +import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher'; +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { type UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step'; +import type { Location } from 'esbuild'; +import { type EventName } from 'chokidar/handler.js'; +import { ASSETS_DIR } from 'twenty-shared/application'; +import { FileFolder } from 'twenty-shared/types'; + +export type StartWatchersOrchestratorStepOutput = { + watchersStarted: boolean; +}; + +export class StartWatchersOrchestratorStep { + private state: OrchestratorState; + private scheduleSync: () => void; + private notify: () => void; + private uploadFilesStep: UploadFilesOrchestratorStep; + + private manifestWatcher: ManifestWatcher | null = null; + private logicFunctionsWatcher: EsbuildWatcher | null = null; + private frontComponentsWatcher: EsbuildWatcher | null = null; + private assetWatcher: FileUploadWatcher | null = null; + private dependencyWatcher: FileUploadWatcher | null = null; + + constructor(options: { + state: OrchestratorState; + scheduleSync: () => void; + notify: () => void; + uploadFilesStep: UploadFilesOrchestratorStep; + }) { + this.state = options.state; + this.scheduleSync = options.scheduleSync; + this.notify = options.notify; + this.uploadFilesStep = options.uploadFilesStep; + } + + async start(): Promise { + this.state.steps.startWatchers.status = 'in_progress'; + this.notify(); + + this.manifestWatcher = new ManifestWatcher({ + appPath: this.state.appPath, + handleChangeDetected: this.handleChangeDetected.bind(this), + }); + + await this.manifestWatcher.start(); + } + + async handleWatcherRestarts(result: ManifestBuildResult): Promise { + const { logicFunctions, frontComponents } = result.filePaths; + + if (!this.state.steps.startWatchers.output.watchersStarted) { + this.state.steps.startWatchers.output.watchersStarted = true; + this.state.steps.startWatchers.status = 'done'; + await this.startFileWatchers(logicFunctions, frontComponents); + + return; + } + + if (this.logicFunctionsWatcher?.shouldRestart(logicFunctions)) { + await this.logicFunctionsWatcher.restart(logicFunctions); + } + + if (this.frontComponentsWatcher?.shouldRestart(frontComponents)) { + await this.frontComponentsWatcher.restart(frontComponents); + } + } + + async close(): Promise { + await Promise.all([ + this.manifestWatcher?.close(), + this.logicFunctionsWatcher?.close(), + this.frontComponentsWatcher?.close(), + this.assetWatcher?.close(), + this.dependencyWatcher?.close(), + ]); + } + + private handleChangeDetected(sourcePath: string, event: EventName): void { + this.state.addEvent({ + message: `Change detected: ${sourcePath}`, + status: 'info', + }); + + if (event === 'unlink') { + this.state.removeEntity(sourcePath); + } else { + this.state.updateEntityStatus(sourcePath, 'building'); + } + + this.notify(); + this.scheduleSync(); + } + + private handleFileBuildError( + errors: { error: string; location: Location | null }[], + ): void { + this.state.addEvent({ + message: 'Build failed:', + status: 'error', + }); + + for (const error of errors) { + this.state.addEvent({ + message: error.error, + status: 'error', + }); + } + + this.notify(); + } + + private handleFileBuilt({ + fileFolder, + builtPath, + sourcePath, + checksum, + }: { + fileFolder: FileFolder; + builtPath: string; + sourcePath: string; + checksum: string; + }): void { + this.state.addEvent({ + message: `Successfully built ${builtPath}`, + status: 'success', + }); + + this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, { + checksum, + builtPath, + sourcePath, + fileFolder, + }); + + if (this.state.steps.uploadFiles.output.fileUploader) { + this.uploadFilesStep.uploadFile(builtPath, sourcePath, fileFolder); + } + + this.notify(); + this.scheduleSync(); + } + + private async startFileWatchers( + logicFunctions: string[], + frontComponents: string[], + ): Promise { + await Promise.all([ + this.startLogicFunctionsWatcher(logicFunctions), + this.startFrontComponentsWatcher(frontComponents), + this.startAssetWatcher(), + this.startDependencyWatcher(), + ]); + } + + private async startLogicFunctionsWatcher( + sourcePaths: string[], + ): Promise { + this.logicFunctionsWatcher = createLogicFunctionsWatcher({ + appPath: this.state.appPath, + sourcePaths, + handleBuildError: this.handleFileBuildError.bind(this), + handleFileBuilt: this.handleFileBuilt.bind(this), + }); + + await this.logicFunctionsWatcher.start(); + } + + private async startFrontComponentsWatcher( + sourcePaths: string[], + ): Promise { + this.frontComponentsWatcher = createFrontComponentsWatcher({ + appPath: this.state.appPath, + sourcePaths, + handleBuildError: this.handleFileBuildError.bind(this), + handleFileBuilt: this.handleFileBuilt.bind(this), + }); + + await this.frontComponentsWatcher.start(); + } + + private async startAssetWatcher(): Promise { + this.assetWatcher = new FileUploadWatcher({ + appPath: this.state.appPath, + fileFolder: FileFolder.PublicAsset, + watchPaths: [ASSETS_DIR], + handleFileBuilt: this.handleFileBuilt.bind(this), + }); + + await this.assetWatcher.start(); + } + + private async startDependencyWatcher(): Promise { + this.dependencyWatcher = new FileUploadWatcher({ + appPath: this.state.appPath, + fileFolder: FileFolder.Dependencies, + watchPaths: ['package.json', 'yarn.lock'], + handleFileBuilt: this.handleFileBuilt.bind(this), + }); + + this.dependencyWatcher.start(); + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step.ts new file mode 100644 index 0000000000..aa86e38e6d --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step.ts @@ -0,0 +1,84 @@ +import { type ApiService } from '@/cli/utilities/api/api-service'; +import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums'; +import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer'; +import { + type OrchestratorState, + type OrchestratorStateBuiltFileInfo, + type OrchestratorStateStepEvent, + type OrchestratorStateSyncStatus, +} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { type Manifest } from 'twenty-shared/application'; + +export type SyncApplicationOrchestratorStepOutput = { + syncStatus: OrchestratorStateSyncStatus; + error: string | null; +}; + +export class SyncApplicationOrchestratorStep { + private apiService: ApiService; + private state: OrchestratorState; + private notify: () => void; + + constructor({ + apiService, + state, + notify, + }: { + apiService: ApiService; + state: OrchestratorState; + notify: () => void; + }) { + this.apiService = apiService; + this.state = state; + this.notify = notify; + } + + async execute(input: { + manifest: Manifest; + builtFileInfos: Map; + appPath: string; + }): Promise { + const step = this.state.steps.syncApplication; + + step.status = 'in_progress'; + this.state.updatePipeline({ status: 'syncing' }); + + const events: OrchestratorStateStepEvent[] = []; + + const manifest = manifestUpdateChecksums({ + manifest: input.manifest, + builtFileInfos: input.builtFileInfos, + }); + + events.push({ message: 'Manifest checksums set', status: 'info' }); + + await writeManifestToOutput(input.appPath, manifest); + + events.push({ + message: 'Manifest saved to output directory', + status: 'info', + }); + events.push({ message: 'Syncing manifest', status: 'info' }); + + const syncResult = await this.apiService.syncApplication(manifest); + + if (syncResult.success) { + events.push({ message: '✓ Synced', status: 'success' }); + step.output = { syncStatus: 'synced', error: null }; + step.status = 'done'; + this.state.updatePipeline({ status: 'synced', error: null }); + this.state.updateAllEntitiesStatus('success'); + this.state.applyStepEvents(events); + + return; + } + + const errorMessage = `Sync failed with error ${JSON.stringify(syncResult.error, null, 2)}`; + + events.push({ message: errorMessage, status: 'error' }); + step.output = { syncStatus: 'error', error: errorMessage }; + step.status = 'error'; + this.state.updatePipeline({ status: 'error', error: errorMessage }); + this.state.applyStepEvents(events); + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts new file mode 100644 index 0000000000..056efdffe3 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step.ts @@ -0,0 +1,114 @@ +import { + type OrchestratorState, + type OrchestratorStateBuiltFileInfo, +} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { FileUploader } from '@/cli/utilities/file/file-uploader'; +import { type FileFolder } from 'twenty-shared/types'; + +export type UploadFilesOrchestratorStepOutput = { + fileUploader: FileUploader | null; + builtFileInfos: Map; + activeUploads: Set>; +}; + +export class UploadFilesOrchestratorStep { + private state: OrchestratorState; + private notify: () => void; + + constructor({ + state, + notify, + }: { + state: OrchestratorState; + notify: () => void; + }) { + this.state = state; + this.notify = notify; + } + + get isInitialized(): boolean { + return this.state.steps.uploadFiles.output.fileUploader !== null; + } + + initialize(input: { appPath: string; universalIdentifier: string }): void { + const step = this.state.steps.uploadFiles; + + step.output = { + ...step.output, + fileUploader: new FileUploader({ + appPath: input.appPath, + applicationUniversalIdentifier: input.universalIdentifier, + }), + }; + step.status = 'in_progress'; + this.notify(); + + this.uploadPendingFiles(); + } + + uploadFile( + builtPath: string, + sourcePath: string, + fileFolder: FileFolder, + ): void { + const step = this.state.steps.uploadFiles; + + if (!step.output.fileUploader) { + return; + } + + this.state.addEvent({ + message: `Uploading ${builtPath}`, + status: 'info', + }); + this.state.updateEntityStatus(sourcePath, 'uploading'); + + const uploadPromise = step.output.fileUploader + .uploadFile({ builtPath, fileFolder }) + .then((result) => { + if (result.success) { + this.state.addEvent({ + message: `Successfully uploaded ${builtPath}`, + status: 'success', + }); + this.state.updateEntityStatus(sourcePath, 'success'); + } else { + this.state.addEvent({ + message: `Failed to upload ${builtPath}: ${result.error}`, + status: 'error', + }); + } + }) + .catch((error) => { + this.state.addEvent({ + message: `Upload failed for ${builtPath}: ${error}`, + status: 'error', + }); + }) + .finally(() => { + step.output.activeUploads.delete(uploadPromise); + }); + + step.output.activeUploads.add(uploadPromise); + } + + async waitForUploads(): Promise { + const step = this.state.steps.uploadFiles; + + while (step.output.activeUploads.size > 0) { + await Promise.all(step.output.activeUploads); + } + + step.status = 'done'; + this.notify(); + } + + private uploadPendingFiles(): void { + for (const [ + builtPath, + { fileFolder, sourcePath }, + ] of this.state.steps.uploadFiles.output.builtFileInfos.entries()) { + this.uploadFile(builtPath, sourcePath, fileFolder); + } + } +} diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-application-panel.tsx b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-application-panel.tsx new file mode 100644 index 0000000000..4f89e82239 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-application-panel.tsx @@ -0,0 +1,127 @@ +import { + type OrchestratorState, + type OrchestratorStateStepStatus, +} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { + DEV_UI_STATUS_CONFIG, + SYNC_STATUS_LABELS, + getApplicationUrl, + getPipelineRows, + groupEntitiesByType, + mapStepStatusToDevUiStatus, + mapSyncStatusToDevUiStatus, +} from '@/cli/utilities/dev/ui/dev-ui-constants'; +import { useStatusIcon } from '@/cli/utilities/dev/ui/dev-ui-hooks'; +import { useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context'; +import { + DevUiEntitySection, + ENTITY_ORDER, +} from '@/cli/utilities/dev/ui/components/dev-ui-entity-section'; +import React from 'react'; + +export const DevUiSyncStatusIndicator = ({ + state, +}: { + state: OrchestratorState; +}): React.ReactElement => { + const { Text } = useInk(); + const uiStatus = mapSyncStatusToDevUiStatus(state.pipeline.status); + const icon = useStatusIcon(uiStatus); + const config = DEV_UI_STATUS_CONFIG[uiStatus]; + const label = SYNC_STATUS_LABELS[state.pipeline.status]; + + return ( + + {icon} {label} + {state.pipeline.error && `: ${state.pipeline.error}`} + + ); +}; + +export const DevUiStepStatusLabel = ({ + label, + status, +}: { + label: string; + status: OrchestratorStateStepStatus; +}): React.ReactElement => { + const { Box, Text } = useInk(); + const uiStatus = mapStepStatusToDevUiStatus(status); + const icon = useStatusIcon(uiStatus); + const config = DEV_UI_STATUS_CONFIG[uiStatus]; + + return ( + + {label}: + + {icon} {status.replace('_', ' ')} + + + ); +}; + +export const DevUiApplicationPanel = ({ + state, +}: { + state: OrchestratorState; +}): React.ReactElement => { + const { Box, Text } = useInk(); + const groupedEntities = groupEntitiesByType(state.entities); + const appUrl = getApplicationUrl(state); + + return ( + + + Application + + + + Name: + {state.pipeline.appName ?? 'Loading...'} + + + Overall Status: + + + {appUrl && ( + + Open: + + {' '} + {appUrl} + + + )} + + + + {getPipelineRows(state).map((row) => ( + + ))} + + + + {ENTITY_ORDER.map((type) => { + const entities = groupedEntities.get(type) ?? []; + + return ( + + ); + })} + + + ); +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-entity-section.tsx b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-entity-section.tsx new file mode 100644 index 0000000000..60fdb32882 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-entity-section.tsx @@ -0,0 +1,101 @@ +import { + type OrchestratorStateEntityInfo, +} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { + type DevUiStatus, + DEV_UI_STATUS_CONFIG, + ENTITY_LABELS, + ENTITY_ORDER, + SPINNER_FRAMES, + UPLOAD_FRAMES, + mapFileStatusToDevUiStatus, + shortenPath, +} from '@/cli/utilities/dev/ui/dev-ui-constants'; +import { useStatusIcon } from '@/cli/utilities/dev/ui/dev-ui-hooks'; +import { useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context'; +import React from 'react'; +import { type SyncableEntity } from 'twenty-shared/application'; + +export const DevUiStatusIcon = ({ + uiStatus, +}: { + uiStatus: DevUiStatus; +}): React.ReactElement => { + const { Text } = useInk(); + const icon = useStatusIcon(uiStatus); + const config = DEV_UI_STATUS_CONFIG[uiStatus]; + + return {icon} ; +}; + +export const DevUiEntityRow = ({ + entity, +}: { + entity: OrchestratorStateEntityInfo; +}): React.ReactElement => { + const { Box, Text } = useInk(); + + return ( + + + {entity.name} + {entity.path !== entity.name && ( + ({shortenPath(entity.path)}) + )} + + ); +}; + +export const DevUiEntitySection = ({ + type, + entities, +}: { + type: SyncableEntity; + entities: OrchestratorStateEntityInfo[]; +}): React.ReactElement | null => { + const { Box, Text } = useInk(); + + if (entities.length === 0) return null; + + return ( + + + {ENTITY_LABELS[type]} + + {entities.map((entity) => ( + + ))} + + ); +}; + +export const DevUiEntityLegend = (): React.ReactElement => { + const { Box, Text } = useInk(); + + return ( + + + + {DEV_UI_STATUS_CONFIG.idle.icon} + {' '} + pending{' '} + + {SPINNER_FRAMES[0]} + {' '} + building{' '} + + {UPLOAD_FRAMES[0]} + {' '} + uploading{' '} + + {DEV_UI_STATUS_CONFIG.done.icon} + {' '} + success + + + ); +}; + +export { ENTITY_ORDER }; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-event-log.tsx b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-event-log.tsx new file mode 100644 index 0000000000..8333a6b9fe --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui-event-log.tsx @@ -0,0 +1,24 @@ +import { type OrchestratorStateEvent } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { + EVENT_COLORS, + formatTime, +} from '@/cli/utilities/dev/ui/dev-ui-constants'; +import { useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context'; +import React from 'react'; + +export const DevUiEventItem = ({ + event, +}: { + event: OrchestratorStateEvent; +}): React.ReactElement => { + const { Box, Text } = useInk(); + const color = EVENT_COLORS[event.status]; + const time = formatTime(event.timestamp); + + return ( + + {time} + {event.message} + + ); +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui.tsx b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui.tsx new file mode 100644 index 0000000000..ce907721cc --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/components/dev-ui.tsx @@ -0,0 +1,54 @@ +import { type OrchestratorStateEvent } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { DevUiApplicationPanel } from '@/cli/utilities/dev/ui/components/dev-ui-application-panel'; +import { DevUiEntityLegend } from '@/cli/utilities/dev/ui/components/dev-ui-entity-section'; +import { DevUiEventItem } from '@/cli/utilities/dev/ui/components/dev-ui-event-log'; +import { InkProvider } from '@/cli/utilities/dev/ui/dev-ui-ink-context'; +import { useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context'; +import { type DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager'; +import React, { useReducer, useEffect } from 'react'; + +const DevUI = ({ + uiStateManager, +}: { + uiStateManager: DevUiStateManager; +}): React.ReactElement => { + const { Box, Static } = useInk(); + + const [, forceRender] = useReducer((tick: number) => tick + 1, 0); + + useEffect(() => { + return uiStateManager.subscribe(() => forceRender()); + }, [uiStateManager]); + + const state = uiStateManager.getSnapshot(); + + return ( + <> + + {(event: OrchestratorStateEvent) => ( + + )} + + + + + + + + ); +}; + +export const renderDevUI = async ( + uiStateManager: DevUiStateManager, +): Promise<{ unmount: () => void }> => { + const ink = await import('ink'); + const { render, Box, Text, Static } = ink; + + const { unmount } = render( + + + , + ); + + return { unmount }; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-constants.ts b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-constants.ts new file mode 100644 index 0000000000..7e9f136a25 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-constants.ts @@ -0,0 +1,224 @@ +import { + type OrchestratorState, + type OrchestratorStateEvent, + type OrchestratorStateFileStatus, + type OrchestratorStateStepStatus, + type OrchestratorStateSyncStatus, + type OrchestratorStateEntityInfo, +} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; +import { SyncableEntity } from 'twenty-shared/application'; + +export type DevUiStatus = + | 'idle' + | 'in_progress' + | 'uploading' + | 'done' + | 'error'; + +export type DevUiStatusConfig = { + color: string; + icon: 'spinner' | 'upload' | string; +}; + +export const DEV_UI_STATUS_CONFIG: Record = { + idle: { color: 'gray', icon: '○' }, + in_progress: { color: 'yellow', icon: 'spinner' }, + uploading: { color: 'cyan', icon: 'upload' }, + done: { color: 'green', icon: '✓' }, + error: { color: 'red', icon: '✗' }, +}; + +export const mapStepStatusToDevUiStatus = ( + status: OrchestratorStateStepStatus, +): DevUiStatus => { + const mapping: Record = { + idle: 'idle', + in_progress: 'in_progress', + done: 'done', + error: 'error', + }; + + return mapping[status]; +}; + +export const mapFileStatusToDevUiStatus = ( + status: OrchestratorStateFileStatus, +): DevUiStatus => { + const mapping: Record = { + pending: 'idle', + building: 'in_progress', + uploading: 'uploading', + success: 'done', + }; + + return mapping[status]; +}; + +export const mapSyncStatusToDevUiStatus = ( + status: OrchestratorStateSyncStatus, +): DevUiStatus => { + const mapping: Record = { + idle: 'idle', + building: 'in_progress', + syncing: 'in_progress', + synced: 'done', + error: 'error', + }; + + return mapping[status]; +}; + +export const SYNC_STATUS_LABELS: Record = { + idle: 'Idle', + building: 'Building...', + syncing: 'Syncing...', + synced: 'Synced', + error: 'Error', +}; + +export const SPINNER_FRAMES = [ + '⠋', + '⠙', + '⠹', + '⠸', + '⠼', + '⠴', + '⠦', + '⠧', + '⠇', + '⠏', +]; + +export const UPLOAD_FRAMES = ['↑', '⇡', '↟', '⤒']; + +export const ENTITY_LABELS: Record = { + [SyncableEntity.Object]: 'Objects', + [SyncableEntity.Field]: 'Fields', + [SyncableEntity.LogicFunction]: 'Logic functions', + [SyncableEntity.FrontComponent]: 'Front components', + [SyncableEntity.Role]: 'Roles', +}; + +export const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[]; + +export const EVENT_COLORS: Record = { + info: 'gray', + success: 'green', + error: 'red', + warning: 'yellow', +}; + +export const formatTime = (date: Date): string => { + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +}; + +export const shortenPath = (path: string, maxLength = 40): string => { + if (path.length <= maxLength) return path; + const parts = path.split('/'); + if (parts.length <= 2) return path; + + return `.../${parts.slice(-2).join('/')}`; +}; + +export const groupEntitiesByType = ( + entities: Map, +): Map => { + const grouped = new Map(); + + for (const type of ENTITY_ORDER) { + grouped.set(type, []); + } + + for (const entity of entities.values()) { + if (!entity.type) { + continue; + } + const list = grouped.get(entity.type) ?? []; + + list.push(entity); + grouped.set(entity.type, list); + } + + return grouped; +}; + +export const getApplicationUrl = (state: OrchestratorState): string | null => { + if ( + !state.frontendUrl || + !state.steps.resolveApplication.output.universalIdentifier + ) { + return null; + } + + return `${state.frontendUrl}/settings/applications`; +}; + +export const mergeStepStatuses = ( + statuses: OrchestratorStateStepStatus[], +): OrchestratorStateStepStatus => { + if (statuses.some((status) => status === 'error')) return 'error'; + if (statuses.some((status) => status === 'in_progress')) return 'in_progress'; + if (statuses.every((status) => status === 'done')) return 'done'; + + return 'idle'; +}; + +export type DevUiPipelineRow = { + label: string; + status: OrchestratorStateStepStatus; +}; + +export const getPipelineRows = ( + state: OrchestratorState, +): DevUiPipelineRow[] => { + const entities = [...state.entities.values()]; + + const isBuilding = entities.some((entity) => entity.status === 'building'); + const allUploaded = + entities.length > 0 && + entities.every( + (entity) => entity.status === 'uploading' || entity.status === 'success', + ); + + const resourcesBuildStatus: OrchestratorStateStepStatus = isBuilding + ? 'in_progress' + : allUploaded + ? 'done' + : 'idle'; + + return [ + { + label: 'Application Initialization', + status: mergeStepStatuses([ + state.steps.checkServer.status, + state.steps.ensureValidTokens.status, + state.steps.resolveApplication.status, + ]), + }, + { + label: 'Resources Build', + status: resourcesBuildStatus, + }, + { + label: 'Resources Upload', + status: state.steps.uploadFiles.status, + }, + { + label: 'Manifest Build', + status: state.steps.buildManifest.status, + }, + { + label: 'Application Synchronization', + status: state.steps.syncApplication.status, + }, + { + label: 'Api Client Generation', + status: state.steps.generateApiClient.status, + }, + ]; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-hooks.ts b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-hooks.ts new file mode 100644 index 0000000000..66f11e3390 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-hooks.ts @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react'; + +import { + type DevUiStatus, + DEV_UI_STATUS_CONFIG, + SPINNER_FRAMES, + UPLOAD_FRAMES, +} from '@/cli/utilities/dev/ui/dev-ui-constants'; + +export const useAnimatedFrame = (frames: string[], interval = 80): string => { + const [frameIndex, setFrameIndex] = useState(0); + + useEffect(() => { + const timer = setInterval(() => { + setFrameIndex((currentIndex) => (currentIndex + 1) % frames.length); + }, interval); + + return () => clearInterval(timer); + }, [frames, interval]); + + return frames[frameIndex]; +}; + +export const useStatusIcon = (uiStatus: DevUiStatus): string => { + const spinnerFrame = useAnimatedFrame(SPINNER_FRAMES, 80); + const uploadFrame = useAnimatedFrame(UPLOAD_FRAMES, 200); + const config = DEV_UI_STATUS_CONFIG[uiStatus]; + + if (config.icon === 'spinner') return spinnerFrame; + if (config.icon === 'upload') return uploadFrame; + + return config.icon; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-ink-context.tsx b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-ink-context.tsx new file mode 100644 index 0000000000..3d743bb99d --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-ink-context.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { Box, Text, Static } from 'ink'; + +type InkComponents = { + Box: typeof Box; + Text: typeof Text; + Static: typeof Static; +}; + +const InkContext = React.createContext(null); + +export const InkProvider = InkContext.Provider; + +export const useInk = (): InkComponents => { + const context = React.useContext(InkContext); + + if (!context) { + throw new Error('useInk must be used within InkProvider'); + } + + return context; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-state-manager.ts b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-state-manager.ts new file mode 100644 index 0000000000..fc01fd4612 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/dev/ui/dev-ui-state-manager.ts @@ -0,0 +1,29 @@ +import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; + +export type DevUiStateListener = (state: OrchestratorState) => void; + +export class DevUiStateManager { + private orchestratorState: OrchestratorState; + private listeners = new Set(); + + constructor(orchestratorState: OrchestratorState) { + this.orchestratorState = orchestratorState; + } + + getSnapshot(): OrchestratorState { + return this.orchestratorState; + } + + subscribe(listener: DevUiStateListener): () => void { + this.listeners.add(listener); + listener(this.orchestratorState); + + return () => this.listeners.delete(listener); + } + + notify(): void { + for (const listener of this.listeners) { + listener(this.orchestratorState); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts index 96904c554a..9a099aa38a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts @@ -26,7 +26,7 @@ import { UploadApplicationFileInput } from 'src/engine/core-modules/application/ import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/dtos/workspace-migration.dto'; import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto'; +import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/dtos/application-token-pair.dto'; import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; @@ -58,13 +58,13 @@ export class ApplicationDevelopmentResolver { private readonly fileStorageService: FileStorageService, ) {} - @Mutation(() => AuthToken) + @Mutation(() => ApplicationTokenPairDTO) @RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED) async generateApplicationToken( @Args() { applicationId }: GenerateApplicationTokenInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ): Promise { - return this.applicationTokenService.generateApplicationAccessToken({ + ): Promise { + return this.applicationTokenService.generateApplicationTokenPair({ workspaceId, applicationId, }); diff --git a/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts index 13902ca6cd..7875fdfe45 100644 --- a/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts @@ -57,22 +57,6 @@ export class ApplicationResolver { return this.applicationService.findManyApplications(workspaceId); } - @Query(() => Boolean) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS)) - @RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED) - async checkApplicationExist( - @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - @Args('id', { type: () => UUIDScalarType, nullable: true }) id?: string, - @Args('universalIdentifier', { type: () => UUIDScalarType, nullable: true }) - universalIdentifier?: string, - ) { - return await this.applicationService.checkApplicationExist({ - id, - universalIdentifier, - workspaceId, - }); - } - @Query(() => ApplicationDTO) @UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS)) @RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED) diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application.service.ts index 2a6b60af5e..65cfd5c0de 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/application.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/application.service.ts @@ -130,20 +130,6 @@ export class ApplicationService { }); } - async checkApplicationExist({ - id, - universalIdentifier, - workspaceId, - }: { - id?: string; - universalIdentifier?: string; - workspaceId: string; - }) { - return isDefined( - await this.findOneApplication({ id, universalIdentifier, workspaceId }), - ); - } - async findOneApplication({ id, universalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/jobs/webhook-job.module.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/jobs/webhook-job.module.ts index 8ca4725b2b..fc7a98d8d4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/jobs/webhook-job.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/jobs/webhook-job.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module'; +import { FlatWebhookModule } from 'src/engine/metadata-modules/flat-webhook/flat-webhook.module'; import { CallWebhookJobsForMetadataJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook-jobs-for-metadata.job'; import { CallWebhookJobsJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook-jobs.job'; import { CallWebhookJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook.job'; @@ -11,6 +12,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache @Module({ imports: [ AuditModule, + FlatWebhookModule, MetricsModule, SecureHttpClientModule, WorkspaceCacheModule, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts index 820a410d18..1e7667c6ec 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/application-token-schema-filtering.integration-spec.ts @@ -59,7 +59,8 @@ describe('Application token schema filtering', () => { expectToFail: false, }); - standardAppToken = tokenData.generateApplicationToken.token; + standardAppToken = + tokenData.generateApplicationToken.applicationAccessToken.token; }); it('should not include custom objects in the schema when using a standard app token', async () => { diff --git a/packages/twenty-server/test/integration/metadata/suites/application/generate-application-token.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/generate-application-token.integration-spec.ts index 4c5d446771..b9baecc873 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/generate-application-token.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/generate-application-token.integration-spec.ts @@ -22,11 +22,16 @@ describe('generateApplicationToken', () => { expectToFail: false, }); - expect(data.generateApplicationToken).toBeDefined(); - expect(data.generateApplicationToken.token).toBeDefined(); - expect(typeof data.generateApplicationToken.token).toBe('string'); - expect(data.generateApplicationToken.token.length).toBeGreaterThan(0); - expect(data.generateApplicationToken.expiresAt).toBeDefined(); + const tokenPair = data.generateApplicationToken; + + expect(tokenPair).toBeDefined(); + expect(tokenPair.applicationAccessToken).toBeDefined(); + expect(typeof tokenPair.applicationAccessToken.token).toBe('string'); + expect(tokenPair.applicationAccessToken.token.length).toBeGreaterThan(0); + expect(tokenPair.applicationAccessToken.expiresAt).toBeDefined(); + expect(tokenPair.applicationRefreshToken).toBeDefined(); + expect(typeof tokenPair.applicationRefreshToken.token).toBe('string'); + expect(tokenPair.applicationRefreshToken.expiresAt).toBeDefined(); }); it('should generate an application token with API key access token', async () => { @@ -36,11 +41,16 @@ describe('generateApplicationToken', () => { token: API_KEY_ACCESS_TOKEN, }); - expect(data.generateApplicationToken).toBeDefined(); - expect(data.generateApplicationToken.token).toBeDefined(); - expect(typeof data.generateApplicationToken.token).toBe('string'); - expect(data.generateApplicationToken.token.length).toBeGreaterThan(0); - expect(data.generateApplicationToken.expiresAt).toBeDefined(); + const tokenPair = data.generateApplicationToken; + + expect(tokenPair).toBeDefined(); + expect(tokenPair.applicationAccessToken).toBeDefined(); + expect(typeof tokenPair.applicationAccessToken.token).toBe('string'); + expect(tokenPair.applicationAccessToken.token.length).toBeGreaterThan(0); + expect(tokenPair.applicationAccessToken.expiresAt).toBeDefined(); + expect(tokenPair.applicationRefreshToken).toBeDefined(); + expect(typeof tokenPair.applicationRefreshToken.token).toBe('string'); + expect(tokenPair.applicationRefreshToken.expiresAt).toBeDefined(); }); it('should fail with a non-existent application id', async () => { diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token-query-factory.util.ts index fa7569c183..91cca96a7d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token-query-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token-query-factory.util.ts @@ -8,8 +8,14 @@ export const generateApplicationTokenQueryFactory = ({ query: gql` mutation GenerateApplicationToken($applicationId: UUID!) { generateApplicationToken(applicationId: $applicationId) { - token - expiresAt + applicationAccessToken { + token + expiresAt + } + applicationRefreshToken { + token + expiresAt + } } } `, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token.util.ts index 5f7910fee6..abc1dbd1db 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/generate-application-token.util.ts @@ -4,7 +4,7 @@ import { type CommonResponseBody } from 'test/integration/metadata/types/common- import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; -import { type AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto'; +import { type ApplicationTokenPairDTO } from 'src/engine/core-modules/application/dtos/application-token-pair.dto'; export const generateApplicationToken = async ({ applicationId, @@ -15,7 +15,7 @@ export const generateApplicationToken = async ({ expectToFail?: boolean; token?: string; }): CommonResponseBody<{ - generateApplicationToken: AuthToken; + generateApplicationToken: ApplicationTokenPairDTO; }> => { const graphqlOperation = generateApplicationTokenQueryFactory({ applicationId, diff --git a/yarn.lock b/yarn.lock index d1886f64a2..e51c012fd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6129,6 +6129,26 @@ __metadata: languageName: node linkType: hard +"@genql/runtime@npm:^2.10.0": + version: 2.10.0 + resolution: "@genql/runtime@npm:2.10.0" + dependencies: + "@types/qs": "npm:^6.9.0" + "@types/ws": "npm:^6.0.1" + graphql-query-batcher: "npm:^1.0.1" + isomorphic-unfetch: "npm:^3.0.0" + lodash: "npm:^4.17.20" + subscriptions-transport-ws: "npm:^0.9.16" + tslib: "npm:^2.0.0" + utility-types: "npm:^3.10.0" + ws: "npm:^6.1.4" + zen-observable-ts: "npm:^0.8.21" + peerDependencies: + graphql: "*" + checksum: 10c0/e2a886c2469c933681e2b0ddd6a5b7f4cb12932251ba460e3cf2db4246817da79313ea4ba9769ec7cbe53ab9c1cb81ad8fcce6a969cd241185b79398d2a4f3c6 + languageName: node + linkType: hard + "@gitbeaker/core@npm:^38.12.1": version: 38.12.1 resolution: "@gitbeaker/core@npm:38.12.1" @@ -24878,6 +24898,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^6.0.1": + version: 6.0.4 + resolution: "@types/ws@npm:6.0.4" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/fa958e64596ca9487c3ed6012834de70b47f25d971f1950cfb8e6a99cb77ff340ae82ac7627744e01b58010674ef8ede07d5a2ac29ca9ad0d67a430fcc69ae14 + languageName: node + linkType: hard + "@types/ws@npm:^8.0.0": version: 8.5.12 resolution: "@types/ws@npm:8.5.12" @@ -28912,6 +28941,13 @@ __metadata: languageName: node linkType: hard +"async-limiter@npm:~1.0.0": + version: 1.0.1 + resolution: "async-limiter@npm:1.0.1" + checksum: 10c0/0693d378cfe86842a70d4c849595a0bb50dc44c11649640ca982fa90cbfc74e3cc4753b5a0847e51933f2e9c65ce8e05576e75e5e1fd963a086e673735b35969 + languageName: node + linkType: hard + "async-retry@npm:1.2.3": version: 1.2.3 resolution: "async-retry@npm:1.2.3" @@ -38581,6 +38617,13 @@ __metadata: languageName: node linkType: hard +"graphql-query-batcher@npm:^1.0.1": + version: 1.0.1 + resolution: "graphql-query-batcher@npm:1.0.1" + checksum: 10c0/804d0f4064721a2116a16b9eac422e9233e85f4ab5b250cb8f83662725658ffde35779a8ae8211037f3dd2f9717de8cb63b394ad8057ce22171a83db2471196a + languageName: node + linkType: hard + "graphql-redis-subscriptions@npm:2.7.0": version: 2.7.0 resolution: "graphql-redis-subscriptions@npm:2.7.0" @@ -41478,6 +41521,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-unfetch@npm:^3.0.0": + version: 3.1.0 + resolution: "isomorphic-unfetch@npm:3.1.0" + dependencies: + node-fetch: "npm:^2.6.1" + unfetch: "npm:^4.2.0" + checksum: 10c0/d3b61fca06304db692b7f76bdfd3a00f410e42cfa7403c3b250546bf71589d18cf2f355922f57198e4cc4a9872d3647b20397a5c3edf1a347c90d57c83cf2a89 + languageName: node + linkType: hard + "isomorphic-ws@npm:5.0.0, isomorphic-ws@npm:^5.0.0": version: 5.0.0 resolution: "isomorphic-ws@npm:5.0.0" @@ -56831,6 +56884,21 @@ __metadata: languageName: node linkType: hard +"subscriptions-transport-ws@npm:^0.9.16": + version: 0.9.19 + resolution: "subscriptions-transport-ws@npm:0.9.19" + dependencies: + backo2: "npm:^1.0.2" + eventemitter3: "npm:^3.1.0" + iterall: "npm:^1.2.1" + symbol-observable: "npm:^1.0.4" + ws: "npm:^5.2.0 || ^6.0.0 || ^7.0.0" + peerDependencies: + graphql: ">=0.10.0" + checksum: 10c0/6f2ade56865f0ba291d3ff82c79781b051c2374873bac853286fedfdbc05001b8c4018ab7cba44af667ead7f573e48d18892d58a8f9ca8d90dfb4bff5c125045 + languageName: node + linkType: hard + "sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -58338,6 +58406,7 @@ __metadata: "@chakra-ui/react": "npm:^3.33.0" "@emotion/react": "npm:^11.14.0" "@genql/cli": "npm:^3.0.3" + "@genql/runtime": "npm:^2.10.0" "@mui/material": "npm:^7.3.8" "@prettier/sync": "npm:^0.5.2" "@quilted/threads": "npm:^4.0.1" @@ -59376,6 +59445,13 @@ __metadata: languageName: node linkType: hard +"unfetch@npm:^4.2.0": + version: 4.2.0 + resolution: "unfetch@npm:4.2.0" + checksum: 10c0/a5c0a896a6f09f278b868075aea65652ad185db30e827cb7df45826fe5ab850124bf9c44c4dafca4bf0c55a0844b17031e8243467fcc38dd7a7d435007151f1b + languageName: node + linkType: hard + "unhead@npm:1.11.20": version: 1.11.20 resolution: "unhead@npm:1.11.20" @@ -61688,6 +61764,15 @@ __metadata: languageName: node linkType: hard +"ws@npm:^6.1.4": + version: 6.2.3 + resolution: "ws@npm:6.2.3" + dependencies: + async-limiter: "npm:~1.0.0" + checksum: 10c0/56a35b9799993cea7ce2260197e7879f21bbbb194a967f31acbbda6f7f46ecda4365951966fb062044c95197e19fb2f053be6f65c172435455186835f494de41 + languageName: node + linkType: hard + "ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.18.0": version: 8.18.2 resolution: "ws@npm:8.18.2" @@ -62297,6 +62382,16 @@ __metadata: languageName: node linkType: hard +"zen-observable-ts@npm:^0.8.21": + version: 0.8.21 + resolution: "zen-observable-ts@npm:0.8.21" + dependencies: + tslib: "npm:^1.9.3" + zen-observable: "npm:^0.8.0" + checksum: 10c0/fe4a02f862b5f7e8ae0f86230c37b84c7d5611f5c206981afb4043e732d04cf7067a6cbe1ba82d20f18b735a3387937195a12542158a631d308ae3959a1d93c4 + languageName: node + linkType: hard + "zen-observable-ts@npm:^1.2.5": version: 1.2.5 resolution: "zen-observable-ts@npm:1.2.5" @@ -62306,7 +62401,7 @@ __metadata: languageName: node linkType: hard -"zen-observable@npm:0.8.15": +"zen-observable@npm:0.8.15, zen-observable@npm:^0.8.0": version: 0.8.15 resolution: "zen-observable@npm:0.8.15" checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8