Skip to content

Add machine-readable output modes (--output text|json), a typed SDK, and agent skills to drizzle-kit#5691

Open
dankochetov wants to merge 99 commits into
betafrom
ai
Open

Add machine-readable output modes (--output text|json), a typed SDK, and agent skills to drizzle-kit#5691
dankochetov wants to merge 99 commits into
betafrom
ai

Conversation

@dankochetov
Copy link
Copy Markdown
Contributor

@dankochetov dankochetov commented Apr 28, 2026

Summary

Gives drizzle-kit generate / push a machine-readable, non-interactive contract so agents, build tools, and CI can drive them without scraping prompts or terminal output. The old single --json boolean is replaced by three orthogonal axes:

  • Output format--output text (human, default) vs --output json (one machine-readable envelope), controls how results render.
  • Interactivity — whether the CLI may prompt, derived purely from the terminal: interactive = output === 'text' && !!process.stdin.isTTY. --output json is therefore always non-interactive.
  • Hints--hints / --hints-file supply rename/data-loss decisions up front so the CLI never has to ask, in either output mode.

Three surfaces ship together: the --output CLI, a typed root-level SDK, and an installable Agent Skills catalog.

What's included

1. --output text | json for generate and push

--output json emits a single JSON object on stdout, a discriminated union keyed on status:

  • ok — work succeeded (migration_path for generate; statements / hints in --explain)
  • no_changes — schema in sync, nothing to do
  • missing_hints — the diff has ambiguous or unsafe operations that need caller input; the response lists unresolved items (exit code 2)
  • error — structured failure with a stable error.code

Under the default --output text, the same run is human-readable. When it is non-interactive (non-TTY) and a decision is still unresolved, it prints a missing-decisions report to stdout and exits 2 instead of hanging on a prompt. Full envelope / status / error-code reference: drizzle-kit/JSON_CONTRACT.md; the text/interactivity model and the report shape: drizzle-kit/OUTPUT_MODES.md.

2. Hints flow (output-agnostic)

Ambiguous diffs (rename vs. create+drop) and unsafe operations surface as typed unresolved items the caller resolves and resubmits — identically in text and JSON mode. Kinds:

  • rename_or_create
  • confirm_data_losstable, column, schema, view, primary_key, add_not_null, add_unique (each carries a reason: non_empty, nulls_present, duplicates_present, type_change)
  • structural errors via unsupported_schema_changedrop_pk_dependency, fk_target_not_unique, rename_blocked_by_check_constraint, rename_schema_unsupported

Implemented for postgres, mysql, sqlite, mssql, cockroach, and singlestore. The full vocabulary (kinds, reasons, identifier-tuple shapes) lives in drizzle-kit/HINTS.md.

3. SDK — typed root-level exports

The same contract is callable as a TypeScript API:

import { generate, push, defineConfig } from 'drizzle-kit';
import type { Config, GenerateOptions, PushOptions, Hint, MissingHint } from 'drizzle-kit';

const res = await generate({ dialect: 'postgresql', schema: './src/schema.ts', out: './drizzle' });
if (res.status === 'missing_hints') {
  // res.unresolved: MissingHint[] — build a Hint[] reply and re-invoke
  await generate({ dialect: 'postgresql', schema: './src/schema.ts', out: './drizzle', hints });
}

The SDK always runs in JSON mode internally (no --output flag at the call site) and returns the same envelope the CLI prints. hints is a raw Hint[] (the CLI --hints flag is the JSON-string form); push credentials are passed flat (url / host / …), with no dbCredentials wrapper. Response shapes are obtained via Awaited<ReturnType<typeof generate | push>> and narrowed on status — they are not separate named exports. Full surface in drizzle-kit/SDK.md.

4. Agent Skills catalog (drizzle-kit skills)

npx drizzle-kit skills installs a bundled Agent Skills catalog into the host project — .claude/skills/<slug>/SKILL.md for Claude Code, .agents/skills/<slug>/SKILL.md for Codex / Cursor / Gemini CLI / Cline / Copilot and any tool following the open AGENTS.md convention. Skills shipped:

  • drizzle — umbrella staleness check against the installed drizzle-kit
  • drizzle-generate, drizzle-push, drizzle-migrations — the command workflows
  • drizzle-hints — resolving missing_hints
  • drizzle-responses-and-errors — decoding the envelope and error codes
  • drizzle-output-modes — choosing --output text vs json, interactivity, and the text report

