diff --git a/.github/actions/spawn-twenty-app-dev-test/action.yml b/.github/actions/spawn-twenty-app-dev-test/action.yml index 4ee25e08636..41f3b6de0fe 100644 --- a/.github/actions/spawn-twenty-app-dev-test/action.yml +++ b/.github/actions/spawn-twenty-app-dev-test/action.yml @@ -29,6 +29,7 @@ runs: -p 2021:2021 \ -e NODE_PORT=2021 \ -e SERVER_URL=http://localhost:2021 \ + -e MARKETPLACE_CATALOG_SYNC_CRON_ENABLED=false \ twentycrm/twenty-app-dev:${{ inputs.twenty-version }} echo "Waiting for Twenty test instance to become healthy…" diff --git a/.github/workflows/ci-internal-apps.yaml b/.github/workflows/ci-internal-apps.yaml index 4d4c7d24894..7e42243aa2f 100644 --- a/.github/workflows/ci-internal-apps.yaml +++ b/.github/workflows/ci-internal-apps.yaml @@ -66,7 +66,7 @@ jobs: // keep this PR small while the remaining apps are made CI-ready. Remove an // app from this list once its checks pass, and delete the list entirely // once every application is covered. - const CI_EXCLUDED_APPLICATIONS = ['call-recording', 'exa', 'people-data-labs', 'self-hosting', 'twenty-fireflies', 'twenty-for-twenty', 'twenty-linear', 'twenty-meeting-bot', 'twenty-partners']; + const CI_EXCLUDED_APPLICATIONS = ['call-recording', 'people-data-labs', 'self-hosting', 'twenty-fireflies', 'twenty-for-twenty', 'twenty-linear', 'twenty-meeting-bot', 'twenty-partners']; const eventName = process.env.EVENT_NAME; const changedFiles = JSON.parse(process.env.CHANGED_FILES || '[]'); const changedApps = new Set(); diff --git a/packages/twenty-apps/internal/exa/.gitignore b/packages/twenty-apps/internal/twenty-exa/.gitignore similarity index 100% rename from packages/twenty-apps/internal/exa/.gitignore rename to packages/twenty-apps/internal/twenty-exa/.gitignore diff --git a/packages/twenty-apps/internal/exa/.nvmrc b/packages/twenty-apps/internal/twenty-exa/.nvmrc similarity index 100% rename from packages/twenty-apps/internal/exa/.nvmrc rename to packages/twenty-apps/internal/twenty-exa/.nvmrc diff --git a/packages/twenty-apps/internal/exa/.oxlintrc.json b/packages/twenty-apps/internal/twenty-exa/.oxlintrc.json similarity index 100% rename from packages/twenty-apps/internal/exa/.oxlintrc.json rename to packages/twenty-apps/internal/twenty-exa/.oxlintrc.json diff --git a/packages/twenty-apps/internal/exa/.yarnrc.yml b/packages/twenty-apps/internal/twenty-exa/.yarnrc.yml similarity index 100% rename from packages/twenty-apps/internal/exa/.yarnrc.yml rename to packages/twenty-apps/internal/twenty-exa/.yarnrc.yml diff --git a/packages/twenty-apps/internal/exa/README.md b/packages/twenty-apps/internal/twenty-exa/README.md similarity index 100% rename from packages/twenty-apps/internal/exa/README.md rename to packages/twenty-apps/internal/twenty-exa/README.md diff --git a/packages/twenty-apps/internal/exa/package.json b/packages/twenty-apps/internal/twenty-exa/package.json similarity index 66% rename from packages/twenty-apps/internal/exa/package.json rename to packages/twenty-apps/internal/twenty-exa/package.json index 0cb35484a5b..184c6e12927 100644 --- a/packages/twenty-apps/internal/exa/package.json +++ b/packages/twenty-apps/internal/twenty-exa/package.json @@ -16,18 +16,22 @@ "twenty": "twenty", "lint": "oxlint -c .oxlintrc.json .", "lint:fix": "oxlint --fix -c .oxlintrc.json .", + "typecheck": "tsgo --noEmit -p tsconfig.spec.json", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:unit": "vitest run --config vitest.unit.config.ts" }, "dependencies": { - "exa-js": "^2.12.1", - "twenty-client-sdk": "2.13.0", - "twenty-sdk": "2.13.0" + "exa-js": "^2.12.1" }, "devDependencies": { "@types/node": "^24.7.2", + "@typescript/native-preview": "^7.0.0-dev.20260116.1", "oxlint": "^0.16.0", + "twenty-client-sdk": "^2.14.0", + "twenty-sdk": "^2.14.0", "typescript": "^5.9.3", + "vite-tsconfig-paths": "^4.2.1", "vitest": "^4.0.0" } } diff --git a/packages/twenty-apps/internal/exa/public/exa-logo.svg b/packages/twenty-apps/internal/twenty-exa/public/exa-logo.svg similarity index 100% rename from packages/twenty-apps/internal/exa/public/exa-logo.svg rename to packages/twenty-apps/internal/twenty-exa/public/exa-logo.svg diff --git a/packages/twenty-apps/internal/exa/public/exa-logomark.svg b/packages/twenty-apps/internal/twenty-exa/public/exa-logomark.svg similarity index 100% rename from packages/twenty-apps/internal/exa/public/exa-logomark.svg rename to packages/twenty-apps/internal/twenty-exa/public/exa-logomark.svg diff --git a/packages/twenty-apps/internal/twenty-exa/src/__tests__/global-setup.ts b/packages/twenty-apps/internal/twenty-exa/src/__tests__/global-setup.ts new file mode 100644 index 00000000000..e0f8c23b054 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-exa/src/__tests__/global-setup.ts @@ -0,0 +1,87 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { appDevOnce, appUninstall } from 'twenty-sdk/cli'; + +const APP_PATH = process.cwd(); +const CONFIG_DIR = path.join(os.homedir(), '.twenty'); + +function validateEnv(): { apiUrl: string; apiKey: string } { + const apiUrl = process.env.TWENTY_API_URL; + const apiKey = process.env.TWENTY_API_KEY; + + if (!apiUrl || !apiKey) { + throw new Error( + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + + 'Start a local server: yarn twenty docker:start\n' + + 'Or set them in vitest env config.', + ); + } + + return { apiUrl, apiKey }; +} + +async function checkServer(apiUrl: string) { + let response: Response; + + try { + response = await fetch(`${apiUrl}/healthz`); + } catch { + throw new Error( + `Twenty server is not reachable at ${apiUrl}. ` + + 'Make sure the server is running before executing integration tests.', + ); + } + + if (!response.ok) { + throw new Error(`Server at ${apiUrl} returned ${response.status}`); + } +} + +function writeConfig(apiUrl: string, apiKey: string) { + const payload = JSON.stringify( + { + remotes: { + local: { apiUrl, apiKey, accessToken: apiKey }, + }, + defaultRemote: 'local', + }, + null, + 2, + ); + + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload); +} + +export async function setup() { + const { apiUrl, apiKey } = validateEnv(); + + await checkServer(apiUrl); + + writeConfig(apiUrl, apiKey); + + await appUninstall({ appPath: APP_PATH }).catch(() => {}); + + const result = await appDevOnce({ + appPath: APP_PATH, + onProgress: (message: string) => console.log(`[dev] ${message}`), + }); + + if (!result.success) { + throw new Error( + `Dev sync failed: ${result.error?.message ?? 'Unknown error'}`, + ); + } +} + +export async function teardown() { + const uninstallResult = await appUninstall({ appPath: APP_PATH }); + + if (!uninstallResult.success) { + console.warn( + `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`, + ); + } +} diff --git a/packages/twenty-apps/internal/twenty-exa/src/__tests__/twenty-exa.integration-test.ts b/packages/twenty-apps/internal/twenty-exa/src/__tests__/twenty-exa.integration-test.ts new file mode 100644 index 00000000000..7b30e9b2e4e --- /dev/null +++ b/packages/twenty-apps/internal/twenty-exa/src/__tests__/twenty-exa.integration-test.ts @@ -0,0 +1,25 @@ +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { describe, expect, it } from 'vitest'; + +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config'; + +describe('App installation', () => { + it('should find the installed Exa app in the applications list', async () => { + const client = new MetadataApiClient(); + + const result = await client.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const matchingApplication = result.findManyApplications.find( + (application: { universalIdentifier: string }) => + application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(matchingApplication).toBeDefined(); + }); +}); diff --git a/packages/twenty-apps/internal/exa/src/application.config.ts b/packages/twenty-apps/internal/twenty-exa/src/application.config.ts similarity index 97% rename from packages/twenty-apps/internal/exa/src/application.config.ts rename to packages/twenty-apps/internal/twenty-exa/src/application.config.ts index 77f7ecee016..0d85c79b965 100644 --- a/packages/twenty-apps/internal/exa/src/application.config.ts +++ b/packages/twenty-apps/internal/twenty-exa/src/application.config.ts @@ -10,7 +10,6 @@ export default defineApplication({ displayName: 'Exa', description: 'Structured web search powered by Exa. Surfaces entity-aware results (companies, people, research, news) to Twenty AI agents.', - icon: 'IconSearch', logoUrl: 'public/exa-logomark.svg', defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, serverVariables: { diff --git a/packages/twenty-apps/internal/twenty-exa/src/logic-functions/__tests__/exa-web-search.test.ts b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/__tests__/exa-web-search.test.ts new file mode 100644 index 00000000000..7d992576f7e --- /dev/null +++ b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/__tests__/exa-web-search.test.ts @@ -0,0 +1,194 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { type ExaWebSearchInput } from '../types/exa-web-search-input.type'; + +const { exaConstructorMock, searchAndContentsMock, chargeCreditsMock } = + vi.hoisted(() => ({ + exaConstructorMock: vi.fn(), + searchAndContentsMock: vi.fn(), + chargeCreditsMock: vi.fn(), + })); + +vi.mock('exa-js', () => ({ + default: vi.fn(function (apiKey: string) { + exaConstructorMock(apiKey); + + return { searchAndContents: searchAndContentsMock }; + }), +})); + +vi.mock('twenty-sdk/billing', () => ({ + chargeCredits: chargeCreditsMock, +})); + +import exaWebSearch from '../exa-web-search'; + +type ExaWebSearchResult = { + success: boolean; + message: string; + result?: { title: string; url: string; snippet: string }[]; + error?: string; +}; + +const handler = exaWebSearch.config.handler as ( + parameters: ExaWebSearchInput, +) => Promise; + +const API_KEY = 'exa-test-key'; + +describe('exa_web_search handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + process.env.EXA_API_KEY = API_KEY; + chargeCreditsMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should return a configuration error and skip the search when EXA_API_KEY is not set', async () => { + delete process.env.EXA_API_KEY; + + const result = await handler({ query: 'twenty crm' }); + + expect(result.success).toBe(false); + expect(result.message).toBe('Exa is not configured'); + expect(result.error).toContain('EXA_API_KEY is not set'); + expect(exaConstructorMock).not.toHaveBeenCalled(); + expect(searchAndContentsMock).not.toHaveBeenCalled(); + expect(chargeCreditsMock).not.toHaveBeenCalled(); + }); + + it('should map Exa results to title/url/snippet and report success', async () => { + searchAndContentsMock.mockResolvedValue({ + results: [ + { + title: 'Twenty CRM', + url: 'https://twenty.com', + highlights: ['Open-source CRM', 'Built with modern tech'], + }, + ], + }); + + const result = await handler({ query: 'twenty crm' }); + + expect(exaConstructorMock).toHaveBeenCalledWith(API_KEY); + expect(result).toEqual({ + success: true, + message: 'Found 1 results for "twenty crm"', + result: [ + { + title: 'Twenty CRM', + url: 'https://twenty.com', + snippet: 'Open-source CRM\nBuilt with modern tech', + }, + ], + }); + }); + + it('should default to DEFAULT_NUM_RESULTS and search with type "auto" and highlights when numResults is omitted', async () => { + searchAndContentsMock.mockResolvedValue({ results: [] }); + + await handler({ query: 'acme corp', category: 'company' }); + + expect(searchAndContentsMock).toHaveBeenCalledWith('acme corp', { + type: 'auto', + numResults: 10, + category: 'company', + highlights: { numSentences: 5 }, + }); + }); + + it('should forward an explicit numResults to Exa', async () => { + searchAndContentsMock.mockResolvedValue({ results: [] }); + + await handler({ query: 'acme corp', numResults: 3 }); + + expect(searchAndContentsMock).toHaveBeenCalledWith( + 'acme corp', + expect.objectContaining({ numResults: 3 }), + ); + }); + + it('should include the category in the success message when one is provided', async () => { + searchAndContentsMock.mockResolvedValue({ + results: [{ title: 'A', url: 'https://a.com', highlights: ['x'] }], + }); + + const result = await handler({ query: 'openai', category: 'company' }); + + expect(result.message).toBe('Found 1 results for "openai" (category: company)'); + }); + + it('should fall back to empty strings for a missing title and missing highlights', async () => { + searchAndContentsMock.mockResolvedValue({ + results: [{ url: 'https://no-title.com' }], + }); + + const result = await handler({ query: 'edge case' }); + + expect(result.result).toEqual([ + { title: '', url: 'https://no-title.com', snippet: '' }, + ]); + }); + + it('should charge the Exa base price (7000 micro-credits) for up to DEFAULT_NUM_RESULTS results', async () => { + searchAndContentsMock.mockResolvedValue({ + results: [{ title: 'A', url: 'https://a.com', highlights: ['x'] }], + }); + + await handler({ query: 'pricing base' }); + + expect(chargeCreditsMock).toHaveBeenCalledWith({ + creditsUsedMicro: 7000, + operationType: 'WEB_SEARCH', + resourceContext: 'exa', + }); + }); + + it('should add 1000 micro-credits per result beyond DEFAULT_NUM_RESULTS', async () => { + searchAndContentsMock.mockResolvedValue({ + results: Array.from({ length: 12 }, (_, index) => ({ + title: `Result ${index}`, + url: `https://example.com/${index}`, + highlights: ['snippet'], + })), + }); + + await handler({ query: 'pricing extra', numResults: 12 }); + + expect(chargeCreditsMock).toHaveBeenCalledWith( + expect.objectContaining({ creditsUsedMicro: 9000 }), + ); + }); + + it('should return a failure result and not charge credits when the Exa search throws', async () => { + searchAndContentsMock.mockRejectedValue(new Error('Exa rate limit exceeded')); + + const result = await handler({ query: 'boom' }); + + expect(result).toEqual({ + success: false, + message: 'Web search failed for "boom"', + error: 'Exa rate limit exceeded', + }); + expect(chargeCreditsMock).not.toHaveBeenCalled(); + }); + + it('should time out and fail when Exa does not respond within the inner timeout', async () => { + vi.useFakeTimers(); + searchAndContentsMock.mockReturnValue(new Promise(() => {})); + + const resultPromise = handler({ query: 'slow query' }); + await vi.advanceTimersByTimeAsync(25_000); + const result = await resultPromise; + + expect(result).toEqual({ + success: false, + message: 'Web search failed for "slow query"', + error: 'Exa search timed out', + }); + expect(chargeCreditsMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-apps/internal/exa/src/logic-functions/constants/default-num-results.constant.ts b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/constants/default-num-results.constant.ts similarity index 100% rename from packages/twenty-apps/internal/exa/src/logic-functions/constants/default-num-results.constant.ts rename to packages/twenty-apps/internal/twenty-exa/src/logic-functions/constants/default-num-results.constant.ts diff --git a/packages/twenty-apps/internal/exa/src/logic-functions/constants/exa-categories.constant.ts b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/constants/exa-categories.constant.ts similarity index 100% rename from packages/twenty-apps/internal/exa/src/logic-functions/constants/exa-categories.constant.ts rename to packages/twenty-apps/internal/twenty-exa/src/logic-functions/constants/exa-categories.constant.ts diff --git a/packages/twenty-apps/internal/exa/src/logic-functions/exa-web-search.ts b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/exa-web-search.ts similarity index 100% rename from packages/twenty-apps/internal/exa/src/logic-functions/exa-web-search.ts rename to packages/twenty-apps/internal/twenty-exa/src/logic-functions/exa-web-search.ts diff --git a/packages/twenty-apps/internal/exa/src/logic-functions/schemas/exa-web-search-input.schema.ts b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/schemas/exa-web-search-input.schema.ts similarity index 100% rename from packages/twenty-apps/internal/exa/src/logic-functions/schemas/exa-web-search-input.schema.ts rename to packages/twenty-apps/internal/twenty-exa/src/logic-functions/schemas/exa-web-search-input.schema.ts diff --git a/packages/twenty-apps/internal/exa/src/logic-functions/types/exa-web-search-input.type.ts b/packages/twenty-apps/internal/twenty-exa/src/logic-functions/types/exa-web-search-input.type.ts similarity index 100% rename from packages/twenty-apps/internal/exa/src/logic-functions/types/exa-web-search-input.type.ts rename to packages/twenty-apps/internal/twenty-exa/src/logic-functions/types/exa-web-search-input.type.ts diff --git a/packages/twenty-apps/internal/exa/src/roles/default-function.role.ts b/packages/twenty-apps/internal/twenty-exa/src/roles/default-function.role.ts similarity index 97% rename from packages/twenty-apps/internal/exa/src/roles/default-function.role.ts rename to packages/twenty-apps/internal/twenty-exa/src/roles/default-function.role.ts index 20e5d65ef82..bd12c6aa0e3 100644 --- a/packages/twenty-apps/internal/exa/src/roles/default-function.role.ts +++ b/packages/twenty-apps/internal/twenty-exa/src/roles/default-function.role.ts @@ -21,5 +21,4 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [], fieldPermissions: [], - permissionFlags: [], }); diff --git a/packages/twenty-apps/internal/exa/tsconfig.json b/packages/twenty-apps/internal/twenty-exa/tsconfig.json similarity index 73% rename from packages/twenty-apps/internal/exa/tsconfig.json rename to packages/twenty-apps/internal/twenty-exa/tsconfig.json index 3e0e97756bb..abacf4dcf6d 100644 --- a/packages/twenty-apps/internal/exa/tsconfig.json +++ b/packages/twenty-apps/internal/twenty-exa/tsconfig.json @@ -5,6 +5,7 @@ "declaration": true, "outDir": "./dist", "rootDir": ".", + "jsx": "react-jsx", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, @@ -17,7 +18,7 @@ "strictBindCallApply": false, "target": "es2020", "module": "esnext", - "lib": ["es2020"], + "lib": ["es2020", "dom"], "skipLibCheck": true, "skipDefaultLibCheck": true, "resolveJsonModule": true, @@ -26,5 +27,16 @@ "~/*": ["./*"] } }, - "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.integration-test.ts" + ], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/packages/twenty-apps/internal/twenty-exa/tsconfig.spec.json b/packages/twenty-apps/internal/twenty-exa/tsconfig.spec.json new file mode 100644 index 00000000000..ea69a6c0105 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-exa/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "types": ["vitest/globals", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/twenty-apps/internal/twenty-exa/vitest.config.ts b/packages/twenty-apps/internal/twenty-exa/vitest.config.ts new file mode 100644 index 00000000000..055af77dbec --- /dev/null +++ b/packages/twenty-apps/internal/twenty-exa/vitest.config.ts @@ -0,0 +1,31 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TWENTY_API_KEY = + process.env.TWENTY_API_KEY ?? + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; + +// Make env vars available to globalSetup (test.env only applies to workers) +process.env.TWENTY_API_URL = TWENTY_API_URL; +process.env.TWENTY_API_KEY = TWENTY_API_KEY; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + fileParallelism: false, + include: ['src/**/*.integration-test.ts'], + globalSetup: ['src/__tests__/global-setup.ts'], + env: { + TWENTY_API_URL, + TWENTY_API_KEY, + }, + }, +}); diff --git a/packages/twenty-apps/internal/twenty-exa/vitest.unit.config.ts b/packages/twenty-apps/internal/twenty-exa/vitest.unit.config.ts new file mode 100644 index 00000000000..e0d140015bb --- /dev/null +++ b/packages/twenty-apps/internal/twenty-exa/vitest.unit.config.ts @@ -0,0 +1,14 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + include: ['src/**/*.test.ts'], + }, +}); diff --git a/packages/twenty-apps/internal/exa/yarn.lock b/packages/twenty-apps/internal/twenty-exa/yarn.lock similarity index 94% rename from packages/twenty-apps/internal/exa/yarn.lock rename to packages/twenty-apps/internal/twenty-exa/yarn.lock index 53cff231c11..f0f10e228f5 100644 --- a/packages/twenty-apps/internal/exa/yarn.lock +++ b/packages/twenty-apps/internal/twenty-exa/yarn.lock @@ -774,6 +774,87 @@ __metadata: languageName: node linkType: hard +"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260616.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260616.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260616.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260616.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260616.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260616.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260616.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260616.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@typescript/native-preview@npm:^7.0.0-dev.20260116.1": + version: 7.0.0-dev.20260616.1 + resolution: "@typescript/native-preview@npm:7.0.0-dev.20260616.1" + dependencies: + "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260616.1" + "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260616.1" + "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260616.1" + "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260616.1" + "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260616.1" + "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260616.1" + "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260616.1" + dependenciesMeta: + "@typescript/native-preview-darwin-arm64": + optional: true + "@typescript/native-preview-darwin-x64": + optional: true + "@typescript/native-preview-linux-arm": + optional: true + "@typescript/native-preview-linux-arm64": + optional: true + "@typescript/native-preview-linux-x64": + optional: true + "@typescript/native-preview-win32-arm64": + optional: true + "@typescript/native-preview-win32-x64": + optional: true + bin: + tsgo: bin/tsgo.js + checksum: 10c0/e858f6a5bc41e615d3a87381ac33c938cbaa3553c47f4f1c3e06682fd96303f624ecd457542b7980a7eb29f73b54da19e8a52650246cc98a145c4a46aa743b40 + languageName: node + linkType: hard + "@vitest/expect@npm:4.1.8": version: 4.1.8 resolution: "@vitest/expect@npm:4.1.8" @@ -1084,7 +1165,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4": +"debug@npm:4, debug@npm:^4.1.1": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -1473,6 +1554,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1 + languageName: node + linkType: hard + "gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -2390,6 +2478,20 @@ __metadata: languageName: node linkType: hard +"tsconfck@npm:^3.0.3": + version: 3.1.6 + resolution: "tsconfck@npm:3.1.6" + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + bin: + tsconfck: bin/tsconfck.js + checksum: 10c0/269c3c513540be44844117bb9b9258fe6f8aeab026d32aeebf458d5299125f330711429dbb556dbf125a0bc25f4a81e6c24ac96de2740badd295c3fb400f66c4 + languageName: node + linkType: hard + "tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -2404,16 +2506,16 @@ __metadata: languageName: node linkType: hard -"twenty-client-sdk@npm:2.13.0": - version: 2.13.0 - resolution: "twenty-client-sdk@npm:2.13.0" +"twenty-client-sdk@npm:2.14.0, twenty-client-sdk@npm:^2.14.0": + version: 2.14.0 + resolution: "twenty-client-sdk@npm:2.14.0" dependencies: "@genql/runtime": "npm:^2.10.0" esbuild: "npm:^0.28.1" graphql: "npm:^16.8.1" lodash: "npm:^4.17.21" prettier: "npm:^3.8.3" - checksum: 10c0/740464acec94c1d4cc5fa50ed6b190a7f1d1681963f61437a6c704835c05ffe0f5a13e254e57601a99b07bbe0f68483dee19211731853aafbcc15ec79f8039ce + checksum: 10c0/eb222a726e21fc98f3a624f9acac7d47f0c769b989911889036665dac7c7c88698ec99beb844bf51a42e1696ea0a067bae8cf47fd751965af52a3720835242f0 languageName: node linkType: hard @@ -2422,18 +2524,20 @@ __metadata: resolution: "twenty-exa@workspace:." dependencies: "@types/node": "npm:^24.7.2" + "@typescript/native-preview": "npm:^7.0.0-dev.20260116.1" exa-js: "npm:^2.12.1" oxlint: "npm:^0.16.0" - twenty-client-sdk: "npm:2.13.0" - twenty-sdk: "npm:2.13.0" + twenty-client-sdk: "npm:^2.14.0" + twenty-sdk: "npm:^2.14.0" typescript: "npm:^5.9.3" + vite-tsconfig-paths: "npm:^4.2.1" vitest: "npm:^4.0.0" languageName: unknown linkType: soft -"twenty-sdk@npm:2.13.0": - version: 2.13.0 - resolution: "twenty-sdk@npm:2.13.0" +"twenty-sdk@npm:^2.14.0": + version: 2.14.0 + resolution: "twenty-sdk@npm:2.14.0" dependencies: "@sniptt/guards": "npm:^0.2.0" axios: "npm:^1.16.0" @@ -2450,12 +2554,12 @@ __metadata: react: "npm:^19.2.0" react-dom: "npm:^19.2.0" tinyglobby: "npm:^0.2.15" - twenty-client-sdk: "npm:2.13.0" + twenty-client-sdk: "npm:2.14.0" typescript: "npm:^5.9.3" uuid: "npm:^13.0.2" bin: twenty: dist/cli.cjs - checksum: 10c0/51c350abe5d344fcad3c961a77632fd3cb2d91e357d0562b3eb02f05f66dbc41221c5463d0f91c022316a16af03a05ad43f038d9534c0ae31c060008d0e48672 + checksum: 10c0/c4211772f8dd2ba81074a74be5f616c4c264d6bf8a174c5724be6d1c9a8cb3ca68a2674653bb58c059dae4bf1b0f43f91991e73d6a01395807206f0ac44afb08 languageName: node linkType: hard @@ -2532,6 +2636,22 @@ __metadata: languageName: node linkType: hard +"vite-tsconfig-paths@npm:^4.2.1": + version: 4.3.2 + resolution: "vite-tsconfig-paths@npm:4.3.2" + dependencies: + debug: "npm:^4.1.1" + globrex: "npm:^0.1.2" + tsconfck: "npm:^3.0.3" + peerDependencies: + vite: "*" + peerDependenciesMeta: + vite: + optional: true + checksum: 10c0/f390ac1d1c3992fc5ac50f9274c1090f8b55ab34a89ea88893db9a6924a3b26c9f64bc1163615150ad100749db73b6b2cf1d57f6cd60df6e762ceb5b8ad30024 + languageName: node + linkType: hard + "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.0.16 resolution: "vite@npm:8.0.16" diff --git a/packages/twenty-server/src/database/commands/cron-register-all.command.ts b/packages/twenty-server/src/database/commands/cron-register-all.command.ts index e24b03d9b00..ca1b1fc18b0 100644 --- a/packages/twenty-server/src/database/commands/cron-register-all.command.ts +++ b/packages/twenty-server/src/database/commands/cron-register-all.command.ts @@ -76,6 +76,10 @@ export class CronRegisterAllCommand extends CommandRunner { this.twentyConfigService.get('SIGNING_KEY_ROTATION_DAYS'), ); + const isMarketplaceCatalogSyncEnabled = this.twentyConfigService.get( + 'MARKETPLACE_CATALOG_SYNC_CRON_ENABLED', + ); + const allCommands = [ { name: 'MessagingMessagesImport', @@ -156,6 +160,7 @@ export class CronRegisterAllCommand extends CommandRunner { { name: 'MarketplaceCatalogSync', command: this.marketplaceCatalogSyncCronCommand, + isEnabled: isMarketplaceCatalogSyncEnabled, }, { name: 'ApplicationVersionCheck', diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index ff94b60ef92..3d09464d8ef 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -1247,6 +1247,15 @@ export class ConfigVariables { @IsOptional() SIGNING_KEY_ROTATION_DAYS?: number; + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.ADVANCED_SETTINGS, + description: + 'Register the cron job that syncs the marketplace catalog from the npm registry. Disable to stop the automatic catalog import.', + type: ConfigVariableType.BOOLEAN, + }) + @IsOptional() + MARKETPLACE_CATALOG_SYNC_CRON_ENABLED = true; + @ConfigVariablesMetadata({ group: ConfigVariablesGroup.RATE_LIMITING, description: 'Maximum number of records affected by mutations',