@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
@@ -23,6 +23,7 @@ load("../secrets/infisical.star", "get_infisical_environment_vars")
23
23
  load("../auth/auth.star", "AuthConfig")
24
24
 
25
25
  load("../constants.star", "PlatformDockerConstants")
26
+ load("../config/healthcheck.star", "DOCKER_HEALTHCHECK", "compose_healthcheck_timing")
26
27
 
27
28
  # Load project name for dynamic host naming
28
29
  # Use TDK_PROJECT_ROOT env var set by Tilt, fallback to current directory
@@ -125,6 +126,7 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
125
126
 
126
127
  return """ {res_name}:
127
128
  image: {image_name}
129
+ init: true
128
130
  {build_config}
129
131
  <<: *frontend-memory-limit
130
132
  env_file:
@@ -138,12 +140,7 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
138
140
  - {network_backend}
139
141
  healthcheck:
140
142
  test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:{port}"]
141
- interval: 10s
142
- timeout: 5s
143
- retries: 3
144
- # STORY 4 FIX: 30s grace period to prevent 504 errors on startup
145
- start_period: 30s
146
- """.format(
143
+ {healthcheck_timing}""".format(
147
144
  res_name=res_name,
148
145
  image_name=image_name,
149
146
  build_config=build_config,
@@ -152,6 +149,7 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
152
149
  port=port,
153
150
  network_traefik_public=PlatformDockerConstants.NETWORK_TRAEFIK_PUBLIC,
154
151
  network_backend=PlatformDockerConstants.NETWORK_BACKEND,
152
+ healthcheck_timing=compose_healthcheck_timing(DOCKER_HEALTHCHECK["frontend_start_period_seconds"]),
155
153
  )
156
154
 
157
155
 
@@ -261,13 +259,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
261
259
  )
262
260
  healthcheck_section = """ healthcheck:
263
261
  test: ["CMD", "curl", "-f", "--max-time", "5", "http://localhost:{internal_port}{health_path}"]
264
- interval: 10s
265
- timeout: 5s
266
- retries: 6
267
- # STORY 4 FIX: 30s grace period to prevent 504 errors during startup
268
- # Services get 30s to initialize before health checks begin
269
- start_period: 30s
270
- """.format(internal_port=internal_port, health_path=health_path)
262
+ """.format(internal_port=internal_port, health_path=health_path) + compose_healthcheck_timing()
271
263
  infisical_env = get_infisical_environment_vars(as_array=True, resource_name=res_name)
272
264
 
273
265
  # Build auth environment variables section using centralized auth utilities
@@ -275,6 +267,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
275
267
  return """
276
268
  {resource_entry_name}:
277
269
  image: {image_name}
270
+ init: true
278
271
  restart: unless-stopped
279
272
  {build_config}
280
273
  {ports_section}
@@ -619,7 +612,7 @@ services:
619
612
  {migrator_name}:
620
613
  image: {image_name}
621
614
  build:
622
- context: ../../../../.
615
+ context: ../../../.
623
616
  dockerfile: {dockerfile_ref}
624
617
  target: migrator
625
618
  environment:
@@ -40,7 +40,7 @@ def compose_build_config(resource_path, res_name, dockerfile=OUTPUT.AUTOGENERATE
40
40
  )
41
41
 
42
42
  base_config = """ build:
43
- context: ../../../../.
43
+ context: ../../../.
44
44
  dockerfile: {full_dockerfile_path}""".format(
45
45
  full_dockerfile_path=full_dockerfile_path
46
46
  )
@@ -6,6 +6,8 @@
6
6
  # skip Traefik entirely and api.{project}.localhost never served.
7
7
 
8
8
  load("../constants.star", "PlatformDockerConstants")
9
+ load("../config/healthcheck.star", "compose_healthcheck_timing")
10
+ load("../networking/traefik_static_routes.star", "normalize_abs_path")
9
11
 
10
12
 
11
13
  def generate_standalone_traefik_compose():
@@ -24,6 +26,7 @@ services:
24
26
  restart: unless-stopped
25
27
  depends_on:
26
28
  - sablier
29
+ - wake-gateway
27
30
  command:
28
31
  - "--api.insecure=true"
29
32
  - "--ping=true"
@@ -31,6 +34,13 @@ services:
31
34
  - "--providers.docker.exposedbydefault=false"
32
35
  - "--providers.docker.watch=true"
33
36
  - "--providers.docker.network={network}"
37
+ # Static routes for sablier.deferStart resources (openspec/changes/
38
+ # prioritized-cold-start): a never-created resource has no container
39
+ # and thus no Docker labels for the provider above to discover, so its
40
+ # route is defined here instead, pointed at wake-gateway. Harmless when
41
+ # no deferStart resource exists (empty directory, no routes emitted).
42
+ - "--providers.file.directory=/etc/traefik/dynamic"
43
+ - "--providers.file.watch=true"
34
44
  - "--entrypoints.web.address=:80"
35
45
  - "--entrypoints.websecure.address=:443"
36
46
  - "--log.level=INFO"
@@ -44,15 +54,12 @@ services:
44
54
  - "8080:8080"
45
55
  volumes:
46
56
  - /var/run/docker.sock:/var/run/docker.sock:ro
57
+ - ./{dynamic_dir_name}:/etc/traefik/dynamic:ro
47
58
  networks:
48
59
  - traefik-public
49
60
  healthcheck:
50
61
  test: ["CMD", "traefik", "healthcheck", "--ping"]
51
- interval: 10s
52
- timeout: 5s
53
- retries: 6
54
- start_period: 10s
55
-
62
+ {healthcheck_timing}
56
63
  sablier:
57
64
  image: sablierapp/sablier:1.18.0
58
65
  container_name: {name}_sablier
@@ -66,8 +73,61 @@ services:
66
73
  networks:
67
74
  - traefik-public
68
75
 
76
+ # Wake gateway (openspec/changes/prioritized-cold-start): fronts every
77
+ # deferStart resource's static route above. Triggers a cold resource (and
78
+ # its dependencies, which Tilt never cascades to on its own -- see design.md
79
+ # D3/task 1.5) via `tilt trigger`, or Sablier's own start when a container
80
+ # already exists, then holds the request until healthy. Needs the same
81
+ # Tilt session token Sablier/Traefik don't: confirmed empirically that
82
+ # `tilt trigger` from inside a container is otherwise rejected with
83
+ # "(403): invalid session token" even when the Tilt HTTP port is reachable.
84
+ wake-gateway:
85
+ build: ./{vendored_gateway_dir}
86
+ container_name: {gateway_container}
87
+ restart: unless-stopped
88
+ environment:
89
+ - TILT_HOST=host.docker.internal
90
+ - TILT_PORT=10350
91
+ - SABLIER_URL=http://sablier:10000
92
+ - PORT={gateway_port}
93
+ volumes:
94
+ - /var/run/docker.sock:/var/run/docker.sock
95
+ - {tilt_dev_dir}:/root/.tilt-dev:ro{project_root_mount}
96
+ networks:
97
+ - traefik-public
98
+
69
99
  networks:
70
100
  traefik-public:
71
101
  name: {network}
72
102
  external: true
73
- """.format(name=name, network=network)
103
+ """.format(
104
+ name=name,
105
+ network=network,
106
+ healthcheck_timing=compose_healthcheck_timing(10),
107
+ dynamic_dir_name=PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL.split("/")[-1],
108
+ vendored_gateway_dir="tdk-cli-ext/engine/assets/docker/wake-gateway",
109
+ gateway_container=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
110
+ gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
111
+ tilt_dev_dir=os.environ.get("HOME", "") + "/.tilt-dev",
112
+ project_root_mount=_project_root_mount(),
113
+ )
114
+
115
+
116
+ def _project_root_mount():
117
+ """Read-only mount of the project at its own absolute host path.
118
+
119
+ The gateway runs `docker compose up --no-build` for a deferred resource
120
+ itself (bypassing Tilt's build queue), using the compose files Tilt uses.
121
+ Compose resolves relative paths in them (env_file etc.) client-side, so
122
+ they must exist at the same absolute paths inside the gateway container.
123
+ Empty when TDK_PROJECT_ROOT is unset (the generated Tiltfile sets it).
124
+ """
125
+ root = project_root_abspath()
126
+ if not root:
127
+ return ""
128
+ return "\n - " + root + ":" + root + ":ro"
129
+
130
+
131
+ def project_root_abspath():
132
+ """Absolute, normalized project root (TDK_PROJECT_ROOT), or "" when unset."""
133
+ return normalize_abs_path(os.environ.get("TDK_PROJECT_ROOT", ""))
@@ -0,0 +1,56 @@
1
+ # =============================================================================
2
+ # Docker healthcheck timing - single source for golden images, L4 runtimes and
3
+ # generated compose files.
4
+ # =============================================================================
5
+ # Steady-state probes are infrequent (each probe is a `docker exec`, which adds
6
+ # up across 100+ containers), while `start_interval` keeps first-healthy time
7
+ # fast during the start period. `start_interval` needs Docker Engine 25+ and
8
+ # Compose 2.20.2+ (checked by `tdk doctor`).
9
+ #
10
+ # Override per run with environment variables, e.g.:
11
+ # TDK_HEALTHCHECK_INTERVAL_SECONDS=15 tdk up
12
+ # =============================================================================
13
+
14
+
15
+ def _env_int(name, default):
16
+ raw = os.environ.get(name, '')
17
+ if raw and raw.isdigit() and int(raw) > 0:
18
+ return int(raw)
19
+ return default
20
+
21
+
22
+ DOCKER_HEALTHCHECK = {
23
+ "path": "/health",
24
+ "interval_seconds": _env_int("TDK_HEALTHCHECK_INTERVAL_SECONDS", 30),
25
+ "start_interval_seconds": _env_int("TDK_HEALTHCHECK_START_INTERVAL_SECONDS", 2),
26
+ "timeout_seconds": _env_int("TDK_HEALTHCHECK_TIMEOUT_SECONDS", 5),
27
+ "start_period_seconds": _env_int("TDK_HEALTHCHECK_START_PERIOD_SECONDS", 30),
28
+ "frontend_start_period_seconds": _env_int("TDK_HEALTHCHECK_START_PERIOD_SECONDS", 30),
29
+ "retries": _env_int("TDK_HEALTHCHECK_RETRIES", 3),
30
+ }
31
+
32
+
33
+ def dockerfile_healthcheck_flags(start_period_seconds = None):
34
+ """Flags for a Dockerfile `HEALTHCHECK` instruction."""
35
+ h = DOCKER_HEALTHCHECK
36
+ start_period = start_period_seconds if start_period_seconds != None else h["start_period_seconds"]
37
+ return "--interval={}s --start-interval={}s --timeout={}s --start-period={}s --retries={}".format(
38
+ h["interval_seconds"],
39
+ h["start_interval_seconds"],
40
+ h["timeout_seconds"],
41
+ start_period,
42
+ h["retries"],
43
+ )
44
+
45
+
46
+ def compose_healthcheck_timing(start_period_seconds = None, indent = " "):
47
+ """Timing keys for a compose `healthcheck:` block (everything except `test:`)."""
48
+ h = DOCKER_HEALTHCHECK
49
+ start_period = start_period_seconds if start_period_seconds != None else h["start_period_seconds"]
50
+ return (
51
+ indent + "interval: {}s\n".format(h["interval_seconds"])
52
+ + indent + "timeout: {}s\n".format(h["timeout_seconds"])
53
+ + indent + "retries: {}\n".format(h["retries"])
54
+ + indent + "start_period: {}s\n".format(start_period)
55
+ + indent + "start_interval: {}s\n".format(h["start_interval_seconds"])
56
+ )
@@ -62,6 +62,13 @@ NETWORK_DATABASE = PROJECT_NAME + "_database"
62
62
  NETWORK_INFISICAL = PROJECT_NAME + "_infisical-network"
63
63
  NETWORK_PROXY = PROJECT_NAME + "_proxy"
64
64
 
65
+ # Traefik file-provider dynamic config: one generated YAML per `sablier.deferStart`
66
+ # resource's static route (see openspec/changes/prioritized-cold-start), mounted
67
+ # read-only into the standalone Traefik container at /etc/traefik/dynamic.
68
+ TRAEFIK_DYNAMIC_DIR_REL = ".tdk/.tdk-out/traefik-dynamic"
69
+ TRAEFIK_WAKE_GATEWAY_CONTAINER = PROJECT_NAME_HYPHEN + "-wake-gateway"
70
+ TRAEFIK_WAKE_GATEWAY_PORT = 9999
71
+
65
72
  # NATS constants
66
73
  NATS_HOST = PROJECT_NAME + "_nats"
67
74
  NATS_PORT = "4222"
@@ -146,6 +153,9 @@ PlatformDockerConstants = struct(
146
153
  NETWORK_DATABASE = NETWORK_DATABASE,
147
154
  NETWORK_INFISICAL = NETWORK_INFISICAL,
148
155
  NETWORK_PROXY = NETWORK_PROXY,
156
+ TRAEFIK_DYNAMIC_DIR_REL = TRAEFIK_DYNAMIC_DIR_REL,
157
+ TRAEFIK_WAKE_GATEWAY_CONTAINER = TRAEFIK_WAKE_GATEWAY_CONTAINER,
158
+ TRAEFIK_WAKE_GATEWAY_PORT = TRAEFIK_WAKE_GATEWAY_PORT,
149
159
  NATS_HOST = NATS_HOST,
150
160
  NATS_PORT = NATS_PORT,
151
161
  NATS_URL = NATS_URL,
@@ -61,7 +61,7 @@ def L1_generate_os_base(base_image = None, maintainer = None, use_golden = True)
61
61
  "# ---- L1: os_base (" + base_image + ") ----\n"
62
62
  + "FROM " + base_image + " AS l1_os_base\n"
63
63
  + "LABEL maintainer=\"" + maintainer + "\"\n"
64
- + "RUN apk add --no-cache ca-certificates tzdata curl file bash openssl \\\n"
64
+ + "RUN apk add --no-cache ca-certificates tzdata curl bash openssl \\\n"
65
65
  + " && addgroup -S app && adduser -S app -G app\n"
66
66
  + "WORKDIR /app\n"
67
67
  )
@@ -1,7 +1,6 @@
1
1
  # === INLINED CONSTANTS for pure extension loading ===
2
2
  BASE_PORT_FRONTEND = 3000
3
3
  BASE_PORT_BACKEND = 4000
4
- get_docker_healthcheck_config = lambda: {"path": "/health", "interval": "10s", "timeout": "5s"}
5
4
  OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
6
5
  # === END INLINED CONSTANTS ===
7
6
 
@@ -29,6 +28,7 @@ load('./prisma/prisma_runtime.star',
29
28
  )
30
29
 
31
30
  load('./bun_helpers.star', 'bun_hoisted_packages_symlink_fix')
31
+ load('../config/healthcheck.star', 'DOCKER_HEALTHCHECK', 'dockerfile_healthcheck_flags')
32
32
  load('./infisical/infisical_docker.star', 'InfisicalDocker')
33
33
 
34
34
 
@@ -51,8 +51,55 @@ _docker_cfg = GLOBAL_CONFIG['docker']
51
51
  GOLDEN_L4_BACKEND_IMAGE = _docker_cfg.get('golden_l4_backend_image', _GOLDEN_PREFIX + '-l4-backend:latest')
52
52
  GOLDEN_L4_FRONTEND_IMAGE = _docker_cfg.get('golden_l4_frontend_image', _GOLDEN_PREFIX + '-l4-frontend:latest')
53
53
 
54
- # Load Docker healthcheck configuration
55
- _docker_health = get_docker_healthcheck_config()
54
+ DEFAULT_BUN_START_COMMAND = 'bun run start'
55
+
56
+ _ENTRY_FILE_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx']
57
+ _SHELL_CHARS = ['=', '&', '|', ';', '$', '`', '<', '>', '(', ')', '"', "'", '*', '\\']
58
+
59
+
60
+ def _is_entry_file(token):
61
+ if token.startswith('-'):
62
+ return False
63
+ for ch in _SHELL_CHARS:
64
+ if ch in token:
65
+ return False
66
+ for ext in _ENTRY_FILE_EXTENSIONS:
67
+ if token.endswith(ext):
68
+ return True
69
+ return False
70
+
71
+
72
+ def start_command_from_script(start_script):
73
+ """
74
+ `bun run start` starts a Bun wrapper that spawns a second Bun runtime for the
75
+ real server. When the start script only runs one entry file, run that file
76
+ directly so each container holds a single Bun process. Anything else keeps
77
+ `bun run start` so custom start scripts behave exactly as before.
78
+ """
79
+ tokens = (start_script or '').split()
80
+ if len(tokens) == 3 and tokens[0] == 'bun' and tokens[1] == 'run' and _is_entry_file(tokens[2]):
81
+ return 'bun ' + tokens[2]
82
+ if len(tokens) == 2 and tokens[0] == 'bun' and _is_entry_file(tokens[1]):
83
+ return 'bun ' + tokens[1]
84
+ return DEFAULT_BUN_START_COMMAND
85
+
86
+
87
+ def resolve_bun_start_command(res_path, cmd = DEFAULT_BUN_START_COMMAND):
88
+ """Resolve the runtime command from the service's package.json `start` script."""
89
+ if cmd != DEFAULT_BUN_START_COMMAND:
90
+ return cmd
91
+ package_json_path = res_path + '/package.json'
92
+ if not res_path.startswith('/'):
93
+ package_json_path = os.environ.get('TDK_PROJECT_ROOT', '.') + '/' + package_json_path
94
+ if not os.path.exists(package_json_path):
95
+ return cmd
96
+ package_json = read_json(package_json_path, default = {}) or {}
97
+ scripts = package_json.get('scripts', {}) or {}
98
+ return start_command_from_script(scripts.get('start', ''))
99
+
100
+
101
+ def _cmd_instruction(command):
102
+ return "CMD [" + ", ".join(["\"" + part + "\"" for part in command.split()]) + "]\n"
56
103
 
57
104
 
58
105
  def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun run start', use_infisical = False, resource_name = 'service', use_golden = True, use_prisma = True, has_prisma_config = False, manifest = None):
@@ -79,6 +126,7 @@ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun r
79
126
  """
80
127
  # Normalize res_path for Docker COPY (must be relative to build context)
81
128
  res_path_rel = normalize_res_path(res_path)
129
+ cmd = resolve_bun_start_command(res_path, cmd)
82
130
 
83
131
  # Use golden L4-backend image if enabled (production-optimized backend runtime)
84
132
  base_image = GOLDEN_L4_BACKEND_IMAGE if use_golden else BUN_BASE
@@ -91,9 +139,6 @@ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun r
91
139
  "ARG SKIP_INFISICAL_SETUP=0\n",
92
140
  "ENV NODE_ENV=production\n",
93
141
  "COPY --from=l3_production_purger /app/" + res_path + "/node_modules ./node_modules\n",
94
- # Bun isolated linker creates service symlinks that resolve through /app/node_modules/.bun.
95
- # Copy from production_purger to avoid dragging full dev dependency store into runtime.
96
- "COPY --from=l3_production_purger /app/" + res_path + "/node_modules/.bun /app/node_modules/.bun\n",
97
142
  prisma_runtime_copy(res_path) if use_prisma else "",
98
143
  prisma_cli_runtime_copy(res_path) if use_prisma else "",
99
144
  "COPY --from=l3_backend_build /app/" + res_path + "/dist ./dist\n",
@@ -124,12 +169,7 @@ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun r
124
169
 
125
170
  # Only add healthcheck if not using golden image (already in golden L4)
126
171
  if not use_golden:
127
- parts.append("HEALTHCHECK --interval={}s --timeout={}s --start-period={}s --retries={} CMD bun --version || exit 1\n".format(
128
- _docker_health["interval_seconds"],
129
- _docker_health["timeout_seconds"],
130
- _docker_health["start_period_seconds"],
131
- _docker_health["retries"],
132
- ))
172
+ parts.append("HEALTHCHECK " + dockerfile_healthcheck_flags() + " CMD bun --version || exit 1\n")
133
173
 
134
174
  if use_infisical:
135
175
  # Use InfisicalDocker generator for proper secret injection setup
@@ -143,7 +183,7 @@ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun r
143
183
  )
144
184
  parts.append(infisical_setup)
145
185
  else:
146
- parts.append("CMD [\"" + cmd + "\"]\n")
186
+ parts.append(_cmd_instruction(cmd))
147
187
 
148
188
  # Provide build targets that match external tooling (e.g. --target production/ development)
149
189
  parts.append("\nFROM l4_backend_runtime AS production\nFROM l4_backend_runtime AS development\n")
@@ -184,12 +224,7 @@ def L4_generate_frontend_runtime(res_path, target_path = '/usr/share/nginx/html'
184
224
  + "COPY --from=l3_frontend_build /app/" + res_path + "/dist " + target_path + "\n"
185
225
  + "COPY " + res_path_rel + "/" + OUTPUT.AUTOGENERATED_FOLDER + "/nginx.autogenerated.conf /etc/nginx/conf.d/default.conf\n"
186
226
  + "EXPOSE 80\n"
187
- + ("" if use_golden else "HEALTHCHECK --interval={}s --timeout={}s --start-period={}s --retries={} CMD curl -f http://localhost/ || exit 1\n".format(
188
- _docker_health["interval_seconds"],
189
- _docker_health["timeout_seconds"],
190
- _docker_health["frontend_start_period_seconds"],
191
- _docker_health["retries"],
192
- ))
227
+ + ("" if use_golden else "HEALTHCHECK " + dockerfile_healthcheck_flags(DOCKER_HEALTHCHECK["frontend_start_period_seconds"]) + " CMD curl -f http://localhost/ || exit 1\n")
193
228
  + "CMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
194
229
  + "FROM l4_frontend_runtime AS production\n"
195
230
  + "FROM l4_frontend_runtime AS development\n"
@@ -208,7 +243,7 @@ def L4_generate_frontend_runtime(res_path, target_path = '/usr/share/nginx/html'
208
243
  + "COPY --from=l3_frontend_build /app/" + res_path + "/" + OUTPUT.AUTOGENERATED_FOLDER + " ./" + OUTPUT.AUTOGENERATED_FOLDER + "\n"
209
244
  + "COPY " + res_path_rel + "/package.json ./package.json\n"
210
245
  + "EXPOSE " + str(expose_port) + "\n"
211
- + 'CMD ["bun", "run", "start"]\n'
246
+ + _cmd_instruction(resolve_bun_start_command(res_path))
212
247
  + "FROM l4_frontend_runtime AS production\n"
213
248
  + "FROM l4_frontend_runtime AS development\n"
214
249
  )
@@ -0,0 +1,161 @@
1
+ # =============================================================================
2
+ # Traefik static (file-provider) route for a deferred (`sablier.deferStart`)
3
+ # resource -- see openspec/changes/prioritized-cold-start.
4
+ # =============================================================================
5
+ # A never-created resource has no container and thus no Docker labels, so its
6
+ # route can't come from Traefik's Docker-label discovery the way every other
7
+ # resource's does. This generates the file-provider equivalent instead:
8
+ # same routing rule as the Docker-label `-project` router (traefik.star's
9
+ # get_backend_traefik_labels), but pointed at the wake gateway with a fixed
10
+ # LOW priority so the real, Docker-discovered router always wins once the
11
+ # container exists (verified empirically -- see design.md D4: at equal
12
+ # priority the file-provider router won even when the Docker-discovered
13
+ # backend was healthy).
14
+ #
15
+ # Config for a request travels to the gateway via headers (X-Wake-*), not
16
+ # the path/query. The route still strips the same `apiPath` prefix the real
17
+ # Docker-label `-project` router strips (get_backend_traefik_labels) before
18
+ # forwarding -- confirmed live against tdk-erp-system (task 7.2) that a
19
+ # backend's app only serves its bare health path (e.g. `/health`), not the
20
+ # full `/api/{name}/health` the public route is matched on; without this, the
21
+ # gateway's own proxy-through would forward the unstripped path once healthy
22
+ # and the app would 404 on it, even though the wake itself succeeded.
23
+
24
+ load("./traefik_helpers.star", "project_backend_rule", "cli_api_path", "build_entrypoints")
25
+ load("./traefik_constants.star",
26
+ "TRAEFIK_BACKEND_ENABLE_HTTP",
27
+ "TRAEFIK_BACKEND_ENABLE_HTTPS",
28
+ )
29
+
30
+ STATIC_ROUTE_PRIORITY = 1
31
+
32
+
33
+ def normalize_abs_path(path):
34
+ """Absolute path with `.`/`..` segments collapsed, or "" for "".
35
+
36
+ Tilt's os.path.abspath does not collapse `..` (confirmed:
37
+ abspath("/a/.tdk/.tdk-out/../..") comes back unchanged), and the
38
+ generated Tiltfile's project root is exactly that shape
39
+ (config.main_dir + "/../.."). The gateway's project mount and the compose
40
+ paths in its route headers must match byte-for-byte, so both go through here.
41
+ """
42
+ if not path:
43
+ return ""
44
+ parts = []
45
+ for segment in os.path.abspath(path).split("/"):
46
+ if segment == "" or segment == ".":
47
+ continue
48
+ if segment == "..":
49
+ if parts:
50
+ parts.pop()
51
+ continue
52
+ parts.append(segment)
53
+ return "/" + "/".join(parts)
54
+
55
+
56
+ def _container_name(compose_project_name, res_name):
57
+ """Docker Compose's own naming convention: <project>-<service>-<replica>.
58
+
59
+ Confirmed empirically against a live TDK container (analytics-autogenerated
60
+ -data-governance-api-1) -- see design.md D2 Context. Always replica 1: a
61
+ deferred resource's replica count is unrelated to which single instance
62
+ the wake gateway needs to address."""
63
+ return compose_project_name + "-" + res_name + "-1"
64
+
65
+
66
+ def generate_static_wake_route(
67
+ resource_entry_name,
68
+ compose_project_name,
69
+ manifest,
70
+ internal_port,
71
+ gateway_host,
72
+ gateway_port,
73
+ dependencies=None,
74
+ compose_files=None,
75
+ env_file="",
76
+ image="",
77
+ ):
78
+ """YAML for one resource's Traefik file-provider dynamic-config entry.
79
+
80
+ `dependencies` is a list of (res_name, compose_project_name) tuples for
81
+ the resource's required startup dependencies (D5) -- each may live in a
82
+ different compose project than the resource itself, so the project name
83
+ travels alongside the resource name rather than being assumed shared.
84
+
85
+ `compose_files` (absolute paths), `env_file` and `image` let the gateway
86
+ create the container itself with `docker compose up --no-build` when the
87
+ image already exists, instead of waiting in Tilt's build queue behind
88
+ `tilt trigger`. Omitted headers make the gateway use `tilt trigger` only.
89
+
90
+ Written to the directory PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL
91
+ mounts into the standalone Traefik container (traefik_standalone.star).
92
+ """
93
+ dependencies = dependencies or []
94
+
95
+ rule = project_backend_rule(manifest, resource_entry_name)
96
+ api_path = cli_api_path(resource_entry_name, manifest)
97
+ entrypoints = build_entrypoints(TRAEFIK_BACKEND_ENABLE_HTTP, TRAEFIK_BACKEND_ENABLE_HTTPS)
98
+ router_name = resource_entry_name + "-wake"
99
+ headers_middleware_name = resource_entry_name + "-wake-headers"
100
+ strip_middleware_name = resource_entry_name + "-wake-strip"
101
+ service_name = resource_entry_name + "-wake-gateway"
102
+ container_name = _container_name(compose_project_name, resource_entry_name)
103
+ deps_header = ",".join([
104
+ dep_res_name + "=" + _container_name(dep_project_name, dep_res_name)
105
+ for dep_res_name, dep_project_name in dependencies
106
+ ])
107
+ compose_headers = ""
108
+ if compose_files and image:
109
+ compose_headers = (
110
+ '\n X-Wake-Compose-Project: "' + compose_project_name + '"'
111
+ + '\n X-Wake-Compose-Files: "' + ",".join(compose_files) + '"'
112
+ + '\n X-Wake-Env-File: "' + env_file + '"'
113
+ + '\n X-Wake-Image: "' + image + '"'
114
+ )
115
+
116
+ return """# SYSTEM-GENERATED - DO NOT EDIT (sablier.deferStart static wake route)
117
+ http:
118
+ routers:
119
+ {router_name}:
120
+ rule: "{rule}"
121
+ entryPoints:
122
+ - {entrypoint}
123
+ service: {service_name}
124
+ middlewares:
125
+ - {strip_middleware_name}
126
+ - {headers_middleware_name}
127
+ priority: {priority}
128
+ middlewares:
129
+ {strip_middleware_name}:
130
+ stripPrefix:
131
+ prefixes:
132
+ - "{api_path}"
133
+ {headers_middleware_name}:
134
+ headers:
135
+ customRequestHeaders:
136
+ X-Wake-Resource: "{resource_entry_name}"
137
+ X-Wake-Container: "{container_name}"
138
+ X-Wake-Deps: "{deps_header}"
139
+ X-Wake-Proxy-Port: "{internal_port}"{compose_headers}
140
+ services:
141
+ {service_name}:
142
+ loadBalancer:
143
+ servers:
144
+ - url: "http://{gateway_host}:{gateway_port}"
145
+ """.format(
146
+ router_name=router_name,
147
+ rule=rule,
148
+ entrypoint=entrypoints.split(",")[0],
149
+ service_name=service_name,
150
+ strip_middleware_name=strip_middleware_name,
151
+ headers_middleware_name=headers_middleware_name,
152
+ api_path=api_path,
153
+ priority=STATIC_ROUTE_PRIORITY,
154
+ resource_entry_name=resource_entry_name,
155
+ container_name=container_name,
156
+ deps_header=deps_header,
157
+ internal_port=internal_port,
158
+ compose_headers=compose_headers,
159
+ gateway_host=gateway_host,
160
+ gateway_port=gateway_port,
161
+ )
@@ -64,7 +64,7 @@ def generate_backend_tsconfig(resource_path, prisma_path, write_fn, internal_dep
64
64
  # isolated linker), while @types/node is only a transitive dependency of
65
65
  # bun-types and isn't hoisted to the top level under the isolated linker -
66
66
  # "types": ["node"] fails to resolve there even though it works locally.
67
- "types": ["bun"],
67
+ "types": ["node"],
68
68
  }
69
69
  content = {
70
70
  "compilerOptions": compiler_options,
@@ -114,7 +114,7 @@ def generate_backend_tsconfig(resource_path, prisma_path, write_fn, internal_dep
114
114
  "outDir": "./dist",
115
115
  "rootDir": docker_root_dir,
116
116
  # See note above: "bun" resolves under the isolated linker, "node" doesn't.
117
- "types": ["bun"],
117
+ "types": ["node"],
118
118
  }
119
119
  docker_content = {
120
120
  "compilerOptions": docker_compiler_options,
@@ -93,7 +93,7 @@ def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_d
93
93
  # (including Docker's isolated linker), while @types/node (when only pulled in
94
94
  # transitively, e.g. via bun-types) isn't hoisted under the isolated linker and
95
95
  # "types": ["node"] fails to resolve there even though it works locally.
96
- "types": ["bun"],
96
+ "types": ["node"],
97
97
  },
98
98
  "exclude": [
99
99
  "node_modules",
@@ -277,6 +277,12 @@ MANIFEST_SCHEMA = {
277
277
  'description': 'How long the resource stays up after its last request, as a Go duration (e.g. "10m", "1h")',
278
278
  'example': '10m',
279
279
  },
280
+ 'deferStart': {
281
+ 'type': 'boolean',
282
+ 'required': False,
283
+ 'default': False,
284
+ 'description': 'Skip this resource during tdk up entirely (no container is created or started); a Traefik static route wakes it and its dependencies on the first request instead. Requires enable: true.',
285
+ },
280
286
  },
281
287
  'description': 'On-demand scaling via Sablier: stop idle containers, wake them on the next request. Not recommended for stateful resources (databases) with in-flight writes. (Premium - requires TDK_LICENSE_KEY; see hasSablierLicense() in extension-fetch.ts)',
282
288
  },
@@ -388,6 +388,22 @@ def _validate_cross_field(manifest, context):
388
388
  else:
389
389
  stats['rules_passed'] += 1
390
390
 
391
+ # sablier.deferStart requires sablier.enable
392
+ sablier = manifest.get('sablier', {}) or {}
393
+ if sablier.get('deferStart', False):
394
+ stats['fields_checked'] += 1
395
+
396
+ if not sablier.get('enable', False):
397
+ errors.append(ManifestErrors.new(
398
+ message="sablier.deferStart requires sablier.enable to also be true",
399
+ category=ManifestErrors.CATEGORY['VALIDATION'],
400
+ severity=ManifestErrors.SEVERITY['ERROR'],
401
+ context={'field': 'sablier.deferStart', 'sablier': sablier},
402
+ ))
403
+ stats['rules_failed'] += 1
404
+ else:
405
+ stats['rules_passed'] += 1
406
+
391
407
  # Backend should have Traefik config
392
408
  if app_type == 'backend':
393
409
  stats['fields_checked'] += 1