Documentation

  • drizzle-kit/JSON_CONTRACT.md — the JSON-mode envelope (statuses, error codes) (new)
  • drizzle-kit/OUTPUT_MODES.md — the --output text | json and interactivity model + missing-decisions text report (new)
  • drizzle-kit/HINTS.md — the output-agnostic hint vocabulary (new)
  • drizzle-kit/SDK.md — the programmatic SDK surface (new)

Compatibility

  • Interactive CLI behavior is unchanged — --output text is the default and prompts on a TTY exactly as before.
  • No changes to the snapshot format or migration SQL output.

…or better output formatting

- Updated connection issue messages in mssql, mysql, and singlestore validations to use humanLog instead of console.log.
- Enhanced error handling in Cockroach, MySQL, and PostgreSQL DDL processes to throw errors with detailed messages instead of logging invalid entities.
- Replaced console.log with humanLog in various serializer files for consistent logging.
- Introduced explainJsonOutput function to format hints for JSON output, ensuring ANSI codes are stripped and statements are excluded.
- Added tests for JSON output in push and generate commands to validate structured responses.
- Ensured that warnings do not leak to stdout in JSON mode, maintaining clean output.
- Introduced `runWithCliContext` and `getCliContext` functions to manage CLI context, allowing for better handling of JSON mode across tests and commands.
- Updated various test files to utilize the new context management functions, ensuring consistent behavior in JSON mode.
- Added a comprehensive JSON contract document detailing the expected behavior and structure for commands supporting `--json`.
- Modified hints handling in tests to ensure proper context is passed, improving clarity and maintainability.
- Enhanced error handling for missing hints in JSON mode, providing clearer feedback for users.
- Adjusted the date for skipping tests related to OR, AND, and NOT conditions in sql-builder.test.ts to June 26, 2026.
- Modified the connection string retrieval in mysql.test.ts to prioritize the MYSQL_CONNECTION_STRING environment variable before falling back to createDockerDB().
@dankochetov dankochetov force-pushed the ai branch 2 times, most recently from 1d51124 to a32f7fa Compare April 28, 2026 15:22
dankochetov added 18 commits May 5, 2026 19:15
Replace stale `docker run` notes with a real bash dispatcher that wraps
`docker compose -p drizzle-<dialect> -f compose/<dialect>.yml` for the
13 supported dialects. Provides up/down/logs/ps/wait/help subcommands;
defaults to `up` over every dialect when invoked with no args.

- `-p drizzle-<dialect>` is passed on every docker compose invocation so
  projects don't collide and `down` is idempotent.
- `up` follows compose's `--wait --wait-timeout 120` with a host-side
  TCP probe via `compose/wait.sh` to absorb Docker Desktop port-forward
  races on macOS.
- `down -v` wipes volumes (test DBs are ephemeral).
- Unknown dialects fail fast with the full allowlist printed to stderr.
- Stays bash 3.2 compatible (macOS default): plain `case` for the
  dialect map, and `${args[@]+"${args[@]}"}` to safely expand a possibly
  empty positional array under `set -u`.
- Drops the macOS singlestore-on-3306 variant; compose/singlestore.yml
  on 33307 is canonical for both Linux and macOS.
