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

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 (309) 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 +149 -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/engine/assets/docker/wake-gateway/Dockerfile +26 -0
  268. package/engine/assets/docker/wake-gateway/decide.test.ts +87 -0
  269. package/engine/assets/docker/wake-gateway/decide.ts +75 -0
  270. package/engine/assets/docker/wake-gateway/server.ts +220 -0
  271. package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +13 -23
  272. package/engine/topologies/platform/docker/compose/compose.star +7 -14
  273. package/engine/topologies/platform/docker/compose/compose_helpers.star +1 -1
  274. package/engine/topologies/platform/docker/compose/traefik_standalone.star +44 -6
  275. package/engine/topologies/platform/docker/config/healthcheck.star +56 -0
  276. package/engine/topologies/platform/docker/constants.star +10 -0
  277. package/engine/topologies/platform/docker/layers/l1_base_layers.star +1 -1
  278. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +55 -20
  279. package/engine/topologies/platform/docker/networking/traefik_static_routes.star +107 -0
  280. package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +2 -2
  281. package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +1 -1
  282. package/engine/topologies/tilt/manifest/schema.star +6 -0
  283. package/engine/topologies/tilt/manifest/validator.star +16 -0
  284. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +107 -8
  285. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +12 -6
  286. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +2 -1
  287. package/knip.json +1 -1
  288. package/openspec/changes/optimize-runtime-docker-layers/.openspec.yaml +3 -0
  289. package/openspec/changes/optimize-runtime-docker-layers/design.md +81 -0
  290. package/openspec/changes/optimize-runtime-docker-layers/proposal.md +44 -0
  291. package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md +79 -0
  292. package/openspec/changes/optimize-runtime-docker-layers/tasks.md +36 -0
  293. package/openspec/changes/prioritized-cold-start/.openspec.yaml +4 -0
  294. package/openspec/changes/prioritized-cold-start/design.md +113 -0
  295. package/openspec/changes/prioritized-cold-start/proposal.md +40 -0
  296. package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +92 -0
  297. package/openspec/changes/prioritized-cold-start/tasks.md +46 -0
  298. package/openspec/config.yaml +1 -0
  299. package/package.json +35 -5
  300. package/scripts/autoresearch/layer-eval.sh +65 -0
  301. package/scripts/benchmark/README.md +25 -0
  302. package/scripts/benchmark/container-scale.ts +503 -0
  303. package/scripts/benchmark/pre-release-gate.sh +9 -0
  304. package/scripts/measure-idle-footprint.sh +58 -0
  305. package/tests/tilt-engine/test_runtime_container_footprint.py +162 -0
  306. package/tests/tilt-engine/test_sablier_defer_start.py +196 -0
  307. package/tests/tilt-engine/test_standalone_traefik.py +38 -0
  308. package/tests/tilt-engine/test_traefik_static_wake_route.py +119 -0
  309. package/bun.lock +0 -538
