feat(sdk): use config file as single source of truth, remove env var fallbacks (#19409)
## Summary - **Config as source of truth**: `~/.twenty/config.json` is now the single source of truth for SDK authentication — env var fallbacks have been removed from the config resolution chain. - **Test instance support**: `twenty server start --test` spins up a dedicated Docker instance on port 2021 with its own config (`config.test.json`), so integration tests don't interfere with the dev environment. - **API key auth for marketplace**: Removed `UserAuthGuard` from `MarketplaceResolver` so API key tokens (workspace-scoped) can call `installMarketplaceApp`. - **CI for example apps**: Added monorepo CI workflows for `hello-world` and `postcard` example apps to catch regressions. - **Simplified CI**: All `ci-create-app-e2e` and example app workflows now use a shared `spawn-twenty-app-dev-test` action (Docker-based) instead of building the server from source. Consolidated auth env vars to `TWENTY_API_URL` + `TWENTY_API_KEY`. - **Template publishing fix**: `create-twenty-app` template now correctly preserves `.github/` and `.gitignore` through npm publish (stored without leading dot, renamed after copy). ## Test plan - [x] CI SDK (lint, typecheck, unit, integration, e2e) — all green - [x] CI Example App Hello World — green - [x] CI Example App Postcard — green - [x] CI Create App E2E minimal — green - [x] CI Front, CI Server, CI Shared — green
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { DevModeOrchestrator } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator';
|
||||
import { OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
@@ -29,9 +30,11 @@ export class AppDevCommand {
|
||||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
|
||||
const config = await new ConfigService().getConfig();
|
||||
|
||||
const orchestratorState = new OrchestratorState({
|
||||
appPath,
|
||||
frontendUrl: process.env.FRONTEND_URL,
|
||||
frontendUrl: config.apiUrl,
|
||||
});
|
||||
|
||||
if (!options.headless) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { authLogin } from '@/cli/operations/login';
|
||||
import { authLoginOAuth } from '@/cli/operations/login-oauth';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
|
||||
import { detectLocalServer } from '@/cli/utilities/server/detect-local-server';
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
@@ -72,14 +73,19 @@ export const registerRemoteCommands = (program: Command): void => {
|
||||
.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)')
|
||||
.option('--test', 'Write to config.test.json (for integration tests)')
|
||||
.action(
|
||||
async (options: {
|
||||
as?: string;
|
||||
apiKey?: string;
|
||||
apiUrl?: string;
|
||||
local?: boolean;
|
||||
test?: boolean;
|
||||
}) => {
|
||||
const configService = new ConfigService();
|
||||
const configPath = options.test ? getConfigPath(true) : undefined;
|
||||
const configService = new ConfigService(
|
||||
configPath ? { configPath } : undefined,
|
||||
);
|
||||
const existingRemotes = await configService.getRemotes();
|
||||
|
||||
if (options.as !== undefined && existingRemotes.includes(options.as)) {
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
CONTAINER_NAME,
|
||||
containerExists,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TEST_PORT,
|
||||
getContainerPort,
|
||||
isContainerRunning,
|
||||
TEST_CONTAINER_NAME,
|
||||
} from '@/cli/utilities/server/docker-container';
|
||||
import { checkServerHealth } from '@/cli/utilities/server/detect-local-server';
|
||||
import chalk from 'chalk';
|
||||
@@ -19,9 +21,11 @@ export const registerServerCommands = (program: Command): void => {
|
||||
server
|
||||
.command('start')
|
||||
.description('Start a local Twenty server')
|
||||
.option('-p, --port <port>', 'HTTP port', String(DEFAULT_PORT))
|
||||
.action(async (options: { port: string }) => {
|
||||
const port = parseInt(options.port, 10);
|
||||
.option('-p, --port <port>', 'HTTP port')
|
||||
.option('--test', 'Start a separate test instance (port 2021)')
|
||||
.action(async (options: { port?: string; test?: boolean }) => {
|
||||
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
|
||||
const port = options.port ? parseInt(options.port, 10) : defaultPort;
|
||||
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
console.error(chalk.red('Invalid port number.'));
|
||||
@@ -30,6 +34,7 @@ export const registerServerCommands = (program: Command): void => {
|
||||
|
||||
const result = await serverStart({
|
||||
port,
|
||||
test: options.test,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
@@ -42,14 +47,17 @@ export const registerServerCommands = (program: Command): void => {
|
||||
server
|
||||
.command('stop')
|
||||
.description('Stop the local Twenty server')
|
||||
.action(() => {
|
||||
if (!containerExists()) {
|
||||
.option('--test', 'Stop the test instance')
|
||||
.action((options: { test?: boolean }) => {
|
||||
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
|
||||
if (!containerExists(containerName)) {
|
||||
console.log(chalk.yellow('No Twenty server container found.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
execSync(`docker stop ${CONTAINER_NAME}`, { stdio: 'ignore' });
|
||||
execSync(`docker stop ${containerName}`, { stdio: 'ignore' });
|
||||
console.log(chalk.green('Twenty server stopped.'));
|
||||
});
|
||||
|
||||
@@ -57,8 +65,11 @@ export const registerServerCommands = (program: Command): void => {
|
||||
.command('logs')
|
||||
.description('Stream Twenty server logs')
|
||||
.option('-n, --lines <lines>', 'Number of lines to show', '50')
|
||||
.action((options: { lines: string }) => {
|
||||
if (!containerExists()) {
|
||||
.option('--test', 'Show logs for the test instance')
|
||||
.action((options: { lines: string; test?: boolean }) => {
|
||||
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
|
||||
if (!containerExists(containerName)) {
|
||||
console.log(chalk.yellow('No Twenty server container found.'));
|
||||
|
||||
return;
|
||||
@@ -67,7 +78,7 @@ export const registerServerCommands = (program: Command): void => {
|
||||
try {
|
||||
spawnSync(
|
||||
'docker',
|
||||
['logs', '-f', '--tail', options.lines, CONTAINER_NAME],
|
||||
['logs', '-f', '--tail', options.lines, containerName],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
} catch {
|
||||
@@ -78,18 +89,24 @@ export const registerServerCommands = (program: Command): void => {
|
||||
server
|
||||
.command('status')
|
||||
.description('Show Twenty server status')
|
||||
.action(async () => {
|
||||
if (!containerExists()) {
|
||||
.option('--test', 'Show status of the test instance')
|
||||
.action(async (options: { test?: boolean }) => {
|
||||
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
|
||||
|
||||
if (!containerExists(containerName)) {
|
||||
console.log(` Status: ${chalk.gray('not created')}`);
|
||||
console.log(
|
||||
chalk.gray(" Run 'yarn twenty server start' to create one."),
|
||||
chalk.gray(
|
||||
` Run 'yarn twenty server start${options.test ? ' --test' : ''}' to create one.`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const running = isContainerRunning();
|
||||
const port = running ? getContainerPort() : DEFAULT_PORT;
|
||||
const running = isContainerRunning(containerName);
|
||||
const port = running ? getContainerPort(containerName) : defaultPort;
|
||||
const healthy = running ? await checkServerHealth(port) : false;
|
||||
|
||||
const statusText = healthy
|
||||
@@ -109,25 +126,33 @@ export const registerServerCommands = (program: Command): void => {
|
||||
server
|
||||
.command('reset')
|
||||
.description('Delete all data and start fresh')
|
||||
.action(() => {
|
||||
if (containerExists()) {
|
||||
execSync(`docker rm -f ${CONTAINER_NAME}`, { stdio: 'ignore' });
|
||||
.option('--test', 'Reset the test instance')
|
||||
.action((options: { test?: boolean }) => {
|
||||
const containerName = options.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
const volumeData = options.test
|
||||
? 'twenty-app-dev-test-data'
|
||||
: 'twenty-app-dev-data';
|
||||
const volumeStorage = options.test
|
||||
? 'twenty-app-dev-test-storage'
|
||||
: 'twenty-app-dev-storage';
|
||||
|
||||
if (containerExists(containerName)) {
|
||||
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
'docker volume rm twenty-app-dev-data twenty-app-dev-storage',
|
||||
{
|
||||
stdio: 'ignore',
|
||||
},
|
||||
);
|
||||
execSync(`docker volume rm ${volumeData} ${volumeStorage}`, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} catch {
|
||||
// Volumes may not exist
|
||||
}
|
||||
|
||||
console.log(chalk.green('Twenty server data reset.'));
|
||||
console.log(
|
||||
chalk.gray("Run 'yarn twenty server start' to start a fresh instance."),
|
||||
chalk.gray(
|
||||
`Run 'yarn twenty server start${options.test ? ' --test' : ''}' to start a fresh instance.`,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user