Fixes https://github.com/twentyhq/core-team-issues/issues/1956 **Problem** Within an app, the `.yarn/releases/` folder contains executable Yarn binaries that run when executing any yarn command (`.yarnrc` file indicates yarn path to be `.yarn/releases/yarn-4.9.2.cjs `.) This is a supply chain attack vector: a malicious actor could submit a PR with a compromised `yarn-4.9.2.cjs binary`, which would execute arbitrary code on developers' machines or CI systems. **Fix** Actually, thanks to Corepack, we don't need to store and execute this binary. Corepack can be seen as the manager of a package manager: in `package.json` we indicate a packageManager version like `"packageManager": "[email protected]"`, and when executing `yarn` Corepack will securely fetch the verified version from npm, avoiding the risk of executing a compromised binary committed to the repository. This was already in our app's package.json template but we were not using it! We can now - remove the folder containing the binary from our app template base-application (that is scaffolded when creating an app through cli), `.yarn/releases/`, and remove `yarnPath: .yarn/releases/yarn-4.9.2.cjs` from its .yarnrc - remove them from the community apps that were already published in the repo - add .yarn to gitignore **Tested** This has been tested and works for app created in the repo, outside the repo, and existing apps in the repo
134 lines
3.8 KiB
TypeScript
134 lines
3.8 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';
|
|
|
|
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
|
|
|
export class CreateAppCommand {
|
|
async execute(directory?: string): Promise<void> {
|
|
try {
|
|
const { appName, appDisplayName, appDirectory, appDescription } =
|
|
await this.getAppInfos(directory);
|
|
|
|
await this.validateDirectory(appDirectory);
|
|
|
|
this.logCreationInfo({ appDirectory, appName });
|
|
|
|
await fs.ensureDir(appDirectory);
|
|
|
|
await copyBaseApplicationProject({
|
|
appName,
|
|
appDisplayName,
|
|
appDescription,
|
|
appDirectory,
|
|
});
|
|
|
|
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: '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 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(` corepack enable # if you don't use yarn@4`));
|
|
console.log(chalk.gray(` yarn install # if you don't use yarn@4`));
|
|
console.log(chalk.gray(' yarn auth:login # Authenticate with Twenty'));
|
|
console.log(chalk.gray(' yarn app:dev # Start dev mode'));
|
|
}
|
|
}
|