@@ -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,107 @@
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, so the original request path reaches the real backend
17
+ # unchanged once the resource is ready (see wake-gateway/server.ts).
18
+
19
+ load("./traefik_helpers.star", "project_backend_rule", "build_entrypoints")
20
+ load("./traefik_constants.star",
21
+ "TRAEFIK_BACKEND_ENABLE_HTTP",
22
+ "TRAEFIK_BACKEND_ENABLE_HTTPS",
23
+ )
24
+
25
+ STATIC_ROUTE_PRIORITY = 1
26
+
27
+
28
+ def _container_name(compose_project_name, res_name):
29
+ """Docker Compose's own naming convention: <project>-<service>-<replica>.
30
+
31
+ Confirmed empirically against a live TDK container (analytics-autogenerated
32
+ -data-governance-api-1) -- see design.md D2 Context. Always replica 1: a
33
+ deferred resource's replica count is unrelated to which single instance
34
+ the wake gateway needs to address."""
35
+ return compose_project_name + "-" + res_name + "-1"
36
+
37
+
38
+ def generate_static_wake_route(
39
+ resource_entry_name,
40
+ compose_project_name,
41
+ manifest,
42
+ internal_port,
43
+ gateway_host,
44
+ gateway_port,
45
+ dependencies=None,
46
+ ):
47
+ """YAML for one resource's Traefik file-provider dynamic-config entry.
48
+
49
+ `dependencies` is a list of (res_name, compose_project_name) tuples for
50
+ the resource's required startup dependencies (D5) -- each may live in a
51
+ different compose project than the resource itself, so the project name
52
+ travels alongside the resource name rather than being assumed shared.
53
+
54
+ Written to the directory PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL
55
+ mounts into the standalone Traefik container (traefik_standalone.star).
56
+ """
57
+ dependencies = dependencies or []
58
+
59
+ rule = project_backend_rule(manifest, resource_entry_name)
60
+ entrypoints = build_entrypoints(TRAEFIK_BACKEND_ENABLE_HTTP, TRAEFIK_BACKEND_ENABLE_HTTPS)
61
+ router_name = resource_entry_name + "-wake"
62
+ middleware_name = resource_entry_name + "-wake-headers"
63
+ service_name = resource_entry_name + "-wake-gateway"
64
+ container_name = _container_name(compose_project_name, resource_entry_name)
65
+ deps_header = ",".join([
66
+ dep_res_name + "=" + _container_name(dep_project_name, dep_res_name)
67
+ for dep_res_name, dep_project_name in dependencies
68
+ ])
69
+
70
+ return """# SYSTEM-GENERATED - DO NOT EDIT (sablier.deferStart static wake route)
71
+ http:
72
+ routers:
73
+ {router_name}:
74
+ rule: "{rule}"
75
+ entryPoints:
76
+ - {entrypoint}
77
+ service: {service_name}
78
+ middlewares:
79
+ - {middleware_name}
80
+ priority: {priority}
81
+ middlewares:
82
+ {middleware_name}:
83
+ headers:
84
+ customRequestHeaders:
85
+ X-Wake-Resource: "{resource_entry_name}"
86
+ X-Wake-Container: "{container_name}"
87
+ X-Wake-Deps: "{deps_header}"
88
+ X-Wake-Proxy-Port: "{internal_port}"
89
+ services:
90
+ {service_name}:
91
+ loadBalancer:
92
+ servers:
93
+ - url: "http://{gateway_host}:{gateway_port}"
94
+ """.format(
95
+ router_name=router_name,
96
+ rule=rule,
97
+ entrypoint=entrypoints.split(",")[0],
98
+ service_name=service_name,
99
+ middleware_name=middleware_name,
100
+ priority=STATIC_ROUTE_PRIORITY,
101
+ resource_entry_name=resource_entry_name,
102
+ container_name=container_name,
103
+ deps_header=deps_header,
104
+ internal_port=internal_port,
105
+ gateway_host=gateway_host,
106
+ gateway_port=gateway_port,
107
+ )
@@ -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
@@ -9,7 +9,9 @@ OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
9
9
 
10
10
  load('../../../platform/docker/index.star', 'Docker')
11
11
  load('../../../platform/docker/constants.star', 'PlatformDockerConstants')
12
- load('../../../tilt/manifest/constants.star', 'BASE_PORT_FRONTEND')
12
+ load('../../../tilt/manifest/constants.star', 'BASE_PORT_FRONTEND', 'BASE_PORT_BACKEND')
13
+ load('../../../platform/docker/networking/traefik_static_routes.star', 'generate_static_wake_route')
14
+ load('../../../platform/docker/networking/sablier_container_cycle.star', 'sablier_middleware_suffix')
13
15
 
14
16
  load('./builders/typescript.star', 'TypescriptBuilders')
15
17
 
