@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +150 -136
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/cli-version-embedded.d.ts.map +1 -1
- package/dist/cli-version-embedded.js +1 -1
- package/dist/cli-version-embedded.js.map +1 -1
- package/dist/commands/admin.d.ts +5 -5
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +10 -10
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +23 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.js +51 -51
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +0 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +26 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/logs.d.ts +14 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +33 -37
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +18 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +33 -64
- package/dist/commands/status.js.map +1 -1
- package/dist/compose-local-server-image.d.ts +11 -0
- package/dist/compose-local-server-image.d.ts.map +1 -1
- package/dist/compose-local-server-image.js +18 -0
- package/dist/compose-local-server-image.js.map +1 -1
- package/dist/compose-services.d.ts +59 -0
- package/dist/compose-services.d.ts.map +1 -0
- package/dist/compose-services.js +160 -0
- package/dist/compose-services.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/dev-compose.d.ts +1 -0
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +202 -30
- package/dist/dev-compose.js.map +1 -1
- package/dist/dev-ports.d.ts +24 -0
- package/dist/dev-ports.d.ts.map +1 -1
- package/dist/dev-ports.js +37 -0
- package/dist/dev-ports.js.map +1 -1
- package/dist/docker-postgres.d.ts +1 -1
- package/dist/docker-postgres.d.ts.map +1 -1
- package/dist/docker-postgres.js +2 -2
- package/dist/docker-postgres.js.map +1 -1
- package/dist/engine-floor.d.ts +50 -0
- package/dist/engine-floor.d.ts.map +1 -0
- package/dist/engine-floor.js +80 -0
- package/dist/engine-floor.js.map +1 -0
- package/dist/field-bounds.d.ts +68 -0
- package/dist/field-bounds.d.ts.map +1 -0
- package/dist/field-bounds.js +277 -0
- package/dist/field-bounds.js.map +1 -0
- package/dist/hooks-generator.d.ts +1 -1
- package/dist/hooks-generator.d.ts.map +1 -1
- package/dist/hooks-generator.js +78 -4
- package/dist/hooks-generator.js.map +1 -1
- package/dist/link.d.ts +1 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/model-hooks.d.ts +44 -2
- package/dist/model-hooks.d.ts.map +1 -1
- package/dist/model-hooks.js +116 -12
- package/dist/model-hooks.js.map +1 -1
- package/dist/pgbouncer-userlist.d.ts +1 -1
- package/dist/pgbouncer-userlist.d.ts.map +1 -1
- package/dist/pgbouncer-userlist.js +1 -1
- package/dist/pgbouncer-userlist.js.map +1 -1
- package/dist/postgres-ctl.d.ts +5 -1
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +43 -4
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/project-config.d.ts +4 -4
- package/dist/resolve-target.d.ts +1 -1
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/schema-ast-v2.d.ts +38 -4
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +87 -4
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +41 -18
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-dev-server.d.ts +8 -1
- package/dist/studio-dev-server.d.ts.map +1 -1
- package/dist/studio-dev-server.js +1 -2
- package/dist/studio-dev-server.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +309 -27
- package/dist/type-extractor.js.map +1 -1
- package/package.json +4 -3
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/admin.ts +10 -10
- package/src/commands/db.ts +27 -1
- package/src/commands/dev.ts +51 -51
- package/src/commands/doctor.ts +30 -0
- package/src/commands/init.ts +1 -1
- package/src/commands/logs.ts +34 -37
- package/src/commands/push.ts +29 -6
- package/src/commands/status.ts +34 -70
- package/src/compose-local-server-image.ts +17 -0
- package/src/compose-services.ts +174 -0
- package/src/config.ts +1 -1
- package/src/dev-compose.ts +226 -29
- package/src/dev-ports.ts +48 -0
- package/src/docker-postgres.ts +2 -2
- package/src/engine-floor.ts +113 -0
- package/src/field-bounds.ts +359 -0
- package/src/hooks-generator.ts +81 -4
- package/src/link.ts +1 -1
- package/src/model-hooks.ts +158 -12
- package/src/pgbouncer-userlist.ts +1 -1
- package/src/postgres-ctl.ts +44 -4
- package/src/project-config.ts +4 -4
- package/src/resolve-target.ts +1 -1
- package/src/schema-ast-v2.ts +114 -10
- package/src/self-host-compose.ts +41 -18
- package/src/studio-dev-server.ts +9 -3
- package/src/type-extractor.ts +374 -39
- package/tests/admin-ensure.test.ts +10 -10
- package/tests/compose-parsable-defaults.test.ts +60 -0
- package/tests/compose-services.test.ts +144 -0
- package/tests/dev-ports-owner.test.ts +40 -0
- package/tests/engine-floor.test.ts +102 -0
- package/tests/external-database-compose.test.ts +3 -3
- package/tests/field-bounds-matrix.test.ts +163 -0
- package/tests/field-bounds.test.ts +139 -0
- package/tests/field-validators.test.ts +139 -0
- package/tests/hooks-generator.test.ts +86 -0
- package/tests/local-server-image-env.test.ts +93 -0
- package/tests/model-constraints.test.ts +293 -0
- package/tests/model-hooks.test.ts +56 -0
- package/tests/port-in-use.test.ts +58 -0
- package/tests/readiness-poll.test.ts +84 -0
- package/tests/runtime-contract.test.ts +1 -1
- package/tests/type-extractor.test.ts +49 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
|
69
|
-
function
|
|
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.
|
|
75
|
-
if (s.port !== undefined) out.
|
|
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.
|
|
78
|
-
if (s.pass !== undefined && s.pass !== "") out.
|
|
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.
|
|
80
|
+
if (admin) out.SUPATYPE_SMTP_ADMIN_EMAIL = admin
|
|
81
81
|
const sender = s.sender_name?.trim()
|
|
82
|
-
if (sender) out.
|
|
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.
|
|
291
|
-
console.log("[supatype] Running
|
|
292
|
-
const migrateEnv =
|
|
293
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
434
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
464
|
+
...(authMailerProvider === "smtp" ? authSMTPFromEmailConfig(config.email) : {}),
|
|
465
465
|
...(config.email?.send_email_hook === true
|
|
466
466
|
? {
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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["
|
|
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
|
-
|
|
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
|
|
1083
|
-
function
|
|
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:
|
|
1091
|
-
|
|
1090
|
+
// envconfig: the service prefix + DB.DATABASE_URL → SUPATYPE_DB_DATABASE_URL
|
|
1091
|
+
SUPATYPE_DB_DATABASE_URL: sqlDbURL,
|
|
1092
1092
|
DATABASE_URL: sqlDbURL,
|
|
1093
|
-
|
|
1094
|
-
|
|
1093
|
+
SUPATYPE_DB_DRIVER: "postgres",
|
|
1094
|
+
SUPATYPE_DB_NAMESPACE: "auth",
|
|
1095
1095
|
PGSSLMODE: "disable",
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
|
1105
|
-
function
|
|
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
|
-
`
|
|
1120
|
+
`auth migrations failed (exit ${result.status ?? "unknown"})` +
|
|
1121
1121
|
(detail ? `:\n${detail}` : ""),
|
|
1122
1122
|
)
|
|
1123
1123
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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`)
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
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) {
|
package/src/commands/logs.ts
CHANGED
|
@@ -1,56 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* supatype logs: tail
|
|
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 {
|
|
6
|
+
import { composeArgs, composeContext, composeServiceNames } from "../compose-services.js"
|
|
7
|
+
import { error, info } from "../ui/messages.js"
|
|
7
8
|
|
|
8
|
-
|
|
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
|
|
14
|
-
.option("--service <name>", "Filter to a
|
|
15
|
-
.option("--since <duration>", "Show logs since duration (e.g
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
29
|
-
if (opts.
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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)
|
package/src/commands/push.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* supatype status: show linked target or local
|
|
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 {
|
|
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
|
|
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
|
|
78
|
-
|
|
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
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
95
|
+
const width = Math.max(...services.map((svc) => svc.service.length))
|
|
98
96
|
for (const svc of services) {
|
|
99
|
-
const icon = svc.
|
|
100
|
-
const
|
|
101
|
-
|
|
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((
|
|
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
|
|
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.
|