@coreframe/scripts 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/bin/coreframe.js +35 -0
  2. package/config.test.ts +15 -0
  3. package/config.ts +9 -0
  4. package/coreframe.ts +272 -0
  5. package/db/client.ts +72 -0
  6. package/db/config.ts +41 -0
  7. package/db/drizzle-kit.test.ts +27 -0
  8. package/db/drizzle-kit.ts +39 -0
  9. package/db/ensure.ts +67 -0
  10. package/db/migrate-held.test.ts +81 -0
  11. package/db/migrate-held.ts +166 -0
  12. package/db/migrate.ts +39 -0
  13. package/db/project.ts +28 -0
  14. package/db/reset.ts +24 -0
  15. package/db/seed.ts +38 -0
  16. package/db/setup.ts +18 -0
  17. package/deploy/check-migrations.ts +31 -0
  18. package/deploy/checks.ts +38 -0
  19. package/deploy/config.test.ts +24 -0
  20. package/deploy/config.ts +68 -0
  21. package/deploy/migrations.test.ts +110 -0
  22. package/deploy/migrations.ts +317 -0
  23. package/deploy/pending-migrations.test.ts +93 -0
  24. package/deploy/pending-migrations.ts +45 -0
  25. package/deploy/tauri-release.test.ts +196 -0
  26. package/deploy/tauri-release.ts +816 -0
  27. package/deploy/version.test.ts +29 -0
  28. package/deploy/version.ts +65 -0
  29. package/deploy.test.ts +101 -0
  30. package/deploy.ts +346 -0
  31. package/dev.test.ts +153 -0
  32. package/dev.ts +155 -0
  33. package/dist/config.d.ts +5 -0
  34. package/dist/config.js +3 -0
  35. package/dist/coreframe.d.ts +280 -0
  36. package/dist/coreframe.js +240 -0
  37. package/dist/db/client.d.ts +22 -0
  38. package/dist/db/client.js +29 -0
  39. package/dist/db/config.d.ts +21 -0
  40. package/dist/db/config.js +32 -0
  41. package/dist/db/drizzle-kit.d.ts +8 -0
  42. package/dist/db/drizzle-kit.js +22 -0
  43. package/dist/db/ensure.d.ts +2 -0
  44. package/dist/db/ensure.js +40 -0
  45. package/dist/db/migrate-held.d.ts +12 -0
  46. package/dist/db/migrate-held.js +119 -0
  47. package/dist/db/migrate.d.ts +2 -0
  48. package/dist/db/migrate.js +25 -0
  49. package/dist/db/project.d.ts +8 -0
  50. package/dist/db/project.js +9 -0
  51. package/dist/db/reset.d.ts +2 -0
  52. package/dist/db/reset.js +19 -0
  53. package/dist/db/seed.d.ts +3 -0
  54. package/dist/db/seed.js +23 -0
  55. package/dist/db/setup.d.ts +3 -0
  56. package/dist/db/setup.js +15 -0
  57. package/dist/deploy/check-migrations.d.ts +2 -0
  58. package/dist/deploy/check-migrations.js +18 -0
  59. package/dist/deploy/checks.d.ts +2 -0
  60. package/dist/deploy/checks.js +27 -0
  61. package/dist/deploy/config.d.ts +50 -0
  62. package/dist/deploy/config.js +9 -0
  63. package/dist/deploy/migrations.d.ts +21 -0
  64. package/dist/deploy/migrations.js +203 -0
  65. package/dist/deploy/pending-migrations.d.ts +5 -0
  66. package/dist/deploy/pending-migrations.js +30 -0
  67. package/dist/deploy/tauri-release.d.ts +85 -0
  68. package/dist/deploy/tauri-release.js +512 -0
  69. package/dist/deploy/version.d.ts +18 -0
  70. package/dist/deploy/version.js +41 -0
  71. package/dist/deploy.d.ts +17 -0
  72. package/dist/deploy.js +247 -0
  73. package/dist/dev.d.ts +13 -0
  74. package/dist/dev.js +96 -0
  75. package/dist/image-generator.d.ts +2 -0
  76. package/dist/image-generator.js +59 -0
  77. package/dist/project.d.ts +1 -0
  78. package/dist/project.js +13 -0
  79. package/dist/typecheck.d.ts +2 -0
  80. package/dist/typecheck.js +45 -0
  81. package/dist/upgrade/check-skill-current.d.ts +6 -0
  82. package/dist/upgrade/check-skill-current.js +88 -0
  83. package/dist/upgrade/ensure-template-remote.d.ts +6 -0
  84. package/dist/upgrade/ensure-template-remote.js +57 -0
  85. package/dist/upgrade/fast-forward-template-files.d.ts +7 -0
  86. package/dist/upgrade/fast-forward-template-files.js +280 -0
  87. package/image-generator.ts +84 -0
  88. package/package.json +48 -8
  89. package/project.ts +6 -0
  90. package/tsconfig.build.json +12 -0
  91. package/tsconfig.json +20 -0
  92. package/typecheck.ts +55 -0
  93. package/upgrade/check-skill-current.ts +112 -0
  94. package/upgrade/ensure-template-remote.ts +79 -0
  95. package/upgrade/fast-forward-template-files.ts +408 -0
  96. package/readme.md +0 -1
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process"
3
+ import { existsSync } from "node:fs"
4
+ import { dirname, resolve } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+
7
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
8
+ const distBin = resolve(packageRoot, "dist/coreframe.js")
9
+
10
+ if (!existsSync(distBin)) {
11
+ const build = spawnSync("pnpm", ["--dir", packageRoot, "build"], {
12
+ shell: process.platform === "win32",
13
+ stdio: "inherit",
14
+ })
15
+
16
+ if (build.error) {
17
+ console.error(build.error.message)
18
+ process.exit(1)
19
+ }
20
+
21
+ if (build.status !== 0) {
22
+ process.exit(build.status ?? 1)
23
+ }
24
+ }
25
+
26
+ const cli = spawnSync(process.execPath, [distBin, ...process.argv.slice(2)], {
27
+ stdio: "inherit",
28
+ })
29
+
30
+ if (cli.error) {
31
+ console.error(cli.error.message)
32
+ process.exit(1)
33
+ }
34
+
35
+ process.exit(cli.status ?? 1)
package/config.test.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { defineConfig } from "./config.ts"
4
+
5
+ describe("defineConfig", () => {
6
+ it("returns a typed Coreframe config object", () => {
7
+ const config = defineConfig({
8
+ deploy: {
9
+ smokeChecks: [{ name: "home", path: "/" }],
10
+ },
11
+ })
12
+
13
+ expect(config.deploy?.smokeChecks).toEqual([{ name: "home", path: "/" }])
14
+ })
15
+ })
package/config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { DeployConfig } from "./deploy/config.ts"
2
+
3
+ export type CoreframeConfig = {
4
+ deploy?: DeployConfig
5
+ }
6
+
7
+ export function defineConfig(config: CoreframeConfig) {
8
+ return config
9
+ }
package/coreframe.ts ADDED
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ array,
4
+ binary,
5
+ command,
6
+ flag,
7
+ multioption,
8
+ oneOf,
9
+ option,
10
+ optional,
11
+ positional,
12
+ rest,
13
+ run,
14
+ string,
15
+ subcommands,
16
+ } from "cmd-ts"
17
+
18
+ import { runDrizzleKitCommand } from "./db/drizzle-kit.ts"
19
+ import { ensureLocalDb } from "./db/ensure.ts"
20
+ import { migrateHeldMigrations } from "./db/migrate-held.ts"
21
+ import { migrateLocalDb } from "./db/migrate.ts"
22
+ import { resetLocalDb } from "./db/reset.ts"
23
+ import { seedLocalDb } from "./db/seed.ts"
24
+ import { setupLocalDb } from "./db/setup.ts"
25
+ import { deployProject } from "./deploy.ts"
26
+ import { checkDeployMigrations } from "./deploy/check-migrations.ts"
27
+ import { tauriReleasePlatforms } from "./deploy/config.ts"
28
+ import { runDevServer } from "./dev.ts"
29
+ import { typecheckProject } from "./typecheck.ts"
30
+ import { checkUpgradeSkillCurrent } from "./upgrade/check-skill-current.ts"
31
+ import { ensureTemplateRemote } from "./upgrade/ensure-template-remote.ts"
32
+ import { fastForwardTemplateFiles } from "./upgrade/fast-forward-template-files.ts"
33
+
34
+ const heldMigrationConfirmation = "run-held-migrations"
35
+ const devPlatforms = ["web", "desktop", "ios", "android"] as const
36
+
37
+ export const coreframeCli = subcommands({
38
+ name: "coreframe",
39
+ cmds: {
40
+ deploy: command({
41
+ name: "deploy",
42
+ args: {
43
+ runMigrations: flag({
44
+ long: "run-migrations",
45
+ description: "Run pending deploy-safe database migrations before deploying.",
46
+ defaultValue: () => false,
47
+ }),
48
+ skipMigrations: flag({
49
+ long: "skip-migrations",
50
+ description: "Deploy without running pending deploy-safe database migrations.",
51
+ defaultValue: () => false,
52
+ }),
53
+ skipPostDeployChecks: flag({
54
+ long: "skip-post-deploy-checks",
55
+ description: "Skip post-deploy checks after Cloudflare deployment.",
56
+ defaultValue: () => false,
57
+ }),
58
+ tauriLocalPlatforms: multioption({
59
+ long: "tauri-local-platform",
60
+ type: array(oneOf(tauriReleasePlatforms)),
61
+ description: "Build one Tauri release platform locally. Repeat for multiple platforms.",
62
+ defaultValue: () => [],
63
+ }),
64
+ tauriWorkflowName: option({
65
+ long: "tauri-workflow-name",
66
+ type: optional(string),
67
+ description: "GitHub Actions workflow file or name for remote Tauri release builds.",
68
+ }),
69
+ tauriWorkflowPlatforms: multioption({
70
+ long: "tauri-workflow-platform",
71
+ type: array(oneOf(tauriReleasePlatforms)),
72
+ description:
73
+ "Build one Tauri release platform with GitHub Actions. Repeat for multiple platforms.",
74
+ defaultValue: () => [],
75
+ }),
76
+ tauriWorkflowRef: option({
77
+ long: "tauri-workflow-ref",
78
+ type: optional(string),
79
+ description: "Git ref used when dispatching remote Tauri release builds.",
80
+ }),
81
+ yes: flag({
82
+ long: "yes",
83
+ short: "y",
84
+ description: "Approve noninteractive deploy prompts.",
85
+ defaultValue: () => false,
86
+ }),
87
+ },
88
+ handler: ({
89
+ runMigrations,
90
+ skipMigrations,
91
+ skipPostDeployChecks,
92
+ tauriLocalPlatforms,
93
+ tauriWorkflowName,
94
+ tauriWorkflowPlatforms,
95
+ tauriWorkflowRef,
96
+ yes,
97
+ }) =>
98
+ deployProject([
99
+ ...(runMigrations ? ["--run-migrations"] : []),
100
+ ...(skipMigrations ? ["--skip-migrations"] : []),
101
+ ...(skipPostDeployChecks ? ["--skip-post-deploy-checks"] : []),
102
+ ...tauriLocalPlatforms.flatMap((platform) => ["--tauri-local-platform", platform]),
103
+ ...(tauriWorkflowName ? ["--tauri-workflow-name", tauriWorkflowName] : []),
104
+ ...tauriWorkflowPlatforms.flatMap((platform) => ["--tauri-workflow-platform", platform]),
105
+ ...(tauriWorkflowRef ? ["--tauri-workflow-ref", tauriWorkflowRef] : []),
106
+ ...(yes ? ["--yes"] : []),
107
+ ]),
108
+ }),
109
+ typecheck: command({
110
+ name: "typecheck",
111
+ args: {},
112
+ handler: typecheckProject,
113
+ }),
114
+ dev: command({
115
+ name: "dev",
116
+ args: {
117
+ platform: positional({
118
+ type: optional(oneOf(devPlatforms)),
119
+ displayName: "platform",
120
+ description: "Development target platform. Defaults to web.",
121
+ }),
122
+ },
123
+ handler: ({ platform }) => runDevServer({ platform: platform ?? "web" }),
124
+ }),
125
+ upgrade: subcommands({
126
+ name: "upgrade",
127
+ cmds: {
128
+ "ensure-template-remote": command({
129
+ name: "ensure-template-remote",
130
+ args: {
131
+ remote: option({
132
+ long: "remote",
133
+ type: string,
134
+ defaultValue: () => "template",
135
+ description: "Remote name to ensure.",
136
+ }),
137
+ noFetch: flag({
138
+ long: "no-fetch",
139
+ description: "Do not fetch the remote after ensuring it exists.",
140
+ defaultValue: () => false,
141
+ }),
142
+ },
143
+ handler: ({ noFetch, remote }) => ensureTemplateRemote({ fetch: !noFetch, remote }),
144
+ }),
145
+ "check-skill-current": command({
146
+ name: "check-skill-current",
147
+ args: {
148
+ target: option({
149
+ long: "target",
150
+ type: string,
151
+ description: "Target Coreframe template git ref.",
152
+ }),
153
+ apply: flag({
154
+ long: "apply",
155
+ description:
156
+ "Replace the local upgrade skill with the target version when it differs.",
157
+ defaultValue: () => false,
158
+ }),
159
+ },
160
+ handler: ({ apply, target }) => {
161
+ process.exitCode = checkUpgradeSkillCurrent({ apply, target })
162
+ },
163
+ }),
164
+ "fast-forward-template-files": command({
165
+ name: "fast-forward-template-files",
166
+ args: {
167
+ target: option({
168
+ long: "target",
169
+ type: string,
170
+ description: "Target Coreframe template git ref.",
171
+ }),
172
+ base: option({
173
+ long: "base",
174
+ type: optional(string),
175
+ description:
176
+ "Previous Coreframe template git ref. Defaults to package.json engines.coreframe.",
177
+ }),
178
+ apply: flag({
179
+ long: "apply",
180
+ description: "Write clean fast-forwards to the worktree.",
181
+ defaultValue: () => false,
182
+ }),
183
+ },
184
+ handler: ({ apply, base, target }) => fastForwardTemplateFiles({ apply, base, target }),
185
+ }),
186
+ },
187
+ }),
188
+ db: subcommands({
189
+ name: "db",
190
+ cmds: {
191
+ ensure: command({
192
+ name: "ensure",
193
+ args: {},
194
+ handler: ensureLocalDb,
195
+ }),
196
+ generate: command({
197
+ name: "generate",
198
+ args: {
199
+ args: rest({
200
+ displayName: "drizzle-kit args",
201
+ description: "Additional arguments passed to drizzle-kit generate.",
202
+ }),
203
+ },
204
+ handler: ({ args }) => runDrizzleKitCommand("generate", args),
205
+ }),
206
+ migrate: command({
207
+ name: "migrate",
208
+ args: {
209
+ args: rest({
210
+ displayName: "drizzle-kit args",
211
+ description: "Additional arguments passed to drizzle-kit migrate.",
212
+ }),
213
+ },
214
+ handler: ({ args }) => runDrizzleKitCommand("migrate", args),
215
+ }),
216
+ "migrate-held": command({
217
+ name: "migrate-held",
218
+ args: {
219
+ confirm: option({
220
+ long: "confirm",
221
+ type: optional(oneOf([heldMigrationConfirmation] as const)),
222
+ description: "Required confirmation value for held migrations.",
223
+ }),
224
+ },
225
+ handler: ({ confirm }) =>
226
+ migrateHeldMigrations({
227
+ argv: confirm ? [`--confirm=${confirm}`] : [],
228
+ }),
229
+ }),
230
+ "migrate-local": command({
231
+ name: "migrate-local",
232
+ args: {},
233
+ handler: migrateLocalDb,
234
+ }),
235
+ "migrations-check": command({
236
+ name: "migrations-check",
237
+ args: {},
238
+ handler: checkDeployMigrations,
239
+ }),
240
+ reset: command({
241
+ name: "reset",
242
+ args: {},
243
+ handler: resetLocalDb,
244
+ }),
245
+ seed: command({
246
+ name: "seed",
247
+ args: {},
248
+ handler: () => seedLocalDb(),
249
+ }),
250
+ setup: command({
251
+ name: "setup",
252
+ args: {},
253
+ handler: () => setupLocalDb(),
254
+ }),
255
+ studio: command({
256
+ name: "studio",
257
+ args: {
258
+ args: rest({
259
+ displayName: "drizzle-kit args",
260
+ description: "Additional arguments passed to drizzle-kit studio.",
261
+ }),
262
+ },
263
+ handler: ({ args }) => runDrizzleKitCommand("studio", args),
264
+ }),
265
+ },
266
+ }),
267
+ },
268
+ })
269
+
270
+ if (import.meta.main) {
271
+ run(binary(coreframeCli), process.argv)
272
+ }
package/db/client.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { sql, type SQL } from "drizzle-orm"
2
+
3
+ export type CoreframeDatabaseDialect = "postgres" | "sqlite"
4
+
5
+ export type CoreframeQueryResult<Row extends Record<string, unknown> = Record<string, unknown>> = {
6
+ rowCount?: number | null
7
+ rows: Row[]
8
+ }
9
+
10
+ export type CoreframeDatabaseClient = {
11
+ dialect: CoreframeDatabaseDialect
12
+ execute<Row extends Record<string, unknown> = Record<string, unknown>>(
13
+ statement: string,
14
+ ): Promise<CoreframeQueryResult<Row>>
15
+ transaction?<Result>(run: (database: CoreframeDatabaseClient) => Promise<Result>): Promise<Result>
16
+ close?(): Promise<void>
17
+ }
18
+
19
+ export type CoreframeDrizzleExecutor = {
20
+ execute<Result = unknown>(query: SQL): Result | Promise<Result>
21
+ }
22
+
23
+ export type CreateDrizzleDatabaseClientOptions = {
24
+ close?(): Promise<void>
25
+ database: CoreframeDrizzleExecutor
26
+ dialect: CoreframeDatabaseDialect
27
+ }
28
+
29
+ export function createDrizzleDatabaseClient({
30
+ close,
31
+ database,
32
+ dialect,
33
+ }: CreateDrizzleDatabaseClientOptions): CoreframeDatabaseClient {
34
+ return {
35
+ dialect,
36
+ async execute<Row extends Record<string, unknown> = Record<string, unknown>>(
37
+ statement: string,
38
+ ) {
39
+ return normalizeQueryResult<Row>(await database.execute(sql.raw(statement)))
40
+ },
41
+ close,
42
+ }
43
+ }
44
+
45
+ export function normalizeQueryResult<Row extends Record<string, unknown> = Record<string, unknown>>(
46
+ result: unknown,
47
+ ): CoreframeQueryResult<Row> {
48
+ if (Array.isArray(result)) {
49
+ return {
50
+ rows: result as Row[],
51
+ }
52
+ }
53
+
54
+ if (isQueryResultLike<Row>(result)) {
55
+ return {
56
+ rowCount: typeof result.rowCount === "number" ? result.rowCount : null,
57
+ rows: result.rows,
58
+ }
59
+ }
60
+
61
+ return {
62
+ rows: [],
63
+ }
64
+ }
65
+
66
+ function isQueryResultLike<Row extends Record<string, unknown>>(
67
+ result: unknown,
68
+ ): result is { rowCount?: unknown; rows: Row[] } {
69
+ return (
70
+ typeof result === "object" && result !== null && "rows" in result && Array.isArray(result.rows)
71
+ )
72
+ }
package/db/config.ts ADDED
@@ -0,0 +1,41 @@
1
+ export const LOCAL_DB_DIR = process.env.PGLITE_DATA_DIR ?? ".pglite"
2
+ export const MIGRATIONS_FOLDER = "drizzle"
3
+ export const HELD_MIGRATIONS_FOLDER = "drizzle-hold"
4
+ export const NOW = new Date("2026-01-15T12:00:00.000Z")
5
+
6
+ export const SEED_IDS = {
7
+ adminUser: "00000000-0000-4000-8000-000000000001",
8
+ viewerUser: "00000000-0000-4000-8000-000000000002",
9
+ inactiveUser: "00000000-0000-4000-8000-000000000003",
10
+ unicodeUser: "00000000-0000-4000-8000-000000000004",
11
+ longNameUser: "00000000-0000-4000-8000-000000000005",
12
+ } as const
13
+
14
+ export const SEED_EMAILS = {
15
+ adminUser: "admin@example.com",
16
+ viewerUser: "viewer@example.com",
17
+ inactiveUser: "inactive@example.com",
18
+ unicodeUser: "unicode@example.com",
19
+ longNameUser: "long-name@example.com",
20
+ } as const
21
+
22
+ export type SeedProfile = "small" | "large" | "demo" | "test"
23
+
24
+ export function getSeedProfile(argv = process.argv.slice(2)): SeedProfile {
25
+ if (argv.includes("--large")) {
26
+ return "large"
27
+ }
28
+
29
+ const profileArg = argv.find((arg) => arg.startsWith("--profile="))
30
+ const profile = profileArg?.slice("--profile=".length) ?? "small"
31
+
32
+ if (profile === "small" || profile === "large" || profile === "demo" || profile === "test") {
33
+ return profile
34
+ }
35
+
36
+ throw new Error(`Unknown seed profile "${profile}". Expected small, large, demo, or test.`)
37
+ }
38
+
39
+ export function generatedUserId(index: number) {
40
+ return `00000000-0000-4000-8000-${String(1000 + index).padStart(12, "0")}`
41
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { runDrizzleKitCommand } from "./drizzle-kit.ts"
4
+
5
+ describe("runDrizzleKitCommand", () => {
6
+ it("forwards drizzle-kit commands through the project binary", async () => {
7
+ const calls: unknown[] = []
8
+
9
+ await runDrizzleKitCommand("generate", ["--config", "drizzle.config.ts"], async (...args) => {
10
+ calls.push(args)
11
+ return { stdout: "", stderr: "", exitCode: 0 }
12
+ })
13
+
14
+ expect(calls).toEqual([
15
+ [
16
+ process.platform === "win32" ? "drizzle-kit.cmd" : "drizzle-kit",
17
+ ["generate", "--config", "drizzle.config.ts"],
18
+ {
19
+ env: expect.objectContaining({
20
+ PATH: expect.stringContaining("node_modules/.bin"),
21
+ }),
22
+ stdio: "inherit",
23
+ },
24
+ ],
25
+ ])
26
+ })
27
+ })
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path"
3
+ import process from "node:process"
4
+
5
+ import nanoSpawn from "nano-spawn"
6
+
7
+ type Spawn = (
8
+ file: string,
9
+ args: readonly string[],
10
+ options: { env: NodeJS.ProcessEnv; stdio: "inherit" },
11
+ ) => PromiseLike<unknown>
12
+ type DrizzleKitCommand = "generate" | "migrate" | "studio"
13
+
14
+ const nodeModulesBinPath = path.resolve("node_modules", ".bin")
15
+ const drizzleKitCommand = process.platform === "win32" ? "drizzle-kit.cmd" : "drizzle-kit"
16
+ const pathEnvValue = process.env.PATH ?? process.env.Path ?? ""
17
+
18
+ export async function runDrizzleKitCommand(
19
+ command: DrizzleKitCommand,
20
+ args: string[] = [],
21
+ spawn: Spawn = nanoSpawn,
22
+ ) {
23
+ await spawn(drizzleKitCommand, [command, ...args], {
24
+ env: {
25
+ ...process.env,
26
+ PATH: nodeModulesBinPath + path.delimiter + pathEnvValue,
27
+ },
28
+ stdio: "inherit",
29
+ })
30
+ }
31
+
32
+ if (import.meta.main) {
33
+ runDrizzleKitCommand(process.argv[2] as DrizzleKitCommand, process.argv.slice(3)).catch(
34
+ (error) => {
35
+ console.error(error)
36
+ process.exit(1)
37
+ },
38
+ )
39
+ }
package/db/ensure.ts ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { eq } from "drizzle-orm"
3
+
4
+ import { importProjectModule } from "../project.ts"
5
+ import { LOCAL_DB_DIR, SEED_EMAILS, SEED_IDS } from "./config.ts"
6
+ import { setupLocalDb } from "./setup.ts"
7
+
8
+ type LocalDbModule = {
9
+ createLocalDb(dataDir?: string): {
10
+ $client: {
11
+ close(): Promise<void>
12
+ }
13
+ select(selection: Record<string, unknown>): {
14
+ from(table: unknown): {
15
+ where(condition: unknown): {
16
+ limit(count: number): Promise<Array<{ id: string }>>
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ type SchemaModule = {
24
+ users: {
25
+ email: unknown
26
+ id: unknown
27
+ }
28
+ }
29
+
30
+ async function hasSeededAdminUser() {
31
+ const [{ createLocalDb }, { users }] = await Promise.all([
32
+ importProjectModule<LocalDbModule>("src/db/local.ts"),
33
+ importProjectModule<SchemaModule>("src/db/schema.ts"),
34
+ ])
35
+ const db = createLocalDb(LOCAL_DB_DIR)
36
+
37
+ try {
38
+ const [adminUser] = await db
39
+ .select({ id: users.id })
40
+ .from(users)
41
+ .where(eq(users.email as never, SEED_EMAILS.adminUser))
42
+ .limit(1)
43
+
44
+ return adminUser?.id === SEED_IDS.adminUser
45
+ } catch {
46
+ return false
47
+ } finally {
48
+ await db.$client.close()
49
+ }
50
+ }
51
+
52
+ export async function ensureLocalDb() {
53
+ if (await hasSeededAdminUser()) {
54
+ console.log("Local database already set up")
55
+ return
56
+ }
57
+
58
+ console.log("Local database is missing or unseeded; running setup")
59
+ await setupLocalDb()
60
+ }
61
+
62
+ if (import.meta.main) {
63
+ ensureLocalDb().catch((error) => {
64
+ console.error(error)
65
+ process.exit(1)
66
+ })
67
+ }
@@ -0,0 +1,81 @@
1
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+
5
+ import { describe, expect, it } from "vitest"
6
+
7
+ import type { CoreframeDatabaseClient, CoreframeDatabaseDialect } from "./client.ts"
8
+ import {
9
+ heldMigrationInsertQuery,
10
+ heldMigrationSelectQuery,
11
+ migrateHeldMigrations,
12
+ } from "./migrate-held.ts"
13
+
14
+ describe("held migration journal queries", () => {
15
+ it("uses dialect-specific held migration journal tables", () => {
16
+ expect(heldMigrationSelectQuery("postgres", "20260703120000_first")).toBe(
17
+ "select sha256 from drizzle.__held_migrations where id = '20260703120000_first'",
18
+ )
19
+ expect(heldMigrationSelectQuery("sqlite", "20260703120000_first")).toBe(
20
+ "select sha256 from __held_migrations where id = '20260703120000_first'",
21
+ )
22
+ expect(heldMigrationInsertQuery("sqlite", "migration's id", "abc123")).toBe(
23
+ "insert into __held_migrations (id, sha256) values ('migration''s id', 'abc123')",
24
+ )
25
+ })
26
+ })
27
+
28
+ describe("migrateHeldMigrations", () => {
29
+ it("runs held migrations through a SQLite database adapter", async () => {
30
+ const root = await mkdtemp(join(tmpdir(), "coreframe-held-migrations-"))
31
+ await mkdir(join(root, "drizzle-hold"), { recursive: true })
32
+ await writeFile(
33
+ join(root, "drizzle-hold", "20260703120000_backfill.sql"),
34
+ `-- Held migration: test backfill
35
+ -- Safe after: the test says so
36
+ -- Verification: inspect the rows
37
+ insert into users (id) values ('user-1');`,
38
+ )
39
+ const database = createRecordingDatabase("sqlite")
40
+ const cwd = process.cwd()
41
+
42
+ try {
43
+ process.chdir(root)
44
+ await migrateHeldMigrations({
45
+ argv: ["--confirm=run-held-migrations"],
46
+ database,
47
+ })
48
+ } finally {
49
+ process.chdir(cwd)
50
+ }
51
+
52
+ expect(database.statements).toEqual([
53
+ expect.stringContaining("create table if not exists __held_migrations"),
54
+ "select sha256 from __held_migrations where id = '20260703120000_backfill'",
55
+ "begin",
56
+ expect.stringContaining("insert into users"),
57
+ expect.stringMatching(
58
+ /^insert into __held_migrations \(id, sha256\) values \('20260703120000_backfill', '[a-f0-9]{64}'\)$/,
59
+ ),
60
+ "commit",
61
+ ])
62
+ })
63
+ })
64
+
65
+ function createRecordingDatabase(
66
+ dialect: CoreframeDatabaseDialect,
67
+ ): CoreframeDatabaseClient & { statements: string[] } {
68
+ const statements: string[] = []
69
+
70
+ return {
71
+ dialect,
72
+ statements,
73
+ async execute(statement) {
74
+ statements.push(statement)
75
+
76
+ return {
77
+ rows: [],
78
+ }
79
+ },
80
+ }
81
+ }