feat(app-dev): sync error hints, flatEntity labels, dev-mode summary UI, and docs (#21252)

Split out of #21240 — all remaining app-dev improvements. Stacked on
#21251 (review/merge that first).

- Actionable recovery hints on failed syncs; unified diff renderer;
`--dry-run` guard.
- Return `flatEntity` on update/delete sync actions and unify the diff
label.
- Summarize the dev-mode entity list unless `--verbose`.
- Docs: syncing & recovery guide + dry-run + open-an-issue prompt.
- Live execution mode for synced logic functions; clearer manifest
warnings.

<img width="1018" height="700" alt="image"
src="https://github.com/user-attachments/assets/5e9ce19e-0f1d-4f99-8524-4e118bde932b"
/>
This commit is contained in:
martmull
2026-06-08 17:43:28 +02:00
committed by GitHub
parent 13e8e26d1c
commit 77d1e8ced6
34 changed files with 716 additions and 292 deletions
@@ -1,13 +1,13 @@
type SuccessfulApiResponse<T = unknown> = {
type SuccessfulApiResponse<TData = unknown> = {
success: true;
data: T;
data: TData;
message?: string;
};
type FailingApiResponse = {
type FailingApiResponse<TError = unknown> = {
success: false;
error?: unknown;
error?: TError;
message?: string;
};
export type ApiResponse<T = unknown> =
| SuccessfulApiResponse<T>
| FailingApiResponse;
export type ApiResponse<TData = unknown, TError = unknown> =
| SuccessfulApiResponse<TData>
| FailingApiResponse<TError>;
@@ -5,7 +5,10 @@ import { FileApi } from '@/cli/utilities/api/file-api';
import { LogicFunctionApi } from '@/cli/utilities/api/logic-function-api';
import { SchemaApi } from '@/cli/utilities/api/schema-api';
import { type Manifest } from 'twenty-shared/application';
import { type SyncAction } from 'twenty-shared/metadata';
import {
type MetadataValidationErrorResponse,
type SyncAction,
} from 'twenty-shared/metadata';
type ApiServiceOptions = {
disableInterceptors?: boolean;
@@ -77,10 +80,13 @@ export class ApiService {
manifest: Manifest,
options?: { dryRun?: boolean },
): Promise<
ApiResponse<{
applicationUniversalIdentifier: string;
actions: SyncAction[];
}>
ApiResponse<
{
applicationUniversalIdentifier: string;
actions: SyncAction[];
},
MetadataValidationErrorResponse
>
> {
return this.applicationApi.syncApplication(manifest, options);
}
@@ -1,7 +1,11 @@
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
import { serializeError } from '@/cli/utilities/error/serialize-error';
import axios, { type AxiosInstance } from 'axios';
import { type Manifest } from 'twenty-shared/application';
import { type SyncAction } from 'twenty-shared/metadata';
import {
type MetadataValidationErrorResponse,
type SyncAction,
} from 'twenty-shared/metadata';
export class ApplicationApi {
constructor(private readonly client: AxiosInstance) {}
@@ -256,10 +260,13 @@ export class ApplicationApi {
manifest: Manifest,
options?: { dryRun?: boolean },
): Promise<
ApiResponse<{
applicationUniversalIdentifier: string;
actions: SyncAction[];
}>
ApiResponse<
{
applicationUniversalIdentifier: string;
actions: SyncAction[];
},
MetadataValidationErrorResponse
>
> {
try {
const mutation = `
@@ -273,7 +280,7 @@ export class ApplicationApi {
const variables = { manifest, dryRun: options?.dryRun ?? false };
const response: AxiosResponse = await this.client.post(
const response = await this.client.post(
'/metadata',
{
query: mutation,
@@ -290,7 +297,8 @@ export class ApplicationApi {
if (response.data.errors) {
return {
success: false,
error: response.data.errors[0],
error: response.data.errors[0]?.extensions,
message: response.data.errors[0]?.message,
};
}
@@ -300,27 +308,9 @@ export class ApplicationApi {
message: `Successfully synced application: ${manifest.application.displayName}`,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
const graphqlErrors = error.response.data?.errors;
if (Array.isArray(graphqlErrors) && graphqlErrors.length > 0) {
return {
success: false,
error: graphqlErrors[0]?.message || error.message,
};
}
return {
success: false,
error:
error.response.data?.message ||
`HTTP ${error.response.status}: ${error.message}`,
};
}
return {
success: false,
error: error instanceof Error ? error.message : error,
message: serializeError(error),
};
}
}
@@ -337,7 +327,7 @@ export class ApplicationApi {
const variables = { universalIdentifier };
const response: AxiosResponse = await this.client.post(
const response = await this.client.post(
'/metadata',
{
query: mutation,
@@ -1,7 +1,9 @@
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { serializeError } from '@/cli/utilities/error/serialize-error';
import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
import * as fs from 'fs';
import * as path from 'path';
import { type MetadataValidationErrorResponse } from 'twenty-shared/metadata';
import { type FileFolder } from 'twenty-shared/types';
import { pascalCase } from 'twenty-shared/utils';
@@ -136,7 +138,7 @@ export class FileApi {
universalIdentifier,
}: {
universalIdentifier: string;
}): Promise<ApiResponse<boolean>> {
}): Promise<ApiResponse<boolean, MetadataValidationErrorResponse>> {
try {
const mutation = `
mutation InstallApplication($universalIdentifier: String!) {
@@ -163,7 +165,9 @@ export class FileApi {
if (response.data.errors) {
return {
success: false,
error: response.data.errors[0] || 'Failed to install application',
error: response.data.errors[0]?.extensions,
message:
response.data.errors[0]?.message || 'Failed to install application',
};
}
@@ -172,16 +176,9 @@ export class FileApi {
data: response.data.data.installApplication,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return {
success: false,
error: error.response.data?.errors?.[0]?.message || error.message,
};
}
return {
success: false,
error,
message: serializeError(error),
};
}
}