@@ -114,6 +116,71 @@ def _build_resource_deps(res, res_name, manifest, resource_config, infra_deps, c
114
116
  return res_deps
115
117
 
116
118
 
119
+ def resource_defers_start(manifest):
120
+ """True when a manifest opts into `sablier.enable` + `sablier.deferStart`
121
+ AND the licensed sablier module is actually active.
122
+
123
+ deferStart stays behind the same TDK_LICENSE_KEY gate as the rest of
124
+ `sablier` (an explicit product decision, not just the opt-in manifest
125
+ flag): the free-tier stub's sablier_middleware_suffix() always reports
126
+ `enabled=False` regardless of what the manifest says, so checking through
127
+ it here -- rather than reading manifest['sablier']['enable'] directly --
128
+ is what keeps deferStart off for unlicensed projects. Without this check,
129
+ a project with no license would still get "no container created or
130
+ started," silently bypassing the license boundary for the disable-the-
131
+ container half of the feature even though the Docker-label wake half
132
+ stays correctly disabled.
133
+
134
+ A deferred resource's image still builds (docker_build has no auto_init
135
+ gate), but its dc_resource/run-only/replica resources are held back with
136
+ auto_init=False: no container is created or started until the Traefik
137
+ static route wakes it on first request (see openspec/changes/
138
+ prioritized-cold-start)."""
139
+ _, sablier_licensed_and_enabled = sablier_middleware_suffix(manifest, '')
140
+ if not sablier_licensed_and_enabled:
141
+ return False
142
+ sablier = manifest.get('sablier', {}) if manifest else {}
143
+ return bool(sablier.get('deferStart', False))
144
+
145
+
146
+ def resolve_manifest_dependency_names(manifest, all_services_map):
147
+ """Resolve a manifest's `dependsOn` short names to full resource names,
148
+ reusing the same `_resolve_dependency_to_resource` that `_build_resource_deps`
149
+ already trusts for Tilt's own resource_deps ordering -- an ambiguous name
150
+ behaves identically here as it already does there."""
151
+ resolved = []
152
+ for dep in (manifest.get('dependsOn', []) if manifest else []):
153
+ resolved_dep = _resolve_dependency_to_resource(dep, all_services_map)
154
+ if resolved_dep and resolved_dep not in resolved:
155
+ resolved.append(resolved_dep)
156
+ return resolved
157
+
158
+
159
+ def resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map):
160
+ """Of a deferStart resource's manifest dependencies, resolve the subset
161
+ that are themselves deferStart resources, for the wake gateway's
162
+ X-Wake-Deps (see generate_static_wake_route's `dependencies` param and
163
+ openspec/changes/prioritized-cold-start design.md D5).
164
+
165
+ A dependsOn name `_resolve_dependency_to_resource` can't map to an exact
166
+ resource (its own fallback branches -- see that function) won't be a key
167
+ in `resource_manifests` either, so it's quietly excluded here rather than
168
+ guessed at -- consistent with D5's existing stance. Always-on infra
169
+ (database, NATS) is never itself a deferStart resource (D5), so in
170
+ practice this only ever picks up the one case D5 flags as mattering: a
171
+ deferStart resource depending on another."""
172
+ dependencies = []
173
+ for resolved_dep in resolve_manifest_dependency_names(manifest, all_services_map):
174
+ dep_manifest = resource_manifests.get(resolved_dep)
175
+ if not dep_manifest or not resource_defers_start(dep_manifest):
176
+ continue
177
+ dep_service_name = all_services_map.get(resolved_dep, {}).get('name', '')
178
+ if not dep_service_name:
179
+ continue
180
+ dependencies.append((resolved_dep, dep_service_name + '-autogenerated'))
181
+ return dependencies
182
+
183
+
117
184
  def _register_replicas(config, auto_init_apps):
118
185
  """Register additional replica resources for high-availability services."""
119
186
  replicas = config.get('replicas', 1)
@@ -219,9 +286,15 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
219
286
  """Register a single dc_resource with optional docker_build and run-only resource."""
220
287
  app_type = config.get('app_type', 'backend')
221
288
  res_name = config['res_name']
