@supatype/cli 0.1.12 → 0.2.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 (143) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +150 -136
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/cli-version-embedded.d.ts.map +1 -1
  5. package/dist/cli-version-embedded.js +1 -1
  6. package/dist/cli-version-embedded.js.map +1 -1
  7. package/dist/commands/admin.d.ts +5 -5
  8. package/dist/commands/admin.d.ts.map +1 -1
  9. package/dist/commands/admin.js +10 -10
  10. package/dist/commands/admin.js.map +1 -1
  11. package/dist/commands/db.d.ts.map +1 -1
  12. package/dist/commands/db.js +23 -1
  13. package/dist/commands/db.js.map +1 -1
  14. package/dist/commands/dev.js +51 -51
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doctor.d.ts +0 -7
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +26 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/init.js +1 -1
  21. package/dist/commands/logs.d.ts +14 -1
  22. package/dist/commands/logs.d.ts.map +1 -1
  23. package/dist/commands/logs.js +33 -37
  24. package/dist/commands/logs.js.map +1 -1
  25. package/dist/commands/push.d.ts.map +1 -1
  26. package/dist/commands/push.js +18 -7
  27. package/dist/commands/push.js.map +1 -1
  28. package/dist/commands/status.d.ts +1 -1
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +33 -64
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/compose-local-server-image.d.ts +11 -0
  33. package/dist/compose-local-server-image.d.ts.map +1 -1
  34. package/dist/compose-local-server-image.js +18 -0
  35. package/dist/compose-local-server-image.js.map +1 -1
  36. package/dist/compose-services.d.ts +59 -0
  37. package/dist/compose-services.d.ts.map +1 -0
  38. package/dist/compose-services.js +160 -0
  39. package/dist/compose-services.js.map +1 -0
  40. package/dist/config.d.ts +1 -1
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/dev-compose.d.ts +1 -0
  43. package/dist/dev-compose.d.ts.map +1 -1
  44. package/dist/dev-compose.js +202 -30
  45. package/dist/dev-compose.js.map +1 -1
  46. package/dist/dev-ports.d.ts +24 -0
  47. package/dist/dev-ports.d.ts.map +1 -1
  48. package/dist/dev-ports.js +37 -0
  49. package/dist/dev-ports.js.map +1 -1
  50. package/dist/docker-postgres.d.ts +1 -1
  51. package/dist/docker-postgres.d.ts.map +1 -1
  52. package/dist/docker-postgres.js +2 -2
  53. package/dist/docker-postgres.js.map +1 -1
  54. package/dist/engine-floor.d.ts +50 -0
  55. package/dist/engine-floor.d.ts.map +1 -0
  56. package/dist/engine-floor.js +80 -0
  57. package/dist/engine-floor.js.map +1 -0
  58. package/dist/field-bounds.d.ts +68 -0
  59. package/dist/field-bounds.d.ts.map +1 -0
  60. package/dist/field-bounds.js +277 -0
  61. package/dist/field-bounds.js.map +1 -0
  62. package/dist/hooks-generator.d.ts +1 -1
  63. package/dist/hooks-generator.d.ts.map +1 -1
  64. package/dist/hooks-generator.js +78 -4
  65. package/dist/hooks-generator.js.map +1 -1
  66. package/dist/link.d.ts +1 -1
  67. package/dist/link.d.ts.map +1 -1
  68. package/dist/model-hooks.d.ts +44 -2
  69. package/dist/model-hooks.d.ts.map +1 -1
  70. package/dist/model-hooks.js +116 -12
  71. package/dist/model-hooks.js.map +1 -1
  72. package/dist/pgbouncer-userlist.d.ts +1 -1
  73. package/dist/pgbouncer-userlist.d.ts.map +1 -1
  74. package/dist/pgbouncer-userlist.js +1 -1
  75. package/dist/pgbouncer-userlist.js.map +1 -1
  76. package/dist/postgres-ctl.d.ts +5 -1
  77. package/dist/postgres-ctl.d.ts.map +1 -1
  78. package/dist/postgres-ctl.js +43 -4
  79. package/dist/postgres-ctl.js.map +1 -1
  80. package/dist/project-config.d.ts +4 -4
  81. package/dist/resolve-target.d.ts +1 -1
  82. package/dist/resolve-target.d.ts.map +1 -1
  83. package/dist/schema-ast-v2.d.ts +38 -4
  84. package/dist/schema-ast-v2.d.ts.map +1 -1
  85. package/dist/schema-ast-v2.js +87 -4
  86. package/dist/schema-ast-v2.js.map +1 -1
  87. package/dist/self-host-compose.d.ts.map +1 -1
  88. package/dist/self-host-compose.js +41 -18
  89. package/dist/self-host-compose.js.map +1 -1
  90. package/dist/studio-dev-server.d.ts +8 -1
  91. package/dist/studio-dev-server.d.ts.map +1 -1
  92. package/dist/studio-dev-server.js +1 -2
  93. package/dist/studio-dev-server.js.map +1 -1
  94. package/dist/type-extractor.d.ts.map +1 -1
  95. package/dist/type-extractor.js +309 -27
  96. package/dist/type-extractor.js.map +1 -1
  97. package/package.json +4 -3
  98. package/src/cli-version-embedded.ts +1 -1
  99. package/src/commands/admin.ts +10 -10
  100. package/src/commands/db.ts +27 -1
  101. package/src/commands/dev.ts +51 -51
  102. package/src/commands/doctor.ts +30 -0
  103. package/src/commands/init.ts +1 -1
  104. package/src/commands/logs.ts +34 -37
  105. package/src/commands/push.ts +29 -6
  106. package/src/commands/status.ts +34 -70
  107. package/src/compose-local-server-image.ts +17 -0
  108. package/src/compose-services.ts +174 -0
  109. package/src/config.ts +1 -1
  110. package/src/dev-compose.ts +226 -29
  111. package/src/dev-ports.ts +48 -0
  112. package/src/docker-postgres.ts +2 -2
  113. package/src/engine-floor.ts +113 -0
  114. package/src/field-bounds.ts +359 -0
  115. package/src/hooks-generator.ts +81 -4
  116. package/src/link.ts +1 -1
  117. package/src/model-hooks.ts +158 -12
  118. package/src/pgbouncer-userlist.ts +1 -1
  119. package/src/postgres-ctl.ts +44 -4
  120. package/src/project-config.ts +4 -4
  121. package/src/resolve-target.ts +1 -1
  122. package/src/schema-ast-v2.ts +114 -10
  123. package/src/self-host-compose.ts +41 -18
  124. package/src/studio-dev-server.ts +9 -3
  125. package/src/type-extractor.ts +374 -39
  126. package/tests/admin-ensure.test.ts +10 -10
  127. package/tests/compose-parsable-defaults.test.ts +60 -0
  128. package/tests/compose-services.test.ts +144 -0
  129. package/tests/dev-ports-owner.test.ts +40 -0
  130. package/tests/engine-floor.test.ts +102 -0
  131. package/tests/external-database-compose.test.ts +3 -3
  132. package/tests/field-bounds-matrix.test.ts +163 -0
  133. package/tests/field-bounds.test.ts +139 -0
  134. package/tests/field-validators.test.ts +139 -0
  135. package/tests/hooks-generator.test.ts +86 -0
  136. package/tests/local-server-image-env.test.ts +93 -0
  137. package/tests/model-constraints.test.ts +293 -0
  138. package/tests/model-hooks.test.ts +56 -0
  139. package/tests/port-in-use.test.ts +58 -0
  140. package/tests/readiness-poll.test.ts +84 -0
  141. package/tests/runtime-contract.test.ts +1 -1
  142. package/tests/type-extractor.test.ts +49 -0
  143. package/tsconfig.tsbuildinfo +1 -1
