Improve getting started doc (#19138)

- improves
`packages/twenty-docs/developers/extend/apps/getting-started.mdx`

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-04-01 22:39:44 +02:00
committed by GitHub
parent 4cc3deb937
commit 16e3e38b79
50 changed files with 2001 additions and 2630 deletions
@@ -134,6 +134,7 @@ export const registerCommands = (program: Command): void => {
program
.command('exec [appPath]')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option('--postInstall', 'Execute post-install logic function if defined')
.option(
'-p, --payload <payload>',
@@ -153,6 +154,7 @@ export const registerCommands = (program: Command): void => {
async (
appPath?: string,
options?: {
preInstall?: boolean;
postInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
@@ -160,13 +162,14 @@ export const registerCommands = (program: Command): void => {
},
) => {
if (
!options?.preInstall &&
!options?.postInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --postInstall or --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
'Error: Either --preInstall, --postInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
+14 -8
View File
@@ -7,12 +7,14 @@ import { isDefined } from 'twenty-shared/utils';
export class LogicFunctionExecuteCommand {
async execute({
appPath = CURRENT_EXECUTION_DIRECTORY,
preInstall = false,
postInstall = false,
functionUniversalIdentifier,
functionName,
payload = '{}',
}: {
appPath?: string;
preInstall?: boolean;
postInstall?: boolean;
functionUniversalIdentifier?: string;
functionName?: string;
@@ -28,18 +30,22 @@ export class LogicFunctionExecuteCommand {
process.exit(1);
}
const identifier = postInstall
? 'post install'
: (functionUniversalIdentifier ?? functionName);
const identifier = preInstall
? 'pre install'
: postInstall
? 'post install'
: (functionUniversalIdentifier ?? functionName);
console.log(chalk.blue(`🚀 Executing function "${identifier}"...`));
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}\n`));
const executeOptions = postInstall
? { appPath, postInstall: true as const, payload: parsedPayload }
: functionUniversalIdentifier
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
: { appPath, functionName: functionName!, payload: parsedPayload };
const executeOptions = preInstall
? { appPath, preInstall: true as const, payload: parsedPayload }
: postInstall
? { appPath, postInstall: true as const, payload: parsedPayload }
: functionUniversalIdentifier
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
: { appPath, functionName: functionName!, payload: parsedPayload };
const result = await functionExecute(executeOptions);
+19 -31
View File
@@ -17,9 +17,9 @@ const deriveRemoteName = (url: string): string => {
}
};
const authenticate = async (apiUrl: string, token?: string): Promise<void> => {
const result = token
? await authLogin({ apiKey: token, apiUrl })
const authenticate = async (apiUrl: string, apiKey?: string): Promise<void> => {
const result = apiKey
? await authLogin({ apiKey, apiUrl })
: await runOAuthWithApiKeyFallback(apiUrl);
if (!result.success) {
@@ -66,40 +66,32 @@ export const registerRemoteCommands = (program: Command): void => {
.description('Manage remote Twenty servers');
remote
.command('add [nameOrUrl]')
.command('add')
.description('Add a new remote or re-authenticate an existing one')
.option('--as <name>', 'Name for this remote')
.option('--token <token>', 'API key for non-interactive auth')
.option('--url <url>', 'Server URL (alternative to positional arg)')
.option('--api-key <apiKey>', 'API key for non-interactive auth')
.option('--api-url <apiUrl>', 'Server URL')
.option('--local', 'Connect to a local Twenty server (auto-detect)')
.action(
async (
nameOrUrl: string | undefined,
options: {
as?: string;
token?: string;
url?: string;
local?: boolean;
},
) => {
async (options: {
as?: string;
apiKey?: string;
apiUrl?: string;
local?: boolean;
}) => {
const configService = new ConfigService();
const existingRemotes = await configService.getRemotes();
// Re-authenticate an existing remote by name
const isExistingRemote =
nameOrUrl !== undefined && existingRemotes.includes(nameOrUrl);
if (options.as !== undefined && existingRemotes.includes(options.as)) {
const config = await configService.getConfigForRemote(options.as);
if (isExistingRemote) {
const config = await configService.getConfigForRemote(nameOrUrl);
ConfigService.setActiveRemote(nameOrUrl);
await authenticate(config.apiUrl, options.token);
ConfigService.setActiveRemote(options.as);
await authenticate(config.apiUrl, options.apiKey);
return;
}
// Resolve the URL — from args, flags, auto-detect, or interactive prompt
let apiUrl = nameOrUrl ?? options.url;
let apiUrl = options.apiUrl;
if (!apiUrl) {
const detectedUrl = await detectLocalServer();
@@ -115,12 +107,8 @@ export const registerRemoteCommands = (program: Command): void => {
process.exit(1);
}
apiUrl = detectedUrl;
} else if (detectedUrl) {
console.log(chalk.gray(`Found local server at ${detectedUrl}`));
apiUrl = detectedUrl;
} else if (options.token) {
apiUrl = 'http://localhost:2020';
} else {
apiUrl = (
await inquirer.prompt<{ apiUrl: string }>([
@@ -146,7 +134,7 @@ export const registerRemoteCommands = (program: Command): void => {
const name = options.as ?? deriveRemoteName(apiUrl);
ConfigService.setActiveRemote(name);
await authenticate(apiUrl, options.token);
await authenticate(apiUrl, options.apiKey);
const defaultRemote = await configService.getDefaultRemote();
@@ -166,7 +154,7 @@ export const registerRemoteCommands = (program: Command): void => {
if (remotes.length === 0) {
console.log('No remotes configured.');
console.log("Use 'twenty remote add <url>' to add one.");
console.log("Use 'twenty remote add' to add one.");
return;
}
@@ -15,6 +15,7 @@ export type FunctionExecuteOptions = {
remote?: string;
payload?: Record<string, unknown>;
} & (
| { preInstall: true }
| { postInstall: true }
| { functionUniversalIdentifier: string }
| { functionName: string }
@@ -38,6 +39,7 @@ const belongsToApplication = (
};
const resolveIdentifier = (options: FunctionExecuteOptions): string => {
if ('preInstall' in options) return 'pre install';
if ('postInstall' in options) return 'post install';
if ('functionUniversalIdentifier' in options)
return options.functionUniversalIdentifier;
@@ -90,6 +92,12 @@ const innerFunctionExecute = async (
);
const targetFunction = appFunctions.find((logicFunction) => {
if ('preInstall' in options && options.preInstall) {
return (
logicFunction.universalIdentifier ===
manifest.application.preInstallLogicFunctionUniversalIdentifier
);
}
if ('postInstall' in options && options.postInstall) {
return (
logicFunction.universalIdentifier ===
@@ -14,25 +14,86 @@ import {
checkServerHealth,
detectLocalServer,
} from '@/cli/utilities/server/detect-local-server';
import { execSync, spawnSync } from 'node:child_process';
import { execSync, spawn, spawnSync } from 'node:child_process';
import chalk from 'chalk';
const HEALTH_POLL_INTERVAL_MS = 2000;
const HEALTH_TIMEOUT_MS = 180 * 1000;
const MILESTONE_START = '==> START ';
const MILESTONE_DONE = '==> DONE';
const waitForHealthy = async (port: number): Promise<boolean> => {
const startTime = Date.now();
const onProgress = (message: string) =>
process.stdout.write(chalk.gray(message));
while (Date.now() - startTime < HEALTH_TIMEOUT_MS) {
if (await checkServerHealth(port)) {
return true;
const logStream = spawn(
'docker',
['logs', '-f', '--since', '1s', CONTAINER_NAME],
{ stdio: ['ignore', 'pipe', 'pipe'] },
);
logStream.on('error', () => {});
let hasPendingStep = false;
const handleLogLine = (line: string) => {
const trimmed = line.trim();
const startIndex = trimmed.indexOf(MILESTONE_START);
const doneIndex = trimmed.indexOf(MILESTONE_DONE);
if (startIndex !== -1) {
if (hasPendingStep) {
onProgress('Done\n');
}
const message = trimmed.slice(startIndex + MILESTONE_START.length);
onProgress(`==> ${message}... `);
hasPendingStep = true;
} else if (doneIndex !== -1 && hasPendingStep) {
onProgress('Done\n');
hasPendingStep = false;
}
};
let logBuffer = '';
const onData = (chunk: Buffer) => {
logBuffer += chunk.toString();
const lines = logBuffer.split('\n');
logBuffer = lines.pop() ?? '';
lines.forEach(handleLogLine);
};
logStream.stdout?.on('data', onData);
logStream.stderr?.on('data', onData);
try {
while (Date.now() - startTime < HEALTH_TIMEOUT_MS) {
if (await checkServerHealth(port)) {
if (hasPendingStep) {
onProgress('Done\n');
}
return true;
}
await new Promise((resolve) =>
setTimeout(resolve, HEALTH_POLL_INTERVAL_MS),
);
}
await new Promise((resolve) =>
setTimeout(resolve, HEALTH_POLL_INTERVAL_MS),
);
}
if (hasPendingStep) {
onProgress('Failed\n');
}
return false;
return false;
} finally {
logStream.kill();
}
};
export type ServerStartOptions = {
@@ -126,8 +187,6 @@ const innerServerStart = async (
} else {
onProgress?.('Starting Twenty container...');
const serverUrl = `http://localhost:${port}`;
const runResult = spawnSync(
'docker',
[
@@ -135,14 +194,16 @@ const innerServerStart = async (
'-d',
'--name',
CONTAINER_NAME,
'-e',
`SERVER_URL=${serverUrl}`,
'-p',
`${port}:3000`,
`${port}:${port}`,
'-e',
`NODE_PORT=${port}`,
'-e',
`SERVER_URL=http://localhost:${port}`,
'-v',
'twenty-app-dev-data:/data/postgres',
'-v',
'twenty-app-dev-storage:/app/.local-storage',
'twenty-app-dev-storage:/app/packages/twenty-server/.local-storage',
IMAGE,
],
{ stdio: 'inherit' },
@@ -73,7 +73,7 @@ export class CheckServerOrchestratorStep {
this.state.applyStepEvents([
{
message:
'Authentication failed. Run `yarn twenty remote add` to authenticate.',
'Authentication failed. Run `yarn twenty remote add --local` to authenticate.',
status: 'error',
},
]);
@@ -20,11 +20,13 @@ export const isContainerRunning = (): boolean => {
export const getContainerPort = (): number => {
try {
const result = execSync(
`docker inspect -f '{{(index (index .NetworkSettings.Ports "3000/tcp") 0).HostPort}}' ${CONTAINER_NAME}`,
`docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' ${CONTAINER_NAME}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
).trim();
);
return parseInt(result, 10) || DEFAULT_PORT;
const match = result.match(/^NODE_PORT=(\d+)$/m);
return match ? parseInt(match[1], 10) : DEFAULT_PORT;
} catch {
return DEFAULT_PORT;
}