@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.
Files changed (143) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +150 -136
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/cli-version-embedded.d.ts.map +1 -1
  5. package/dist/cli-version-embedded.js +1 -1
  6. package/dist/cli-version-embedded.js.map +1 -1
  7. package/dist/commands/admin.d.ts +5 -5
  8. package/dist/commands/admin.d.ts.map +1 -1
  9. package/dist/commands/admin.js +10 -10
  10. package/dist/commands/admin.js.map +1 -1
  11. package/dist/commands/db.d.ts.map +1 -1
  12. package/dist/commands/db.js +23 -1
  13. package/dist/commands/db.js.map +1 -1
  14. package/dist/commands/dev.js +51 -51
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doctor.d.ts +0 -7
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +26 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/init.js +1 -1
  21. package/dist/commands/logs.d.ts +14 -1
  22. package/dist/commands/logs.d.ts.map +1 -1
  23. package/dist/commands/logs.js +33 -37
  24. package/dist/commands/logs.js.map +1 -1
  25. package/dist/commands/push.d.ts.map +1 -1
  26. package/dist/commands/push.js +18 -7
  27. package/dist/commands/push.js.map +1 -1
  28. package/dist/commands/status.d.ts +1 -1
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +33 -64
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/compose-local-server-image.d.ts +11 -0
  33. package/dist/compose-local-server-image.d.ts.map +1 -1
  34. package/dist/compose-local-server-image.js +18 -0
  35. package/dist/compose-local-server-image.js.map +1 -1
  36. package/dist/compose-services.d.ts +59 -0
  37. package/dist/compose-services.d.ts.map +1 -0
  38. package/dist/compose-services.js +160 -0
  39. package/dist/compose-services.js.map +1 -0
  40. package/dist/config.d.ts +1 -1
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/dev-compose.d.ts +1 -0
  43. package/dist/dev-compose.d.ts.map +1 -1
  44. package/dist/dev-compose.js +202 -30
  45. package/dist/dev-compose.js.map +1 -1
  46. package/dist/dev-ports.d.ts +24 -0
  47. package/dist/dev-ports.d.ts.map +1 -1
  48. package/dist/dev-ports.js +37 -0
  49. package/dist/dev-ports.js.map +1 -1
  50. package/dist/docker-postgres.d.ts +1 -1
  51. package/dist/docker-postgres.d.ts.map +1 -1
  52. package/dist/docker-postgres.js +2 -2
  53. package/dist/docker-postgres.js.map +1 -1
  54. package/dist/engine-floor.d.ts +50 -0
  55. package/dist/engine-floor.d.ts.map +1 -0
  56. package/dist/engine-floor.js +80 -0
  57. package/dist/engine-floor.js.map +1 -0
  58. package/dist/field-bounds.d.ts +68 -0
  59. package/dist/field-bounds.d.ts.map +1 -0
  60. package/dist/field-bounds.js +277 -0
  61. package/dist/field-bounds.js.map +1 -0
  62. package/dist/hooks-generator.d.ts +1 -1
  63. package/dist/hooks-generator.d.ts.map +1 -1
  64. package/dist/hooks-generator.js +78 -4
  65. package/dist/hooks-generator.js.map +1 -1
  66. package/dist/link.d.ts +1 -1
  67. package/dist/link.d.ts.map +1 -1
  68. package/dist/model-hooks.d.ts +44 -2
  69. package/dist/model-hooks.d.ts.map +1 -1
  70. package/dist/model-hooks.js +116 -12
  71. package/dist/model-hooks.js.map +1 -1
  72. package/dist/pgbouncer-userlist.d.ts +1 -1
  73. package/dist/pgbouncer-userlist.d.ts.map +1 -1
  74. package/dist/pgbouncer-userlist.js +1 -1
  75. package/dist/pgbouncer-userlist.js.map +1 -1
  76. package/dist/postgres-ctl.d.ts +5 -1
  77. package/dist/postgres-ctl.d.ts.map +1 -1
  78. package/dist/postgres-ctl.js +43 -4
  79. package/dist/postgres-ctl.js.map +1 -1
  80. package/dist/project-config.d.ts +4 -4
  81. package/dist/resolve-target.d.ts +1 -1
  82. package/dist/resolve-target.d.ts.map +1 -1
  83. package/dist/schema-ast-v2.d.ts +38 -4
  84. package/dist/schema-ast-v2.d.ts.map +1 -1
  85. package/dist/schema-ast-v2.js +87 -4
  86. package/dist/schema-ast-v2.js.map +1 -1
  87. package/dist/self-host-compose.d.ts.map +1 -1
  88. package/dist/self-host-compose.js +41 -18
  89. package/dist/self-host-compose.js.map +1 -1
  90. package/dist/studio-dev-server.d.ts +8 -1
  91. package/dist/studio-dev-server.d.ts.map +1 -1
  92. package/dist/studio-dev-server.js +1 -2
  93. package/dist/studio-dev-server.js.map +1 -1
  94. package/dist/type-extractor.d.ts.map +1 -1
  95. package/dist/type-extractor.js +309 -27
  96. package/dist/type-extractor.js.map +1 -1
  97. package/package.json +4 -3
  98. package/src/cli-version-embedded.ts +1 -1
  99. package/src/commands/admin.ts +10 -10
  100. package/src/commands/db.ts +27 -1
  101. package/src/commands/dev.ts +51 -51
  102. package/src/commands/doctor.ts +30 -0
  103. package/src/commands/init.ts +1 -1
  104. package/src/commands/logs.ts +34 -37
  105. package/src/commands/push.ts +29 -6
  106. package/src/commands/status.ts +34 -70
  107. package/src/compose-local-server-image.ts +17 -0
  108. package/src/compose-services.ts +174 -0
  109. package/src/config.ts +1 -1
  110. package/src/dev-compose.ts +226 -29
  111. package/src/dev-ports.ts +48 -0
  112. package/src/docker-postgres.ts +2 -2
  113. package/src/engine-floor.ts +113 -0
  114. package/src/field-bounds.ts +359 -0
  115. package/src/hooks-generator.ts +81 -4
  116. package/src/link.ts +1 -1
  117. package/src/model-hooks.ts +158 -12
  118. package/src/pgbouncer-userlist.ts +1 -1
  119. package/src/postgres-ctl.ts +44 -4
  120. package/src/project-config.ts +4 -4
  121. package/src/resolve-target.ts +1 -1
  122. package/src/schema-ast-v2.ts +114 -10
  123. package/src/self-host-compose.ts +41 -18
  124. package/src/studio-dev-server.ts +9 -3
  125. package/src/type-extractor.ts +374 -39
  126. package/tests/admin-ensure.test.ts +10 -10
  127. package/tests/compose-parsable-defaults.test.ts +60 -0
  128. package/tests/compose-services.test.ts +144 -0
  129. package/tests/dev-ports-owner.test.ts +40 -0
  130. package/tests/engine-floor.test.ts +102 -0
  131. package/tests/external-database-compose.test.ts +3 -3
  132. package/tests/field-bounds-matrix.test.ts +163 -0
  133. package/tests/field-bounds.test.ts +139 -0
  134. package/tests/field-validators.test.ts +139 -0
  135. package/tests/hooks-generator.test.ts +86 -0
  136. package/tests/local-server-image-env.test.ts +93 -0
  137. package/tests/model-constraints.test.ts +293 -0
  138. package/tests/model-hooks.test.ts +56 -0
  139. package/tests/port-in-use.test.ts +58 -0
  140. package/tests/readiness-poll.test.ts +84 -0
  141. package/tests/runtime-contract.test.ts +1 -1
  142. package/tests/type-extractor.test.ts +49 -0
  143. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,160 @@
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
+ * composeContext locates the generated compose file for this project.
21
+ *
22
+ * Returns undefined when the stack has never been generated, which is the
23
+ * ordinary case in a project that only uses `supatype dev`.
24
+ */
25
+ export function composeContext(cwd) {
26
+ const paths = selfHostComposePaths(cwd);
27
+ if (!existsSync(paths.composePath))
28
+ return undefined;
29
+ let projectName = "supatype";
30
+ try {
31
+ const config = loadConfig(cwd);
32
+ if (config)
33
+ projectName = composeProjectName(config.project.name);
34
+ }
35
+ catch {
36
+ // A project whose config will not load can still have a running stack, and
37
+ // reporting on it is more useful than refusing to.
38
+ }
39
+ return { composePath: paths.composePath, projectRoot: cwd, projectName };
40
+ }
41
+ /**
42
+ * composeServices asks Docker what the stack is running.
43
+ *
44
+ * `--all` so a stopped service is reported as stopped rather than vanishing,
45
+ * which is the question `supatype status` is usually asked.
46
+ */
47
+ export function composeServices(ctx) {
48
+ const result = spawnSync("docker", [...composeArgs(ctx), "ps", "--all", "--format", "json"], {
49
+ encoding: "utf8",
50
+ timeout: 15_000,
51
+ });
52
+ if (result.status !== 0 || !result.stdout)
53
+ return [];
54
+ return parseComposePs(result.stdout);
55
+ }
56
+ /**
57
+ * parseComposePs reads `docker compose ps --format json`.
58
+ *
59
+ * Compose v2 emits one JSON object per line; some versions emit a single JSON
60
+ * array instead. Both are accepted, because which one you get depends on the
61
+ * Docker version the developer happens to have.
62
+ */
63
+ export function parseComposePs(stdout) {
64
+ const trimmed = stdout.trim();
65
+ if (trimmed === "")
66
+ return [];
67
+ const rows = [];
68
+ if (trimmed.startsWith("[")) {
69
+ try {
70
+ const parsed = JSON.parse(trimmed);
71
+ if (Array.isArray(parsed))
72
+ rows.push(...parsed);
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ else {
79
+ for (const line of trimmed.split("\n")) {
80
+ if (line.trim() === "")
81
+ continue;
82
+ try {
83
+ rows.push(JSON.parse(line));
84
+ }
85
+ catch {
86
+ // A line that is not JSON is progress noise, not a service.
87
+ }
88
+ }
89
+ }
90
+ const services = [];
91
+ for (const row of rows) {
92
+ if (typeof row !== "object" || row === null)
93
+ continue;
94
+ const record = row;
95
+ const service = typeof record["Service"] === "string" ? record["Service"] : "";
96
+ if (service === "")
97
+ continue;
98
+ services.push({
99
+ service,
100
+ container: typeof record["Name"] === "string" ? record["Name"] : "",
101
+ state: typeof record["State"] === "string" ? record["State"] : "unknown",
102
+ ports: typeof record["Publishers"] === "string" ? record["Publishers"] : formatPorts(record["Publishers"]),
103
+ status: typeof record["Status"] === "string" ? record["Status"] : "",
104
+ });
105
+ }
106
+ services.sort((a, b) => a.service.localeCompare(b.service));
107
+ return services;
108
+ }
109
+ /**
110
+ * formatPorts renders the structured Publishers array Compose emits, keeping
111
+ * only the ports actually reachable from the host.
112
+ */
113
+ function formatPorts(publishers) {
114
+ if (!Array.isArray(publishers))
115
+ return "";
116
+ const published = [];
117
+ for (const entry of publishers) {
118
+ if (typeof entry !== "object" || entry === null)
119
+ continue;
120
+ const record = entry;
121
+ const host = record["PublishedPort"];
122
+ const target = record["TargetPort"];
123
+ if (typeof host === "number" && host > 0 && typeof target === "number") {
124
+ published.push(`${host}->${target}`);
125
+ }
126
+ }
127
+ return [...new Set(published)].join(", ");
128
+ }
129
+ /**
130
+ * composeArgs is the invocation prefix every compose call needs.
131
+ *
132
+ * The compose file lives under `.supatype/self-host/`, so the project directory
133
+ * has to be given explicitly: without it Compose resolves relative build
134
+ * contexts and env files against the file's own directory rather than the
135
+ * project root.
136
+ */
137
+ export function composeArgs(ctx) {
138
+ return ["compose", "-p", ctx.projectName, "--project-directory", ctx.projectRoot, "-f", ctx.composePath];
139
+ }
140
+ /**
141
+ * composeServiceNames lists the services the compose file defines.
142
+ *
143
+ * This is what `logs --service` should be validated against, not the running
144
+ * containers: a service that failed to start is exactly the one whose logs you
145
+ * want, and it has no container.
146
+ */
147
+ export function composeServiceNames(ctx) {
148
+ const result = spawnSync("docker", [...composeArgs(ctx), "config", "--services"], {
149
+ encoding: "utf8",
150
+ timeout: 15_000,
151
+ });
152
+ if (result.status !== 0 || !result.stdout)
153
+ return [];
154
+ return result.stdout
155
+ .split("\n")
156
+ .map((line) => line.trim())
157
+ .filter((line) => line !== "")
158
+ .sort();
159
+ }
160
+ //# sourceMappingURL=compose-services.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-services.js","sourceRoot":"","sources":["../src/compose-services.ts"],"names":[],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -30,7 +30,7 @@ export interface SelfHostConfig {
30
30
  */
31
31
  services?: {
32
32
  db?: ServiceVersionPin;
33
- gotrue?: ServiceVersionPin;
33
+ auth?: ServiceVersionPin;
34
34
  postgrest?: ServiceVersionPin;
35
35
  kong?: ServiceVersionPin;
36
36
  caddy?: ServiceVersionPin;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE9D,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAEhE,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,iHAAiH;IACjH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,GAAG,CAAC,EAAE;QACJ,kEAAkE;QAClE,UAAU,EAAE,MAAM,CAAA;QAClB,+BAA+B;QAC/B,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,GAAG,CAAC,EAAE;QACJ,gFAAgF;QAChF,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;QAC1B,+EAA+E;QAC/E,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,EAAE,iBAAiB,CAAA;QACtB,MAAM,CAAC,EAAE,iBAAiB,CAAA;QAC1B,SAAS,CAAC,EAAE,iBAAiB,CAAA;QAC7B,IAAI,CAAC,EAAE,iBAAiB,CAAA;QACxB,KAAK,CAAC,EAAE,iBAAiB,CAAA;QACzB,SAAS,CAAC,EAAE,iBAAiB,CAAA;KAC9B,CAAA;CACF;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEtG,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,MAAM,EAAE,qBAAqB,GAAG,qBAAqB,CAEjF;AAiCD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,qBAAqB,CA+B7E;AAcD;;;;;;;;;GASG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;gBAEf,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK/C;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAExD;AAqED,2EAA2E;AAC3E,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAsB,GAC1B,oBAAoB,CActB"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE9D,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAEhE,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,iHAAiH;IACjH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,GAAG,CAAC,EAAE;QACJ,kEAAkE;QAClE,UAAU,EAAE,MAAM,CAAA;QAClB,+BAA+B;QAC/B,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,GAAG,CAAC,EAAE;QACJ,gFAAgF;QAChF,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;QAC1B,+EAA+E;QAC/E,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,EAAE,iBAAiB,CAAA;QACtB,IAAI,CAAC,EAAE,iBAAiB,CAAA;QACxB,SAAS,CAAC,EAAE,iBAAiB,CAAA;QAC7B,IAAI,CAAC,EAAE,iBAAiB,CAAA;QACxB,KAAK,CAAC,EAAE,iBAAiB,CAAA;QACzB,SAAS,CAAC,EAAE,iBAAiB,CAAA;KAC9B,CAAA;CACF;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEtG,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,MAAM,EAAE,qBAAqB,GAAG,qBAAqB,CAEjF;AAiCD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,qBAAqB,CA+B7E;AAcD;;;;;;;;;GASG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;gBAEf,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK/C;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAExD;AAqED,2EAA2E;AAC3E,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAsB,GAC1B,oBAAoB,CActB"}
@@ -27,6 +27,7 @@ export declare function usesLocalDockerEngineDb(config: SupatypeProjectConfig, e
27
27
  * Remote self-host → set `connection` in config or pass `--connection`.
28
28
  */
29
29
  export declare function resolveHostEngineDatabaseUrl(cwd: string, config: SupatypeProjectConfig, explicit?: string): Promise<string>;
30
+ export declare function upsertDevComposeEnv(cwd: string, config: SupatypeProjectConfig, anonKey: string, serviceRoleKey: string, kongPort: number, devDbPort?: number): void;
30
31
  /**
31
32
  * `supatype diff` when `provider: docker`. Uses in-compose schema-engine unless
32
33
  * `overrides.engine` is set: then Postgres is published to the host and diff runs
@@ -1 +1 @@
1
- {"version":3,"file":"dev-compose.d.ts","sourceRoot":"","sources":["../src/dev-compose.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAA;AAiB5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAI7D,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AA2BjF,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI,CASrF;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAA;CACf;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD;AA+BD;;;GAGG;AACH,wBAAsB,oCAAoC,CACxD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,qBAAqB,EAC7B,KAAK,CAAC,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAOT;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,qBAAqB,EAC7B,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAQjB;AA8gBD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CAsDtG;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChG;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2QtH"}
1
+ {"version":3,"file":"dev-compose.d.ts","sourceRoot":"","sources":["../src/dev-compose.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,EAQL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAA;AAkB5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAQ7D,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AA4BjF,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI,CASrF;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAA;CACf;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD;AA+BD;;;GAGG;AACH,wBAAsB,oCAAoC,CACxD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,qBAAqB,EAC7B,KAAK,CAAC,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAOT;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,qBAAqB,EAC7B,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAoED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,IAAI,CAuEN;AAogBD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CAsDtG;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChG;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+QtH"}
@@ -8,7 +8,8 @@ import { spawnSync } from "node:child_process";
8
8
  import { startProxyDevApp, resolveProxyDevScript } from "./app/proxy-dev-app.js";
9
9
  import { loadSchemaAst } from "./config.js";
10
10
  import { withComposeSchemaPushLock } from "./schema-push-lock.js";
11
- import { connectionString, projectRootFromConfig, resolveRuntimeProvider, schemaPathFromProject, usesExternalDatabase, } from "./project-config.js";
11
+ import { connectionString, projectRootFromConfig, resolveRuntimeProvider, hooksPathFromProject, schemaPathFromProject, usesExternalDatabase, } from "./project-config.js";
12
+ import { syncManifestHooks, writeHooksModule } from "./model-hooks.js";
12
13
  import { signJwt } from "./jwt.js";
13
14
  import { ensureDevDbPort, ensureKongPort } from "./dev-ports.js";
14
15
  import { handleComposeProjectRename } from "./compose-rename.js";
@@ -18,11 +19,11 @@ import { ensureDevApiConfig } from "./ensure-dev-api-config.js";
18
19
  import { COMPOSE_PINNED_IMAGE_ENV_KEYS, composeDockerImageEnv, composeProjectName, exitComposeFailed, runDockerCompose, schemaEngineImageForPush, writeSelfHostCompose, } from "./self-host-compose.js";
19
20
  import { hasEngineOverride } from "./binary-cache.js";
20
21
  import { STUDIO_DEV_PORT, startStudioViteDevServer } from "./studio-dev-server.js";
21
- import { ensureLocalServerDockerImage } from "./compose-local-server-image.js";
22
+ import { ensureLocalServerDockerImage, usesLocalServerImage, LOCAL_SERVER_DOCKER_IMAGE, } from "./compose-local-server-image.js";
22
23
  import { ensureEngine, engineRequest } from "./engine-client.js";
23
24
  import { writeSchemaSourcePushArtifacts } from "./schema-sources.js";
24
25
  import { readEnvValue, upsertEnvFile } from "./env-file.js";
25
- import { devJwtSecret, devPostgresPassword, seedMissingDatabaseIdentity, seedMissingLocalSecrets, } from "./local-secrets.js";
26
+ import { devAuthenticatorPassword, devJwtSecret, devPostgresPassword, seedMissingDatabaseIdentity, seedMissingLocalSecrets, } from "./local-secrets.js";
26
27
  import { writeLocalEnvironment } from "./link.js";
27
28
  import { registerDevShutdown } from "./dev-shutdown.js";
28
29
  import { filterComposeNoise, formatEnginePushMessage, parseEngineJsonOutput, parseEnginePushOutput, } from "./engine-push-output.js";
@@ -90,7 +91,7 @@ export async function ensureDockerDbPublishedForHostEngine(cwd, config, brand) {
90
91
  "Set overrides.engine in supatype.local.config.ts or pass --connection.");
91
92
  }
92
93
  const project = composeProjectName(config.project.name);
93
- const kongPort = await resolveKongPort(cwd);
94
+ const kongPort = await resolveKongPort(cwd, project);
94
95
  const devDbPort = await resolveDevDbPort(cwd);
95
96
  const now = Math.floor(Date.now() / 1000);
96
97
  const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 };
@@ -131,8 +132,43 @@ export async function resolveHostEngineDatabaseUrl(cwd, config, explicit) {
131
132
  }
132
133
  return connectionString(config);
133
134
  }
134
- async function resolveKongPort(cwd) {
135
- return ensureKongPort(cwd, { context: "dev" });
135
+ /**
136
+ * `email.provider` and `email.smtp` for the compose server service.
137
+ *
138
+ * The native dev path has mapped these since it existed; the compose path never
139
+ * did, so a project that asked for smtp got the auth service's noop client and
140
+ * no message at all. Console is the documented default, and it at least says
141
+ * that a message would have been sent.
142
+ */
143
+ function composeMailerEnv(config) {
144
+ const email = config.email;
145
+ const out = {
146
+ // Mailer.MailerProvider, so the doubled word is the real name.
147
+ SUPATYPE_MAILER_MAILER_PROVIDER: email?.provider ?? "console",
148
+ };
149
+ const smtp = email?.smtp;
150
+ if (email?.provider !== "smtp" || smtp === undefined)
151
+ return out;
152
+ if (smtp.host !== undefined && smtp.host !== "")
153
+ out.SUPATYPE_SMTP_HOST = smtp.host;
154
+ if (smtp.port !== undefined)
155
+ out.SUPATYPE_SMTP_PORT = String(smtp.port);
156
+ if (smtp.user !== undefined && smtp.user !== "")
157
+ out.SUPATYPE_SMTP_USER = smtp.user;
158
+ if (smtp.pass !== undefined && smtp.pass !== "")
159
+ out.SUPATYPE_SMTP_PASS = smtp.pass;
160
+ if (smtp.admin_email !== undefined && smtp.admin_email !== "") {
161
+ out.SUPATYPE_SMTP_ADMIN_EMAIL = smtp.admin_email;
162
+ }
163
+ if (smtp.sender_name !== undefined && smtp.sender_name !== "") {
164
+ out.SUPATYPE_SMTP_SENDER_NAME = smtp.sender_name;
165
+ }
166
+ return out;
167
+ }
168
+ async function resolveKongPort(cwd, composeProject) {
169
+ // The compose project is passed so an already-running stack of *this* project counts as
170
+ // available rather than as a collision.
171
+ return ensureKongPort(cwd, { context: "dev", ...(composeProject !== undefined && { composeProject }) });
136
172
  }
137
173
  /**
138
174
  * Bring up the compose `db` service and wait for it to answer.
@@ -155,7 +191,7 @@ async function startComposeDatabase(config, paths, cwd, project, brand, timeoutM
155
191
  }
156
192
  await waitComposeHealthy(paths, cwd, timeoutMs, project);
157
193
  }
158
- function upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort, localServerImage) {
194
+ export function upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort) {
159
195
  const apiUrl = `http://localhost:${kongPort}`;
160
196
  const imagePins = composeDockerImageEnv(config);
161
197
  // `POSTGRES_PASSWORD` and `JWT_SECRET` are deliberately absent.
@@ -187,9 +223,9 @@ function upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, dev
187
223
  SUPATYPE_KONG_PORT: String(kongPort),
188
224
  API_EXTERNAL_URL: apiUrl,
189
225
  SITE_URL: apiUrl,
190
- GOTRUE_MAILER_AUTOCONFIRM: "true",
226
+ SUPATYPE_MAILER_AUTOCONFIRM: "true",
227
+ ...composeMailerEnv(config),
191
228
  ...imagePins,
192
- ...(localServerImage !== undefined && { SUPATYPE_SERVER_IMAGE: localServerImage }),
193
229
  };
194
230
  // Never for an external database: this URL describes the `db` container, which that project does
195
231
  // not have. Writing it overwrote the operator's own DATABASE_URL, the value the whole stack and
@@ -206,15 +242,28 @@ function upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, dev
206
242
  updates.DATABASE_URL =
207
243
  `postgresql://${dbUser}:${devPostgresPassword(cwd)}@localhost:${devDbPort}/${dbName}?sslmode=disable`;
208
244
  }
209
- const removeImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter((key) => !(key in imagePins));
245
+ // `SUPATYPE_SERVER_IMAGE` is written here, not passed in, and it is deliberately *not* marked
246
+ // managed. The managed marker means "this value came from `versions` in the config and is mine to
247
+ // clean up when the pin goes away". The locally built image comes from `overrides`, so removing
248
+ // it as an unpinned managed key was wrong: any `.env` write that did not know about the local
249
+ // image deleted it, and compose then recreated the server from the published image.
250
+ const wantsLocalServer = usesLocalServerImage(cwd, config);
251
+ if (wantsLocalServer)
252
+ updates.SUPATYPE_SERVER_IMAGE = LOCAL_SERVER_DOCKER_IMAGE;
253
+ const managedImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter((key) => !(wantsLocalServer && key === "SUPATYPE_SERVER_IMAGE"));
254
+ const removeImageKeys = managedImageKeys.filter((key) => !(key in imagePins));
210
255
  upsertEnvFile(cwd, updates, {
211
256
  removeManaged: removeImageKeys,
212
- managed: COMPOSE_PINNED_IMAGE_ENV_KEYS,
257
+ managed: managedImageKeys,
258
+ // A local image left in `.env` after the project stopped asking for one would keep pointing
259
+ // compose at a stale build, and it carries no marker for `removeManaged` to act on.
260
+ ...(!wantsLocalServer &&
261
+ !("SUPATYPE_SERVER_IMAGE" in imagePins) && { remove: ["SUPATYPE_SERVER_IMAGE"] }),
213
262
  });
214
263
  }
215
264
  /** Keep compose + Studio on the same freshly signed dev JWTs; sync optional image pins from config. */
216
- function ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort, localServerImage) {
217
- upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort, localServerImage);
265
+ function ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort) {
266
+ upsertDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort);
218
267
  }
