1a475d0edd
## What & why Syncing a Twenty app's metadata is destructive (removing a field/object drops the backing column/table), but the only preview was `dev --once --dry-run`, which collapsed every change into one line per entity — no before/after, no color, no destructive warning, and no confirmation before a real sync. This introduces a `terraform plan`-style flow. The server's `syncApplication(manifest, dryRun)` already returns a complete `SyncAction[]` (create/update/delete with per-attribute `before`/`after`), so this is a CLI-only change — **no server changes**. ## Command surface `plan` previews, `apply` applies; `dev` is the watch wrapper over the same engine. | Command | Behavior | | --- | --- | | `twenty plan [appPath]` | Render the full plan, read-only | | `twenty apply [appPath]` | Plan → confirm on destructive → apply | | `twenty dev --once` | **Deprecated** alias of `twenty apply` (still works, warns) | | `twenty dev --once --dry-run` | **Deprecated** alias of `twenty plan` (still works, warns) | | `twenty dev` (watch) | Compact summary; inline `[y/N]` confirm on destructive saves | | `-f, --force` | Skip the destructive gate (on `apply` and `dev`) | ## Plan output ``` Twenty will perform the following actions: # objectMetadata "rocket" will be created + nameSingular = "rocket" + labelSingular = "Rocket" # fieldMetadata "name" will be updated in-place ~ label = "Name" -> "Launch name" ~ isNullable = true -> false # fieldMetadata "legacyCode" will be destroyed - name = "legacyCode" Plan: 1 to add, 1 to change, 1 to destroy. Warning: 1 destructive change(s) will permanently delete data. - fieldMetadata "legacyCode" — drops the column and its data Destroys are irreversible. Review carefully before applying. ``` Grouped by metadata type, ordered create → update → destroy, `=` aligned per block. Internal keys (`id`, `workspaceId`, `*Id`, timestamps, nulls) are filtered; updates show only changed keys via the server `diff`. ## Destructive safety gate The server applies the manifest diff atomically, so every apply path computes the plan read-only first, then decides whether to apply: - **`twenty apply` / `dev --once`** — interactive `y/N` prompt when the plan deletes metadata; `--force` skips; **fails closed** (exit 1) in CI / non-TTY. - **`dev` (watch)** — creates/updates auto-apply with the compact summary; a save that deletes metadata shows an inline `y/N` prompt in the Ink UI. **Declining cleanly stops the watch** (exit 1) rather than leaving the session in a nagging/blocked state — since the atomic apply would otherwise also block the additive changes on every subsequent save until resolved. `dev --force` applies deletions without asking. ## Notes - `twenty apply` / `dev --once` now do one extra **read-only** dry-run before applying (to compute the plan + gate). `--force` skips it. - The watch sync step now skips API-client regeneration on any non-synced outcome (error or decline), avoiding a partial client write during shutdown. - The Ink watch UI keeps its existing compact summary; the full plan renders only on the plain-console surfaces — `dev` watch output is unchanged in the common case. ## Test plan - `npx nx typecheck twenty-sdk` ✓ - `npx nx lint twenty-sdk` ✓ - Unit tests (vitest): renderer (`format-sync-actions-plan.spec.ts`) + confirm gate (`confirm-destructive-apply.spec.ts`); existing summary / sync-step specs still green. - Manual against `simple-app` + a local server: `plan`, `apply` (destructive prompt + `--force` + non-TTY fail-closed), and the `dev` watch inline confirm (incl. decline → stop).