@tdk-landscape/tdk-cli-core 1.3.71 → 1.3.73

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,14 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - Free-tier projects no longer start the `sablier` and `wake-gateway` containers, and Traefik no longer downloads the Sablier plugin from GitHub on every start. They were always launched (the `sablier` one with a read-write Docker socket) even though the license only gated the per-service labels, so nothing could use them. They are now generated only when a license grants Sablier. Fewer containers, one image build less, and no network fetch at Traefik boot. Spotted in the ERP CI run log.
8
+
9
+ ## 1.3.72 (2026-09-28)
10
+
11
+ - `tdk doctor` no longer fails with "Verdaccio is not reachable" when the project config enables Verdaccio but no Premium license grants it. TDK does not start Verdaccio in that case, so the check could never pass; it is now skipped with a note. A registry your own `.npmrc` points at (`:4873`) is still checked. Found by the ERP CI workflow.
12
+
13
+ ## 1.3.71 (2026-09-28)
14
+
7
15
  - `tdk project` in a freshly cloned repo (one that already has `.tdk/project.json`) now creates the missing `.env` with working defaults. Before, it regenerated the configs but left no `.env`, and `tdk doctor` then failed with "set VERDACCIO_URL_DOCKER=<value>". Doctor now says to run `tdk project` when there is no `.env` at all. Found by the ERP CI workflow.
8
16
 
9
17
  ## 1.3.70 (2026-09-28)
@@ -3,8 +3,9 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, normalize } from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { Command } from "commander";
6
+ import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
6
7
  import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
7
- import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
8
+ import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
8
9
  import { validateEnvFile } from "../utils/env-validator.js";
9
10
  import { formatCount } from "../utils/formatting.js";
10
11
  import { findProjectRoot } from "../utils/paths.js";
@@ -755,7 +756,13 @@ export const doctorCommand = new Command("doctor")
755
756
  // Preflight: a local Postgres or web server on 5432/80/443.
756
757
  () => checkHostPorts(),
757
758
  // Preflight: Verdaccio down causes ImageBuild bun install ConnectionRefused.
758
- checkPrivateNpmRegistry,
759
+ async () => {
760
+ const root = findProjectRoot() ?? process.cwd();
761
+ const licensed = projectConfigEnablesVerdaccio(root)
762
+ ? await hasVerdaccioLicense(root)
763
+ : true;
764
+ return checkPrivateNpmRegistry(execSync, root, undefined, licensed);
765
+ },
759
766
  // Runtime checks: skip gracefully if the stack isn't started yet.
760
767
  checkContainerResourceHealth,
761
768
  // When Tilt is up, surface red resources (Traefik/apps never started).
