@supatype/cli 0.1.5 → 0.1.7
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 +143 -93
- package/.turbo/turbo-typecheck.log +1 -1
- package/deno.d.ts +52 -0
- package/dist/augmentation-generator.d.ts +22 -0
- package/dist/augmentation-generator.d.ts.map +1 -1
- package/dist/augmentation-generator.js +43 -7
- package/dist/augmentation-generator.js.map +1 -1
- package/dist/binary-cache.d.ts +14 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +33 -13
- package/dist/binary-cache.js.map +1 -1
- package/dist/cache-clean.d.ts +15 -0
- package/dist/cache-clean.d.ts.map +1 -0
- package/dist/cache-clean.js +76 -0
- package/dist/cache-clean.js.map +1 -0
- package/dist/cache-pins.d.ts +51 -0
- package/dist/cache-pins.d.ts.map +1 -0
- package/dist/cache-pins.js +184 -0
- package/dist/cache-pins.js.map +1 -0
- package/dist/cli-install-method.d.ts +19 -0
- package/dist/cli-install-method.d.ts.map +1 -0
- package/dist/cli-install-method.js +60 -0
- package/dist/cli-install-method.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-credentials.d.ts +34 -0
- package/dist/cloud-credentials.d.ts.map +1 -0
- package/dist/cloud-credentials.js +139 -0
- package/dist/cloud-credentials.js.map +1 -0
- package/dist/commands/admin.d.ts +11 -0
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +131 -22
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/cache.d.ts.map +1 -1
- package/dist/commands/cache.js +67 -71
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +32 -8
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +147 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +50 -10
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +28 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/functions.d.ts +7 -0
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +40 -6
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/hooks.d.ts +12 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +118 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +41 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/internal.d.ts +6 -0
- package/dist/commands/internal.d.ts.map +1 -0
- package/dist/commands/internal.js +22 -0
- package/dist/commands/internal.js.map +1 -0
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +2 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/link-helpers.d.ts.map +1 -1
- package/dist/commands/link-helpers.js +8 -2
- package/dist/commands/link-helpers.js.map +1 -1
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +110 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +110 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/self-update.d.ts +7 -0
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +18 -11
- package/dist/commands/self-update.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -3
- package/dist/config.js.map +1 -1
- package/dist/db-preflight.d.ts +78 -0
- package/dist/db-preflight.d.ts.map +1 -0
- package/dist/db-preflight.js +452 -0
- package/dist/db-preflight.js.map +1 -0
- package/dist/dev-compose.d.ts +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +129 -72
- package/dist/dev-compose.js.map +1 -1
- package/dist/env-file.d.ts +40 -2
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +132 -11
- package/dist/env-file.js.map +1 -1
- package/dist/field-masking-tier.d.ts +50 -0
- package/dist/field-masking-tier.d.ts.map +1 -0
- package/dist/field-masking-tier.js +91 -0
- package/dist/field-masking-tier.js.map +1 -0
- package/dist/functions-deno-types.d.ts +24 -0
- package/dist/functions-deno-types.d.ts.map +1 -0
- package/dist/functions-deno-types.js +155 -0
- package/dist/functions-deno-types.js.map +1 -0
- package/dist/hook-upload.d.ts +30 -0
- package/dist/hook-upload.d.ts.map +1 -0
- package/dist/hook-upload.js +86 -0
- package/dist/hook-upload.js.map +1 -0
- package/dist/hooks-generator.d.ts +8 -0
- package/dist/hooks-generator.d.ts.map +1 -0
- package/dist/hooks-generator.js +332 -0
- package/dist/hooks-generator.js.map +1 -0
- package/dist/link.d.ts +4 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +2 -0
- package/dist/link.js.map +1 -1
- package/dist/local-secrets.d.ts +71 -0
- package/dist/local-secrets.d.ts.map +1 -0
- package/dist/local-secrets.js +100 -0
- package/dist/local-secrets.js.map +1 -0
- package/dist/model-hooks.d.ts +73 -0
- package/dist/model-hooks.d.ts.map +1 -0
- package/dist/model-hooks.js +211 -0
- package/dist/model-hooks.js.map +1 -0
- package/dist/postgres-ctl.d.ts +11 -0
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +25 -2
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/project-config.d.ts +121 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +150 -1
- package/dist/project-config.js.map +1 -1
- package/dist/resolve-target.d.ts +4 -0
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/resolve-target.js +69 -73
- package/dist/resolve-target.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +2 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +4 -2
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/schema-push-lock.d.ts.map +1 -1
- package/dist/schema-push-lock.js +43 -16
- package/dist/schema-push-lock.js.map +1 -1
- package/dist/self-host-compose.d.ts +24 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +301 -69
- package/dist/self-host-compose.js.map +1 -1
- package/dist/service-role-check.d.ts +21 -0
- package/dist/service-role-check.d.ts.map +1 -0
- package/dist/service-role-check.js +85 -0
- package/dist/service-role-check.js.map +1 -0
- package/dist/target-client.d.ts +17 -0
- package/dist/target-client.d.ts.map +1 -1
- package/dist/target-client.js +69 -17
- package/dist/target-client.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +736 -32
- package/dist/type-extractor.js.map +1 -1
- package/dist/ui/runtime/command-chrome.js +1 -1
- package/dist/ui/runtime/command-chrome.js.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.js +3 -1
- package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
- package/package.json +4 -1
- package/scripts/generate-homebrew-formula.mjs +121 -0
- package/src/augmentation-generator.ts +46 -9
- package/src/binary-cache.ts +47 -15
- package/src/cache-clean.ts +81 -0
- package/src/cache-pins.ts +243 -0
- package/src/cli-install-method.ts +67 -0
- package/src/cli.ts +6 -0
- package/src/cloud-credentials.ts +183 -0
- package/src/commands/admin.ts +153 -28
- package/src/commands/cache.ts +90 -75
- package/src/commands/cloud.ts +32 -8
- package/src/commands/db.ts +162 -1
- package/src/commands/dev.ts +57 -8
- package/src/commands/doctor.ts +69 -3
- package/src/commands/functions.ts +42 -5
- package/src/commands/generate.ts +5 -1
- package/src/commands/hooks.ts +138 -0
- package/src/commands/init.ts +42 -8
- package/src/commands/internal.ts +23 -0
- package/src/commands/keys.ts +2 -0
- package/src/commands/link-helpers.ts +9 -2
- package/src/commands/login.ts +127 -0
- package/src/commands/push.ts +124 -7
- package/src/commands/self-update.ts +85 -13
- package/src/config.ts +22 -3
- package/src/db-preflight.ts +575 -0
- package/src/dev-compose.ts +144 -73
- package/src/env-file.ts +157 -11
- package/src/field-masking-tier.ts +107 -0
- package/src/functions-deno-types.ts +188 -0
- package/src/hook-upload.ts +107 -0
- package/src/hooks-generator.ts +352 -0
- package/src/link.ts +7 -0
- package/src/local-secrets.ts +104 -0
- package/src/model-hooks.ts +260 -0
- package/src/postgres-ctl.ts +29 -2
- package/src/project-config.ts +236 -2
- package/src/resolve-target.ts +113 -68
- package/src/schema-ast-v2.ts +5 -2
- package/src/schema-push-lock.ts +56 -17
- package/src/self-host-compose.ts +344 -68
- package/src/service-role-check.ts +111 -0
- package/src/target-client.ts +105 -18
- package/src/type-extractor.ts +883 -28
- package/src/ui/runtime/command-chrome.ts +1 -1
- package/src/ui/runtime/stdin-after-ink.ts +3 -1
- package/tests/access-composition.test.ts +667 -0
- package/tests/admin-studio-membership.test.ts +121 -0
- package/tests/api-schemas.test.ts +83 -0
- package/tests/archive-name-alias.test.ts +42 -0
- package/tests/augmentation-generator.test.ts +72 -4
- package/tests/cache-clean.test.ts +206 -0
- package/tests/cli-help.test.ts +3 -0
- package/tests/cli-install-method.test.ts +66 -0
- package/tests/command-chrome-coverage.test.ts +31 -26
- package/tests/command-chrome-wrap.test.ts +1 -0
- package/tests/db-preflight.test.ts +72 -0
- package/tests/doctor-render.test.ts +62 -0
- package/tests/doctor-service-role.test.ts +55 -0
- package/tests/env-file.test.ts +121 -0
- package/tests/external-database-compose.test.ts +184 -0
- package/tests/external-database-config.test.ts +154 -0
- package/tests/field-masking-tier.test.ts +184 -0
- package/tests/functions-deno-types.test.ts +197 -0
- package/tests/homebrew-formula.test.ts +58 -0
- package/tests/hook-upload.test.ts +125 -0
- package/tests/hooks-generator.test.ts +212 -0
- package/tests/init.test.ts +3 -0
- package/tests/local-secrets.test.ts +92 -0
- package/tests/model-hooks.test.ts +322 -0
- package/tests/runtime-contract.test.ts +113 -1
- package/tests/service-role-check.test.ts +109 -0
- package/tests/service-role-routes.test.ts +51 -0
- package/tests/target-client-refresh.test.ts +94 -0
- package/tests/type-extractor.test.ts +187 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tests/engine-distribution.test.ts +0 -418
package/src/dev-compose.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
projectRootFromConfig,
|
|
16
16
|
resolveRuntimeProvider,
|
|
17
17
|
schemaPathFromProject,
|
|
18
|
+
usesExternalDatabase,
|
|
18
19
|
type SupatypeProjectConfig,
|
|
19
20
|
} from "./project-config.js"
|
|
20
21
|
import { signJwt } from "./jwt.js"
|
|
@@ -40,6 +41,12 @@ import { ensureLocalServerDockerImage } from "./compose-local-server-image.js"
|
|
|
40
41
|
import { ensureEngine, engineRequest, type DiffResult } from "./engine-client.js"
|
|
41
42
|
import { writeSchemaSourcePushArtifacts, type SchemaSourcePushArtifacts } from "./schema-sources.js"
|
|
42
43
|
import { readEnvValue, upsertEnvFile } from "./env-file.js"
|
|
44
|
+
import {
|
|
45
|
+
devJwtSecret,
|
|
46
|
+
devPostgresPassword,
|
|
47
|
+
seedMissingDatabaseIdentity,
|
|
48
|
+
seedMissingLocalSecrets,
|
|
49
|
+
} from "./local-secrets.js"
|
|
43
50
|
import { writeLocalEnvironment } from "./link.js"
|
|
44
51
|
import { registerDevShutdown } from "./dev-shutdown.js"
|
|
45
52
|
import {
|
|
@@ -55,16 +62,19 @@ import type { ExtractedSchemaAstV2 } from "./schema-ast-v2.js"
|
|
|
55
62
|
import { ensureFirstAdminUserForProject } from "./commands/admin.js"
|
|
56
63
|
import { publishDevReady } from "./dev-ready-panel.js"
|
|
57
64
|
|
|
58
|
-
const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"
|
|
59
|
-
|
|
60
65
|
/** Default host port for compose Postgres when `overrides.engine` is set (devLocal). */
|
|
61
66
|
const COMPOSE_DEV_DB_PORT = 54329
|
|
62
67
|
|
|
63
68
|
/** Sync optional Docker image pins from config into `.env` (no JWT rotation). */
|
|
64
69
|
export function syncComposeImagePins(cwd: string, config: SupatypeProjectConfig): void {
|
|
65
70
|
const imagePins = composeDockerImageEnv(config)
|
|
71
|
+
// Clean up only what a previous run wrote from config. A hand-written image tag in `.env` is the
|
|
72
|
+
// documented way to run a local build, and this used to delete it on every compose run.
|
|
66
73
|
const removeImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter((key) => !(key in imagePins))
|
|
67
|
-
upsertEnvFile(cwd, imagePins,
|
|
74
|
+
upsertEnvFile(cwd, imagePins, {
|
|
75
|
+
removeManaged: removeImageKeys,
|
|
76
|
+
managed: COMPOSE_PINNED_IMAGE_ENV_KEYS,
|
|
77
|
+
})
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
export interface DevComposeOptions {
|
|
@@ -72,8 +82,10 @@ export interface DevComposeOptions {
|
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/** In-compose Postgres URL (SCRAM; not published to the host). */
|
|
75
|
-
export function composeDbUrl(): string {
|
|
76
|
-
|
|
85
|
+
export function composeDbUrl(cwd: string): string {
|
|
86
|
+
const user = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
|
|
87
|
+
const db = readEnvValue(cwd, "POSTGRES_DB", "supatype")
|
|
88
|
+
return `postgresql://${user}:${devPostgresPassword(cwd)}@db:5432/${db}?sslmode=disable`
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
/**
|
|
@@ -84,10 +96,23 @@ async function resolveDevDbPort(cwd: string): Promise<number> {
|
|
|
84
96
|
return ensureDevDbPort(cwd)
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The database URL to hand a tool, from wherever it happens to run.
|
|
101
|
+
*
|
|
102
|
+
* The compose helpers below describe the `db` *container* — on the host at
|
|
103
|
+
* `SUPATYPE_DEV_DB_PORT`, or in-network at `db:5432`. Neither exists for a project pointed at an
|
|
104
|
+
* external database, and passing one produced "pool timed out while waiting for an open connection"
|
|
105
|
+
* — a message with nothing in it about the URL being wrong.
|
|
106
|
+
*/
|
|
107
|
+
function projectDatabaseUrl(cwd: string, config: SupatypeProjectConfig, inNetwork = false): string {
|
|
108
|
+
if (usesExternalDatabase(config)) return connectionString(config)
|
|
109
|
+
return inNetwork ? composeDbUrl(cwd) : hostComposeDbUrl(cwd)
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
function hostComposeDbUrl(cwd: string): string {
|
|
88
113
|
const port = readEnvValue(cwd, "SUPATYPE_DEV_DB_PORT", String(COMPOSE_DEV_DB_PORT))
|
|
89
114
|
const user = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
|
|
90
|
-
const pass =
|
|
115
|
+
const pass = devPostgresPassword(cwd)
|
|
91
116
|
const db = readEnvValue(cwd, "POSTGRES_DB", "supatype")
|
|
92
117
|
return `postgresql://${user}:${pass}@127.0.0.1:${port}/${db}?sslmode=disable`
|
|
93
118
|
}
|
|
@@ -117,19 +142,12 @@ export async function ensureDockerDbPublishedForHostEngine(
|
|
|
117
142
|
|
|
118
143
|
const now = Math.floor(Date.now() / 1000)
|
|
119
144
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
120
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
121
|
-
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" },
|
|
145
|
+
const anonKey = signJwt({ ...jwtBase, role: "anon" }, devJwtSecret(cwd))
|
|
146
|
+
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, devJwtSecret(cwd))
|
|
122
147
|
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort)
|
|
123
148
|
|
|
124
149
|
const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
|
|
125
|
-
|
|
126
|
-
quiet: true,
|
|
127
|
-
...(brand !== undefined && { brand }),
|
|
128
|
-
})
|
|
129
|
-
if (up !== 0) {
|
|
130
|
-
exitComposeFailed(up, "Could not start Postgres (compose db service).", brand)
|
|
131
|
-
}
|
|
132
|
-
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
150
|
+
await startComposeDatabase(config, paths, cwd, project, brand)
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
/**
|
|
@@ -142,6 +160,9 @@ export function usesLocalDockerEngineDb(
|
|
|
142
160
|
): boolean {
|
|
143
161
|
if (explicitConnection?.trim()) return false
|
|
144
162
|
if (config.connection?.trim()) return false
|
|
163
|
+
// An external database is already reachable and is not ours to publish. Without this the engine
|
|
164
|
+
// path tried `compose up -d db` against a compose file that deliberately has no `db` service.
|
|
165
|
+
if (usesExternalDatabase(config)) return false
|
|
145
166
|
return resolveRuntimeProvider(config) === "docker" && hasEngineOverride(config)
|
|
146
167
|
}
|
|
147
168
|
|
|
@@ -168,6 +189,36 @@ async function resolveKongPort(cwd: string): Promise<number> {
|
|
|
168
189
|
return ensureKongPort(cwd, { context: "dev" })
|
|
169
190
|
}
|
|
170
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Bring up the compose `db` service and wait for it to answer.
|
|
194
|
+
*
|
|
195
|
+
* A no-op when the project uses an external database: there is no such service, the database is
|
|
196
|
+
* already running, and its readiness is the operator's — `supatype db check` is what reports on it.
|
|
197
|
+
* Four call sites did this inline, so an external project hit `compose up -d db` against a compose
|
|
198
|
+
* file that deliberately has no `db`.
|
|
199
|
+
*/
|
|
200
|
+
async function startComposeDatabase(
|
|
201
|
+
config: SupatypeProjectConfig,
|
|
202
|
+
paths: SelfHostComposePaths,
|
|
203
|
+
cwd: string,
|
|
204
|
+
project: string,
|
|
205
|
+
brand: DockerBrandOptions | undefined,
|
|
206
|
+
timeoutMs = 120_000,
|
|
207
|
+
onFailure?: () => void,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
if (usesExternalDatabase(config)) return
|
|
210
|
+
|
|
211
|
+
const up = runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
|
|
212
|
+
quiet: true,
|
|
213
|
+
...(brand !== undefined && { brand }),
|
|
214
|
+
})
|
|
215
|
+
if (up !== 0) {
|
|
216
|
+
onFailure?.()
|
|
217
|
+
exitComposeFailed(up, "Could not start Postgres (compose db service).", brand)
|
|
218
|
+
}
|
|
219
|
+
await waitComposeHealthy(paths, cwd, timeoutMs, project)
|
|
220
|
+
}
|
|
221
|
+
|
|
171
222
|
function upsertDevComposeEnv(
|
|
172
223
|
cwd: string,
|
|
173
224
|
config: SupatypeProjectConfig,
|
|
@@ -179,16 +230,32 @@ function upsertDevComposeEnv(
|
|
|
179
230
|
): void {
|
|
180
231
|
const apiUrl = `http://localhost:${kongPort}`
|
|
181
232
|
const imagePins = composeDockerImageEnv(config)
|
|
233
|
+
// `POSTGRES_PASSWORD` and `JWT_SECRET` are deliberately absent.
|
|
234
|
+
//
|
|
235
|
+
// They used to be pinned here to published constants on every `dev` run, against the same
|
|
236
|
+
// `.env` a self-host deployment reads — so a project could not hold its own secrets, and one
|
|
237
|
+
// `dev` after generating them put the published values back with nothing to show it. They are
|
|
238
|
+
// now resolved from `.env` (see `local-secrets.ts`) and written by `init` alone.
|
|
239
|
+
//
|
|
240
|
+
// The keys below still have to be written, because they are *derived*: the anon and
|
|
241
|
+
// service-role tokens are signed with the effective secret, so leaving a stale pair in place
|
|
242
|
+
// would desync them from the secret that validates them.
|
|
182
243
|
const updates: Record<string, string> = {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
244
|
+
// The docker dev path renders the *self-host* compose file, where the secrets have no
|
|
245
|
+
// defaults — an unset value is a hard compose error rather than a service quietly starting
|
|
246
|
+
// with a published constant. This guarantees presence without overwriting: only keys
|
|
247
|
+
// genuinely absent from `.env` are filled, and with the value the project has been running
|
|
248
|
+
// with rather than a fresh one.
|
|
249
|
+
...seedMissingLocalSecrets(cwd),
|
|
250
|
+
// Project configuration, seeded not overwritten — see seedMissingDatabaseIdentity.
|
|
251
|
+
...seedMissingDatabaseIdentity(cwd),
|
|
187
252
|
ANON_KEY: anonKey,
|
|
188
253
|
SERVICE_ROLE_KEY: serviceRoleKey,
|
|
189
254
|
PUBLIC_SUPATYPE_ANON_KEY: anonKey,
|
|
190
255
|
VITE_SUPATYPE_ANON_KEY: anonKey,
|
|
256
|
+
EXPO_PUBLIC_SUPATYPE_ANON_KEY: anonKey,
|
|
191
257
|
PUBLIC_SUPATYPE_URL: apiUrl,
|
|
258
|
+
EXPO_PUBLIC_SUPATYPE_URL: apiUrl,
|
|
192
259
|
SUPATYPE_KONG_PORT: String(kongPort),
|
|
193
260
|
API_EXTERNAL_URL: apiUrl,
|
|
194
261
|
SITE_URL: apiUrl,
|
|
@@ -196,13 +263,26 @@ function upsertDevComposeEnv(
|
|
|
196
263
|
...imagePins,
|
|
197
264
|
...(localServerImage !== undefined && { SUPATYPE_SERVER_IMAGE: localServerImage }),
|
|
198
265
|
}
|
|
199
|
-
|
|
266
|
+
// Never for an external database: this URL describes the `db` container, which that project does
|
|
267
|
+
// not have. Writing it overwrote the operator's own DATABASE_URL — the value the whole stack and
|
|
268
|
+
// every CLI command reads — with a DSN pointing at a database that does not exist. Found by
|
|
269
|
+
// rehearsing a push against a real external Postgres, where the compose guard then refused to
|
|
270
|
+
// proceed because .env and the config disagreed. They disagreed because of this line.
|
|
271
|
+
if (devDbPort !== undefined && !usesExternalDatabase(config)) {
|
|
200
272
|
updates.SUPATYPE_DEV_DB_PORT = String(devDbPort)
|
|
273
|
+
// User and database from the project, not hardcoded — same reason as
|
|
274
|
+
// `seedMissingDatabaseIdentity`: a project not named "supatype" had this URL pointing at a
|
|
275
|
+
// database that does not exist.
|
|
276
|
+
const dbUser = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
|
|
277
|
+
const dbName = readEnvValue(cwd, "POSTGRES_DB", "supatype")
|
|
201
278
|
updates.DATABASE_URL =
|
|
202
|
-
`postgresql
|
|
279
|
+
`postgresql://${dbUser}:${devPostgresPassword(cwd)}@localhost:${devDbPort}/${dbName}?sslmode=disable`
|
|
203
280
|
}
|
|
204
281
|
const removeImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter((key) => !(key in imagePins))
|
|
205
|
-
upsertEnvFile(cwd, updates,
|
|
282
|
+
upsertEnvFile(cwd, updates, {
|
|
283
|
+
removeManaged: removeImageKeys,
|
|
284
|
+
managed: COMPOSE_PINNED_IMAGE_ENV_KEYS,
|
|
285
|
+
})
|
|
206
286
|
}
|
|
207
287
|
|
|
208
288
|
/** Keep compose + Studio on the same freshly signed dev JWTs; sync optional image pins from config. */
|
|
@@ -438,7 +518,7 @@ async function runComposeSchemaPush(
|
|
|
438
518
|
try {
|
|
439
519
|
await engineRequest("/push", {
|
|
440
520
|
ast,
|
|
441
|
-
database_url:
|
|
521
|
+
database_url: projectDatabaseUrl(cwd, config),
|
|
442
522
|
schema: pgSchema,
|
|
443
523
|
force: true,
|
|
444
524
|
...(sources
|
|
@@ -538,7 +618,7 @@ async function runComposeEnginePush(
|
|
|
538
618
|
"-i",
|
|
539
619
|
"/project/.supatype/schema.ast.json",
|
|
540
620
|
"--database-url",
|
|
541
|
-
|
|
621
|
+
projectDatabaseUrl(cwd, config, true),
|
|
542
622
|
"--force",
|
|
543
623
|
"--non-interactive",
|
|
544
624
|
)
|
|
@@ -604,7 +684,7 @@ async function runComposeEngineDiff(
|
|
|
604
684
|
"-i",
|
|
605
685
|
"/project/.supatype/schema.ast.json",
|
|
606
686
|
"--database-url",
|
|
607
|
-
|
|
687
|
+
projectDatabaseUrl(cwd, config, true),
|
|
608
688
|
"--schema",
|
|
609
689
|
pgSchema,
|
|
610
690
|
)
|
|
@@ -649,7 +729,7 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
649
729
|
await ensureEngine()
|
|
650
730
|
return engineRequest<DiffResult>("/diff", {
|
|
651
731
|
ast,
|
|
652
|
-
database_url:
|
|
732
|
+
database_url: projectDatabaseUrl(cwd, config),
|
|
653
733
|
schema: pgSchema,
|
|
654
734
|
})
|
|
655
735
|
}
|
|
@@ -657,21 +737,14 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
657
737
|
const kongPort = await resolveKongPort(cwd)
|
|
658
738
|
const now = Math.floor(Date.now() / 1000)
|
|
659
739
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
660
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
661
|
-
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" },
|
|
740
|
+
const anonKey = signJwt({ ...jwtBase, role: "anon" }, devJwtSecret(cwd))
|
|
741
|
+
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, devJwtSecret(cwd))
|
|
662
742
|
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, undefined)
|
|
663
743
|
|
|
664
744
|
const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
|
|
665
745
|
const diffBrand = { intro: "Schema diff" }
|
|
666
746
|
|
|
667
|
-
|
|
668
|
-
quiet: true,
|
|
669
|
-
brand: diffBrand,
|
|
670
|
-
})
|
|
671
|
-
if (up !== 0) {
|
|
672
|
-
exitComposeFailed(up, "Could not start Postgres (compose db service).", diffBrand)
|
|
673
|
-
}
|
|
674
|
-
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
747
|
+
await startComposeDatabase(config, paths, cwd, project, diffBrand)
|
|
675
748
|
|
|
676
749
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
677
750
|
const ast = loadSchemaAst(schemaPath, cwd)
|
|
@@ -708,26 +781,25 @@ export async function pushSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
708
781
|
}
|
|
709
782
|
const project = composeProjectName(config.project.name)
|
|
710
783
|
const kongPort = await resolveKongPort(cwd)
|
|
711
|
-
|
|
784
|
+
// No dev db port for an external database: `ensureDevDbPort` allocates a host port for the `db`
|
|
785
|
+
// container *and persists a matching DATABASE_URL*, which overwrote the operator's own URL — the
|
|
786
|
+
// one the whole stack and every CLI command reads.
|
|
787
|
+
const devDbPort =
|
|
788
|
+
hasEngineOverride(config) && !usesExternalDatabase(config)
|
|
789
|
+
? await resolveDevDbPort(cwd)
|
|
790
|
+
: undefined
|
|
712
791
|
|
|
713
792
|
const now = Math.floor(Date.now() / 1000)
|
|
714
793
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
715
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
716
|
-
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" },
|
|
794
|
+
const anonKey = signJwt({ ...jwtBase, role: "anon" }, devJwtSecret(cwd))
|
|
795
|
+
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, devJwtSecret(cwd))
|
|
717
796
|
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort)
|
|
718
797
|
|
|
719
798
|
const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
|
|
720
799
|
const pushBrand = { intro: "Push schema" }
|
|
721
800
|
|
|
722
801
|
console.log(`[supatype] provider: docker — applying schema via compose (project ${project})...`)
|
|
723
|
-
|
|
724
|
-
quiet: true,
|
|
725
|
-
brand: pushBrand,
|
|
726
|
-
})
|
|
727
|
-
if (up !== 0) {
|
|
728
|
-
exitComposeFailed(up, "Could not start Postgres (compose db service).", pushBrand)
|
|
729
|
-
}
|
|
730
|
-
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
802
|
+
await startComposeDatabase(config, paths, cwd, project, pushBrand)
|
|
731
803
|
|
|
732
804
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
733
805
|
const ast = loadSchemaAst(schemaPath, cwd)
|
|
@@ -760,12 +832,18 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
760
832
|
// stack on the machine (own containers, volumes, network, and gateway port).
|
|
761
833
|
const project = composeProjectName(config.project.name)
|
|
762
834
|
const kongPort = await resolveKongPort(cwd)
|
|
763
|
-
|
|
835
|
+
// No dev db port for an external database: `ensureDevDbPort` allocates a host port for the `db`
|
|
836
|
+
// container *and persists a matching DATABASE_URL*, which overwrote the operator's own URL — the
|
|
837
|
+
// one the whole stack and every CLI command reads.
|
|
838
|
+
const devDbPort =
|
|
839
|
+
hasEngineOverride(config) && !usesExternalDatabase(config)
|
|
840
|
+
? await resolveDevDbPort(cwd)
|
|
841
|
+
: undefined
|
|
764
842
|
|
|
765
843
|
const now = Math.floor(Date.now() / 1000)
|
|
766
844
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
767
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
768
|
-
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" },
|
|
845
|
+
const anonKey = signJwt({ ...jwtBase, role: "anon" }, devJwtSecret(cwd))
|
|
846
|
+
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, devJwtSecret(cwd))
|
|
769
847
|
|
|
770
848
|
const devBrand = { intro: "Local development" }
|
|
771
849
|
const localServerImage = await ensureLocalServerDockerImage(cwd, config, devBrand)
|
|
@@ -816,18 +894,10 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
816
894
|
await recoverStaleDevSession(cwd)
|
|
817
895
|
await handleComposeProjectRename(cwd, config.project.name, paths)
|
|
818
896
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
quiet: true,
|
|
822
|
-
brand: devBrand,
|
|
823
|
-
})
|
|
824
|
-
if (upDbStatus !== 0) {
|
|
825
|
-
endDevSession()
|
|
826
|
-
exitComposeFailed(upDbStatus, "Could not start Postgres (compose db service).", devBrand)
|
|
897
|
+
if (!usesExternalDatabase(config)) {
|
|
898
|
+
console.log("[supatype] Bringing up Postgres (compose db)...")
|
|
827
899
|
}
|
|
828
|
-
|
|
829
|
-
console.log("[supatype] Waiting for Postgres (compose)...")
|
|
830
|
-
await waitComposeHealthy(paths, cwd, 180_000, project)
|
|
900
|
+
await startComposeDatabase(config, paths, cwd, project, devBrand, 180_000, endDevSession)
|
|
831
901
|
// Settle before DDL — pg_isready can pass slightly before the instance is stable.
|
|
832
902
|
await new Promise((r) => setTimeout(r, 3000))
|
|
833
903
|
|
|
@@ -849,16 +919,17 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
849
919
|
)
|
|
850
920
|
dumpComposeDbLogs(paths, cwd, project, `schema push attempt ${attempt}/${maxAttempts}`)
|
|
851
921
|
if (attempt < maxAttempts) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
922
|
+
// Only for a database Supatype created. Tearing down an external one is not ours to do,
|
|
923
|
+
// and `down -v` would destroy the stack's other volumes for a failure that was never
|
|
924
|
+
// about Postgres.
|
|
925
|
+
if (!usesExternalDatabase(config)) {
|
|
926
|
+
console.log("[supatype] Resetting Postgres after failed schema push...")
|
|
927
|
+
runDockerCompose(paths.composePath, ["down", "-v"], cwd, project, {
|
|
928
|
+
quiet: true,
|
|
929
|
+
brand: devBrand,
|
|
930
|
+
})
|
|
931
|
+
await startComposeDatabase(config, paths, cwd, project, devBrand)
|
|
932
|
+
}
|
|
862
933
|
await new Promise((r) => setTimeout(r, 3000 * attempt))
|
|
863
934
|
}
|
|
864
935
|
}
|
|
@@ -925,7 +996,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
925
996
|
writeLocalEnvironment(cwd, {
|
|
926
997
|
target: "local",
|
|
927
998
|
apiUrl: `http://localhost:${kongPort}`,
|
|
928
|
-
databaseUrl:
|
|
999
|
+
databaseUrl: projectDatabaseUrl(cwd, config, !hasEngineOverride(config)),
|
|
929
1000
|
projectRef: config.project.name,
|
|
930
1001
|
kongPort,
|
|
931
1002
|
provider: "docker",
|
package/src/env-file.ts
CHANGED
|
@@ -1,23 +1,162 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Marks a value the CLI wrote from `versions` in `supatype.config.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Provenance, so that removing a pin from config can clean up the stale image reference it left
|
|
8
|
+
* behind *without* deleting a value the operator wrote by hand. Before this, the image keys derived
|
|
9
|
+
* from config were removed unconditionally on every compose run: an operator could override
|
|
10
|
+
* `SUPATYPE_REALTIME_IMAGE` from `.env` and could not override `SUPATYPE_SERVER_IMAGE`, because the
|
|
11
|
+
* latter is derivable from a pin and the former is not. Same file, same shape, different rules.
|
|
12
|
+
*/
|
|
13
|
+
export const MANAGED_MARKER = "# supatype:managed — value from versions in supatype.config.ts"
|
|
14
|
+
|
|
15
|
+
interface EnvLine {
|
|
16
|
+
/** `KEY` for an assignment, undefined for comments and blanks. */
|
|
17
|
+
key?: string
|
|
18
|
+
text: string
|
|
19
|
+
/** True when the preceding line is the managed marker. */
|
|
20
|
+
managed: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseLines(text: string): EnvLine[] {
|
|
24
|
+
const out: EnvLine[] = []
|
|
25
|
+
if (text === "") return out
|
|
26
|
+
let markerPending = false
|
|
27
|
+
// A file ending in a newline yields a final empty element; keeping it would put a blank line
|
|
28
|
+
// between the existing content and anything appended.
|
|
29
|
+
const raws = text.split(/\r?\n/)
|
|
30
|
+
while (raws.length > 0 && raws[raws.length - 1] === "") raws.pop()
|
|
31
|
+
for (const raw of raws) {
|
|
32
|
+
const trimmed = raw.trim()
|
|
33
|
+
if (trimmed === MANAGED_MARKER) {
|
|
34
|
+
markerPending = true
|
|
35
|
+
out.push({ text: raw, managed: false })
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const eq = raw.indexOf("=")
|
|
39
|
+
const key = eq > 0 ? raw.slice(0, eq).trim() : undefined
|
|
40
|
+
const isAssignment = key !== undefined && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
|
|
41
|
+
out.push({ ...(isAssignment && { key }), text: raw, managed: isAssignment && markerPending })
|
|
42
|
+
if (!isAssignment && trimmed !== "") markerPending = false
|
|
43
|
+
else if (isAssignment) markerPending = false
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface UpsertOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Keys to delete — but only where the CLI wrote them (see [`MANAGED_MARKER`]). A hand-written value
|
|
51
|
+
* is left alone, because the operator putting an image tag in `.env` is the documented way to run a
|
|
52
|
+
* local build and deleting it silently sent them back to `:latest`.
|
|
53
|
+
*/
|
|
54
|
+
removeManaged?: readonly string[]
|
|
55
|
+
/** Keys in `updates` that come from config and may later be cleaned up. Written with the marker. */
|
|
56
|
+
managed?: readonly string[]
|
|
57
|
+
/** Keys to delete outright, whoever wrote them. For secrets being rotated out. */
|
|
58
|
+
remove?: readonly string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Merge key/value updates into a project `.env`.
|
|
63
|
+
*
|
|
64
|
+
* Comments, blank lines and ordering are preserved: this file is edited by hand, and the previous
|
|
65
|
+
* implementation rebuilt it from the assignments alone, so every comment an operator had written
|
|
66
|
+
* disappeared the first time they ran anything that touched `.env`.
|
|
67
|
+
*/
|
|
5
68
|
export function upsertEnvFile(
|
|
6
69
|
cwd: string,
|
|
7
70
|
updates: Record<string, string>,
|
|
8
|
-
|
|
71
|
+
options: UpsertOptions | readonly string[] = {},
|
|
9
72
|
): void {
|
|
73
|
+
// Historically the third argument was a plain list of keys to remove unconditionally.
|
|
74
|
+
const opts: UpsertOptions = Array.isArray(options)
|
|
75
|
+
? { remove: options as readonly string[] }
|
|
76
|
+
: (options as UpsertOptions)
|
|
77
|
+
const removeManaged = new Set(opts.removeManaged ?? [])
|
|
78
|
+
const removeAlways = new Set(opts.remove ?? [])
|
|
79
|
+
const managed = new Set(opts.managed ?? [])
|
|
80
|
+
|
|
10
81
|
const envPath = join(cwd, ".env")
|
|
11
82
|
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : ""
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
83
|
+
const lines = parseLines(existing)
|
|
84
|
+
|
|
85
|
+
const written = new Set<string>()
|
|
86
|
+
const kept: EnvLine[] = []
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i]!
|
|
90
|
+
const key = line.key
|
|
91
|
+
|
|
92
|
+
if (key === undefined) {
|
|
93
|
+
kept.push(line)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (removeAlways.has(key) || (removeManaged.has(key) && line.managed)) {
|
|
98
|
+
// Drop the marker that introduced it, so no orphan comment is left behind.
|
|
99
|
+
if (line.managed && kept[kept.length - 1]?.text.trim() === MANAGED_MARKER) kept.pop()
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (key in updates) {
|
|
104
|
+
const wantsMarker = managed.has(key)
|
|
105
|
+
if (wantsMarker && kept[kept.length - 1]?.text.trim() !== MANAGED_MARKER) {
|
|
106
|
+
kept.push({ text: MANAGED_MARKER, managed: false })
|
|
107
|
+
}
|
|
108
|
+
if (!wantsMarker && line.managed && kept[kept.length - 1]?.text.trim() === MANAGED_MARKER) {
|
|
109
|
+
kept.pop()
|
|
110
|
+
}
|
|
111
|
+
kept.push({ key, text: `${key}=${updates[key]}`, managed: wantsMarker })
|
|
112
|
+
written.add(key)
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
kept.push(line)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const appended: string[] = []
|
|
120
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
121
|
+
if (written.has(key)) continue
|
|
122
|
+
if (managed.has(key)) appended.push(MANAGED_MARKER)
|
|
123
|
+
appended.push(`${key}=${value}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const body = [...kept.map((l) => l.text), ...appended].join("\n")
|
|
127
|
+
writeFileSync(envPath, `${body.replace(/\n+$/, "")}\n`, "utf8")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a project `.env` into key/value pairs.
|
|
132
|
+
*
|
|
133
|
+
* Deliberately small: `KEY=value`, `#` comments, one optional layer of surrounding quotes. No
|
|
134
|
+
* interpolation and no `export` handling, because Compose's own parser does not do those either and
|
|
135
|
+
* a `.env` that means two different things depending on who reads it is worse than a limited one.
|
|
136
|
+
*/
|
|
137
|
+
export function readEnvFile(cwd: string): Record<string, string> {
|
|
138
|
+
const envPath = join(cwd, ".env")
|
|
139
|
+
if (!existsSync(envPath)) return {}
|
|
140
|
+
|
|
141
|
+
const out: Record<string, string> = {}
|
|
142
|
+
for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
|
|
143
|
+
const trimmed = line.trim()
|
|
144
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue
|
|
145
|
+
const eq = trimmed.indexOf("=")
|
|
146
|
+
if (eq <= 0) continue
|
|
147
|
+
const key = trimmed.slice(0, eq).trim()
|
|
148
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue
|
|
149
|
+
let value = trimmed.slice(eq + 1).trim()
|
|
150
|
+
if (
|
|
151
|
+
value.length >= 2 &&
|
|
152
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
153
|
+
(value.startsWith("'") && value.endsWith("'")))
|
|
154
|
+
) {
|
|
155
|
+
value = value.slice(1, -1)
|
|
156
|
+
}
|
|
157
|
+
out[key] = value
|
|
158
|
+
}
|
|
159
|
+
return out
|
|
21
160
|
}
|
|
22
161
|
|
|
23
162
|
export function readEnvValue(cwd: string, key: string, fallback: string): string {
|
|
@@ -29,6 +168,13 @@ export function readEnvValue(cwd: string, key: string, fallback: string): string
|
|
|
29
168
|
return fallback
|
|
30
169
|
}
|
|
31
170
|
|
|
171
|
+
/** Whether `.env` carries a non-empty value for `key`. Distinguishes absent from defaulted. */
|
|
172
|
+
export function hasEnvValue(cwd: string, key: string): boolean {
|
|
173
|
+
const envPath = join(cwd, ".env")
|
|
174
|
+
if (!existsSync(envPath)) return false
|
|
175
|
+
return new RegExp(`^${key}=(.+)$`, "m").test(readFileSync(envPath, "utf8"))
|
|
176
|
+
}
|
|
177
|
+
|
|
32
178
|
export function readEnvInt(cwd: string, key: string): number | null {
|
|
33
179
|
const raw = readEnvValue(cwd, key, "")
|
|
34
180
|
if (!raw) return null
|