@tdk-landscape/tdk-cli-core 1.3.62 → 1.3.64

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - `traefik.healthCheck` in service.json (the path Traefik's load balancer probes, documented in the schema) is now honored. It was read by discovery but never used, so Traefik always probed `healthCheckPath`. It defaults to `healthCheckPath`, so manifests that only set that generate the same output as before.
8
+ - `tdk doctor` checks that every backend defines the health routes it is probed on (`healthCheckPath` for the container, `traefik.healthCheck` for Traefik, both defaulting to `/health`) somewhere in `src/`. A backend without them never turns healthy, so Traefik returns 404, which used to surface only after a slow image build.
9
+
10
+ ## 1.3.62 (2026-09-28)
11
+
7
12
  - `tdk doctor` no longer skips the service health check when Traefik answers with an error. A 404 (no route yet) or 502/503/504 (route found, container not answering) is reported as a failure that names the service and URL, instead of being read as "Traefik never bound :80". It is skipped only when every probe fails to connect.
8
13
 
9
14
  ## 1.3.61 (2026-09-28)
@@ -4,7 +4,7 @@ import { dirname, join, normalize } from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { Command } from "commander";
6
6
  import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
7
- import { checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
7
+ import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
8
8
  import { validateEnvFile } from "../utils/env-validator.js";
9
9
  import { formatCount } from "../utils/formatting.js";
10
10
  import { findProjectRoot } from "../utils/paths.js";
@@ -683,6 +683,8 @@ export const doctorCommand = new Command("doctor")
683
683
  checkGeneratedProjectRuntimeAssets,
684
684
  checkStarlarkLoadExports,
685
685
  checkStartupScripts,
686
+ // A backend without its health route never turns healthy; Traefik 404s it.
687
+ () => checkHealthRoutes(),
686
688
  checkTypeScriptTypeDependencies,
687
689
  checkFrontendDockerPreflight,
688
690
  checkEnvironmentVariables,
@@ -1,11 +1,11 @@
1
1
  import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { createServer } from "node:net";
4
4
  import { join } from "node:path";
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
+ import { discoverResources, discoverResourcesFromRoot } from "./services.js";
9
9
  const EXEC_TIMEOUT_MS = 10_000;
10
10
  const REGISTRY_PROBE_TIMEOUT_MS = 3_000;
11
11
  const DEFAULT_VERDACCIO_URL = "http://localhost:4873";
@@ -636,4 +636,109 @@ export function describeTiltFailure(failure, exec = execSync) {
636
636
  }
637
637
  return failure.updateStatus || failure.runtimeStatus || "unknown error";
638
638
  }
639
+ const SOURCE_FILE = /\.(ts|tsx|mts|js|mjs|cjs)$/;
640
+ const SKIPPED_DIRS = new Set(["node_modules", "dist", ".autogenerated", "tests", "__tests__"]);
641
+ function listSourceFiles(dir) {
642
+ const files = [];
643
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
644
+ if (entry.isDirectory()) {
645
+ if (!SKIPPED_DIRS.has(entry.name))
646
+ files.push(...listSourceFiles(join(dir, entry.name)));
647
+ }
648
+ else if (SOURCE_FILE.test(entry.name) && !/\.(test|spec)\./.test(entry.name)) {
649
+ files.push(join(dir, entry.name));
650
+ }
651
+ }
652
+ return files;
653
+ }
654
+ /**
655
+ * The two paths a backend is probed on, resolved like the engine does:
656
+ * the container healthcheck uses `healthCheckPath` (default /health), and
657
+ * Traefik's load balancer uses `traefik.healthCheck`, defaulting to the
658
+ * container's path.
659
+ */
660
+ export function resolveHealthPaths(config) {
661
+ const container = config?.healthCheckPath || "/health";
662
+ return { container, traefik: config?.traefik?.healthCheck || container };
663
+ }
664
+ /**
665
+ * Backends whose source never mentions a path they are probed on as a string
666
+ * literal ('/health', "/health" or `/health`). Covers Hono, Express, Elysia,
667
+ * Fastify and hand-rolled `url.pathname === "/health"` checks. Services
668
+ * without a src/ directory are skipped, since there is nothing to read.
669
+ */
670
+ export function findMissingHealthRoutes(resources) {
671
+ const missing = [];
672
+ for (const resource of resources) {
673
+ if (resource.config?.appType !== "backend")
674
+ continue;
675
+ const srcDir = join(resource.path, "src");
676
+ if (!existsSync(srcDir))
677
+ continue;
678
+ const files = listSourceFiles(srcDir);
679
+ if (files.length === 0)
680
+ continue;
681
+ const sources = files.map((file) => readFileSync(file, "utf-8"));
682
+ const { container, traefik } = resolveHealthPaths(resource.config);
683
+ const probes = container === traefik
684
+ ? [{ path: container, usedBy: "container and Traefik healthchecks" }]
685
+ : [
686
+ { path: container, usedBy: "container healthcheck" },
687
+ { path: traefik, usedBy: "Traefik healthcheck" },
688
+ ];
689
+ for (const { path, usedBy } of probes) {
690
+ const literals = ["'", '"', "`"].map((q) => `${q}${path}${q}`);
691
+ if (!sources.some((source) => literals.some((literal) => source.includes(literal)))) {
692
+ missing.push({ name: resource.name, healthPath: path, usedBy });
693
+ }
694
+ }
695
+ }
696
+ return missing;
697
+ }
698
+ /**
699
+ * A backend without its health route never passes the container healthcheck,
700
+ * so Traefik never routes to it and every request gets a 404 - after a slow
701
+ * image build, with nothing in `tdk up` saying why. Catch it before starting.
702
+ */
703
+ export function checkHealthRoutes(projectRoot = findProjectRoot() ?? process.cwd()) {
704
+ let resources = [];
705
+ try {
706
+ resources = discoverResourcesFromRoot(projectRoot);
707
+ }
708
+ catch {
709
+ return {
710
+ name: "Health Routes",
711
+ didPass: true,
712
+ isSkipped: true,
713
+ message: "Could not read services - skipped health route check",
714
+ };
715
+ }
716
+ const backends = resources.filter((r) => r.config?.appType === "backend");
717
+ if (backends.length === 0) {
718
+ return {
719
+ name: "Health Routes",
720
+ didPass: true,
721
+ isSkipped: true,
722
+ message: "No backend services to check",
723
+ };
724
+ }
725
+ const missing = findMissingHealthRoutes(backends);
726
+ if (missing.length === 0) {
727
+ return {
728
+ name: "Health Routes",
729
+ didPass: true,
730
+ message: `All ${formatCount(backends.length, "backend")} define their health route`,
731
+ };
732
+ }
733
+ const details = missing
734
+ .map((m) => `${m.name}: no "${m.healthPath}" route in src/ (probed by the ${m.usedBy})`)
735
+ .join("\n ");
736
+ return {
737
+ name: "Health Routes",
738
+ didPass: false,
739
+ message: `Backends without a health route (the container never turns healthy, so Traefik returns 404):\n ${details}`,
740
+ fix: `Add the route, e.g. for Hono: app.get("${missing[0]?.healthPath}", (c) => c.json({ status: "ok" })). ` +
741
+ 'To use a different path, set "healthCheckPath" (container healthcheck) or "traefik.healthCheck" (Traefik) in its service.json.',
742
+ };
743
+ }
639
744
  //# sourceMappingURL=doctor-runtime.js.map
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.62",
4
+ "version": "1.3.64",
5
5
  "description": "TDK CLI - Tilt Development Kit command line interface",
