@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
@@ -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', 'normalize_abs_path')
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)
@@ -146,9 +213,38 @@ def _build_live_update_rules(res_path, full_res_path, syncs):
146
213
  return live_update_rules
147
214
 
148
215
 
216
+ def _app_image_name(resource_name, res_name):
217
+ """The image ref docker_build builds and the app compose service declares."""
218
+ return resource_name + '_' + res_name + ':dev'
219
+
220
+
221
+ def _wake_compose_invocation(project_root, resource_path, res_name, resource_name):
222
+ """Compose files, env file and image for the wake gateway's direct start.
223
+
224
+ The same files/--env-file the `-run-only` resource passes, as absolute
225
+ normalized paths: the gateway mounts the project at the same normalized
226
+ root (traefik_standalone.star), so these resolve identically there.
227
+ Returns None without a project root (paths would be relative to Tilt's cwd).
228
+ """
229
+ if not project_root:
230
+ return None
231
+ root = normalize_abs_path(project_root)
232
+ compose_dir = root + '/' + resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER
233
+ files = [compose_dir + '/docker-compose.app.autogenerated.yml']
234
+ port_override = compose_dir + '/docker-compose.port-override.yml'
235
+ if os.path.exists(port_override):
236
+ files.append(port_override)
237
+ env_file = root + '/.env'
238
+ return {
239
+ 'compose_files': files,
240
+ 'env_file': env_file if os.path.exists(env_file) else '',
241
+ 'image': _app_image_name(resource_name, res_name),
242
+ }
243
+
244
+
149
245
  def _register_docker_build(config, live_update_rules):
150
246
  """Register docker_build with live_update for hot-reload support."""
151
- image_name = config['resource_name'] + '_' + config['res_name'] + ':dev'
247
+ image_name = _app_image_name(config['resource_name'], config['res_name'])
152
248
  project_root = config.get('project_root', '')
153
249
  # Use custom dockerfile from config if specified, otherwise use autogenerated default
154
250
  # The res_path is relative to project_root (e.g., 'services/product/...')
@@ -219,9 +315,15 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
219
315
  """Register a single dc_resource with optional docker_build and run-only resource."""
220
316
  app_type = config.get('app_type', 'backend')
221
317
  res_name = config['res_name']
222
-
223
- print("DEBUG REGISTER: Registering '{}' app_type='{}' auto_init={}".format(res_name, app_type, auto_init_apps))
224
-
318
+
319
+ # A sablier.enable + sablier.deferStart resource is held back from tdk up's
320
+ # normal bring-up: its container is never created or started here, only via
321
+ # the Traefik wake path on first request. docker_build below is unaffected
322
+ # (it has no auto_init gate), so the image still builds either way.
323
+ effective_auto_init = auto_init_apps and not resource_defers_start(config.get('manifest', {}))
324
+
325
+ print("DEBUG REGISTER: Registering '{}' app_type='{}' auto_init={}".format(res_name, app_type, effective_auto_init))
326
+
225
327
  # Skip docker_build for libraries and SDKs - they don't have Dockerfiles
226
328
  if app_type not in ['library', 'sdk'] and config.get('syncs'):
227
329
  print("DEBUG REGISTER: '{}' has syncs, calling docker_build".format(res_name))
@@ -239,12 +341,12 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
239
341
  config['res_name'],
240
342
  labels=config['labels'],
241
343
  resource_deps=config['res_deps'],
242
- auto_init=auto_init_apps,
344
+ auto_init=effective_auto_init,
243
345
  )
244
346
  print("DEBUG REGISTER: dc_resource completed for '{}'".format(res_name))
245
347
 
246
- _register_run_only_resource(config, resource_path, compose_project_name, auto_init_apps)
247
- _register_replicas(config, auto_init_apps)
348
+ _register_run_only_resource(config, resource_path, compose_project_name, effective_auto_init)
349
+ _register_replicas(config, effective_auto_init)
248
350
 
249
351
 
250
352
  def _register_dc_resources(resource_configs, resource_path, compose_project_name, auto_init_apps):
@@ -322,6 +424,7 @@ def _build_resource_config(res, manifest, resource_config, full_res_path, infra_
322
424
  'res_path': full_res_path,
323
425
  'env': env_vars,
324
426
  'project_root': project_root,
427
+ 'manifest': manifest,
325
428
  }
326
429
 
327
430
 
