@tdk-landscape/tdk-cli-core 1.3.49 → 1.3.51

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 (311) hide show
  1. package/.claude/skills/layer-autoresearch/SKILL.md +48 -0
  2. package/.claude/skills/tdk-troubleshoot/SKILL.md +222 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +40 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
  6. package/.github/dependabot.yml +33 -0
  7. package/.github/pull_request_template.md +5 -0
  8. package/.github/workflows/ci.yml +187 -0
  9. package/.github/workflows/release-binaries.yml +19 -9
  10. package/.github/zizmor.yml +9 -0
  11. package/CODE_OF_CONDUCT.md +9 -0
  12. package/CONTRIBUTING.md +42 -0
  13. package/LICENSE +21 -0
  14. package/README.md +151 -0
  15. package/SECURITY.md +17 -0
  16. package/benchmarks/results/container-scale-2026-09-25T20-54-49-736Z.json +45 -0
  17. package/benchmarks/results/container-scale-2026-09-25T21-01-20-540Z.json +150 -0
  18. package/benchmarks/results/container-scale-2026-09-25T21-15-01-441Z.json +73 -0
  19. package/benchmarks/results/container-scale-2026-09-25T22-09-01-630Z.json +77 -0
  20. package/benchmarks/results/container-scale-2026-09-25T22-18-28-371Z.json +202 -0
  21. package/cli/README.md +25 -0
  22. package/cli/biome.json +1 -1
  23. package/cli/dist/cli.d.ts +3 -0
  24. package/cli/dist/cli.d.ts.map +1 -0
  25. package/cli/dist/cli.js +64 -0
  26. package/cli/dist/cli.js.map +1 -0
  27. package/cli/dist/commands/completion.d.ts +3 -0
  28. package/cli/dist/commands/completion.d.ts.map +1 -0
  29. package/cli/dist/commands/completion.js +294 -0
  30. package/cli/dist/commands/completion.js.map +1 -0
  31. package/cli/dist/commands/config.d.ts +3 -0
  32. package/cli/dist/commands/config.d.ts.map +1 -0
  33. package/cli/dist/commands/config.js +231 -0
  34. package/cli/dist/commands/config.js.map +1 -0
  35. package/cli/dist/commands/doctor.d.ts +19 -0
  36. package/cli/dist/commands/doctor.d.ts.map +1 -0
  37. package/cli/dist/commands/doctor.js +745 -0
  38. package/cli/dist/commands/doctor.js.map +1 -0
  39. package/cli/dist/commands/down.d.ts +3 -0
  40. package/cli/dist/commands/down.d.ts.map +1 -0
  41. package/cli/dist/commands/down.js +32 -0
  42. package/cli/dist/commands/down.js.map +1 -0
  43. package/cli/dist/commands/help.d.ts +2 -0
  44. package/cli/dist/commands/help.d.ts.map +1 -0
  45. package/cli/dist/commands/help.js +138 -0
  46. package/cli/dist/commands/help.js.map +1 -0
  47. package/cli/dist/commands/networks.d.ts +3 -0
  48. package/cli/dist/commands/networks.d.ts.map +1 -0
  49. package/cli/dist/commands/networks.js +260 -0
  50. package/cli/dist/commands/networks.js.map +1 -0
  51. package/cli/dist/commands/project.d.ts +3 -0
  52. package/cli/dist/commands/project.d.ts.map +1 -0
  53. package/cli/dist/commands/project.js +370 -0
  54. package/cli/dist/commands/project.js.map +1 -0
  55. package/cli/dist/commands/projects.d.ts +3 -0
  56. package/cli/dist/commands/projects.d.ts.map +1 -0
  57. package/cli/dist/commands/projects.js +71 -0
  58. package/cli/dist/commands/projects.js.map +1 -0
  59. package/cli/dist/commands/resource.d.ts +69 -0
  60. package/cli/dist/commands/resource.d.ts.map +1 -0
  61. package/cli/dist/commands/resource.js +579 -0
  62. package/cli/dist/commands/resource.js.map +1 -0
  63. package/cli/dist/commands/resources.d.ts +3 -0
  64. package/cli/dist/commands/resources.d.ts.map +1 -0
  65. package/cli/dist/commands/resources.js +73 -0
  66. package/cli/dist/commands/resources.js.map +1 -0
  67. package/cli/dist/commands/stack.d.ts +3 -0
  68. package/cli/dist/commands/stack.d.ts.map +1 -0
  69. package/cli/dist/commands/stack.js +97 -0
  70. package/cli/dist/commands/stack.js.map +1 -0
  71. package/cli/dist/commands/stacks.d.ts +3 -0
  72. package/cli/dist/commands/stacks.d.ts.map +1 -0
  73. package/cli/dist/commands/stacks.js +43 -0
  74. package/cli/dist/commands/stacks.js.map +1 -0
  75. package/cli/dist/commands/status.d.ts +3 -0
  76. package/cli/dist/commands/status.d.ts.map +1 -0
  77. package/cli/dist/commands/status.js +71 -0
  78. package/cli/dist/commands/status.js.map +1 -0
  79. package/cli/dist/commands/ui.d.ts +3 -0
  80. package/cli/dist/commands/ui.d.ts.map +1 -0
  81. package/cli/dist/commands/ui.js +445 -0
  82. package/cli/dist/commands/ui.js.map +1 -0
  83. package/cli/dist/commands/up.d.ts +3 -0
  84. package/cli/dist/commands/up.d.ts.map +1 -0
  85. package/cli/dist/commands/up.js +124 -0
  86. package/cli/dist/commands/up.js.map +1 -0
  87. package/cli/dist/commands/upgrade.d.ts +11 -0
  88. package/cli/dist/commands/upgrade.d.ts.map +1 -0
  89. package/cli/dist/commands/upgrade.js +496 -0
  90. package/cli/dist/commands/upgrade.js.map +1 -0
  91. package/cli/dist/commands/version.d.ts +3 -0
  92. package/cli/dist/commands/version.d.ts.map +1 -0
  93. package/cli/dist/commands/version.js +9 -0
  94. package/cli/dist/commands/version.js.map +1 -0
  95. package/cli/dist/components/Accessible.d.ts +8 -0
  96. package/cli/dist/components/Accessible.d.ts.map +1 -0
  97. package/cli/dist/components/Accessible.js +10 -0
  98. package/cli/dist/components/Accessible.js.map +1 -0
  99. package/cli/dist/components/BaseTooltip.d.ts +4 -0
  100. package/cli/dist/components/BaseTooltip.d.ts.map +1 -0
  101. package/cli/dist/components/BaseTooltip.js +28 -0
  102. package/cli/dist/components/BaseTooltip.js.map +1 -0
  103. package/cli/dist/components/DetailPanel.d.ts +4 -0
  104. package/cli/dist/components/DetailPanel.d.ts.map +1 -0
  105. package/cli/dist/components/DetailPanel.js +18 -0
  106. package/cli/dist/components/DetailPanel.js.map +1 -0
  107. package/cli/dist/components/FileTree.d.ts +4 -0
  108. package/cli/dist/components/FileTree.d.ts.map +1 -0
  109. package/cli/dist/components/FileTree.js +47 -0
  110. package/cli/dist/components/FileTree.js.map +1 -0
  111. package/cli/dist/components/ResourceSelectInput.d.ts +4 -0
  112. package/cli/dist/components/ResourceSelectInput.d.ts.map +1 -0
  113. package/cli/dist/components/ResourceSelectInput.js +7 -0
  114. package/cli/dist/components/ResourceSelectInput.js.map +1 -0
  115. package/cli/dist/components/ResourceTable.d.ts +4 -0
  116. package/cli/dist/components/ResourceTable.d.ts.map +1 -0
  117. package/cli/dist/components/ResourceTable.js +18 -0
  118. package/cli/dist/components/ResourceTable.js.map +1 -0
  119. package/cli/dist/components/TabBar.d.ts +4 -0
  120. package/cli/dist/components/TabBar.d.ts.map +1 -0
  121. package/cli/dist/components/TabBar.js +17 -0
  122. package/cli/dist/components/TabBar.js.map +1 -0
  123. package/cli/dist/components/Tooltip.d.ts +12 -0
  124. package/cli/dist/components/Tooltip.d.ts.map +1 -0
  125. package/cli/dist/components/Tooltip.js +12 -0
  126. package/cli/dist/components/Tooltip.js.map +1 -0
  127. package/cli/dist/components/index.d.ts +8 -0
  128. package/cli/dist/components/index.d.ts.map +1 -0
  129. package/cli/dist/components/index.js +8 -0
  130. package/cli/dist/components/index.js.map +1 -0
  131. package/cli/dist/config/platform-standards.d.ts +159 -0
  132. package/cli/dist/config/platform-standards.d.ts.map +1 -0
  133. package/cli/dist/config/platform-standards.js +181 -0
  134. package/cli/dist/config/platform-standards.js.map +1 -0
  135. package/cli/dist/generator/extension-fetch.d.ts +25 -0
  136. package/cli/dist/generator/extension-fetch.d.ts.map +1 -0
  137. package/cli/dist/generator/extension-fetch.js +259 -0
  138. package/cli/dist/generator/extension-fetch.js.map +1 -0
  139. package/cli/dist/generator/template-engine.d.ts +62 -0
  140. package/cli/dist/generator/template-engine.d.ts.map +1 -0
  141. package/cli/dist/generator/template-engine.js +623 -0
  142. package/cli/dist/generator/template-engine.js.map +1 -0
  143. package/cli/dist/index.d.ts +11 -0
  144. package/cli/dist/index.d.ts.map +1 -0
  145. package/cli/dist/index.js +11 -0
  146. package/cli/dist/index.js.map +1 -0
  147. package/cli/dist/types/index.d.ts +257 -0
  148. package/cli/dist/types/index.d.ts.map +1 -0
  149. package/cli/dist/types/index.js +17 -0
  150. package/cli/dist/types/index.js.map +1 -0
  151. package/cli/dist/utils/cache.d.ts +19 -0
  152. package/cli/dist/utils/cache.d.ts.map +1 -0
  153. package/cli/dist/utils/cache.js +64 -0
  154. package/cli/dist/utils/cache.js.map +1 -0
  155. package/cli/dist/utils/command-helpers.d.ts +17 -0
  156. package/cli/dist/utils/command-helpers.d.ts.map +1 -0
  157. package/cli/dist/utils/command-helpers.js +41 -0
  158. package/cli/dist/utils/command-helpers.js.map +1 -0
  159. package/cli/dist/utils/constants.d.ts +53 -0
  160. package/cli/dist/utils/constants.d.ts.map +1 -0
  161. package/cli/dist/utils/constants.js +115 -0
  162. package/cli/dist/utils/constants.js.map +1 -0
  163. package/cli/dist/utils/discovery-context.d.ts +4 -0
  164. package/cli/dist/utils/discovery-context.d.ts.map +1 -0
  165. package/cli/dist/utils/discovery-context.js +39 -0
  166. package/cli/dist/utils/discovery-context.js.map +1 -0
  167. package/cli/dist/utils/docker.d.ts +4 -0
  168. package/cli/dist/utils/docker.d.ts.map +1 -0
  169. package/cli/dist/utils/docker.js +19 -0
  170. package/cli/dist/utils/docker.js.map +1 -0
  171. package/cli/dist/utils/doctor-runtime.d.ts +81 -0
  172. package/cli/dist/utils/doctor-runtime.d.ts.map +1 -0
  173. package/cli/dist/utils/doctor-runtime.js +498 -0
  174. package/cli/dist/utils/doctor-runtime.js.map +1 -0
  175. package/cli/dist/utils/env-validator.d.ts +15 -0
  176. package/cli/dist/utils/env-validator.d.ts.map +1 -0
  177. package/cli/dist/utils/env-validator.js +109 -0
  178. package/cli/dist/utils/env-validator.js.map +1 -0
  179. package/cli/dist/utils/errors.d.ts +31 -0
  180. package/cli/dist/utils/errors.d.ts.map +1 -0
  181. package/cli/dist/utils/errors.js +140 -0
  182. package/cli/dist/utils/errors.js.map +1 -0
  183. package/cli/dist/utils/file-helpers.d.ts +14 -0
  184. package/cli/dist/utils/file-helpers.d.ts.map +1 -0
  185. package/cli/dist/utils/file-helpers.js +45 -0
  186. package/cli/dist/utils/file-helpers.js.map +1 -0
  187. package/cli/dist/utils/formatting.d.ts +30 -0
  188. package/cli/dist/utils/formatting.d.ts.map +1 -0
  189. package/cli/dist/utils/formatting.js +164 -0
  190. package/cli/dist/utils/formatting.js.map +1 -0
  191. package/cli/dist/utils/paths.d.ts +3 -0
  192. package/cli/dist/utils/paths.d.ts.map +1 -0
  193. package/cli/dist/utils/paths.js +41 -0
  194. package/cli/dist/utils/paths.js.map +1 -0
  195. package/cli/dist/utils/port-assignment.d.ts +6 -0
  196. package/cli/dist/utils/port-assignment.d.ts.map +1 -0
  197. package/cli/dist/utils/port-assignment.js +81 -0
  198. package/cli/dist/utils/port-assignment.js.map +1 -0
  199. package/cli/dist/utils/project-features.d.ts +23 -0
  200. package/cli/dist/utils/project-features.d.ts.map +1 -0
  201. package/cli/dist/utils/project-features.js +98 -0
  202. package/cli/dist/utils/project-features.js.map +1 -0
  203. package/cli/dist/utils/project-templates.d.ts +7 -0
  204. package/cli/dist/utils/project-templates.d.ts.map +1 -0
  205. package/cli/dist/utils/project-templates.js +24 -0
  206. package/cli/dist/utils/project-templates.js.map +1 -0
  207. package/cli/dist/utils/prompt.d.ts +28 -0
  208. package/cli/dist/utils/prompt.d.ts.map +1 -0
  209. package/cli/dist/utils/prompt.js +79 -0
  210. package/cli/dist/utils/prompt.js.map +1 -0
  211. package/cli/dist/utils/resource-features.d.ts +29 -0
  212. package/cli/dist/utils/resource-features.d.ts.map +1 -0
  213. package/cli/dist/utils/resource-features.js +175 -0
  214. package/cli/dist/utils/resource-features.js.map +1 -0
  215. package/cli/dist/utils/service-urls.d.ts +30 -0
  216. package/cli/dist/utils/service-urls.d.ts.map +1 -0
  217. package/cli/dist/utils/service-urls.js +93 -0
  218. package/cli/dist/utils/service-urls.js.map +1 -0
  219. package/cli/dist/utils/services.d.ts +18 -0
  220. package/cli/dist/utils/services.d.ts.map +1 -0
  221. package/cli/dist/utils/services.js +292 -0
  222. package/cli/dist/utils/services.js.map +1 -0
  223. package/cli/dist/utils/stack-features.d.ts +22 -0
  224. package/cli/dist/utils/stack-features.d.ts.map +1 -0
  225. package/cli/dist/utils/stack-features.js +13 -0
  226. package/cli/dist/utils/stack-features.js.map +1 -0
  227. package/cli/dist/utils/tar.d.ts +7 -0
  228. package/cli/dist/utils/tar.d.ts.map +1 -0
  229. package/cli/dist/utils/tar.js +21 -0
  230. package/cli/dist/utils/tar.js.map +1 -0
  231. package/cli/dist/utils/tilt.d.ts +20 -0
  232. package/cli/dist/utils/tilt.d.ts.map +1 -0
  233. package/cli/dist/utils/tilt.js +95 -0
  234. package/cli/dist/utils/tilt.js.map +1 -0
  235. package/cli/dist/utils/validation.d.ts +16 -0
  236. package/cli/dist/utils/validation.d.ts.map +1 -0
  237. package/cli/dist/utils/validation.js +58 -0
  238. package/cli/dist/utils/validation.js.map +1 -0
  239. package/cli/package.json +11 -12
  240. package/cli/src/commands/__tests__/always-enabled-infra.test.ts +2 -8
  241. package/cli/src/commands/__tests__/doctor.test.ts +181 -8
  242. package/cli/src/commands/__tests__/phases-model.test.ts +2 -4
  243. package/cli/src/commands/__tests__/project-resource-features-e2e.test.ts +4 -24
  244. package/cli/src/commands/__tests__/saas-starter-integration.test.ts +8 -10
  245. package/cli/src/commands/__tests__/stack-features-e2e.test.ts +10 -4
  246. package/cli/src/commands/__tests__/upgrade.test.ts +23 -13
  247. package/cli/src/commands/doctor.ts +136 -0
  248. package/cli/src/commands/help.ts +13 -3
  249. package/cli/src/commands/project.ts +11 -14
  250. package/cli/src/commands/projects.ts +2 -2
  251. package/cli/src/commands/up.ts +13 -4
  252. package/cli/src/commands/upgrade.ts +11 -4
  253. package/cli/src/config/platform-standards.test.ts +6 -6
  254. package/cli/src/generator/__tests__/extension-fetch.test.ts +36 -14
  255. package/cli/src/generator/extension-fetch.ts +12 -5
  256. package/cli/src/generator/template-engine.ts +19 -21
  257. package/cli/src/types/index.ts +2 -0
  258. package/cli/src/utils/__tests__/tilt.test.ts +29 -0
  259. package/cli/src/utils/constants.ts +4 -0
  260. package/cli/src/utils/doctor-runtime.ts +67 -27
  261. package/cli/src/utils/env-validator.ts +2 -2
  262. package/cli/src/utils/project-templates.ts +2 -1
  263. package/cli/src/utils/prompt.ts +2 -4
  264. package/cli/src/utils/resource-features.ts +10 -4
  265. package/cli/src/utils/services.ts +2 -1
  266. package/cli/src/utils/tilt.ts +20 -0
  267. package/docs/demo.svg +217 -0
  268. package/engine/assets/docker/wake-gateway/Dockerfile +27 -0
  269. package/engine/assets/docker/wake-gateway/decide.test.ts +133 -0
  270. package/engine/assets/docker/wake-gateway/decide.ts +105 -0
  271. package/engine/assets/docker/wake-gateway/server.ts +311 -0
  272. package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +13 -23
  273. package/engine/topologies/platform/docker/compose/compose.star +7 -14
  274. package/engine/topologies/platform/docker/compose/compose_helpers.star +1 -1
  275. package/engine/topologies/platform/docker/compose/traefik_standalone.star +66 -6
  276. package/engine/topologies/platform/docker/config/healthcheck.star +56 -0
  277. package/engine/topologies/platform/docker/constants.star +10 -0
  278. package/engine/topologies/platform/docker/layers/l1_base_layers.star +1 -1
  279. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +55 -20
  280. package/engine/topologies/platform/docker/networking/traefik_static_routes.star +161 -0
  281. package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +2 -2
  282. package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +1 -1
  283. package/engine/topologies/tilt/manifest/schema.star +6 -0
  284. package/engine/topologies/tilt/manifest/validator.star +16 -0
  285. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +143 -9
  286. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +12 -6
  287. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +2 -1
  288. package/knip.json +1 -1
  289. package/openspec/changes/optimize-runtime-docker-layers/.openspec.yaml +3 -0
  290. package/openspec/changes/optimize-runtime-docker-layers/design.md +81 -0
  291. package/openspec/changes/optimize-runtime-docker-layers/proposal.md +44 -0
  292. package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md +79 -0
  293. package/openspec/changes/optimize-runtime-docker-layers/tasks.md +36 -0
  294. package/openspec/changes/prioritized-cold-start/.openspec.yaml +4 -0
  295. package/openspec/changes/prioritized-cold-start/design.md +133 -0
  296. package/openspec/changes/prioritized-cold-start/proposal.md +41 -0
  297. package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +92 -0
  298. package/openspec/changes/prioritized-cold-start/tasks.md +56 -0
  299. package/openspec/config.yaml +1 -0
  300. package/package.json +35 -5
  301. package/scripts/autoresearch/layer-eval.sh +65 -0
  302. package/scripts/benchmark/README.md +25 -0
  303. package/scripts/benchmark/container-scale.ts +503 -0
  304. package/scripts/benchmark/pre-release-gate.sh +9 -0
  305. package/scripts/measure-idle-footprint.sh +58 -0
  306. package/scripts/record-demo.sh +43 -0
  307. package/tests/tilt-engine/test_runtime_container_footprint.py +162 -0
  308. package/tests/tilt-engine/test_sablier_defer_start.py +196 -0
  309. package/tests/tilt-engine/test_standalone_traefik.py +52 -0
  310. package/tests/tilt-engine/test_traefik_static_wake_route.py +187 -0
  311. package/bun.lock +0 -538
