twenty 0.2.0 → 2.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CONTRIBUTING.md +166 -0
  2. package/LICENSE +21 -0
  3. package/README.md +64 -57
  4. package/dist/app-template/.nvmrc +1 -0
  5. package/dist/app-template/.oxlintrc.json +37 -0
  6. package/dist/app-template/AGENTS.md +70 -0
  7. package/dist/app-template/CHANGELOG.md +7 -0
  8. package/dist/app-template/README.md +34 -0
  9. package/dist/app-template/SETUP.md +47 -0
  10. package/dist/app-template/github/workflows/cd.yml +42 -0
  11. package/dist/app-template/github/workflows/ci.yml +60 -0
  12. package/dist/app-template/github/workflows/publish.yml +52 -0
  13. package/dist/app-template/gitignore +43 -0
  14. package/dist/app-template/locales/en.json +1 -0
  15. package/dist/app-template/package.json +36 -0
  16. package/dist/app-template/public/logo.svg +7 -0
  17. package/dist/app-template/src/__tests__/application-config.test.ts +14 -0
  18. package/dist/app-template/src/__tests__/global-setup.ts +56 -0
  19. package/dist/app-template/src/__tests__/run-twenty.ts +66 -0
  20. package/dist/app-template/src/__tests__/schema.integration-test.ts +46 -0
  21. package/dist/app-template/src/application-config.ts +13 -0
  22. package/dist/app-template/src/constants/universal-identifiers.ts +12 -0
  23. package/dist/app-template/src/default-role.ts +16 -0
  24. package/dist/app-template/src/front-components/main-page.tsx +287 -0
  25. package/dist/app-template/src/logic-functions/health-check.ts +11 -0
  26. package/dist/app-template/src/navigation-menu-items/main-page.navigation-menu-item.ts +19 -0
  27. package/dist/app-template/src/page-layouts/main-page.page-layout.ts +42 -0
  28. package/dist/app-template/tsconfig.json +37 -0
  29. package/dist/app-template/tsconfig.spec.json +9 -0
  30. package/dist/app-template/vitest.config.ts +31 -0
  31. package/dist/app-template/vitest.unit.config.ts +14 -0
  32. package/dist/app-template/yarn.lock +3374 -0
  33. package/dist/app-template/yarnrc.yml +1 -0
  34. package/dist/app-worker.cjs +1 -0
  35. package/dist/chunks/application-DuIJE2qG.cjs +5 -0
  36. package/dist/chunks/assert-pull-paths-BuQ1ZRvQ.cjs +1 -0
  37. package/dist/chunks/build-CX6FBrbu.cjs +1 -0
  38. package/dist/chunks/build-pull-entities-zwArrVMk.cjs +5 -0
  39. package/dist/chunks/build-source-manifest-CLKH5vzc.cjs +2 -0
  40. package/dist/chunks/build-source-snapshot-CbGsy1-v.cjs +206 -0
  41. package/dist/chunks/check-node-requirement-mqwNPktb.cjs +1 -0
  42. package/dist/chunks/cli-error-CSsayo0u.cjs +1 -0
  43. package/dist/chunks/cli-version.constant-aSp8WZDh.cjs +1 -0
  44. package/dist/chunks/collect-watch-inputs-B9jWG4W0.cjs +1 -0
  45. package/dist/chunks/compile-application-translations-B59jYVWc.cjs +1 -0
  46. package/dist/chunks/config-file-mode.constant-DLBzKKUl.cjs +1 -0
  47. package/dist/chunks/confirm-in-terminal-BxEM5lxE.cjs +1 -0
  48. package/dist/chunks/constants-CL6mrTMT-B748cNNG.cjs +23 -0
  49. package/dist/chunks/constants-Cf5-CwMu.cjs +1 -0
  50. package/dist/chunks/convert-to-label-BPBLtc_O.cjs +1 -0
  51. package/dist/chunks/create-app-not-installed-error-Cc8YZqI7.cjs +1 -0
  52. package/dist/chunks/create-cancelled-error-DgrNfOv2.cjs +1 -0
  53. package/dist/chunks/create-http-status-error-D3gy3DzB.cjs +1 -0
  54. package/dist/chunks/create-metadata-client-ChjDalSi.cjs +6 -0
  55. package/dist/chunks/create-pull-base-write-rxjcKtD-.cjs +1 -0
  56. package/dist/chunks/create-rest-client-eAfOFfDA.cjs +58 -0
  57. package/dist/chunks/create-tooling-failure-DLI5FlQP.cjs +1 -0
  58. package/dist/chunks/credential-kind-labels.constant-CKdoQF8o.cjs +1 -0
  59. package/dist/chunks/extract-manifest-from-file-j-Y9wNas.cjs +1 -0
  60. package/dist/chunks/fetch-metadata-inspection-Dp034TpK.cjs +1 -0
  61. package/dist/chunks/fetch-signed-in-identity-BhvaL3zp.cjs +1 -0
  62. package/dist/chunks/fetch-workspace-id-DJStmf_V.cjs +1 -0
  63. package/dist/chunks/find-remote-D48r20g0.cjs +1 -0
  64. package/dist/chunks/format-app-duration-gu2oRLB4.cjs +1 -0
  65. package/dist/chunks/format-body-VcKL4WXt.cjs +1 -0
  66. package/dist/chunks/format-data-value-B6rIbpJv.cjs +1 -0
  67. package/dist/chunks/format-details-COEwKz_M.cjs +2 -0
  68. package/dist/chunks/format-metadata-fields-DJnTg1R9.cjs +2 -0
  69. package/dist/chunks/format-metadata-owner-yUeocnwY.cjs +1 -0
  70. package/dist/chunks/format-table-4drtI5sP.cjs +2 -0
  71. package/dist/chunks/fs-utils-Bl1fQcaJ.cjs +2 -0
  72. package/dist/chunks/generate-application-client-CCVLjxTj.cjs +1 -0
  73. package/dist/chunks/get-app-plan-summary-B0uwHXbO.cjs +8 -0
  74. package/dist/chunks/get-config-path-C-D-QHuX.cjs +1 -0
  75. package/dist/chunks/get-credential-kind-JI9heHe2.cjs +1 -0
  76. package/dist/chunks/get-function-input-schema-GNk3NRLJ-Bfph1o9z.cjs +1 -0
  77. package/dist/chunks/get-tooling-error-context-C69TBSej.cjs +1 -0
  78. package/dist/chunks/global-flags.constant-8GfW0sNU.cjs +1 -0
  79. package/dist/chunks/has-yarn-plug-and-play-X3ay5Xl0.cjs +1 -0
  80. package/dist/chunks/is-access-token-expiring-gICPXsbO.cjs +1 -0
  81. package/dist/chunks/is-application-not-found-error-mNeGbxmT.cjs +1 -0
  82. package/dist/chunks/is-exported-manifest-CriNOWvx.cjs +1 -0
  83. package/dist/chunks/is-inside-directory-t3ixLQDr.cjs +1 -0
  84. package/dist/chunks/is-interaction-allowed-ycbAFMIj.cjs +1 -0
  85. package/dist/chunks/is-same-universal-identifier-BCBib3RP.cjs +1 -0
  86. package/dist/chunks/is-supported-locale-BAjnKnKj.cjs +20 -0
  87. package/dist/chunks/kebab-case-D0usAzG-.cjs +1 -0
  88. package/dist/chunks/list-application-source-files-jEau5Rie.cjs +1 -0
  89. package/dist/chunks/metadata-D9sQOTRK.cjs +1 -0
  90. package/dist/chunks/metadata-page.constant-BkG4ndrO.cjs +1 -0
  91. package/dist/chunks/oauth-timing.constant-BClMQcKn.cjs +1 -0
  92. package/dist/chunks/open-browser-LPuVspT7.cjs +1 -0
  93. package/dist/chunks/package-CdYv4LEx.cjs +1 -0
  94. package/dist/chunks/parse-api-url-BllxRdpE.cjs +1 -0
  95. package/dist/chunks/parse-app-export-BkfflyUY.cjs +1 -0
  96. package/dist/chunks/parse-graphql-response-CtP9KTS2.cjs +1 -0
  97. package/dist/chunks/parse-json-input-CvMTKAWw.cjs +1 -0
  98. package/dist/chunks/print-typescript-value-DhElcFzI.cjs +3 -0
  99. package/dist/chunks/pull-source-eoiJwFb9.cjs +1 -0
  100. package/dist/chunks/read-app-identity-SeleQ_D6.cjs +1 -0
  101. package/dist/chunks/read-application-identity-BFg_p84F.cjs +1 -0
  102. package/dist/chunks/read-command-values-uSJqBG_E.cjs +1 -0
  103. package/dist/chunks/read-config-DUJ27809.cjs +1 -0
  104. package/dist/chunks/read-input-value-Cxgh_r2g.cjs +1 -0
  105. package/dist/chunks/read-source-identity-C8LG1v6U.cjs +1 -0
  106. package/dist/chunks/read-watch-input-stamp-BxeOb0vI.cjs +1 -0
  107. package/dist/chunks/record-pull-base-BbxvlnU8.cjs +1 -0
  108. package/dist/chunks/record-typecheck-config-inputs-BDqkKCrj.cjs +1 -0
  109. package/dist/chunks/request-oauth-tokens-C4KKlTQT.cjs +1 -0
  110. package/dist/chunks/request-timeout-milliseconds.constant-DtyQHrll.cjs +1 -0
  111. package/dist/chunks/require-approval-fhegB1ap.cjs +1 -0
  112. package/dist/chunks/resolve-app-project-D5ZXIpsG.cjs +1 -0
  113. package/dist/chunks/resolve-inside-sdk-p-16gKb4.cjs +1 -0
  114. package/dist/chunks/resolve-metadata-object-QoDi-Aik.cjs +1 -0
  115. package/dist/chunks/resolve-project-typescript-CDY8BbmP.cjs +1 -0
  116. package/dist/chunks/resolve-request-url-jC5kYwkn.cjs +1 -0
  117. package/dist/chunks/resolve-source-sdk-mK-f4q55.cjs +1 -0
  118. package/dist/chunks/result-item-limit.constant-CDrOhiWD.cjs +1 -0
  119. package/dist/chunks/run-api-graphql-command-Biy0WqXa.cjs +1 -0
  120. package/dist/chunks/run-api-rest-command-D9kdQceV.cjs +1 -0
  121. package/dist/chunks/run-app-add-command-DcTQg7Si.cjs +163 -0
  122. package/dist/chunks/run-app-apply-command-DwLPPJ-Y.cjs +2 -0
  123. package/dist/chunks/run-app-build-command-BRYb0one.cjs +2 -0
  124. package/dist/chunks/run-app-dev-command-CVeh9Gab.cjs +2 -0
  125. package/dist/chunks/run-app-exec-command-DKLNLbL5.cjs +3 -0
  126. package/dist/chunks/run-app-init-command-CJj8_spC.cjs +2 -0
  127. package/dist/chunks/run-app-logs-command-Dusb8K-W.cjs +9 -0
  128. package/dist/chunks/run-app-operation-BWrY9YZK.cjs +1 -0
  129. package/dist/chunks/run-app-plan-command-C0_3e332.cjs +1 -0
  130. package/dist/chunks/run-app-pull-command-DcZqs8gR.cjs +3 -0
  131. package/dist/chunks/run-app-typecheck-command-C8LqpOej.cjs +1 -0
  132. package/dist/chunks/run-app-uninstall-command-ncWS_k-p.cjs +2 -0
  133. package/dist/chunks/run-auth-login-command-_zDgRcth.cjs +1 -0
  134. package/dist/chunks/run-auth-logout-command-BO6O7GVi.cjs +2 -0
  135. package/dist/chunks/run-auth-status-command-Cj-vqoHg.cjs +1 -0
  136. package/dist/chunks/run-auth-token-command-D4geJ-rx.cjs +1 -0
  137. package/dist/chunks/run-cli-c9TOR0zj.cjs +23 -0
  138. package/dist/chunks/run-commands-command-C6gvMpQ_.cjs +2 -0
  139. package/dist/chunks/run-data-get-command-jDEU4EG1.cjs +1 -0
  140. package/dist/chunks/run-data-list-command-EnCdbuzc.cjs +1 -0
  141. package/dist/chunks/run-doctor-command-DnJ6io-m.cjs +2 -0
  142. package/dist/chunks/run-metadata-field-describe-command-vJngpw2S.cjs +3 -0
  143. package/dist/chunks/run-metadata-field-list-command-CR4w7fI6.cjs +3 -0
  144. package/dist/chunks/run-metadata-object-describe-command-CHP8K-W2.cjs +3 -0
  145. package/dist/chunks/run-metadata-object-list-command-BqXMcAp6.cjs +2 -0
  146. package/dist/chunks/run-open-command-DKl8acw0.cjs +1 -0
  147. package/dist/chunks/run-remote-list-command-DeAzr5Mn.cjs +1 -0
  148. package/dist/chunks/run-remote-remove-command-D0C5sWjP.cjs +2 -0
  149. package/dist/chunks/run-remote-rename-command-Byq9WRHd.cjs +1 -0
  150. package/dist/chunks/run-remote-use-command-B6VVDlMG.cjs +1 -0
  151. package/dist/chunks/run-version-command-BmSk8PUV.cjs +1 -0
  152. package/dist/chunks/select-target-C1VZjf-A.cjs +1 -0
  153. package/dist/chunks/sign-in-to-remote-again-CW8HPSTP.cjs +1 -0
  154. package/dist/chunks/sign-in-with-browser-C3kC6uP2.cjs +1 -0
  155. package/dist/chunks/style-BR52fK4V.cjs +1 -0
  156. package/dist/chunks/target-environment-variable.constant-D7z8AihP.cjs +1 -0
  157. package/dist/chunks/to-cli-error-BBYf_wqo.cjs +1 -0
  158. package/dist/chunks/to-public-target-CIAwoOKL.cjs +1 -0
  159. package/dist/chunks/to-worker-output-diagnostics-lprh9b0l.cjs +2 -0
  160. package/dist/chunks/typecheck-application-DGrovgX9.cjs +2 -0
  161. package/dist/chunks/undici-D3mKdfwl.cjs +95 -0
  162. package/dist/chunks/update-config-BkKBEfAp.cjs +1 -0
  163. package/dist/chunks/utils-B8EXJeaX-DK3gCslL.cjs +33 -0
  164. package/dist/chunks/validate-app-path-Dau-PC0a.cjs +1 -0
  165. package/dist/chunks/validate-remote-name-w-qnr43w.cjs +1 -0
  166. package/dist/chunks/write-config-atomically-DRWTukU-.cjs +1 -0
  167. package/dist/cli.cjs +2 -0
  168. package/docs/commands.md +564 -0
  169. package/package.json +41 -18
  170. package/src/app/README.md +43 -0
  171. package/src/app/add/README.md +69 -0
  172. package/src/app/bundles/README.md +85 -0
  173. package/src/app/client/README.md +62 -0
  174. package/src/app/dev/README.md +35 -0
  175. package/src/app/exec/README.md +48 -0
  176. package/src/app/function-logs/README.md +44 -0
  177. package/src/app/manifest/README.md +74 -0
  178. package/src/app/pull/README.md +28 -0
  179. package/src/app/source/README.md +33 -0
  180. package/src/app/typecheck/README.md +67 -0
  181. package/.npmignore +0 -2
  182. package/bin/twenty +0 -2
  183. package/index.js +0 -118
@@ -0,0 +1,52 @@
1
+ # Publishes the app to npm with provenance (npm trusted publishing).
2
+ #
3
+ # One-time setup:
4
+ # 1. On npmjs.com open your package > Settings > Trusted Publisher and register
5
+ # this repository and this workflow file (publish.yml).
6
+ # See https://docs.npmjs.com/trusted-publishers
7
+ # 2. Push a version tag (e.g. `git tag v1.0.0 && git push --tags`) or run the
8
+ # workflow manually from the Actions tab.
9
+ #
10
+ # Publishing with provenance certifies which GitHub repository the package is
11
+ # built from. This is also how you claim ownership of your app in a Twenty
12
+ # marketplace: the claim flow verifies you own the GitHub account or
13
+ # organization the provenance points to.
14
+ name: Publish
15
+
16
+ on:
17
+ push:
18
+ tags:
19
+ - 'v*'
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: read
24
+ # Required for npm trusted publishing (OIDC provenance).
25
+ id-token: write
26
+
27
+ jobs:
28
+ publish:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - name: Checkout
32
+ uses: actions/checkout@v4
33
+
34
+ - name: Setup Node.js
35
+ uses: actions/setup-node@v4
36
+ with:
37
+ node-version: 24
38
+ registry-url: https://registry.npmjs.org
39
+
40
+ - name: Update npm
41
+ # Trusted publishing requires npm 11.5.1 or later.
42
+ run: npm install -g npm@latest
43
+
44
+ - name: Install dependencies
45
+ run: yarn install --immutable
46
+
47
+ - name: Publish to npm
48
+ # Uncomment if publishing from a private source repo: npm rejects
49
+ # OIDC provenance for private repos
50
+ # env:
51
+ # TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
52
+ run: yarn twenty app:publish
@@ -0,0 +1,43 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/sdks
12
+ !.yarn/versions
13
+
14
+ # codegen
15
+ generated
16
+
17
+ # testing
18
+ /coverage
19
+
20
+ # dev
21
+ /dist/
22
+
23
+ .twenty
24
+
25
+ # production
26
+ /build
27
+
28
+ # misc
29
+ .DS_Store
30
+ *.pem
31
+
32
+ # debug
33
+ npm-debug.log*
34
+ yarn-debug.log*
35
+ yarn-error.log*
36
+ .pnpm-debug.log*
37
+
38
+ # env files (can opt-in for committing if needed)
39
+ .env*
40
+
41
+ # typescript
42
+ *.tsbuildinfo
43
+ *.d.ts
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "TO-BE-GENERATED",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "engines": {
6
+ "node": "^24.5.0",
7
+ "npm": "please-use-yarn",
8
+ "yarn": ">=4.0.2"
9
+ },
10
+ "keywords": [],
11
+ "packageManager": "yarn@4.13.0",
12
+ "scripts": {
13
+ "twenty": "twenty",
14
+ "lint": "oxlint -c .oxlintrc.json .",
15
+ "lint:fix": "oxlint --fix -c .oxlintrc.json .",
16
+ "typecheck": "tsgo --noEmit -p tsconfig.spec.json",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "test:unit": "vitest run --config vitest.unit.config.ts"
20
+ },
21
+ "dependencies": {},
22
+ "devDependencies": {
23
+ "@types/node": "^24.7.2",
24
+ "@types/react": "^19.0.0",
25
+ "@typescript/native-preview": "^7.0.0-dev.20260116.1",
26
+ "oxlint": "^0.16.0",
27
+ "react": "^19.0.0",
28
+ "react-dom": "^19.0.0",
29
+ "twenty-client-sdk": "TO-BE-GENERATED",
30
+ "twenty-sdk": "TO-BE-GENERATED",
31
+ "twenty-ui": "TO-BE-GENERATED",
32
+ "typescript": "^5.9.3",
33
+ "vite-tsconfig-paths": "^4.2.1",
34
+ "vitest": "^4.0.0"
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
2
+ <rect width="80" height="80" rx="16" fill="#141414"/>
3
+ <rect x="20" y="20" width="16" height="16" rx="4" fill="#FAFAFA"/>
4
+ <rect x="44" y="20" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
5
+ <rect x="20" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
6
+ <rect x="44" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.3"/>
7
+ </svg>
@@ -0,0 +1,14 @@
1
+ import {
2
+ APP_DESCRIPTION,
3
+ APP_DISPLAY_NAME,
4
+ APPLICATION_UNIVERSAL_IDENTIFIER,
5
+ } from 'src/constants/universal-identifiers';
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ describe('application identifiers', () => {
9
+ it('should expose the application metadata constants', () => {
10
+ expect(APP_DISPLAY_NAME).toBeTruthy();
11
+ expect(typeof APP_DESCRIPTION).toBe('string');
12
+ expect(APPLICATION_UNIVERSAL_IDENTIFIER).toBeTruthy();
13
+ });
14
+ });
@@ -0,0 +1,56 @@
1
+ import { runTwenty } from './run-twenty';
2
+
3
+ function validateEnv(): { apiUrl: string; apiKey: string } {
4
+ const apiUrl = process.env.TWENTY_API_URL;
5
+ const apiKey = process.env.TWENTY_API_KEY;
6
+
7
+ if (!apiUrl || !apiKey) {
8
+ throw new Error(
9
+ 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
10
+ 'Start a local server: yarn twenty docker:start\n' +
11
+ 'Or set them in vitest env config.',
12
+ );
13
+ }
14
+
15
+ return { apiUrl, apiKey };
16
+ }
17
+
18
+ async function checkServer(apiUrl: string) {
19
+ let response: Response;
20
+
21
+ try {
22
+ response = await fetch(`${apiUrl}/healthz`);
23
+ } catch {
24
+ throw new Error(
25
+ `Twenty server is not reachable at ${apiUrl}. ` +
26
+ 'Make sure the server is running before executing integration tests.',
27
+ );
28
+ }
29
+
30
+ if (!response.ok) {
31
+ throw new Error(`Server at ${apiUrl} returned ${response.status}`);
32
+ }
33
+ }
34
+
35
+ export async function setup() {
36
+ const { apiUrl } = validateEnv();
37
+
38
+ await checkServer(apiUrl);
39
+ await runTwenty(['app', 'uninstall', '--yes']);
40
+
41
+ const result = await runTwenty(['app', 'apply', '--create']);
42
+
43
+ if (!result.ok) {
44
+ throw new Error(
45
+ `App apply failed: ${result.error.code} ${result.error.message}`,
46
+ );
47
+ }
48
+ }
49
+
50
+ export async function teardown() {
51
+ const result = await runTwenty(['app', 'uninstall', '--yes']);
52
+
53
+ if (!result.ok) {
54
+ console.warn(`App uninstall failed: ${result.error.message}`);
55
+ }
56
+ }
@@ -0,0 +1,66 @@
1
+ import { spawn } from 'child_process';
2
+ import { delimiter, join } from 'path';
3
+
4
+ type TwentyCliError = {
5
+ code: string;
6
+ message: string;
7
+ hint?: string;
8
+ };
9
+
10
+ export type TwentyCliResult =
11
+ | { ok: true; data: unknown }
12
+ | { ok: false; error: TwentyCliError };
13
+
14
+ function withoutProjectBinaries(pathValue: string) {
15
+ return pathValue
16
+ .split(delimiter)
17
+ .filter(
18
+ (entry) =>
19
+ entry !== process.env.BERRY_BIN_FOLDER &&
20
+ !entry.endsWith(join('node_modules', '.bin')),
21
+ )
22
+ .join(delimiter);
23
+ }
24
+
25
+ export function runTwenty(args: string[]): Promise<TwentyCliResult> {
26
+ const executable = process.env.TWENTY_CLI ?? 'twenty';
27
+
28
+ return new Promise((resolve, reject) => {
29
+ const child = spawn(executable, [...args, '--json'], {
30
+ env: {
31
+ ...process.env,
32
+ PATH: withoutProjectBinaries(process.env.PATH ?? ''),
33
+ },
34
+ stdio: ['ignore', 'pipe', 'inherit'],
35
+ });
36
+ let stdout = '';
37
+
38
+ child.stdout.setEncoding('utf8');
39
+ child.stdout.on('data', (chunk: string) => {
40
+ stdout += chunk;
41
+ });
42
+
43
+ child.on('error', (error: NodeJS.ErrnoException) => {
44
+ reject(
45
+ error.code === 'ENOENT'
46
+ ? new Error(
47
+ `${executable} was not found. Install the Twenty CLI globally, or set TWENTY_CLI to its path.`,
48
+ )
49
+ : error,
50
+ );
51
+ });
52
+
53
+ child.on('close', (exitCode) => {
54
+ try {
55
+ resolve(JSON.parse(stdout));
56
+ } catch {
57
+ reject(
58
+ new Error(
59
+ `${executable} ${args.join(' ')} exited with code ${exitCode} without a JSON result. ` +
60
+ 'Check that it is the Twenty CLI (twenty --version), or set TWENTY_CLI to its path.',
61
+ ),
62
+ );
63
+ }
64
+ });
65
+ });
66
+ }
@@ -0,0 +1,46 @@
1
+ import { CoreApiClient } from 'twenty-client-sdk/core';
2
+ import { MetadataApiClient } from 'twenty-client-sdk/metadata';
3
+ import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ describe('App installation', () => {
7
+ it('should find the installed app in the applications list', async () => {
8
+ const client = new MetadataApiClient();
9
+
10
+ const result = await client.query({
11
+ findManyApplications: {
12
+ id: true,
13
+ name: true,
14
+ universalIdentifier: true,
15
+ },
16
+ });
17
+
18
+ const app = result.findManyApplications.find(
19
+ (a: { universalIdentifier: string }) =>
20
+ a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
21
+ );
22
+
23
+ expect(app).toBeDefined();
24
+ });
25
+ });
26
+
27
+ describe('CoreApiClient', () => {
28
+ it('should support CRUD on standard objects', async () => {
29
+ const client = new CoreApiClient();
30
+
31
+ const created = await client.mutation({
32
+ createNote: {
33
+ __args: { data: { title: 'Integration test note' } },
34
+ id: true,
35
+ },
36
+ });
37
+ expect(created.createNote.id).toBeDefined();
38
+
39
+ await client.mutation({
40
+ destroyNote: {
41
+ __args: { id: created.createNote.id },
42
+ id: true,
43
+ },
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,13 @@
1
+ import { defineApplication } from 'twenty-sdk/define';
2
+
3
+ import {
4
+ APP_DESCRIPTION,
5
+ APP_DISPLAY_NAME,
6
+ APPLICATION_UNIVERSAL_IDENTIFIER,
7
+ } from 'src/constants/universal-identifiers';
8
+
9
+ export default defineApplication({
10
+ universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
11
+ displayName: APP_DISPLAY_NAME,
12
+ description: APP_DESCRIPTION,
13
+ });
@@ -0,0 +1,12 @@
1
+ export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
2
+ export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
3
+ export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
4
+ export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
5
+ export const HEALTH_CHECK_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
6
+ export const MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
7
+ 'UUID-TO-BE-GENERATED';
8
+ export const MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
9
+ export const MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
10
+ export const MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
11
+ export const MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
12
+ 'UUID-TO-BE-GENERATED';
@@ -0,0 +1,16 @@
1
+ import { defineApplicationRole } from 'twenty-sdk/define';
2
+
3
+ import {
4
+ APP_DISPLAY_NAME,
5
+ DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
6
+ } from 'src/constants/universal-identifiers';
7
+
8
+ export default defineApplicationRole({
9
+ universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
10
+ label: `${APP_DISPLAY_NAME} default function role`,
11
+ description: `${APP_DISPLAY_NAME} default function role`,
12
+ canReadAllObjectRecords: true,
13
+ canUpdateAllObjectRecords: true,
14
+ canSoftDeleteAllObjectRecords: true,
15
+ canDestroyAllObjectRecords: false,
16
+ });
@@ -0,0 +1,287 @@
1
+ import { defineFrontComponent } from 'twenty-sdk/define';
2
+ import { useState } from 'react';
3
+ import { Avatar } from 'twenty-ui/primitives/data-display';
4
+ import {
5
+ IconBox,
6
+ IconHierarchy,
7
+ IconLayout,
8
+ IconSettingsAutomation,
9
+ } from 'twenty-ui/icon';
10
+
11
+ import {
12
+ APP_DISPLAY_NAME,
13
+ MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
14
+ } from 'src/constants/universal-identifiers';
15
+
16
+ const DOCS_BASE_URL = 'https://docs.twenty.com/developers/extend/apps';
17
+
18
+ const CATEGORIES = [
19
+ {
20
+ title: 'Data model',
21
+ color: '#73D08D',
22
+ items: [
23
+ { label: 'CUSTOM OBJECT', href: `${DOCS_BASE_URL}/data/objects` },
24
+ {
25
+ label: 'CUSTOM FIELDS',
26
+ href: `${DOCS_BASE_URL}/data/extending-objects`,
27
+ },
28
+ ],
29
+ rotation: '2.4deg',
30
+ },
31
+ {
32
+ title: 'Logic',
33
+ color: '#F4D345',
34
+ items: [
35
+ {
36
+ label: 'TOOLS',
37
+ href: `${DOCS_BASE_URL}/logic/logic-functions`,
38
+ },
39
+ {
40
+ label: 'LOGIC FUNCTION',
41
+ href: `${DOCS_BASE_URL}/logic/logic-functions`,
42
+ },
43
+ {
44
+ label: 'SKILLS',
45
+ href: `${DOCS_BASE_URL}/logic/skills-and-agents`,
46
+ },
47
+ ],
48
+ rotation: '0deg',
49
+ },
50
+ {
51
+ title: 'Layout',
52
+ color: '#C4A2E0',
53
+ items: [
54
+ { label: 'VIEWS', href: `${DOCS_BASE_URL}/layout/views` },
55
+ { label: 'WIDGETS', href: `${DOCS_BASE_URL}/layout/page-layouts` },
56
+ {
57
+ label: 'LAYOUT PAGES',
58
+ href: `${DOCS_BASE_URL}/layout/page-layouts`,
59
+ },
60
+ {
61
+ label: 'COMMANDS',
62
+ href: `${DOCS_BASE_URL}/layout/command-menu-items`,
63
+ },
64
+ ],
65
+ rotation: '-2.8deg',
66
+ },
67
+ ] as const;
68
+
69
+ const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
70
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
71
+ <path
72
+ d="M4.5 3.5H10.5V9.5M10.5 3.5L3.5 10.5"
73
+ stroke={color}
74
+ strokeWidth="1.2"
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ />
78
+ </svg>
79
+ );
80
+
81
+ const CategoryCard = ({
82
+ title,
83
+ color,
84
+ items,
85
+ rotation,
86
+ }: {
87
+ title: string;
88
+ color: string;
89
+ items: ReadonlyArray<{ label: string; href: string }>;
90
+ rotation: string;
91
+ }) => {
92
+ const [hoveredItem, setHoveredItem] = useState<string | null>(null);
93
+
94
+ const CategoryIcon = () => {
95
+ if (title === 'Data model') {
96
+ return <IconHierarchy color={color} size="20px" />;
97
+ }
98
+ if (title === 'Logic') {
99
+ return <IconSettingsAutomation color={color} size="20px" />;
100
+ }
101
+ if (title === 'Layout') {
102
+ return <IconLayout color={color} size="20px" />;
103
+ }
104
+ };
105
+
106
+ return (
107
+ <div
108
+ style={{
109
+ display: 'flex',
110
+ flexDirection: 'column',
111
+ border: `1px solid ${color}80`,
112
+ borderRadius: '12px',
113
+ overflow: 'hidden',
114
+ width: '240px',
115
+ background: '#FFFFFF',
116
+ transform: `rotate(${rotation})`,
117
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
118
+ }}
119
+ >
120
+ <div
121
+ style={{
122
+ padding: '16px 20px',
123
+ background: `${color}22`,
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ gap: '12px',
127
+ }}
128
+ >
129
+ <CategoryIcon />
130
+ <span
131
+ style={{
132
+ fontSize: '16px',
133
+ fontWeight: 600,
134
+ color: color,
135
+ }}
136
+ >
137
+ {title}
138
+ </span>
139
+ </div>
140
+ <div
141
+ style={{
142
+ display: 'flex',
143
+ flexDirection: 'column',
144
+ padding: '8px',
145
+ gap: '4px',
146
+ }}
147
+ >
148
+ {items.map((item) => {
149
+ const isHovered = hoveredItem === item.label;
150
+
151
+ return (
152
+ <a
153
+ key={item.label}
154
+ href={item.href}
155
+ target="_blank"
156
+ rel="noopener noreferrer"
157
+ onMouseEnter={() => setHoveredItem(item.label)}
158
+ onMouseLeave={() => setHoveredItem(null)}
159
+ style={{
160
+ display: 'flex',
161
+ alignItems: 'center',
162
+ gap: '10px',
163
+ textDecoration: 'none',
164
+ cursor: 'pointer',
165
+ padding: '10px 12px',
166
+ borderRadius: '8px',
167
+ background: isHovered ? '#0000000A' : 'transparent',
168
+ transition: 'background 0.15s',
169
+ }}
170
+ >
171
+ <IconBox color={color} size="20px" />
172
+ <span
173
+ style={{
174
+ fontSize: '13px',
175
+ fontWeight: 300,
176
+ color: '#333',
177
+ letterSpacing: '0.5px',
178
+ flex: 1,
179
+ }}
180
+ >
181
+ {item.label}
182
+ </span>
183
+ {isHovered && <ArrowUpRight />}
184
+ </a>
185
+ );
186
+ })}
187
+ </div>
188
+ </div>
189
+ );
190
+ };
191
+
192
+ const MainPage = () => {
193
+ return (
194
+ <div
195
+ style={{
196
+ display: 'flex',
197
+ flexDirection: 'column',
198
+ alignItems: 'center',
199
+ justifyContent: 'center',
200
+ height: '100%',
201
+ fontFamily:
202
+ 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
203
+ gap: '8px',
204
+ padding: '40px',
205
+ }}
206
+ >
207
+ <Avatar name={APP_DISPLAY_NAME} colorSeed={APP_DISPLAY_NAME} size="xl" />
208
+ <span
209
+ style={{
210
+ fontSize: '24px',
211
+ fontWeight: 600,
212
+ color: '#333',
213
+ marginTop: '8px',
214
+ }}
215
+ >
216
+ {APP_DISPLAY_NAME}
217
+ </span>
218
+ <span
219
+ style={{
220
+ fontSize: '13px',
221
+ color: '#888',
222
+ textAlign: 'center',
223
+ lineHeight: '1.5',
224
+ }}
225
+ >
226
+ Was installed successfully.
227
+ <br />
228
+ You can now add content to your app.
229
+ </span>
230
+ <a
231
+ href="/settings/applications#installed"
232
+ style={{
233
+ display: 'inline-flex',
234
+ alignItems: 'center',
235
+ gap: '6px',
236
+ marginTop: '16px',
237
+ fontSize: '13px',
238
+ color: '#333',
239
+ textDecoration: 'none',
240
+ padding: '8px 16px',
241
+ borderRadius: '8px',
242
+ border: '1px solid #e0e0e0',
243
+ background: '#fafafa',
244
+ transition: 'background 0.15s, border-color 0.15s',
245
+ }}
246
+ onMouseEnter={(e) => {
247
+ e.currentTarget.style.background = '#f0f0f0';
248
+ e.currentTarget.style.borderColor = '#ccc';
249
+ }}
250
+ onMouseLeave={(e) => {
251
+ e.currentTarget.style.background = '#fafafa';
252
+ e.currentTarget.style.borderColor = '#e0e0e0';
253
+ }}
254
+ >
255
+ Open app settings
256
+ <ArrowUpRight color="#333" />
257
+ </a>
258
+ <div
259
+ style={{
260
+ display: 'flex',
261
+ gap: '16px',
262
+ marginTop: '32px',
263
+ flexWrap: 'wrap',
264
+ justifyContent: 'center',
265
+ alignItems: 'flex-start',
266
+ }}
267
+ >
268
+ {CATEGORIES.map((category) => (
269
+ <CategoryCard
270
+ key={category.title}
271
+ title={category.title}
272
+ color={category.color}
273
+ items={category.items}
274
+ rotation={category.rotation}
275
+ />
276
+ ))}
277
+ </div>
278
+ </div>
279
+ );
280
+ };
281
+
282
+ export default defineFrontComponent({
283
+ universalIdentifier: MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
284
+ name: APP_DISPLAY_NAME,
285
+ description: `${APP_DISPLAY_NAME} front component displaying the app logo and name`,
286
+ component: MainPage,
287
+ });
@@ -0,0 +1,11 @@
1
+ import { defineHealthCheck } from 'twenty-sdk/define';
2
+
3
+ import { HEALTH_CHECK_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
4
+
5
+ export default defineHealthCheck({
6
+ universalIdentifier: HEALTH_CHECK_UNIVERSAL_IDENTIFIER,
7
+ name: 'health-check',
8
+ handler: async () => {
9
+ return { status: 'OK' };
10
+ },
11
+ });
@@ -0,0 +1,19 @@
1
+ import {
2
+ defineNavigationMenuItem,
3
+ NavigationMenuItemType,
4
+ } from 'twenty-sdk/define';
5
+
6
+ import {
7
+ APP_DISPLAY_NAME,
8
+ MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
9
+ MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
10
+ } from 'src/constants/universal-identifiers';
11
+
12
+ export default defineNavigationMenuItem({
13
+ universalIdentifier: MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
14
+ name: APP_DISPLAY_NAME,
15
+ icon: 'IconFile',
16
+ position: -1,
17
+ type: NavigationMenuItemType.PAGE_LAYOUT,
18
+ pageLayoutUniversalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
19
+ });