@@ -388,7 +491,38 @@ def register_compose_resources(resource_config, ctx, runtime_flags, manifest_sta
388
491
  entry = _process_frontend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable)
389
492
  else:
390
493
  entry = _process_backend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable)
391
-
494
+ # A deferStart resource has no container Traefik can discover via
495
+ # labels until its first request creates one (D2/D4) -- give it a
496
+ # static file-provider route instead, pointed at the wake gateway.
497
+ # Scoped to backends for now, matching the proposal's motivating
498
+ # case; frontend support is a natural follow-up, not implemented
499
+ # here. Dependency waking (D5): TDK's real infra dependencies
500
+ # (database, NATS, a resource's own migrator) live in
501
+ # separately-registered compose files that `depends_on` cannot
502
+ # reference (confirmed empirically, see design.md D5), and they're
503
+ # always-on infra that's essentially never actually cold, so they
504
+ # never show up here regardless. The one real case D5 flags as
505
+ # mattering -- a peer resource that is itself deferStart -- is
506
+ # resolved via the manifest's own `dependsOn` below.
507
+ if resource_defers_start(manifest) and write_file:
508
+ internal_port = res.get('port', manifest.get('port', BASE_PORT_BACKEND))
509
+ static_route_path = PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL + '/' + res['name'] + '.yml'
510
+ compose_invocation = _wake_compose_invocation(
511
+ ctx.get('project_root', ''), resource_path, res['name'], resource_name,
512
+ ) or {}
513
+ write_file(static_route_path, generate_static_wake_route(
514
+ resource_entry_name=res['name'],
515
+ compose_project_name=compose_project_name,
516
+ manifest=manifest,
517
+ internal_port=internal_port,
518
+ gateway_host=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
519
+ gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
520
+ dependencies=resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map),
521
+ compose_files=compose_invocation.get('compose_files'),
522
+ env_file=compose_invocation.get('env_file', ''),
523
+ image=compose_invocation.get('image', ''),
524
+ ))
525
+
392
526
  resource_entries.append(entry)
