Setup local instance on app creation (#18184)
Needs for `generate-api-key` command to be available on docker
This commit is contained in:
@@ -100,7 +100,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app"
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app" --skip-local-instance
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
|
||||
@@ -27,6 +27,10 @@ const program = new Command(packageJson.name)
|
||||
'--description <description>',
|
||||
'Application description (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--skip-local-instance',
|
||||
'Skip the local Twenty instance setup prompt',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -37,6 +41,7 @@ const program = new Command(packageJson.name)
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||
@@ -72,6 +77,7 @@ const program = new Command(packageJson.name)
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { install } from '@/utils/install';
|
||||
import {
|
||||
type LocalInstanceResult,
|
||||
setupLocalInstance,
|
||||
} from '@/utils/setup-local-instance';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as path from 'path';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -22,6 +27,7 @@ type CreateAppOptions = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
@@ -52,7 +58,29 @@ export class CreateAppCommand {
|
||||
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
this.logSuccess(appDirectory);
|
||||
let localResult: LocalInstanceResult = { running: false };
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
const { needsLocalInstance } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'needsLocalInstance',
|
||||
message:
|
||||
'Do you need a local instance of Twenty? Recommended if you not have one already.',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (needsLocalInstance) {
|
||||
localResult = await setupLocalInstance();
|
||||
}
|
||||
|
||||
if (isDefined(localResult.apiKey)) {
|
||||
this.runAuthLogin(appDirectory, localResult.apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, localResult);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Initialization failed:'),
|
||||
@@ -179,20 +207,49 @@ export class CreateAppCommand {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private logSuccess(appDirectory: string): void {
|
||||
private runAuthLogin(appDirectory: string, apiKey: string): void {
|
||||
try {
|
||||
execSync(
|
||||
`yarn twenty auth:login --api-key "${apiKey}" --api-url http://localhost:3000`,
|
||||
{ cwd: appDirectory, stdio: 'inherit' },
|
||||
);
|
||||
console.log(chalk.green('✅ Authenticated with local Twenty instance.'));
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Auto auth:login failed. Run `yarn twenty auth:login` manually.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
localResult: LocalInstanceResult,
|
||||
): void {
|
||||
const dirName = appDirectory.split('/').reverse()[0] ?? '';
|
||||
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(` cd ${dirName}`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(' yarn twenty dev # Start dev mode'),
|
||||
);
|
||||
|
||||
if (localResult.apiKey) {
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
} else if (localResult.running) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const INSTALL_SCRIPT_URL =
|
||||
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh';
|
||||
|
||||
const SERVER_CONTAINER = 'twenty-server-1';
|
||||
const DB_CONTAINER = 'twenty-db-1';
|
||||
|
||||
const isDockerAvailable = (): boolean => {
|
||||
try {
|
||||
execSync('docker compose version', { stdio: 'ignore' });
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isDockerRunning = (): boolean => {
|
||||
try {
|
||||
execSync('docker info', { stdio: 'ignore' });
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isTwentyServerRunning = async (): Promise<boolean> => {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch('http://localhost:3000/healthz', {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
return body.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getActiveWorkspaceId = (): string | null => {
|
||||
try {
|
||||
const result = execSync(
|
||||
`docker exec ${DB_CONTAINER} psql -U postgres -d default -t -c "SELECT id FROM core.workspace WHERE \\"activationStatus\\" = 'ACTIVE' LIMIT 1"`,
|
||||
{ encoding: 'utf-8' },
|
||||
).trim();
|
||||
|
||||
return result || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const generateApiKeyToken = (workspaceId: string): string | null => {
|
||||
try {
|
||||
const output = execSync(
|
||||
`docker exec -e NODE_ENV=development ${SERVER_CONTAINER} yarn command:prod workspace:generate-api-key -w ${workspaceId}`,
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
|
||||
const TOKEN_PREFIX = 'TOKEN:';
|
||||
const tokenLine = output
|
||||
.trim()
|
||||
.split('\n')
|
||||
.find((line) => line.includes(TOKEN_PREFIX));
|
||||
|
||||
if (!tokenLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenStartIndex =
|
||||
tokenLine.indexOf(TOKEN_PREFIX) + TOKEN_PREFIX.length;
|
||||
|
||||
return tokenLine.slice(tokenStartIndex).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export type LocalInstanceResult = {
|
||||
running: boolean;
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
export const setupLocalInstance = async (): Promise<LocalInstanceResult> => {
|
||||
console.log('');
|
||||
console.log(chalk.blue('🐳 Setting up local Twenty instance...'));
|
||||
|
||||
if (await isTwentyServerRunning()) {
|
||||
console.log(
|
||||
chalk.green('✅ Twenty server is already running on localhost:3000.'),
|
||||
);
|
||||
} else {
|
||||
if (!isDockerAvailable()) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Docker Compose is not installed. Please install Docker first.',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' See https://docs.docker.com/get-docker/'));
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
if (!isDockerRunning()) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Docker is not running. Please start Docker and try again.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`bash <(curl -sL ${INSTALL_SCRIPT_URL})`, {
|
||||
stdio: 'inherit',
|
||||
shell: '/bin/bash',
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow('⚠️ Local instance setup did not complete successfully.'),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
chalk.blue(
|
||||
'👉 Please create your workspace in the browser before continuing.',
|
||||
),
|
||||
);
|
||||
|
||||
const { workspaceCreated } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'workspaceCreated',
|
||||
message: 'Have you finished creating your workspace?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!workspaceCreated) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Skipping API key generation. Run `yarn twenty remote add --local` manually after creating your workspace.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: true };
|
||||
}
|
||||
|
||||
console.log(chalk.blue('🔑 Generating API key for your workspace...'));
|
||||
|
||||
const workspaceId = getActiveWorkspaceId();
|
||||
|
||||
if (!isDefined(workspaceId)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ No active workspace found. Make sure you completed the signup flow, then run `yarn twenty auth:login` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: true };
|
||||
}
|
||||
|
||||
const apiKey = generateApiKeyToken(workspaceId);
|
||||
|
||||
if (!isDefined(apiKey)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Could not generate API key. Run `yarn twenty auth:login` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: true };
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ API key generated for your workspace.'));
|
||||
|
||||
return { running: true, apiKey };
|
||||
};
|
||||
Reference in New Issue
Block a user