Files
twenty/packages/create-twenty-app/src/create-app.command.ts
T
Félix Malfait d4ce4414e2 Replace JWT claim system with npm maintainer email matching and provenance metadata
- Remove ApplicationNpmClaimService, NpmClaimTokenDTO, NPM_CLAIM JWT type,
  and all claim-related mutations/methods across server, SDK, and frontend
- Add provenanceRepositoryUrl, isProvenanceVerified, provenanceVerifiedAt
  fields to ApplicationRegistrationEntity with core migration
- Create ApplicationNpmRegistrationService with email-based ownership
  verification and lightweight npm provenance attestation fetching
- Add registerNpmPackage GraphQL mutation with workspace auth guards
- Enforce twenty-app- prefix for all npm package names in validation
  and marketplace search filtering
- Create .github/actions/publish-twenty-app composite action for
  building and publishing apps with --provenance
- Scaffold publish.yml GitHub workflow in create-twenty-app
- Add app:register CLI command and public operation in SDK
- Update frontend empty state to show app:register instructions
- Enrich MarketplaceCatalogSyncService to fetch and store provenance
  metadata during npm app discovery
- Add unit tests for registration service and package name validation

Made-with: Cursor
2026-03-07 11:02:47 +01:00

171 lines
4.7 KiB
TypeScript

import { copyBaseApplicationProject } from '@/utils/app-template';
import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install';
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 * as path from 'path';
import {
type ExampleOptions,
type ScaffoldingMode,
} from '@/types/scaffolding-options';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
export class CreateAppCommand {
async execute(
directory?: string,
mode: ScaffoldingMode = 'exhaustive',
): Promise<void> {
try {
const { appName, appDisplayName, appDirectory, appDescription } =
await this.getAppInfos(directory);
const exampleOptions = this.resolveExampleOptions(mode);
await this.validateDirectory(appDirectory);
this.logCreationInfo({ appDirectory, appName });
await fs.ensureDir(appDirectory);
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
exampleOptions,
});
await install(appDirectory);
await tryGitInit(appDirectory);
this.logSuccess(appDirectory);
} catch (error) {
console.error(
chalk.red('Initialization failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
private async getAppInfos(directory?: string): Promise<{
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}> {
const { name, displayName, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
when: () => !directory,
default: 'twenty-app-my-awesome-app',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
return true;
},
},
{
type: 'input',
name: 'displayName',
message: 'Application display name:',
default: (answers: any) => {
return convertToLabel(answers?.name ?? directory);
},
},
{
type: 'input',
name: 'description',
message: 'Application description (optional):',
default: '',
},
]);
const computedName = name ?? directory;
const appName = computedName.trim();
const appDisplayName = displayName.trim();
const appDescription = description.trim();
const appDirectory = directory
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
return { appName, appDisplayName, appDirectory, appDescription };
}
private resolveExampleOptions(mode: ScaffoldingMode): ExampleOptions {
if (mode === 'minimal') {
return {
includeExampleObject: false,
includeExampleField: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
includeExampleIntegrationTest: false,
};
}
return {
includeExampleObject: true,
includeExampleField: true,
includeExampleLogicFunction: true,
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
includeExampleIntegrationTest: true,
};
}
private async validateDirectory(appDirectory: string): Promise<void> {
if (!(await fs.pathExists(appDirectory))) {
return;
}
const files = await fs.readdir(appDirectory);
if (files.length > 0) {
throw new Error(
`Directory ${appDirectory} already exists and is not empty`,
);
}
}
private logCreationInfo({
appDirectory,
appName,
}: {
appDirectory: string;
appName: string;
}): void {
console.log(chalk.blue('🎯 Creating Twenty Application'));
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
console.log(chalk.gray(`📝 Name: ${appName}`));
console.log('');
}
private logSuccess(appDirectory: string): 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 auth:login # Authenticate with Twenty'),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
}
}