@@ -175,23 +175,20 @@ export function summarizeTiltBuildError(error) {
175
175
  export function isRegistryRelatedBuildError(error) {
176
176
  return /ConnectionRefused downloading package manifest|Verdaccio|private registry|dependency install failed during ImageBuild|install-deps\.sh|bun install/i.test(error);
177
177
  }
178
- export function projectExpectsVerdaccio(projectRoot = findProjectRoot() ?? process.cwd()) {
178
+ export function projectConfigEnablesVerdaccio(projectRoot = findProjectRoot() ?? process.cwd()) {
179
179
  const projectJsonPath = join(projectRoot, ".tdk", "project.json");
180
- if (existsSync(projectJsonPath)) {
181
- try {
182
- const parsed = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
183
- if (parsed.optional_infra?.verdaccio === true) {
184
- return true;
185
- }
186
- if (Object.values(parsed.phases ?? {}).some((phase) => (phase.enabledStacks ?? []).includes("verdaccio"))) {
187
- return true;
188
- }
189
- }
190
- catch {
191
- // Fall through to npmrc heuristics.
192
- }
180
+ if (!existsSync(projectJsonPath))
181
+ return false;
182
+ try {
183
+ const parsed = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
184
+ return (parsed.optional_infra?.verdaccio === true ||
185
+ Object.values(parsed.phases ?? {}).some((phase) => (phase.enabledStacks ?? []).includes("verdaccio")));
193
186
  }
194
- // Heuristic: scoped private registry pointed at :4873
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ function npmrcPointsAtVerdaccio(projectRoot) {
195
192
  for (const candidate of [join(projectRoot, ".npmrc"), join(projectRoot, "package.json")]) {
196
193
  if (!existsSync(candidate))
197
194
  continue;
@@ -207,13 +204,24 @@ export function projectExpectsVerdaccio(projectRoot = findProjectRoot() ?? proce
207
204
  }
208
205
  return false;
209
206
  }
207
+ export function projectExpectsVerdaccio(projectRoot = findProjectRoot() ?? process.cwd()) {
208
+ return projectConfigEnablesVerdaccio(projectRoot) || npmrcPointsAtVerdaccio(projectRoot);
209
+ }
210
210
  /**
211
211
  * When the project uses a local Verdaccio registry, fail early with a clear
212
212
  * fix instead of only showing truncated ImageBuild exit codes later.
213
213
  */
214
214
  export function checkPrivateNpmRegistry(exec = execSync, projectRoot = findProjectRoot() ?? process.cwd(), registryUrl = process.env.VERDACCIO_URL ??
215
215
  process.env.NPM_REGISTRY_URL ??
216
- DEFAULT_VERDACCIO_URL) {
216
+ DEFAULT_VERDACCIO_URL, verdaccioLicensed = true) {
217
+ if (!verdaccioLicensed && !npmrcPointsAtVerdaccio(projectRoot)) {
218
+ return {
219
+ name: "Private npm registry",
220
+ didPass: true,
221
+ isSkipped: true,
222
+ message: "Verdaccio is enabled in the project config but needs a Premium license, so TDK will not start it - skipped",
223
+ };
224
+ }
217
225
  if (!projectExpectsVerdaccio(projectRoot)) {
218
226
  return {
219
227
  name: "Private npm registry",
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.71",
4
+ "version": "1.3.73",
5
5
  "description": "TDK CLI - Tilt Development Kit command line interface",
6
6
  "keywords": [
7
7
  "tdk",
@@ -10,8 +10,14 @@ load("../config/healthcheck.star", "compose_healthcheck_timing")
10
10
  load("../networking/traefik_static_routes.star", "normalize_abs_path")
11
11
 
12
12
 
13
- def generate_standalone_traefik_compose():
14
- """Minimal Traefik that routes Docker labels on api.{project}.localhost."""
13
+ def generate_standalone_traefik_compose(sablier_enabled = False):
14
+ """Minimal Traefik that routes Docker labels on api.{project}.localhost.
15
+
16
+ The Sablier plugin, the sablier container and the wake gateway are only
17
+ emitted when sablier_enabled is True (a license grants "sablier"). Without
18
+ it they would run for nothing: a container holding a read-write Docker
19
+ socket, an image build, and a plugin download from GitHub on every start.
20
+ """
15
21
  network = PlatformDockerConstants.NETWORK_TRAEFIK_PUBLIC
16
22
  name = PlatformDockerConstants.PROJECT_NAME
17
23
  return """###############################################################################
@@ -24,10 +30,7 @@ services:
24
30
  image: traefik:v3.6.8
25
31
  container_name: {name}_traefik
26
32
  restart: unless-stopped
27
- depends_on:
28
- - sablier
29
- - wake-gateway
30
- command:
33
+ {traefik_depends_on} command:
31
34
  - "--api.insecure=true"
32
35
  - "--ping=true"
33
36
  - "--providers.docker=true"
@@ -44,12 +47,7 @@ services:
44
47
  - "--entrypoints.web.address=:80"
45
48
  - "--entrypoints.websecure.address=:443"
46
49
  - "--log.level=INFO"
47
- # On-demand scaling: routes to stopped containers (opt-in via a
48
- # resource's `sablier:` manifest block), keeps idle services from
49
- # burning memory. Requires Traefik >=3.6 (see allownonrunning below).
50
- - "--experimental.plugins.sablier.moduleName=github.com/sablierapp/sablier-traefik-plugin"
51
- - "--experimental.plugins.sablier.version=v1.3.1"
52
- ports:
50
+ {sablier_flags} ports:
53
51
  - "80:80"
54
52
  - "8080:8080"
55
53
  volumes:
@@ -60,7 +58,66 @@ services:
60
58
  healthcheck:
61
59
  test: ["CMD", "traefik", "healthcheck", "--ping"]
62
60
  {healthcheck_timing}
63
- sablier:
61
+ {sablier_services}networks:
62
+ traefik-public:
63
+ name: {network}
64
+ external: true
65
+ """.format(
66
+ name=name,
67
+ network=network,
68
+ healthcheck_timing=compose_healthcheck_timing(10),
69
+ traefik_depends_on=_SABLIER_DEPENDS_ON if sablier_enabled else "",
70
+ sablier_flags=_SABLIER_FLAGS if sablier_enabled else "",
71
+ sablier_services=_SABLIER_SERVICES.format(
72
+ name=name,
73
+ gateway_container=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
74
+ gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
75
+ vendored_gateway_dir="tdk-cli-ext/engine/assets/docker/wake-gateway",
76
+ tilt_dev_dir=os.environ.get("HOME", "") + "/.tilt-dev",
77
+ project_root_mount=_project_root_mount(),
78
+ ) if sablier_enabled else "",
79
+ dynamic_dir_name=PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL.split("/")[-1],
80
+ vendored_gateway_dir="tdk-cli-ext/engine/assets/docker/wake-gateway",
81
+ gateway_container=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
82
+ gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
83
+ tilt_dev_dir=os.environ.get("HOME", "") + "/.tilt-dev",
84
+ project_root_mount=_project_root_mount(),
85
+ )
86
+
87
+
88
+ def _project_root_mount():
89
+ """Read-only mount of the project at its own absolute host path.
90
+
91
+ The gateway runs `docker compose up --no-build` for a deferred resource
92
+ itself (bypassing Tilt's build queue), using the compose files Tilt uses.
93
+ Compose resolves relative paths in them (env_file etc.) client-side, so
94
+ they must exist at the same absolute paths inside the gateway container.
95
+ Empty when TDK_PROJECT_ROOT is unset (the generated Tiltfile sets it).
96
+ """
97
+ root = project_root_abspath()
98
+ if not root:
99
+ return ""
100
+ return "\n - " + root + ":" + root + ":ro"
101
+
102
+
103
+ def project_root_abspath():
104
+ """Absolute, normalized project root (TDK_PROJECT_ROOT), or "" when unset."""
105
+ return normalize_abs_path(os.environ.get("TDK_PROJECT_ROOT", ""))
106
+
107
+
108
+ _SABLIER_DEPENDS_ON = """ depends_on:
109
+ - sablier
110
+ - wake-gateway
111
+ """
112
+
113
+ _SABLIER_FLAGS = """ # On-demand scaling: routes to stopped containers (opt-in via a
114
+ # resource's `sablier:` manifest block), keeps idle services from
115
+ # burning memory. Requires Traefik >=3.6 (see allownonrunning below).
116
+ - "--experimental.plugins.sablier.moduleName=github.com/sablierapp/sablier-traefik-plugin"
117
+ - "--experimental.plugins.sablier.version=v1.3.1"
118
+ """
119
+
120
+ _SABLIER_SERVICES = """ sablier:
64
121
  image: sablierapp/sablier:1.18.0
65
122
  container_name: {name}_sablier
66
123
  restart: unless-stopped
@@ -96,38 +153,4 @@ services:
96
153
  networks:
97
154
  - traefik-public
98
155
 
99
- networks:
100
- traefik-public:
101
- name: {network}
102
- external: true
103
- """.format(
104
- name=name,
105
- network=network,
106
- healthcheck_timing=compose_healthcheck_timing(10),
107
- dynamic_dir_name=PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL.split("/")[-1],
108
- vendored_gateway_dir="tdk-cli-ext/engine/assets/docker/wake-gateway",
109
- gateway_container=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
110
- gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
111
- tilt_dev_dir=os.environ.get("HOME", "") + "/.tilt-dev",
112
- project_root_mount=_project_root_mount(),
113
- )
114
-
115
-
116
- def _project_root_mount():
117
- """Read-only mount of the project at its own absolute host path.
118
-
119
- The gateway runs `docker compose up --no-build` for a deferred resource
120
- itself (bypassing Tilt's build queue), using the compose files Tilt uses.
121
- Compose resolves relative paths in them (env_file etc.) client-side, so
122
- they must exist at the same absolute paths inside the gateway container.
123
- Empty when TDK_PROJECT_ROOT is unset (the generated Tiltfile sets it).
124
- """
125
- root = project_root_abspath()
126
- if not root:
127
- return ""
128
- return "\n - " + root + ":" + root + ":ro"
129
-
130
-
131
- def project_root_abspath():
132
- """Absolute, normalized project root (TDK_PROJECT_ROOT), or "" when unset."""
133
- return normalize_abs_path(os.environ.get("TDK_PROJECT_ROOT", ""))
156
+ """
@@ -20,6 +20,7 @@
20
20
 
21
21
  load("../../platform/docker/constants.star", "PlatformDockerConstants")
22
22
  load("../../platform/docker/compose/traefik_standalone.star", "generate_standalone_traefik_compose")
23
+ load("../../platform/docker/networking/sablier_container_cycle.star", "sablier_middleware_suffix")
23
24
  load("../../platform/registries/verdaccio_loader.star", "load_verdaccio")
24
25
 
25
26
  # =============================================================================
@@ -194,7 +195,11 @@ def _load_standalone_traefik(root_prefix, env_file, write_fn):
194
195
  """Generate and load a self-contained Traefik compose for standalone projects."""
195
196
  _stop_conflicting_traefik()
196
197
  compose_rel = ".tdk/.tdk-out/docker-compose.traefik.yml"
197
- content = generate_standalone_traefik_compose()
198
+ # The free-tier stub always reports "disabled"; the licensed module reports
199
+ # enabled for a manifest that opts in. Probing it keeps this file free of any
200
+ # license logic while starting the sablier containers only when they can work.
201
+ _, sablier_enabled = sablier_middleware_suffix({"sablier": {"enable": True}}, "probe")
202
+ content = generate_standalone_traefik_compose(sablier_enabled)
198
203
  if write_fn:
199
204
  write_fn(compose_rel, content)
200
205
  compose_file = root_prefix + compose_rel if root_prefix else compose_rel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.71",
3
+ "version": "1.3.73",
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": {