Simplify create-twenty-app command (#20512)

## Simplify `create-twenty-app` for zero-interaction use

Makes `npx create-twenty-app@latest my-app` a fully non-interactive,
single-command experience suitable for automated environments (Codex,
Claude plugins).

  ### Changes

- **Remove all interactive prompts** — app name, display name,
description, and scaffold confirmation are now derived from CLI args
with sensible defaults. `inquirer` dependency removed
   entirely.
- **Replace OAuth with API key auth** — use the seeded dev API key
(`DEV_API_KEY`) to authenticate against the Docker instance as
`tim@apple.dev`, eliminating the browser-based OAuth
  flow.
- **Docker-first with early validation** — check Docker is installed
before scaffolding; if missing, print the install URL and exit. Detect
alternative runtimes (Podman, nerdctl).
- **Parallel image pull** — `docker pull` runs in the background during
scaffold + dependency install, saving 10-30s on typical runs.
- **Always pull latest image** — ensures the dev server is up-to-date on
every run.
- **Stop detecting port 3000** — only check port 2020 (Docker instance).
- **Update CLI flags** — remove `--skip-local-instance` and `--yes`; add
`--skip-docker`.
- **Update CI workflows and docs** — align e2e workflows, package
README, and template README/cd.yml with the new flow.
This commit is contained in:
martmull
2026-05-13 18:44:27 +02:00
committed by GitHub
parent 70a3b25680
commit 0cc2194399
25 changed files with 383 additions and 378 deletions
@@ -7,6 +7,7 @@ import { ApplicationDevelopmentResolver } from 'src/engine/core-modules/applicat
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@@ -21,6 +22,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
TokenModule,
FileStorageModule,
PermissionsModule,
ThrottlerModule,
],
providers: [
ApplicationDevelopmentResolver,
@@ -42,19 +42,21 @@ import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspac
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { DevelopmentGuard } from 'src/engine/guards/development.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
const APP_DEV_RATE_LIMIT_MAX = 30;
const APP_DEV_RATE_LIMIT_WINDOW_MS = 30_000;
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
@UseFilters(ApplicationExceptionFilter)
@UseGuards(
WorkspaceAuthGuard,
DevelopmentGuard,
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
)
export class ApplicationDevelopmentResolver {
@@ -67,6 +69,7 @@ export class ApplicationDevelopmentResolver {
private readonly fileStorageService: FileStorageService,
private readonly sdkClientGenerationService: SdkClientGenerationService,
private readonly twentyConfigService: TwentyConfigService,
private readonly throttlerService: ThrottlerService,
) {}
@Mutation(() => DevelopmentApplicationDTO)
@@ -74,6 +77,8 @@ export class ApplicationDevelopmentResolver {
@Args() { universalIdentifier, name }: CreateDevelopmentApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<DevelopmentApplicationDTO> {
await this.throttlePerApplication(universalIdentifier, workspaceId);
const applicationRegistrationId =
await this.findApplicationRegistrationId(universalIdentifier);
@@ -111,6 +116,8 @@ export class ApplicationDevelopmentResolver {
@AuthUser({ allowUndefined: true }) user?: { id: string },
@AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string,
): Promise<ApplicationTokenPairDTO> {
await this.throttlePerApplication(applicationId, workspaceId);
return this.applicationTokenService.generateApplicationTokenPair({
workspaceId,
applicationId,
@@ -124,6 +131,11 @@ export class ApplicationDevelopmentResolver {
@Args() { manifest }: ApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<WorkspaceMigrationDTO> {
await this.throttlePerApplication(
manifest.application.universalIdentifier,
workspaceId,
);
const applicationRegistrationId = await this.findApplicationRegistrationId(
manifest.application.universalIdentifier,
);
@@ -187,6 +199,11 @@ export class ApplicationDevelopmentResolver {
filePath,
}: UploadApplicationFileInput,
): Promise<FileDTO> {
await this.throttlePerApplication(
applicationUniversalIdentifier,
workspaceId,
);
const allowedApplicationFileFolders: FileFolder[] = [
FileFolder.BuiltLogicFunction,
FileFolder.BuiltFrontComponent,
@@ -215,6 +232,18 @@ export class ApplicationDevelopmentResolver {
});
}
private async throttlePerApplication(
applicationIdentifier: string,
workspaceId: string,
): Promise<void> {
await this.throttlerService.tokenBucketThrottleOrThrow(
`app-dev:${workspaceId}:${applicationIdentifier}`,
1,
APP_DEV_RATE_LIMIT_MAX,
APP_DEV_RATE_LIMIT_WINDOW_MS,
);
}
private async findApplicationRegistrationId(
universalIdentifier: string,
): Promise<string> {