@@ -0,0 +1,27 @@
1
+ # Wake gateway (see openspec/changes/prioritized-cold-start): fronts every
2
+ # `sablier.deferStart` resource's Traefik static route, triggers cold
3
+ # containers via `tilt trigger` and/or Sablier, then holds the request until
4
+ # healthy. Needs the Docker CLI plus its Compose plugin (client only, talks
5
+ # to the mounted /var/run/docker.sock; compose starts an already-built image
6
+ # directly, ahead of Tilt's build queue) and the real `tilt` CLI (D3: shells out to the
7
+ # documented `tilt trigger` command rather than depending on Tilt's
8
+ # undocumented internal HTTP API).
9
+
10
+ FROM oven/bun:1.3.11-alpine AS base
11
+
12
+ # Pin to the same Tilt version this workspace runs (v0.37.7) so `tilt trigger`
13
+ # talks the same protocol as the real daemon it's connecting to. Installed
14
+ # directly (not via tilt-dev's install.sh, which shells out to `sudo` -- not
15
+ # present in this minimal image, and unnecessary since we're already root).
16
+ ARG TILT_VERSION=0.37.7
17
+ RUN apk add --no-cache docker-cli docker-cli-compose curl gcompat \
18
+ && arch="$(uname -m)" \
19
+ && case "$arch" in aarch64) tilt_arch=arm64 ;; x86_64) tilt_arch=x86_64 ;; *) echo "unsupported arch: $arch" >&2; exit 1 ;; esac \
20
+ && curl -fsSL "https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.${tilt_arch}.tar.gz" \
21
+ | tar -xz -C /usr/local/bin tilt
22
+
23
+ WORKDIR /app
24
+ COPY decide.ts server.ts ./
25
+
26
+ EXPOSE 9999
27
+ CMD ["bun", "run", "server.ts"]
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ COMPOSE_START_TIMEOUT_MS,
4
+ TILT_TRIGGER_TIMEOUT_MS,
5
+ decideWakeAction,
6
+ evaluateReadiness,
7
+ wakeTimeoutMs,
8
+ } from "./decide";
9
+
10
+ describe("decideWakeAction", () => {
11
+ test("never-created resource: tilt trigger only, no Sablier start", () => {
12
+ const action = decideWakeAction({
13
+ resourceContainerName: "data-governance-api",
14
+ resourceState: "absent",
15
+ dependencies: [],
16
+ });
17
+ expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
18
+ expect(action.useSablierStart).toBe(false);
19
+ });
20
+
21
+ test("previously-run, now-exited resource: both tilt trigger and Sablier start", () => {
22
+ const action = decideWakeAction({
23
+ resourceContainerName: "data-governance-api",
24
+ resourceState: "exited",
25
+ dependencies: [],
26
+ });
27
+ expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
28
+ expect(action.useSablierStart).toBe(true);
29
+ });
30
+
31
+ test("created-but-not-started resource: both paths apply", () => {
32
+ const action = decideWakeAction({
33
+ resourceContainerName: "data-governance-api",
34
+ resourceState: "created",
35
+ dependencies: [],
36
+ });
37
+ expect(action.useSablierStart).toBe(true);
38
+ });
39
+
40
+ test("already running: no trigger needed", () => {
41
+ const action = decideWakeAction({
42
+ resourceContainerName: "data-governance-api",
43
+ resourceState: "running",
44
+ dependencies: [],
45
+ });
46
+ expect(action.tiltTriggerNames).toEqual([]);
47
+ expect(action.useSablierStart).toBe(false);
48
+ });
49
+
50
+ test("cold dependency is triggered alongside the resource (task 1.5 finding)", () => {
51
+ const action = decideWakeAction({
52
+ resourceContainerName: "data-governance-api",
53
+ resourceState: "absent",
54
+ dependencies: [
55
+ { name: "postgres", state: "running" },
56
+ { name: "data-governance-api-db-migrator", state: "absent" },
57
+ ],
58
+ });
59
+ expect(action.tiltTriggerNames).toEqual([
60
+ "data-governance-api",
61
+ "data-governance-api-db-migrator",
62
+ ]);
63
+ });
64
+
65
+ test("never-created resource with compose details: started directly, not queued in Tilt", () => {
66
+ const action = decideWakeAction({
67
+ resourceContainerName: "data-governance-api",
68
+ resourceState: "absent",
69
+ dependencies: [{ name: "peer-api", state: "absent" }],
70
+ hasComposeInvocation: true,
71
+ });
72
+ expect(action.useComposeStart).toBe(true);
73
+ // Dependencies carry no compose details, so they still go through Tilt.
74
+ expect(action.tiltTriggerNames).toEqual(["peer-api"]);
75
+ expect(action.useSablierStart).toBe(false);
76
+ });
77
+
78
+ test("existing (exited) container keeps the Sablier path even with compose details", () => {
79
+ const action = decideWakeAction({
80
+ resourceContainerName: "data-governance-api",
81
+ resourceState: "exited",
82
+ dependencies: [],
83
+ hasComposeInvocation: true,
84
+ });
85
+ expect(action.useComposeStart).toBe(false);
86
+ expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
87
+ expect(action.useSablierStart).toBe(true);
88
+ });
89
+
90
+ test("running dependency is not re-triggered", () => {
91
+ const action = decideWakeAction({
92
+ resourceContainerName: "data-governance-api",
93
+ resourceState: "running",
94
+ dependencies: [{ name: "postgres", state: "running" }],
95
+ });
96
+ expect(action.tiltTriggerNames).toEqual([]);
97
+ });
98
+ });
99
+
100
+ describe("wakeTimeoutMs", () => {
101
+ test("a direct compose start only waits for boot + healthcheck", () => {
102
+ expect(wakeTimeoutMs(true)).toBe(COMPOSE_START_TIMEOUT_MS);
103
+ });
104
+
105
+ test("the tilt trigger path allows for an image build, under Bun's 255s idle cap", () => {
106
+ expect(wakeTimeoutMs(false)).toBe(TILT_TRIGGER_TIMEOUT_MS);
107
+ expect(TILT_TRIGGER_TIMEOUT_MS).toBeLessThan(255_000);
108
+ });
109
+
110
+ test("an explicit X-Wake-Timeout-Ms wins", () => {
111
+ expect(wakeTimeoutMs(false, 5_000)).toBe(5_000);
112
+ });
113
+ });
114
+
115
+ describe("evaluateReadiness", () => {
116
+ test("ready once every required name reports healthy", () => {
117
+ const outcome = evaluateReadiness(["a", "b"], { a: true, b: true });
118
+ expect(outcome.ready).toBe(true);
119
+ expect(outcome.pending).toEqual([]);
120
+ });
121
+
122
+ test("not ready while some names are still unhealthy", () => {
123
+ const outcome = evaluateReadiness(["a", "b"], { a: true, b: false });
124
+ expect(outcome.ready).toBe(false);
125
+ expect(outcome.pending).toEqual(["b"]);
126
+ });
127
+
128
+ test("missing entries count as not healthy", () => {
129
+ const outcome = evaluateReadiness(["a", "b"], { a: true });
130
+ expect(outcome.ready).toBe(false);
131
+ expect(outcome.pending).toEqual(["b"]);
132
+ });
133
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Pure decision logic for the wake gateway (see openspec/changes/prioritized-cold-start).
3
+ *
4
+ * The gateway sits behind a Traefik static (file-provider) route for every
5
+ * `sablier: {enable: true, deferStart: true}` resource. This module decides
6
+ * what to *do* given a snapshot of container state; the actual Docker/Tilt
7
+ * I/O and the healthcheck-polling loop live in server.ts, which is a thin
8
+ * wrapper around this so the decision itself stays testable without a live
9
+ * Docker daemon or Tilt instance.
10
+ */
11
+
12
+ export type ContainerState = "absent" | "created" | "running" | "exited";
13
+
14
+ export interface DependencySnapshot {
15
+ name: string;
16
+ state: ContainerState;
17
+ }
18
+
19
+ export interface WakeRequestSnapshot {
20
+ resourceContainerName: string;
21
+ resourceState: ContainerState;
22
+ dependencies: DependencySnapshot[];
23
+ /**
24
+ * Whether the route carried enough compose detail (project, files, image)
25
+ * for the gateway to create the resource's container itself.
26
+ */
27
+ hasComposeInvocation?: boolean;
28
+ }
29
+
30
+ export interface WakeAction {
31
+ /** Resources to `tilt trigger` (task 1.5: Tilt never cascades this on its own). */
32
+ tiltTriggerNames: string[];
33
+ /**
34
+ * Whether to create/start the resource's container directly with
35
+ * `docker compose up --no-build` instead of `tilt trigger`. `tilt trigger`
36
+ * only queues the resource: with Tilt's build slots busy (e.g. 100+
37
+ * resources rebuilding after a re-vendor), a triggered resource waited
38
+ * behind them and the request timed out (observed live against
39
+ * tdk-erp-system, see tasks.md 7.2). server.ts falls back to `tilt trigger`
40
+ * when no image exists yet to start from.
41
+ */
42
+ useComposeStart: boolean;
43
+ /**
44
+ * Whether to also try Sablier's own Docker-level start for the resource.
45
+ * Only meaningful when the resource's container already exists (D2/D3):
46
+ * Sablier has nothing to manage for a container that was never created.
47
+ */
48
+ useSablierStart: boolean;
49
+ }
50
+
51
+ /**
52
+ * Decide which resources need `tilt trigger` and whether Sablier's Docker-level
53
+ * start also applies, given one point-in-time snapshot (D2/D3/D5).
54
+ */
55
+ export function decideWakeAction(snapshot: WakeRequestSnapshot): WakeAction {
56
+ const tiltTriggerNames: string[] = [];
57
+ const useComposeStart = snapshot.resourceState === "absent" && snapshot.hasComposeInvocation === true;
58
+
59
+ if (snapshot.resourceState !== "running" && !useComposeStart) {
60
+ tiltTriggerNames.push(snapshot.resourceContainerName);
61
+ }
62
+ for (const dep of snapshot.dependencies) {
63
+ if (dep.state !== "running") {
64
+ tiltTriggerNames.push(dep.name);
65
+ }
66
+ }
67
+
68
+ const useSablierStart = snapshot.resourceState === "exited" || snapshot.resourceState === "created";
69
+
70
+ return { tiltTriggerNames, useComposeStart, useSablierStart };
71
+ }
72
+
73
+ /** Wait budget when the gateway starts an existing image itself (boot + healthcheck only). */
74
+ export const COMPOSE_START_TIMEOUT_MS = 180_000;
75
+ /**
76
+ * Wait budget when the resource has to go through `tilt trigger` (image build
77
+ * included). Kept under Bun.serve's 255s idleTimeout cap in server.ts.
78
+ */
79
+ export const TILT_TRIGGER_TIMEOUT_MS = 240_000;
80
+
81
+ /** The wait budget for a wake: an explicit X-Wake-Timeout-Ms wins, else by start path. */
82
+ export function wakeTimeoutMs(startedViaCompose: boolean, overrideMs?: number): number {
83
+ if (overrideMs && overrideMs > 0) return overrideMs;
84
+ return startedViaCompose ? COMPOSE_START_TIMEOUT_MS : TILT_TRIGGER_TIMEOUT_MS;
85
+ }
86
+
87
+ export interface WaitOutcome {
88
+ ready: boolean;
89
+ /** Names still not running/healthy when the wait ended (empty when ready). */
90
+ pending: string[];
91
+ }
92
+
93
+ /**
94
+ * Decide whether a set of health polls (resource + dependencies) means the
95
+ * request can be let through yet, or should keep waiting / time out.
96
+ * `healthy` maps each name from a WakeAction's tiltTriggerNames (plus the
97
+ * resource itself) to its latest observed health state.
98
+ */
99
+ export function evaluateReadiness(
100
+ requiredNames: string[],
101
+ healthy: Record<string, boolean>,
102
+ ): WaitOutcome {
103
+ const pending = requiredNames.filter((name) => !healthy[name]);
104
+ return { ready: pending.length === 0, pending };
105
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Wake gateway: fronts every `sablier.deferStart` resource's Traefik static
3
+ * route (see openspec/changes/prioritized-cold-start). On the first request
4
+ * for a resource with no running container, it triggers the resource (and
5
+ * its cold dependencies) via `tilt trigger`, and/or Sablier's own start for
6
+ * an already-created container, then holds the request until the resource
7
+ * answers its healthcheck or the configured timeout elapses.
8
+ *
9
+ * Config for each request comes from headers a Traefik `headers` middleware
10
+ * injects (set by the file-provider entry generated in
11
+ * traefik_static_routes.star) rather than the path, so the original request
12
+ * path/query reach the real backend unchanged once it's ready:
13
+ * X-Wake-Resource Tilt/Compose resource name (for `tilt trigger` and
14
+ * `docker compose up <service>`).
15
+ * X-Wake-Container The actual Docker container name (for `docker
16
+ * inspect` state/health and the proxy target -- NOT the same string as
17
+ * the resource name; Compose derives it as
18
+ * <project>-<service>-<replica>, confirmed empirically
19
+ * against a live TDK container, e.g.
20
+ * "analytics-autogenerated-data-governance-api-1"
21
+ * for the "data-governance-api" resource).
22
+ * X-Wake-Deps Comma-separated "resourceName=containerName" pairs
23
+ * for required dependencies (task 1.5: Tilt never
24
+ * cascades a trigger to these on its own).
25
+ * X-Wake-Compose-Project The resource's Compose project (e.g. "analytics-autogenerated").
26
+ * X-Wake-Compose-Files Comma-separated absolute compose file paths, the
27
+ * same ones Tilt's `<resource>-run-only` resource
28
+ * passes. The project root is mounted into this
29
+ * container at the same absolute path, so relative
30
+ * paths inside those files resolve exactly as for Tilt.
31
+ * X-Wake-Env-File Absolute path of the project's .env (optional).
32
+ * X-Wake-Image The image the compose service declares
33
+ * (<service>_<resource>:dev).
34
+ * X-Wake-Timeout-Ms, X-Wake-Proxy-Port, X-Wake-Session-Duration
35
+ */
36
+ import {
37
+ decideWakeAction,
38
+ evaluateReadiness,
39
+ wakeTimeoutMs,
40
+ type ContainerState,
41
+ } from "./decide";
42
+
43
+ const TILT_HOST = process.env.TILT_HOST || "host.docker.internal";
44
+ const TILT_PORT = process.env.TILT_PORT || "10350";
45
+ const SABLIER_URL = process.env.SABLIER_URL || "http://sablier:10000";
46
+ const POLL_INTERVAL_MS = 1000;
47
+
48
+ async function run(cmd: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
49
+ const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
50
+ const [stdout, stderr, code] = await Promise.all([
51
+ new Response(proc.stdout).text(),
52
+ new Response(proc.stderr).text(),
53
+ proc.exited,
54
+ ]);
55
+ return { code, stdout, stderr };
56
+ }
57
+
58
+ async function containerState(name: string): Promise<ContainerState> {
59
+ const { code, stdout } = await run([
60
+ "docker", "inspect", "--format", "{{.State.Status}}", name,
61
+ ]);
62
+ if (code !== 0) return "absent";
63
+ const status = stdout.trim();
64
+ if (status === "running") return "running";
65
+ if (status === "created") return "created";
66
+ return "exited";
67
+ }
68
+
69
+ async function isHealthy(name: string): Promise<boolean> {
70
+ const { code, stdout } = await run([
71
+ "docker", "inspect",
72
+ "--format", "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}",
73
+ name,
74
+ ]);
75
+ if (code !== 0) return false;
76
+ const status = stdout.trim();
77
+ return status === "healthy" || status === "running";
78
+ }
79
+
80
+ async function tiltTrigger(name: string): Promise<void> {
81
+ const { code, stderr } = await run(["tilt", "trigger", name, "--host", TILT_HOST, "--port", TILT_PORT]);
82
+ console.log(`tilt trigger ${name}: ${code === 0 ? "queued" : `failed (${stderr.trim()})`}`);
83
+ }
84
+
85
+ interface ComposeInvocation {
86
+ project: string;
87
+ files: string[];
88
+ envFile: string;
89
+ image: string;
90
+ }
91
+
92
+ /**
93
+ * Make sure the compose service's declared image exists locally. Tilt tags its
94
+ * own builds `<repo>:tilt-<hash>` and injects that tag when it runs compose, so
95
+ * the declared `<repo>:dev` tag may be missing even though the resource was
96
+ * built; retag Tilt's newest build in that case. False when nothing was ever
97
+ * built -- only `tilt trigger` (which builds) can wake the resource then.
98
+ */
99
+ async function ensureImage(image: string): Promise<boolean> {
100
+ if ((await run(["docker", "image", "inspect", "--format", "{{.Id}}", image])).code === 0) return true;
101
+ const repo = image.split(":")[0];
102
+ // `docker images` lists newest first.
103
+ const { code, stdout } = await run(["docker", "images", repo, "--format", "{{.Tag}}"]);
104
+ if (code !== 0) return false;
105
+ const tiltTag = stdout.split("\n").map((t) => t.trim()).find((t) => t.startsWith("tilt-"));
106
+ if (!tiltTag) return false;
107
+ return (await run(["docker", "tag", `${repo}:${tiltTag}`, image])).code === 0;
108
+ }
109
+
110
+ /**
111
+ * Create and start the resource's container directly, with the same
112
+ * `docker compose ... up -d --no-build` its Tilt `-run-only` resource runs,
113
+ * bypassing Tilt's build queue (see decide.ts useComposeStart).
114
+ */
115
+ async function composeStart(compose: ComposeInvocation, service: string): Promise<boolean> {
116
+ if (!(await ensureImage(compose.image))) {
117
+ console.log(`compose start ${service}: no local image for ${compose.image}, falling back to tilt trigger`);
118
+ return false;
119
+ }
120
+ const args = ["docker", "compose", "-p", compose.project];
121
+ for (const file of compose.files) args.push("-f", file);
122
+ if (compose.envFile) args.push("--env-file", compose.envFile);
123
+ args.push("up", "-d", "--no-build", "--no-deps", service);
124
+ const { code, stderr } = await run(args);
125
+ console.log(`compose start ${service}: ${code === 0 ? "started" : `failed (${stderr.trim()})`}`);
126
+ return code === 0;
127
+ }
128
+
129
+ /**
130
+ * Sablier's real blocking-strategy endpoint (GET /api/strategies/blocking,
131
+ * per https://sablierapp.dev/openapi.json): starts the named instance and
132
+ * holds until it's ready or `timeoutMs` elapses, returning that outcome via
133
+ * the X-Sablier-Session-Status header. Only valid for an instance Sablier
134
+ * already knows about (a container that exists with sablier.enable=true) --
135
+ * see decide.ts's useSablierStart.
136
+ */
137
+ async function sablierBlockingWait(resource: string, sessionDuration: string, timeoutMs: number): Promise<boolean> {
138
+ const params = new URLSearchParams({
139
+ names: resource,
140
+ session_duration: sessionDuration,
141
+ timeout: `${Math.max(1, Math.round(timeoutMs / 1000))}s`,
142
+ });
143
+ try {
144
+ const res = await fetch(`${SABLIER_URL}/api/strategies/blocking?${params}`);
145
+ return res.headers.get("X-Sablier-Session-Status") === "ready";
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ interface NamedDep {
152
+ resourceName: string;
153
+ containerName: string;
154
+ }
155
+
156
+ function parseDeps(raw: string): NamedDep[] {
157
+ return raw
158
+ .split(",")
159
+ .filter(Boolean)
160
+ .map((pair) => {
161
+ const [resourceName, containerName] = pair.split("=");
162
+ return { resourceName, containerName: containerName || resourceName };
163
+ });
164
+ }
165
+
166
+ function parseRequest(req: Request) {
167
+ const resource = req.headers.get("X-Wake-Resource") || "";
168
+ const container = req.headers.get("X-Wake-Container") || resource;
169
+ const deps = parseDeps(req.headers.get("X-Wake-Deps") || "");
170
+ const timeoutOverrideMs = Number(req.headers.get("X-Wake-Timeout-Ms")) || undefined;
171
+ const proxyPort = req.headers.get("X-Wake-Proxy-Port") || "80";
172
+ const sessionDuration = req.headers.get("X-Wake-Session-Duration") || "10m";
173
+ const composeFiles = (req.headers.get("X-Wake-Compose-Files") || "").split(",").filter(Boolean);
174
+ const compose: ComposeInvocation = {
175
+ project: req.headers.get("X-Wake-Compose-Project") || "",
176
+ files: composeFiles,
177
+ envFile: req.headers.get("X-Wake-Env-File") || "",
178
+ image: req.headers.get("X-Wake-Image") || "",
179
+ };
180
+ const hasCompose = Boolean(compose.project && compose.files.length && compose.image);
181
+ return { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose };
182
+ }
183
+
184
+ async function handleWake(req: Request): Promise<Response> {
185
+ const { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose } =
186
+ parseRequest(req);
187
+ if (!resource) {
188
+ return new Response("wake gateway: missing X-Wake-Resource header (set by the generated Traefik static route)", { status: 400 });
189
+ }
190
+
191
+ const resourceState = await containerState(container);
192
+ const dependencies = await Promise.all(
193
+ deps.map(async (dep) => ({ name: dep.resourceName, state: await containerState(dep.containerName) })),
194
+ );
195
+
196
+ const action = decideWakeAction({
197
+ resourceContainerName: resource,
198
+ resourceState,
199
+ dependencies,
200
+ hasComposeInvocation: hasCompose,
201
+ });
202
+ console.log(
203
+ `wake ${resource}: state=${resourceState} compose=${action.useComposeStart} ` +
204
+ `trigger=[${action.tiltTriggerNames.join(",")}] sablier=${action.useSablierStart}`,
205
+ );
206
+
207
+ // Dependencies (and the resource itself when it can't be started directly)
208
+ // go through tilt trigger (task 1.5: Tilt never cascades this on its own).
209
+ const tiltNames = [...action.tiltTriggerNames];
210
+ let startedViaCompose = false;
211
+ if (action.useComposeStart) {
212
+ startedViaCompose = await composeStart(compose, resource);
213
+ if (!startedViaCompose) tiltNames.unshift(resource);
214
+ }
215
+ const timeoutMs = wakeTimeoutMs(startedViaCompose, timeoutOverrideMs);
216
+ const deadline = Date.now() + timeoutMs;
217
+
218
+ // Sablier's blocking call, when applicable, both starts AND waits for the
219
+ // resource itself in one round trip.
220
+ const [, resourceReadyViaSablier] = await Promise.all([
221
+ Promise.all(tiltNames.map(tiltTrigger)),
222
+ action.useSablierStart
223
+ ? sablierBlockingWait(resource, sessionDuration, timeoutMs)
224
+ : Promise.resolve(false),
225
+ ]);
226
+
227
+ const requiredNames = [resource, ...deps.map((d) => d.resourceName)];
228
+ const containerByName: Record<string, string> = { [resource]: container };
229
+ for (const dep of deps) containerByName[dep.resourceName] = dep.containerName;
230
+
231
+ while (Date.now() < deadline) {
232
+ const healthy: Record<string, boolean> = {};
233
+ // Sablier's blocking call already confirmed the resource itself; re-check
234
+ // it too (cheap) so a false result from Sablier still gets a fair retry.
235
+ healthy[resource] = resourceReadyViaSablier || (await isHealthy(container));
236
+ for (const dep of deps) {
237
+ healthy[dep.resourceName] = await isHealthy(dep.containerName);
238
+ }
239
+ const outcome = evaluateReadiness(requiredNames, healthy);
240
+ if (outcome.ready) {
241
+ return proxyThrough(req, container, proxyPort);
242
+ }
243
+ await Bun.sleep(POLL_INTERVAL_MS);
244
+ }
245
+
246
+ return new Response(
247
+ `wake gateway: timed out after ${timeoutMs}ms waiting for ${resource} and its dependencies to become healthy`,
248
+ { status: 504 },
249
+ );
250
+ }
251
+
252
+ // Headers that must not be forwarded verbatim to the upstream fetch: Host
253
+ // would otherwise carry the gateway's own address, and the X-Wake-* control
254
+ // headers are gateway-internal, not part of the real app request.
255
+ const STRIPPED_REQUEST_HEADERS = new Set([
256
+ "host",
257
+ "x-wake-resource",
258
+ "x-wake-container",
259
+ "x-wake-deps",
260
+ "x-wake-timeout-ms",
261
+ "x-wake-proxy-port",
262
+ "x-wake-session-duration",
263
+ "x-wake-compose-project",
264
+ "x-wake-compose-files",
265
+ "x-wake-env-file",
266
+ "x-wake-image",
267
+ ]);
268
+
269
+ // Proxies to the container name rather than the Compose service name: every
270
+ // TDK project's resources share the traefik-public network, where a bare
271
+ // service name is only an alias and could collide across projects.
272
+ async function proxyThrough(req: Request, container: string, port: string): Promise<Response> {
273
+ // The original path/query reach the gateway untouched (config travels via
274
+ // X-Wake-* headers, not the path), so they're forwarded as-is.
275
+ const target = new URL(req.url);
276
+ target.protocol = "http:";
277
+ // Set hostname/port separately, not via `.host` -- confirmed empirically
278
+ // that `target.host = "name:port"` silently keeps the URL's original port
279
+ // when one is already present instead of replacing it.
280
+ target.hostname = container;
281
+ target.port = port;
282
+
283
+ const headers = new Headers();
284
+ for (const [key, value] of req.headers) {
285
+ if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase())) headers.set(key, value);
286
+ }
287
+
288
+ const upstream = await fetch(target.toString(), {
289
+ method: req.method,
290
+ headers,
291
+ body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
292
+ });
293
+ return new Response(upstream.body, { status: upstream.status, headers: upstream.headers });
294
+ }
295
+
296
+ Bun.serve({
297
+ port: Number(process.env.PORT) || 9999,
298
+ // Bun's default idleTimeout (10s) is shorter than a real wake can take
299
+ // (tilt trigger + healthcheck polling) -- confirmed empirically: a request
300
+ // was killed mid-wait with "request timed out after 10 seconds" before this
301
+ // was raised. 255s is Bun's own max for this setting.
302
+ idleTimeout: 255,
303
+ fetch(req) {
304
+ return handleWake(req).catch((err) => {
305
+ console.error("wake gateway error:", err);
306
+ return new Response(`wake gateway error: ${String(err)}`, { status: 500 });
307
+ });
308
+ },
309
+ });
310
+
311
+ console.log(`wake gateway listening on :${Number(process.env.PORT) || 9999}`);
@@ -2,16 +2,8 @@
2
2
  # 🏗️ TILT SDK - GOLDEN IMAGE DOCKERFILE GENERATOR
3
3
  # =============================================================================
4
4
 
5
- # === INLINED CONSTANTS for pure extension loading ===
6
- get_docker_healthcheck_config = lambda: {"path": "/health", "interval_seconds": 10, "timeout_seconds": 5, "start_period_seconds": 30, "retries": 3, "frontend_start_period_seconds": 30}
7
- # === END INLINED CONSTANTS ===
8
-
9
-
10
5
  load('../../../tilt/common/utils.star', 'Utils')
11
-
12
-
13
- # Load centralized healthcheck configuration
14
- _docker_health = get_docker_healthcheck_config()
6
+ load('../config/healthcheck.star', 'DOCKER_HEALTHCHECK', 'dockerfile_healthcheck_flags')
15
7
 
16
8
 
17
9
  # Load project name for dynamic naming
@@ -76,7 +68,6 @@ RUN apk add --no-cache \
76
68
  ca-certificates \
77
69
  tzdata \
78
70
  curl \
79
- file \
80
71
  bash \
81
72
  openssl
82
73
 
@@ -180,9 +171,11 @@ LABEL layer="l4-backend-bun" \
180
171
  maintainer="{prefix}"
181
172
 
182
173
  ENV NODE_ENV=production
174
+ # Smaller heap growth and more frequent GC: idle services dominate a 100-service landscape.
175
+ ENV BUN_OPTIONS=--smol
183
176
 
184
- # Install hugo for services that require static site generation (specified via manifest features)
185
- RUN apk add --no-cache hugo
177
+ # Service-specific tooling (e.g. hugo) is installed per service by the L4 runtime
178
+ # layer when the manifest lists it in featuresEnabled - not here for every backend.
186
179
 
187
180
  # ⚠️ Infisical CLI is NOT installed in golden L4 images
188
181
  # Services use TypeScript SDK (fetch API) or read from env vars - no CLI needed
@@ -192,7 +185,7 @@ RUN apk add --no-cache hugo
192
185
  USER bun
193
186
 
194
187
  # Backend-specific healthcheck (checks Bun runtime)
195
- HEALTHCHECK --interval={interval}s --timeout={timeout}s --start-period={start_period}s --retries={retries} \
188
+ HEALTHCHECK {hc_flags} \
196
189
  CMD bun --version || exit 1
197
190
 
198
191
  WORKDIR /app
@@ -221,7 +214,7 @@ RUN addgroup -S app && adduser -S app -G app
221
214
  USER app
222
215
 
223
216
  # Backend-specific healthcheck (checks Node.js runtime)
224
- HEALTHCHECK --interval={interval}s --timeout={timeout}s --start-period={start_period}s --retries={retries} \
217
+ HEALTHCHECK {hc_flags} \
225
218
  CMD node --version || exit 1
226
219
 
227
220
  WORKDIR /app
@@ -246,7 +239,7 @@ ENV NODE_ENV=production
246
239
  RUN apk add --no-cache curl
247
240
 
248
241
  # Frontend-specific healthcheck (checks Nginx)
249
- HEALTHCHECK --interval={interval}s --timeout={timeout}s --start-period={frontend_start_period}s --retries={retries} \
242
+ HEALTHCHECK {hc_flags_frontend} \
250
243
  CMD curl -f http://localhost/ || exit 1
251
244
 
252
245
  WORKDIR /usr/share/nginx/html
@@ -276,16 +269,13 @@ RUN mkdir -p /cache/bun && chown -R bun:bun /cache/bun
276
269
 
277
270
  USER bun
278
271
 
279
- # Migrator-specific healthcheck (checks Prisma CLI)
280
- HEALTHCHECK --interval={interval}s --timeout={timeout}s --start-period={start_period}s --retries={retries} \
281
- CMD bunx prisma --version || exit 1
272
+ # Migrators are one-shot jobs (migrate.sh runs to completion), so no healthcheck.
273
+ # The per-service l4_migrator_runtime stage sets HEALTHCHECK NONE as well.
274
+ HEALTHCHECK NONE
282
275
 
283
276
  WORKDIR /app
284
277
  """.format(
285
- interval=_docker_health["interval_seconds"],
286
- timeout=_docker_health["timeout_seconds"],
287
- start_period=_docker_health["start_period_seconds"],
288
- retries=_docker_health["retries"],
289
- frontend_start_period=_docker_health["frontend_start_period_seconds"],
278
+ hc_flags=dockerfile_healthcheck_flags(),
279
+ hc_flags_frontend=dockerfile_healthcheck_flags(DOCKER_HEALTHCHECK["frontend_start_period_seconds"]),
290
280
  prefix=_GOLDEN_PREFIX,
291
281
  )