--- title: CLI e test description: Comandi CLI, configurazione dei test, asset pubblici, pacchetti npm, remoti e configurazione CI. icon: terminal --- ## Asset pubblici (cartella `public/`) La cartella `public/` alla radice della tua app contiene file statici — immagini, icone, font o qualsiasi altro asset di cui la tua app ha bisogno a runtime. Questi file sono inclusi automaticamente nelle build, sincronizzati durante la modalità di sviluppo e caricati sul server. I file posizionati in `public/` sono: * **Pubblicamente accessibili** — una volta sincronizzati sul server, gli asset sono serviti a un URL pubblico. Non è necessaria alcuna autenticazione per accedervi. * **Disponibili nei componenti front-end** — usa gli URL degli asset per visualizzare immagini, icone o qualsiasi media all'interno dei tuoi componenti React. * **Disponibili nelle funzioni logiche** — fai riferimento agli URL degli asset nelle email, nelle risposte API o in qualsiasi logica lato server. * **Usati per i metadati del marketplace** — i campi `logoUrl` e `screenshots` in `defineApplication()` fanno riferimento a file di questa cartella (ad es., `public/logo.png`). Questi vengono visualizzati nel marketplace quando la tua app viene pubblicata. * **Sincronizzati automaticamente in modalità dev** — quando aggiungi, aggiorni o elimini un file in `public/`, viene sincronizzato automaticamente con il server. Nessun riavvio necessario. * **Inclusi nelle build** — `yarn twenty build` raggruppa tutti gli asset pubblici nell'output di distribuzione. ### Accedere agli asset pubblici con `getPublicAssetUrl` Usa l'helper `getPublicAssetUrl` da `twenty-sdk` per ottenere l'URL completo di un file nella tua directory `public/`. Funziona sia nelle funzioni logiche che nei componenti front-end. **In una funzione logica:** ```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 un componente front-end:** ```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; }); ``` L'argomento `path` è relativo alla cartella `public/` della tua app. Sia `getPublicAssetUrl('logo.png')` sia `getPublicAssetUrl('public/logo.png')` risolvono allo stesso URL — il prefisso `public/` viene rimosso automaticamente se presente. ## Uso dei pacchetti npm Puoi installare e usare qualsiasi pacchetto npm nella tua app. Sia le funzioni logiche sia i componenti front-end vengono impacchettati con [esbuild](https://esbuild.github.io/), che incorpora tutte le dipendenze nell'output — non sono necessari i `node_modules` a runtime. ### Installazione di un pacchetto ```bash filename="Terminal" yarn add axios ``` Quindi importalo nel tuo codice: ```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, }); ``` Lo stesso vale per i componenti front-end: ```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, }); ``` ### Come funziona il bundling La fase di build usa esbuild per produrre un singolo file autonomo per ogni funzione logica e per ogni componente front-end. Tutti i pacchetti importati sono incorporati nel bundle. **Le funzioni logiche** vengono eseguite in un ambiente Node.js. I moduli integrati di Node (`fs`, `path`, `crypto`, `http`, ecc.) sono disponibili e non necessitano di essere installati. **I componenti front-end** vengono eseguiti in un Web Worker. I moduli integrati di Node non sono disponibili — solo le API del browser e i pacchetti npm che funzionano in un ambiente browser. Entrambi gli ambienti hanno `twenty-client-sdk/core` e `twenty-client-sdk/metadata` disponibili come moduli preforniti — questi non vengono inclusi nel bundle ma vengono risolti a runtime dal server. ## Testare la tua app L'SDK fornisce API programmatiche che ti consentono di compilare, distribuire, installare e disinstallare la tua app dal codice di test. In combinazione con [Vitest](https://vitest.dev/) e i client API tipizzati, puoi scrivere test di integrazione che verificano che la tua app funzioni end-to-end contro un server Twenty reale. ### Impostazione L'app generata tramite scaffolding include già Vitest. Se la configuri manualmente, installa le dipendenze: ```bash filename="Terminal" yarn add -D vitest vite-tsconfig-paths ``` Crea un `vitest.config.ts` alla radice della tua app: ```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', }, }, }); ``` Crea un file di setup che verifichi che il server sia raggiungibile prima dell'esecuzione dei test: ```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), ); }); ``` ### API programmatiche dell'SDK Il sottopercorso `twenty-sdk/cli` esporta funzioni che puoi chiamare direttamente dal codice di test: | Funzione | Descrizione | | -------------- | ----------------------------------------------- | | `appBuild` | Compila l'app e, opzionalmente, crea un tarball | | `appDeploy` | Carica un tarball sul server | | `appInstall` | Installa l'app nello spazio di lavoro attivo | | `appUninstall` | Disinstalla l'app dallo spazio di lavoro attivo | Ogni funzione restituisce un oggetto risultato con `success: boolean` e `data` oppure `error`. ### Scrivere un test di integrazione Ecco un esempio completo che compila, distribuisce e installa l'app, quindi verifica che compaia nello spazio di lavoro: ```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(); }); }); ``` ### Esecuzione dei test Assicurati che il tuo server Twenty locale sia in esecuzione, quindi: ```bash filename="Terminal" yarn test ``` Oppure in modalità watch durante lo sviluppo: ```bash filename="Terminal" yarn test:watch ``` ### Controllo dei tipi Puoi anche eseguire il controllo dei tipi sulla tua app senza eseguire i test: ```bash filename="Terminal" yarn twenty typecheck ``` Questo esegue `tsc --noEmit` e riporta eventuali errori di tipo. ## Riferimento CLI Oltre a `dev`, `build`, `add` e `typecheck`, la CLI fornisce comandi per eseguire funzioni, visualizzare i log e gestire le installazioni delle app. ### Esecuzione delle funzioni (`yarn twenty exec`) Esegui manualmente una funzione logica senza attivarla tramite HTTP, cron o evento del database: ```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 ``` ### Visualizzazione dei log delle funzioni (`yarn twenty logs`) Esegui lo streaming dei log di esecuzione per le funzioni logiche della tua app: ```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 ``` Questo è diverso da `yarn twenty server logs`, che mostra i log del container Docker. `yarn twenty logs` mostra i log di esecuzione delle funzioni della tua app dal server Twenty. ### Disinstallazione di un'app (`yarn twenty uninstall`) Rimuovi la tua app dallo spazio di lavoro attivo: ```bash filename="Terminal" yarn twenty uninstall # Skip the confirmation prompt yarn twenty uninstall --yes ``` ## Gestione dei remoti Un **remoto** è un server Twenty a cui la tua app si connette. Durante la configurazione, lo strumento di scaffolding ne crea uno automaticamente per te. Puoi aggiungere altri remoti o passare da uno all'altro in qualsiasi momento. ```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 ``` Le tue credenziali sono archiviate in `~/.twenty/config.json`. ## CI con GitHub Actions Lo strumento di scaffolding genera un workflow GitHub Actions pronto all'uso in `.github/workflows/ci.yml`. Esegue automaticamente i test di integrazione a ogni push su `main` e sulle pull request. Il workflow: 1. Esegue il checkout del tuo codice 2. Avvia un server Twenty temporaneo utilizzando l'azione `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` 3. Installa le dipendenze con `yarn install --immutable` 4. Esegue `yarn test` con `TWENTY_API_URL` e `TWENTY_API_KEY` iniettati dagli output dell'azione ```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 }} ``` Non è necessario configurare alcun secret — l'azione `spawn-twenty-docker-image` avvia un server Twenty effimero direttamente nel runner e fornisce i dettagli di connessione. Il secret `GITHUB_TOKEN` è fornito automaticamente da GitHub. Per fissare una versione specifica di Twenty invece di `latest`, modifica la variabile d'ambiente `TWENTY_VERSION` all'inizio del workflow.