@supatype/cli 0.1.6 → 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 +144 -94
- 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 +18 -4
- 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 +121 -19
- 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/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 +127 -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 +18 -5
- 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 +140 -25
- 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/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 +142 -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 +28 -0
- 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,11 +230,25 @@ 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,
|
|
@@ -198,13 +263,26 @@ function upsertDevComposeEnv(
|
|
|
198
263
|
...imagePins,
|
|
199
264
|
...(localServerImage !== undefined && { SUPATYPE_SERVER_IMAGE: localServerImage }),
|
|
200
265
|
}
|
|
201
|
-
|
|
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)) {
|
|
202
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")
|
|
203
278
|
updates.DATABASE_URL =
|
|
204
|
-
`postgresql
|
|
279
|
+
`postgresql://${dbUser}:${devPostgresPassword(cwd)}@localhost:${devDbPort}/${dbName}?sslmode=disable`
|
|
205
280
|
}
|
|
206
281
|
const removeImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter((key) => !(key in imagePins))
|
|
207
|
-
upsertEnvFile(cwd, updates,
|
|
282
|
+
upsertEnvFile(cwd, updates, {
|
|
283
|
+
removeManaged: removeImageKeys,
|
|
284
|
+
managed: COMPOSE_PINNED_IMAGE_ENV_KEYS,
|
|
285
|
+
})
|
|
208
286
|
}
|
|
209
287
|
|
|
210
288
|
/** Keep compose + Studio on the same freshly signed dev JWTs; sync optional image pins from config. */
|
|
@@ -440,7 +518,7 @@ async function runComposeSchemaPush(
|
|
|
440
518
|
try {
|
|
441
519
|
await engineRequest("/push", {
|
|
442
520
|
ast,
|
|
443
|
-
database_url:
|
|
521
|
+
database_url: projectDatabaseUrl(cwd, config),
|
|
444
522
|
schema: pgSchema,
|
|
445
523
|
force: true,
|
|
446
524
|
...(sources
|
|
@@ -540,7 +618,7 @@ async function runComposeEnginePush(
|
|
|
540
618
|
"-i",
|
|
541
619
|
"/project/.supatype/schema.ast.json",
|
|
542
620
|
"--database-url",
|
|
543
|
-
|
|
621
|
+
projectDatabaseUrl(cwd, config, true),
|
|
544
622
|
"--force",
|
|
545
623
|
"--non-interactive",
|
|
546
624
|
)
|
|
@@ -606,7 +684,7 @@ async function runComposeEngineDiff(
|
|
|
606
684
|
"-i",
|
|
607
685
|
"/project/.supatype/schema.ast.json",
|
|
608
686
|
"--database-url",
|
|
609
|
-
|
|
687
|
+
projectDatabaseUrl(cwd, config, true),
|
|
610
688
|
"--schema",
|
|
611
689
|
pgSchema,
|
|
612
690
|
)
|
|
@@ -651,7 +729,7 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
651
729
|
await ensureEngine()
|
|
652
730
|
return engineRequest<DiffResult>("/diff", {
|
|
653
731
|
ast,
|
|
654
|
-
database_url:
|
|
732
|
+
database_url: projectDatabaseUrl(cwd, config),
|
|
655
733
|
schema: pgSchema,
|
|
656
734
|
})
|
|
657
735
|
}
|
|
@@ -659,21 +737,14 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
659
737
|
const kongPort = await resolveKongPort(cwd)
|
|
660
738
|
const now = Math.floor(Date.now() / 1000)
|
|
661
739
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
662
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
663
|
-
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))
|
|
664
742
|
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, undefined)
|
|
665
743
|
|
|
666
744
|
const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
|
|
667
745
|
const diffBrand = { intro: "Schema diff" }
|
|
668
746
|
|
|
669
|
-
|
|
670
|
-
quiet: true,
|
|
671
|
-
brand: diffBrand,
|
|
672
|
-
})
|
|
673
|
-
if (up !== 0) {
|
|
674
|
-
exitComposeFailed(up, "Could not start Postgres (compose db service).", diffBrand)
|
|
675
|
-
}
|
|
676
|
-
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
747
|
+
await startComposeDatabase(config, paths, cwd, project, diffBrand)
|
|
677
748
|
|
|
678
749
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
679
750
|
const ast = loadSchemaAst(schemaPath, cwd)
|
|
@@ -710,26 +781,25 @@ export async function pushSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
710
781
|
}
|
|
711
782
|
const project = composeProjectName(config.project.name)
|
|
712
783
|
const kongPort = await resolveKongPort(cwd)
|
|
713
|
-
|
|
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
|
|
714
791
|
|
|
715
792
|
const now = Math.floor(Date.now() / 1000)
|
|
716
793
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
717
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
718
|
-
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))
|
|
719
796
|
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort)
|
|
720
797
|
|
|
721
798
|
const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
|
|
722
799
|
const pushBrand = { intro: "Push schema" }
|
|
723
800
|
|
|
724
801
|
console.log(`[supatype] provider: docker — applying schema via compose (project ${project})...`)
|
|
725
|
-
|
|
726
|
-
quiet: true,
|
|
727
|
-
brand: pushBrand,
|
|
728
|
-
})
|
|
729
|
-
if (up !== 0) {
|
|
730
|
-
exitComposeFailed(up, "Could not start Postgres (compose db service).", pushBrand)
|
|
731
|
-
}
|
|
732
|
-
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
802
|
+
await startComposeDatabase(config, paths, cwd, project, pushBrand)
|
|
733
803
|
|
|
734
804
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
735
805
|
const ast = loadSchemaAst(schemaPath, cwd)
|
|
@@ -762,12 +832,18 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
762
832
|
// stack on the machine (own containers, volumes, network, and gateway port).
|
|
763
833
|
const project = composeProjectName(config.project.name)
|
|
764
834
|
const kongPort = await resolveKongPort(cwd)
|
|
765
|
-
|
|
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
|
|
766
842
|
|
|
767
843
|
const now = Math.floor(Date.now() / 1000)
|
|
768
844
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
769
|
-
const anonKey = signJwt({ ...jwtBase, role: "anon" },
|
|
770
|
-
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))
|
|
771
847
|
|
|
772
848
|
const devBrand = { intro: "Local development" }
|
|
773
849
|
const localServerImage = await ensureLocalServerDockerImage(cwd, config, devBrand)
|
|
@@ -818,18 +894,10 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
818
894
|
await recoverStaleDevSession(cwd)
|
|
819
895
|
await handleComposeProjectRename(cwd, config.project.name, paths)
|
|
820
896
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
quiet: true,
|
|
824
|
-
brand: devBrand,
|
|
825
|
-
})
|
|
826
|
-
if (upDbStatus !== 0) {
|
|
827
|
-
endDevSession()
|
|
828
|
-
exitComposeFailed(upDbStatus, "Could not start Postgres (compose db service).", devBrand)
|
|
897
|
+
if (!usesExternalDatabase(config)) {
|
|
898
|
+
console.log("[supatype] Bringing up Postgres (compose db)...")
|
|
829
899
|
}
|
|
830
|
-
|
|
831
|
-
console.log("[supatype] Waiting for Postgres (compose)...")
|
|
832
|
-
await waitComposeHealthy(paths, cwd, 180_000, project)
|
|
900
|
+
await startComposeDatabase(config, paths, cwd, project, devBrand, 180_000, endDevSession)
|
|
833
901
|
// Settle before DDL — pg_isready can pass slightly before the instance is stable.
|
|
834
902
|
await new Promise((r) => setTimeout(r, 3000))
|
|
835
903
|
|
|
@@ -851,16 +919,17 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
851
919
|
)
|
|
852
920
|
dumpComposeDbLogs(paths, cwd, project, `schema push attempt ${attempt}/${maxAttempts}`)
|
|
853
921
|
if (attempt < maxAttempts) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
+
}
|
|
864
933
|
await new Promise((r) => setTimeout(r, 3000 * attempt))
|
|
865
934
|
}
|
|
866
935
|
}
|
|
@@ -927,7 +996,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
927
996
|
writeLocalEnvironment(cwd, {
|
|
928
997
|
target: "local",
|
|
929
998
|
apiUrl: `http://localhost:${kongPort}`,
|
|
930
|
-
databaseUrl:
|
|
999
|
+
databaseUrl: projectDatabaseUrl(cwd, config, !hasEngineOverride(config)),
|
|
931
1000
|
projectRef: config.project.name,
|
|
932
1001
|
kongPort,
|
|
933
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
|