222
-
223
- print("DEBUG REGISTER: Registering '{}' app_type='{}' auto_init={}".format(res_name, app_type, auto_init_apps))
224
-
289
+
290
+ # A sablier.enable + sablier.deferStart resource is held back from tdk up's
291
+ # normal bring-up: its container is never created or started here, only via
292
+ # the Traefik wake path on first request. docker_build below is unaffected
293
+ # (it has no auto_init gate), so the image still builds either way.
294
+ effective_auto_init = auto_init_apps and not resource_defers_start(config.get('manifest', {}))
295
+
296
+ print("DEBUG REGISTER: Registering '{}' app_type='{}' auto_init={}".format(res_name, app_type, effective_auto_init))
297
+
225
298
  # Skip docker_build for libraries and SDKs - they don't have Dockerfiles
226
299
  if app_type not in ['library', 'sdk'] and config.get('syncs'):
227
300
  print("DEBUG REGISTER: '{}' has syncs, calling docker_build".format(res_name))
@@ -239,12 +312,12 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
239
312
  config['res_name'],
240
313
  labels=config['labels'],
241
314
  resource_deps=config['res_deps'],
242
- auto_init=auto_init_apps,
315
+ auto_init=effective_auto_init,
243
316
  )
244
317
  print("DEBUG REGISTER: dc_resource completed for '{}'".format(res_name))
245
318
 
246
- _register_run_only_resource(config, resource_path, compose_project_name, auto_init_apps)
247
- _register_replicas(config, auto_init_apps)
319
+ _register_run_only_resource(config, resource_path, compose_project_name, effective_auto_init)
320
+ _register_replicas(config, effective_auto_init)
248
321
 
249
322
 
250
323
  def _register_dc_resources(resource_configs, resource_path, compose_project_name, auto_init_apps):
@@ -322,6 +395,7 @@ def _build_resource_config(res, manifest, resource_config, full_res_path, infra_
322
395
  'res_path': full_res_path,
323
396
  'env': env_vars,
324
397
  'project_root': project_root,
398
+ 'manifest': manifest,
325
399
  }
326
400
 
327
401
 
@@ -388,7 +462,32 @@ def register_compose_resources(resource_config, ctx, runtime_flags, manifest_sta
388
462
  entry = _process_frontend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable)
389
463
  else:
390
464
  entry = _process_backend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable)
391
-
465
+ # A deferStart resource has no container Traefik can discover via
466
+ # labels until its first request creates one (D2/D4) -- give it a
467
+ # static file-provider route instead, pointed at the wake gateway.
468
+ # Scoped to backends for now, matching the proposal's motivating
469
+ # case; frontend support is a natural follow-up, not implemented
470
+ # here. Dependency waking (D5): TDK's real infra dependencies
471
+ # (database, NATS, a resource's own migrator) live in
472
+ # separately-registered compose files that `depends_on` cannot
473
+ # reference (confirmed empirically, see design.md D5), and they're
474
+ # always-on infra that's essentially never actually cold, so they
475
+ # never show up here regardless. The one real case D5 flags as
476
+ # mattering -- a peer resource that is itself deferStart -- is
477
+ # resolved via the manifest's own `dependsOn` below.
478
+ if resource_defers_start(manifest) and write_file:
479
+ internal_port = res.get('port', manifest.get('port', BASE_PORT_BACKEND))
480
+ static_route_path = PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL + '/' + res['name'] + '.yml'
481
+ write_file(static_route_path, generate_static_wake_route(
482
+ resource_entry_name=res['name'],
483
+ compose_project_name=compose_project_name,
484
+ manifest=manifest,
485
+ internal_port=internal_port,
486
+ gateway_host=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
487
+ gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
488
+ dependencies=resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map),
489
+ ))
490
+
392
491
  resource_entries.append(entry)
