Publish twenty-cli npm package (#14871)

as title
This commit is contained in:
martmull
2025-10-03 13:08:06 +00:00
committed by GitHub
parent 0648dc5c65
commit f973a1bcdb
14 changed files with 235 additions and 45 deletions
+2 -1
View File
@@ -43,6 +43,7 @@
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18.0.0"
"node": "^24.5.0",
"yarn": "^4.0.2"
}
}
@@ -20,8 +20,8 @@
},
"name": {
"type": "string",
"description": "Name singular for the serverless function",
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$",
"description": "Name singular for the serverless function (eg: my-serverless-function)",
"pattern": "^[a-z0-9-]+$",
"minLength": 1,
"maxLength": 100
},
@@ -0,0 +1,54 @@
import inquirer from 'inquirer';
import chalk from 'chalk';
import { ApiService } from '../services/api.service';
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
import { loadManifest } from '../utils/app-manifest-loader';
export class AppDeleteCommand {
private apiService = new ApiService();
async execute(): Promise<void> {
try {
const appPath = CURRENT_EXECUTION_DIRECTORY;
console.log(chalk.blue('🚀 Deleting Twenty Application'));
console.log(chalk.gray(`📁 App Path: ${appPath}`));
console.log('');
if (!(await this.confirmationPrompt())) {
console.error(chalk.red('⛔️ Aborting deletion'));
process.exit(1);
}
const { packageJson } = await loadManifest(appPath);
const result = await this.apiService.deleteApplication(packageJson);
if (!result.success) {
console.error(chalk.red('❌ Deletion failed:'), result.error);
process.exit(1);
}
console.log(chalk.green('✅ Application deleted successfully'));
} catch (error) {
console.error(
chalk.red('Deletion failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
private async confirmationPrompt(): Promise<boolean> {
const { confirmation } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmation',
message: 'Are you sure you want to delete this application?',
default: false,
},
]);
return confirmation;
}
}
@@ -1,8 +1,8 @@
import chalk from 'chalk';
import * as chokidar from 'chokidar';
import { ApiService } from '../services/api.service';
import { syncApp } from '../utils/app-sync';
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
import { loadManifest } from '../utils/app-manifest-loader';
export class AppDevCommand {
private apiService = new ApiService();
@@ -15,7 +15,13 @@ export class AppDevCommand {
this.logStartupInfo(appPath, debounceMs);
await syncApp(appPath, this.apiService);
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
await this.apiService.syncApplication({
manifest,
packageJson,
yarnLock,
});
const watcher = this.setupFileWatcher(appPath, debounceMs);
@@ -54,7 +60,15 @@ export class AppDevCommand {
timeout = setTimeout(async () => {
console.log(chalk.blue('🔄 Changes detected, syncing...'));
await syncApp(appPath, this.apiService);
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
await this.apiService.syncApplication({
manifest,
packageJson,
yarnLock,
});
console.log(
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
);
@@ -1,7 +1,7 @@
import chalk from 'chalk';
import { ApiService } from '../services/api.service';
import { syncApp } from '../utils/app-sync';
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
import { loadManifest } from '../utils/app-manifest-loader';
export class AppSyncCommand {
private apiService = new ApiService();
@@ -14,7 +14,13 @@ export class AppSyncCommand {
console.log(chalk.gray(`📁 App Path: ${appPath}`));
console.log('');
const result = await syncApp(appPath, this.apiService);
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
const result = await this.apiService.syncApplication({
manifest,
packageJson,
yarnLock,
});
if (!result.success) {
console.error(chalk.red('❌ Sync failed:'), result.error);
@@ -2,6 +2,7 @@ import { Command } from 'commander';
import { AppSyncCommand } from './app-sync.command';
import { AppDevCommand } from './app-dev.command';
import { AppInitCommand } from './app-init.command';
import { AppDeleteCommand } from './app-delete.command';
import {
AppAddCommand,
isSyncableEntity,
@@ -12,6 +13,7 @@ import chalk from 'chalk';
export class AppCommand {
private devCommand = new AppDevCommand();
private syncCommand = new AppSyncCommand();
private deleteCommand = new AppDeleteCommand();
private initCommand = new AppInitCommand();
private addCommand = new AppAddCommand();
@@ -34,6 +36,13 @@ export class AppCommand {
await this.syncCommand.execute();
});
appCommand
.command('delete')
.description('Delete application from Twenty')
.action(async () => {
await this.deleteCommand.execute();
});
appCommand
.command('init [directory]')
.description('Initialize a new Twenty application')
@@ -72,7 +72,6 @@ export class ApiService {
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
'x-schema-version': '6',
},
},
);
@@ -115,7 +114,6 @@ export class ApiService {
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
'x-schema-version': '6',
},
},
);
@@ -143,4 +141,52 @@ export class ApiService {
throw error;
}
}
async deleteApplication(packageJson: PackageJson): Promise<ApiResponse> {
try {
const mutation = `
mutation DeleteApplication($packageJson: JSON!) {
deleteApplication(packageJson: $packageJson)
}
`;
const variables = { packageJson };
const response: AxiosResponse = await this.client.post(
'/metadata',
{
query: mutation,
variables,
},
{
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
},
},
);
if (response.data.errors) {
return {
success: false,
error:
response.data.errors[0]?.message || 'Failed to delete application',
};
}
return {
success: true,
data: response.data.data.deleteApplication,
message: `Successfully deleted application: ${packageJson.name}`,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return {
success: false,
error: error.response.data?.errors?.[0]?.message || error.message,
};
}
throw error;
}
}
}
-32
View File
@@ -1,32 +0,0 @@
import chalk from 'chalk';
import { ApiService } from '../services/api.service';
import { loadManifest } from './app-manifest-loader';
export const syncApp = async (
appPath: string,
apiService: ApiService,
): Promise<any> => {
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
try {
const result = await apiService.syncApplication({
manifest,
packageJson,
yarnLock,
});
if (result.success) {
console.log(chalk.green('✅ Application synced successfully'));
} else {
console.error(chalk.red('❌ Sync failed:'), result.error);
}
return result;
} catch (error) {
console.error(
chalk.red('Sync error:'),
error instanceof Error ? error.message : error,
);
throw error;
}
};
@@ -1694,6 +1694,7 @@ export type Mutation = {
createWorkflowVersionEdge: WorkflowVersionStepChanges;
createWorkflowVersionStep: WorkflowVersionStepChanges;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteApplication: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCoreView: Scalars['Boolean'];
deleteCoreViewField: CoreViewField;
@@ -2057,6 +2058,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
};
export type MutationDeleteApplicationArgs = {
packageJson: Scalars['JSON'];
};
export type MutationDeleteApprovedAccessDomainArgs = {
input: DeleteApprovedAccessDomainInput;
};
@@ -1650,6 +1650,7 @@ export type Mutation = {
createWorkflowVersionEdge: WorkflowVersionStepChanges;
createWorkflowVersionStep: WorkflowVersionStepChanges;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteApplication: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCoreView: Scalars['Boolean'];
deleteCoreViewField: CoreViewField;
@@ -1993,6 +1994,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
};
export type MutationDeleteApplicationArgs = {
packageJson: Scalars['JSON'];
};
export type MutationDeleteApprovedAccessDomainArgs = {
input: DeleteApprovedAccessDomainInput;
};
@@ -6,12 +6,15 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import { DeleteApplicationInput } from 'src/engine/core-modules/application/dtos/deleteApplication.input';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
export class ApplicationResolver {
constructor(
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationService: ApplicationService,
) {}
@Mutation(() => Boolean)
@@ -28,4 +31,17 @@ export class ApplicationResolver {
return true;
}
@Mutation(() => Boolean)
async deleteApplication(
@Args() { packageJson }: DeleteApplicationInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
await this.applicationService.delete(
packageJson.universalIdentifier,
workspaceId,
);
return true;
}
}
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
@@ -70,4 +71,20 @@ export class ApplicationService {
return updatedApplication;
}
async delete(universalIdentifier: string, workspaceId: string) {
const application = await this.findByUniversalIdentifier(
universalIdentifier,
workspaceId,
);
if (!isDefined(application)) {
throw new Error(`Application does not exist`);
}
await this.applicationRepository.delete({
universalIdentifier,
workspaceId,
});
}
}
@@ -0,0 +1,11 @@
import { ArgsType, Field } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
@ArgsType()
export class DeleteApplicationInput {
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
}
+39 -3
View File
@@ -21784,7 +21784,16 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^20.0.0, @types/node@npm:^20.3.1":
"@types/node@npm:^20.0.0":
version: 20.19.19
resolution: "@types/node@npm:20.19.19"
dependencies:
undici-types: "npm:~6.21.0"
checksum: 10c0/488589d6d1d562ad4e8ed9bb36703a8f3bb7feed206bba867358eda0965e781a408f14ed7ff1ca334ae6f6823dcfe2eb2536ffe48e2ab02d47b4c3c5395d274a
languageName: node
linkType: hard
"@types/node@npm:^20.3.1":
version: 20.19.13
resolution: "@types/node@npm:20.19.13"
dependencies:
@@ -25015,7 +25024,18 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.6.0, axios@npm:^1.6.1, axios@npm:^1.7.7, axios@npm:^1.8.2, axios@npm:^1.8.3":
"axios@npm:^1.6.0":
version: 1.12.2
resolution: "axios@npm:1.12.2"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/80b063e318cf05cd33a4d991cea0162f3573481946f9129efb7766f38fde4c061c34f41a93a9f9521f02b7c9565ccbc197c099b0186543ac84a24580017adfed
languageName: node
linkType: hard
"axios@npm:^1.6.1, axios@npm:^1.7.7, axios@npm:^1.8.2, axios@npm:^1.8.3":
version: 1.11.0
resolution: "axios@npm:1.11.0"
dependencies:
@@ -52246,7 +52266,7 @@ __metadata:
languageName: node
linkType: hard
"tsx@npm:^4.17.0, tsx@npm:^4.19.3, tsx@npm:^4.7.0":
"tsx@npm:^4.17.0, tsx@npm:^4.19.3":
version: 4.20.5
resolution: "tsx@npm:4.20.5"
dependencies:
@@ -52262,6 +52282,22 @@ __metadata:
languageName: node
linkType: hard
"tsx@npm:^4.7.0":
version: 4.20.6
resolution: "tsx@npm:4.20.6"
dependencies:
esbuild: "npm:~0.25.0"
fsevents: "npm:~2.3.3"
get-tsconfig: "npm:^4.7.5"
dependenciesMeta:
fsevents:
optional: true
bin:
tsx: dist/cli.mjs
checksum: 10c0/07757a9bf62c271e0a00869b2008c5f2d6e648766536e4faf27d9d8027b7cde1ac8e4871f4bb570c99388bcee0018e6869dad98c07df809b8052f9c549cd216f
languageName: node
linkType: hard
"tty-browserify@npm:0.0.1":
version: 0.0.1
resolution: "tty-browserify@npm:0.0.1"