Fix app:dev CLI by removing deleted createOneApplication mutation (#18460)
## Summary - The `createOneApplication` GraphQL mutation was removed from the server during the application architecture refactor (#18432), but the SDK CLI (`app:dev`, `app:build --sync`) still called it, causing failures. - Simplified the SDK to use `syncApplication` (which now internally creates the `ApplicationEntity` via `ensureApplicationExists`) instead of a separate create step. - On first run (clean install), the orchestrator now runs an initial sync before initializing the file uploader, so file uploads can proceed (they require the `ApplicationEntity` to exist). ## Test plan - [x] Typecheck passes for both `twenty-sdk` and `twenty-server` - [x] `app:dev` tested locally with existing app (finds app, uploads, syncs) - [x] `app:dev` tested locally after `app:uninstall` (creates app via sync, uploads, syncs) - [x] SDK unit tests pass (23/26 files, 3 pre-existing failures unrelated) Made with [Cursor](https://cursor.com)
This commit is contained in:
+5
-5
@@ -1,7 +1,7 @@
|
||||
import { resolve } from 'path';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { appBuild } from '@/cli/public-operations/app-build';
|
||||
import { appGenerateClient } from '@/cli/public-operations/app-generate-client';
|
||||
import { appUninstall } from '@/cli/public-operations/app-uninstall';
|
||||
import { functionExecute } from '@/cli/public-operations/function-execute';
|
||||
import { ADD_NUMBERS_UNIVERSAL_IDENTIFIER } from '../src/logic-functions/add-numbers.function';
|
||||
@@ -10,15 +10,15 @@ const APP_PATH = resolve(__dirname, '../');
|
||||
|
||||
describe('functionExecute E2E', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({ appPath: APP_PATH });
|
||||
const generateResult = await appGenerateClient({ appPath: APP_PATH });
|
||||
|
||||
if (!buildResult.success) {
|
||||
if (!generateResult.success) {
|
||||
throw new Error(
|
||||
`appBuild failed: ${buildResult.error.code} – ${buildResult.error.message}`,
|
||||
`appGenerateClient failed: ${generateResult.error.code} – ${generateResult.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Although appBuild uploads files before syncing the manifest, the server
|
||||
// Although appGenerateClient uploads files before syncing the manifest, the server
|
||||
// may need a moment to make them readable by the execution engine.
|
||||
// Retry a dummy execution until the handler file becomes available.
|
||||
await vi.waitFor(
|
||||
|
||||
+5
-2
@@ -7,10 +7,13 @@ export const defineEntitiesTests = (appPath: string): void => {
|
||||
describe('logicFunctions', () => {
|
||||
it('should have built logicFunctions preserving source path structure', async () => {
|
||||
const files = await readdir(outputDir, { recursive: true });
|
||||
const sortedFiles = files.map((f) => f.toString()).sort();
|
||||
// api-client is generated post-sync and depends on server schema availability
|
||||
const sortedFiles = files
|
||||
.map((f) => f.toString())
|
||||
.filter((f) => !f.startsWith('api-client'))
|
||||
.sort();
|
||||
|
||||
expect(sortedFiles).toEqual([
|
||||
'api-client',
|
||||
'manifest.json',
|
||||
'package.json',
|
||||
'public',
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export const runAppDevInProcess = async (options: {
|
||||
|
||||
const command = new AppDevCommand();
|
||||
|
||||
await command.execute({ appPath });
|
||||
await command.execute({ appPath, headless: true });
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ import { vi } from 'vitest';
|
||||
|
||||
const mockApiService = {
|
||||
validateAuth: vi.fn().mockResolvedValue({ authValid: true, serverUp: true }),
|
||||
findOneApplication: vi.fn().mockResolvedValue({ success: true, data: null }),
|
||||
createApplication: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ success: true, data: { id: 'mock-id' } }),
|
||||
generateApplicationToken: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -39,6 +35,10 @@ const mockApiService = {
|
||||
clientSecret: 'mock-client-secret',
|
||||
},
|
||||
}),
|
||||
createDevelopmentApplication: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: { id: 'mock-app-id', universalIdentifier: 'mock-uid' },
|
||||
}),
|
||||
syncApplication: vi.fn().mockResolvedValue({ success: true, data: true }),
|
||||
uploadFile: vi.fn().mockResolvedValue({ success: true, data: true }),
|
||||
};
|
||||
@@ -46,14 +46,13 @@ const mockApiService = {
|
||||
vi.mock('@/cli/utilities/api/api-service', () => ({
|
||||
ApiService: class {
|
||||
validateAuth = mockApiService.validateAuth;
|
||||
findOneApplication = mockApiService.findOneApplication;
|
||||
createApplication = mockApiService.createApplication;
|
||||
generateApplicationToken = mockApiService.generateApplicationToken;
|
||||
renewApplicationToken = mockApiService.renewApplicationToken;
|
||||
findApplicationRegistrationByUniversalIdentifier =
|
||||
mockApiService.findApplicationRegistrationByUniversalIdentifier;
|
||||
createApplicationRegistration =
|
||||
mockApiService.createApplicationRegistration;
|
||||
createDevelopmentApplication = mockApiService.createDevelopmentApplication;
|
||||
syncApplication = mockApiService.syncApplication;
|
||||
uploadFile = mockApiService.uploadFile;
|
||||
},
|
||||
|
||||
@@ -2,9 +2,9 @@ import { formatPath } from '@/cli/utilities/file/file-path';
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import { AppBuildCommand } from './app/app-build';
|
||||
import { AppGenerateClientCommand } from './app/app-generate-client';
|
||||
import { AppDevCommand } from './app/app-dev';
|
||||
import { AppPackCommand } from './app/app-pack';
|
||||
import { AppPushCommand } from './app/app-push';
|
||||
import { AppPublishCommand } from './app/app-publish';
|
||||
import { AppTypecheckCommand } from './app/app-typecheck';
|
||||
import { AppUninstallCommand } from './app/app-uninstall';
|
||||
import { AuthListCommand } from './auth/auth-list';
|
||||
@@ -64,24 +64,15 @@ export const registerCommands = (program: Command): void => {
|
||||
|
||||
// App commands
|
||||
const buildCommand = new AppBuildCommand();
|
||||
const generateClientCommand = new AppGenerateClientCommand();
|
||||
const devCommand = new AppDevCommand();
|
||||
const packCommand = new AppPackCommand();
|
||||
const pushCommand = new AppPushCommand();
|
||||
const publishCommand = new AppPublishCommand();
|
||||
const typecheckCommand = new AppTypecheckCommand();
|
||||
const uninstallCommand = new AppUninstallCommand();
|
||||
const addCommand = new EntityAddCommand();
|
||||
const logsCommand = new LogicFunctionLogsCommand();
|
||||
const executeCommand = new LogicFunctionExecuteCommand();
|
||||
|
||||
program
|
||||
.command('app:build [appPath]')
|
||||
.description('Build the application without watching for changes')
|
||||
.action(async (appPath) => {
|
||||
await buildCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:dev [appPath]')
|
||||
.description('Watch and sync local application changes')
|
||||
@@ -91,6 +82,47 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:generate-client [appPath]')
|
||||
.description(
|
||||
'Build, sync to local server, and generate the typed API client',
|
||||
)
|
||||
.action(async (appPath) => {
|
||||
await generateClientCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:build [appPath]')
|
||||
.description(
|
||||
'Build the application into .twenty/output/ (no server needed)',
|
||||
)
|
||||
.option('--tarball', 'Also pack into a .tgz tarball')
|
||||
.action(async (appPath, options) => {
|
||||
await buildCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
tarball: options.tarball,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:publish [appPath]')
|
||||
.description(
|
||||
'Build and publish to npm, or to a Twenty server with --server',
|
||||
)
|
||||
.option('--server <url>', 'Publish to a Twenty server instead of npm')
|
||||
.option('--token <token>', 'Auth token for the server')
|
||||
.option('--tag <tag>', 'npm dist-tag (e.g. beta, next)')
|
||||
.action(async (appPath, options) => {
|
||||
await publishCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
server: options.server,
|
||||
token: options.token,
|
||||
tag: options.tag,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:typecheck [appPath]')
|
||||
.description('Run TypeScript type checking on the application')
|
||||
@@ -116,28 +148,6 @@ export const registerCommands = (program: Command): void => {
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:pack [appPath]')
|
||||
.description('Build and pack the application into a .tgz tarball')
|
||||
.action(async (appPath) => {
|
||||
await packCommand.execute({ appPath: formatPath(appPath) });
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:push [appPath]')
|
||||
.description(
|
||||
'Build, upload, and install a local application on a Twenty server (for air-gapped/dev deployments)',
|
||||
)
|
||||
.option('--server <url>', 'Twenty server URL')
|
||||
.option('--token <token>', 'Auth token for the server')
|
||||
.action(async (appPath, options) => {
|
||||
await pushCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
server: options.server,
|
||||
token: options.token,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('entity:add [entityType]')
|
||||
.option('--path <path>', 'Path in which the entity should be created.')
|
||||
|
||||
@@ -4,18 +4,20 @@ import chalk from 'chalk';
|
||||
|
||||
export type AppBuildCommandOptions = {
|
||||
appPath?: string;
|
||||
tarball?: boolean;
|
||||
};
|
||||
|
||||
export class AppBuildCommand {
|
||||
async execute(options: AppBuildCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('Building and syncing application...'));
|
||||
console.log(chalk.blue('Building application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appBuild({
|
||||
appPath,
|
||||
tarball: options.tarball,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
@@ -26,8 +28,13 @@ export class AppBuildCommand {
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Build and sync succeeded (${result.data.fileCount} file${result.data.fileCount === 1 ? '' : 's'})`,
|
||||
`✓ Build succeeded (${result.data.fileCount} file${result.data.fileCount === 1 ? '' : 's'})`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(`Output: ${result.data.outputDir}`));
|
||||
|
||||
if (result.data.tarballPath) {
|
||||
console.log(chalk.gray(`Tarball: ${result.data.tarballPath}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orc
|
||||
|
||||
export type AppDevOptions = {
|
||||
appPath?: string;
|
||||
headless?: boolean;
|
||||
};
|
||||
|
||||
export class AppDevCommand {
|
||||
@@ -29,20 +30,25 @@ export class AppDevCommand {
|
||||
frontendUrl: process.env.FRONTEND_URL,
|
||||
});
|
||||
|
||||
const uiStateManager = new DevUiStateManager(orchestratorState);
|
||||
if (!options.headless) {
|
||||
const uiStateManager = new DevUiStateManager(orchestratorState);
|
||||
|
||||
orchestratorState.onChange = () => uiStateManager.notify();
|
||||
orchestratorState.onChange = () => uiStateManager.notify();
|
||||
|
||||
const { unmount } = await renderDevUI(uiStateManager);
|
||||
const { unmount } = await renderDevUI(uiStateManager);
|
||||
|
||||
this.unmountUI = unmount;
|
||||
this.unmountUI = unmount;
|
||||
}
|
||||
|
||||
this.orchestrator = new DevModeOrchestrator({
|
||||
state: orchestratorState,
|
||||
});
|
||||
|
||||
await this.orchestrator.start();
|
||||
this.setupGracefulShutdown();
|
||||
|
||||
if (!options.headless) {
|
||||
this.setupGracefulShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private setupGracefulShutdown(): void {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { appGenerateClient } from '@/cli/public-operations/app-generate-client';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppGenerateClientCommandOptions = {
|
||||
appPath?: string;
|
||||
};
|
||||
|
||||
export class AppGenerateClientCommand {
|
||||
async execute(options: AppGenerateClientCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('Generating API client...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appGenerateClient({
|
||||
appPath,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(result.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Client generated (${result.data.fileCount} file${result.data.fileCount === 1 ? '' : 's'})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { appPack } from '@/cli/public-operations/app-pack';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppPackCommandOptions = {
|
||||
appPath?: string;
|
||||
};
|
||||
|
||||
export class AppPackCommand {
|
||||
async execute(options: AppPackCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('Building and packing application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appPack({
|
||||
appPath,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(result.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Application packed successfully'));
|
||||
console.log(chalk.gray(`Tarball: ${result.data.tarballPath}`));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { appPublish } from '@/cli/public-operations/app-publish';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppPublishCommandOptions = {
|
||||
appPath?: string;
|
||||
server?: string;
|
||||
token?: string;
|
||||
tag?: string;
|
||||
};
|
||||
|
||||
export class AppPublishCommand {
|
||||
async execute(options: AppPublishCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
const isServerPublish = !!options.server;
|
||||
|
||||
console.log(
|
||||
chalk.blue(
|
||||
isServerPublish
|
||||
? `Publishing to server ${options.server}...`
|
||||
: 'Publishing to npm...',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appPublish({
|
||||
appPath,
|
||||
server: options.server,
|
||||
token: options.token,
|
||||
npmTag: options.tag,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(result.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.data.target === 'npm') {
|
||||
console.log(chalk.green('✓ Published to npm successfully'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green('✓ Published to server and installed successfully'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { appPack } from '@/cli/public-operations/app-pack';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
|
||||
export type AppPushCommandOptions = {
|
||||
appPath?: string;
|
||||
server?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export class AppPushCommand {
|
||||
async execute(options: AppPushCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('Building, packing, and pushing application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const packResult = await appPack({
|
||||
appPath,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (!packResult.success) {
|
||||
console.error(chalk.red(packResult.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { tarballPath } = packResult.data;
|
||||
|
||||
console.log(chalk.gray(`Uploading ${tarballPath}...`));
|
||||
|
||||
const tarballBuffer = fs.readFileSync(tarballPath);
|
||||
|
||||
const apiService = new ApiService({
|
||||
serverUrl: options.server,
|
||||
token: options.token,
|
||||
});
|
||||
|
||||
const uploadResult = await apiService.uploadAppTarball({ tarballBuffer });
|
||||
|
||||
if (!uploadResult.success) {
|
||||
console.error(chalk.red(`Upload failed: ${uploadResult.error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Installing application...'));
|
||||
|
||||
const installResult = await apiService.installTarballApp({
|
||||
universalIdentifier: uploadResult.data.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!installResult.success) {
|
||||
console.error(chalk.red(`Install failed: ${installResult.error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Application pushed and installed successfully'));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
import { buildApplication } from '@/cli/utilities/build/common/build-application';
|
||||
import { synchronizeBuiltApplication } from '@/cli/utilities/build/common/synchronize-built-application';
|
||||
import { runTypecheck } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest';
|
||||
import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums';
|
||||
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
|
||||
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 = {
|
||||
appPath: string;
|
||||
tarball?: boolean;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppBuildResult = {
|
||||
outputDir: string;
|
||||
fileCount: number;
|
||||
tarballPath?: string;
|
||||
};
|
||||
|
||||
const innerAppBuild = async (
|
||||
@@ -39,33 +46,27 @@ const innerAppBuild = async (
|
||||
for (const warning of manifestResult.warnings) {
|
||||
onProgress?.(`⚠ ${warning}`);
|
||||
}
|
||||
|
||||
const clientService = new ClientService();
|
||||
|
||||
await clientService.ensureGeneratedClientStub({ appPath });
|
||||
|
||||
onProgress?.('Building application files...');
|
||||
|
||||
const firstBuildResult = await buildApplication({
|
||||
const buildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing application schema...');
|
||||
onProgress?.('Updating manifest checksums...');
|
||||
|
||||
const firstSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
const updatedManifest = manifestUpdateChecksums({
|
||||
manifest,
|
||||
builtFileInfos: firstBuildResult.builtFileInfos,
|
||||
builtFileInfos: buildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!firstSyncResult.success) {
|
||||
return firstSyncResult;
|
||||
}
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
await clientService.generate({ appPath });
|
||||
await writeManifestToOutput(appPath, updatedManifest);
|
||||
|
||||
onProgress?.('Running typecheck...');
|
||||
|
||||
@@ -86,35 +87,30 @@ const innerAppBuild = async (
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Rebuilding with generated client...');
|
||||
const outputDir = path.join(appPath, '.twenty', 'output');
|
||||
|
||||
const finalBuildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
const result: AppBuildResult = {
|
||||
outputDir,
|
||||
fileCount: buildResult.builtFileInfos.size,
|
||||
};
|
||||
|
||||
onProgress?.('Syncing built files...');
|
||||
if (options.tarball) {
|
||||
onProgress?.('Packing tarball...');
|
||||
|
||||
const finalSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: finalBuildResult.builtFileInfos,
|
||||
});
|
||||
const packOutput = execSync('npm pack --pack-destination .', {
|
||||
cwd: outputDir,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
|
||||
if (!finalSyncResult.success) {
|
||||
return finalSyncResult;
|
||||
const tarballName = packOutput.split('\n').pop()!;
|
||||
|
||||
result.tarballPath = path.join(outputDir, tarballName);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
fileCount: finalBuildResult.builtFileInfos.size,
|
||||
},
|
||||
};
|
||||
return { success: true, data: result };
|
||||
};
|
||||
|
||||
export const appBuild = (
|
||||
options: AppBuildOptions,
|
||||
): Promise<CommandResult<AppBuildResult>> =>
|
||||
runSafe(() => innerAppBuild(options), APP_ERROR_CODES.SYNC_FAILED);
|
||||
runSafe(() => innerAppBuild(options), APP_ERROR_CODES.BUILD_FAILED);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { buildApplication } from '@/cli/utilities/build/common/build-application';
|
||||
import { synchronizeBuiltApplication } from '@/cli/utilities/build/common/synchronize-built-application';
|
||||
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 AppGenerateClientOptions = {
|
||||
appPath: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppGenerateClientResult = {
|
||||
fileCount: number;
|
||||
};
|
||||
|
||||
const innerAppGenerateClient = async (
|
||||
options: AppGenerateClientOptions,
|
||||
): Promise<CommandResult<AppGenerateClientResult>> => {
|
||||
const { appPath, onProgress } = options;
|
||||
|
||||
onProgress?.('Building manifest...');
|
||||
|
||||
const manifestResult = await buildAndValidateManifest(appPath);
|
||||
|
||||
if (!manifestResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_BUILD_FAILED,
|
||||
message: manifestResult.errors.join('\n'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { manifest, filePaths } = manifestResult;
|
||||
|
||||
for (const warning of manifestResult.warnings) {
|
||||
onProgress?.(`⚠ ${warning}`);
|
||||
}
|
||||
const clientService = new ClientService();
|
||||
|
||||
await clientService.ensureGeneratedClientStub({ appPath });
|
||||
|
||||
onProgress?.('Building application files...');
|
||||
|
||||
const buildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing application schema...');
|
||||
|
||||
const syncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: buildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!syncResult.success) {
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
await clientService.generate({ appPath });
|
||||
|
||||
onProgress?.('Running typecheck...');
|
||||
|
||||
const typecheckErrors = await runTypecheck(appPath);
|
||||
|
||||
if (typecheckErrors.length > 0) {
|
||||
const errorMessages = typecheckErrors.map(
|
||||
(error) =>
|
||||
`${error.file}(${error.line},${error.column + 1}): ${error.text}`,
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.TYPECHECK_FAILED,
|
||||
message: `Typecheck failed:\n${errorMessages.join('\n')}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
fileCount: buildResult.builtFileInfos.size,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const appGenerateClient = (
|
||||
options: AppGenerateClientOptions,
|
||||
): Promise<CommandResult<AppGenerateClientResult>> =>
|
||||
runSafe(() => innerAppGenerateClient(options), APP_ERROR_CODES.SYNC_FAILED);
|
||||
@@ -1,39 +0,0 @@
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
import { appBuild, type AppBuildOptions } from './app-build';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { APP_ERROR_CODES, type CommandResult } from './types';
|
||||
|
||||
export type AppPackResult = {
|
||||
tarballPath: string;
|
||||
};
|
||||
|
||||
const innerAppPack = async (
|
||||
options: AppBuildOptions,
|
||||
): Promise<CommandResult<AppPackResult>> => {
|
||||
const buildResult = await appBuild(options);
|
||||
|
||||
if (!buildResult.success) {
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
options.onProgress?.('Packing tarball...');
|
||||
|
||||
const outputDir = path.join(options.appPath, '.twenty', 'output');
|
||||
|
||||
const packOutput = execSync('npm pack --pack-destination .', {
|
||||
cwd: outputDir,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
|
||||
const tarballName = packOutput.split('\n').pop()!;
|
||||
const tarballPath = path.join(outputDir, tarballName);
|
||||
|
||||
return { success: true, data: { tarballPath } };
|
||||
};
|
||||
|
||||
export const appPack = (
|
||||
options: AppBuildOptions,
|
||||
): Promise<CommandResult<AppPackResult>> =>
|
||||
runSafe(() => innerAppPack(options), APP_ERROR_CODES.SYNC_FAILED);
|
||||
@@ -0,0 +1,146 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { appBuild } from './app-build';
|
||||
import { APP_ERROR_CODES, type CommandResult } from './types';
|
||||
|
||||
export type AppPublishOptions = {
|
||||
appPath: string;
|
||||
server?: string;
|
||||
token?: string;
|
||||
npmTag?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppPublishResult = {
|
||||
target: 'npm' | 'server';
|
||||
universalIdentifier?: string;
|
||||
};
|
||||
|
||||
const innerAppPublish = async (
|
||||
options: AppPublishOptions,
|
||||
): Promise<CommandResult<AppPublishResult>> => {
|
||||
const { appPath, onProgress } = options;
|
||||
const isServerPublish = !!options.server;
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath,
|
||||
tarball: isServerPublish,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
if (isServerPublish) {
|
||||
return publishToServer({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
server: options.server!,
|
||||
token: options.token,
|
||||
onProgress,
|
||||
});
|
||||
}
|
||||
|
||||
return publishToNpm({
|
||||
outputDir: buildResult.data.outputDir,
|
||||
npmTag: options.npmTag,
|
||||
onProgress,
|
||||
});
|
||||
};
|
||||
|
||||
const publishToNpm = async ({
|
||||
outputDir,
|
||||
npmTag,
|
||||
onProgress,
|
||||
}: {
|
||||
outputDir: string;
|
||||
npmTag?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
}): Promise<CommandResult<AppPublishResult>> => {
|
||||
onProgress?.('Publishing to npm...');
|
||||
|
||||
const tagArg = npmTag ? ` --tag ${npmTag}` : '';
|
||||
|
||||
try {
|
||||
execSync(`npm publish${tagArg}`, {
|
||||
cwd: outputDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.PUBLISH_FAILED,
|
||||
message:
|
||||
'npm publish failed. Make sure you are logged in (`npm login`) and the package name is available.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: { target: 'npm' } };
|
||||
};
|
||||
|
||||
const publishToServer = async ({
|
||||
tarballPath,
|
||||
server,
|
||||
token,
|
||||
onProgress,
|
||||
}: {
|
||||
tarballPath: string;
|
||||
server: string;
|
||||
token?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
}): Promise<CommandResult<AppPublishResult>> => {
|
||||
onProgress?.(`Uploading ${tarballPath}...`);
|
||||
|
||||
const tarballBuffer = fs.readFileSync(tarballPath);
|
||||
|
||||
const apiService = new ApiService({
|
||||
serverUrl: server,
|
||||
token,
|
||||
});
|
||||
|
||||
const uploadResult = await apiService.uploadAppTarball({ tarballBuffer });
|
||||
|
||||
if (!uploadResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.PUBLISH_FAILED,
|
||||
message: `Upload failed: ${uploadResult.error}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Installing application...');
|
||||
|
||||
const installResult = await apiService.installTarballApp({
|
||||
universalIdentifier: uploadResult.data.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!installResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.PUBLISH_FAILED,
|
||||
message: `Install failed: ${installResult.error}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
target: 'server',
|
||||
universalIdentifier: uploadResult.data.universalIdentifier,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const appPublish = (
|
||||
options: AppPublishOptions,
|
||||
): Promise<CommandResult<AppPublishResult>> =>
|
||||
runSafe(() => innerAppPublish(options), APP_ERROR_CODES.PUBLISH_FAILED);
|
||||
@@ -25,7 +25,7 @@ const innerAppUninstall = async (
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message:
|
||||
'Manifest not found. Run `app:build` or `app:dev` to generate it first.',
|
||||
'Manifest not found. Run `app:build`, `app:generate-client`, or `app:dev` first.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const innerFunctionExecute = async (
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message:
|
||||
'Manifest not found. Run `app:build` or `app:dev` to generate it first.',
|
||||
'Manifest not found. Run `app:build`, `app:generate-client`, or `app:dev` first.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,8 +7,13 @@ export type { AuthLogoutOptions } from './auth-logout';
|
||||
// App
|
||||
export { appBuild } from './app-build';
|
||||
export type { AppBuildOptions, AppBuildResult } from './app-build';
|
||||
export { appPack } from './app-pack';
|
||||
export type { AppPackResult } from './app-pack';
|
||||
export { appGenerateClient } from './app-generate-client';
|
||||
export type {
|
||||
AppGenerateClientOptions,
|
||||
AppGenerateClientResult,
|
||||
} from './app-generate-client';
|
||||
export { appPublish } from './app-publish';
|
||||
export type { AppPublishOptions, AppPublishResult } from './app-publish';
|
||||
export { appUninstall } from './app-uninstall';
|
||||
export type { AppUninstallOptions } from './app-uninstall';
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ export const AUTH_ERROR_CODES = {
|
||||
export const APP_ERROR_CODES = {
|
||||
MANIFEST_NOT_FOUND: 'MANIFEST_NOT_FOUND',
|
||||
MANIFEST_BUILD_FAILED: 'MANIFEST_BUILD_FAILED',
|
||||
BUILD_FAILED: 'BUILD_FAILED',
|
||||
PUBLISH_FAILED: 'PUBLISH_FAILED',
|
||||
UNINSTALL_FAILED: 'UNINSTALL_FAILED',
|
||||
SYNC_FAILED: 'SYNC_FAILED',
|
||||
TYPECHECK_FAILED: 'TYPECHECK_FAILED',
|
||||
|
||||
@@ -101,61 +101,6 @@ export class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async findOneApplication(
|
||||
universalIdentifier: string,
|
||||
): Promise<ApiResponse<{ id: string; universalIdentifier: string } | null>> {
|
||||
try {
|
||||
const query = `
|
||||
query FindOneApplication($universalIdentifier: UUID!) {
|
||||
findOneApplication(universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
universalIdentifier
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await this.client.post(
|
||||
'/metadata',
|
||||
{
|
||||
query,
|
||||
variables: { universalIdentifier },
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*/*',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.data.errors) {
|
||||
const isNotFound = response.data.errors.some(
|
||||
(error: { extensions?: { code?: string } }) =>
|
||||
error.extensions?.code === 'NOT_FOUND',
|
||||
);
|
||||
|
||||
if (isNotFound) {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.data.errors[0],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data.findOneApplication,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async generateApplicationToken(applicationId: string): Promise<
|
||||
ApiResponse<{
|
||||
applicationAccessToken: { token: string; expiresAt: string };
|
||||
@@ -383,40 +328,25 @@ export class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async createApplication(
|
||||
manifest: Manifest,
|
||||
options?: { applicationRegistrationId?: string },
|
||||
): Promise<ApiResponse<{ id: string; universalIdentifier: string }>> {
|
||||
async createDevelopmentApplication(input: {
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
}): Promise<ApiResponse<{ id: string; universalIdentifier: string }>> {
|
||||
try {
|
||||
const mutation = `
|
||||
mutation CreateOneApplication($input: CreateApplicationInput!) {
|
||||
createOneApplication(input: $input) {
|
||||
mutation CreateDevelopmentApplication($universalIdentifier: String!, $name: String!) {
|
||||
createDevelopmentApplication(universalIdentifier: $universalIdentifier, name: $name) {
|
||||
id
|
||||
universalIdentifier
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const input: Record<string, string> = {
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name: manifest.application.displayName,
|
||||
version: '0.0.1',
|
||||
sourcePath: 'cli-sync',
|
||||
};
|
||||
|
||||
if (options?.applicationRegistrationId) {
|
||||
input.applicationRegistrationId = options.applicationRegistrationId;
|
||||
}
|
||||
|
||||
const variables = {
|
||||
input,
|
||||
};
|
||||
|
||||
const response: AxiosResponse = await this.client.post(
|
||||
const response = await this.client.post(
|
||||
'/metadata',
|
||||
{
|
||||
query: mutation,
|
||||
variables,
|
||||
variables: input,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -435,8 +365,7 @@ export class ApiService {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data.createOneApplication,
|
||||
message: `Successfully create application: ${manifest.application.displayName}`,
|
||||
data: response.data.data.createDevelopmentApplication,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { type ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export type EnsureApplicationSuccess = {
|
||||
success: true;
|
||||
applicationId: string;
|
||||
universalIdentifier: string;
|
||||
created: boolean;
|
||||
};
|
||||
|
||||
export type EnsureApplicationFailure = {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type EnsureApplicationResult =
|
||||
| EnsureApplicationSuccess
|
||||
| EnsureApplicationFailure;
|
||||
|
||||
export const findOrCreateApplication = async ({
|
||||
apiService,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
}: {
|
||||
apiService: ApiService;
|
||||
manifest: Manifest;
|
||||
applicationRegistrationId?: string;
|
||||
}): Promise<EnsureApplicationResult> => {
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
|
||||
const findResult = await apiService.findOneApplication(universalIdentifier);
|
||||
|
||||
if (!findResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to resolve application: ${serializeError(findResult.error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (findResult.data) {
|
||||
return {
|
||||
success: true,
|
||||
applicationId: findResult.data.id,
|
||||
universalIdentifier: findResult.data.universalIdentifier,
|
||||
created: false,
|
||||
};
|
||||
}
|
||||
|
||||
let registrationId = applicationRegistrationId;
|
||||
|
||||
if (!registrationId) {
|
||||
const registerResult =
|
||||
await apiService.findApplicationRegistrationByUniversalIdentifier(
|
||||
universalIdentifier,
|
||||
);
|
||||
|
||||
if (registerResult.success && registerResult.data) {
|
||||
registrationId = registerResult.data.id;
|
||||
}
|
||||
}
|
||||
|
||||
const createResult = await apiService.createApplication(manifest, {
|
||||
applicationRegistrationId: registrationId,
|
||||
});
|
||||
|
||||
if (!createResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to create application: ${serializeError(createResult.error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
applicationId: createResult.data!.id,
|
||||
universalIdentifier: createResult.data!.universalIdentifier,
|
||||
created: true,
|
||||
};
|
||||
};
|
||||
+76
-18
@@ -3,7 +3,6 @@ import {
|
||||
type CommandResult,
|
||||
} from '@/cli/public-operations/types';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { findOrCreateApplication } from '@/cli/utilities/application/find-or-create-application';
|
||||
import { type BuiltFileInfo } from '@/cli/utilities/build/common/build-application';
|
||||
import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums';
|
||||
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
|
||||
@@ -16,6 +15,62 @@ export type AppSyncOptions = {
|
||||
workspace?: string;
|
||||
};
|
||||
|
||||
const ensureApplicationRegistrationExists = async (
|
||||
apiService: ApiService,
|
||||
manifest: Manifest,
|
||||
): Promise<CommandResult> => {
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
|
||||
const findResult =
|
||||
await apiService.findApplicationRegistrationByUniversalIdentifier(
|
||||
universalIdentifier,
|
||||
);
|
||||
|
||||
if (findResult.success && findResult.data) {
|
||||
return { success: true, data: undefined };
|
||||
}
|
||||
|
||||
const createResult = await apiService.createApplicationRegistration({
|
||||
name: manifest.application.displayName,
|
||||
description: manifest.application.description,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
if (!createResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: `Failed to create application registration: ${serializeError(createResult.error)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
const ensureDevelopmentApplicationExists = async (
|
||||
apiService: ApiService,
|
||||
manifest: Manifest,
|
||||
): Promise<CommandResult> => {
|
||||
const result = await apiService.createDevelopmentApplication({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name: manifest.application.displayName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: `Failed to create development application: ${serializeError(result.error)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
export const synchronizeBuiltApplication = async ({
|
||||
appPath,
|
||||
manifest,
|
||||
@@ -26,37 +81,40 @@ export const synchronizeBuiltApplication = async ({
|
||||
builtFileInfos: Map<string, BuiltFileInfo>;
|
||||
}): Promise<CommandResult> => {
|
||||
const apiService = new ApiService();
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
|
||||
const ensureResult = await findOrCreateApplication({
|
||||
const registrationResult = await ensureApplicationRegistrationExists(
|
||||
apiService,
|
||||
manifest,
|
||||
});
|
||||
);
|
||||
|
||||
if (!ensureResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: ensureResult.error,
|
||||
},
|
||||
};
|
||||
if (!registrationResult.success) {
|
||||
return registrationResult;
|
||||
}
|
||||
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
const applicationResult = await ensureDevelopmentApplicationExists(
|
||||
apiService,
|
||||
manifest,
|
||||
);
|
||||
|
||||
if (!applicationResult.success) {
|
||||
return applicationResult;
|
||||
}
|
||||
|
||||
const fileUploader = new FileUploader({
|
||||
applicationUniversalIdentifier: universalIdentifier,
|
||||
appPath,
|
||||
});
|
||||
|
||||
const uploadPromises = Array.from(builtFileInfos.values()).map((fileInfo) =>
|
||||
fileUploader.uploadFile({
|
||||
builtPath: fileInfo.builtPath,
|
||||
fileFolder: fileInfo.fileFolder,
|
||||
}),
|
||||
const uploadResults = await Promise.all(
|
||||
[...builtFileInfos.values()].map((fileInfo) =>
|
||||
fileUploader.uploadFile({
|
||||
builtPath: fileInfo.builtPath,
|
||||
fileFolder: fileInfo.fileFolder,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const uploadResults = await Promise.all(uploadPromises);
|
||||
const failedUploads = uploadResults.filter((result) => !result.success);
|
||||
|
||||
if (failedUploads.length > 0) {
|
||||
|
||||
+4
-2
@@ -1,7 +1,6 @@
|
||||
import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
|
||||
import { type BuildManifestOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step';
|
||||
import { type CheckServerOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step';
|
||||
import { type ResolveApplicationOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step';
|
||||
import { type StartWatchersOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
|
||||
import { type SyncApplicationOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
|
||||
import { type UploadFilesOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
|
||||
@@ -96,7 +95,10 @@ export class OrchestratorState {
|
||||
steps: {
|
||||
checkServer: OrchestratorStepState<CheckServerOrchestratorStepOutput>;
|
||||
ensureValidTokens: OrchestratorStepState<Record<string, never>>;
|
||||
resolveApplication: OrchestratorStepState<ResolveApplicationOrchestratorStepOutput>;
|
||||
resolveApplication: OrchestratorStepState<{
|
||||
applicationId: string | null;
|
||||
universalIdentifier: string | null;
|
||||
}>;
|
||||
buildManifest: OrchestratorStepState<BuildManifestOrchestratorStepOutput>;
|
||||
uploadFiles: OrchestratorStepState<UploadFilesOrchestratorStepOutput>;
|
||||
generateApiClient: OrchestratorStepState<Record<string, never>>;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/st
|
||||
import { EnsureValidTokensOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step';
|
||||
import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step';
|
||||
import { RegisterAppOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step';
|
||||
import { ResolveApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step';
|
||||
import {
|
||||
StartWatchersOrchestratorStep,
|
||||
type FileBuiltEvent,
|
||||
@@ -30,13 +29,13 @@ export class DevModeOrchestrator {
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
private serverCheckInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private apiService: ApiService;
|
||||
private clientService: ClientService;
|
||||
private skipTypecheck = true;
|
||||
private checkServerStep: CheckServerOrchestratorStep;
|
||||
private ensureValidTokensStep: EnsureValidTokensOrchestratorStep;
|
||||
private buildManifestStep: BuildManifestOrchestratorStep;
|
||||
private registerAppStep: RegisterAppOrchestratorStep;
|
||||
private resolveApplicationStep: ResolveApplicationOrchestratorStep;
|
||||
private uploadFilesStep: UploadFilesOrchestratorStep;
|
||||
private generateApiClientStep: GenerateApiClientOrchestratorStep;
|
||||
private syncApplicationStep: SyncApplicationOrchestratorStep;
|
||||
@@ -46,7 +45,8 @@ export class DevModeOrchestrator {
|
||||
this.debounceMs = options.debounceMs ?? 200;
|
||||
this.state = options.state;
|
||||
|
||||
const apiService = new ApiService({ disableInterceptors: true });
|
||||
this.apiService = new ApiService({ disableInterceptors: true });
|
||||
const apiService = this.apiService;
|
||||
const configService = new ConfigService();
|
||||
this.clientService = new ClientService();
|
||||
const stepDeps = { state: this.state, notify: () => this.state.notify() };
|
||||
@@ -66,10 +66,6 @@ export class DevModeOrchestrator {
|
||||
apiService,
|
||||
configService,
|
||||
});
|
||||
this.resolveApplicationStep = new ResolveApplicationOrchestratorStep({
|
||||
...stepDeps,
|
||||
apiService,
|
||||
});
|
||||
this.uploadFilesStep = new UploadFilesOrchestratorStep(stepDeps);
|
||||
this.generateApiClientStep = new GenerateApiClientOrchestratorStep({
|
||||
...stepDeps,
|
||||
@@ -223,20 +219,37 @@ export class DevModeOrchestrator {
|
||||
}
|
||||
|
||||
private async initializePipeline(manifest: Manifest): Promise<boolean> {
|
||||
const registerResult = await this.registerAppStep.execute({ manifest });
|
||||
await this.registerAppStep.execute({ manifest });
|
||||
|
||||
const resolveResult = await this.resolveApplicationStep.execute({
|
||||
manifest,
|
||||
applicationRegistrationId:
|
||||
registerResult.applicationRegistrationId ?? undefined,
|
||||
const createResult = await this.apiService.createDevelopmentApplication({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name: manifest.application.displayName,
|
||||
});
|
||||
|
||||
if (!resolveResult.applicationId) {
|
||||
if (!createResult.success || !createResult.data) {
|
||||
this.state.applyStepEvents([
|
||||
{
|
||||
message: 'Failed to create development application',
|
||||
status: 'error',
|
||||
},
|
||||
]);
|
||||
this.state.updatePipeline({ status: 'error' });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.state.steps.resolveApplication.output = {
|
||||
applicationId: createResult.data.id,
|
||||
universalIdentifier: createResult.data.universalIdentifier,
|
||||
};
|
||||
this.state.steps.resolveApplication.status = 'done';
|
||||
|
||||
this.state.applyStepEvents([
|
||||
{ message: 'Application created', status: 'success' },
|
||||
]);
|
||||
|
||||
await this.ensureValidTokensStep.exchangeTokens({
|
||||
applicationId: resolveResult.applicationId,
|
||||
applicationId: createResult.data.id,
|
||||
});
|
||||
|
||||
this.uploadFilesStep.initialize({
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import { type ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { findOrCreateApplication } from '@/cli/utilities/application/find-or-create-application';
|
||||
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export type ResolveApplicationOrchestratorStepOutput = {
|
||||
applicationId: string | null;
|
||||
universalIdentifier: string | null;
|
||||
};
|
||||
|
||||
export class ResolveApplicationOrchestratorStep {
|
||||
private apiService: ApiService;
|
||||
private state: OrchestratorState;
|
||||
private notify: () => void;
|
||||
|
||||
constructor({
|
||||
apiService,
|
||||
state,
|
||||
notify,
|
||||
}: {
|
||||
apiService: ApiService;
|
||||
state: OrchestratorState;
|
||||
notify: () => void;
|
||||
}) {
|
||||
this.apiService = apiService;
|
||||
this.state = state;
|
||||
this.notify = notify;
|
||||
}
|
||||
|
||||
async execute(input: {
|
||||
manifest: Manifest;
|
||||
applicationRegistrationId?: string;
|
||||
}): Promise<ResolveApplicationOrchestratorStepOutput> {
|
||||
const step = this.state.steps.resolveApplication;
|
||||
|
||||
step.status = 'in_progress';
|
||||
this.notify();
|
||||
|
||||
const result = await findOrCreateApplication({
|
||||
apiService: this.apiService,
|
||||
manifest: input.manifest,
|
||||
applicationRegistrationId: input.applicationRegistrationId,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
this.state.applyStepEvents([
|
||||
{
|
||||
message: result.error,
|
||||
status: 'error',
|
||||
},
|
||||
]);
|
||||
step.status = 'error';
|
||||
this.state.updatePipeline({ status: 'error' });
|
||||
|
||||
return step.output;
|
||||
}
|
||||
|
||||
if (result.created) {
|
||||
this.state.applyStepEvents([
|
||||
{ message: 'Creating application', status: 'info' },
|
||||
{ message: 'Application created', status: 'success' },
|
||||
]);
|
||||
}
|
||||
|
||||
step.output = {
|
||||
applicationId: result.applicationId,
|
||||
universalIdentifier: result.universalIdentifier,
|
||||
};
|
||||
step.status = 'done';
|
||||
this.notify();
|
||||
|
||||
return step.output;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user