diff --git a/packages/twenty-apps/hello-world/package.json b/packages/twenty-apps/hello-world/package.json index de31379acf..118b7c03a9 100644 --- a/packages/twenty-apps/hello-world/package.json +++ b/packages/twenty-apps/hello-world/package.json @@ -9,11 +9,13 @@ }, "packageManager": "yarn@4.9.2", "scripts": { - "create-entity": "twenty app add", - "dev": "twenty app dev", - "sync": "twenty app sync", - "uninstall": "twenty app uninstall", - "auth": "twenty auth login" + "twenty": "twenty", + "auth": "twenty auth:login", + "dev": "twenty app:dev", + "build": "twenty app:build", + "typecheck": "twenty app:typecheck", + "uninstall": "twenty app:uninstall", + "entity:add": "twenty entity:add" }, "dependencies": { "twenty-sdk": "0.6.3" diff --git a/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts b/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts index b469559320..41b3600c58 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts @@ -12,32 +12,24 @@ export class AppUninstallCommand { appPath?: string; askForConfirmation: boolean; }): Promise> { - try { - console.log(chalk.blue('🚀 Uninstall Twenty Application')); - console.log(chalk.gray(`📁 App Path: ${appPath}`)); - console.log(''); + console.log(chalk.blue('🚀 Uninstall Twenty Application')); + console.log(chalk.gray(`📁 App Path: ${appPath}`)); + console.log(''); - if (askForConfirmation && !(await this.confirmationPrompt())) { - console.error(chalk.red('⛔️ Aborting uninstall')); - process.exit(1); - } - - const result = await appUninstall({ appPath }); - - if (!result.success) { - console.error(chalk.red('❌ Uninstall failed:'), result.error.message); - return { success: false, error: result.error.message }; - } - - console.log(chalk.green('✅ Application uninstalled successfully')); - return { success: true, data: undefined }; - } catch (error) { - console.error( - chalk.red('Uninstall failed:'), - error instanceof Error ? error.message : error, - ); - throw error; + if (askForConfirmation && !(await this.confirmationPrompt())) { + console.error(chalk.red('⛔️ Aborting uninstall')); + process.exit(1); } + + const result = await appUninstall({ appPath }); + + if (!result.success) { + console.error(chalk.red('❌ Uninstall failed:'), result.error.message); + return { success: false, error: result.error.message }; + } + + console.log(chalk.green('✅ Application uninstalled successfully')); + return { success: true, data: undefined }; } private async confirmationPrompt(): Promise { diff --git a/packages/twenty-sdk/src/cli/commands/auth/auth-login.ts b/packages/twenty-sdk/src/cli/commands/auth/auth-login.ts index 642ee42aba..996b9d07b0 100644 --- a/packages/twenty-sdk/src/cli/commands/auth/auth-login.ts +++ b/packages/twenty-sdk/src/cli/commands/auth/auth-login.ts @@ -7,63 +7,55 @@ export class AuthLoginCommand { private configService = new ConfigService(); async execute(options: { apiKey?: string; apiUrl?: string }): Promise { - try { - let { apiKey, apiUrl } = options; + let { apiKey, apiUrl } = options; - const config = await this.configService.getConfig(); + const config = await this.configService.getConfig(); - if (!apiUrl) { - const urlAnswer = await inquirer.prompt([ - { - type: 'input', - name: 'apiUrl', - message: 'Twenty API URL:', - default: config.apiUrl, - validate: (input) => { - try { - new URL(input); - return true; - } catch { - return 'Please enter a valid URL'; - } - }, + if (!apiUrl) { + const urlAnswer = await inquirer.prompt([ + { + type: 'input', + name: 'apiUrl', + message: 'Twenty API URL:', + default: config.apiUrl, + validate: (input) => { + try { + new URL(input); + return true; + } catch { + return 'Please enter a valid URL'; + } }, - ]); - apiUrl = urlAnswer.apiUrl; - } + }, + ]); + apiUrl = urlAnswer.apiUrl; + } - if (!apiKey) { - const keyAnswer = await inquirer.prompt([ - { - type: 'password', - name: 'apiKey', - message: 'API Key:', - mask: '*', - validate: (input) => input.length > 0 || 'API key is required', - }, - ]); - apiKey = keyAnswer.apiKey; - } + if (!apiKey) { + const keyAnswer = await inquirer.prompt([ + { + type: 'password', + name: 'apiKey', + message: 'API Key:', + mask: '*', + validate: (input) => input.length > 0 || 'API key is required', + }, + ]); + apiKey = keyAnswer.apiKey; + } - const result = await authLogin({ apiKey: apiKey!, apiUrl: apiUrl! }); + const result = await authLogin({ apiKey: apiKey!, apiUrl: apiUrl! }); - if (result.success) { - const activeWorkspace = ConfigService.getActiveWorkspace(); - console.log( - chalk.green( - `✓ Successfully authenticated with Twenty (workspace: ${activeWorkspace})`, - ), - ); - } else { - console.log( - chalk.red('✗ Authentication failed. Please check your credentials.'), - ); - process.exit(1); - } - } catch (error) { - console.error( - chalk.red('Login failed:'), - error instanceof Error ? error.message : error, + if (result.success) { + const activeWorkspace = ConfigService.getActiveWorkspace(); + console.log( + chalk.green( + `✓ Successfully authenticated with Twenty (workspace: ${activeWorkspace})`, + ), + ); + } else { + console.log( + chalk.red('✗ Authentication failed. Please check your credentials.'), ); process.exit(1); } diff --git a/packages/twenty-sdk/src/cli/commands/auth/auth-logout.ts b/packages/twenty-sdk/src/cli/commands/auth/auth-logout.ts index 837f72c207..6008e476a4 100644 --- a/packages/twenty-sdk/src/cli/commands/auth/auth-logout.ts +++ b/packages/twenty-sdk/src/cli/commands/auth/auth-logout.ts @@ -4,20 +4,16 @@ import chalk from 'chalk'; export class AuthLogoutCommand { async execute(): Promise { - try { - await authLogout(); - const activeWorkspace = ConfigService.getActiveWorkspace(); - console.log( - chalk.green( - `✓ Successfully logged out (workspace: ${activeWorkspace})`, - ), - ); - } catch (error) { - console.error( - chalk.red('Logout failed:'), - error instanceof Error ? error.message : error, - ); + const result = await authLogout(); + + if (!result.success) { + console.error(chalk.red('Logout failed:'), result.error.message); process.exit(1); } + + const activeWorkspace = ConfigService.getActiveWorkspace(); + console.log( + chalk.green(`✓ Successfully logged out (workspace: ${activeWorkspace})`), + ); } } diff --git a/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts b/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts index 2703635fab..68b10ccf29 100644 --- a/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts +++ b/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts @@ -21,134 +21,126 @@ export class LogicFunctionExecuteCommand { functionName?: string; payload?: string; }): Promise { + let parsedPayload: Record; try { - let parsedPayload: Record; - try { - parsedPayload = JSON.parse(payload); - } catch { - console.error( - chalk.red('Invalid JSON payload. Please provide valid JSON.'), - ); - process.exit(1); - } - - const identifier = postInstall - ? 'post install' - : (functionUniversalIdentifier ?? functionName); - - console.log(chalk.blue(`🚀 Executing function "${identifier}"...`)); - console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}`)); - console.log(''); - - const executeOptions = postInstall - ? { appPath, postInstall: true as const, payload: parsedPayload } - : functionUniversalIdentifier - ? { appPath, functionUniversalIdentifier, payload: parsedPayload } - : { appPath, functionName: functionName!, payload: parsedPayload }; - - const result = await functionExecute(executeOptions); - - if (!result.success) { - switch (result.error.code) { - case APP_ERROR_CODES.MANIFEST_NOT_FOUND: { - console.error(chalk.red('Failed to build manifest.')); - break; - } - case FUNCTION_ERROR_CODES.FETCH_FUNCTIONS_FAILED: { - console.error( - chalk.red('Failed to fetch functions:'), - result.error.message, - ); - break; - } - case FUNCTION_ERROR_CODES.FUNCTION_NOT_FOUND: { - console.error(chalk.red(result.error.message)); - console.log(''); - - const availableFunctions = (result.error.details - ?.availableFunctions ?? []) as Array<{ - name: string; - universalIdentifier: string; - }>; - - if (availableFunctions.length > 0) { - console.log(chalk.cyan('Available functions:')); - availableFunctions.forEach((logicFunction) => { - console.log( - ` - ${chalk.white(logicFunction.name)} (${logicFunction.universalIdentifier})`, - ); - }); - } else { - console.log( - chalk.yellow( - 'No functions found for this application. Have you synced your app with `yarn app:dev`?', - ), - ); - } - break; - } - case FUNCTION_ERROR_CODES.EXECUTION_FAILED: { - console.error(chalk.red('Execution failed:'), result.error.message); - break; - } - default: { - console.error(chalk.red(result.error.message)); - } - } - process.exit(1); - } - - const executionResult = result.data; - - console.log(chalk.cyan('─'.repeat(60))); - console.log(chalk.cyan('Execution Result')); - console.log(chalk.cyan('─'.repeat(60))); - - const statusColor = - executionResult.status === 'SUCCESS' ? chalk.green : chalk.red; - console.log( - `${chalk.bold('Status:')} ${statusColor(executionResult.status)}`, - ); - - console.log(`${chalk.bold('Duration:')} ${executionResult.duration}ms`); - - if (isDefined(executionResult.data)) { - console.log(''); - console.log(chalk.bold('Data:')); - console.log(chalk.white(JSON.stringify(executionResult.data, null, 2))); - } - - if (executionResult.error) { - console.log(''); - console.log(chalk.bold.red('Error:')); - console.log(chalk.red(` Type: ${executionResult.error.errorType}`)); - console.log( - chalk.red(` Message: ${executionResult.error.errorMessage}`), - ); - if (executionResult.error.stackTrace) { - console.log(''); - console.log(chalk.gray('Stack trace:')); - console.log(chalk.gray(executionResult.error.stackTrace)); - } - } - - if (executionResult.logs) { - console.log(''); - console.log(chalk.bold('Logs:')); - console.log(chalk.gray(executionResult.logs)); - } - - console.log(chalk.cyan('─'.repeat(60))); - - if (executionResult.status !== 'SUCCESS') { - process.exit(1); - } - } catch (error) { + parsedPayload = JSON.parse(payload); + } catch { console.error( - chalk.red('Execution failed:'), - error instanceof Error ? error.message : error, + chalk.red('Invalid JSON payload. Please provide valid JSON.'), ); process.exit(1); } + + const identifier = postInstall + ? 'post install' + : (functionUniversalIdentifier ?? functionName); + + console.log(chalk.blue(`🚀 Executing function "${identifier}"...`)); + console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}`)); + console.log(''); + + const executeOptions = postInstall + ? { appPath, postInstall: true as const, payload: parsedPayload } + : functionUniversalIdentifier + ? { appPath, functionUniversalIdentifier, payload: parsedPayload } + : { appPath, functionName: functionName!, payload: parsedPayload }; + + const result = await functionExecute(executeOptions); + + if (!result.success) { + switch (result.error.code) { + case APP_ERROR_CODES.MANIFEST_NOT_FOUND: { + console.error(chalk.red('Failed to build manifest.')); + break; + } + case FUNCTION_ERROR_CODES.FETCH_FUNCTIONS_FAILED: { + console.error( + chalk.red('Failed to fetch functions:'), + result.error.message, + ); + break; + } + case FUNCTION_ERROR_CODES.FUNCTION_NOT_FOUND: { + console.error(chalk.red(result.error.message)); + console.log(''); + + const availableFunctions = (result.error.details + ?.availableFunctions ?? []) as Array<{ + name: string; + universalIdentifier: string; + }>; + + if (availableFunctions.length > 0) { + console.log(chalk.cyan('Available functions:')); + availableFunctions.forEach((logicFunction) => { + console.log( + ` - ${chalk.white(logicFunction.name)} (${logicFunction.universalIdentifier})`, + ); + }); + } else { + console.log( + chalk.yellow( + 'No functions found for this application. Have you synced your app with `yarn app:dev`?', + ), + ); + } + break; + } + case FUNCTION_ERROR_CODES.EXECUTION_FAILED: { + console.error(chalk.red('Execution failed:'), result.error.message); + break; + } + default: { + console.error(chalk.red(result.error.message)); + } + } + process.exit(1); + } + + const executionResult = result.data; + + console.log(chalk.cyan('─'.repeat(60))); + console.log(chalk.cyan('Execution Result')); + console.log(chalk.cyan('─'.repeat(60))); + + const statusColor = + executionResult.status === 'SUCCESS' ? chalk.green : chalk.red; + console.log( + `${chalk.bold('Status:')} ${statusColor(executionResult.status)}`, + ); + + console.log(`${chalk.bold('Duration:')} ${executionResult.duration}ms`); + + if (isDefined(executionResult.data)) { + console.log(''); + console.log(chalk.bold('Data:')); + console.log(chalk.white(JSON.stringify(executionResult.data, null, 2))); + } + + if (executionResult.error) { + console.log(''); + console.log(chalk.bold.red('Error:')); + console.log(chalk.red(` Type: ${executionResult.error.errorType}`)); + console.log( + chalk.red(` Message: ${executionResult.error.errorMessage}`), + ); + if (executionResult.error.stackTrace) { + console.log(''); + console.log(chalk.gray('Stack trace:')); + console.log(chalk.gray(executionResult.error.stackTrace)); + } + } + + if (executionResult.logs) { + console.log(''); + console.log(chalk.bold('Logs:')); + console.log(chalk.gray(executionResult.logs)); + } + + console.log(chalk.cyan('─'.repeat(60))); + + if (executionResult.status !== 'SUCCESS') { + process.exit(1); + } } } diff --git a/packages/twenty-sdk/src/cli/public-operations/app-build.ts b/packages/twenty-sdk/src/cli/public-operations/app-build.ts index 2d500bf1b7..55d5eaef53 100644 --- a/packages/twenty-sdk/src/cli/public-operations/app-build.ts +++ b/packages/twenty-sdk/src/cli/public-operations/app-build.ts @@ -3,6 +3,7 @@ import { synchronizeBuiltApplication } from '@/cli/utilities/build/common/synchr import { runTypecheck } from '@/cli/utilities/build/common/typecheck-plugin'; import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest'; import { ClientService } from '@/cli/utilities/client/client-service'; +import { runSafe } from '@/cli/utilities/run-safe'; import { APP_ERROR_CODES, type CommandResult } from './types'; export type AppBuildOptions = { @@ -14,7 +15,7 @@ export type AppBuildResult = { fileCount: number; }; -export const appBuild = async ( +const innerAppBuild = async ( options: AppBuildOptions, ): Promise> => { const { appPath, onProgress } = options; @@ -34,6 +35,10 @@ export const appBuild = async ( } const { manifest, filePaths } = manifestResult; + + for (const warning of manifestResult.warnings) { + onProgress?.(`⚠ ${warning}`); + } const clientService = new ClientService(); await clientService.ensureGeneratedClientStub({ appPath }); @@ -68,13 +73,14 @@ export const appBuild = async ( if (typecheckErrors.length > 0) { const errorMessages = typecheckErrors.map( - (error) => `${error.file}(${error.line},${error.column}): ${error.text}`, + (error) => + `${error.file}(${error.line},${error.column + 1}): ${error.text}`, ); return { success: false, error: { - code: APP_ERROR_CODES.SYNC_FAILED, + code: APP_ERROR_CODES.TYPECHECK_FAILED, message: `Typecheck failed:\n${errorMessages.join('\n')}`, }, }; @@ -107,3 +113,8 @@ export const appBuild = async ( }, }; }; + +export const appBuild = ( + options: AppBuildOptions, +): Promise> => + runSafe(() => innerAppBuild(options), APP_ERROR_CODES.SYNC_FAILED); diff --git a/packages/twenty-sdk/src/cli/public-operations/app-uninstall.ts b/packages/twenty-sdk/src/cli/public-operations/app-uninstall.ts index fcb4f1fab8..1eddc1fb8a 100644 --- a/packages/twenty-sdk/src/cli/public-operations/app-uninstall.ts +++ b/packages/twenty-sdk/src/cli/public-operations/app-uninstall.ts @@ -1,6 +1,7 @@ import { ApiService } from '@/cli/utilities/api/api-service'; import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader'; import { ConfigService } from '@/cli/utilities/config/config-service'; +import { runSafe } from '@/cli/utilities/run-safe'; import { APP_ERROR_CODES, type CommandResult } from './types'; export type AppUninstallOptions = { @@ -8,7 +9,7 @@ export type AppUninstallOptions = { workspace?: string; }; -export const appUninstall = async ( +const innerAppUninstall = async ( options: AppUninstallOptions, ): Promise => { if (options.workspace) { @@ -23,7 +24,8 @@ export const appUninstall = async ( success: false, error: { code: APP_ERROR_CODES.MANIFEST_NOT_FOUND, - message: 'Failed to build manifest.', + message: + 'Manifest not found. Run `app:build` or `app:dev` to generate it first.', }, }; } @@ -49,3 +51,8 @@ export const appUninstall = async ( return { success: true, data: undefined }; }; + +export const appUninstall = ( + options: AppUninstallOptions, +): Promise => + runSafe(() => innerAppUninstall(options), APP_ERROR_CODES.UNINSTALL_FAILED); diff --git a/packages/twenty-sdk/src/cli/public-operations/auth-login.ts b/packages/twenty-sdk/src/cli/public-operations/auth-login.ts index d913734485..dabfbb4475 100644 --- a/packages/twenty-sdk/src/cli/public-operations/auth-login.ts +++ b/packages/twenty-sdk/src/cli/public-operations/auth-login.ts @@ -1,5 +1,6 @@ import { ApiService } from '@/cli/utilities/api/api-service'; import { ConfigService } from '@/cli/utilities/config/config-service'; +import { runSafe } from '@/cli/utilities/run-safe'; import { AUTH_ERROR_CODES, type CommandResult } from './types'; export type AuthLoginOptions = { @@ -8,7 +9,7 @@ export type AuthLoginOptions = { workspace?: string; }; -export const authLogin = async ( +const innerAuthLogin = async ( options: AuthLoginOptions, ): Promise => { const { apiKey, apiUrl, workspace } = options; @@ -38,3 +39,6 @@ export const authLogin = async ( return { success: true, data: undefined }; }; + +export const authLogin = (options: AuthLoginOptions): Promise => + runSafe(() => innerAuthLogin(options), AUTH_ERROR_CODES.AUTH_FAILED); diff --git a/packages/twenty-sdk/src/cli/public-operations/auth-logout.ts b/packages/twenty-sdk/src/cli/public-operations/auth-logout.ts index 34ab4c833a..ad5ec01fdb 100644 --- a/packages/twenty-sdk/src/cli/public-operations/auth-logout.ts +++ b/packages/twenty-sdk/src/cli/public-operations/auth-logout.ts @@ -1,11 +1,12 @@ import { ConfigService } from '@/cli/utilities/config/config-service'; -import { type CommandResult } from './types'; +import { runSafe } from '@/cli/utilities/run-safe'; +import { AUTH_ERROR_CODES, type CommandResult } from './types'; export type AuthLogoutOptions = { workspace?: string; }; -export const authLogout = async ( +const innerAuthLogout = async ( options?: AuthLogoutOptions, ): Promise => { if (options?.workspace) { @@ -18,3 +19,8 @@ export const authLogout = async ( return { success: true, data: undefined }; }; + +export const authLogout = ( + options?: AuthLogoutOptions, +): Promise => + runSafe(() => innerAuthLogout(options), AUTH_ERROR_CODES.AUTH_FAILED); diff --git a/packages/twenty-sdk/src/cli/public-operations/function-execute.ts b/packages/twenty-sdk/src/cli/public-operations/function-execute.ts index ff01e5d13a..2464a253f6 100644 --- a/packages/twenty-sdk/src/cli/public-operations/function-execute.ts +++ b/packages/twenty-sdk/src/cli/public-operations/function-execute.ts @@ -2,6 +2,7 @@ import { ApiService } from '@/cli/utilities/api/api-service'; import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader'; import { ConfigService } from '@/cli/utilities/config/config-service'; import { type Manifest } from 'twenty-shared/application'; +import { runSafe } from '@/cli/utilities/run-safe'; import { APP_ERROR_CODES, FUNCTION_ERROR_CODES, @@ -45,7 +46,7 @@ const resolveIdentifier = (options: FunctionExecuteOptions): string => { return 'unknown'; }; -export const functionExecute = async ( +const innerFunctionExecute = async ( options: FunctionExecuteOptions, ): Promise> => { if (options.workspace) { @@ -60,7 +61,8 @@ export const functionExecute = async ( success: false, error: { code: APP_ERROR_CODES.MANIFEST_NOT_FOUND, - message: 'Failed to build manifest.', + message: + 'Manifest not found. Run `app:build` or `app:dev` to generate it first.', }, }; } @@ -153,3 +155,11 @@ export const functionExecute = async ( }, }; }; + +export const functionExecute = ( + options: FunctionExecuteOptions, +): Promise> => + runSafe( + () => innerFunctionExecute(options), + FUNCTION_ERROR_CODES.EXECUTION_FAILED, + ); diff --git a/packages/twenty-sdk/src/cli/public-operations/types.ts b/packages/twenty-sdk/src/cli/public-operations/types.ts index fd1e229ee2..3ed7dcafe4 100644 --- a/packages/twenty-sdk/src/cli/public-operations/types.ts +++ b/packages/twenty-sdk/src/cli/public-operations/types.ts @@ -19,6 +19,7 @@ export const APP_ERROR_CODES = { MANIFEST_BUILD_FAILED: 'MANIFEST_BUILD_FAILED', UNINSTALL_FAILED: 'UNINSTALL_FAILED', SYNC_FAILED: 'SYNC_FAILED', + TYPECHECK_FAILED: 'TYPECHECK_FAILED', } as const; export const FUNCTION_ERROR_CODES = { diff --git a/packages/twenty-sdk/src/cli/utilities/run-safe.ts b/packages/twenty-sdk/src/cli/utilities/run-safe.ts new file mode 100644 index 0000000000..3d1013ec23 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/run-safe.ts @@ -0,0 +1,18 @@ +import { type CommandResult } from '@/cli/public-operations/types'; + +export const runSafe = async ( + operation: () => Promise>, + fallbackErrorCode: string, +): Promise> => { + try { + return await operation(); + } catch (error) { + return { + success: false, + error: { + code: fallbackErrorCode, + message: error instanceof Error ? error.message : 'Unexpected error', + }, + }; + } +};