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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user