@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
@@ -104,6 +104,35 @@ describe("buildTiltUpArgs", () => {
104
104
  buildTiltUpArgs([], {});
105
105
  expect(findProjectRoot).toHaveBeenCalledTimes(1);
106
106
  });
107
+
108
+ it("should include -- --focus=<stack> before service names when focusTargets is set", async () => {
109
+ // Regression test: `tdk up <stack>` used to pass only bare positional service
110
+ // names, which the generated Tiltfile's focus filter never reads (it only reads
111
+ // --focus), so it silently fell back to building every stack instead of the one
112
+ // requested. --focus must be present, carry the stack name, and be preceded by
113
+ // `--` (tilt up rejects "--focus" as an unrecognized top-level flag otherwise -
114
+ // `--` is what routes it to the Tiltfile's own config.parse()).
115
+ const { buildTiltUpArgs } = await import("../tilt.js");
116
+ const args = buildTiltUpArgs(["finance-api", "finance-app"], { focusTargets: ["finance"] });
117
+ const dashDashIndex = args.indexOf("--");
118
+ expect(dashDashIndex).toBeGreaterThanOrEqual(0);
119
+ expect(args[dashDashIndex + 1]).toBe("--focus=finance");
120
+ expect(args.indexOf("--focus=finance")).toBeLessThan(args.indexOf("finance-api"));
121
+ });
122
+
123
+ it("should join multiple focusTargets with commas in a single --focus flag", async () => {
124
+ const { buildTiltUpArgs } = await import("../tilt.js");
125
+ const args = buildTiltUpArgs([], { focusTargets: ["finance", "hr"] });
126
+ expect(args).toContain("--focus=finance,hr");
127
+ });
128
+
129
+ it("should NOT include --focus when focusTargets is empty or omitted", async () => {
130
+ const { buildTiltUpArgs } = await import("../tilt.js");
131
+ const withEmpty = buildTiltUpArgs(["svc"], { focusTargets: [] });
132
+ const withOmitted = buildTiltUpArgs(["svc"], {});
133
+ expect(withEmpty.some((a) => a.startsWith("--focus"))).toBe(false);
134
+ expect(withOmitted.some((a) => a.startsWith("--focus"))).toBe(false);
135
+ });
107
136
  });
108
137
 