Wire connection-string env vars consumed by drizzle-kit's mariadb,
mysql, postgres, cockroach, mssql, and singlestore test suites to the
canonical compose/*.yml host ports driven by compose/dockers.sh.

- Commit drizzle-kit/.env.example as the source of truth (ports,
  credentials, the cockroach `/defaultdb` correction, and a documented
  MYSQL_CONNECTION_STRING singlestore overload).
- Ignore drizzle-kit/.env via the root .gitignore so each developer
  keeps a local copy without committing it. Did not introduce a global
  *.env rule; integration-tests/.env (separate package, separate
  conventions) remains unaffected.
- Whitelist .env.example in drizzle-kit/.gitignore (its leading `/*`
  rule otherwise hides every new file in the package).
Without the postgres-vector branch, dockers.sh up postgres-vector would
silently skip the host-side TCP gate (catch-all printed to stdout and
returned 0). The catch-all now writes to stderr and exits 1 so a typo
fails loudly instead of looking healthy.
Project-scoped skill at .claude/skills/run-tests/SKILL.md that walks
through ensuring drizzle-kit/.env exists, picking the right pnpm test:*
target, bringing up the matching dialect via compose/dockers.sh, and
the singlestore MYSQL_CONNECTION_STRING overload. Update .claude
gitignore to expose skills/ while keeping worktrees/ excluded.
…d document expired skipIf workflow

The two cli test files relied on the env var being set by the top-level pnpm test
script and failed under pnpm test:other, which doesn't set it. Match the existing
pattern from cli-push.test.ts and cli-generate.test.ts so the files defend
themselves regardless of which script invokes vitest.

Also extend the run-tests skill with the expired skipIf-postpone workflow so
future failures get checked for expired date gates before being treated as
regressions.
Conflict resolution preserved v1.1 contract: no aborted reintroduction;
hints.ts/errors.ts/context.ts/JSON_CONTRACT.md kept from ai (new on ai
post merge-base, naturally preserved); consolidated unsupported_schema_change
+ meta.kind shape unchanged; FIX-01..03 push-handler json-mode gates preserved.

Non-trivial conflicts (per 17-MERGE-SURVEY.md):
- drizzle-kit/src/cli/commands/generate-sqlite.ts: kept ai's full json-mode +
  hint-aware flow; added beta's checkResult?: CheckHandlerResult parameter
  and threaded it through prepareSqliteSnapshot. Skipped beta's try/catch
  wrapper as it would swallow errors and conflict with the json contract.
- drizzle-kit/src/dialects/sqlite/serializer.ts: kept humanLog import from
  ai (used in body), added CheckHandlerResult type and assertUnreachable
  imports from beta.

CONTEXT.md anticipated heavy push-*.ts conflicts; post-unshallow merge-base
(56b4b28) shows all v1.1 contract surface files (push-mysql/mssql/postgres/
cockroach/sqlite/singlestore, prompts.ts, generate-mysql/mssql/postgres/
cockroach/singlestore, cli/index.ts, cli/utils.ts, release-feature-branch.yaml,
pnpm-lock.yaml) auto-merged unchanged because beta's eec7260 does not modify
them relative to merge-base.
Introduce a programmatic SDK (drizzle-kit/src/sdk/) for generate/push,
unify the CLI around an envelope contract (drizzle-kit/src/cli/contract.ts),
and extract shared generate/push pipelines into schema.ts. Adds SDK
conformance + regression tests and reworks affected CLI/dialect/api
modules accordingly.
Non-trivial resolutions:
- Adopted beta's validations/cli.ts deletion + EntitiesFilterConfig move to validations/common.ts.
- Absorbed beta's per-command zod schemas (configPush, configPull, configCheck, configGenerate, configExport, configStudio, configMigrate) in validations/common.ts; preserved ai's typed-throw + HintsHandler stack across the boundary by relocating pullParams / pushParams / studioConfig / CliConfig into validations/common.ts and restoring AmbiguousParamsCliError / ConfigConnectionCliError throws in common.ts, libsql.ts, and mssql.ts (beta's console.log + process.exit fallbacks reverted to ai's typed-error pattern).
- Confirmed cli/schema.ts auto-merge preserves prepareGenerate / runGenerate / preparePush / runPush extraction + the statusToExitCode tail at the brocli handler exit; beta's CheckConfig typing + studio transform hook compose cleanly with ai's dispatch pipeline.
- Reconciled cli-{export,generate,migrate,push}.test.ts conflicts at the imports only; beta's rewritten test bodies retained as the new baseline.
- skipIf date conflicts in pg-*, sql-builder, and integration-tests/common-* tests resolved by keeping ai's later expiry dates (2026-06-01 / 2026-06-26).
- mysql integration test TestContext narrowed to env-var-driven connection (ai's fixture pattern) while adopting beta's MySql2Database single-generic signature from the MySQL refactor.

Accepted deletions:
- drizzle-kit/src/cli/validations/cli.ts, integration-tests/tests/mysql/mysql-v1.test.ts, integration-tests/tests/mysql/mysql.duplicates.test.ts (the three modify/delete conflicts).
- Additional clean upstream deletions: drizzle-kit/src/cli/validations/studio.ts (studioCliParams folded into common.ts), drizzle-orm/src/mysql-core/query-builders/_query.ts, drizzle-orm/type-tests/mysql/db-rel.ts, integration-tests/tests/mysql/mysql.duplicates.ts, integration-tests/tests/mysql/mysql.planetscale-v1.test.ts (per beta d8460d5).

Hint contract preserved:
- No aborted status reintroduced anywhere in drizzle-kit/src/cli.
- isJsonMode() caller-context gates on push throws intact: drop_pk_dependency in mysql, rename_blocked_by_check_constraint + rename_schema_unsupported in mssql.
- missing_hints exit-code 2 behavior unchanged; --hints / --hints-file parsing preserved through pushParams / pullParams retention.
Production changes
- validations/common.ts: pre-merge import path lost its src/ alias; switch back to relative ../../utils/schemaValidator so jiti resolves at test time.
- utils-node.ts loadModule: jiti.import branch was missing the mod.default ?? mod unwrap that the Bun/Deno and ESM branches both perform. Without it drizzleConfigFromFile receives a namespace object with default-forwarding proxy fields that zod's safeParse drops, so config.schema / config.sql / config.tablesFilter become undefined and the prepare* helpers throw RequiredParamsCliError on otherwise valid configs.
- commands/utils.ts prepareExportConfig: take sql from the CLI options first, then from the config file, then default true; previously the CLI --sql=false flag was ignored when the rest of the command came from a config file. Also re-export the CheckConfig type the dispatch needs.

Test adaptations
- 8 cli-*.test.ts files: replaced vi.spyOn(console, 'log') + 'process.exit unexpectedly called with "1"' assertions with res.error.message checks built from the same error() / wrapParam() helpers the runtime uses, so the captured CliError message matches byte-for-byte (including ANSI). Three GenerateConfig / push expected fixtures gained the missing explain + hints fields. cli-check.test.ts, cli-pull.test.ts, cli-studio.test.ts now self-set TEST_CONFIG_PATH_PREFIX so they work under test:other (which doesn't set it).
- conformance-live-db.test.ts push mysql describe gained a beforeAll that drops every table in the active mysql database. Without it, leftover tables from earlier mysql test files in the same run made the diff engine treat new tables as rename_or_create candidates and emit missing_hints instead of the expected ok / no_changes envelopes.
…-undefined

Today (2026-05-20) is the postpone date in five integration-test files for the
"Query error wrapping" / "Mappers: deep nullification" / "Same table name joined
between schemas" gates, so vitest started running them and they fail because sync
drivers (better-sqlite3, sqljs, postgres.js, mssql/tedious, cockroach pg) still
return raw driver errors instead of DrizzleQueryError. Per the repo's skipIf
convention (drizzle-orm/.claude/skills/tests), bump the dates to one month forward
rather than fix in-band: integration-tests/tests/{sqlite/sqlite-common,
pg/common-cache, pg/common-pt2, cockroach/common, mssql/mssql}.ts, plus the
@ts-ignore searchable-marker comment in pg/common-pt2.ts.

Also rewrite the no-useless-undefined violations in
drizzle-kit/tests/sdk/regressions/no-stdout.test.ts (process.exit mock
implementation): `() => undefined` to `() => {}` so the gate that ran lint on the
last green CI build (post-merge) passes. Behavior is identical; the spy still
swallows the exit call.
Tests
- cli-{check,export,generate,migrate,pull,push,studio}.test.ts: assertion text
  for every `validate config #N` case now matches beta's exact wording
  byte-for-byte, constructed via the same error() / wrapParam() helpers the
  runtime uses. The mechanism is res.error.message (not console.log spy +
  process.exit) because the cli path throws DrizzleCliError; the literal
  text it asserts is identical to beta.
- cli-studio.test.ts #5/#6: prepareStudioConfig is still in the legacy
  humanLog + process.exit path, so those tests keep beta's spy mechanism
  unchanged; the only deviation is stripAnsi() on captured calls because
  withStyle.error wraps the message with chalk codes.

Runtime
- validations/common.ts: configCommonSchema.dialect is now optional. The
  early MissingConfigDialectCliError throw in drizzleConfigFromFile is gone
  so each prepare* helper owns the dialect-missing wording (mirrors beta).
- commands/utils.ts:
  - prepareCheckParams: wrapParam('dialect', config.dialect) was being
    passed the imported zod schema; pass the actual config value.
  - prepareGenerateConfig + prepareExportConfig: reorder RequiredParamsCliError
    builders to dialect-first / schema-second; drop the wrapParam('out',...)
    line from generate so the message matches beta exactly.
  - drizzleConfigFromFile: removed the chalk.grey wrap around "Reading config
    file" so spy assertions can compare against plain text (matches beta).
- validations/mssql.ts: rewrote printConfigConnectionIssues to match the
  postgres.ts shape — url branch, server-form branch, generic fallthrough
  with "Either connection \"url\" or \"server\", \"user\", \"password\" are
  required for MsSQL database connection". The previous form duplicated the
  same "Please provide required params for MsSQL driver:" header on both
  branches and had no fallthrough.

Verified
- pnpm tsc --noEmit -p tsconfig.json + tsconfig.typetest.json both clean
- TEST_CONFIG_PATH_PREFIX=./tests/cli/ vitest run tests/other/ → 376/376 pass
loadModule used to apply `mod?.default ?? mod` in all three branches
(Bun/Deno, jiti, ESM Node), so every caller silently received the
default export when present. Only drizzleConfigFromFile actually needs
that — drizzle.config.ts uses `export default defineConfig(...)` —
while the 11 schema-loading call sites in cli/commands/studio.ts and
dialects/*/drizzle.ts iterate `Object.entries(mod)` to find PgTable /
MySqlTable / Relations values, where the unwrap masks bugs and silently
rescues non-canonical `export default { tables }` schemas.