219
268
  async function waitComposeHealthy(paths, cwd, maxMs, composeProject) {
220
269
  const composeDir = dirname(paths.composePath);
@@ -273,31 +322,98 @@ function dumpComposeDbLogs(paths, cwd, composeProject, reason) {
273
322
  console.error(`[supatype] Postgres logs after ${reason} (saved ${logPath}):`);
274
323
  console.error(tail);
275
324
  }
276
- async function waitKongReady(kongPort, maxSec) {
277
- const base = `http://localhost:${kongPort}`;
325
+ /**
326
+ * The gateway as the CLI reaches it, over IPv4.
327
+ *
328
+ * Not `localhost`. Docker publishes on `0.0.0.0`, so the IPv4 address is the one that is certainly
329
+ * bound, while `localhost` resolves to `::1` first on Windows and macOS. Docker Desktop does forward
330
+ * IPv6 through a relay, and when that relay is unhealthy it accepts the connection and then never
331
+ * answers, so every readiness poll hangs rather than failing.
332
+ *
333
+ * The URLs written to `.env` and printed for the operator stay on `localhost`: those are for a
334
+ * browser, where the name is the friendlier thing to see and the browser will fall back on its own.
335
+ */
336
+ function loopbackBase(port) {
337
+ return `http://127.0.0.1:${port}`;
338
+ }
339
+ /**
340
+ * `fetch` with a deadline, because the built-in has none.
341
+ *
342
+ * A poll loop bounded by `maxSec` iterations is only bounded if each attempt can end. Against a
343
+ * socket that accepts and never responds, an un-timed `fetch` never settles, the loop never reaches
344
+ * its second iteration, and a wait advertised as 120 seconds runs until someone kills it. That is
345
+ * how `supatype push` came to hang indefinitely after reporting the schema applied.
346
+ */
347
+ async function fetchWithin(url, ms, init) {
348
+ return await fetch(url, { ...init, signal: AbortSignal.timeout(ms) });
349
+ }
350
+ /**
351
+ * Print what the stack is doing when the gateway never comes up.
352
+ *
353
+ * A 503 through the gateway means Kong is running and something behind it is
354
+ * not, and until now the only thing said was that the gateway was not ready.
355
+ * That sends people to look at Kong, which is almost never the problem, and in
356
+ * CI the containers are gone by the time anything else can inspect them.
357
+ */
358
+ function reportUnhealthyStack(composePath, cwd, project) {
359
+ const capture = (args) => {
360
+ const result = spawnSync("docker", ["compose", "-p", project, "--project-directory", cwd, "-f", composePath, ...args], {
361
+ encoding: "utf8",
362
+ cwd,
363
+ });
364
+ return `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
365
+ };
366
+ console.error("");
367
+ console.error("[supatype] The gateway is up but a service behind it is not. Container state:");
368
+ const state = capture(["ps", "-a"]);
369
+ console.error(state === "" ? " (no containers)" : state);
370
+ // The two the gateway health check depends on, and db because both need it.
371
+ for (const service of ["server", "realtime", "db"]) {
372
+ const logs = capture(["logs", "--no-color", "--tail", "40", service]);
373
+ if (logs === "")
374
+ continue;
375
+ console.error("");
376
+ console.error(`[supatype] last 40 lines from ${service}:`);
377
+ console.error(logs);
378
+ }
379
+ console.error("");
380
+ }
381
+ async function waitKongReady(kongPort, maxSec, stack) {
382
+ const base = loopbackBase(kongPort);
383
+ // Remembered so the failure can name the unhealthy upstream. Blaming "the
384
+ // Kong gateway" sent more than one investigation after Kong when Kong was
385
+ // fine and a service behind it was not.
386
+ let lastAuth = "no response";
387
+ let lastRealtime = "no response";
278
388
  for (let i = 0; i < maxSec; i++) {
279
389
  try {
280
390
  const [auth, realtime] = await Promise.all([
281
- fetch(`${base}/auth/v1/health`),
282
- fetch(`${base}/realtime/v1/health`),
391
+ fetchWithin(`${base}/auth/v1/health`, 2000),
392
+ fetchWithin(`${base}/realtime/v1/health`, 2000),
283
393
  ]);
394
+ lastAuth = `HTTP ${auth.status}`;
395
+ lastRealtime = `HTTP ${realtime.status}`;
284
396
  if (auth.ok && realtime.ok)
285
397
  return;
286
398
  }
287
- catch {
288
- /* retry */
399
+ catch (err) {
400
+ lastAuth = lastRealtime = err instanceof Error ? err.message : String(err);
289
401
  }
290
402
  await new Promise((r) => setTimeout(r, 1000));
291
403
  }
292
- throw new Error(`Kong gateway at ${base} did not become ready within ${maxSec}s`);
404
+ if (stack !== undefined) {
405
+ reportUnhealthyStack(stack.composePath, stack.cwd, stack.project);
406
+ }
407
+ throw new Error(`Gateway at ${base} did not become ready within ${maxSec}s ` +
408
+ `(auth/v1/health: ${lastAuth}, realtime/v1/health: ${lastRealtime})`);
293
409
  }
294
410
  /** Kong may be up while server → storage is still starting (503 or upstream errors). */
295
411
  async function waitStorageApiReady(kongPort, serviceRoleKey, maxSec) {
296
- const url = `http://localhost:${kongPort}/storage/v1/bucket`;
412
+ const url = `${loopbackBase(kongPort)}/storage/v1/bucket`;
297
413
  const headers = { Authorization: `Bearer ${serviceRoleKey}` };
298
414
  for (let i = 0; i < maxSec; i++) {
299
415
  try {
300
- const res = await fetch(url, { headers });
416
+ const res = await fetchWithin(url, 2000, { headers });
301
417
  if (res.ok)
302
418
  return;
303
419
  const body = await res.text();
@@ -315,7 +431,7 @@ async function waitStorageApiReady(kongPort, serviceRoleKey, maxSec) {
315
431
  console.warn(`[supatype] Storage API at ${url} did not become ready within ${maxSec}s, bucket provisioning may fail.`);
316
432
  }
317
433
  async function provisionDockerStorageBuckets(ast, kongPort, serviceRoleKey) {
318
- await provisionBucketsFromAst(ast, `http://localhost:${kongPort}/storage/v1`, serviceRoleKey);
434
+ await provisionBucketsFromAst(ast, `${loopbackBase(kongPort)}/storage/v1`, serviceRoleKey);
319
435
  }
320
436
  let _lastPushedAst = null;
321
437
  let _lastFailedAst = null;
@@ -328,6 +444,20 @@ let _composePushQueued = false;
328
444
  async function refreshSchemaArtifacts(cwd, config, ast) {
329
445
  const supatypeDir = join(cwd, ".supatype");
330
446
  const adminConfigPath = join(supatypeDir, "admin-config.json");
447
+ // Before the engine gate below: these are read straight off the AST, so a push whose engine is
448
+ // unavailable should still leave the server the right maps. The server watches this file, so a
449
+ // hook or validator added to the schema takes effect without restarting the stack.
450
+ //
451
+ // `dev` used to skip both entirely, because only the `direct`/`local` push branch wrote them.
452
+ // Every project running on Compose therefore had a manifest with no `hooks` and no `validators`
453
+ // key, and the server, having nothing to call, ran neither. A hook silently not firing is bad; a
454
+ // validator silently not firing means a write the schema says is checked is accepted with a 201.
455
+ const hooksModule = writeHooksModule(cwd, hooksPathFromProject(config, cwd), ast);
456
+ if (hooksModule !== null)
457
+ console.log(`[supatype] Hook handler types written to ${hooksModule}`);
458
+ if (syncManifestHooks(cwd, ast)) {
459
+ console.log("[supatype] Hook and validator maps written to .supatype/manifest.json");
460
+ }
331
461
  try {
332
462
  await ensureEngine();
333
463
  }
@@ -554,7 +684,7 @@ export async function diffSchemaDocker(cwd, config) {
554
684
  schema: pgSchema,
555
685
  });
556
686
  }
557
- const kongPort = await resolveKongPort(cwd);
687
+ const kongPort = await resolveKongPort(cwd, project);
558
688
  const now = Math.floor(Date.now() / 1000);
559
689
  const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 };
560
690
  const anonKey = signJwt({ ...jwtBase, role: "anon" }, devJwtSecret(cwd));
@@ -594,7 +724,7 @@ export async function pushSchemaDocker(cwd, config) {
594
724
  throw new Error("pushSchemaDocker requires provider: docker");
595
725
  }
596
726
  const project = composeProjectName(config.project.name);
597
- const kongPort = await resolveKongPort(cwd);
727
+ const kongPort = await resolveKongPort(cwd, project);
598
728
  // No dev db port for an external database: `ensureDevDbPort` allocates a host port for the `db`
599
729
  // container *and persists a matching DATABASE_URL*, which overwrote the operator's own URL, the
600
730
  // one the whole stack and every CLI command reads.
@@ -620,7 +750,7 @@ export async function pushSchemaDocker(cwd, config) {
620
750
  if (upGateway !== 0) {
621
751
  exitComposeFailed(upGateway, "Could not start the Compose gateway stack.", pushBrand);
622
752
  }
623
- await waitKongReady(kongPort, 120);
753
+ await waitKongReady(kongPort, 120, { composePath: paths.composePath, cwd, project });
624
754
  await waitStorageApiReady(kongPort, serviceRoleKey, 90);
625
755
  await provisionDockerStorageBuckets(ast, kongPort, serviceRoleKey);
626
756
  await ensureFirstAdminUserForProject(cwd, config, {
@@ -635,7 +765,7 @@ export async function runDevCompose(cwd, config, opts) {
635
765
  // Per-project compose name + port isolate this project from any other Supatype
636
766
  // stack on the machine (own containers, volumes, network, and gateway port).
637
767
  const project = composeProjectName(config.project.name);
638
- const kongPort = await resolveKongPort(cwd);
768
+ const kongPort = await resolveKongPort(cwd, project);
639
769
  // No dev db port for an external database: `ensureDevDbPort` allocates a host port for the `db`
640
770
  // container *and persists a matching DATABASE_URL*, which overwrote the operator's own URL, the
641
771
  // one the whole stack and every CLI command reads.
@@ -648,7 +778,7 @@ export async function runDevCompose(cwd, config, opts) {
648
778
  const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, devJwtSecret(cwd));
649
779
  const devBrand = { intro: "Local development" };
650
780
  const localServerImage = await ensureLocalServerDockerImage(cwd, config, devBrand);
651
- ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort, localServerImage);
781
+ ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort);
652
782
  console.log(`[supatype] provider docker, starting self-host Compose stack (project ${project}, gateway :${kongPort})...`);
653
783
  const paths = writeSelfHostCompose(cwd, config, { devLocal: true });
654
784
  if (ensureDevApiConfig(cwd)) {
@@ -689,6 +819,9 @@ export async function runDevCompose(cwd, config, opts) {
689
819
  await startComposeDatabase(config, paths, cwd, project, devBrand, 180_000, endDevSession);
690
820
  // Settle before DDL: pg_isready can pass slightly before the instance is stable.
691
821
  await new Promise((r) => setTimeout(r, 3000));
822
+ if (!usesExternalDatabase(config)) {
823
+ reconcileAuthenticatorPassword(paths, cwd, project);
824
+ }
692
825
  // A: apply schema before realtime (and the rest of the stack) starts decoding WAL.
693
826
  const schemaPath = schemaPathFromProject(config, cwd);
694
827
  {
@@ -753,7 +886,7 @@ export async function runDevCompose(cwd, config, opts) {
753
886
  }
754
887
  }
755
888
  console.log("[supatype] Waiting for API gateway...");
756
- await waitKongReady(kongPort, 120);
889
+ await waitKongReady(kongPort, 120, { composePath: paths.composePath, cwd, project });
757
890
  console.log("[supatype] Waiting for storage API...");
758
891
  await waitStorageApiReady(kongPort, serviceRoleKey, 90);
759
892
  await ensureFirstAdminUserForProject(cwd, config, {
@@ -786,7 +919,7 @@ export async function runDevCompose(cwd, config, opts) {
786
919
  cwd,
787
920
  studioOverride,
788
921
  pidDir,
789
- serviceRoleKey,
922
+ anonKey,
790
923
  proxyTarget: `http://localhost:${kongPort}`,
791
924
  viteSupatypeUrl: `http://localhost:${STUDIO_DEV_PORT}`,
792
925
  basePath: "/studio/",
@@ -858,6 +991,45 @@ function astHasSystemAuthRelation(ast) {
858
991
  }
859
992
  return false;
860
993
  }
994
+ /**
995
+ * Set `authenticator`'s password to the one `.env` holds, every time the stack starts.
996
+ *
997
+ * The Postgres image passwords that role from `AUTHENTICATOR_PASSWORD` in its init scripts, which
998
+ * run once, on an empty data directory. So the value the role actually has is whatever `.env` said
999
+ * the day the volume was created, and `.env` can move afterwards. When the two diverge PostgREST
1000
+ * cannot log in, exits, and every REST request answers 502 with the real reason visible only in a
1001
+ * container log the developer has no reason to read.
1002
+ *
1003
+ * Reconciling here makes `.env` the answer to what the password is, rather than the volume's
1004
+ * birthday. It is idempotent, and it runs before the schema push so the API is already reachable by
1005
+ * the time the stack reports ready.
1006
+ */
1007
+ function reconcileAuthenticatorPassword(paths, cwd, composeProject) {
1008
+ const composeDir = dirname(paths.composePath);
1009
+ const owner = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin");
1010
+ const database = readEnvValue(cwd, "POSTGRES_DB", "supatype");
1011
+ const result = spawnSync("docker", [
1012
+ "compose", "-p", composeProject, "-f", paths.composePath,
1013
+ "exec", "-T",
1014
+ "-e", `PGPASSWORD=${devPostgresPassword(cwd)}`,
1015
+ "-e", `SUPATYPE_AUTHENTICATOR_PASSWORD=${devAuthenticatorPassword(cwd)}`,
1016
+ "db", "psql", "-v", "ON_ERROR_STOP=1", "-U", owner, "-d", database,
1017
+ ], {
1018
+ cwd: composeDir,
1019
+ encoding: "utf8",
1020
+ timeout: 10_000,
1021
+ // `\getenv` reads the value from the container's environment, so the password is never a
1022
+ // `docker exec` argument, which any process listing on the machine would show.
1023
+ input: [
1024
+ "\\getenv pw SUPATYPE_AUTHENTICATOR_PASSWORD",
1025
+ "ALTER ROLE authenticator WITH LOGIN PASSWORD :'pw';",
1026
+ "",
1027
+ ].join("\n"),
1028
+ });
1029
+ if (result.status !== 0) {
1030
+ console.warn("[supatype] Could not set the authenticator password; the REST API may answer 502.");
1031
+ }
1032
+ }
861
1033
  function grantAuthSchemaAccess(paths, cwd, composeProject) {
862
1034
  const composeDir = dirname(paths.composePath);
863
1035
  const baseArgs = [