diff --git a/.changeset/cli-drizzle-migrations-out.md b/.changeset/cli-drizzle-migrations-out.md new file mode 100644 index 0000000000..a0df9103bc --- /dev/null +++ b/.changeset/cli-drizzle-migrations-out.md @@ -0,0 +1,5 @@ +--- +'stash': patch +--- + +`stash encrypt drop` and `stash decrypt drop` now write the Drizzle migration to the directory drizzle-kit reports (the `out` in `drizzle.config.ts`) instead of scanning `--migrations-dir`. Previously, when the two differed, the SQL could be written over an older migration with the same name while drizzle-kit journaled an empty stub in the real `out`, so the DROP never shipped. When a scan fallback is needed it matches the exact `_.sql` suffix and refuses to overwrite any file that is not a drizzle-kit scaffold (blank or comment-only), and `--migrations-dir` now warns when drizzle-kit wrote elsewhere. diff --git a/languages/typescript/packages/cli/src/cli/registry.ts b/languages/typescript/packages/cli/src/cli/registry.ts index e052aa7122..26e45bdfde 100644 --- a/languages/typescript/packages/cli/src/cli/registry.ts +++ b/languages/typescript/packages/cli/src/cli/registry.ts @@ -634,7 +634,8 @@ export const registry: CommandGroup[] = [ { name: '--migrations-dir', value: '', - description: 'Directory to write the drop migration into.', + description: + 'Directory to write the drop migration into (Drizzle projects: drizzle.config.ts `out` wins; this is the fallback).', }, ], }, @@ -773,7 +774,8 @@ export const registry: CommandGroup[] = [ { name: '--migrations-dir', value: '', - description: 'Directory to write the drop migration into.', + description: + 'Directory to write the drop migration into (Drizzle projects: drizzle.config.ts `out` wins; this is the fallback).', }, { name: '--uninstall-eql', diff --git a/languages/typescript/packages/cli/src/commands/db/__tests__/find-generated-migration.test.ts b/languages/typescript/packages/cli/src/commands/db/__tests__/find-generated-migration.test.ts index f75dfc9482..7322c56f4c 100644 --- a/languages/typescript/packages/cli/src/commands/db/__tests__/find-generated-migration.test.ts +++ b/languages/typescript/packages/cli/src/commands/db/__tests__/find-generated-migration.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { findGeneratedMigration } from '../../eql/migration.js' +import { findGeneratedMigration } from '../drizzle-migration.js' /** * `findGeneratedMigration` locates the custom migration scaffolded by diff --git a/languages/typescript/packages/cli/src/commands/db/drizzle-migration.ts b/languages/typescript/packages/cli/src/commands/db/drizzle-migration.ts new file mode 100644 index 0000000000..48e11b7a72 --- /dev/null +++ b/languages/typescript/packages/cli/src/commands/db/drizzle-migration.ts @@ -0,0 +1,85 @@ +import { existsSync } from 'node:fs' +import { readdir } from 'node:fs/promises' +import { isAbsolute, join, resolve } from 'node:path' + +/** + * Helpers for locating the file `drizzle-kit generate --custom` just wrote. + * Shared by `eql migration --drizzle` and the `encrypt drop` / `decrypt` + * scaffolds, so both agree on where drizzle-kit's `drizzle.config.ts` sent it. + */ + +/** + * Find the most recently generated Drizzle migration matching the name. + * + * Matches the exact `_.sql` suffix, so a longer name that merely contains + * the stem (`drop_users_email_backup`) is never mistaken for it. + */ +export async function findGeneratedMigration( + outDir: string, + migrationName: string, +): Promise { + if (!existsSync(outDir)) { + throw new Error( + `Drizzle output directory not found: ${outDir}\nMake sure drizzle-kit is configured correctly.`, + ) + } + const migrationSuffix = `_${migrationName}.sql` + const matchingFiles = (await readdir(outDir)) + .filter((entry) => entry.endsWith(migrationSuffix)) + .sort() + if (matchingFiles.length === 0) { + throw new Error( + `Could not find a migration matching "${migrationName}" in ${outDir}`, + ) + } + return join(outDir, matchingFiles[matchingFiles.length - 1]) +} + +// biome-ignore lint/suspicious/noControlCharactersInRegex: matching ESC is the point +const ANSI_ESCAPE = /\u001b\[[0-9;]*[A-Za-z]/g + +function stripAnsi(text: string): string { + return text.replace(ANSI_ESCAPE, '') +} + +/** + * Pull the migration path out of drizzle-kit's own success line: + * + * [✓] Your SQL migration file ➜ drizzle/0000_install-eql.sql 🚀 + * + * Since we no longer pass `--out` (drizzle-kit rejects the run when we do — + * see `generateDrizzleEqlMigration`), `drizzle.config.ts` owns the output + * directory and this line is how we learn it. The path is printed relative to + * the cwd drizzle-kit ran in, which is ours. + * + * Returns `undefined` unless the file is actually there, so an unrecognised + * banner or a moved file falls through to the directory scan rather than + * pointing the SQL write at a path that does not exist. + */ +export function parseReportedMigrationPath( + stdout: string | undefined, +): string | undefined { + if (!stdout) return undefined + // Scanned per line, taking everything between the arrow and the LAST `.sql` + // on it, because an output directory may contain spaces — a `\S+` pattern + // truncates such a path, fails the existence check, and drops us into the + // fallback scan of an unrelated directory. + // + // Last line wins: one invocation writes one file, but a wrapper that echoes + // its own banner first should not shadow drizzle-kit's. + let found: string | undefined + for (const line of stdout.split(/\r?\n/)) { + const arrow = line.indexOf('➜') + if (arrow === -1) continue + // drizzle-kit colours its banner on a TTY-like stdout; escape codes inside + // the path would fail the existence check and drop us into the fallback. + const rest = stripAnsi(line.slice(arrow + 1)) + const end = rest.lastIndexOf('.sql') + if (end === -1) continue + const candidate = rest.slice(0, end + '.sql'.length).trim() + if (!candidate) continue + const abs = isAbsolute(candidate) ? candidate : resolve(candidate) + if (existsSync(abs)) found = abs + } + return found +} diff --git a/languages/typescript/packages/cli/src/commands/encrypt/__tests__/drizzle-helper.test.ts b/languages/typescript/packages/cli/src/commands/encrypt/__tests__/drizzle-helper.test.ts new file mode 100644 index 0000000000..dbe569c623 --- /dev/null +++ b/languages/typescript/packages/cli/src/commands/encrypt/__tests__/drizzle-helper.test.ts @@ -0,0 +1,177 @@ +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const spawnSyncMock = vi.hoisted(() => vi.fn()) +vi.mock('node:child_process', () => ({ spawnSync: spawnSyncMock })) +vi.mock('@/commands/init/utils.js', () => ({ + detectPackageManager: () => 'npm', + runnerArgv: () => ({ command: 'npx', prefixArgs: [] }), +})) + +import { scaffoldDrizzleMigration } from '../drizzle-helper.js' + +/** + * drizzle.config.ts's `out` owns where drizzle-kit writes. The scaffold must + * follow the path drizzle-kit reports, never the --migrations-dir it scans as + * a fallback (CIP-5162). + */ +// What drizzle-kit's `generate --custom` actually writes. +const PLACEHOLDER = '-- Custom SQL migration file, put your code below! --\n' + +describe('scaffoldDrizzleMigration', () => { + let root: string + let cwd: string + beforeEach(() => { + // realpath: process.cwd() reports the physical path after chdir, so a + // symlinked TMPDIR would otherwise break the expected-path assertions. + root = realpathSync(mkdtempSync(join(tmpdir(), 'stash-scaffold-'))) + cwd = process.cwd() + process.chdir(root) + spawnSyncMock.mockReset() + }) + afterEach(() => { + process.chdir(cwd) + rmSync(root, { recursive: true, force: true }) + }) + + function drizzleKitWrites(relPath: string) { + spawnSyncMock.mockImplementation(() => { + mkdirSync(join(relPath, '..'), { recursive: true }) + writeFileSync(relPath, PLACEHOLDER) + return { + status: 0, + stdout: `[✓] Your SQL migration file ➜ ${relPath} 🚀\n`, + stderr: '', + } + }) + } + + it('writes to the directory drizzle-kit reports, not the scanned one', async () => { + mkdirSync('drizzle') + writeFileSync( + 'drizzle/0000_drop_users_email.sql', + 'ALTER TABLE t DROP COLUMN c;', + ) + drizzleKitWrites('src/db/migrations/0003_drop_users_email.sql') + + const result = await scaffoldDrizzleMigration({ + name: 'drop_users_email', + outDir: 'drizzle', + sql: 'ALTER TABLE users DROP COLUMN email;', + }) + + expect(result.path).toBe( + join(root, 'src/db/migrations/0003_drop_users_email.sql'), + ) + expect(readFileSync(result.path, 'utf-8')).toContain('DROP COLUMN email') + expect(readFileSync('drizzle/0000_drop_users_email.sql', 'utf-8')).toBe( + 'ALTER TABLE t DROP COLUMN c;', + ) + expect(result.dir).not.toBe(result.requestedDir) + }) + + it('does not require the fallback directory to exist when the path is reported', async () => { + drizzleKitWrites('db/0000_drop_a.sql') + const result = await scaffoldDrizzleMigration({ + name: 'drop_a', + outDir: 'drizzle', + sql: 'SELECT 1;', + }) + expect(existsSync(result.path)).toBe(true) + }) + + it('falls back to the scan when the banner is unrecognised, taking only a new exact-suffix file', async () => { + mkdirSync('drizzle') + writeFileSync('drizzle/0000_drop_a.sql', 'ALTER TABLE t DROP COLUMN c;') + spawnSyncMock.mockImplementation(() => { + writeFileSync('drizzle/0001_drop_a.sql', PLACEHOLDER) + writeFileSync('drizzle/0002_drop_a_backup.sql', '') + return { status: 0, stdout: 'done', stderr: '' } + }) + const result = await scaffoldDrizzleMigration({ + name: 'drop_a', + outDir: 'drizzle', + sql: 'SELECT 1;', + }) + expect(result.path).toBe(join(root, 'drizzle/0001_drop_a.sql')) + expect(readFileSync('drizzle/0000_drop_a.sql', 'utf-8')).toBe( + 'ALTER TABLE t DROP COLUMN c;', + ) + }) + + it('refuses to overwrite a pre-existing same-stem file when nothing new appeared', async () => { + mkdirSync('drizzle') + writeFileSync('drizzle/0000_drop_a.sql', 'ALTER TABLE t DROP COLUMN c;') + spawnSyncMock.mockReturnValue({ status: 0, stdout: 'done', stderr: '' }) + await expect( + scaffoldDrizzleMigration({ + name: 'drop_a', + outDir: 'drizzle', + sql: 'SELECT 1;', + }), + ).rejects.toThrow(/Refusing to overwrite/) + expect(readFileSync('drizzle/0000_drop_a.sql', 'utf-8')).toBe( + 'ALTER TABLE t DROP COLUMN c;', + ) + }) + + it('follows a reported path even when the banner is colourised', async () => { + mkdirSync('db') + spawnSyncMock.mockImplementation(() => { + writeFileSync('db/0000_drop_a.sql', PLACEHOLDER) + return { + status: 0, + stdout: + '[✓] Your SQL migration file \u001b[2m➜\u001b[0m \u001b[1mdb/0000_drop_a.sql\u001b[0m 🚀', + stderr: '', + } + }) + const result = await scaffoldDrizzleMigration({ + name: 'drop_a', + outDir: 'drizzle', + sql: 'SELECT 1;', + }) + expect(result.path).toBe(join(root, 'db/0000_drop_a.sql')) + }) + + it('refuses to overwrite a reported file that already has content', async () => { + mkdirSync('db') + writeFileSync('db/0000_old.sql', 'ALTER TABLE t DROP COLUMN c;') + spawnSyncMock.mockReturnValue({ + status: 0, + stdout: '➜ db/0000_old.sql', + stderr: '', + }) + await expect( + scaffoldDrizzleMigration({ + name: 'x', + outDir: 'drizzle', + sql: 'SELECT 1;', + }), + ).rejects.toThrow(/not a drizzle-kit scaffold/) + expect(readFileSync('db/0000_old.sql', 'utf-8')).toBe( + 'ALTER TABLE t DROP COLUMN c;', + ) + }) + + it('surfaces drizzle-kit stdout and stderr on failure', async () => { + spawnSyncMock.mockReturnValue({ + status: 1, + stdout: 'config error', + stderr: 'boom', + }) + await expect( + scaffoldDrizzleMigration({ name: 'x', outDir: 'drizzle', sql: '' }), + ).rejects.toThrow(/config error\nboom/) + }) +}) diff --git a/languages/typescript/packages/cli/src/commands/encrypt/drizzle-helper.ts b/languages/typescript/packages/cli/src/commands/encrypt/drizzle-helper.ts index f820d6c46e..632b4a86c6 100644 --- a/languages/typescript/packages/cli/src/commands/encrypt/drizzle-helper.ts +++ b/languages/typescript/packages/cli/src/commands/encrypt/drizzle-helper.ts @@ -1,7 +1,11 @@ import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' -import { readdir, writeFile } from 'node:fs/promises' -import { join, resolve } from 'node:path' +import { readdir, readFile, writeFile } from 'node:fs/promises' +import { basename, dirname, resolve } from 'node:path' +import { + findGeneratedMigration, + parseReportedMigrationPath, +} from '@/commands/db/drizzle-migration.js' import { detectPackageManager, runnerArgv } from '@/commands/init/utils.js' /** @@ -10,12 +14,21 @@ import { detectPackageManager, runnerArgv } from '@/commands/init/utils.js' * does — `drizzle-kit generate --custom` creates the file and records the * journal entry / snapshot, then we overwrite the empty body with our SQL. * - * Used by `encrypt drop` to ship the plaintext-column drop migration in a - * shape `drizzle-kit migrate` actually picks up. + * Used by `encrypt drop` and the `decrypt` commands to ship the + * plaintext-column drop migration in a shape `drizzle-kit migrate` actually + * picks up. * - * Throws if `drizzle-kit` isn't on PATH or the generated file can't be - * located afterwards. Callers should fall back to the self-named-file - * approach for non-Drizzle projects. + * `drizzle.config.ts`'s `out` decides where drizzle-kit writes (we cannot pass + * `--out`: it switches drizzle-kit into CLI mode — see + * `generateDrizzleEqlMigration`), so the path drizzle-kit REPORTS is the + * authority on where the file landed. `outDir` is only the fallback place to + * look when that report is missing, and the fallback only accepts a file that + * did not exist before the run — otherwise an older same-stem migration in an + * unrelated directory would be overwritten. + * + * Throws if `drizzle-kit` can't run or the generated file can't be located + * afterwards. Callers should fall back to the self-named-file approach for + * non-Drizzle projects. */ export async function scaffoldDrizzleMigration(opts: { /** @@ -23,17 +36,13 @@ export async function scaffoldDrizzleMigration(opts: { * Drizzle prefixes with the next sequential index (`0003_.sql`). */ name: string - /** Drizzle's `out` directory, defaults to `./drizzle` if unset. */ + /** Fallback directory to scan if drizzle-kit's reported path is unusable. */ outDir: string /** SQL to write into the generated file. */ sql: string -}): Promise<{ path: string }> { +}): Promise<{ path: string; dir: string; requestedDir: string }> { const outDir = resolve(opts.outDir) - if (!existsSync(outDir)) { - throw new Error( - `Drizzle output directory not found: ${outDir}\nMake sure drizzle-kit is configured correctly (check drizzle.config.ts's \`out\`).`, - ) - } + const before = await listSqlFiles(outDir) // `drizzle-kit generate --custom` scaffolds an empty migration file with // the right prefix and records the journal/snapshot entry. spawnSync @@ -53,34 +62,68 @@ export async function scaffoldDrizzleMigration(opts: { { stdio: 'pipe', encoding: 'utf-8' }, ) if (cp.error || cp.status !== 0) { - const stderr = typeof cp.stderr === 'string' ? cp.stderr.trim() : undefined + const output = [cp.stdout, cp.stderr] + .map((stream) => (typeof stream === 'string' ? stream.trim() : '')) + .filter(Boolean) + .join('\n') throw new Error( - `Failed to scaffold Drizzle migration: ${stderr ?? cp.error?.message ?? 'unknown error'}`, + `Failed to scaffold Drizzle migration: ${output || cp.error?.message || 'unknown error'}`, ) } - const generatedPath = await findLatestNamedMigration(outDir, opts.name) + const generatedPath = + parseReportedMigrationPath(cp.stdout) ?? + (await findNewMigration(outDir, opts.name, before)) + // `generate --custom` leaves a comment-only placeholder (drizzle-kit writes + // `-- Custom SQL migration file, put your code below! --`). Anything with + // real SQL is an existing migration (a banner line naming one, a wrapper's + // echo): never overwrite it. + if (!isScaffoldOnly(await readFile(generatedPath, 'utf-8'))) { + throw new Error( + `${generatedPath} is not a drizzle-kit scaffold; refusing to overwrite it. If drizzle-kit just journaled a placeholder "${opts.name}" migration, it is in the \`out\` of your drizzle.config.ts.`, + ) + } await writeFile(generatedPath, opts.sql, 'utf-8') - return { path: generatedPath } + return { + path: generatedPath, + dir: dirname(generatedPath), + requestedDir: outDir, + } +} + +/** Blank, or nothing but `--` comment lines: no statement to lose. */ +function isScaffoldOnly(content: string): boolean { + return content + .split(/\r?\n/) + .every((line) => line.trim() === '' || line.trim().startsWith('--')) +} + +async function listSqlFiles(dir: string): Promise> { + if (!existsSync(dir)) return new Set() + return new Set((await readdir(dir)).filter((f) => f.endsWith('.sql'))) } /** - * Find the most recent `0NNN_.sql` in the Drizzle out directory. - * Drizzle's flat numbered prefix means string-sort is the right order. + * Fallback when drizzle-kit's banner is not recognised: the newest exact + * `_.sql` in `outDir`, provided this run created it. A file that was + * already there is somebody's earlier (maybe applied) migration, and + * overwriting it would silently lose the new one. */ -async function findLatestNamedMigration( +async function findNewMigration( outDir: string, name: string, + before: Set, ): Promise { - const entries = await readdir(outDir) - const matching = entries - .filter((entry) => entry.endsWith('.sql') && entry.includes(name)) - .sort() - if (matching.length === 0) { + const found = await findGeneratedMigration(outDir, name).catch(() => { + throw new Error( + `Could not locate the migration drizzle-kit generated for "${name}". It was not reported on stdout and is not in ${outDir}. Check the \`out\` in your drizzle.config.ts — a custom migration may have been journaled there with an empty body.`, + ) + }) + if (before.has(basename(found))) { throw new Error( - `Could not find a migration matching "${name}" in ${outDir} after running drizzle-kit generate.`, + `drizzle-kit did not report where it wrote "${name}", and ${outDir} only holds a pre-existing file with that name (${found}). Refusing to overwrite it. Check the \`out\` in your drizzle.config.ts — an empty custom migration may have been journaled there.`, ) } - return join(outDir, matching[matching.length - 1] as string) + return found } diff --git a/languages/typescript/packages/cli/src/commands/encrypt/lib/migration-file.ts b/languages/typescript/packages/cli/src/commands/encrypt/lib/migration-file.ts index 5b0ef63177..f0acb93d06 100644 --- a/languages/typescript/packages/cli/src/commands/encrypt/lib/migration-file.ts +++ b/languages/typescript/packages/cli/src/commands/encrypt/lib/migration-file.ts @@ -1,7 +1,9 @@ import fs from 'node:fs' import path from 'node:path' +import * as p from '@clack/prompts' import { detectDrizzle } from '@/commands/db/detect.js' import { detectPackageManager, runnerCommand } from '@/commands/init/utils.js' +import { messages } from '@/messages.js' import { scaffoldDrizzleMigration } from '../drizzle-helper.js' /** Where a generated migration landed, and how to apply it. */ @@ -41,6 +43,21 @@ export async function writeMigrationFile(opts: { outDir: migrationsDir, sql: opts.sql, }) + // `drizzle.config.ts` decides where drizzle-kit writes, so an explicit + // --migrations-dir is only honoured as a fallback. Say so rather than leave + // the user believing the file is where they asked for it. + if ( + opts.migrationsDir !== undefined && + result.dir !== result.requestedDir + ) { + p.log.warn( + messages.eql.migrationDrizzleOutOverridden( + result.dir, + result.requestedDir, + '--migrations-dir', + ), + ) + } return { filePath: result.path, nextStep: `Apply with your usual Drizzle migrate command:\n ${runnerCommand(detectPackageManager(), 'drizzle-kit migrate')}`, diff --git a/languages/typescript/packages/cli/src/commands/eql/migration.ts b/languages/typescript/packages/cli/src/commands/eql/migration.ts index 8bec470905..bbc9920791 100644 --- a/languages/typescript/packages/cli/src/commands/eql/migration.ts +++ b/languages/typescript/packages/cli/src/commands/eql/migration.ts @@ -1,11 +1,14 @@ import { spawnSync } from 'node:child_process' import { existsSync, unlinkSync, writeFileSync } from 'node:fs' -import { readdir } from 'node:fs/promises' -import { dirname, isAbsolute, join, resolve } from 'node:path' +import { dirname, resolve } from 'node:path' import { MIGRATIONS_SCHEMA_SQL } from '@cipherstash/migrate' import * as p from '@clack/prompts' import { CliExit } from '@/cli/exit.js' import { detectSupabaseProject } from '@/commands/db/detect.js' +import { + findGeneratedMigration, + parseReportedMigrationPath, +} from '@/commands/db/drizzle-migration.js' import { printNextSteps, SAFE_MIGRATION_NAME } from '@/commands/db/install.js' import { rewriteEncryptedAlterColumns } from '@/commands/db/rewrite-migrations.js' import { @@ -35,68 +38,6 @@ import { messages } from '@/messages.js' const DEFAULT_MIGRATION_NAME = 'install-eql' const DEFAULT_DRIZZLE_OUT = 'drizzle' -/** Find the most recently generated Drizzle migration matching the name. */ -export async function findGeneratedMigration( - outDir: string, - migrationName: string, -): Promise { - if (!existsSync(outDir)) { - throw new Error( - `Drizzle output directory not found: ${outDir}\nMake sure drizzle-kit is configured correctly.`, - ) - } - const migrationSuffix = `_${migrationName}.sql` - const matchingFiles = (await readdir(outDir)) - .filter((entry) => entry.endsWith(migrationSuffix)) - .sort() - if (matchingFiles.length === 0) { - throw new Error( - `Could not find a migration matching "${migrationName}" in ${outDir}`, - ) - } - return join(outDir, matchingFiles[matchingFiles.length - 1]) -} - -/** - * Pull the migration path out of drizzle-kit's own success line: - * - * [✓] Your SQL migration file ➜ drizzle/0000_install-eql.sql 🚀 - * - * Since we no longer pass `--out` (drizzle-kit rejects the run when we do — - * see `generateDrizzleEqlMigration`), `drizzle.config.ts` owns the output - * directory and this line is how we learn it. The path is printed relative to - * the cwd drizzle-kit ran in, which is ours. - * - * Returns `undefined` unless the file is actually there, so an unrecognised - * banner or a moved file falls through to the directory scan rather than - * pointing the SQL write at a path that does not exist. - */ -export function parseReportedMigrationPath( - stdout: string | undefined, -): string | undefined { - if (!stdout) return undefined - // Scanned per line, taking everything between the arrow and the LAST `.sql` - // on it, because an output directory may contain spaces — a `\S+` pattern - // truncates such a path, fails the existence check, and drops us into the - // fallback scan of an unrelated directory. - // - // Last line wins: one invocation writes one file, but a wrapper that echoes - // its own banner first should not shadow drizzle-kit's. - let found: string | undefined - for (const line of stdout.split(/\r?\n/)) { - const arrow = line.indexOf('➜') - if (arrow === -1) continue - const rest = line.slice(arrow + 1) - const end = rest.lastIndexOf('.sql') - if (end === -1) continue - const candidate = rest.slice(0, end + '.sql'.length).trim() - if (!candidate) continue - const abs = isAbsolute(candidate) ? candidate : resolve(candidate) - if (existsSync(abs)) found = abs - } - return found -} - /** * Does drizzle-kit's output say `DATABASE_URL` is *absent*, as opposed to * present and wrong? diff --git a/languages/typescript/packages/cli/src/messages.ts b/languages/typescript/packages/cli/src/messages.ts index a6a5776c03..24a6117656 100644 --- a/languages/typescript/packages/cli/src/messages.ts +++ b/languages/typescript/packages/cli/src/messages.ts @@ -175,8 +175,12 @@ export const messages = { * config wins and the user needs to know which one their file landed in — * silently using the real one would leave `--out` looking honoured. */ - migrationDrizzleOutOverridden: (configured: string, requested: string) => - `drizzle-kit wrote into ${configured}, not the --out you passed (${requested}) — the \`out\` in your drizzle.config.ts decides where migrations go, and stash follows it so the journal stays consistent. Change it there if you want them elsewhere.`, + migrationDrizzleOutOverridden: ( + configured: string, + requested: string, + flag = '--out', + ) => + `drizzle-kit wrote into ${configured}, not the ${flag} you passed (${requested}) — the \`out\` in your drizzle.config.ts decides where migrations go, and stash follows it so the journal stays consistent. Change it there if you want them elsewhere.`, /** * `--name` with `--supabase`. The Supabase filename is fixed because * duplicate detection matches on the `_cipherstash_eql.sql` suffix, so the diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index f45c156bf9..4ea65c39ca 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -643,6 +643,8 @@ Runs from the `backfilled` phase and drops the original plaintext ``. It ve Flags: `--table`, `--column`, `--migrations-dir `. +On Drizzle projects the migration is journaled by `drizzle-kit generate --custom`, so it lands in the `out` directory of `drizzle.config.ts` (the path drizzle-kit reports). `--migrations-dir` is only a fallback there, and the command warns when drizzle-kit wrote elsewhere. + ### Decrypt The reverse lifecycle: moving encrypted columns back to plaintext, for leaving CipherStash or un-encrypting a column. It mirrors `encrypt` step for step, on the same `@cipherstash/migrate` engine, and records its own events in `cipherstash.cs_migrations` — keyed by the **encrypted** column, so they never mix with the forward history kept under the plaintext name. Decryption is never charged. @@ -720,6 +722,8 @@ Generating records `encrypted-dropped`, which stops a second drop being generate Flags: `--table`, `--column`, `--plaintext-column`, `--migrations-dir `, `--uninstall-eql`. +On Drizzle projects the migration is journaled by `drizzle-kit generate --custom`, so it lands in the `out` directory of `drizzle.config.ts` (the path drizzle-kit reports). `--migrations-dir` is only a fallback there, and the command warns when drizzle-kit wrote elsewhere. + ### Deployment #### `env`