6
6
  "keywords": [
7
7
  "tdk",
@@ -287,7 +287,9 @@ def _apply_manifest_defaults(manifest, resource_path):
287
287
  computed_traefik = {
288
288
  'host': stack + '.backend.' + PlatformDockerConstants.PROJECT_NAME + '.local',
289
289
  'pathPrefix': default_path_prefix,
290
- 'healthCheck': HEALTH_CHECK_PATH,
290
+ # Follows healthCheckPath so a manifest that only sets that keeps one
291
+ # path for both probes; `traefik.healthCheck` overrides just Traefik's.
292
+ 'healthCheck': result.get('healthCheckPath') or HEALTH_CHECK_PATH,
291
293
  }
292
294
 
293
295
  # Check for overrides
@@ -248,13 +248,16 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
248
248
  traefik_labels = ' - "traefik.enable=false"'
249
249
  healthcheck_section = ""
250
250
  else:
251
+ # Traefik's load-balancer check can be overridden separately in service.json
252
+ # (`traefik.healthCheck`); it defaults to the container check's path.
253
+ traefik_health_path = traefik_cfg.get('healthCheck') or health_path
251
254
  traefik_labels = get_backend_traefik_labels(
252
255
  resource_entry_name=resource_entry_name,
253
256
  traefik_host=traefik_host,
254
257
  traefik_path=traefik_path,
255
258
  traefik_resource_name=traefik_resource_name,
256
259
  internal_port=internal_port,
257
- health_path=health_path,
260
+ health_path=traefik_health_path,
258
261
  manifest=manifest,
259
262
  )
260
263
  healthcheck_section = """ healthcheck:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.62",
3
+ "version": "1.3.64",
4
4
  "description": "Run 100 microservices on a 16 GB laptop without Kubernetes. Scaffold services and run them locally with hot reload, built on Tilt.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {