Implement sync in dev mode (#17405)

implement orchestrator to sync application in dev mode

Log example when starting dev mode, delete and add back functions

```bash
👩‍💻 Workspace - default
[init] 🚀 Starting Twenty Application Development Mode
[init] 📁 App Path: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto

[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/.gitignore
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/.nvmrc
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/.yarnrc.yml
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/README.md
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/eslint.config.mjs
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/package.json
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/tsconfig.json
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/yarn.lock
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/.yarn/install-state.gz
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/ooo.front-component.tsx
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/tata.object.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/application.config.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/default-function.role.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-2.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-3.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world.front-component.tsx
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/myObject.object.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/utils/toto.ts
[dev-mode] Building manifest...
[dev-mode] Successfully built manifest
[dev-mode] Syncing...
[dev-mode] ✓ Successfully built src/ooo.front-component.tsx
[dev-mode] Uploading .twenty/output/src/ooo.front-component.mjs...
[dev-mode] ✓ Successfully built src/app/hello-world-2.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world-2.function.mjs...
[dev-mode] ✓ Successfully built src/app/hello-world.front-component.tsx
[dev-mode] Uploading .twenty/output/src/app/hello-world.front-component.mjs...
[dev-mode] ✓ Successfully built src/app/hello-world-3.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world-3.function.mjs...
[dev-mode] ✓ Successfully built src/app/hello-world.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world.function.mjs...
[dev-mode] Successfully uploaded .twenty/output/src/ooo.front-component.mjs
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world-3.function.mjs
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world.front-component.mjs
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world.function.mjs
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world-2.function.mjs
[dev-mode] ✓ Synced

[dev-mode] ✓ Successfully built src/app/hello-world-2.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world-2.function.mjs...
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world-2.function.mjs
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-2.function.ts
[dev-mode] Building manifest...
[dev-mode] Successfully built manifest
[dev-mode] Syncing...
[dev-mode] ✓ Synced
[dev-mode] Build failed:
[dev-mode]   Could not resolve "/Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world.function.ts"
[dev-mode]   Could not resolve "/Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-3.function.ts"
[dev-mode]   Could not resolve "/Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-2.function.ts"
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-2.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-3.function.ts
[dev-mode] Building manifest...
[dev-mode] ⚠ No functions defined
[dev-mode] Successfully built manifest
[dev-mode] Syncing...
  🗑️  Removed src/app/hello-world-2.function.mjs
  🗑️  Removed src/app/hello-world-3.function.mjs
  🗑️  Removed src/app/hello-world.function.mjs
[dev-mode] ✓ Synced
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-2.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world-3.function.ts
[dev-mode] File changed: /Users/martinmuller/Desktop/twenty/packages/twenty-apps/toto/src/app/hello-world.function.ts
[dev-mode] Building manifest...
[dev-mode] Successfully built manifest
[dev-mode] Syncing...
[dev-mode] ✓ Successfully built src/app/hello-world-2.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world-2.function.mjs...
[dev-mode] ✓ Successfully built src/app/hello-world-3.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world-3.function.mjs...
[dev-mode] ✓ Successfully built src/app/hello-world.function.ts
[dev-mode] Uploading .twenty/output/src/app/hello-world.function.mjs...
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world.function.mjs
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world-2.function.mjs
[dev-mode] Successfully uploaded .twenty/output/src/app/hello-world-3.function.mjs
[dev-mode] ✓ Synced
```
This commit is contained in:
martmull
2026-01-26 20:32:13 +01:00
committed by GitHub
parent 570f83ea76
commit 41d470e687
64 changed files with 946 additions and 2009 deletions
@@ -1,9 +1,10 @@
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
const APP_PATH = join(__dirname, '..');
const MANIFEST_OUTPUT_PATH = join(APP_PATH, '.twenty/output/manifest.json');
const MANIFEST_OUTPUT_PATH = join(APP_PATH, OUTPUT_DIR, 'ioi', 'manifest.json');
describe('invalid-app manifest', () => {
it('should fail to build manifest due to duplicate universalIdentifier', async () => {
@@ -1,14 +1,15 @@
import { getTestedApplicationPath } from '@/cli/__tests__/e2e/utils/get-tested-application-path.util';
import { AppSyncCommand } from '@/cli/commands/app/app-sync';
import { AppUninstallCommand } from '@/cli/commands/app/app-uninstall';
import { existsSync } from 'fs';
import { inspect } from 'util';
import { join } from 'path';
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
inspect.defaultOptions.depth = 10;
describe('Application: install delete and reinstall rich-app', () => {
const applicationName = 'rich-app';
const syncCommand = new AppSyncCommand();
const deleteCommand = new AppUninstallCommand();
const appPath = getTestedApplicationPath(applicationName);
@@ -26,9 +27,9 @@ describe('Application: install delete and reinstall rich-app', () => {
});
it(`should successfully install ${applicationName} application`, async () => {
const result = await syncCommand.execute(appPath);
await runAppDev({ appPath });
expect(result.success).toBe(true);
expect(existsSync(join(appPath, OUTPUT_DIR, 'manifest.json'))).toBe(true);
});
it(`should successfully delete ${applicationName} application`, async () => {
@@ -41,8 +42,8 @@ describe('Application: install delete and reinstall rich-app', () => {
});
it(`should successfully re-install ${applicationName} application`, async () => {
const result = await syncCommand.execute(appPath);
await runAppDev({ appPath });
expect(result.success).toBe(true);
expect(existsSync(join(appPath, OUTPUT_DIR, 'manifest.json'))).toBe(true);
});
});
@@ -1,25 +0,0 @@
import { join } from 'path';
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
const APP_PATH = join(__dirname, '../..');
describe('rich-app app:build', () => {
let result: RunCliCommandResult;
beforeAll(async () => {
result = await runAppBuild({ appPath: APP_PATH });
expect(result.success).toBe(true);
}, 60000);
defineConsoleOutputTests(() => result);
defineManifestTests(APP_PATH);
defineFunctionsTests(APP_PATH);
defineFrontComponentsTests(APP_PATH);
});
@@ -1,52 +0,0 @@
import { getOutputByPrefix } from '@/cli/__tests__/integration/utils/get-output-by-prefix.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
export const defineConsoleOutputTests = (
getResult: () => RunCliCommandResult,
): void => {
describe('console output', () => {
it('should contain init messages', () => {
const output = getOutputByPrefix(getResult().output, 'init');
expect(output).toContain('[init] 🚀 Building Twenty Application');
expect(output).toContain('[init] 📁 App Path:');
expect(output).toContain('[init] ✅ Build completed successfully');
});
it('should contain manifest-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'manifest-watch');
expect(output).toContain('[manifest-watch] 🔄 Building...');
expect(output).toContain('[manifest-watch] ✓ Loaded "Hello World"');
expect(output).toContain('[manifest-watch] ✓ Found 2 object(s)');
expect(output).toContain('[manifest-watch] ✓ Found 4 function(s)');
expect(output).toContain('[manifest-watch] ✓ Found 4 front component(s)');
expect(output).toContain('[manifest-watch] ✓ Found 2 role(s)');
expect(output).toContain('[manifest-watch] ✓ Written to');
});
it('should contain functions-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'functions-watch');
expect(output).toContain('[functions-watch] 📦 Building...');
expect(output).toContain('[functions-watch] ✓ Built');
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
});
it('should not contain watching messages', () => {
const output = getResult().output;
expect(output).not.toContain('👀 Watching for changes...');
expect(output).not.toContain('📂 Watcher started');
});
});
};
@@ -3,9 +3,8 @@ import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-
import { join } from 'path';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
import { defineManifestTests } from './tests/manifest.tests';
import { defineEntitiesTests } from './tests/entities.tests';
const APP_PATH = join(__dirname, '../..');
@@ -20,6 +19,5 @@ describe('rich-app app:dev', () => {
defineConsoleOutputTests(() => result);
defineManifestTests(APP_PATH);
defineFunctionsTests(APP_PATH);
defineFrontComponentsTests(APP_PATH);
defineEntitiesTests(APP_PATH);
});
@@ -4,6 +4,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
export const EXPECTED_MANIFEST: ApplicationManifest = {
sources: {},
yarnLock: '',
application: {
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
@@ -21,7 +22,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
},
frontComponents: [
{
builtComponentPath: 'front-components/src/root.front-component.mjs',
builtComponentPath: 'src/root.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'RootComponent',
description: 'A root-level front component',
@@ -30,8 +31,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/card.front-component.mjs',
builtComponentPath: 'src/components/card.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'CardDisplay',
description: 'A component using an external component file',
@@ -40,8 +40,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
universalIdentifier: 'i0i1i2i3-i4i5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/greeting.front-component.mjs',
builtComponentPath: 'src/components/greeting.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'GreetingComponent',
description: 'A component that uses greeting utility',
@@ -50,8 +49,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
universalIdentifier: 'h0h1h2h3-h4h5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/test.front-component.mjs',
builtComponentPath: 'src/components/test.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'TestComponent',
description: 'A test front component',
@@ -293,8 +291,8 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
functions: [
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/root.function.mjs',
handlerName: 'rootHandler',
builtHandlerPath: 'src/root.function.mjs',
handlerName: 'default.handler',
name: 'root-function',
sourceHandlerPath: 'src/root.function.ts',
timeoutSeconds: 5,
@@ -311,8 +309,8 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/greeting.function.mjs',
handlerName: 'greetingHandler',
builtHandlerPath: 'src/functions/greeting.function.mjs',
handlerName: 'default.handler',
name: 'greeting-function',
sourceHandlerPath: 'src/functions/greeting.function.ts',
timeoutSeconds: 5,
@@ -322,17 +320,17 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
isAuthRequired: false,
path: '/greet',
type: 'route',
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000002',
universalIdentifier: 'f7b12160-3d25-4c92-a13f-a325dd60eb04',
},
],
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000001',
universalIdentifier: '9d412d9e-2caf-487c-8b66-d1585883dd4e',
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/test-function-2.function.mjs',
handlerName: 'testFunction2',
builtHandlerPath: 'src/functions/test-function-2.function.mjs',
handlerName: 'config.handler',
name: 'test-function-2',
sourceHandlerPath: 'src/utils/test-function-2.util.ts',
sourceHandlerPath: 'src/functions/test-function-2.function.ts',
timeoutSeconds: 2,
triggers: [
{
@@ -345,8 +343,8 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/test-function.function.mjs',
handlerName: 'handler',
builtHandlerPath: 'src/functions/test-function.function.mjs',
handlerName: 'default.handler',
name: 'test-function',
sourceHandlerPath: 'src/functions/test-function.function.ts',
timeoutSeconds: 2,
@@ -1,372 +0,0 @@
{
"application": {
"applicationVariables": {
"DEFAULT_RECIPIENT_NAME": {
"description": "Default recipient name for postcards",
"isSecret": false,
"universalIdentifier": "19e94e59-d4fe-4251-8981-b96d0a9f74de",
"value": "Alex Karp"
}
},
"description": "A simple hello world app",
"displayName": "Hello World",
"functionRoleUniversalIdentifier": "b648f87b-1d26-4961-b974-0908fd991061",
"icon": "IconWorld",
"universalIdentifier": "4ec0391d-18d5-411c-b2f3-266ddc1c3ef7"
},
"frontComponents": [
{
"builtComponentPath": "front-components/src/root.front-component.mjs",
"builtComponentChecksum": "[checksum]",
"componentName": "RootComponent",
"description": "A root-level front component",
"name": "root-component",
"sourceComponentPath": "src/root.front-component.tsx",
"universalIdentifier": "a0a1a2a3-a4a5-4000-8000-000000000001"
},
{
"builtComponentPath": "front-components/src/components/card.front-component.mjs",
"builtComponentChecksum": "[checksum]",
"componentName": "CardDisplay",
"description": "A component using an external component file",
"name": "card-component",
"sourceComponentPath": "src/components/card.front-component.tsx",
"universalIdentifier": "i0i1i2i3-i4i5-4000-8000-000000000001"
},
{
"builtComponentPath": "front-components/src/components/greeting.front-component.mjs",
"builtComponentChecksum": "[checksum]",
"componentName": "GreetingComponent",
"description": "A component that uses greeting utility",
"name": "greeting-component",
"sourceComponentPath": "src/components/greeting.front-component.tsx",
"universalIdentifier": "h0h1h2h3-h4h5-4000-8000-000000000001"
},
{
"builtComponentPath": "front-components/src/components/test.front-component.mjs",
"builtComponentChecksum": "[checksum]",
"componentName": "TestComponent",
"description": "A test front component",
"name": "test-component",
"sourceComponentPath": "src/components/test.front-component.tsx",
"universalIdentifier": "f1234567-abcd-4000-8000-000000000001"
}
],
"objectExtensions": [
{
"fields": [
{
"description": "Priority level for the post card (1-10)",
"label": "Priority",
"name": "priority",
"type": "NUMBER",
"universalIdentifier": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
},
{
"description": "Post card category",
"label": "Category",
"name": "category",
"options": [
{
"color": "blue",
"label": "Personal",
"position": 0,
"value": "PERSONAL"
},
{
"color": "green",
"label": "Business",
"position": 1,
"value": "BUSINESS"
},
{
"color": "orange",
"label": "Promotional",
"position": 2,
"value": "PROMOTIONAL"
}
],
"type": "SELECT",
"universalIdentifier": "8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e"
}
],
"targetObject": {
"nameSingular": "postCard"
}
}
],
"objects": [
{
"description": "A simple root-level object",
"fields": [
{
"label": "Title",
"name": "title",
"type": "TEXT",
"universalIdentifier": "b0b1b2b3-b4b5-4000-8000-000000000002"
},
{
"label": "Body",
"name": "body",
"type": "TEXT",
"universalIdentifier": "b0b1b2b3-b4b5-4000-8000-000000000003"
}
],
"icon": "IconNote",
"labelPlural": "Root notes",
"labelSingular": "Root note",
"namePlural": "rootNotes",
"nameSingular": "rootNote",
"universalIdentifier": "b0b1b2b3-b4b5-4000-8000-000000000001"
},
{
"description": "A post card object",
"fields": [
{
"description": "Postcard's content",
"icon": "IconAbc",
"label": "Content",
"name": "content",
"type": "TEXT",
"universalIdentifier": "58a0a314-d7ea-4865-9850-7fb84e72f30b"
},
{
"icon": "IconUser",
"label": "Recipient name",
"name": "recipientName",
"type": "FULL_NAME",
"universalIdentifier": "c6aa31f3-da76-4ac6-889f-475e226009ac"
},
{
"icon": "IconHome",
"label": "Recipient address",
"name": "recipientAddress",
"type": "ADDRESS",
"universalIdentifier": "95045777-a0ad-49ec-98f9-22f9fc0c8266"
},
{
"defaultValue": "'DRAFT'",
"icon": "IconSend",
"label": "Status",
"name": "status",
"options": [
{
"color": "gray",
"label": "Draft",
"position": 0,
"value": "DRAFT"
},
{
"color": "orange",
"label": "Sent",
"position": 1,
"value": "SENT"
},
{
"color": "green",
"label": "Delivered",
"position": 2,
"value": "DELIVERED"
},
{
"color": "orange",
"label": "Returned",
"position": 3,
"value": "RETURNED"
}
],
"type": "SELECT",
"universalIdentifier": "87b675b8-dd8c-4448-b4ca-20e5a2234a1e"
},
{
"defaultValue": null,
"icon": "IconCheck",
"isNullable": true,
"label": "Delivered at",
"name": "deliveredAt",
"type": "DATE_TIME",
"universalIdentifier": "e06abe72-5b44-4e7f-93be-afc185a3c433"
}
],
"icon": "IconMail",
"labelPlural": "Post cards",
"labelSingular": "Post card",
"namePlural": "postCards",
"nameSingular": "postCard",
"universalIdentifier": "54b589ca-eeed-4950-a176-358418b85c05"
}
],
"packageJson": {
"name": "rich-app",
"version": "0.1.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:build": "twenty app:build",
"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",
"help": "twenty help",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"dependencies": {
"twenty-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/node": "^24.7.2",
"@types/react": "^19.0.2",
"react": "^19.0.2",
"eslint": "^9.32.0",
"typescript-eslint": "^8.50.0"
}
},
"roles": [
{
"canBeAssignedToAgents": false,
"canBeAssignedToApiKeys": false,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": true,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"description": "A simple root-level role",
"label": "Root role",
"universalIdentifier": "c0c1c2c3-c4c5-4000-8000-000000000001"
},
{
"canBeAssignedToAgents": false,
"canBeAssignedToApiKeys": false,
"canBeAssignedToUsers": false,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"description": "Default role for function Twenty client",
"fieldPermissions": [
{
"canReadFieldValue": false,
"canUpdateFieldValue": false,
"fieldName": "content",
"objectNameSingular": "postCard"
}
],
"label": "Default function role",
"objectPermissions": [
{
"canDestroyObjectRecords": false,
"canReadObjectRecords": true,
"canSoftDeleteObjectRecords": false,
"canUpdateObjectRecords": true,
"objectNameSingular": "postCard"
}
],
"permissionFlags": [
"APPLICATIONS"
],
"universalIdentifier": "b648f87b-1d26-4961-b974-0908fd991061"
}
],
"functions": [
{
"builtHandlerChecksum": "[checksum]",
"builtHandlerPath": "functions/src/root.function.mjs",
"handlerName": "rootHandler",
"name": "root-function",
"sourceHandlerPath": "src/root.function.ts",
"timeoutSeconds": 5,
"triggers": [
{
"httpMethod": "GET",
"isAuthRequired": false,
"path": "/root",
"type": "route",
"universalIdentifier": "f0f1f2f3-f4f5-4000-8000-000000000002"
}
],
"universalIdentifier": "f0f1f2f3-f4f5-4000-8000-000000000001"
},
{
"builtHandlerChecksum": "[checksum]",
"builtHandlerPath": "functions/src/functions/greeting.function.mjs",
"handlerName": "greetingHandler",
"name": "greeting-function",
"sourceHandlerPath": "src/functions/greeting.function.ts",
"timeoutSeconds": 5,
"triggers": [
{
"httpMethod": "GET",
"isAuthRequired": false,
"path": "/greet",
"type": "route",
"universalIdentifier": "g0g1g2g3-g4g5-4000-8000-000000000002"
}
],
"universalIdentifier": "g0g1g2g3-g4g5-4000-8000-000000000001"
},
{
"builtHandlerChecksum": "[checksum]",
"builtHandlerPath": "functions/src/functions/test-function-2.function.mjs",
"handlerName": "testFunction2",
"name": "test-function-2",
"sourceHandlerPath": "src/utils/test-function-2.util.ts",
"timeoutSeconds": 2,
"triggers": [
{
"pattern": "0 0 1 1 *",
"type": "cron",
"universalIdentifier": "9fd0dda9-4664-4fbc-9656-509f4477b9ff"
}
],
"universalIdentifier": "eb3ffc98-88ec-45d4-9b4a-56833b219ccb"
},
{
"builtHandlerChecksum": "[checksum]",
"builtHandlerPath": "functions/src/functions/test-function.function.mjs",
"handlerName": "handler",
"name": "test-function",
"sourceHandlerPath": "src/functions/test-function.function.ts",
"timeoutSeconds": 2,
"triggers": [
{
"forwardedRequestHeaders": [
"signature"
],
"httpMethod": "GET",
"isAuthRequired": false,
"path": "/post-card/create",
"type": "route",
"universalIdentifier": "c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6"
},
{
"pattern": "0 0 1 1 *",
"type": "cron",
"universalIdentifier": "dd802808-0695-49e1-98c9-d5c9e2704ce2"
},
{
"eventName": "person.created",
"type": "databaseEvent",
"universalIdentifier": "203f1df3-4a82-4d06-a001-b8cf22a31156"
}
],
"universalIdentifier": "e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf"
}
]
}
@@ -14,34 +14,23 @@ export const defineConsoleOutputTests = (
expect(output).toContain('[init] 📁 App Path:');
});
it('should contain manifest-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'manifest-watch');
it('should contain dev-mode build messages', () => {
const output = getOutputByPrefix(getResult().output, 'dev-mode');
expect(output).toContain('[manifest-watch] 🔄 Building...');
expect(output).toContain('[manifest-watch] ✓ Loaded "Hello World"');
expect(output).toContain('[manifest-watch] ✓ Found 2 object(s)');
expect(output).toContain('[manifest-watch] ✓ Found 4 function(s)');
expect(output).toContain('[manifest-watch] ✓ Found 4 front component(s)');
expect(output).toContain('[manifest-watch] ✓ Found 2 role(s)');
expect(output).toContain('[manifest-watch] ✓ Written to');
expect(output).toContain('[manifest-watch] 📂 Watcher started');
expect(output).toContain('[dev-mode] Building manifest...');
expect(output).toContain('[dev-mode] Successfully built manifest');
});
it('should contain functions-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'functions-watch');
it('should contain dev-mode function build messages', () => {
const output = getOutputByPrefix(getResult().output, 'dev-mode');
expect(output).toContain('[functions-watch] 📦 Building...');
expect(output).toContain('[functions-watch] ✓ Built');
expect(output).toContain('[dev-mode] ✓ Successfully built');
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
it('should contain dev-mode sync messages', () => {
const output = getOutputByPrefix(getResult().output, 'dev-mode');
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
expect(output).toContain('[dev-mode] ✓ Synced');
});
});
};
@@ -1,15 +1,24 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from '@/cli/utilities/build/common/constants';
export const defineFunctionsTests = (appPath: string): void => {
export const defineEntitiesTests = (appPath: string): void => {
const outputDir = join(appPath, OUTPUT_DIR);
describe('functions', () => {
it('should have built functions preserving source path structure', async () => {
const functionsDir = join(appPath, '.twenty/output/functions');
const files = await fs.readdir(functionsDir, { recursive: true });
const files = await fs.readdir(outputDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
expect(sortedFiles).toEqual([
'manifest.json',
'src',
'src/components',
'src/components/card.front-component.mjs',
'src/components/card.front-component.mjs.map',
'src/components/greeting.front-component.mjs',
'src/components/greeting.front-component.mjs.map',
'src/components/test.front-component.mjs',
'src/components/test.front-component.mjs.map',
'src/functions',
'src/functions/greeting.function.mjs',
'src/functions/greeting.function.mjs.map',
@@ -17,19 +26,23 @@ export const defineFunctionsTests = (appPath: string): void => {
'src/functions/test-function-2.function.mjs.map',
'src/functions/test-function.function.mjs',
'src/functions/test-function.function.mjs.map',
'src/root.front-component.mjs',
'src/root.front-component.mjs.map',
'src/root.function.mjs',
'src/root.function.mjs.map',
]);
});
it('should not create shared chunk files for utilities', async () => {
const functionsDir = join(appPath, '.twenty/output/functions');
const files = await fs.readdir(functionsDir, { recursive: true });
const files = await fs.readdir(outputDir, { recursive: true });
// Chunk files have a hash suffix like "greeting.util-CipJsYK0.mjs"
const chunkFiles = files
.map((f) => f.toString())
.filter((f) => f.endsWith('.mjs') && !f.includes('.function.mjs'));
.filter((f) => f.endsWith('.mjs') && !f.includes('.function.mjs'))
.filter(
(f) => f.endsWith('.mjs') && !f.includes('.front-component.mjs'),
);
expect(chunkFiles).toEqual([]);
});
@@ -1,28 +0,0 @@
import * as fs from 'fs-extra';
import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components preserving source path structure', async () => {
const frontComponentsDir = join(
appPath,
'.twenty/output/front-components',
);
const files = await fs.readdir(frontComponentsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
expect(sortedFiles).toEqual([
'src',
'src/components',
'src/components/card.front-component.mjs',
'src/components/card.front-component.mjs.map',
'src/components/greeting.front-component.mjs',
'src/components/greeting.front-component.mjs.map',
'src/components/test.front-component.mjs',
'src/components/test.front-component.mjs.map',
'src/root.front-component.mjs',
'src/root.front-component.mjs.map',
]);
});
});
};
@@ -6,13 +6,13 @@ const greetingHandler = () => {
};
export default defineFunction({
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000001',
universalIdentifier: '9d412d9e-2caf-487c-8b66-d1585883dd4e',
name: 'greeting-function',
timeoutSeconds: 5,
handler: greetingHandler,
triggers: [
{
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000002',
universalIdentifier: 'f7b12160-3d25-4c92-a13f-a325dd60eb04',
type: 'route',
path: '/greet',
httpMethod: 'GET',
@@ -1,7 +1,3 @@
export const testFunction2 = () => {
const Twenty = require('../../generated').default;
const client = new Twenty();
return client.query('testQuery');
return 'test-function-2-result';
};
@@ -1,25 +0,0 @@
import { join } from 'path';
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
const APP_PATH = join(__dirname, '../..');
describe('root-app app:build', () => {
let result: RunCliCommandResult;
beforeAll(async () => {
result = await runAppBuild({ appPath: APP_PATH });
expect(result.success).toBe(true);
}, 60000);
defineConsoleOutputTests(() => result);
defineManifestTests(APP_PATH);
defineFunctionsTests(APP_PATH);
defineFrontComponentsTests(APP_PATH);
});
@@ -1,52 +0,0 @@
import { getOutputByPrefix } from '@/cli/__tests__/integration/utils/get-output-by-prefix.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
export const defineConsoleOutputTests = (
getResult: () => RunCliCommandResult,
): void => {
describe('console output', () => {
it('should contain init messages', () => {
const output = getOutputByPrefix(getResult().output, 'init');
expect(output).toContain('[init] 🚀 Building Twenty Application');
expect(output).toContain('[init] 📁 App Path:');
expect(output).toContain('[init] ✅ Build completed successfully');
});
it('should contain manifest-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'manifest-watch');
expect(output).toContain('[manifest-watch] 🔄 Building...');
expect(output).toContain('[manifest-watch] ✓ Loaded "Root App"');
expect(output).toContain('[manifest-watch] ✓ Found 1 object(s)');
expect(output).toContain('[manifest-watch] ✓ Found 1 function(s)');
expect(output).toContain('[manifest-watch] ✓ Found 1 front component(s)');
expect(output).toContain('[manifest-watch] ✓ Found 1 role(s)');
expect(output).toContain('[manifest-watch] ✓ Written to');
});
it('should contain functions-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'functions-watch');
expect(output).toContain('[functions-watch] 📦 Building...');
expect(output).toContain('[functions-watch] ✓ Built');
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
});
it('should not contain watching messages', () => {
const output = getResult().output;
expect(output).not.toContain('👀 Watching for changes...');
expect(output).not.toContain('📂 Watcher started');
});
});
};
@@ -0,0 +1,119 @@
import { FieldType } from '@/application';
import type { ApplicationManifest } from 'twenty-shared/application';
export const EXPECTED_MANIFEST: ApplicationManifest = {
sources: {},
yarnLock: '',
packageJson: {
name: 'root-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'app:build': 'twenty app:build',
'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',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
application: {
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
},
objects: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030',
nameSingular: 'myNote',
namePlural: 'myNotes',
labelSingular: 'My note',
labelPlural: 'My notes',
description: 'A simple root-level object',
icon: 'IconNote',
fields: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000031',
type: FieldType.TEXT,
label: 'Title',
name: 'title',
},
],
},
],
functions: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000010',
name: 'my-function',
timeoutSeconds: 5,
triggers: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000011',
type: 'route',
path: '/my-function',
httpMethod: 'GET',
isAuthRequired: false,
},
],
handlerName: 'default.handler',
sourceHandlerPath: 'my.function.ts',
builtHandlerPath: 'my.function.mjs',
builtHandlerChecksum: '[checksum]',
},
],
frontComponents: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000020',
name: 'my-component',
description: 'A root-level front component',
componentName: 'MyComponent',
sourceComponentPath: 'my.front-component.tsx',
builtComponentPath: 'my.front-component.mjs',
builtComponentChecksum: '[checksum]',
},
],
roles: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',
label: 'My role',
description: 'A simple root-level role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: true,
canBeAssignedToApiKeys: false,
},
],
};
@@ -1,74 +0,0 @@
{
"application": {
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000001",
"displayName": "Root App",
"description": "An app with all entities at root level",
"icon": "IconFolder",
"functionRoleUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000002"
},
"objects": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"nameSingular": "myNote",
"namePlural": "myNotes",
"labelSingular": "My note",
"labelPlural": "My notes",
"description": "A simple root-level object",
"icon": "IconNote",
"fields": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000031",
"type": "TEXT",
"label": "Title",
"name": "title"
}
]
}
],
"functions": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000010",
"name": "my-function",
"timeoutSeconds": 5,
"triggers": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000011",
"type": "route",
"path": "/my-function",
"httpMethod": "GET",
"isAuthRequired": false
}
],
"handlerName": "myHandler",
"sourceHandlerPath": "my.function.ts",
"builtHandlerPath": "functions/my.function.mjs",
"builtHandlerChecksum": "[checksum]"
}
],
"frontComponents": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000020",
"name": "my-component",
"description": "A root-level front component",
"componentName": "MyComponent",
"sourceComponentPath": "my.front-component.tsx",
"builtComponentPath": "front-components/my.front-component.mjs",
"builtComponentChecksum": "[checksum]"
}
],
"roles": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000040",
"label": "My role",
"description": "A simple root-level role",
"canReadAllObjectRecords": true,
"canUpdateAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canDestroyAllObjectRecords": false,
"canUpdateAllSettings": false,
"canBeAssignedToAgents": false,
"canBeAssignedToUsers": true,
"canBeAssignedToApiKeys": false
}
]
}
@@ -14,34 +14,23 @@ export const defineConsoleOutputTests = (
expect(output).toContain('[init] 📁 App Path:');
});
it('should contain manifest-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'manifest-watch');
it('should contain dev-mode build messages', () => {
const output = getOutputByPrefix(getResult().output, 'dev-mode');
expect(output).toContain('[manifest-watch] 🔄 Building...');
expect(output).toContain('[manifest-watch] ✓ Loaded "Root App"');
expect(output).toContain('[manifest-watch] ✓ Found 1 object(s)');
expect(output).toContain('[manifest-watch] ✓ Found 1 function(s)');
expect(output).toContain('[manifest-watch] ✓ Found 1 front component(s)');
expect(output).toContain('[manifest-watch] ✓ Found 1 role(s)');
expect(output).toContain('[manifest-watch] ✓ Written to');
expect(output).toContain('[manifest-watch] 📂 Watcher started');
expect(output).toContain('[dev-mode] Building manifest...');
expect(output).toContain('[dev-mode] Successfully built manifest');
});
it('should contain functions-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'functions-watch');
it('should contain dev-mode function build messages', () => {
const output = getOutputByPrefix(getResult().output, 'dev-mode');
expect(output).toContain('[functions-watch] 📦 Building...');
expect(output).toContain('[functions-watch] ✓ Built');
expect(output).toContain('[dev-mode] ✓ Successfully built');
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
it('should contain dev-mode sync messages', () => {
const output = getOutputByPrefix(getResult().output, 'dev-mode');
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
expect(output).toContain('[dev-mode] ✓ Synced');
});
});
};
@@ -4,14 +4,14 @@ import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components at root level', async () => {
const frontComponentsDir = join(
appPath,
'.twenty/output/front-components',
);
const files = await fs.readdir(frontComponentsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
const outputDir = join(appPath, '.twenty/output');
const files = await fs.readdir(outputDir, { recursive: true });
const componentFiles = files
.map((f) => f.toString())
.filter((f) => f.includes('.front-component.'))
.sort();
expect(sortedFiles).toEqual([
expect(componentFiles).toEqual([
'my.front-component.mjs',
'my.front-component.mjs.map',
]);
@@ -4,11 +4,14 @@ import { join } from 'path';
export const defineFunctionsTests = (appPath: string): void => {
describe('functions', () => {
it('should have built functions at root level', async () => {
const functionsDir = join(appPath, '.twenty/output/functions');
const files = await fs.readdir(functionsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
const outputDir = join(appPath, '.twenty/output');
const files = await fs.readdir(outputDir, { recursive: true });
const functionFiles = files
.map((f) => f.toString())
.filter((f) => f.includes('.function.'))
.sort();
expect(sortedFiles).toEqual(['my.function.mjs', 'my.function.mjs.map']);
expect(functionFiles).toEqual(['my.function.mjs', 'my.function.mjs.map']);
});
});
};
@@ -3,6 +3,7 @@ import { join } from 'path';
import { type ApplicationManifest } from 'twenty-shared/application';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest';
export const defineManifestTests = (appPath: string): void => {
describe('manifest', () => {
@@ -16,21 +17,17 @@ export const defineManifestTests = (appPath: string): void => {
it('should have correct manifest content', async () => {
const manifestPath = join(appPath, '.twenty/output/manifest.json');
const manifest: ApplicationManifest = await fs.readJSON(manifestPath);
const expectedPath = join(
appPath,
'__integration__/app-dev/manifest.expected.json',
);
const expected: ApplicationManifest = await fs.readJSON(expectedPath);
expect(manifest.application).toEqual(expected.application);
expect(manifest.objects).toEqual(expected.objects);
expect(manifest.application).toEqual(EXPECTED_MANIFEST.application);
expect(manifest.objects).toEqual(EXPECTED_MANIFEST.objects);
expect(
normalizeManifestForComparison({ functions: manifest.functions })
.functions,
).toEqual(
normalizeManifestForComparison({ functions: expected.functions })
.functions,
normalizeManifestForComparison({
functions: EXPECTED_MANIFEST.functions,
}).functions,
);
for (const fn of manifest.functions) {
@@ -45,7 +42,7 @@ export const defineManifestTests = (appPath: string): void => {
}).frontComponents,
).toEqual(
normalizeManifestForComparison({
frontComponents: expected.frontComponents,
frontComponents: EXPECTED_MANIFEST.frontComponents,
}).frontComponents,
);
@@ -54,7 +51,7 @@ export const defineManifestTests = (appPath: string): void => {
expect(component.builtComponentChecksum).not.toBeNull();
expect(typeof component.builtComponentChecksum).toBe('string');
}
expect(manifest.roles).toEqual(expected.roles);
expect(manifest.roles).toEqual(EXPECTED_MANIFEST.roles);
});
});
};
@@ -1,5 +1,6 @@
export type LogPrefix =
| 'init'
| 'dev-mode'
| 'manifest-watch'
| 'functions-watch'
| 'front-components-watch';
@@ -1,22 +0,0 @@
import {
runCliCommand,
type RunCliCommandResult,
} from './run-cli-command.util';
export type RunAppBuildOptions = {
appPath: string;
timeout?: number;
};
export const runAppBuild = (
options: RunAppBuildOptions,
): Promise<RunCliCommandResult> => {
const { appPath, timeout = 30000 } = options;
// app:build runs once and exits, so we don't wait for specific output
return runCliCommand({
command: 'app:build',
args: [appPath],
timeout,
});
};
@@ -16,11 +16,7 @@ export const runAppDev = (
return runCliCommand({
command: 'app:dev',
args: [appPath],
waitForOutput: [
'[manifest-watch] ✓ Written to',
'[functions-watch] ✓ Built',
'[front-components-watch] ✓ Built',
],
waitForOutput: ['[dev-mode] ✓ Synced'],
timeout,
});
};
@@ -33,7 +33,11 @@ export const runCliCommand = (
{
cwd: CLI_DIR,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: '0' },
env: {
...process.env,
FORCE_COLOR: '0',
TWENTY_SKIP_SERVER_CHECK: 'true',
},
},
);