393
527
  resource_configs.append(_build_resource_config(
394
528
  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.
@@ -0,0 +1,44 @@
1
+ # Proposal
2
+
3
+ ## Why
4
+
5
+ A full-landscape `tdk up` of `tdk-erp-system` (107 resources, 110 containers) saturated a 16 GB MacBook Air on 2026-09-25. Load average reached 30–43, only 99 MB of memory was free with 8 GB compressed, and `docker ps` took 6–13 s, which made `tdk doctor` and `tdk down` fail their own 10 s Docker check. A single idle backend is cheap (0.3–1.2% CPU, about 20 MiB in its cgroup). The overload comes from per-container overhead that the generated runtime layers multiply by 110: a redundant second Bun process, a 10 s exec-based healthcheck, and tooling nobody uses baked into the golden images. Cutting that overhead at the layer level makes large landscapes usable on the laptops TDK targets.
6
+
7
+ ## What Changes
8
+
9
+ - **Start the backend with one process.** The generated L4 backend runtime (and the Bun-static frontend runtime) now start the compiled entrypoint directly, e.g. `CMD ["bun", "dist/index.js"]`, instead of `bun run start`. `bun run start` launches a Bun wrapper that then spawns a second Bun runtime (about 10 MB + 52 MB resident per container). A service whose `start` script does more than run a single file keeps `bun run start`.
10
+ - **Add an init process for signals.** Generated runtime services run under Docker's minimal init (`init: true`) so `docker stop` still reaches the app once the wrapper is gone.
11
+ - **Slow down healthchecks.** The default steady-state healthcheck interval goes from 10 s to 30 s for generated compose services and golden images. A fast start interval keeps first-healthy time where it is today. The value stays configurable through the existing healthcheck config, and compose files now read that config instead of hard-coding 10 s.
12
+ - **BREAKING (environment):** the fast start interval needs Docker Engine 25+ and Compose 2.20.2+. `tdk doctor` gets a check for this.
13
+ - **Make the migrator healthcheck cheap.** The `l4_migrator_golden` healthcheck no longer runs `bunx prisma --version`, which starts a full bunx/Prisma process on every check. It checks the locally installed Prisma binary instead.
14
+ - **Remove hugo from the golden backend image.** `apk add hugo` is removed from the golden `l4_backend_bun` stage (about 57 MB per backend image). The per-service L4 layer already installs hugo only when a manifest lists the `hugo` feature, and that stays as is.
15
+ - **Document a per-container budget.** An idle generated backend has a documented footprint target, so regressions like these are visible.
16
+
17
+ Nothing here is breaking for service code. Services that relied on hugo being present without declaring the `hugo` feature must add it to `featuresEnabled`.
18
+
19
+ ## Capabilities
20
+
21
+ ### New Capabilities
22
+ - `runtime-container-footprint`: Requirements on generated runtime images and containers: one application process per container, healthcheck cadence and cost, golden image contents limited to what every service of that type needs, and an idle footprint budget.
23
+
24
+ ### Modified Capabilities
25
+ - (none)
26
+
27
+ ## Non-goals
28
+
29
+ - Changing which resources `tdk up` starts by default, or putting backends under Sablier scale-to-zero. Even with these fixes, starting all 107 services at once stays heavy on 16 GB machines. That is a separate product decision with its own change.
30
+ - Changing the Bun version, base OS, or Prisma version.
31
+
32
+ ## Impact
33
+
34
+ - **Engine (Starlark):**
35
+ - `engine/topologies/platform/docker/layers/l4_runtime_layers.star`: backend and Bun-static frontend `CMD`.
36
+ - `engine/topologies/platform/docker/layers/infisical/infisical_docker.star`: `_runtime_setup` default command.
37
+ - `engine/topologies/platform/docker/layers/l3_builder_layers.star`: `RUNTIME_CONFIG.bun.backend_start_command`.
38
+ - `engine/topologies/platform/docker/dockerfile/dockerfile.star` and `generators/golden_docker_generator_v2.star`: default `cmd` parameters.
39
+ - `engine/topologies/platform/docker/build/golden_image_dockerfile.star`: hugo removal, healthcheck interval, migrator healthcheck.
40
+ - `engine/topologies/platform/docker/compose/compose.star`: compose healthcheck (lines 141, 264) reads the shared config; `init: true`.
41
+ - **CLI:** `cli/src/commands/doctor.ts` gets a Docker Engine / Compose minimum-version check.
42
+ - **Generated projects:** every project needs `tdk project --yes` (re-vendoring `.tdk/.tdk-out/tdk-cli-ext/`) and a golden image rebuild to pick up the change. Existing images keep working until then.
43
+ - **Behaviour:** after a failure, Docker marks a container unhealthy up to about 20 s later than today. Tilt readiness and dependency ordering still use the start period.
44
+ - **Tests:** starlark and generated-Dockerfile snapshot tests covering these files need updating.
@@ -0,0 +1,79 @@
1
+ # Spec Delta
2
+
3
+ ## Purpose
4
+
5
+ Keeps the per-container CPU, memory and image-size cost of generated TDK runtimes small enough that a large landscape (100+ resources) can run on a 16 GB developer laptop without starving the container runtime.
6
+
7
+ ## ADDED Requirements
8
+
9
+ ### Requirement: One Application Process Per Runtime Container
10
+
11
+ A generated Bun backend or Bun-static frontend runtime container SHALL run its application as a single Bun process. The container entrypoint MUST hand off to that process with `exec` rather than keep a wrapper process alive. This applies when the service's `start` script runs a single entry file.
12
+
13
+ #### Scenario: Default backend start script
14
+
15
+ - **WHEN** a generated backend's `package.json` has `"start": "bun run dist/index.js"` and its runtime container has finished starting
16
+ - **THEN** exactly one Bun process is running in the container (as listed by `docker top`)
17
+ - **AND** that process is running `dist/index.js`
18
+
19
+ #### Scenario: Custom start script is preserved
20
+
21
+ - **WHEN** a service's `start` script does more than run a single entry file (for example it chains commands or sets environment variables)
22
+ - **THEN** the runtime starts the service through `bun run start`
23
+ - **AND** the service behaves as it did before this change
24
+
25
+ #### Scenario: Signals reach the application
26
+
27
+ - **WHEN** a running backend container is stopped with `docker stop`
28
+ - **THEN** the application process receives SIGTERM directly and the container exits within the stop timeout
29
+
30
+ ### Requirement: Healthchecks Are Low-Cost And Low-Frequency
31
+
32
+ Container healthchecks generated for services and golden images SHALL default to an interval of 30 seconds, and the timing MUST be overridable per run through `TDK_HEALTHCHECK_*` environment variables. A healthcheck command MUST NOT download packages or start a package-runner process (such as `bunx` or `npx`).
33
+
34
+ #### Scenario: Default service healthcheck interval
35
+
36
+ - **WHEN** a project is generated with default healthcheck configuration
37
+ - **THEN** every generated compose service healthcheck has `interval: 30s`
38
+ - **AND** the start period is at least as long as it was before this change
39
+
40
+ #### Scenario: Startup readiness is not delayed
41
+
42
+ - **WHEN** a generated service container starts and begins answering its health endpoint
43
+ - **THEN** Docker reports it healthy within 5 seconds of the first successful answer, even with a 30-second steady-state interval
44
+
45
+ #### Scenario: Configured interval is honoured
46
+
47
+ - **WHEN** `tdk up` runs with `TDK_HEALTHCHECK_INTERVAL_SECONDS` set (e.g. `15`)
48
+ - **THEN** the generated compose files and golden images use that interval
49
+
50
+ #### Scenario: One-shot migrator images carry no healthcheck
51
+
52
+ - **WHEN** the golden migrator image or a generated migrator runtime image is inspected
53
+ - **THEN** it declares no healthcheck (`HEALTHCHECK NONE`)
54
+ - **AND** no healthcheck anywhere in the generated images invokes `bunx` or `npx`
55
+
56
+ ### Requirement: Golden Images Contain Only Universally Required Tooling
57
+
58
+ A golden runtime image SHALL contain only tooling that every service of that runtime type needs. Tooling that only some services use MUST be installed in the per-service layer, and only when that service's manifest declares the matching feature.
59
+
60
+ #### Scenario: Backend golden image has no hugo
61
+
62
+ - **WHEN** the golden L4 backend Bun image is built
63
+ - **THEN** the `hugo` binary is not present in the image
64
+
65
+ #### Scenario: Service declaring the hugo feature
66
+
67
+ - **WHEN** a service's manifest lists `hugo` in `featuresEnabled`
68
+ - **THEN** that service's runtime image contains the `hugo` binary
69
+ - **AND** services that do not declare it do not
70
+
71
+ ### Requirement: Idle Backend Footprint Budget
72
+
73
+ A backend generated from the default TDK backend template SHALL, when idle (running and healthy, with no incoming requests), stay within 64 MiB container memory and average under 2% of one CPU core over 60 seconds, as reported by `docker stats`.
74
+
75
+ #### Scenario: Measuring an idle template backend
76
+
77
+ - **WHEN** a backend generated with `tdk resource <name> --type backend` has been running and healthy for 60 seconds with no incoming requests
78
+ - **THEN** its container memory usage is at most 64 MiB
79
+ - **AND** its average CPU usage over the following 60 seconds is below 2% of one core
@@ -0,0 +1,36 @@
1
+ # Tasks
2
+
3
+ ## 1. Preflight checks
4
+
5
+ - [x] 1.1 Search every TDK project in the workspace (`tdk-erp-system`, `tdk-saas-starter`, `tdk-restaurant-example`, `tdk-user-management`) for services that use `hugo` without listing it in `featuresEnabled`. Record the result under design D5. Verify by attaching the search command and its (expected empty) output to the design note.
6
+ - [x] 1.2 Determine whether any `l4_migrator_golden`-based container runs long-lived or all are one-shot jobs. Record the answer under design D4 and keep or switch the D4 decision accordingly. Verify that design.md is updated with the evidence (compose `restart` policy and Tilt resource type).
7
+
8
+ ## 2. Single-process runtime (D1, D2)
9
+
10
+ - [x] 2.1 Add one start-command resolver to the engine. It returns `bun <file>` for a `start` script of exactly `bun run <file>` or `bun <file>`, and `bun run start` otherwise. Verify with a new pytest in `tests/tilt-engine/` that generates Dockerfiles for fixture services with these start scripts: `bun run dist/index.js`, `bun dist/index.js`, `bun run dist/index.js --port 4000`, `NODE_ENV=x bun run dist/index.js`, `tsc && bun dist/index.js`, and no `start`. Assert the resulting `CMD` for each.
11
+ - [x] 2.2 Apply the resolver where every Bun `CMD` is emitted: the backend runtime (plain and Infisical-entrypoint paths) and the Bun-static frontend in `l4_runtime_layers.star`. Upstream `cmd` defaults stay as the no-override value and explicit overrides are kept (design D1). Verify with `test_backend_cmd_follows_start_script` (both paths) and `test_explicit_cmd_override_is_kept`.
12
+ - [x] 2.3 Emit `init: true` for generated backend and frontend runtime services in `compose.star`. Verify with a pytest asserting `init: true` on both service kinds in a generated compose file.
13
+ - [ ] 2.4 Build one real backend (`cash-management-api` in `tdk-erp-system`) from the new layers. Verify that `docker top` lists exactly one Bun process running `dist/index.js`, and that `docker stop` finishes in under 3 s.
14
+
15
+ ## 3. Healthchecks (D3, D4)
16
+
17
+ - [x] 3.1 Set `get_docker_healthcheck_config` to `interval 30`, `start_interval 2`, `start_period 30`, `retries 3`, and add `--start-interval` to every golden `HEALTHCHECK`. Verify with a pytest on the generated `golden-layers.Dockerfile` asserting `--interval=30s --start-interval=2s` on each stage.
18
+ - [x] 3.2 Make `compose.star` (lines 141, 264) and `traefik_standalone.star` read the shared healthcheck config instead of hard-coding `10s`. Verify with a pytest asserting that generated compose files contain `interval: 30s` and `start_interval: 2s` and no `interval: 10s` for app services.
19
+ - [ ] 3.3 Replace the golden migrator `bunx prisma --version` healthcheck with `HEALTHCHECK NONE` (design D4). Verify with a pytest asserting that no generated golden stage or migrator runtime contains `bunx` or `npx` in a `HEALTHCHECK`, and that after a golden rebuild `docker inspect <prefix>-l4-migrator:latest` shows `Healthcheck.Test` = `["NONE"]`.
20
+ - [x] 3.4 Add a `tdk doctor` check for Docker Engine 25+ and Compose 2.20.2+ in `cli/src/commands/doctor.ts`, with vitest cases in `cli/src/commands/__tests__/doctor.test.ts` for an old version (fails, with an upgrade hint) and a current one (passes). Verify with `bun run test -- doctor.test.ts`.
21
+ - [ ] 3.5 Verify startup readiness with `docker events`: a freshly started backend is marked `healthy` within 5 s of its first successful `/health` response.
22
+
23
+ ## 4. Golden image contents (D5)
24
+
25
+ - [ ] 4.1 Remove `RUN apk add --no-cache hugo` from the `l4_backend_bun` stage in `golden_image_dockerfile.star`. Verify that `docker run --rm <prefix>-l4-backend:latest which hugo` exits non-zero, and that `docker image inspect` shows the image about 57 MB smaller than before.
26
+ - [x] 4.2 Pass the manifest from `manifest_resource.star` to `Docker.backend` so the feature-gated install actually runs (design D5), then verify: a service with `"featuresEnabled": ["hugo"]` gets `apk add --no-cache hugo` in its generated Dockerfile and a service without it does not (`test_hugo_installed_only_for_services_declaring_it`).
27
+
28
+ ## 5. Footprint budget and docs (D6)
29
+
30
+ - [ ] 5.1 Add `scripts/measure-idle-footprint.sh <container>`. It waits for the running backend container to be `healthy`, counts Bun processes, samples `docker stats` for 60 s, and exits non-zero if it finds more than one Bun process, memory over 64 MiB, or average CPU of 2% or more. (It measures an existing container instead of generating a template backend, because generation needs a whole project. The ERP backends are generated from the default template.) Verify it passes against a finance backend built from the new layers.
31
+ - [ ] 5.2 Document the minimum Docker Engine / Compose versions, the `TDK_HEALTHCHECK_*` overrides, the `featuresEnabled: ["hugo"]` migration note, and how to run the footprint script in `cli/README.md` (the repo has no root README). Verify the documented commands run as written.
32
+
33
+ ## 6. Integration
34
+
35
+ - [ ] 6.1 Re-vendor into `tdk-erp-system` (`tdk project --yes`, touch the Tiltfile, rebuild golden layers) and run `tdk up finance`. Verify that every finance service is healthy and answers HTTP 200, and record per-container idle memory next to the baseline in proposal.md.
36
+ - [ ] 6.2 Measurement only: start the full `tdk-erp-system` landscape on the 16 GB reference machine. Record load average, free memory, and `docker ps` latency against the 2026-09-25 baseline, then add the numbers to proposal.md. This informs the separate default-startup-scope change and is not a pass/fail gate.
@@ -0,0 +1,4 @@
1
+ schema: spec-driven
2
+ created: 2026-09-26
3
+ goal: Requests to a not-yet-running resource trigger and prioritize its startup
4
+ instead of waiting behind the full dependency graph