@supatype/cli 0.1.13 → 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 +151 -143
- 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/dev.js +51 -51
- package/dist/commands/dev.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 +3 -0
- 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-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.map +1 -1
- package/dist/dev-compose.js +123 -20
- 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/link.d.ts +1 -1
- package/dist/link.d.ts.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/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/package.json +2 -2
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/admin.ts +10 -10
- package/src/commands/dev.ts +51 -51
- package/src/commands/init.ts +1 -1
- package/src/commands/logs.ts +34 -37
- package/src/commands/push.ts +3 -0
- package/src/commands/status.ts +34 -70
- package/src/compose-services.ts +174 -0
- package/src/config.ts +1 -1
- package/src/dev-compose.ts +130 -20
- package/src/dev-ports.ts +48 -0
- package/src/docker-postgres.ts +2 -2
- package/src/engine-floor.ts +113 -0
- package/src/link.ts +1 -1
- 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/self-host-compose.ts +41 -18
- package/src/studio-dev-server.ts +9 -3
- 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/port-in-use.test.ts +58 -0
- package/tests/readiness-poll.test.ts +84 -0
- package/tests/runtime-contract.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supatype/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Supatype CLI: schema push, introspect, generate",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@types/pg": "^8",
|
|
42
42
|
"@types/react": "^18.3.18",
|
|
43
43
|
"vitest": "^4.0.18",
|
|
44
|
-
"@supatype/types": "0.
|
|
44
|
+
"@supatype/types": "0.2.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsc --project tsconfig.json && node --input-type=module -e \"import{cpSync,mkdirSync}from'node:fs';const a='dist/assets';mkdirSync(a,{recursive:true});cpSync('assets/supatype-logo-wordmark.ascii.txt',a+'/supatype-logo-wordmark.ascii.txt')\"",
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
// Annotated `string`, not left to inference: once a release stamps a value here, the
|
|
10
10
|
// inferred literal type makes the `!== ""` test in cliPackageVersion() a compile error
|
|
11
11
|
// (TS2367, no overlap), so the build would only fail during a release.
|
|
12
|
-
export const EMBEDDED_CLI_VERSION: string = "0.
|
|
12
|
+
export const EMBEDDED_CLI_VERSION: string = "0.2.0"
|
package/src/commands/admin.ts
CHANGED
|
@@ -52,18 +52,18 @@ function assertStudioRole(role: string): void {
|
|
|
52
52
|
process.exit(1)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
export const
|
|
55
|
+
/** The auth service scopes users to the nil instance id in single-tenant/self-host mode. */
|
|
56
|
+
export const SUPATYPE_NIL_INSTANCE_ID = "00000000-0000-0000-0000-000000000000"
|
|
57
57
|
|
|
58
|
-
/** Audience claim used
|
|
59
|
-
export function
|
|
60
|
-
return readEnvValue(cwd, "
|
|
58
|
+
/** Audience claim used when looking up users (matches SUPATYPE_JWT_AUD in compose). */
|
|
59
|
+
export function authJwtAud(cwd: string): string {
|
|
60
|
+
return readEnvValue(cwd, "SUPATYPE_JWT_AUD", "authenticated")
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
type DbQuery = (sql: string, params?: unknown[]) => Promise<QueryResult>
|
|
64
64
|
type AuthConfirmedColumn = "email_confirmed_at" | "confirmed_at"
|
|
65
65
|
|
|
66
|
-
/** Prefer
|
|
66
|
+
/** Prefer the migrated column when present; fall back to postgres init `confirmed_at`. */
|
|
67
67
|
export async function resolveAuthConfirmedAtColumn(query: DbQuery): Promise<AuthConfirmedColumn> {
|
|
68
68
|
const result = await query(
|
|
69
69
|
`SELECT column_name
|
|
@@ -464,14 +464,14 @@ async function createAdminUser(
|
|
|
464
464
|
: (db as DbQuery)
|
|
465
465
|
|
|
466
466
|
const normalized = email.toLowerCase()
|
|
467
|
-
const aud = opts.aud ?? (opts.cwd ?
|
|
467
|
+
const aud = opts.aud ?? (opts.cwd ? authJwtAud(opts.cwd) : "authenticated")
|
|
468
468
|
const existing = await query(
|
|
469
469
|
`SELECT id FROM auth.users
|
|
470
470
|
WHERE instance_id = $1::uuid
|
|
471
471
|
AND LOWER(email) = $2
|
|
472
472
|
AND aud = $3
|
|
473
473
|
AND is_sso_user = false`,
|
|
474
|
-
[
|
|
474
|
+
[SUPATYPE_NIL_INSTANCE_ID, normalized, aud],
|
|
475
475
|
)
|
|
476
476
|
if (existing.rows.length > 0) {
|
|
477
477
|
throw new Error(`User with email "${email}" already exists.`)
|
|
@@ -507,7 +507,7 @@ async function createAdminUser(
|
|
|
507
507
|
false, false,
|
|
508
508
|
now(), now()
|
|
509
509
|
) RETURNING id, email`,
|
|
510
|
-
[
|
|
510
|
+
[SUPATYPE_NIL_INSTANCE_ID, aud, normalized, passwordHash, appMetadata, userMetadata],
|
|
511
511
|
)
|
|
512
512
|
|
|
513
513
|
const user = result.rows[0] as { id: string; email: string }
|
|
@@ -601,7 +601,7 @@ async function hasAdminUsers(query: DbQuery): Promise<boolean> {
|
|
|
601
601
|
if (!(await studioMembersTableExists(query))) return false
|
|
602
602
|
|
|
603
603
|
// Inner JOIN on purpose, unlike `list-users`: a membership held by a cloud
|
|
604
|
-
// account cannot log in to a self-hosted
|
|
604
|
+
// account cannot log in to a self-hosted auth service, so a project exported from
|
|
605
605
|
// cloud with only those grants genuinely still needs a first admin.
|
|
606
606
|
const adminCount = await query(
|
|
607
607
|
`SELECT COUNT(*)::int as count
|
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/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
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
serverBaseUrl,
|
|
19
19
|
} from "../project-config.js"
|
|
20
20
|
import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
|
|
21
|
+
import { assertEngineSupportsSchema } from "../engine-floor.js"
|
|
22
|
+
import { pinnedVersion } from "../binary-cache.js"
|
|
21
23
|
import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
|
|
22
24
|
import { signJwt } from "../jwt.js"
|
|
23
25
|
import { provisionBucketsFromAst } from "../storage-provision.js"
|
|
@@ -72,6 +74,7 @@ export function registerPush(program: Command): void {
|
|
|
72
74
|
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
73
75
|
assertModelHooksResolve(cwd, config, ast)
|
|
74
76
|
assertServiceRoleGrantsResolve(cwd, config)
|
|
77
|
+
assertEngineSupportsSchema(ast, pinnedVersion("engine", config))
|
|
75
78
|
|
|
76
79
|
const linked = loadProjectLink(cwd)
|
|
77
80
|
const useDirect = opts.direct || opts.local || Boolean(opts.connection)
|
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
|
-
}
|