393
492
  resource_configs.append(_build_resource_config(
394
493
  res, manifest, resource_config, full_res_path, infra_deps,
@@ -84,14 +84,20 @@ def _get_resource_only_paths(resource_path):
84
84
 
85
85
  # Shared runtime scripts copied in generated Dockerfiles.
86
86
  'shared-platform-engineering/docker-templates',
87
-
88
- # Common DDD layers used by backend Dockerfiles.
89
- 'shared-ddd-layers/domain',
90
- 'shared-ddd-layers/infrastructure',
91
87
  ]
92
88
 
93
- # Include only internal libraries referenced by this service package.json.
94
- # This prevents unrelated library changes from triggering global rebuilds.
89
+ # Include only internal libraries referenced by this service package.json
90
+ # (covers shared-ddd-layers/<lib> too, via resolve_lib_path's 'ddd' root -
91
+ # no Dockerfile ever COPYs a bare shared-ddd-layers/domain or /infrastructure
92
+ # path, so hardcoding those two here was pure dead weight: every one of a
93
+ # large landscape's docker_build resources got its own Tilt watch
94
+ # registration on paths most projects don't even have on disk. At 107
95
+ # resources that's 107 duplicate watchers on the same couple of
96
+ # nonexistent paths - real amplification if anything ever touches them,
97
+ # and a plausible contributor to "fsnotify: queue or buffer overflow" at
98
+ # this scale, on top of being wasted watch overhead regardless).
99
+ # This also prevents unrelated library changes from triggering rebuilds
100
+ # of services that don't use them.
95
101
  deps_map = Utils.build_deps_mapping(resource_path)
96
102
  for dep_path in deps_map.values():
97
103
  if dep_path not in only_paths:
@@ -361,7 +361,8 @@ def _generate_all_configs_for_resource(
361
361
  res_path=resource_path,
362
362
  resource_name=resource_name,
363
363
  use_golden=should_enable('golden-image'),
364
- use_prisma='prisma' in manifest.get('featuresEnabled', [])
364
+ use_prisma='prisma' in manifest.get('featuresEnabled', []),
365
+ manifest=manifest,
365
366
  )
366
367
 
367
368
  write_file(resource_path + "/" + OUTPUT.AUTOGENERATED_FOLDER + "/" + GENERATED_CONFIG_FILENAMES['dockerfile_app'], dockerfile_content)
package/knip.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "cli": {
5
5
  "entry": ["src/cli.ts"],
6
6
  "project": ["src/**/*.ts", "src/**/*.tsx"],
7
- "ignoreBinaries": ["biome"]
7
+ "ignoreBinaries": ["vi", "colima", "podman", "killall", "file", "tdk", "tilt"]
8
8
  }
9
9
  }
10
10
  }
