@supatype/cli 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +150 -136
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/cli-version-embedded.d.ts.map +1 -1
- package/dist/cli-version-embedded.js +1 -1
- package/dist/cli-version-embedded.js.map +1 -1
- package/dist/commands/admin.d.ts +5 -5
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +10 -10
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +23 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.js +51 -51
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +0 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +26 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/logs.d.ts +14 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +33 -37
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +18 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +33 -64
- package/dist/commands/status.js.map +1 -1
- package/dist/compose-local-server-image.d.ts +11 -0
- package/dist/compose-local-server-image.d.ts.map +1 -1
- package/dist/compose-local-server-image.js +18 -0
- package/dist/compose-local-server-image.js.map +1 -1
- package/dist/compose-services.d.ts +59 -0
- package/dist/compose-services.d.ts.map +1 -0
- package/dist/compose-services.js +160 -0
- package/dist/compose-services.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/dev-compose.d.ts +1 -0
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +202 -30
- package/dist/dev-compose.js.map +1 -1
- package/dist/dev-ports.d.ts +24 -0
- package/dist/dev-ports.d.ts.map +1 -1
- package/dist/dev-ports.js +37 -0
- package/dist/dev-ports.js.map +1 -1
- package/dist/docker-postgres.d.ts +1 -1
- package/dist/docker-postgres.d.ts.map +1 -1
- package/dist/docker-postgres.js +2 -2
- package/dist/docker-postgres.js.map +1 -1
- package/dist/engine-floor.d.ts +50 -0
- package/dist/engine-floor.d.ts.map +1 -0
- package/dist/engine-floor.js +80 -0
- package/dist/engine-floor.js.map +1 -0
- package/dist/field-bounds.d.ts +68 -0
- package/dist/field-bounds.d.ts.map +1 -0
- package/dist/field-bounds.js +277 -0
- package/dist/field-bounds.js.map +1 -0
- package/dist/hooks-generator.d.ts +1 -1
- package/dist/hooks-generator.d.ts.map +1 -1
- package/dist/hooks-generator.js +78 -4
- package/dist/hooks-generator.js.map +1 -1
- package/dist/link.d.ts +1 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/model-hooks.d.ts +44 -2
- package/dist/model-hooks.d.ts.map +1 -1
- package/dist/model-hooks.js +116 -12
- package/dist/model-hooks.js.map +1 -1
- package/dist/pgbouncer-userlist.d.ts +1 -1
- package/dist/pgbouncer-userlist.d.ts.map +1 -1
- package/dist/pgbouncer-userlist.js +1 -1
- package/dist/pgbouncer-userlist.js.map +1 -1
- package/dist/postgres-ctl.d.ts +5 -1
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +43 -4
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/project-config.d.ts +4 -4
- package/dist/resolve-target.d.ts +1 -1
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/schema-ast-v2.d.ts +38 -4
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +87 -4
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +41 -18
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-dev-server.d.ts +8 -1
- package/dist/studio-dev-server.d.ts.map +1 -1
- package/dist/studio-dev-server.js +1 -2
- package/dist/studio-dev-server.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +309 -27
- package/dist/type-extractor.js.map +1 -1
- package/package.json +4 -3
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/admin.ts +10 -10
- package/src/commands/db.ts +27 -1
- package/src/commands/dev.ts +51 -51
- package/src/commands/doctor.ts +30 -0
- package/src/commands/init.ts +1 -1
- package/src/commands/logs.ts +34 -37
- package/src/commands/push.ts +29 -6
- package/src/commands/status.ts +34 -70
- package/src/compose-local-server-image.ts +17 -0
- package/src/compose-services.ts +174 -0
- package/src/config.ts +1 -1
- package/src/dev-compose.ts +226 -29
- package/src/dev-ports.ts +48 -0
- package/src/docker-postgres.ts +2 -2
- package/src/engine-floor.ts +113 -0
- package/src/field-bounds.ts +359 -0
- package/src/hooks-generator.ts +81 -4
- package/src/link.ts +1 -1
- package/src/model-hooks.ts +158 -12
- package/src/pgbouncer-userlist.ts +1 -1
- package/src/postgres-ctl.ts +44 -4
- package/src/project-config.ts +4 -4
- package/src/resolve-target.ts +1 -1
- package/src/schema-ast-v2.ts +114 -10
- package/src/self-host-compose.ts +41 -18
- package/src/studio-dev-server.ts +9 -3
- package/src/type-extractor.ts +374 -39
- package/tests/admin-ensure.test.ts +10 -10
- package/tests/compose-parsable-defaults.test.ts +60 -0
- package/tests/compose-services.test.ts +144 -0
- package/tests/dev-ports-owner.test.ts +40 -0
- package/tests/engine-floor.test.ts +102 -0
- package/tests/external-database-compose.test.ts +3 -3
- package/tests/field-bounds-matrix.test.ts +163 -0
- package/tests/field-bounds.test.ts +139 -0
- package/tests/field-validators.test.ts +139 -0
- package/tests/hooks-generator.test.ts +86 -0
- package/tests/local-server-image-env.test.ts +93 -0
- package/tests/model-constraints.test.ts +293 -0
- package/tests/model-hooks.test.ts +56 -0
- package/tests/port-in-use.test.ts +58 -0
- package/tests/readiness-poll.test.ts +84 -0
- package/tests/runtime-contract.test.ts +1 -1
- package/tests/type-extractor.test.ts +49 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the local stack is actually running, asked of Docker rather than guessed.
|
|
3
|
+
*
|
|
4
|
+
* `status` and `logs` each used to carry a hardcoded list of container names.
|
|
5
|
+
* Both lists were wrong: they named containers the compose file does not create
|
|
6
|
+
* (`supatype-postgres`, `supatype-kong`), omitted every service it does create
|
|
7
|
+
* (`server`, `storage`, `functions-worker`, `schema-engine`, `valkey`), and used
|
|
8
|
+
* a naming scheme the compose file never applies, since it sets no
|
|
9
|
+
* `container_name` and Docker therefore names containers
|
|
10
|
+
* `<project>-<service>-<n>`.
|
|
11
|
+
*
|
|
12
|
+
* A list of services in the source is a list that goes stale the next time the
|
|
13
|
+
* stack changes shape. `docker compose ps` cannot.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from "node:child_process"
|
|
16
|
+
import { existsSync } from "node:fs"
|
|
17
|
+
import { loadConfig } from "./config.js"
|
|
18
|
+
import { composeProjectName, selfHostComposePaths } from "./self-host-compose.js"
|
|
19
|
+
|
|
20
|
+
/** Where the local stack's compose file is, and what project name it runs under. */
|
|
21
|
+
export interface ComposeContext {
|
|
22
|
+
composePath: string
|
|
23
|
+
projectRoot: string
|
|
24
|
+
projectName: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One service in the local stack. */
|
|
28
|
+
export interface ComposeService {
|
|
29
|
+
/** The compose service name, which is what `logs --service` takes. */
|
|
30
|
+
service: string
|
|
31
|
+
/** The container Docker created for it, if any. */
|
|
32
|
+
container: string
|
|
33
|
+
/** Docker's own word: running, exited, created, restarting, and so on. */
|
|
34
|
+
state: string
|
|
35
|
+
/** Published ports, as Docker reports them. */
|
|
36
|
+
ports: string
|
|
37
|
+
/** How long it has been up, as Docker reports it. */
|
|
38
|
+
status: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* composeContext locates the generated compose file for this project.
|
|
43
|
+
*
|
|
44
|
+
* Returns undefined when the stack has never been generated, which is the
|
|
45
|
+
* ordinary case in a project that only uses `supatype dev`.
|
|
46
|
+
*/
|
|
47
|
+
export function composeContext(cwd: string): ComposeContext | undefined {
|
|
48
|
+
const paths = selfHostComposePaths(cwd)
|
|
49
|
+
if (!existsSync(paths.composePath)) return undefined
|
|
50
|
+
|
|
51
|
+
let projectName = "supatype"
|
|
52
|
+
try {
|
|
53
|
+
const config = loadConfig(cwd)
|
|
54
|
+
if (config) projectName = composeProjectName(config.project.name)
|
|
55
|
+
} catch {
|
|
56
|
+
// A project whose config will not load can still have a running stack, and
|
|
57
|
+
// reporting on it is more useful than refusing to.
|
|
58
|
+
}
|
|
59
|
+
return { composePath: paths.composePath, projectRoot: cwd, projectName }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* composeServices asks Docker what the stack is running.
|
|
64
|
+
*
|
|
65
|
+
* `--all` so a stopped service is reported as stopped rather than vanishing,
|
|
66
|
+
* which is the question `supatype status` is usually asked.
|
|
67
|
+
*/
|
|
68
|
+
export function composeServices(ctx: ComposeContext): ComposeService[] {
|
|
69
|
+
const result = spawnSync("docker", [...composeArgs(ctx), "ps", "--all", "--format", "json"], {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
timeout: 15_000,
|
|
72
|
+
})
|
|
73
|
+
if (result.status !== 0 || !result.stdout) return []
|
|
74
|
+
return parseComposePs(result.stdout)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* parseComposePs reads `docker compose ps --format json`.
|
|
79
|
+
*
|
|
80
|
+
* Compose v2 emits one JSON object per line; some versions emit a single JSON
|
|
81
|
+
* array instead. Both are accepted, because which one you get depends on the
|
|
82
|
+
* Docker version the developer happens to have.
|
|
83
|
+
*/
|
|
84
|
+
export function parseComposePs(stdout: string): ComposeService[] {
|
|
85
|
+
const trimmed = stdout.trim()
|
|
86
|
+
if (trimmed === "") return []
|
|
87
|
+
|
|
88
|
+
const rows: unknown[] = []
|
|
89
|
+
if (trimmed.startsWith("[")) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed: unknown = JSON.parse(trimmed)
|
|
92
|
+
if (Array.isArray(parsed)) rows.push(...parsed)
|
|
93
|
+
} catch {
|
|
94
|
+
return []
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
for (const line of trimmed.split("\n")) {
|
|
98
|
+
if (line.trim() === "") continue
|
|
99
|
+
try {
|
|
100
|
+
rows.push(JSON.parse(line))
|
|
101
|
+
} catch {
|
|
102
|
+
// A line that is not JSON is progress noise, not a service.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const services: ComposeService[] = []
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
if (typeof row !== "object" || row === null) continue
|
|
110
|
+
const record = row as Record<string, unknown>
|
|
111
|
+
const service = typeof record["Service"] === "string" ? record["Service"] : ""
|
|
112
|
+
if (service === "") continue
|
|
113
|
+
services.push({
|
|
114
|
+
service,
|
|
115
|
+
container: typeof record["Name"] === "string" ? record["Name"] : "",
|
|
116
|
+
state: typeof record["State"] === "string" ? record["State"] : "unknown",
|
|
117
|
+
ports: typeof record["Publishers"] === "string" ? record["Publishers"] : formatPorts(record["Publishers"]),
|
|
118
|
+
status: typeof record["Status"] === "string" ? record["Status"] : "",
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
services.sort((a, b) => a.service.localeCompare(b.service))
|
|
122
|
+
return services
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* formatPorts renders the structured Publishers array Compose emits, keeping
|
|
127
|
+
* only the ports actually reachable from the host.
|
|
128
|
+
*/
|
|
129
|
+
function formatPorts(publishers: unknown): string {
|
|
130
|
+
if (!Array.isArray(publishers)) return ""
|
|
131
|
+
const published: string[] = []
|
|
132
|
+
for (const entry of publishers) {
|
|
133
|
+
if (typeof entry !== "object" || entry === null) continue
|
|
134
|
+
const record = entry as Record<string, unknown>
|
|
135
|
+
const host = record["PublishedPort"]
|
|
136
|
+
const target = record["TargetPort"]
|
|
137
|
+
if (typeof host === "number" && host > 0 && typeof target === "number") {
|
|
138
|
+
published.push(`${host}->${target}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...new Set(published)].join(", ")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* composeArgs is the invocation prefix every compose call needs.
|
|
146
|
+
*
|
|
147
|
+
* The compose file lives under `.supatype/self-host/`, so the project directory
|
|
148
|
+
* has to be given explicitly: without it Compose resolves relative build
|
|
149
|
+
* contexts and env files against the file's own directory rather than the
|
|
150
|
+
* project root.
|
|
151
|
+
*/
|
|
152
|
+
export function composeArgs(ctx: ComposeContext): string[] {
|
|
153
|
+
return ["compose", "-p", ctx.projectName, "--project-directory", ctx.projectRoot, "-f", ctx.composePath]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* composeServiceNames lists the services the compose file defines.
|
|
158
|
+
*
|
|
159
|
+
* This is what `logs --service` should be validated against, not the running
|
|
160
|
+
* containers: a service that failed to start is exactly the one whose logs you
|
|
161
|
+
* want, and it has no container.
|
|
162
|
+
*/
|
|
163
|
+
export function composeServiceNames(ctx: ComposeContext): string[] {
|
|
164
|
+
const result = spawnSync("docker", [...composeArgs(ctx), "config", "--services"], {
|
|
165
|
+
encoding: "utf8",
|
|
166
|
+
timeout: 15_000,
|
|
167
|
+
})
|
|
168
|
+
if (result.status !== 0 || !result.stdout) return []
|
|
169
|
+
return result.stdout
|
|
170
|
+
.split("\n")
|
|
171
|
+
.map((line) => line.trim())
|
|
172
|
+
.filter((line) => line !== "")
|
|
173
|
+
.sort()
|
|
174
|
+
}
|
package/src/config.ts
CHANGED
package/src/dev-compose.ts
CHANGED
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
connectionString,
|
|
15
15
|
projectRootFromConfig,
|
|
16
16
|
resolveRuntimeProvider,
|
|
17
|
+
hooksPathFromProject,
|
|
17
18
|
schemaPathFromProject,
|
|
18
19
|
usesExternalDatabase,
|
|
19
20
|
type SupatypeProjectConfig,
|
|
20
21
|
} from "./project-config.js"
|
|
22
|
+
import { syncManifestHooks, writeHooksModule } from "./model-hooks.js"
|
|
21
23
|
import { signJwt } from "./jwt.js"
|
|
22
24
|
import { ensureDevDbPort, ensureKongPort } from "./dev-ports.js"
|
|
23
25
|
import { handleComposeProjectRename } from "./compose-rename.js"
|
|
@@ -37,11 +39,16 @@ import {
|
|
|
37
39
|
import type { DockerBrandOptions } from "./docker-runtime.js"
|
|
38
40
|
import { hasEngineOverride } from "./binary-cache.js"
|
|
39
41
|
import { STUDIO_DEV_PORT, startStudioViteDevServer } from "./studio-dev-server.js"
|
|
40
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ensureLocalServerDockerImage,
|
|
44
|
+
usesLocalServerImage,
|
|
45
|
+
LOCAL_SERVER_DOCKER_IMAGE,
|
|
46
|
+
} from "./compose-local-server-image.js"
|
|
41
47
|
import { ensureEngine, engineRequest, type DiffResult } from "./engine-client.js"
|
|
42
48
|
import { writeSchemaSourcePushArtifacts, type SchemaSourcePushArtifacts } from "./schema-sources.js"
|
|
43
49
|
import { readEnvValue, upsertEnvFile } from "./env-file.js"
|
|
44
50
|
import {
|
|
51
|
+
devAuthenticatorPassword,
|
|
45
52
|
devJwtSecret,
|
|
46
53
|
devPostgresPassword,
|
|
47
54
|
seedMissingDatabaseIdentity,
|
|
@@ -137,7 +144,7 @@ export async function ensureDockerDbPublishedForHostEngine(
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
const project = composeProjectName(config.project.name)
|
|
140
|
-
const kongPort = await resolveKongPort(cwd)
|
|
147
|
+
const kongPort = await resolveKongPort(cwd, project)
|
|
141
148
|
const devDbPort = await resolveDevDbPort(cwd)
|
|
142
149
|
|
|
143
150
|
const now = Math.floor(Date.now() / 1000)
|
|
@@ -185,8 +192,40 @@ export async function resolveHostEngineDatabaseUrl(
|
|
|
185
192
|
return connectionString(config)
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
/**
|
|
196
|
+
* `email.provider` and `email.smtp` for the compose server service.
|
|
197
|
+
*
|
|
198
|
+
* The native dev path has mapped these since it existed; the compose path never
|
|
199
|
+
* did, so a project that asked for smtp got the auth service's noop client and
|
|
200
|
+
* no message at all. Console is the documented default, and it at least says
|
|
201
|
+
* that a message would have been sent.
|
|
202
|
+
*/
|
|
203
|
+
function composeMailerEnv(config: SupatypeProjectConfig): Record<string, string> {
|
|
204
|
+
const email = config.email
|
|
205
|
+
const out: Record<string, string> = {
|
|
206
|
+
// Mailer.MailerProvider, so the doubled word is the real name.
|
|
207
|
+
SUPATYPE_MAILER_MAILER_PROVIDER: email?.provider ?? "console",
|
|
208
|
+
}
|
|
209
|
+
const smtp = email?.smtp
|
|
210
|
+
if (email?.provider !== "smtp" || smtp === undefined) return out
|
|
211
|
+
|
|
212
|
+
if (smtp.host !== undefined && smtp.host !== "") out.SUPATYPE_SMTP_HOST = smtp.host
|
|
213
|
+
if (smtp.port !== undefined) out.SUPATYPE_SMTP_PORT = String(smtp.port)
|
|
214
|
+
if (smtp.user !== undefined && smtp.user !== "") out.SUPATYPE_SMTP_USER = smtp.user
|
|
215
|
+
if (smtp.pass !== undefined && smtp.pass !== "") out.SUPATYPE_SMTP_PASS = smtp.pass
|
|
216
|
+
if (smtp.admin_email !== undefined && smtp.admin_email !== "") {
|
|
217
|
+
out.SUPATYPE_SMTP_ADMIN_EMAIL = smtp.admin_email
|
|
218
|
+
}
|
|
219
|
+
if (smtp.sender_name !== undefined && smtp.sender_name !== "") {
|
|
220
|
+
out.SUPATYPE_SMTP_SENDER_NAME = smtp.sender_name
|
|
221
|
+
}
|
|
222
|
+
return out
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function resolveKongPort(cwd: string, composeProject?: string): Promise<number> {
|
|
226
|
+
// The compose project is passed so an already-running stack of *this* project counts as
|
|
227
|
+
// available rather than as a collision.
|
|
228
|
+
return ensureKongPort(cwd, { context: "dev", ...(composeProject !== undefined && { composeProject }) })
|
|
190
229
|
}
|
|
191
230
|
|
|
192
231
|
/**
|
|
@@ -219,14 +258,13 @@ async function startComposeDatabase(
|
|
|
219
258
|
await waitComposeHealthy(paths, cwd, timeoutMs, project)
|
|
220
259
|
}
|
|
221
260
|
|
|
222
|
-
function upsertDevComposeEnv(
|
|
261
|
+
export function upsertDevComposeEnv(
|
|
223
262
|
cwd: string,
|
|
224
263
|
config: SupatypeProjectConfig,
|
|
225
264
|
anonKey: string,
|
|
226
265
|
serviceRoleKey: string,
|
|
227
266
|
kongPort: number,
|
|
228
267
|
devDbPort?: number,
|
|
229
|
-
localServerImage?: string,
|
|
230
268
|
): void {
|
|
231
269
|
const apiUrl = `http://localhost:${kongPort}`
|
|
232
270
|
const imagePins = composeDockerImageEnv(config)
|
|
@@ -259,9 +297,9 @@ function upsertDevComposeEnv(
|
|
|
259
297
|
SUPATYPE_KONG_PORT: String(kongPort),
|
|
260
298
|
API_EXTERNAL_URL: apiUrl,
|
|
261
299
|
SITE_URL: apiUrl,
|
|
262
|
-
|
|
300
|
+
SUPATYPE_MAILER_AUTOCONFIRM: "true",
|
|
301
|
+
...composeMailerEnv(config),
|
|
263
302
|
...imagePins,
|
|
264
|
-
...(localServerImage !== undefined && { SUPATYPE_SERVER_IMAGE: localServerImage }),
|
|
265
303
|
}
|
|
266
304
|
// Never for an external database: this URL describes the `db` container, which that project does
|
|
267
305
|
// not have. Writing it overwrote the operator's own DATABASE_URL, the value the whole stack and
|
|
@@ -278,10 +316,25 @@ function upsertDevComposeEnv(
|
|
|
278
316
|
updates.DATABASE_URL =
|
|
279
317
|
`postgresql://${dbUser}:${devPostgresPassword(cwd)}@localhost:${devDbPort}/${dbName}?sslmode=disable`
|
|
280
318
|
}
|
|
281
|
-
|
|
319
|
+
// `SUPATYPE_SERVER_IMAGE` is written here, not passed in, and it is deliberately *not* marked
|
|
320
|
+
// managed. The managed marker means "this value came from `versions` in the config and is mine to
|
|
321
|
+
// clean up when the pin goes away". The locally built image comes from `overrides`, so removing
|
|
322
|
+
// it as an unpinned managed key was wrong: any `.env` write that did not know about the local
|
|
323
|
+
// image deleted it, and compose then recreated the server from the published image.
|
|
324
|
+
const wantsLocalServer = usesLocalServerImage(cwd, config)
|
|
325
|
+
if (wantsLocalServer) updates.SUPATYPE_SERVER_IMAGE = LOCAL_SERVER_DOCKER_IMAGE
|
|
326
|
+
|
|
327
|
+
const managedImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter(
|
|
328
|
+
(key) => !(wantsLocalServer && key === "SUPATYPE_SERVER_IMAGE"),
|
|
329
|
+
)
|
|
330
|
+
const removeImageKeys = managedImageKeys.filter((key) => !(key in imagePins))
|
|
282
331
|
upsertEnvFile(cwd, updates, {
|
|
283
332
|
removeManaged: removeImageKeys,
|
|
284
|
-
managed:
|
|
333
|
+
managed: managedImageKeys,
|
|
334
|
+
// A local image left in `.env` after the project stopped asking for one would keep pointing
|
|
335
|
+
// compose at a stale build, and it carries no marker for `removeManaged` to act on.
|
|
336
|
+
...(!wantsLocalServer &&
|
|
337
|
+
!("SUPATYPE_SERVER_IMAGE" in imagePins) && { remove: ["SUPATYPE_SERVER_IMAGE"] }),
|
|
285
338
|
})
|
|
286
339
|
}
|
|
287
340
|
|
|
@@ -293,9 +346,8 @@ function ensureDevComposeEnv(
|
|
|
293
346
|
serviceRoleKey: string,
|
|
294
347
|
kongPort: number,
|
|
295
348
|
devDbPort?: number,
|
|
296
|
-
localServerImage?: string,
|
|
297
349
|
): void {
|
|
298
|
-
upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort
|
|
350
|
+
upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort)
|
|
299
351
|
}
|
|
300
352
|
|
|
301
353
|
async function waitComposeHealthy(paths: SelfHostComposePaths, cwd: string, maxMs: number, composeProject: string): Promise<void> {
|
|
@@ -371,21 +423,98 @@ function dumpComposeDbLogs(
|
|
|
371
423
|
console.error(tail)
|
|
372
424
|
}
|
|
373
425
|
|
|
374
|
-
|
|
375
|
-
|
|
426
|
+
/**
|
|
427
|
+
* The gateway as the CLI reaches it, over IPv4.
|
|
428
|
+
*
|
|
429
|
+
* Not `localhost`. Docker publishes on `0.0.0.0`, so the IPv4 address is the one that is certainly
|
|
430
|
+
* bound, while `localhost` resolves to `::1` first on Windows and macOS. Docker Desktop does forward
|
|
431
|
+
* IPv6 through a relay, and when that relay is unhealthy it accepts the connection and then never
|
|
432
|
+
* answers, so every readiness poll hangs rather than failing.
|
|
433
|
+
*
|
|
434
|
+
* The URLs written to `.env` and printed for the operator stay on `localhost`: those are for a
|
|
435
|
+
* browser, where the name is the friendlier thing to see and the browser will fall back on its own.
|
|
436
|
+
*/
|
|
437
|
+
function loopbackBase(port: number): string {
|
|
438
|
+
return `http://127.0.0.1:${port}`
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* `fetch` with a deadline, because the built-in has none.
|
|
443
|
+
*
|
|
444
|
+
* A poll loop bounded by `maxSec` iterations is only bounded if each attempt can end. Against a
|
|
445
|
+
* socket that accepts and never responds, an un-timed `fetch` never settles, the loop never reaches
|
|
446
|
+
* its second iteration, and a wait advertised as 120 seconds runs until someone kills it. That is
|
|
447
|
+
* how `supatype push` came to hang indefinitely after reporting the schema applied.
|
|
448
|
+
*/
|
|
449
|
+
async function fetchWithin(url: string, ms: number, init?: RequestInit): Promise<Response> {
|
|
450
|
+
return await fetch(url, { ...init, signal: AbortSignal.timeout(ms) })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Print what the stack is doing when the gateway never comes up.
|
|
455
|
+
*
|
|
456
|
+
* A 503 through the gateway means Kong is running and something behind it is
|
|
457
|
+
* not, and until now the only thing said was that the gateway was not ready.
|
|
458
|
+
* That sends people to look at Kong, which is almost never the problem, and in
|
|
459
|
+
* CI the containers are gone by the time anything else can inspect them.
|
|
460
|
+
*/
|
|
461
|
+
function reportUnhealthyStack(composePath: string, cwd: string, project: string): void {
|
|
462
|
+
const capture = (args: string[]): string => {
|
|
463
|
+
const result = spawnSync("docker", ["compose", "-p", project, "--project-directory", cwd, "-f", composePath, ...args], {
|
|
464
|
+
encoding: "utf8",
|
|
465
|
+
cwd,
|
|
466
|
+
})
|
|
467
|
+
return `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.error("")
|
|
471
|
+
console.error("[supatype] The gateway is up but a service behind it is not. Container state:")
|
|
472
|
+
const state = capture(["ps", "-a"])
|
|
473
|
+
console.error(state === "" ? " (no containers)" : state)
|
|
474
|
+
|
|
475
|
+
// The two the gateway health check depends on, and db because both need it.
|
|
476
|
+
for (const service of ["server", "realtime", "db"]) {
|
|
477
|
+
const logs = capture(["logs", "--no-color", "--tail", "40", service])
|
|
478
|
+
if (logs === "") continue
|
|
479
|
+
console.error("")
|
|
480
|
+
console.error(`[supatype] last 40 lines from ${service}:`)
|
|
481
|
+
console.error(logs)
|
|
482
|
+
}
|
|
483
|
+
console.error("")
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function waitKongReady(
|
|
487
|
+
kongPort: number,
|
|
488
|
+
maxSec: number,
|
|
489
|
+
stack?: { composePath: string; cwd: string; project: string },
|
|
490
|
+
): Promise<void> {
|
|
491
|
+
const base = loopbackBase(kongPort)
|
|
492
|
+
// Remembered so the failure can name the unhealthy upstream. Blaming "the
|
|
493
|
+
// Kong gateway" sent more than one investigation after Kong when Kong was
|
|
494
|
+
// fine and a service behind it was not.
|
|
495
|
+
let lastAuth = "no response"
|
|
496
|
+
let lastRealtime = "no response"
|
|
376
497
|
for (let i = 0; i < maxSec; i++) {
|
|
377
498
|
try {
|
|
378
499
|
const [auth, realtime] = await Promise.all([
|
|
379
|
-
|
|
380
|
-
|
|
500
|
+
fetchWithin(`${base}/auth/v1/health`, 2000),
|
|
501
|
+
fetchWithin(`${base}/realtime/v1/health`, 2000),
|
|
381
502
|
])
|
|
503
|
+
lastAuth = `HTTP ${auth.status}`
|
|
504
|
+
lastRealtime = `HTTP ${realtime.status}`
|
|
382
505
|
if (auth.ok && realtime.ok) return
|
|
383
|
-
} catch {
|
|
384
|
-
|
|
506
|
+
} catch (err) {
|
|
507
|
+
lastAuth = lastRealtime = err instanceof Error ? err.message : String(err)
|
|
385
508
|
}
|
|
386
509
|
await new Promise((r) => setTimeout(r, 1000))
|
|
387
510
|
}
|
|
388
|
-
|
|
511
|
+
if (stack !== undefined) {
|
|
512
|
+
reportUnhealthyStack(stack.composePath, stack.cwd, stack.project)
|
|
513
|
+
}
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Gateway at ${base} did not become ready within ${maxSec}s ` +
|
|
516
|
+
`(auth/v1/health: ${lastAuth}, realtime/v1/health: ${lastRealtime})`,
|
|
517
|
+
)
|
|
389
518
|
}
|
|
390
519
|
|
|
391
520
|
/** Kong may be up while server → storage is still starting (503 or upstream errors). */
|
|
@@ -394,11 +523,11 @@ async function waitStorageApiReady(
|
|
|
394
523
|
serviceRoleKey: string,
|
|
395
524
|
maxSec: number,
|
|
396
525
|
): Promise<void> {
|
|
397
|
-
const url =
|
|
526
|
+
const url = `${loopbackBase(kongPort)}/storage/v1/bucket`
|
|
398
527
|
const headers = { Authorization: `Bearer ${serviceRoleKey}` }
|
|
399
528
|
for (let i = 0; i < maxSec; i++) {
|
|
400
529
|
try {
|
|
401
|
-
const res = await
|
|
530
|
+
const res = await fetchWithin(url, 2000, { headers })
|
|
402
531
|
if (res.ok) return
|
|
403
532
|
const body = await res.text()
|
|
404
533
|
const kongUpstreamDown = body.includes("invalid response was received from the upstream server")
|
|
@@ -421,7 +550,7 @@ async function provisionDockerStorageBuckets(
|
|
|
421
550
|
kongPort: number,
|
|
422
551
|
serviceRoleKey: string,
|
|
423
552
|
): Promise<void> {
|
|
424
|
-
await provisionBucketsFromAst(ast,
|
|
553
|
+
await provisionBucketsFromAst(ast, `${loopbackBase(kongPort)}/storage/v1`, serviceRoleKey)
|
|
425
554
|
}
|
|
426
555
|
|
|
427
556
|
let _lastPushedAst: string | null = null
|
|
@@ -441,6 +570,20 @@ async function refreshSchemaArtifacts(
|
|
|
441
570
|
const supatypeDir = join(cwd, ".supatype")
|
|
442
571
|
const adminConfigPath = join(supatypeDir, "admin-config.json")
|
|
443
572
|
|
|
573
|
+
// Before the engine gate below: these are read straight off the AST, so a push whose engine is
|
|
574
|
+
// unavailable should still leave the server the right maps. The server watches this file, so a
|
|
575
|
+
// hook or validator added to the schema takes effect without restarting the stack.
|
|
576
|
+
//
|
|
577
|
+
// `dev` used to skip both entirely, because only the `direct`/`local` push branch wrote them.
|
|
578
|
+
// Every project running on Compose therefore had a manifest with no `hooks` and no `validators`
|
|
579
|
+
// key, and the server, having nothing to call, ran neither. A hook silently not firing is bad; a
|
|
580
|
+
// validator silently not firing means a write the schema says is checked is accepted with a 201.
|
|
581
|
+
const hooksModule = writeHooksModule(cwd, hooksPathFromProject(config, cwd), ast)
|
|
582
|
+
if (hooksModule !== null) console.log(`[supatype] Hook handler types written to ${hooksModule}`)
|
|
583
|
+
if (syncManifestHooks(cwd, ast)) {
|
|
584
|
+
console.log("[supatype] Hook and validator maps written to .supatype/manifest.json")
|
|
585
|
+
}
|
|
586
|
+
|
|
444
587
|
try {
|
|
445
588
|
await ensureEngine()
|
|
446
589
|
} catch (err) {
|
|
@@ -734,7 +877,7 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
734
877
|
})
|
|
735
878
|
}
|
|
736
879
|
|
|
737
|
-
const kongPort = await resolveKongPort(cwd)
|
|
880
|
+
const kongPort = await resolveKongPort(cwd, project)
|
|
738
881
|
const now = Math.floor(Date.now() / 1000)
|
|
739
882
|
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
740
883
|
const anonKey = signJwt({ ...jwtBase, role: "anon" }, devJwtSecret(cwd))
|
|
@@ -780,7 +923,7 @@ export async function pushSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
780
923
|
throw new Error("pushSchemaDocker requires provider: docker")
|
|
781
924
|
}
|
|
782
925
|
const project = composeProjectName(config.project.name)
|
|
783
|
-
const kongPort = await resolveKongPort(cwd)
|
|
926
|
+
const kongPort = await resolveKongPort(cwd, project)
|
|
784
927
|
// No dev db port for an external database: `ensureDevDbPort` allocates a host port for the `db`
|
|
785
928
|
// container *and persists a matching DATABASE_URL*, which overwrote the operator's own URL, the
|
|
786
929
|
// one the whole stack and every CLI command reads.
|
|
@@ -812,7 +955,7 @@ export async function pushSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
812
955
|
if (upGateway !== 0) {
|
|
813
956
|
exitComposeFailed(upGateway, "Could not start the Compose gateway stack.", pushBrand)
|
|
814
957
|
}
|
|
815
|
-
await waitKongReady(kongPort, 120)
|
|
958
|
+
await waitKongReady(kongPort, 120, { composePath: paths.composePath, cwd, project })
|
|
816
959
|
await waitStorageApiReady(kongPort, serviceRoleKey, 90)
|
|
817
960
|
await provisionDockerStorageBuckets(ast, kongPort, serviceRoleKey)
|
|
818
961
|
|
|
@@ -831,7 +974,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
831
974
|
// Per-project compose name + port isolate this project from any other Supatype
|
|
832
975
|
// stack on the machine (own containers, volumes, network, and gateway port).
|
|
833
976
|
const project = composeProjectName(config.project.name)
|
|
834
|
-
const kongPort = await resolveKongPort(cwd)
|
|
977
|
+
const kongPort = await resolveKongPort(cwd, project)
|
|
835
978
|
// No dev db port for an external database: `ensureDevDbPort` allocates a host port for the `db`
|
|
836
979
|
// container *and persists a matching DATABASE_URL*, which overwrote the operator's own URL, the
|
|
837
980
|
// one the whole stack and every CLI command reads.
|
|
@@ -848,7 +991,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
848
991
|
const devBrand = { intro: "Local development" }
|
|
849
992
|
const localServerImage = await ensureLocalServerDockerImage(cwd, config, devBrand)
|
|
850
993
|
|
|
851
|
-
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort
|
|
994
|
+
ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort)
|
|
852
995
|
|
|
853
996
|
console.log(`[supatype] provider docker, starting self-host Compose stack (project ${project}, gateway :${kongPort})...`)
|
|
854
997
|
const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
|
|
@@ -901,6 +1044,10 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
901
1044
|
// Settle before DDL: pg_isready can pass slightly before the instance is stable.
|
|
902
1045
|
await new Promise((r) => setTimeout(r, 3000))
|
|
903
1046
|
|
|
1047
|
+
if (!usesExternalDatabase(config)) {
|
|
1048
|
+
reconcileAuthenticatorPassword(paths, cwd, project)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
904
1051
|
// A: apply schema before realtime (and the rest of the stack) starts decoding WAL.
|
|
905
1052
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
906
1053
|
{
|
|
@@ -985,7 +1132,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
985
1132
|
}
|
|
986
1133
|
|
|
987
1134
|
console.log("[supatype] Waiting for API gateway...")
|
|
988
|
-
await waitKongReady(kongPort, 120)
|
|
1135
|
+
await waitKongReady(kongPort, 120, { composePath: paths.composePath, cwd, project })
|
|
989
1136
|
console.log("[supatype] Waiting for storage API...")
|
|
990
1137
|
await waitStorageApiReady(kongPort, serviceRoleKey, 90)
|
|
991
1138
|
|
|
@@ -1025,7 +1172,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
1025
1172
|
cwd,
|
|
1026
1173
|
studioOverride,
|
|
1027
1174
|
pidDir,
|
|
1028
|
-
|
|
1175
|
+
anonKey,
|
|
1029
1176
|
proxyTarget: `http://localhost:${kongPort}`,
|
|
1030
1177
|
viteSupatypeUrl: `http://localhost:${STUDIO_DEV_PORT}`,
|
|
1031
1178
|
basePath: "/studio/",
|
|
@@ -1104,6 +1251,56 @@ function astHasSystemAuthRelation(ast: unknown): boolean {
|
|
|
1104
1251
|
return false
|
|
1105
1252
|
}
|
|
1106
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* Set `authenticator`'s password to the one `.env` holds, every time the stack starts.
|
|
1256
|
+
*
|
|
1257
|
+
* The Postgres image passwords that role from `AUTHENTICATOR_PASSWORD` in its init scripts, which
|
|
1258
|
+
* run once, on an empty data directory. So the value the role actually has is whatever `.env` said
|
|
1259
|
+
* the day the volume was created, and `.env` can move afterwards. When the two diverge PostgREST
|
|
1260
|
+
* cannot log in, exits, and every REST request answers 502 with the real reason visible only in a
|
|
1261
|
+
* container log the developer has no reason to read.
|
|
1262
|
+
*
|
|
1263
|
+
* Reconciling here makes `.env` the answer to what the password is, rather than the volume's
|
|
1264
|
+
* birthday. It is idempotent, and it runs before the schema push so the API is already reachable by
|
|
1265
|
+
* the time the stack reports ready.
|
|
1266
|
+
*/
|
|
1267
|
+
function reconcileAuthenticatorPassword(
|
|
1268
|
+
paths: SelfHostComposePaths,
|
|
1269
|
+
cwd: string,
|
|
1270
|
+
composeProject: string,
|
|
1271
|
+
): void {
|
|
1272
|
+
const composeDir = dirname(paths.composePath)
|
|
1273
|
+
const owner = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
|
|
1274
|
+
const database = readEnvValue(cwd, "POSTGRES_DB", "supatype")
|
|
1275
|
+
const result = spawnSync(
|
|
1276
|
+
"docker",
|
|
1277
|
+
[
|
|
1278
|
+
"compose", "-p", composeProject, "-f", paths.composePath,
|
|
1279
|
+
"exec", "-T",
|
|
1280
|
+
"-e", `PGPASSWORD=${devPostgresPassword(cwd)}`,
|
|
1281
|
+
"-e", `SUPATYPE_AUTHENTICATOR_PASSWORD=${devAuthenticatorPassword(cwd)}`,
|
|
1282
|
+
"db", "psql", "-v", "ON_ERROR_STOP=1", "-U", owner, "-d", database,
|
|
1283
|
+
],
|
|
1284
|
+
{
|
|
1285
|
+
cwd: composeDir,
|
|
1286
|
+
encoding: "utf8",
|
|
1287
|
+
timeout: 10_000,
|
|
1288
|
+
// `\getenv` reads the value from the container's environment, so the password is never a
|
|
1289
|
+
// `docker exec` argument, which any process listing on the machine would show.
|
|
1290
|
+
input: [
|
|
1291
|
+
"\\getenv pw SUPATYPE_AUTHENTICATOR_PASSWORD",
|
|
1292
|
+
"ALTER ROLE authenticator WITH LOGIN PASSWORD :'pw';",
|
|
1293
|
+
"",
|
|
1294
|
+
].join("\n"),
|
|
1295
|
+
},
|
|
1296
|
+
)
|
|
1297
|
+
if (result.status !== 0) {
|
|
1298
|
+
console.warn(
|
|
1299
|
+
"[supatype] Could not set the authenticator password; the REST API may answer 502.",
|
|
1300
|
+
)
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1107
1304
|
function grantAuthSchemaAccess(
|
|
1108
1305
|
paths: SelfHostComposePaths,
|
|
1109
1306
|
cwd: string,
|
package/src/dev-ports.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* availability so multiple projects and port collisions are surfaced clearly.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { spawnSync } from "node:child_process"
|
|
7
8
|
import { CLACK_CANCEL, isCancel, p } from "./ui/clack.js"
|
|
8
9
|
import { COMPOSE_DEV_KONG_PORT } from "./project-config.js"
|
|
9
10
|
import { isPortInUse } from "./postgres-ctl.js"
|
|
@@ -14,6 +15,41 @@ import { fatalError } from "./ui/fatal.js"
|
|
|
14
15
|
const MIN_PORT = 1024
|
|
15
16
|
const MAX_PORT = 65535
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Which compose project publishes a host port, from `docker ps` output.
|
|
20
|
+
*
|
|
21
|
+
* Separated from the spawn so the decision is testable: the question "is this port held by my own
|
|
22
|
+
* stack" is the whole point of the check, and a check that can only be exercised by starting Docker
|
|
23
|
+
* is a check nobody exercises.
|
|
24
|
+
*
|
|
25
|
+
* Docker prints one line per matching container, and a container with no compose label prints an
|
|
26
|
+
* empty line. Any line naming a different project means someone else holds the port.
|
|
27
|
+
*/
|
|
28
|
+
export function composeProjectOwnsPort(dockerPsOutput: string, project: string): boolean {
|
|
29
|
+
const owners = dockerPsOutput
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => line.trim())
|
|
32
|
+
.filter((line) => line.length > 0)
|
|
33
|
+
return owners.length > 0 && owners.every((owner) => owner === project)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when the container publishing `port` belongs to this project's compose stack.
|
|
38
|
+
*
|
|
39
|
+
* Asks Docker rather than probing the port over HTTP. A health check would answer just as happily
|
|
40
|
+
* for a *different* project's gateway on the same port, and proceeding there would push a schema
|
|
41
|
+
* into the wrong database. Refusing is the safe answer for a stranger; only our own stack is fine.
|
|
42
|
+
*/
|
|
43
|
+
export function portHeldByComposeProject(port: number, project: string): boolean {
|
|
44
|
+
const result = spawnSync(
|
|
45
|
+
"docker",
|
|
46
|
+
["ps", "--filter", `publish=${port}`, "--format", '{{.Label "com.docker.compose.project"}}'],
|
|
47
|
+
{ encoding: "utf8" },
|
|
48
|
+
)
|
|
49
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return false
|
|
50
|
+
return composeProjectOwnsPort(result.stdout, project)
|
|
51
|
+
}
|
|
52
|
+
|
|
17
53
|
export function isValidHostPort(port: number): boolean {
|
|
18
54
|
return Number.isInteger(port) && port >= MIN_PORT && port <= MAX_PORT
|
|
19
55
|
}
|
|
@@ -62,6 +98,11 @@ export interface EnsureKongPortOptions {
|
|
|
62
98
|
interactive?: boolean
|
|
63
99
|
/** init wizard: slightly different copy */
|
|
64
100
|
context?: "dev" | "init"
|
|
101
|
+
/**
|
|
102
|
+
* Compose project for this directory. When given, a port published by that project is accepted
|
|
103
|
+
* rather than refused: that is this project's own stack, not a stranger's.
|
|
104
|
+
*/
|
|
105
|
+
composeProject?: string
|
|
65
106
|
}
|
|
66
107
|
|
|
67
108
|
/**
|
|
@@ -81,6 +122,13 @@ export async function ensureKongPort(
|
|
|
81
122
|
if (persisted !== null) {
|
|
82
123
|
if (!(await isPortInUse(persisted))) return persisted
|
|
83
124
|
|
|
125
|
+
// Occupied by this project's own stack is not a conflict, it is the normal state after
|
|
126
|
+
// `supatype self-host compose up -d`. Refusing here made a hand-run `up -d` followed by
|
|
127
|
+
// `push` impossible, and it stopped the validation soak, which does exactly that.
|
|
128
|
+
if (opts.composeProject !== undefined && portHeldByComposeProject(persisted, opts.composeProject)) {
|
|
129
|
+
return persisted
|
|
130
|
+
}
|
|
131
|
+
|
|
84
132
|
if (!interactive) {
|
|
85
133
|
fatalError(`Port ${persisted} is already in use (SUPATYPE_KONG_PORT in .env).`, [
|
|
86
134
|
"Stop the other service or set a different SUPATYPE_KONG_PORT.",
|