Add an explicit `{ defaultExport }` options flag, default false. All
three branches now return `defaultExport ? (mod?.default ?? mod) : mod`.
drizzleConfigFromFile passes `{ defaultExport: true }`; the schema-
loading call sites are unchanged and now receive the raw namespace.

Behavior change: schema files using `export default { users, posts }`
stop working — must switch to named exports (`export const users =
pgTable(...)`), the documented drizzle pattern.

- pnpm exec tsc --noEmit -p tsconfig.json → 0
- vitest run tests/other/ → 376/376
…d and align `primary_key` hint kind on the wire

Ship five SKILL.md files under `drizzle-kit/skills/` (drizzle-migrations,
drizzle-generate, drizzle-push, drizzle-hints, drizzle-responses-and-errors)
with deduped shared sections — response envelope owned by
drizzle-responses-and-errors, reasons by drizzle-hints, per-dialect notes
by drizzle-migrations. Descriptions trimmed to <=25 words;
`metadata.version: "1.0.0"` on all five; worked examples added for the
`privilege` 5-tuple and the `add_unique` constraint-name slot. Catalog
ships via the new `drizzle-kit skills` install subcommand and a
`tests/skills/catalog.test.ts` smoke gate; the build copies the catalog
into `dist/skills/`.

Rename the rename/create kind literal `primary key` (space) to
`primary_key` (underscore) across the zod schema in `src/cli/hints.ts`,
the runtime resolver call sites, and the hint test fixtures. Both
rename-or-create and confirm-data-loss unions now carry a `primary_key`
entry but stay discriminated by the outer `type` field. `foreign key`
sites preserved. `JSON_CONTRACT.md` updated.