@@ -65,21 +65,21 @@ import {
65
65
  isPortInUse,
66
66
  pgSpawnEnv,
67
67
  } from "../postgres-ctl.js"
68
- /** Map `email.smtp` from supatype.config.ts into GOTRUE_SMTP_* for the embedded GoTrue process. */
69
- function gotrueSMTPFromEmailConfig(email: SupatypeProjectConfig["email"] | undefined): Record<string, string> {
68
+ /** Map `email.smtp` from supatype.config.ts into SUPATYPE_SMTP_* for the embedded auth service. */
69
+ function authSMTPFromEmailConfig(email: SupatypeProjectConfig["email"] | undefined): Record<string, string> {
70
70
  const s = email?.smtp
71
71
  if (!s) return {}
72
72
  const out: Record<string, string> = {}
73
73
  const host = s.host?.trim()
74
- if (host) out.GOTRUE_SMTP_HOST = host
75
- if (s.port !== undefined) out.GOTRUE_SMTP_PORT = String(s.port)
74
+ if (host) out.SUPATYPE_SMTP_HOST = host
75
+ if (s.port !== undefined) out.SUPATYPE_SMTP_PORT = String(s.port)
76
76
  const user = s.user?.trim()
77
- if (user) out.GOTRUE_SMTP_USER = user
78
- if (s.pass !== undefined && s.pass !== "") out.GOTRUE_SMTP_PASS = s.pass
77
+ if (user) out.SUPATYPE_SMTP_USER = user
78
+ if (s.pass !== undefined && s.pass !== "") out.SUPATYPE_SMTP_PASS = s.pass
79
79
  const admin = s.admin_email?.trim()
80
- if (admin) out.GOTRUE_SMTP_ADMIN_EMAIL = admin
80
+ if (admin) out.SUPATYPE_SMTP_ADMIN_EMAIL = admin
81
81
  const sender = s.sender_name?.trim()
82
- if (sender) out.GOTRUE_SMTP_SENDER_NAME = sender
82
+ if (sender) out.SUPATYPE_SMTP_SENDER_NAME = sender
83
83
  return out
84
84
  }
85
85
 
@@ -287,10 +287,10 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
287
287
  ? `${dbURL}&search_path=auth`
288
288
  : `${dbURL}?search_path=auth`
289
289
 
290
- // ── 8. GoTrue migrations (auth.users before engine studio SQL) ─────────
291
- console.log("[supatype] Running GoTrue migrations...")
292
- const migrateEnv = gotrueMigrateEnv(serverPort, dbURL, LOCAL_JWT_SECRET)
293
- runGotrueMigrations(serverBin, migrateEnv)
290
+ // ── 8. auth migrations (auth.users before engine studio SQL) ─────────
291
+ console.log("[supatype] Running auth migrations...")
292
+ const migrateEnv = authMigrateEnv(serverPort, dbURL, LOCAL_JWT_SECRET)
293
+ runAuthMigrations(serverBin, migrateEnv)
294
294
 
295
295
  // ── 9. Engine: apply schema ───────────────────────────────────────────
296
296
  const schemaPath = schemaPathFromProject(config, cwd)
@@ -348,7 +348,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
348
348
  }