109
138
  describe("buildTiltDownArgs", () => {
@@ -27,6 +27,10 @@ export const REQUIRED_PACKAGE_SCRIPTS: Record<ResourceType, readonly string[]> =
27
27
  sdk: ["dev", "build"],
28
28
  } as const;
29
29
 
30
+ // Version specifiers that pin nothing at all - a fresh `bun install`/`npm install`
31
+ // can pull a different (possibly breaking) version on every run.
32
+ export const UNPINNED_DEPENDENCY_VERSIONS = new Set(["latest", "*"]);
33
+
30
34
  export const OPTIONAL_INFRA_SERVICES = [
31
35
  "monitoring",
32
36
  "elk",
@@ -5,6 +5,7 @@ import type { CheckResult } from "../types/index.js";
5
5
  import { formatCount } from "./formatting.js";
6
6
  import { findProjectRoot } from "./paths.js";
7
7
  import { getProjectName } from "./service-urls.js";
8
+ import { discoverResources } from "./services.js";
8
9
 
9
10
  const EXEC_TIMEOUT_MS = 10_000;
10
11
  const REGISTRY_PROBE_TIMEOUT_MS = 3_000;
@@ -88,12 +89,41 @@ interface TiltUiResourceItem {
88
89
  };
89
90
  }
90
91
 
92
+ /**
93
+ * Resource names with `sablier: {enable: true, deferStart: true}` in their
94
+ * manifest (openspec/changes/prioritized-cold-start): these are expected to
95
+ * sit at `runtimeStatus: none` until their first request, not a sign of
96
+ * anything stuck. Best-effort — returns an empty set if discovery fails
97
+ * (e.g. not run from inside a project), so callers degrade to today's
98
+ * behavior rather than erroring.
99
+ */
100
+ export function getDeferredResourceNames(): Set<string> {
101
+ try {
102
+ return new Set(
103
+ discoverResources()
104
+ .filter((svc) => svc.config?.sablier?.enable && svc.config?.sablier?.deferStart)
105
+ .map((svc) => svc.name),
106
+ );
107
+ } catch {
108
+ return new Set();
109
+ }
110
+ }
111
+
91
112
  /**
92
113
  * Extract failing UIResources from `tilt get uiresources -o json`.
114
+ *
115
+ * `deferredNames` (typically from getDeferredResourceNames()) separates a
116
+ * `sablier.deferStart` resource's expected `none`/`pending` status into its
117
+ * own count so it's never described as "blocked" or otherwise lumped in with
118
+ * genuinely stuck resources.
93
119
  */
94
- export function parseTiltResourceFailures(jsonText: string): {
120
+ export function parseTiltResourceFailures(
121
+ jsonText: string,
122
+ deferredNames: Set<string> = new Set(),
123
+ ): {
95
124
  failures: TiltResourceFailure[];
96
125
  pendingCount: number;
126
+ deferredCount: number;
97
127
  okCount: number;
98
128
  total: number;
99
129
  } {
@@ -101,6 +131,7 @@ export function parseTiltResourceFailures(jsonText: string): {
101
131
  const items = parsed.items ?? [];
102
132
  const failures: TiltResourceFailure[] = [];
103
133
  let pendingCount = 0;
134
+ let deferredCount = 0;
104
135
  let okCount = 0;
105
136
 
106
137
  for (const item of items) {
@@ -133,11 +164,15 @@ export function parseTiltResourceFailures(jsonText: string): {
133
164
  updateStatus === "none" ||
134
165
  runtimeStatus === "none")
135
166
  ) {
136
- pendingCount += 1;
167
+ if (deferredNames.has(name)) {
168
+ deferredCount += 1;
169
+ } else {
170
+ pendingCount += 1;
171
+ }
137
172
  }
138
173
  }
139
174
 
140
- return { failures, pendingCount, okCount, total: items.length };
175
+ return { failures, pendingCount, deferredCount, okCount, total: items.length };
141
176
  }
142
177
 
143
178
  /**
@@ -156,9 +191,7 @@ export function summarizeTiltBuildError(error: string): string {
156
191
  }
157
192
 
158
193
  const refusedPackages = [
159
- ...normalized.matchAll(
160
- /ConnectionRefused downloading package manifest (@[A-Za-z0-9._/-]+)/gi,
161
- ),
194
+ ...normalized.matchAll(/ConnectionRefused downloading package manifest (@[A-Za-z0-9._/-]+)/gi),
162
195
  ].map((match) => match[1]);
163
196
  if (refusedPackages.length > 0) {
164
197
  const unique = [...new Set(refusedPackages)];
@@ -167,7 +200,11 @@ export function summarizeTiltBuildError(error: string): string {
167
200
  return `private registry ConnectionRefused for ${shown}${more} — Verdaccio (:4873) is down or unreachable from the build`;
168
201
  }
169
202
 
170
- if (/failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i.test(normalized)) {
203
+ if (
204
+ /failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i.test(
205
+ normalized,
206
+ )
207
+ ) {
171
208
  const image = normalized.match(
172
209
  /failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i,
173
210
  )?.[1];
@@ -186,10 +223,12 @@ export function summarizeTiltBuildError(error: string): string {
186
223
  normalized.match(/No such image:\s*([^\s]+)/i)?.[1] ??
187
224
  normalized.match(/pull access denied for ([^,:\s]+)/i)?.[1] ??
188
225
  undefined;
189
- const composeService = normalized.match(
190
- /\bup\s+-d\s+--no-build\s+([A-Za-z0-9._-]+)/i,
191
- )?.[1];
192
- if (missingImage || composeService || /docker compose .*\bup\s+-d\s+--no-build\b/i.test(normalized)) {
226
+ const composeService = normalized.match(/\bup\s+-d\s+--no-build\s+([A-Za-z0-9._-]+)/i)?.[1];
227
+ if (
228
+ missingImage ||
229
+ composeService ||
230
+ /docker compose .*\bup\s+-d\s+--no-build\b/i.test(normalized)
231
+ ) {
193
232
  const imageLabel = missingImage ?? `${composeService ?? "service"}:dev`;
194
233
  const serviceHint = composeService ? ` for ${composeService}` : "";
195
234
  return `compose run failed because image is missing (${imageLabel})${serviceHint} — wait for the ImageBuild resource to finish, then re-trigger the *-run-only resource`;
@@ -212,7 +251,9 @@ export function isRegistryRelatedBuildError(error: string): boolean {
212
251
  );
213
252
  }
214
253
 
215
- export function projectExpectsVerdaccio(projectRoot: string = findProjectRoot() ?? process.cwd()): boolean {
254
+ export function projectExpectsVerdaccio(
255
+ projectRoot: string = findProjectRoot() ?? process.cwd(),
256
+ ): boolean {
216
257
  const projectJsonPath = join(projectRoot, ".tdk", "project.json");
217
258
  if (existsSync(projectJsonPath)) {
218
259
  try {
@@ -236,10 +277,7 @@ export function projectExpectsVerdaccio(projectRoot: string = findProjectRoot()
236
277
  }
237
278
 
238
279
  // Heuristic: scoped private registry pointed at :4873
239
- for (const candidate of [
240
- join(projectRoot, ".npmrc"),
241
- join(projectRoot, "package.json"),
242
- ]) {
280
+ for (const candidate of [join(projectRoot, ".npmrc"), join(projectRoot, "package.json")]) {
243
281
  if (!existsSync(candidate)) continue;
244
282
  try {
245
283
  const text = readFileSync(candidate, "utf-8");
@@ -419,7 +457,7 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
419
457
 
420
458
  let parsed: ReturnType<typeof parseTiltResourceFailures>;
421
459
  try {
422
- parsed = parseTiltResourceFailures(jsonText);
460
+ parsed = parseTiltResourceFailures(jsonText, getDeferredResourceNames());
423
461
  } catch {
424
462
  return {
425
463
  name: "Tilt Resources",
@@ -439,10 +477,11 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
439
477
  }
440
478
 
441
479
  if (parsed.failures.length === 0) {
480
+ const deferredNote = parsed.deferredCount > 0 ? `, ${parsed.deferredCount} deferred` : "";
442
481
  return {
443
482
  name: "Tilt Resources",
444
483
  didPass: true,
445
- message: `Tilt resources healthy (${parsed.okCount} ok, ${parsed.pendingCount} pending)`,
484
+ message: `Tilt resources healthy (${parsed.okCount} ok, ${parsed.pendingCount} pending${deferredNote})`,
446
485
  };
447
486
  }
448
487
 
@@ -458,9 +497,7 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
458
497
  failure,
459
498
  why: describeTiltFailure(failure, exec),
460
499
  }));
461
- const details = described
462
- .map(({ failure, why }) => `${failure.name}: ${why}`)
463
- .join("\n ");
500
+ const details = described.map(({ failure, why }) => `${failure.name}: ${why}`).join("\n ");
464
501
  const omittedNote =
465
502
  omitted > 0 ? `\n …and ${formatCount(omitted, "more failed resource")}` : "";
466
503
 
@@ -468,6 +505,10 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
468
505
  parsed.pendingCount > 0
469
506
  ? `\n ${formatCount(parsed.pendingCount, "resource")} still pending/not started — often blocked by the failures above.`
470
507
  : "";
508
+ const deferredNote =
509
+ parsed.deferredCount > 0
510
+ ? `\n ${formatCount(parsed.deferredCount, "resource")} intentionally deferred (sablier.deferStart) — not started until first request, unrelated to the failures above.`
511
+ : "";
471
512
 
472
513
  const hasPortConflict = ordered.some((failure) =>
473
514
  /port is already allocated/i.test(failure.error),
@@ -497,13 +538,12 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
497
538
  return {
498
539
  name: "Tilt Resources",
499
540
  didPass: false,
500
- message: `Tilt reports ${formatCount(parsed.failures.length, "failed resource")}:\n ${details}${omittedNote}${pendingNote}`,
541
+ message: `Tilt reports ${formatCount(parsed.failures.length, "failed resource")}:\n ${details}${omittedNote}${pendingNote}${deferredNote}`,
501
542
  fix,
502
543
  };
503
544
  }
504
545
 
505
- const INFRA_FAILURE_NAME =
506
- /^(traefik|postgres|nats|infisical|verdaccio|proxy)([.-]|$)/i;
546
+ const INFRA_FAILURE_NAME = /^(traefik|postgres|nats|infisical|verdaccio|proxy)([.-]|$)/i;
507
547
 
508
548
  export function isInfraTiltFailure(name: string): boolean {
509
549
  return INFRA_FAILURE_NAME.test(name);
@@ -563,9 +603,9 @@ export function probeContainerRuntimeError(
563
603
 
564
604
  // Zod env/config validation: pull required field paths from the dump.
565
605
  if (/ZodError/i.test(logs)) {
566
- const requiredPaths = [
567
- ...logs.matchAll(/"path"\s*:\s*\[\s*"([^"]+)"\s*\]/g),
568
- ].map((match) => match[1]);
606
+ const requiredPaths = [...logs.matchAll(/"path"\s*:\s*\[\s*"([^"]+)"\s*\]/g)].map(
607
+ (match) => match[1],
608
+ );
569
609
  const uniquePaths = [...new Set(requiredPaths)].slice(0, 6);
570
610
  if (uniquePaths.length > 0) {
571
611
  return `runtime crash: ZodError missing required config/env: ${uniquePaths.join(", ")}`;
@@ -52,7 +52,7 @@ function generateDbPassword(): string {
52
52
  return randomBytes(16).toString("hex");
53
53
  }
54
54
 
55
- export function generateEnvFile(projectRoot: string): string {
55
+ export function generateEnvFile(_projectRoot: string): string {
56
56
  const lines: string[] = [
57
57
  "# 🔧 TDK Environment Configuration",
58
58
  "# Auto-generated by TDK CLI - edit as needed",
@@ -107,7 +107,7 @@ export function validateEnvFile(projectRoot: string): {
107
107
  .split("\n")
108
108
  .filter((line) => !line.startsWith("#") && line.includes("="))
109
109
  .map((line) => line.split("=")[0])
110
- .filter((name) => name.length > 0)
110
+ .filter((name) => name.length > 0),
111
111
  );
112
112
 
113
113
  for (const envVar of REQUIRED_ENV_VARS) {
@@ -11,7 +11,8 @@ export const PROJECT_TEMPLATES: Record<string, ProjectTemplate> = {
11
11
  },
12
12
  saas: {
13
13
  repo: "https://github.com/tdk-landscape/tdk-saas-starter.git",
14
- description: "SaaS starter: account dashboard and a working checkout button (Hono + Vite + Bun)",
14
+ description:
15
+ "SaaS starter: account dashboard and a working checkout button (Hono + Vite + Bun)",
15
16
  },
16
17
  erp: {
17
18
  repo: "https://github.com/tdk-landscape/tdk-erp-system.git",
@@ -1,5 +1,5 @@
1
- import { createInterface } from "node:readline/promises";
2
1
  import { stdin as input, stdout as output } from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
3
 
4
4
  type ValidationResult = true | string;
5
5
 
@@ -67,9 +67,7 @@ export async function promptConfirm(options: ConfirmPromptOptions): Promise<bool
67
67
  }
68
68
  }
69
69
 
70
- export async function promptSelect<T extends string>(
71
- options: ChoicePromptOptions<T>,
72
- ): Promise<T> {
70
+ export async function promptSelect<T extends string>(options: ChoicePromptOptions<T>): Promise<T> {
73
71
  console.log(options.message);
74
72
  options.choices.forEach((choice, index) => {
75
73
  console.log(` ${index + 1}. ${choice.title}`);
@@ -92,7 +92,8 @@ export const RESOURCE_FEATURES: Record<string, ResourceFeature> = {
92
92
  description: "Docker container configuration for production builds",
93
93
  applies_to: ["frontend", "backend", "worker", "migrator", "sdk"],
94
94
  enabled_by_default: true, // Always enabled
95
- generator_file: "engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star",
95
+ generator_file:
96
+ "engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star",
96
97
  output_file: ".autogenerated/Dockerfile.app",
97
98
  },
98
99
 
@@ -169,15 +170,20 @@ export const RESOURCE_FEATURES: Record<string, ResourceFeature> = {
169
170
  name: "sablier",
170
171
  description:
171
172
  "On-demand scaling: stop this resource when idle, wake it on the next request via a " +
172
- "`sablier: {enable: true}` manifest block (not the featuresEnabled array) " +
173
+ "`sablier: {enable: true}` manifest block (not the featuresEnabled array). Add " +
174
+ "`deferStart: true` to also skip this resource during `tdk up` entirely — no container " +
175
+ "is created or started, and a Traefik static route wakes it on the first request instead " +
176
+ "(backends only for now), so it doesn't wait behind the rest of a large landscape's " +
177
+ "bring-up. Requires `enable: true`. " +
173
178
  "(Premium - requires TDK_LICENSE_KEY; free tier writes a disabled stub; see " +
174
179
  "hasSablierLicense() in extension-fetch.ts KNOWN_RESOURCES/PREMIUM_PATH_MAP)",
175
180
  applies_to: ["backend", "frontend"],
176
181
  enabled_by_default: false,
177
182
  category: "premium",
178
183
  generator_file:
179
- "engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star",
180
- output_file: "(Traefik + Sablier Docker labels on the resource's own compose service, no autogenerated file)",
184
+ "engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star, engine/topologies/platform/docker/compose/traefik_standalone.star (deferStart wake gateway + static route), engine/topologies/platform/docker/compose/compose.star (deferStart depends_on)",
185
+ output_file:
186
+ "(Traefik + Sablier Docker labels on the resource's own compose service; with deferStart, also a generated Traefik file-provider dynamic-config entry and a depends_on block)",
181
187
  },
182
188
  };
183
189
 
@@ -73,7 +73,8 @@ function parseResource(serviceJsonPath: string): DiscoveredResource {
73
73
  const parsedConfig = parsed as unknown as ResourceConfig;
74
74
  const config: ResourceConfig = {
75
75
  ...parsedConfig,
76
- appType: (parsedConfig.appType || (parsedConfig as any).type) as ResourceConfig["appType"],
76
+ appType: (parsedConfig.appType ||
77
+ (parsedConfig as unknown as { type?: string }).type) as ResourceConfig["appType"],
77
78
  };
78
79
  const resourceDir = dirname(serviceJsonPath);
79
80
 
@@ -84,10 +84,30 @@ export function buildTiltUpArgs(
84
84
  quiet?: boolean;
85
85
  force?: boolean;
86
86
  watch?: boolean;
87
+ focusTargets?: string[];
87
88
  } = {},
88
89
  ): string[] {
89
90
  const args: string[] = [];
90
91
  addTiltfilePath(args);
92
+
93
+ // Bare positional resource names only scope what this `tilt up` invocation waits
94
+ // on - they do NOT limit which resources the generated Tiltfile enables. Its focus
95
+ // filter (Config.apply_focus / apply_focus_filter) only reads the `--focus` flag;
96
+ // the positional-args catch-all it also defines (`cfg['args']`) is never read. So
97
+ // without `--focus`, every `tdk up <stack>` silently fell back to Tilt's default
98
+ // "pre-alpha" phase and built every stack in that phase, not just the one asked
99
+ // for. Passing the stack name via `--focus` reuses the Tiltfile's own (already
100
+ // dependency-aware) domain expansion instead of re-deriving it here.
101
+ //
102
+ // `--focus=...` needs a `--` before it: Tilt's own `tilt up` only accepts its own
103
+ // fixed flag set directly (see `tilt up --help`) and rejects anything else with
104
+ // "unknown flag" - `--` is what routes a token through to the Tiltfile's own
105
+ // config.parse(). Bare words like a service name aren't flag-shaped, so they don't
106
+ // need `--` to reach the Tiltfile's positional `args` catch-all.
107
+ if (options.focusTargets && options.focusTargets.length > 0) {
108
+ args.push("--", `--focus=${options.focusTargets.join(",")}`);
109
+ }
110
+
91
111
  args.push(...serviceNames);
92
112
 
93
113
  if (options.verbose && !options.quiet) {
@@ -0,0 +1,26 @@
1
+ # Wake gateway (see openspec/changes/prioritized-cold-start): fronts every
2
+ # `sablier.deferStart` resource's Traefik static route, triggers cold
3
+ # containers via `tilt trigger` and/or Sablier, then holds the request until
4
+ # healthy. Needs the Docker CLI (client only, talks to the mounted
5
+ # /var/run/docker.sock) and the real `tilt` CLI (D3: shells out to the
6
+ # documented `tilt trigger` command rather than depending on Tilt's
7
+ # undocumented internal HTTP API).
8
+
9
+ FROM oven/bun:1.3.11-alpine AS base
10
+
11
+ # Pin to the same Tilt version this workspace runs (v0.37.7) so `tilt trigger`
12
+ # talks the same protocol as the real daemon it's connecting to. Installed
13
+ # directly (not via tilt-dev's install.sh, which shells out to `sudo` -- not
14
+ # present in this minimal image, and unnecessary since we're already root).
15
+ ARG TILT_VERSION=0.37.7
16
+ RUN apk add --no-cache docker-cli curl gcompat \
17
+ && arch="$(uname -m)" \
18
+ && case "$arch" in aarch64) tilt_arch=arm64 ;; x86_64) tilt_arch=x86_64 ;; *) echo "unsupported arch: $arch" >&2; exit 1 ;; esac \
19
+ && curl -fsSL "https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.${tilt_arch}.tar.gz" \
20
+ | tar -xz -C /usr/local/bin tilt
21
+
22
+ WORKDIR /app
23
+ COPY decide.ts server.ts ./
24
+
25
+ EXPOSE 9999
26
+ CMD ["bun", "run", "server.ts"]
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { decideWakeAction, evaluateReadiness } from "./decide";
3
+
4
+ describe("decideWakeAction", () => {
5
+ test("never-created resource: tilt trigger only, no Sablier start", () => {
6
+ const action = decideWakeAction({
7
+ resourceContainerName: "data-governance-api",
8
+ resourceState: "absent",
9
+ dependencies: [],
10
+ });
11
+ expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
12
+ expect(action.useSablierStart).toBe(false);
13
+ });
14
+
15
+ test("previously-run, now-exited resource: both tilt trigger and Sablier start", () => {
16
+ const action = decideWakeAction({
17
+ resourceContainerName: "data-governance-api",
18
+ resourceState: "exited",
19
+ dependencies: [],
20
+ });
21
+ expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
22
+ expect(action.useSablierStart).toBe(true);
23
+ });
24
+
25
+ test("created-but-not-started resource: both paths apply", () => {
26
+ const action = decideWakeAction({
27
+ resourceContainerName: "data-governance-api",
28
+ resourceState: "created",
29
+ dependencies: [],
30
+ });
31
+ expect(action.useSablierStart).toBe(true);
32
+ });
33
+
34
+ test("already running: no trigger needed", () => {
35
+ const action = decideWakeAction({
36
+ resourceContainerName: "data-governance-api",
37
+ resourceState: "running",
38
+ dependencies: [],
39
+ });
40
+ expect(action.tiltTriggerNames).toEqual([]);
41
+ expect(action.useSablierStart).toBe(false);
42
+ });
43
+
44
+ test("cold dependency is triggered alongside the resource (task 1.5 finding)", () => {
45
+ const action = decideWakeAction({
46
+ resourceContainerName: "data-governance-api",
47
+ resourceState: "absent",
48
+ dependencies: [
49
+ { name: "postgres", state: "running" },
50
+ { name: "data-governance-api-db-migrator", state: "absent" },
51
+ ],
52
+ });
53
+ expect(action.tiltTriggerNames).toEqual([
54
+ "data-governance-api",
55
+ "data-governance-api-db-migrator",
56
+ ]);
57
+ });
58
+
59
+ test("running dependency is not re-triggered", () => {
60
+ const action = decideWakeAction({
61
+ resourceContainerName: "data-governance-api",
62
+ resourceState: "running",
63
+ dependencies: [{ name: "postgres", state: "running" }],
64
+ });
65
+ expect(action.tiltTriggerNames).toEqual([]);
66
+ });
67
+ });
68
+
69
+ describe("evaluateReadiness", () => {
70
+ test("ready once every required name reports healthy", () => {
71
+ const outcome = evaluateReadiness(["a", "b"], { a: true, b: true });
72
+ expect(outcome.ready).toBe(true);
73
+ expect(outcome.pending).toEqual([]);
74
+ });
75
+
76
+ test("not ready while some names are still unhealthy", () => {
77
+ const outcome = evaluateReadiness(["a", "b"], { a: true, b: false });
78
+ expect(outcome.ready).toBe(false);
79
+ expect(outcome.pending).toEqual(["b"]);
80
+ });
81
+
82
+ test("missing entries count as not healthy", () => {
83
+ const outcome = evaluateReadiness(["a", "b"], { a: true });
84
+ expect(outcome.ready).toBe(false);
85
+ expect(outcome.pending).toEqual(["b"]);
86
+ });
87
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Pure decision logic for the wake gateway (see openspec/changes/prioritized-cold-start).
3
+ *
4
+ * The gateway sits behind a Traefik static (file-provider) route for every
5
+ * `sablier: {enable: true, deferStart: true}` resource. This module decides
6
+ * what to *do* given a snapshot of container state; the actual Docker/Tilt
7
+ * I/O and the healthcheck-polling loop live in server.ts, which is a thin
8
+ * wrapper around this so the decision itself stays testable without a live
9
+ * Docker daemon or Tilt instance.
10
+ */
11
+
12
+ export type ContainerState = "absent" | "created" | "running" | "exited";
13
+
14
+ export interface DependencySnapshot {
15
+ name: string;
16
+ state: ContainerState;
17
+ }
18
+
19
+ export interface WakeRequestSnapshot {
20
+ resourceContainerName: string;
21
+ resourceState: ContainerState;
22
+ dependencies: DependencySnapshot[];
23
+ }
24
+
25
+ export interface WakeAction {
26
+ /** Resources to `tilt trigger` (task 1.5: Tilt never cascades this on its own). */
27
+ tiltTriggerNames: string[];
28
+ /**
29
+ * Whether to also try Sablier's own Docker-level start for the resource.
30
+ * Only meaningful when the resource's container already exists (D2/D3):
31
+ * Sablier has nothing to manage for a container that was never created.
32
+ */
33
+ useSablierStart: boolean;
34
+ }
35
+
36
+ /**
37
+ * Decide which resources need `tilt trigger` and whether Sablier's Docker-level
38
+ * start also applies, given one point-in-time snapshot (D2/D3/D5).
39
+ */
40
+ export function decideWakeAction(snapshot: WakeRequestSnapshot): WakeAction {
41
+ const tiltTriggerNames: string[] = [];
42
+
43
+ if (snapshot.resourceState !== "running") {
44
+ tiltTriggerNames.push(snapshot.resourceContainerName);
45
+ }
46
+ for (const dep of snapshot.dependencies) {
47
+ if (dep.state !== "running") {
48
+ tiltTriggerNames.push(dep.name);
49
+ }
50
+ }
51
+
52
+ const useSablierStart = snapshot.resourceState === "exited" || snapshot.resourceState === "created";
53
+
54
+ return { tiltTriggerNames, useSablierStart };
55
+ }
56
+
57
+ export interface WaitOutcome {
58
+ ready: boolean;
59
+ /** Names still not running/healthy when the wait ended (empty when ready). */
60
+ pending: string[];
61
+ }
62
+
63
+ /**
64
+ * Decide whether a set of health polls (resource + dependencies) means the
65
+ * request can be let through yet, or should keep waiting / time out.
66
+ * `healthy` maps each name from a WakeAction's tiltTriggerNames (plus the
67
+ * resource itself) to its latest observed health state.
68
+ */
69
+ export function evaluateReadiness(
70
+ requiredNames: string[],
71
+ healthy: Record<string, boolean>,
72
+ ): WaitOutcome {
73
+ const pending = requiredNames.filter((name) => !healthy[name]);
74
+ return { ready: pending.length === 0, pending };
75
+ }