Add a `humanizeKind` helper so TTY prompts render "primary key" rather
than the wire literal.
@dankochetov dankochetov marked this pull request as ready for review May 21, 2026 15:19
…ocal-path `npx skills` invocation and package-manager detection
…adata.revision` bumps on `drizzle-kit/skills/**`

Move skills revision into a dedicated `drizzle-kit skills version` subcommand; diff the whole feature branch against an auto-detected fork point so the gate covers multi-commit branches; and run the skills-revision gate only for pull requests, not raw pushes.
# Conflicts:
#	.github/workflows/release-feature-branch.yaml
#	drizzle-kit/tests/postgres/pg-columns.test.ts
#	drizzle-kit/tests/postgres/pg-enums.test.ts
#	drizzle-kit/tests/postgres/pg-tables.test.ts
#	drizzle-kit/tests/postgres/pull.test.ts
#	drizzle-orm/tests/sql-builder.test.ts
#	integration-tests/tests/mysql/mysql-common-8.ts
#	integration-tests/tests/pg/common-pt1.ts
#	integration-tests/tests/pg/common-pt2.ts
#	integration-tests/tests/pg/common-rqb.ts
#	integration-tests/tests/sqlite/sqlite-common.ts
…ractivity axes, and ship the public SDK and agent skills

- Replace the `--json` boolean with `--output text|json` and decompose `isJsonMode()` into orthogonal `outputFormat()` and `isInteractive()` predicates (interactive = text output and a TTY)
- Accept a raw `Hint[]` for the SDK `hints` option and root-export `Hint`/`MissingHint`; map a malformed `Hint[]` to `invalid_hints` rather than `internal_error`
- Add a `drizzle-output-modes` skill; separate output format from interactivity in the generate/push skills; reference sibling skills by name instead of file links; correct the `confirm_data_loss` `view` availability to postgresql/cockroach materialized views only
- Extract the output-agnostic hint vocabulary into `HINTS.md` and trim `JSON_CONTRACT.md`/`OUTPUT_MODES.md`
- Drop the duplicated `--output` default; delete the redundant text-mode regression tests; convert the SDK `.test-d.ts` files to vitest type tests asserting flat push credentials and `hints: Hint[]`
@dankochetov dankochetov changed the title Add JSON mode to drizzle-kit Add machine-readable output modes (--output text|json), a typed SDK, and agent skills to drizzle-kit May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants