--- title: CLI & Testing description: CLI commands, testing setup, public assets, npm packages, remotes, and CI configuration. icon: terminal --- ## Public assets (`public/` folder) The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. Files placed in `public/` are: * **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. * **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. * **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. * **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. * **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. * **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output. ### Accessing public assets with `getPublicAssetUrl` Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. **In a logic function:** ```ts src/logic-functions/send-invoice.ts import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; const handler = async (): Promise => { const logoUrl = getPublicAssetUrl('logo.png'); const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); // Fetch the file content (no auth required — public endpoint) const response = await fetch(invoiceUrl); const buffer = await response.arrayBuffer(); return { logoUrl, size: buffer.byteLength }; }; export default defineLogicFunction({ universalIdentifier: 'a1b2c3d4-...', name: 'send-invoice', description: 'Sends an invoice with the app logo', timeoutSeconds: 10, handler, }); ``` **In a front component:** ```tsx src/front-components/company-card.tsx import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; export default defineFrontComponent(() => { const logoUrl = getPublicAssetUrl('logo.png'); return App logo; }); ``` The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. ## Using npm packages You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime. ### Installing a package ```bash filename="Terminal" yarn add axios ``` Then import it in your code: ```ts src/logic-functions/fetch-data.ts import { defineLogicFunction } from 'twenty-sdk/define'; import axios from 'axios'; const handler = async (): Promise => { const { data } = await axios.get('https://api.example.com/data'); return { data }; }; export default defineLogicFunction({ universalIdentifier: '...', name: 'fetch-data', description: 'Fetches data from an external API', timeoutSeconds: 10, handler, }); ``` The same works for front components: ```tsx src/front-components/chart.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { format } from 'date-fns'; const DateWidget = () => { return

Today is {format(new Date(), 'MMMM do, yyyy')}

; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'date-widget', component: DateWidget, }); ``` ### How bundling works 构建步骤使用 esbuild 为每个逻辑函数和每个前端组件生成一个自包含文件。 All imported packages are inlined into the bundle. **Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. **Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. ## 测试你的应用 该 SDK 提供可编程的 API,使你可以在测试代码中构建、部署、安装和卸载你的应用。 结合 [Vitest](https://vitest.dev/) 和类型化 API 客户端,你可以编写集成测试,在真实的 Twenty 服务器上验证你的应用端到端运行是否正常。 ### 设置 脚手架生成的应用已包含 Vitest。 如果你手动进行设置,请安装这些依赖: ```bash filename="Terminal" yarn add -D vitest vite-tsconfig-paths ``` 在应用根目录下创建一个 `vitest.config.ts`: ```ts vitest.config.ts import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [ tsconfigPaths({ projects: ['tsconfig.spec.json'], ignoreConfigErrors: true, }), ], test: { testTimeout: 120_000, hookTimeout: 120_000, include: ['src/**/*.integration-test.ts'], setupFiles: ['src/__tests__/setup-test.ts'], env: { TWENTY_API_URL: 'http://localhost:2020', TWENTY_API_KEY: 'your-api-key', }, }, }); ``` 创建一个设置文件,在测试运行前验证服务器可达: ```ts src/__tests__/setup-test.ts import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { beforeAll } from 'vitest'; const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); beforeAll(async () => { // Verify the server is running const response = await fetch(`${TWENTY_API_URL}/healthz`); if (!response.ok) { throw new Error( `Twenty server is not reachable at ${TWENTY_API_URL}. ` + 'Start the server before running integration tests.', ); } // Write a temporary config for the SDK fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); fs.writeFileSync( path.join(TEST_CONFIG_DIR, 'config.json'), JSON.stringify({ remotes: { local: { apiUrl: process.env.TWENTY_API_URL, apiKey: process.env.TWENTY_API_KEY, }, }, defaultRemote: 'local', }, null, 2), ); }); ``` ### 可编程的 SDK API 子路径 `twenty-sdk/cli` 导出了可直接在测试代码中调用的函数: | 函数 | 描述 | | -------------- | ------------------ | | `appBuild` | 构建应用,并可选地打包为 tar 包 | | `appDeploy` | 将 tar 包上传到服务器 | | `appInstall` | 在活动工作区安装该应用 | | `appUninstall` | 从活动工作区卸载该应用 | 每个函数都会返回一个结果对象,包含 `success: boolean`,以及 `data` 或 `error` 之一。 ### 编写集成测试 下面是一个完整示例:构建、部署并安装该应用,然后验证它出现在工作区中: ```ts src/__tests__/app-install.integration-test.ts import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; import { MetadataApiClient } from 'twenty-client-sdk/metadata'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const APP_PATH = process.cwd(); describe('App installation', () => { beforeAll(async () => { const buildResult = await appBuild({ appPath: APP_PATH, tarball: true, onProgress: (message: string) => console.log(`[build] ${message}`), }); if (!buildResult.success) { throw new Error(`Build failed: ${buildResult.error?.message}`); } const deployResult = await appDeploy({ tarballPath: buildResult.data.tarballPath!, onProgress: (message: string) => console.log(`[deploy] ${message}`), }); if (!deployResult.success) { throw new Error(`Deploy failed: ${deployResult.error?.message}`); } const installResult = await appInstall({ appPath: APP_PATH }); if (!installResult.success) { throw new Error(`Install failed: ${installResult.error?.message}`); } }); afterAll(async () => { await appUninstall({ appPath: APP_PATH }); }); it('should find the installed app in the workspace', async () => { const metadataClient = new MetadataApiClient(); const result = await metadataClient.query({ findManyApplications: { id: true, name: true, universalIdentifier: true, }, }); const installedApp = result.findManyApplications.find( (app: { universalIdentifier: string }) => app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, ); expect(installedApp).toBeDefined(); }); }); ``` ### 运行测试 确保你的本地 Twenty 服务器正在运行,然后: ```bash filename="Terminal" yarn test ``` 或者在开发期间使用监听模式: ```bash filename="Terminal" yarn test:watch ``` ### 类型检查 你也可以在不运行测试的情况下对应用进行类型检查: ```bash filename="Terminal" yarn twenty typecheck ``` 这会运行 `tsc --noEmit` 并报告所有类型错误。 ## CLI 参考 除了 `dev`、`build`、`add` 和 `typecheck` 外,CLI 还提供了用于执行函数、查看日志和管理应用安装的命令。 ### 执行函数(`yarn twenty exec`) 手动运行逻辑函数,而无需通过 HTTP、定时任务或数据库事件来触发: ```bash filename="Terminal" # Execute by function name yarn twenty exec -n create-new-post-card # Execute by universalIdentifier yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf # Pass a JSON payload yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' # Execute the post-install function yarn twenty exec --postInstall ``` ### 查看函数日志(`yarn twenty logs`) 实时流式查看你的应用逻辑函数的执行日志: ```bash filename="Terminal" # Stream all function logs yarn twenty logs # Filter by function name yarn twenty logs -n create-new-post-card # Filter by universalIdentifier yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf ``` 这与 `yarn twenty server logs` 不同,后者显示的是 Docker 容器日志。 `yarn twenty logs` 会显示来自 Twenty 服务器的应用函数执行日志。 ### 卸载应用(`yarn twenty uninstall`) 将你的应用从活动工作区中移除: ```bash filename="Terminal" yarn twenty uninstall # Skip the confirmation prompt yarn twenty uninstall --yes ``` ## 管理远程 “远程”是指你的应用连接到的 Twenty 服务器。 在设置期间,脚手架工具会为你自动创建一个。 你可以随时添加更多远程或在它们之间切换。 ```bash filename="Terminal" # Add a new remote (opens a browser for OAuth login) yarn twenty remote add # Connect to a local Twenty server (auto-detects port 2020 or 3000) yarn twenty remote add --local # Add a remote non-interactively (useful for CI) yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote # List all configured remotes yarn twenty remote list # Switch the active remote yarn twenty remote switch ``` 你的凭据存储在 `~/.twenty/config.json` 中。 ## 使用 GitHub Actions 进行 CI 脚手架工具会在 `.github/workflows/ci.yml` 生成一个开箱即用的 GitHub Actions 工作流。 它会在每次向 `main` 推送以及拉取请求上自动运行你的集成测试。 工作流: 1. 检出你的代码 2. 使用 `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` 动作启动一个临时的 Twenty 服务器 3. 使用 `yarn install --immutable` 安装依赖 4. 运行 `yarn test`,并从该动作的输出中注入 `TWENTY_API_URL` 和 `TWENTY_API_KEY` ```yaml .github/workflows/ci.yml name: CI on: push: branches: - main pull_request: {} env: TWENTY_VERSION: latest jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Spawn Twenty instance id: twenty uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main with: twenty-version: ${{ env.TWENTY_VERSION }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Enable Corepack run: corepack enable - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' - name: Install dependencies run: yarn install --immutable - name: Run integration tests run: yarn test env: TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} ``` 你无需配置任何机密——`spawn-twenty-docker-image` 动作会在运行器中直接启动一个临时的 Twenty 服务器,并输出连接详情。 GitHub 会自动提供 `GITHUB_TOKEN` 机密。 若要固定为特定的 Twenty 版本而不是 `latest`,请在工作流顶部修改 `TWENTY_VERSION` 环境变量。