349
349
  }
350
350
 
351
- // Matches GOTRUE_HOOK_SEND_EMAIL_SECRETS symmetric format (dev only). Override via .env.
351
+ // Matches SUPATYPE_HOOK_SEND_EMAIL_SECRETS symmetric format (dev only). Override via .env.
352
352
  const LOCAL_SEND_EMAIL_HOOK_SECRETS =
353
353
  "v1,whsec_abcdefghijklmnopqrstuvwxyz01234567"
354
354
  const now = Math.floor(Date.now() / 1000)
@@ -390,7 +390,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
390
390
 
391
391
 
392
392
  const emailProvider = config.email?.provider ?? "console"
393
- const gotrueMailerProvider =
393
+ const authMailerProvider =
394
394
  emailProvider === "console"
395
395
  ? "console"
396
396
  : emailProvider === "resend"
@@ -430,47 +430,47 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
430
430
  ...(config.app.vite_dev_url !== undefined && config.app.vite_dev_url.trim() !== ""
431
431
  ? { SUPATYPE_VITE_DEV_URL: config.app.vite_dev_url.trim() }
432
432
  : {}),
433
- // GoTrue required fields (sensible local-dev defaults)
434
- GOTRUE_DB_DATABASE_URL: authDbURL,
433
+ // Auth service required fields (sensible local-dev defaults)
434
+ SUPATYPE_DB_DATABASE_URL: authDbURL,
435
435
  DATABASE_URL: authDbURL,
436
436
  SUPATYPE_SQL_DATABASE_URL: dbURL,
437
437
  PGSSLMODE: "disable",
438
- GOTRUE_DB_NAMESPACE: "auth",
439
- GOTRUE_DB_DRIVER: "postgres",
440
- GOTRUE_JWT_SECRET: LOCAL_JWT_SECRET,
441
- GOTRUE_JWT_EXP: "3600",
442
- GOTRUE_JWT_AUD: "authenticated",
443
- GOTRUE_JWT_ADMIN_ROLES: "supatype_admin,service_role",
444
- API_EXTERNAL_URL: `http://localhost:${serverPort}/auth/v1`,
445
- GOTRUE_API_HOST: "localhost",
446
- GOTRUE_SITE_URL: `http://localhost:${serverPort}`,
447
- GOTRUE_MAILER_MAILER_PROVIDER: gotrueMailerProvider,
448
- GOTRUE_MAILER_AUTOCONFIRM: "true",
449
- GOTRUE_LOG_LEVEL: "info",
450
- GOTRUE_DISABLE_SIGNUP: "false",
438
+ SUPATYPE_DB_NAMESPACE: "auth",
439
+ SUPATYPE_DB_DRIVER: "postgres",
440
+ SUPATYPE_JWT_SECRET: LOCAL_JWT_SECRET,
441
+ SUPATYPE_JWT_EXP: "3600",
442
+ SUPATYPE_JWT_AUD: "authenticated",
443
+ SUPATYPE_JWT_ADMIN_ROLES: "supatype_admin,service_role",
444
+ SUPATYPE_API_EXTERNAL_URL: `http://localhost:${serverPort}/auth/v1`,
445
+ SUPATYPE_API_HOST: "localhost",
446
+ SUPATYPE_SITE_URL: `http://localhost:${serverPort}`,
447
+ SUPATYPE_MAILER_MAILER_PROVIDER: authMailerProvider,
448
+ SUPATYPE_MAILER_AUTOCONFIRM: "true",
449
+ SUPATYPE_LOG_LEVEL: "info",
450
+ SUPATYPE_DISABLE_SIGNUP: "false",
451
451
  ...(config.email?.resend_api_key !== undefined && config.email.resend_api_key !== ""
452
452
  ? { RESEND_API_KEY: config.email.resend_api_key }
453
453
  : {}),
454
- ...(gotrueMailerProvider === "resend" &&
454
+ ...(authMailerProvider === "resend" &&
455
455
  config.email?.resend_from !== undefined &&
456
456
  config.email.resend_from.trim() !== ""
457
457
  ? { RESEND_FROM: config.email.resend_from.trim() }
458
458
  : {}),
459
- ...(gotrueMailerProvider === "ses" &&
459
+ ...(authMailerProvider === "ses" &&
460
460
  config.email?.ses_from !== undefined &&
461
461
  config.email.ses_from.trim() !== ""
462
462
  ? { SES_FROM: config.email.ses_from.trim() }
463
463
  : {}),
464
- ...(gotrueMailerProvider === "smtp" ? gotrueSMTPFromEmailConfig(config.email) : {}),
464
+ ...(authMailerProvider === "smtp" ? authSMTPFromEmailConfig(config.email) : {}),
465
465
  ...(config.email?.send_email_hook === true
466
466
  ? {
467
- GOTRUE_HOOK_SEND_EMAIL_ENABLED: "true",
468
- GOTRUE_HOOK_SEND_EMAIL_URI:
467
+ SUPATYPE_HOOK_SEND_EMAIL_ENABLED: "true",
468
+ SUPATYPE_HOOK_SEND_EMAIL_URI:
469
469
  config.email?.send_email_hook_uri !== undefined &&
470
470
  config.email.send_email_hook_uri.trim() !== ""
471
471
  ? config.email.send_email_hook_uri.trim()
472
472
  : `http://127.0.0.1:${serverPort}/internal/v0hooks/send-email`,
473
- GOTRUE_HOOK_SEND_EMAIL_SECRETS:
473
+ SUPATYPE_HOOK_SEND_EMAIL_SECRETS:
474
474
  config.email?.send_email_hook_secrets !== undefined &&
475
475
  config.email.send_email_hook_secrets.trim() !== ""
476
476
  ? config.email.send_email_hook_secrets.trim()
@@ -526,7 +526,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
526
526
  PGRST_DB_ANON_ROLE: "anon",
527
527
  PGRST_SERVER_PORT: postgrestPort,
528
528
  PGRST_SERVER_HOST: "127.0.0.1",
529
- PGRST_JWT_SECRET: serverEnv["GOTRUE_JWT_SECRET"] ?? "",
529
+ PGRST_JWT_SECRET: serverEnv["SUPATYPE_JWT_SECRET"] ?? "",
530
530
  PGRST_LOG_LEVEL: "warn",
531
531
  // On Windows, PostgREST (MinGW/GHC binary) needs libpq.dll and
532
532
  // OpenSSL DLLs. Prepend a Postgres bin dir which bundles these
@@ -571,7 +571,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
571
571
  cwd,
572
572
  studioOverride,
573
573
  pidDir,
574
- serviceRoleKey,
574
+ anonKey,
575
575
  proxyTarget: `http://localhost:${serverPort}`,
576
576
  viteSupatypeUrl: `http://localhost:${studioPort}`,
577
577
  })
@@ -1079,30 +1079,30 @@ function adaptUnsupportedKinds(
1079
1079
  // .env loader
1080
1080
  // ---------------------------------------------------------------------------
1081
1081
 
1082
- /** Minimal GoTrue env for `migrate` (matches required fields in serverEnv below). */
1083
- function gotrueMigrateEnv(
1082
+ /** Minimal auth env for `migrate` (matches required fields in serverEnv below). */
1083
+ function authMigrateEnv(
1084
1084
  serverPort: string,
1085
1085
  sqlDbURL: string,
1086
1086
  jwtSecret: string,
1087
1087
  ): Record<string, string> {
1088
1088
  const base = `http://localhost:${serverPort}`
1089
1089
  return {
1090
- // envconfig: gotrue + DB.DATABASE_URL → GOTRUE_DB_DATABASE_URL
1091
- GOTRUE_DB_DATABASE_URL: sqlDbURL,
1090
+ // envconfig: the service prefix + DB.DATABASE_URL → SUPATYPE_DB_DATABASE_URL
1091
+ SUPATYPE_DB_DATABASE_URL: sqlDbURL,
1092
1092
  DATABASE_URL: sqlDbURL,
1093
- GOTRUE_DB_DRIVER: "postgres",
1094
- GOTRUE_DB_NAMESPACE: "auth",
1093
+ SUPATYPE_DB_DRIVER: "postgres",
1094
+ SUPATYPE_DB_NAMESPACE: "auth",
1095
1095
  PGSSLMODE: "disable",
1096
- GOTRUE_JWT_SECRET: jwtSecret,
1097
- API_EXTERNAL_URL: `${base}/auth/v1`,
1098
- GOTRUE_API_HOST: "localhost",
1099
- GOTRUE_SITE_URL: base,
1100
- GOTRUE_MAILER_AUTOCONFIRM: "true",
1096
+ SUPATYPE_JWT_SECRET: jwtSecret,
1097
+ SUPATYPE_API_EXTERNAL_URL: `${base}/auth/v1`,
1098
+ SUPATYPE_API_HOST: "localhost",
1099
+ SUPATYPE_SITE_URL: base,
1100
+ SUPATYPE_MAILER_AUTOCONFIRM: "true",
1101
1101
  }
1102
1102
  }
1103
1103
 
1104
- /** Apply GoTrue DDL (auth.users, etc.) before engine push references auth schema. */
1105
- function runGotrueMigrations(
1104
+ /** Apply auth DDL (auth.users, etc.) before engine push references auth schema. */
1105
+ function runAuthMigrations(
1106
1106
  serverBin: string,
1107
1107
  migrateEnv: Record<string, string>,
1108
1108
  ): void {
@@ -1117,7 +1117,7 @@ function runGotrueMigrations(
1117
1117
  if (result.status !== 0) {
1118
1118
  const detail = (result.stderr ?? result.stdout ?? "").trim()
1119
1119
  throw new Error(
1120
- `GoTrue migrations failed (exit ${result.status ?? "unknown"})` +
1120
+ `auth migrations failed (exit ${result.status ?? "unknown"})` +
1121
1121
  (detail ? `:\n${detail}` : ""),
1122
1122
  )
1123
1123
  }
@@ -117,7 +117,37 @@ export function registerDoctor(program: Command): void {
117
117
  * stack with functions switched off, produces no error anywhere, the write just succeeds
118
118
  * unvalidated. Drift you cannot see is the thing doctor exists for.
119
119
  */
120
+ /**
121
+ * Field validators, reported separately from hooks because the consequence differs.
122
+ *
123
+ * A hook that never fires is a step that did not happen. A validator that never fires would be a
124
+ * field written unchecked, so the server refuses the write instead: these fields cannot be saved at
125
+ * all until the function exists, and the line has to say that rather than imply a silent gap.
126
+ */
127
+ function printFieldValidators(report: HooksReport): void {
128
+ if (report.validators.length === 0) return
129
+
130
+ plain(`\nField validators (${report.validators.length}):\n`)
131
+ for (const entry of report.validators) {
132
+ const broken = report.validatorsMissing.some(
133
+ (m) => m.model === entry.model && m.field === entry.field,
134
+ )
135
+ plain(` ${broken ? "✗" : "•"} ${entry.model}.${entry.field} → ${entry.function}`)
136
+ }
137
+
138
+ if (report.validatorsMissing.length > 0) {
139
+ plain("\n Those marked ✗ name a function that does not exist. An unreachable validator refuses")
140
+ plain(" the write, so these fields cannot be saved until the function is created.")
141
+ plain(" Create it with: supatype hooks new <name>")
142
+ }
143
+ if (report.validatorMapMissing) {
144
+ plain("\n .supatype/manifest.json carries no validator map, so no field is being checked.")
145
+ plain(" Run: supatype push")
146
+ }
147
+ }
148
+
120
149
  export function printHooks(report: HooksReport): void {
150
+ printFieldValidators(report)
121
151
  if (report.declared.length === 0) return
122
152
 
123
153
  plain(`\nHooks (${report.declared.length}):\n`)
@@ -1226,7 +1226,7 @@ JWT_SECRET=${randomBytes(48).toString("base64url")}
1226
1226
  ANON_KEY=
1227
1227
  SERVICE_ROLE_KEY=`)
1228
1228
 
1229
- sections.push(`# Site URL (used by GoTrue for email redirects)
1229
+ sections.push(`# Site URL (used for email redirects)
1230
1230
  SITE_URL=http://localhost:3000`)
1231
1231
 
1232
1232
  if (opts.provider === "docker" && opts.kongPort !== undefined) {
@@ -1,56 +1,53 @@
1
1
  /**
2
- * supatype logs: tail aggregated logs from local Docker containers.
2
+ * supatype logs: tail logs from the local stack.
3
3
  */
4
4
  import type { Command } from "commander"
5
5
  import { spawn } from "node:child_process"
6
- import { error } from "../ui/messages.js"
6
+ import { composeArgs, composeContext, composeServiceNames } from "../compose-services.js"
7
+ import { error, info } from "../ui/messages.js"
7
8
 
8
- const SERVICES = ["postgres", "postgrest", "gotrue", "kong", "minio", "realtime", "studio"]
9
+ /**
10
+ * logsArgs builds the compose invocation.
11
+ *
12
+ * Compose is asked for the logs rather than `docker logs <name>`, because the
13
+ * container names are Compose's to choose: the generated file sets no
14
+ * `container_name`, so a container is `<project>-<service>-<n>` and guessing at
15
+ * that name is what left this command tailing containers that never existed.
16
+ */
17
+ export function logsArgs(
18
+ base: string[],
19
+ opts: { service?: string; since?: string; follow?: boolean },
20
+ ): string[] {
21
+ const args = [...base, "logs", "--tail", "100"]
22
+ if (opts.follow) args.push("--follow")
23
+ if (opts.since) args.push("--since", opts.since)
24
+ if (opts.service) args.push(opts.service)
25
+ return args
26
+ }
9
27
 
10
28
  export function registerLogs(program: Command): void {
11
29
  program
12
30
  .command("logs")
13
- .description("Tail aggregated logs from local dev services")
14
- .option("--service <name>", "Filter to a specific service")
15
- .option("--since <duration>", "Show logs since duration (e.g., 5m, 1h)", "5m")
31
+ .description("Tail logs from the local stack")
32
+ .option("--service <name>", "Filter to a single service")
33
+ .option("--since <duration>", "Show logs since duration (e.g. 5m, 1h)", "5m")
16
34
  .option("-f, --follow", "Follow log output", true)
17
35
  .action((opts: { service?: string; since?: string; follow?: boolean }) => {
18
- const services = opts.service
19
- ? [`supatype-${opts.service}`]
20
- : SERVICES.map((s) => `supatype-${s}`)
21
-
22
- if (opts.service && !SERVICES.includes(opts.service)) {
23
- error(`Unknown service: ${opts.service}`)
24
- error(`Available: ${SERVICES.join(", ")}`)
36
+ const ctx = composeContext(process.cwd())
37
+ if (!ctx) {
38
+ error("No generated compose file here.")
39
+ info("Run `supatype self-host compose up` to create the stack.")
25
40
  process.exit(1)
26
41
  }
27
42
 
28
- const args = ["compose", "logs"]
29
- if (opts.follow) args.push("-f")
30
- if (opts.since) args.push("--since", opts.since)
31
- args.push("--tail", "100")
32
-
33
- // If filtering by service, use docker logs for that container
34
- if (opts.service) {
35
- const containerArgs = ["logs"]
36
- if (opts.follow) containerArgs.push("-f")
37
- if (opts.since) containerArgs.push("--since", opts.since)
38
- containerArgs.push("--tail", "100")
39
- containerArgs.push(`supatype-${opts.service}`)
40
-
41
- const child = spawn("docker", containerArgs, { stdio: "inherit" })
42
- child.on("error", () => {
43
- error("Docker not found. Ensure Docker is installed and running.")
44
- process.exit(1)
45
- })
46
- return
43
+ const available = composeServiceNames(ctx)
44
+ if (opts.service && available.length > 0 && !available.includes(opts.service)) {
45
+ error(`Unknown service: ${opts.service}`)
46
+ error(`Available: ${available.join(", ")}`)
47
+ process.exit(1)
47
48
  }
48
49
 
49
- // Aggregated logs via docker compose
50
- const child = spawn("docker", args, {
51
- stdio: "inherit",
52
- cwd: process.cwd(),
53
- })
50
+ const child = spawn("docker", logsArgs(composeArgs(ctx), opts), { stdio: "inherit" })
54
51
  child.on("error", () => {
55
52
  error("Docker not found. Ensure Docker is installed and running.")
56
53
  process.exit(1)
@@ -2,7 +2,12 @@ import type { Command } from "commander"
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { loadConfig, loadSchemaAst } from "../config.js"
5
- import { syncManifestHooks, validateModelHooks, writeHooksModule } from "../model-hooks.js"
5
+ import {
6
+ syncManifestHooks,
7
+ validateModelHooks,
8
+ validateModelValidators,
9
+ writeHooksModule,
10
+ } from "../model-hooks.js"
6
11
  import { adapterEntry, readHookUpload } from "../hook-upload.js"
7
12
  import { checkServiceRoleRoutes, serviceRoleProblemLines } from "../service-role-check.js"
8
13
  import { fatalError } from "../ui/fatal.js"
@@ -13,6 +18,8 @@ import {
13
18
  serverBaseUrl,
14
19
  } from "../project-config.js"
15
20
  import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
21
+ import { assertEngineSupportsSchema } from "../engine-floor.js"
22
+ import { pinnedVersion } from "../binary-cache.js"
16
23
  import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
17
24
  import { signJwt } from "../jwt.js"
18
25
  import { provisionBucketsFromAst } from "../storage-provision.js"
@@ -67,6 +74,7 @@ export function registerPush(program: Command): void {
67
74
  const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
68
75
  assertModelHooksResolve(cwd, config, ast)
69
76
  assertServiceRoleGrantsResolve(cwd, config)
77
+ assertEngineSupportsSchema(ast, pinnedVersion("engine", config))
70
78
 
71
79
  const linked = loadProjectLink(cwd)
72
80
  const useDirect = opts.direct || opts.local || Boolean(opts.connection)
@@ -312,9 +320,24 @@ function assertServiceRoleGrantsResolve(cwd: string, config: SupatypeProjectConf
312
320
  }
313
321
 
314
322
  function assertModelHooksResolve(cwd: string, config: SupatypeProjectConfig, ast: unknown): void {
315
- const problems = validateModelHooks(ast, hooksPathFromProject(config, cwd), cwd)
316
- if (problems.length === 0) return
317
- fatalError("A model declares a hook whose function does not exist.", problems, {
318
- brand: { intro: "Push" },
319
- })
323
+ const dir = hooksPathFromProject(config, cwd)
324
+
325
+ const problems = validateModelHooks(ast, dir, cwd)
326
+ if (problems.length > 0) {
327
+ fatalError("A model declares a hook whose function does not exist.", problems, {
328
+ brand: { intro: "Push" },
329
+ })
330
+ }
331
+
332
+ // Reported separately from hooks, because the fix is different: a missing validator means a field
333
+ // nobody is checking, and the message should say so rather than talk about lifecycle events.
334
+ const validatorProblems = validateModelValidators(ast, dir, cwd)
335
+ if (validatorProblems.length > 0) {
336
+ fatalError(
337
+ "A model declares a field validator whose function does not exist, so that field would be " +
338
+ "written unchecked.",
339
+ validatorProblems,
340
+ { brand: { intro: "Push" } },
341
+ )
342
+ }
320
343
  }
@@ -1,27 +1,19 @@
1
1
  /**
2
- * supatype status: show linked target or local dev stack state.
2
+ * supatype status: show linked target or local stack state.
3
3
  */
4
4
  import type { Command } from "commander"
5
- import { spawnSync } from "node:child_process"
6
5
  import { existsSync } from "node:fs"
7
6
  import { resolve } from "node:path"
8
- import { LOCAL_KONG_HOST_PORT, localKongBaseUrl } from "../local-gateway.js"
7
+ import { composeContext, composeServices } from "../compose-services.js"
8
+ import { localKongBaseUrl } from "../local-gateway.js"
9
9
  import { loadLocalEnvironment, loadProjectLink } from "../link.js"
10
10
  import { resolveTarget, targetStatus } from "../resolve-target.js"
11
11
  import { error, info, plain, warn } from "../ui/messages.js"
12
12
 
13
- interface ServiceStatus {
14
- name: string
15
- container: string
16
- status: "running" | "stopped" | "error"
17
- port?: number
18
- uptime?: string
19
- }
20
-
21
13
  export function registerStatus(program: Command): void {
22
14
  program
23
15
  .command("status")
24
- .description("Show linked target status or local dev services")
16
+ .description("Show linked target status or local stack services")
25
17
  .option("--env <name>", "Target environment when linked")
26
18
  .action(async (opts: { env?: string }) => {
27
19
  const cwd = process.cwd()
@@ -73,47 +65,48 @@ async function printLinkedStatus(target: ReturnType<typeof resolveTarget>): Prom
73
65
  }
74
66
  }
75
67
 
68
+ /**
69
+ * printLocalStackStatus reports what the stack is actually running.
70
+ *
71
+ * It asks Docker rather than checking a list of container names held here. The
72
+ * list this replaced named containers the compose file does not create, and
73
+ * omitted every one it does, so the command described an eight-service stack
74
+ * that has not existed since those services moved in-process behind
75
+ * supatype-server.
76
+ */
76
77
  function printLocalStackStatus(cwd: string): void {
77
- const localEnv = loadLocalEnvironment(cwd)
78
- const kongPort = localEnv?.kongPort ?? LOCAL_KONG_HOST_PORT
78
+ const ctx = composeContext(cwd)
79
+ if (!ctx) {
80
+ plain("Supatype local stack\n")
81
+ info("No generated compose file here.")
82
+ info("Run `supatype self-host compose up` to create the stack, or `supatype dev`,")
83
+ info("which runs the development stack outside Docker.")
84
+ return
85
+ }
79
86
 
80
- const services: ServiceStatus[] = [
81
- { name: "Postgres", container: "supatype-postgres", port: 5432 },
82
- { name: "PostgREST", container: "supatype-postgrest", port: 3000 },
83
- { name: "GoTrue", container: "supatype-gotrue", port: 9999 },
84
- { name: "Kong", container: "supatype-kong", port: kongPort },
85
- { name: "Control plane", container: "supatype-control-plane" },
86
- { name: "MinIO", container: "supatype-minio", port: 9000 },
87
- { name: "Realtime", container: "supatype-realtime", port: 4000 },
88
- { name: "Studio", container: "supatype-studio", port: 3100 },
89
- ].map((svc) => {
90
- const status = getContainerStatus(svc.container)
91
- const uptime = getContainerUptime(svc.container)
92
- return { ...svc, status, ...(uptime !== undefined && { uptime }) }
93
- })
87
+ const services = composeServices(ctx)
88
+ plain(`Supatype local stack (${ctx.projectName})\n`)
94
89
 
95
- plain("Supatype Local Development Stack\n")
90
+ if (services.length === 0) {
91
+ info("No containers yet. Start the stack with `supatype self-host compose up`.")
92
+ return
93
+ }
96
94
 
97
- const maxName = Math.max(...services.map((s) => s.name.length))
95
+ const width = Math.max(...services.map((svc) => svc.service.length))
98
96
  for (const svc of services) {
99
- const icon = svc.status === "running" ? "●" : svc.status === "stopped" ? "○" : "✕"
100
- const status = svc.status.padEnd(8)
101
- const port = svc.port ? `:${svc.port}` : ""
102
- const uptime = svc.uptime ? ` (${svc.uptime})` : ""
103
- plain(` ${icon} ${svc.name.padEnd(maxName)} ${status} ${port}${uptime}`)
97
+ const icon = svc.state === "running" ? "●" : svc.state === "exited" ? "○" : "✕"
98
+ const ports = svc.ports ? ` ${svc.ports}` : ""
99
+ plain(` ${icon} ${svc.service.padEnd(width)} ${svc.state.padEnd(10)}${ports}`)
104
100
  }
105
101
 
106
- const running = services.filter((s) => s.status === "running")
102
+ const running = services.filter((svc) => svc.state === "running")
107
103
  plain(`\n${running.length}/${services.length} services running`)
108
104
 
109
105
  if (running.length > 0) {
110
- const apiUrl = localEnv?.apiUrl ?? localKongBaseUrl()
111
- plain(`\nAPI URL: ${apiUrl}`)
112
- plain(`Studio: http://localhost:3100`)
106
+ const localEnv = loadLocalEnvironment(cwd)
107
+ plain(`\nAPI URL: ${localEnv?.apiUrl ?? localKongBaseUrl()}`)
113
108
  if (localEnv?.databaseUrl) {
114
109
  plain(`Database: ${localEnv.databaseUrl}`)
115
- } else {
116
- plain(`Database: postgresql://supatype_admin:postgres@localhost:5432/postgres`)
117
110
  }
118
111
  }
119
112
 
@@ -122,32 +115,3 @@ function printLocalStackStatus(cwd: string): void {
122
115
  info("Link remote ops: supatype link --url <api> --token $SERVICE_ROLE_KEY")
123
116
  }
124
117
  }
125
-
126
- function getContainerStatus(name: string): "running" | "stopped" | "error" {
127
- const result = spawnSync("docker", ["inspect", "--format", "{{.State.Status}}", name], {
128
- timeout: 5000,
129
- })
130
- const status = result.stdout?.toString().trim()
131
- if (status === "running") return "running"
132
- if (result.status !== 0) return "stopped"
133
- return "error"
134
- }
135
-
136
- function getContainerUptime(name: string): string | undefined {
137
- const result = spawnSync("docker", ["inspect", "--format", "{{.State.StartedAt}}", name], {
138
- timeout: 5000,
139
- })
140
- if (result.status !== 0) return undefined
141
- const startedAt = result.stdout?.toString().trim()
142
- if (!startedAt) return undefined
143
-
144
- const started = new Date(startedAt)
145
- const now = new Date()
146
- const diffMs = now.getTime() - started.getTime()
147
- if (diffMs < 0) return undefined
148
-
149
- const hours = Math.floor(diffMs / (1000 * 60 * 60))
150
- const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
151
- if (hours > 0) return `${hours}h ${minutes}m`
152
- return `${minutes}m`
153
- }
@@ -22,6 +22,23 @@ export function resolveServerSourceRoot(serverOverride: string, cwd: string): st
22
22
  return null
23
23
  }
24
24
 
25
+ /**
26
+ * Whether this project's compose stack should run the locally built server image.
27
+ *
28
+ * Synchronous and cheap, so any code path that writes `.env` can ask rather than being handed the
29
+ * answer. It used to be handed down as an argument from the one path that built the image, and the
30
+ * four paths that did not know about it wrote `.env` without `SUPATYPE_SERVER_IMAGE` while still
31
+ * asking for unpinned image keys to be cleaned up. Each of those removed the key `dev` had just
32
+ * written, so `overrides.server` held only until the next `push`, after which compose silently
33
+ * recreated the container from the published image and the local build was not running at all.
34
+ */
35
+ export function usesLocalServerImage(cwd: string, config: SupatypeProjectConfig): boolean {
36
+ if (config.versions?.server !== VERSION_PIN_LOCAL) return false
37
+ const override = config.overrides?.server?.trim()
38
+ if (!override) return false
39
+ return resolveServerSourceRoot(override, cwd) !== null
40
+ }
41
+
25
42
  /**
26
43
  * When `versions.server` is `local`, build a Linux server image from the source tree
27
44
  * next to `overrides.server` so compose dev matches the contributor's binary.