@@ -0,0 +1,3 @@
1
+ schema: spec-driven
2
+ created: 2026-09-25
3
+ goal: Cut per-container CPU/memory so a full-landscape tdk up fits on a 16GB laptop
@@ -0,0 +1,81 @@
1
+ # Design
2
+
3
+ ## Context
4
+
5
+ See `proposal.md` (Why) for the measurements. Current runtime layer behaviour relevant to the approach:
6
+
7
+ - The backend runtime Dockerfile is `ENTRYPOINT ["/entrypoint.sh"]` + `CMD ["bun", "run", "start"]`. `infisical-entrypoint.sh` ends in `exec "$@"`, so the `bun run start` wrapper becomes PID 1 and spawns the real `bun run dist/index.js` child. The wrapper is currently what receives `SIGTERM` from `docker stop` and passes it on to the child.
8
+ - The start command is set in several places: `RUNTIME_CONFIG.bun.backend_start_command` (`l3_builder_layers.star`), default `cmd` parameters in `dockerfile.star`, `golden_docker_generator_v2.star` and `l4_runtime_layers.star`, the `_runtime_setup` default (`infisical_docker.star`), and a hard-coded `CMD` for the Bun-static frontend (`l4_runtime_layers.star:213`). The Node runtime already uses `node dist/index.js` directly.
9
+ - Healthcheck timing lives in `get_docker_healthcheck_config` (`golden_image_dockerfile.star:6`), but `compose.star` (lines 141, 264) hard-codes `interval: 10s` with `retries: 6`.
10
+ - The per-service L4 layer already installs hugo only when a service's `featuresEnabled` contains `hugo`. The golden `l4_backend_bun` stage installs it for every backend anyway.
11
+
12
+ ## Goals / Non-Goals
13
+
14
+ **Goals:**
15
+ - Meet every requirement in `specs/runtime-container-footprint/spec.md` without changing any service's source files or `package.json`.
16
+ - Keep a single source of truth for healthcheck timing.
17
+
18
+ **Non-Goals:**
19
+ - Reworking the entrypoint script beyond what signal handling needs.
20
+ - Default startup scope and Sablier scale-to-zero (see proposal Non-goals).
21
+
22
+ ## Decisions
23
+
24
+ ### D1: Resolve the start command at generation time, exec the entry file directly
25
+
26
+ When generating a Bun backend or Bun-static frontend runtime, the generator reads the service's `package.json` `scripts.start`. If it is exactly `bun run <file>` or `bun <file>` (one file path, no `&&`, `;`, pipes or env assignments), the runtime gets `CMD ["bun", "<file>"]`. In every other case it falls back to `CMD ["bun", "run", "start"]`. The resolver (`resolve_bun_start_command` in `l4_runtime_layers.star`) runs at the single point where the backend and Bun-static frontend `CMD` are emitted, both on the plain path and the Infisical-entrypoint path. Upstream defaults (`cmd='bun run start'` in `dockerfile.star`, `golden_docker_generator_v2.star` and `infisical_docker.star`) stay as they are and act as the "no explicit override" value. An explicit different `cmd` passed by a caller is always kept.
27
+
28
+ *Alternatives considered:*
29
+ - Rewriting `start` in each service's `package.json`: rejected because it edits user-owned files and breaks `bun run start` locally.
30
+ - Always emitting `bun <file>` from a fixed path: rejected because it silently ignores custom start scripts.
31
+ - `bun run --bun start`: rejected because it still keeps the wrapper process.
32
+
33
+ ### D2: Add `init: true` to generated runtime services
34
+
35
+ With direct exec, Bun becomes PID 1. The kernel does not apply default signal actions to PID 1, so an app without a `SIGTERM` handler would ignore `docker stop` and be killed only after the stop timeout. Generated compose services get `init: true` (Docker's bundled tini, about 1 MB resident). Tini passes signals on and reaps zombies. It is not a Bun process, so the "exactly one Bun process" requirement still holds.
36
+
37
+ *Alternative:* inject a `SIGTERM` handler into generated service templates. Rejected because it doesn't cover existing services or custom servers.
38
+
39
+ ### D3: One healthcheck config, 30 s interval with a 2 s start interval
40
+
41
+ A new shared module, `engine/topologies/platform/docker/config/healthcheck.star`, replaces the per-file inlined `get_docker_healthcheck_config` copies. The copies had drifted: `l4_runtime_layers.star`'s copy lacked keys its own code read. Defaults: `interval 30s`, `start_period 30s` (unchanged), `start_interval 2s`, `timeout 5s`, `retries 3`. Golden images, the L4 runtime layer, `compose.star` and `traefik_standalone.star` all read it. Each value can be overridden per run with `TDK_HEALTHCHECK_INTERVAL_SECONDS`, `TDK_HEALTHCHECK_START_INTERVAL_SECONDS`, `TDK_HEALTHCHECK_TIMEOUT_SECONDS`, `TDK_HEALTHCHECK_START_PERIOD_SECONDS` and `TDK_HEALTHCHECK_RETRIES`. Environment variables are used because there is no per-project healthcheck setting today, and `tdk up` does not pass the project `.env` through to Tilt.
42
+
43
+ During the start period Docker probes every 2 s, so a service is marked healthy right after it starts answering (see the spec's "Startup readiness is not delayed" scenario). After that, probes drop to every 30 s. With `retries 3`, a failing service is marked unhealthy within about 90 s, compared with about 60 s today.
44
+
45
+ *Alternative:* 30 s interval with no start interval. Rejected because first-healthy time would move from about 10 s to about 30 s per container, which slows every dependency chain in `tdk up`.
46
+
47
+ `start_interval` needs Docker Engine 25+ and Compose 2.20.2+. `tdk doctor` gets a check that fails with an upgrade hint on older versions.
48
+
49
+ ### D4: Golden migrator stage declares `HEALTHCHECK NONE`
50
+
51
+ Resolved by task 1.2. Migrators are one-shot jobs: every migrator container is built from the per-service `l4_migrator_runtime` stage (`prisma_runtime.star`), which already sets `HEALTHCHECK NONE` and runs `ENTRYPOINT ["/app/migrate.sh"]` to completion. The golden `l4_migrator_golden` stage's `bunx prisma --version` healthcheck is therefore always overridden and never protects anything. It only runs if someone starts the bare golden image, and then it starts a bunx/Prisma process every interval. The golden stage now sets `HEALTHCHECK NONE` too, so both layers agree.
52
+
53
+ Evidence (2026-09-25): `grep -rn L4_generate_migrator_runtime engine/` shows a single caller (`golden_docker_generator_v2.star:57`), and that stage emits `HEALTHCHECK NONE` before `ENTRYPOINT`. No project in the workspace declares `"appType": "migrator"`.
54
+
55
+ *Alternative (original plan):* a `test -x` check on a symlinked Prisma binary. Dropped because a healthcheck on a run-to-completion container has no meaning.
56
+
57
+ ### D5: Remove hugo from the golden backend stage only
58
+
59
+ Delete `RUN apk add --no-cache hugo` from `l4_backend_bun` in `golden_image_dockerfile.star`. The per-service feature-gated install stays, but it never actually ran: `manifest_resource.star` called `Docker.backend(...)` without `manifest`, so `featuresEnabled` was always empty in the L4 layer. The manifest is now passed through, which makes the per-service install work and makes removing hugo from the golden stage safe.
60
+
61
+ Evidence (task 1.1, 2026-09-25): no service in `tdk-erp-system`, `tdk-saas-starter`, `tdk-restaurant-example` or `tdk-user-management` lists `"hugo"` in `service.json`, and no source, script or config file mentions hugo (search excluded `node_modules`, `.autogenerated`, `dist` and `.tdk`). Removal affects no existing project.
62
+
63
+ ### D6: Verifying the idle budget
64
+
65
+ `tests/tilt-engine/test_runtime_container_footprint.py` executes the real Starlark generators through `tilt alpha tiltfile-result` (skipped when Tilt isn't installed) rather than inspecting source text. Those tests assert the generated `CMD`, `init: true` and healthcheck values. The idle budget is checked by a smoke script that generates a template backend, runs it, waits for healthy, samples `docker stats` for 60 s, and fails if memory is over 64 MiB or CPU averages 2% or more. It's run manually and in the release checklist rather than on every CI run, because it needs Docker.
66
+
67
+ ## Risks / Trade-offs
68
+
69
+ - [The start-script parser misreads a script, e.g. `bun run dist/index.js --port 4000`] → Only a single file argument qualifies. Anything with extra tokens falls back to `bun run start`, so the fallback is always the current behaviour.
70
+ - [Docker Engine / Compose is older than 25 / 2.20.2 and rejects `start_interval`] → New `tdk doctor` version check; the release notes state the minimum version.
71
+ - [Unhealthy detection is slower (about 90 s vs 60 s)] → Acceptable for local dev. `retries` stays configurable.
72
+ - [A project relied on hugo without declaring the feature] → The build fails loudly (`hugo: not found`). Migration note: add `"hugo"` to `featuresEnabled`. No project in this workspace is affected (see D5 evidence).
73
+ - [Background auto-commit tooling in this workspace pushes partial engine edits] → Make each decision's edits in one sitting and run the snapshot tests before moving on, so every intermediate push is still consistent.
74
+
75
+ ## Migration Plan
76
+
77
+ 1. Ship the engine changes in a TDK release.
78
+ 2. In each project: run `tdk project --yes` to re-vendor `.tdk/.tdk-out/tdk-cli-ext/` and regenerate compose files and Dockerfiles, touch the Tiltfile, then rebuild golden layers with the `golden-layers-build` resource or a fresh `tdk up`.
79
+ 3. Existing images keep working until they are rebuilt, and nothing needs to change inside services.
80
+
81
+ **Rollback:** revert the engine change, re-run `tdk project --yes`, and rebuild the golden layers.