@tdk-landscape/tdk-cli-core 1.3.80 → 1.3.82

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,8 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.3.80 (2026-09-30)
8
+
7
9
  - Add `tdk doctor --json` with schema-versioned readiness, checks, and structured errors; diagnostics remain on stderr.
8
10
  - Stabilize doctor exits: 0 ready, 1 blocking findings, 2 invalid arguments/internal check failures. Invalid `--ping-timeout` now exits 2 instead of 1 and rejects fractional/unit-suffixed values.
9
11
  - Add versioned `--json` envelopes to `tdk status`, `tdk resources`, and `tdk networks`; existing `networks --json` array consumers can migrate with `--json-legacy` through CLI 1.4.x. The compatibility flag is scheduled for removal in 1.5.0.
package/README.md CHANGED
@@ -1,6 +1,24 @@
1
- # TDK — Tilt Development Kit
1
+ # TDK CLI — start services on your laptop
2
2
 
3
- TDK runs many services on your laptop with Docker + Tilt. Helm still deploys the cluster.
3
+ TDK CLI starts your services on your laptop. It is not a deploy and not a Compose file: define each service in `service.json`, then run `tdk up`. No Kubernetes is needed on the machine.
4
+
5
+ Docker runs the containers. Tilt runs the development loop. TDK CLI writes that configuration. Production deployment stays with Helm, Argo CD, or Kustomize.
6
+
7
+ [Website](https://tdk-landscape.github.io/tdk-website/) · [Quickstart](https://tdk-landscape.github.io/tdk-website/docs/quickstart/) · [Examples](https://tdk-landscape.github.io/tdk-website/docs/examples/) · [Awesome TDK](https://github.com/tdk-landscape/awesome-tdk-framework) · [Demo](https://tdk-landscape.github.io/tdk-demo-animation/) · [Report a bug](https://github.com/tdk-landscape/tdk-cli-core/issues)
8
+
9
+ ## Quick start
10
+
11
+ ```bash
12
+ mkdir shop && cd shop
13
+ tdk project --yes
14
+ tdk resource orders-api --type backend --stack shop --yes
15
+ tdk up shop
16
+ curl http://api.shop.localhost/api/orders-api/health
17
+ ```
18
+
19
+ If Helm, Compose, or your existing Tilt setup already gives you a working local environment, keep using it. TDK CLI is for engineers managing several services who want a clear local service contract and one command to start the stack.
20
+
21
+ See [how TDK CLI works alongside Helm](https://tdk-landscape.github.io/tdk-website/docs/with-helm/), the [service schema](engine/schemas/service-schema.json), and the [project configuration schema](engine/schemas/project-schema.json).
4
22
 
5
23
  [![npm version](https://img.shields.io/npm/v/@tdk-landscape/tdk-cli-core.svg?style=flat&color=blue)](https://www.npmjs.com/package/@tdk-landscape/tdk-cli-core)
6
24
  [![CI](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
@@ -21,21 +39,12 @@ npm install -g @tdk-landscape/tdk-cli-core
21
39
  curl -fsSL https://tdk-landscape.github.io/install.sh | sh
22
40
  ```
23
41
 
24
- ## Quick start
25
-
26
- ```bash
27
- mkdir shop && cd shop
28
- tdk project --yes
29
- tdk resource orders-api --type backend --stack shop --yes
30
- tdk up shop
31
- tdk networks
32
- ```
33
-
34
42
  `tdk up shop --dry-run` previews selected local services and URLs before starting containers. The default starter is Bun/TypeScript; TDK's core role is running local containers through Docker + Tilt, not providing a Node.js application framework. See the [one-backend example](examples/one-backend/README.md).
35
43
 
36
44
  ## When not to use TDK
37
45
 
38
46
  - Your existing Compose or Tilt workflow already gives you a working local environment.
47
+ - Helm is your whole development workflow and you do not want local services on Docker.
39
48
  - You do not want `service.json` manifests and generated local configuration.
40
49
 
41
50
  ## Docs
@@ -52,7 +61,9 @@ tdk networks
52
61
 
53
62
  ## Requirements and support
54
63
 
55
- For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. Ports 80, 443, and 5432 must be free. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness. See [WSL2 setup](docs/wsl2.md).
64
+ For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness. See [WSL2 setup](docs/wsl2.md).
65
+
66
+ **Ecosystem map:** examples, articles, related tools, and notes live in [awesome-tdk-framework](https://github.com/tdk-landscape/awesome-tdk-framework). Star this repo (`tdk-cli-core`) if the CLI is what you run; use the awesome list to browse the rest.
56
67
 
57
68
  ## Contributing and license
58
69
 
package/cli/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { projectCommand } from "./commands/project.js";
13
13
  import { projectsCommand } from "./commands/projects.js";
14
14
  import { resourceCommand } from "./commands/resource.js";
15
15
  import { resourcesCommand } from "./commands/resources.js";
16
+ import { runtimeCommand } from "./commands/runtime.js";
16
17
  import { stackCommand } from "./commands/stack.js";
17
18
  import { stacksCommand } from "./commands/stacks.js";
18
19
  import { statusCommand } from "./commands/status.js";
@@ -46,6 +47,7 @@ program.addCommand(ejectCommand);
46
47
  program.addCommand(statusCommand);
47
48
  program.addCommand(stackCommand);
48
49
  program.addCommand(resourceCommand);
50
+ program.addCommand(runtimeCommand);
49
51
  program.addCommand(projectCommand);
50
52
  program.addCommand(configCommand);
51
53
  program.addCommand(uiCommand);
@@ -11,10 +11,14 @@ _tdk_completions() {
11
11
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
12
12
 
13
13
  # Main commands
14
- local commands="project projects stack stacks resource resources up down status ui doctor version completion help upgrade"
14
+ local commands="project projects stack stacks resource resources up down status ui doctor runtime version completion help upgrade"
15
15
 
16
16
  # Options for specific commands
17
17
  case "\${prev}" in
18
+ runtime)
19
+ COMPREPLY=( $(compgen -W "--check-assets --json" -- \${cur}) )
20
+ return 0
21
+ ;;
18
22
  tdk)
19
23
  COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
20
24
  return 0
@@ -108,6 +112,11 @@ _tdk() {
108
112
  _arguments
109
113
  '--fix[Attempt to fix issues]'
110
114
  ;;
115
+ runtime)
116
+ _arguments
117
+ '--check-assets[Check bundled engine and template assets]'
118
+ '--json[Print the check result as JSON]'
119
+ ;;
111
120
  upgrade)
112
121
  _arguments
113
122
  '--force[Force upgrade even if on latest]'
@@ -129,6 +138,7 @@ _tdk_commands() {
129
138
  'status:Show resource status'
130
139
  'ui:Open interactive UI'
131
140
  'doctor:Check environment'
141
+ 'runtime:Inspect packaged runtime assets'
132
142
  'version:Show version'
133
143
  'upgrade:Upgrade TDK CLI'
134
144
  'completion:Generate shell completions'
@@ -173,6 +183,7 @@ complete -c tdk -n '__fish_use_subcommand' -a 'down' -d 'Stop all services'
173
183
  complete -c tdk -n '__fish_use_subcommand' -a 'status' -d 'Show resource status'
174
184
  complete -c tdk -n '__fish_use_subcommand' -a 'ui' -d 'Open interactive UI'
175
185
  complete -c tdk -n '__fish_use_subcommand' -a 'doctor' -d 'Check environment'
186
+ complete -c tdk -n '__fish_use_subcommand' -a 'runtime' -d 'Inspect packaged runtime assets'
176
187
  complete -c tdk -n '__fish_use_subcommand' -a 'version' -d 'Show version'
177
188
  complete -c tdk -n '__fish_use_subcommand' -a 'upgrade' -d 'Upgrade TDK CLI'
178
189
  complete -c tdk -n '__fish_use_subcommand' -a 'completion' -d 'Generate shell completions'
@@ -208,6 +219,8 @@ complete -c tdk -n '__fish_seen_subcommand_from up' -a '(tdk resources 2>/dev/nu
208
219
 
209
220
  # doctor command options
210
221
  complete -c tdk -n '__fish_seen_subcommand_from doctor' -l fix -d 'Attempt to fix issues'
222
+ complete -c tdk -n '__fish_seen_subcommand_from runtime' -l check-assets -d 'Check bundled engine and template assets'
223
+ complete -c tdk -n '__fish_seen_subcommand_from runtime' -l json -d 'Print the check result as JSON'
211
224
 
212
225
  # upgrade command options
213
226
  complete -c tdk -n '__fish_seen_subcommand_from upgrade' -l force -d 'Force upgrade even if on latest'
@@ -215,7 +228,7 @@ complete -c tdk -n '__fish_seen_subcommand_from upgrade' -l dry-run -d 'Show wha
215
228
  `;
216
229
  const POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName tdk -ScriptBlock {
217
230
  param($wordToComplete)
218
- $cmds = @('up','down','doctor','project','resource','stacks','status','networks','upgrade','version','ui','config')
231
+ $cmds = @('up','down','doctor','project','resource','stacks','status','networks','upgrade','runtime','version','ui','config')
219
232
  $cmds | Where-Object { $_ -like "$wordToComplete*" }
220
233
  }`;
221
234
  const POWERSHELL_START = "# TDK-CLI-COMPLETION-START";
@@ -7,11 +7,13 @@ import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
7
7
  import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constants.js";
8
8
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
9
9
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
10
- import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
10
+ import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
11
11
  import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
12
12
  import { validateEnvFile } from "../utils/env-validator.js";
13
13
  import { execAsync, isExecTimeout } from "../utils/exec-async.js";
14
14
  import { formatCount } from "../utils/formatting.js";
15
+ import { getHostPortPlan } from "../utils/host-port-config.js";
16
+ import { createHostPortPlan } from "../utils/host-port-plan.js";
15
17
  import { findProjectRoot } from "../utils/paths.js";
16
18
  import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
17
19
  import { discoverResourcesFromRoot } from "../utils/services.js";
@@ -954,9 +956,9 @@ const DEFAULT_PING_TIMEOUT_MS = 5000;
954
956
  * it catches the case where a container is running but Traefik never routed
955
957
  * to it, which `docker ps` alone will not show.
956
958
  */
957
- async function checkServiceHealth(timeoutMs) {
959
+ async function checkServiceHealth(timeoutMs, ingressPort) {
958
960
  const projectRoot = findProjectRoot() ?? process.cwd();
959
- const targets = buildHealthTargets(discoverResourcesFromRoot(projectRoot));
961
+ const targets = buildHealthTargets(discoverResourcesFromRoot(projectRoot), ingressPort);
960
962
  if (targets.length === 0) {
961
963
  return {
962
964
  name: "Service Health",
@@ -999,6 +1001,13 @@ export const doctorCommand = new Command("doctor")
999
1001
  }
1000
1002
  if (process.platform === "win32") {
1001
1003
  if (options.json) {
1004
+ let windowsPortPlan = null;
1005
+ try {
1006
+ windowsPortPlan = await createHostPortPlan();
1007
+ }
1008
+ catch {
1009
+ /* The native-Windows platform finding remains the primary failure. */
1010
+ }
1002
1011
  console.log(JSON.stringify(createDoctorReport([
1003
1012
  {
1004
1013
  name: "Native Windows support",
@@ -1006,7 +1015,7 @@ export const doctorCommand = new Command("doctor")
1006
1015
  message: NATIVE_WINDOWS_DOCTOR_MESSAGE,
1007
1016
  fix: "Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md",
1008
1017
  },
1009
- ], Boolean(findProjectRoot()))));
1018
+ ], Boolean(findProjectRoot()), [], windowsPortPlan)));
1010
1019
  }
1011
1020
  console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
1012
1021
  process.exit(1);
@@ -1016,9 +1025,39 @@ export const doctorCommand = new Command("doctor")
1016
1025
  console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
1017
1026
  console.log("Checking environment...\n");
1018
1027
  }
1028
+ const projectRoot = findProjectRoot();
1029
+ let hostPortPlan;
1030
+ let portPlanError;
1031
+ try {
1032
+ hostPortPlan = await getHostPortPlan(projectRoot ?? process.cwd());
1033
+ }
1034
+ catch (error) {
1035
+ hostPortPlan = null;
1036
+ portPlanError = error instanceof Error ? error.message : String(error);
1037
+ }
1019
1038
  const machineChecks = [
1020
- () => checkHostPorts(),
1021
- () => checkIngressPorts(),
1039
+ () => hostPortPlan
1040
+ ? {
1041
+ name: "Host Ports",
1042
+ didPass: true,
1043
+ message: `HTTP ${hostPortPlan.ingressHttp}, HTTPS ${hostPortPlan.ingressHttps}, Postgres ${hostPortPlan.postgres}`,
1044
+ }
1045
+ : {
1046
+ name: "Host Ports",
1047
+ didPass: false,
1048
+ message: portPlanError ?? "Could not select host ports",
1049
+ fix: "Set TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT to available host ports.",
1050
+ },
1051
+ () => hostPortPlan
1052
+ ? checkIngressPorts(execSync, undefined, [
1053
+ hostPortPlan.ingressHttp,
1054
+ hostPortPlan.ingressHttps,
1055
+ ])
1056
+ : {
1057
+ name: "Ingress Ports",
1058
+ didPass: false,
1059
+ message: "Ingress port plan is unavailable",
1060
+ },
1022
1061
  checkDockerRuntime,
1023
1062
  checkDockerOperatingSystem,
1024
1063
  checkTilt,
@@ -1061,14 +1100,14 @@ export const doctorCommand = new Command("doctor")
1061
1100
  ];
1062
1101
  // Runs last: needs routable services (and working Traefik) to mean anything.
1063
1102
  if (options.ping) {
1064
- projectChecks.push(() => checkServiceHealth(pingTimeout));
1103
+ projectChecks.push(() => checkServiceHealth(pingTimeout, hostPortPlan?.ingressHttp));
1065
1104
  }
1066
1105
  // Right after installing, people run `tdk doctor` before they have a
1067
1106
  // project. Only the machine checks mean anything there; the project checks
1068
1107
  // would all fail with "run tdk project".
1069
- const inProject = Boolean(findProjectRoot());
1108
+ const inProject = Boolean(projectRoot);
1070
1109
  const { checks: results, errors } = await collectDoctorChecks(machineChecks, inProject ? projectChecks : []);
1071
- const report = createDoctorReport(orderDoctorResults(results), inProject, errors);
1110
+ const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan);
1072
1111
  const exitCode = getDoctorExitCode(report);
1073
1112
  const allPassed = report.data.ready;
1074
1113
  if (options.json) {
@@ -63,6 +63,11 @@ const COMMAND_GROUPS = [
63
63
  alias: "regenerate, verify, edit",
64
64
  },
65
65
  { name: "doctor", desc: "Check environment readiness", alias: "" },
66
+ {
67
+ name: "runtime",
68
+ desc: "Inspect packaged engine and template assets",
69
+ alias: "--check-assets",
70
+ },
66
71
  { name: "completion", desc: "Generate shell completions", alias: "--install --shell zsh" },
67
72
  { name: "upgrade, update", desc: "Self-update to latest version", alias: "--force" },
68
73
  { name: "version, -v", desc: "Display version number", alias: "" },
@@ -6,6 +6,8 @@ import { getStackEmoji } from "../utils/constants.js";
6
6
  import { createDiscoveryContext } from "../utils/discovery-context.js";
7
7
  import { logVerbose, requireProjectRoot } from "../utils/errors.js";
8
8
  import { colorizeByStatus, DEFAULT_BOX_WIDTH, formatBoxLine, formatPadded, getStatusIcon, printBoxedHeader, } from "../utils/formatting.js";
9
+ import { readSavedHostPortPlan } from "../utils/host-port-config.js";
10
+ import { createHostPortPlan } from "../utils/host-port-plan.js";
9
11
  import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
10
12
  import { findProjectRoot } from "../utils/paths.js";
11
13
  import { checkPortStatus } from "../utils/port-assignment.js";
@@ -166,6 +168,9 @@ export const networksCommand = new Command("networks")
166
168
  if (!projectRoot) {
167
169
  throw new Error("Could not find project root (no .tdk/project.json found)");
168
170
  }
171
+ const savedPlan = readSavedHostPortPlan(projectRoot);
172
+ const hostPortPlan = savedPlan ?? (await createHostPortPlan());
173
+ const httpPort = hostPortPlan.ingressHttp;
169
174
  const discovery = createDiscoveryContext();
170
175
  const baseDomain = determineDefaultDomain();
171
176
  const bareDomain = baseDomain.replace(/^(app|api)\./, "");
@@ -178,7 +183,7 @@ export const networksCommand = new Command("networks")
178
183
  const basePath = s.config.basePath.replace(/^\//, "");
179
184
  const isBackend = s.config?.appType === "backend";
180
185
  const host = isBackend ? apiDomain : appDomain;
181
- const url = `http://${host}/${basePath}`;
186
+ const url = `http://${host}:${httpPort}/${basePath}`;
182
187
  const port = s.config.port;
183
188
  const status = await checkServiceStatus(s.name, port, url);
184
189
  return {
@@ -187,7 +192,7 @@ export const networksCommand = new Command("networks")
187
192
  basePath: s.config.basePath,
188
193
  url,
189
194
  ...(process.platform === "win32"
190
- ? { loopbackUrl: `http://127.0.0.1:80/${basePath}`.replace(/\/$/, "") }
195
+ ? { loopbackUrl: `http://127.0.0.1:${httpPort}/${basePath}`.replace(/\/$/, "") }
191
196
  : {}),
192
197
  port,
193
198
  status,
@@ -222,7 +227,7 @@ export const networksCommand = new Command("networks")
222
227
  }
223
228
  process.exit(0);
224
229
  }
225
- printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}`, DEFAULT_BOX_WIDTH);
230
+ printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}:${httpPort}`, DEFAULT_BOX_WIDTH);
226
231
  // Group services by stack using discovery context's stack names for consistent ordering
227
232
  const stacks = new Map();
228
233
  for (const stackName of discovery.stackNames) {
@@ -0,0 +1,74 @@
1
+ import { statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { Command } from "commander";
4
+ import { findCliAssetRoot, loadTemplate } from "../generator/template-engine.js";
5
+ const REQUIRED_ASSET_PATHS = [
6
+ { path: "Tiltfile", directory: false },
7
+ { path: "engine", directory: true },
8
+ { path: "discovery", directory: true },
9
+ { path: "specs", directory: true },
10
+ { path: "ext", directory: true },
11
+ { path: "cli/templates", directory: true },
12
+ { path: "shared-platform-engineering/docker-templates", directory: true },
13
+ ];
14
+ const REQUIRED_TEMPLATES = [
15
+ ".tiltignore.hbs",
16
+ "TILT_RESOURCE_DEFAULTS.star.hbs",
17
+ "TILT_TECH_STACK.star.hbs",
18
+ "Tiltfile.hbs",
19
+ "spec.master.hbs",
20
+ ];
21
+ export const runtimeCommand = new Command("runtime")
22
+ .description("Inspect the packaged TDK runtime assets")
23
+ .option("--check-assets", "Check bundled engine and template assets")
24
+ .option("--json", "Print the check result as JSON")
25
+ .action((options) => {
26
+ if (!options.checkAssets) {
27
+ runtimeCommand.outputHelp();
28
+ process.exitCode = 2;
29
+ return;
30
+ }
31
+ const candidate = findCliAssetRoot();
32
+ const assetRoot = candidate ? resolve(candidate) : null;
33
+ const missing = assetRoot
34
+ ? REQUIRED_ASSET_PATHS.filter(({ path, directory }) => {
35
+ try {
36
+ const stats = statSync(join(assetRoot, path));
37
+ return directory ? !stats.isDirectory() : !stats.isFile();
38
+ }
39
+ catch {
40
+ return true;
41
+ }
42
+ }).map(({ path }) => path)
43
+ : REQUIRED_ASSET_PATHS.map(({ path }) => path);
44
+ const templateErrors = [];
45
+ if (assetRoot && missing.length === 0) {
46
+ for (const template of REQUIRED_TEMPLATES) {
47
+ try {
48
+ loadTemplate(template);
49
+ }
50
+ catch (error) {
51
+ templateErrors.push(error instanceof Error ? error.message : `Failed to load template ${template}`);
52
+ }
53
+ }
54
+ }
55
+ const ok = Boolean(assetRoot) && missing.length === 0 && templateErrors.length === 0;
56
+ if (options.json) {
57
+ console.log(JSON.stringify({ ok, assetRoot, missing, templateErrors }));
58
+ }
59
+ else if (ok) {
60
+ console.log(`TDK runtime assets found at ${assetRoot}`);
61
+ }
62
+ else {
63
+ console.error([
64
+ assetRoot
65
+ ? `Incomplete TDK runtime assets at ${assetRoot}`
66
+ : "TDK runtime assets not found",
67
+ ...missing.map((path) => `Missing: ${path}`),
68
+ ...templateErrors,
69
+ ].join("\n"));
70
+ }
71
+ if (!ok)
72
+ process.exitCode = 1;
73
+ });
74
+ //# sourceMappingURL=runtime.js.map
@@ -3,9 +3,10 @@ import chalk from "chalk";
3
3
  import { Command } from "commander";
4
4
  import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
5
5
  import { handleDryRun } from "../utils/command-helpers.js";
6
- import { checkHostPorts } from "../utils/doctor-runtime.js";
7
6
  import { errorFactories, handleTiltFailure, requireProjectRoot, runCommand, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
8
7
  import { formatCount } from "../utils/formatting.js";
8
+ import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "../utils/host-port-config.js";
9
+ import { formatHostPortPlan } from "../utils/host-port-plan.js";
9
10
  import { findProjectRoot } from "../utils/paths.js";
10
11
  import { findAvailablePort } from "../utils/port-assignment.js";
11
12
  import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
@@ -71,6 +72,9 @@ export const upCommand = new Command("up")
71
72
  const projectRoot = options.dryRun
72
73
  ? requireProjectRoot()
73
74
  : (findProjectRoot() ?? process.cwd());
75
+ const hostPortPlan = await getHostPortPlan(projectRoot, {
76
+ inspectDocker: !options.dryRun,
77
+ });
74
78
  if (!options.dryRun) {
75
79
  const copiedAssets = ensureProjectRuntimeAssets(projectRoot);
76
80
  if (copiedAssets.length > 0 && options.verbose && !options.quiet) {
@@ -106,7 +110,7 @@ export const upCommand = new Command("up")
106
110
  serviceNames.forEach((name) => {
107
111
  console.log(chalk.gray(` - ${name}`));
108
112
  });
109
- const { appBase, apiBase } = resolveSubdomainBases();
113
+ const { appBase, apiBase } = resolveSubdomainBases(hostPortPlan.ingressHttp);
110
114
  const frontends = servicesToStart.filter((s) => s.config?.appType === "frontend");
111
115
  const backends = servicesToStart.filter((s) => s.config?.appType === "backend");
112
116
  if (frontends.length > 0) {
@@ -132,17 +136,15 @@ export const upCommand = new Command("up")
132
136
  const dryRunCommand = stackName
133
137
  ? `tilt up -- --focus=${stackName} ${focusServiceNames.join(" ")}`
134
138
  : "tilt up";
139
+ if (!options.quiet) {
140
+ console.log(chalk.blue(formatHostPortPlan(hostPortPlan)));
141
+ console.log(chalk.gray("Override with TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT."));
142
+ }
135
143
  if (handleDryRun(options, "not starting services", dryRunCommand)) {
136
144
  return;
137
145
  }
138
- // Warn rather than block: a stale local Postgres on 5432 makes Tilt fail
139
- // halfway through startup with a bind error that's hard to read.
140
- const hostPorts = await checkHostPorts();
141
- if (!hostPorts.didPass && !options.quiet) {
142
- console.log(chalk.yellow(`\n⚠️ ${hostPorts.message}`));
143
- if (hostPorts.fix)
144
- console.log(chalk.gray(` ${hostPorts.fix}\n`));
145
- }
146
+ exportHostPortPlan(hostPortPlan);
147
+ writeSavedHostPortPlan(projectRoot, hostPortPlan);
146
148
  const basePort = 10350;
147
149
  let port = basePort;
148
150
  if (process.env.TILT_PORT) {
@@ -231,7 +231,7 @@ services:
231
231
  POSTGRES_PASSWORD: \${DB_PASSWORD}
232
232
  POSTGRES_DB: postgres
233
233
  ports:
234
- - "5432:5432"
234
+ - "\${TDK_POSTGRES_PORT:-15432}:5432"
235
235
  volumes:
236
236
  - ${name}_postgres_data:/var/lib/postgresql/data
237
237
  networks:
@@ -494,7 +494,7 @@ function findSelfContainedEngineRoot() {
494
494
  }
495
495
  return null;
496
496
  }
497
- function findCliAssetRoot() {
497
+ export function findCliAssetRoot() {
498
498
  let dir = import.meta.dirname || process.cwd();
499
499
  for (let i = 0; i < 8; i++) {
500
500
  if (fs.existsSync(path.join(dir, "shared-platform-engineering", "docker-templates"))) {
@@ -2,8 +2,8 @@ import { execFileSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { checkDockerCompose, checkDockerRuntime, checkDockerVersions, checkTilt, } from "../commands/doctor.js";
5
- import { checkHostPorts, checkIngressPorts } from "./doctor-runtime.js";
6
5
  import { checkNatsBroker } from "./doctor-wiring.js";
6
+ import { getHostPortPlan } from "./host-port-config.js";
7
7
  import { findProjectRoot } from "./paths.js";
8
8
  import { discoverResourcesFromRoot } from "./services.js";
9
9
  import { findOnPath } from "./which.js";
@@ -99,26 +99,20 @@ export async function runColdPreflight(opts = {}) {
99
99
  message: bun ? "Bun 1.2+ is available on PATH" : "Bun 1.2+ not found on PATH",
100
100
  fix: bun ? undefined : "curl -fsSL https://bun.sh/install | bash",
101
101
  });
102
- const [hostPorts, ingressPorts] = await Promise.all([
103
- safeCheck("ports", checkHostPorts, "Could not check required host ports"),
104
- safeCheck("ports", checkIngressPorts, "Could not check ingress ports"),
105
- ]);
106
- if (!hostPorts.ok || !ingressPorts.ok) {
107
- const port5432 = !hostPorts.ok && /(?:^|\n)\s*5432\s*\(/.test(hostPorts.message);
108
- const messages = [
109
- !hostPorts.ok
110
- ? port5432
111
- ? "Port 5432 is taken (usually local Postgres). Stop it or TDK cannot bind the bundled Postgres."
112
- : hostPorts.message
113
- : undefined,
114
- !ingressPorts.ok ? ingressPorts.message : undefined,
115
- ].filter((message) => Boolean(message));
116
- const fixes = [hostPorts.fix, ingressPorts.fix].filter((fix) => Boolean(fix));
102
+ try {
103
+ const plan = await getHostPortPlan(projectRoot ?? cwd);
104
+ items.push({
105
+ id: "ports",
106
+ ok: true,
107
+ message: `HTTP ${plan.ingressHttp}, HTTPS ${plan.ingressHttps}, Postgres ${plan.postgres}`,
108
+ });
109
+ }
110
+ catch (error) {
117
111
  items.push({
118
112
  id: "ports",
119
113
  ok: false,
120
- message: messages.join("\n"),
121
- fix: fixes.join(" "),
114
+ message: error instanceof Error ? error.message : String(error),
115
+ fix: "Set TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT to available host ports.",
122
116
  });
123
117
  }
124
118
  if (projectRoot) {
@@ -21,7 +21,7 @@ export async function collectDoctorChecks(machineChecks, projectChecks) {
21
21
  return { checks, errors };
22
22
  }
23
23
  /** Produce the same readiness decision for human and machine consumers. */
24
- export function createDoctorReport(checks, inProject, errors = []) {
24
+ export function createDoctorReport(checks, inProject, errors = [], portPlan) {
25
25
  return {
26
26
  schemaVersion: 1,
27
27
  data: {
@@ -29,6 +29,30 @@ export function createDoctorReport(checks, inProject, errors = []) {
29
29
  checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
30
30
  inProject,
31
31
  checks,
32
+ ...(portPlan !== undefined
33
+ ? {
34
+ ports: {
35
+ http: {
36
+ requested: portPlan?.requested.ingressHttp ?? 80,
37
+ chosen: portPlan?.ingressHttp ?? null,
38
+ explicit: portPlan?.explicit.ingressHttp ?? Boolean(process.env.TDK_HTTP_PORT),
39
+ reason: portPlan?.reason.ingressHttp ?? "port planning failed",
40
+ },
41
+ https: {
42
+ requested: portPlan?.requested.ingressHttps ?? 443,
43
+ chosen: portPlan?.ingressHttps ?? null,
44
+ explicit: portPlan?.explicit.ingressHttps ?? Boolean(process.env.TDK_HTTPS_PORT),
45
+ reason: portPlan?.reason.ingressHttps ?? "port planning failed",
46
+ },
47
+ postgres: {
48
+ requested: portPlan?.requested.postgres ?? 5432,
49
+ chosen: portPlan?.postgres ?? null,
50
+ explicit: portPlan?.explicit.postgres ?? Boolean(process.env.TDK_POSTGRES_PORT),
51
+ reason: portPlan?.reason.postgres ?? "port planning failed",
52
+ },
53
+ },
54
+ }
55
+ : {}),
32
56
  },
33
57
  errors,
34
58
  };
@@ -305,7 +305,7 @@ export function checkPrivateNpmRegistry(exec = execSync, projectRoot = findProje
305
305
  * This is the exact failure mode that leaves apps never scheduled while doctor
306
306
  * previously reported "Environment ready".
307
307
  */
308
- export function checkIngressPorts(exec = execSync, projectName = getProjectName()) {
308
+ export function checkIngressPorts(exec = execSync, projectName = getProjectName(), ports = INGRESS_PORTS) {
309
309
  const projectPrefix = toComposeProjectPrefix(projectName);
310
310
  let dockerPs = "";
311
311
  try {
@@ -323,7 +323,7 @@ export function checkIngressPorts(exec = execSync, projectName = getProjectName(
323
323
  message: "Docker not available - skipped ingress port check",
324
324
  };
325
325
  }
326
- const holders = parsePublishedPortHolders(dockerPs, INGRESS_PORTS);
326
+ const holders = parsePublishedPortHolders(dockerPs, ports);
327
327
  const foreign = findForeignIngressHolders(holders, projectPrefix);
328
328
  if (foreign.length === 0) {
329
329
  const own = holders.filter((holder) => isOwnIngressContainer(holder.name, projectPrefix));
@@ -331,7 +331,7 @@ export function checkIngressPorts(exec = execSync, projectName = getProjectName(
331
331
  return {
332
332
  name: "Ingress Ports",
333
333
  didPass: true,
334
- message: `Ingress ports ${INGRESS_PORTS.join("/")} held by this project's Traefik (${own
334
+ message: `Ingress ports ${ports.join("/")} held by this project's Traefik (${own
335
335
  .map((h) => h.name)
336
336
  .join(", ")})`,
337
337
  };
@@ -339,7 +339,7 @@ export function checkIngressPorts(exec = execSync, projectName = getProjectName(
339
339
  return {
340
340
  name: "Ingress Ports",
341
341
  didPass: true,
342
- message: `Ingress ports ${INGRESS_PORTS.join("/")} are free for Traefik`,
342
+ message: `Ingress ports ${ports.join("/")} are free for Traefik`,
343
343
  };
344
344
  }
345
345
  const details = foreign
@@ -0,0 +1,88 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHostPortPlan, isHostPortAvailable } from "./host-port-plan.js";
5
+ const CONFIG_PATH = join(".tdk", ".tdk-out", "host-ports.json");
6
+ export function isDockerPortOwnedByProject(dockerPs, projectPrefix, port) {
7
+ return dockerPs.split("\n").some((line) => {
8
+ const [name = "", ports = ""] = line.split("|");
9
+ const containerName = name.toLowerCase();
10
+ const prefix = projectPrefix.toLowerCase();
11
+ return ((containerName.startsWith(`${prefix}_`) || containerName.startsWith(`${prefix}-`)) &&
12
+ new RegExp(`:${port}->`).test(ports));
13
+ });
14
+ }
15
+ export function readSavedHostPortPlan(projectRoot) {
16
+ const path = join(projectRoot, CONFIG_PATH);
17
+ if (!existsSync(path))
18
+ return null;
19
+ try {
20
+ const value = JSON.parse(readFileSync(path, "utf-8"));
21
+ if (!value || typeof value !== "object")
22
+ return null;
23
+ const plan = value;
24
+ if (!Number.isInteger(plan.ingressHttp) ||
25
+ !Number.isInteger(plan.ingressHttps) ||
26
+ !Number.isInteger(plan.postgres) ||
27
+ !plan.requested ||
28
+ !plan.explicit ||
29
+ !plan.reason)
30
+ return null;
31
+ return plan;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function writeSavedHostPortPlan(projectRoot, plan) {
38
+ const path = join(projectRoot, CONFIG_PATH);
39
+ mkdirSync(join(projectRoot, ".tdk", ".tdk-out"), { recursive: true });
40
+ writeFileSync(path, `${JSON.stringify(plan, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
41
+ }
42
+ export function exportHostPortPlan(plan) {
43
+ process.env.TDK_HTTP_PORT = String(plan.ingressHttp);
44
+ process.env.TDK_HTTPS_PORT = String(plan.ingressHttps);
45
+ process.env.TDK_POSTGRES_PORT = String(plan.postgres);
46
+ }
47
+ /** Reuse this project's last selection when its ports are still free or held by its own containers. */
48
+ export async function getHostPortPlan(projectRoot, options = {}) {
49
+ const saved = readSavedHostPortPlan(projectRoot);
50
+ const hasOverride = ["TDK_HTTP_PORT", "TDK_HTTPS_PORT", "TDK_POSTGRES_PORT"].some((key) => process.env[key]);
51
+ // Dry-run must not call Docker. A saved plan is the best representation of
52
+ // the current landscape in that mode; explicit overrides are still planned.
53
+ if (options.inspectDocker === false && saved && !hasOverride)
54
+ return saved;
55
+ let prefix = "";
56
+ try {
57
+ const project = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
58
+ prefix = String(project?.project?.name ?? "tdk-project")
59
+ .replace(/-/g, "_")
60
+ .toLowerCase();
61
+ }
62
+ catch {
63
+ prefix = "tdk_project";
64
+ }
65
+ let dockerPs = "";
66
+ if (options.inspectDocker !== false) {
67
+ try {
68
+ dockerPs = execFileSync("docker", ["ps", "--format", "{{.Names}}|{{.Ports}}"], {
69
+ encoding: "utf-8",
70
+ timeout: 5000,
71
+ });
72
+ }
73
+ catch {
74
+ // If Docker is down, reuse only when host binds are still available.
75
+ }
76
+ }
77
+ const ownedByProject = (port) => isDockerPortOwnedByProject(dockerPs, prefix, port);
78
+ const isAvailable = async (port) => (await isHostPortAvailable(port)) || ownedByProject(port);
79
+ if (saved && !hasOverride) {
80
+ for (const port of [saved.ingressHttp, saved.ingressHttps, saved.postgres]) {
81
+ if (!(await isAvailable(port)))
82
+ return createHostPortPlan({ isAvailable });
83
+ }
84
+ return saved;
85
+ }
86
+ return createHostPortPlan({ isAvailable });
87
+ }
88
+ //# sourceMappingURL=host-port-config.js.map
@@ -1,5 +1,5 @@
1
1
  import { createServer } from "node:net";
2
- const DEFAULT_RANGES = {
2
+ export const DEFAULT_HOST_PORT_RANGES = {
3
3
  ingressHttp: { start: 8080, end: 8180 },
4
4
  ingressHttps: { start: 8443, end: 8543 },
5
5
  postgres: { start: 15432, end: 15532 },
@@ -9,7 +9,8 @@ const ENV_KEYS = {
9
9
  ingressHttps: "TDK_HTTPS_PORT",
10
10
  postgres: "TDK_POSTGRES_PORT",
11
11
  };
12
- async function isHostPortAvailable(port) {
12
+ const REQUESTED_PORTS = { ingressHttp: 80, ingressHttps: 443, postgres: 5432 };
13
+ export async function isHostPortAvailable(port) {
13
14
  return new Promise((resolve) => {
14
15
  const server = createServer();
15
16
  server.once("error", () => resolve(false));
@@ -29,12 +30,13 @@ export async function createHostPortPlan(options = {}) {
29
30
  const env = options.env ?? process.env;
30
31
  const isAvailable = options.isAvailable ?? isHostPortAvailable;
31
32
  const explicitFlags = { ingressHttp: false, ingressHttps: false, postgres: false };
33
+ const reasons = { ingressHttp: "", ingressHttps: "", postgres: "" };
32
34
  const selected = { explicit: explicitFlags };
33
35
  const reserved = new Set();
34
36
  for (const key of ["ingressHttp", "ingressHttps", "postgres"]) {
35
37
  const envValue = env[ENV_KEYS[key]];
36
38
  const explicit = envValue !== undefined && envValue.trim() !== "";
37
- const range = options.ranges?.[key] ?? DEFAULT_RANGES[key];
39
+ const range = options.ranges?.[key] ?? DEFAULT_HOST_PORT_RANGES[key];
38
40
  const candidates = explicit
39
41
  ? [parsePort(ENV_KEYS[key], envValue)]
40
42
  : Array.from({ length: range.end - range.start + 1 }, (_, index) => range.start + index);
@@ -56,7 +58,17 @@ export async function createHostPortPlan(options = {}) {
56
58
  reserved.add(chosen);
57
59
  selected[key] = chosen;
58
60
  explicitFlags[key] = explicit;
61
+ reasons[key] = explicit
62
+ ? "explicit override"
63
+ : `selected from fallback range ${range.start}-${range.end}`;
59
64
  }
60
- return selected;
65
+ return {
66
+ ...selected,
67
+ requested: { ...REQUESTED_PORTS },
68
+ reason: reasons,
69
+ };
70
+ }
71
+ export function formatHostPortPlan(plan) {
72
+ return `Host ports: HTTP ${plan.ingressHttp}, HTTPS ${plan.ingressHttps}, Postgres ${plan.postgres}`;
61
73
  }
62
74
  //# sourceMappingURL=host-port-plan.js.map
@@ -15,22 +15,24 @@ export function getProjectName() {
15
15
  }
16
16
  return "tdk-project";
17
17
  }
18
- export function resolveSubdomainBases() {
18
+ export function resolveSubdomainBases(ingressPort) {
19
19
  const projectName = getProjectName();
20
- const raw = process.env.TDK_SERVICE_BASE_URL ?? `http://${projectName}.localhost`;
20
+ const raw = process.env.TDK_SERVICE_BASE_URL ??
21
+ `http://${projectName}.localhost${ingressPort ? `:${ingressPort}` : ""}`;
21
22
  try {
22
23
  const u = new URL(raw.includes("://") ? raw : `http://${raw}`);
23
24
  const host = u.hostname;
25
+ const port = u.port || (ingressPort ? String(ingressPort) : "");
24
26
  if (host === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(host)) {
25
27
  return {
26
- appBase: `${u.protocol}//${host}${u.port ? `:${u.port}` : ""}`,
27
- apiBase: `${u.protocol}//${host}${u.port ? `:${u.port}` : ""}`,
28
+ appBase: `${u.protocol}//${host}${port ? `:${port}` : ""}`,
29
+ apiBase: `${u.protocol}//${host}${port ? `:${port}` : ""}`,
28
30
  };
29
31
  }
30
32
  const bare = host.replace(/^(app|api)\./, "");
31
33
  return {
32
- appBase: `${u.protocol}//app.${bare}${u.port ? `:${u.port}` : ""}`,
33
- apiBase: `${u.protocol}//api.${bare}${u.port ? `:${u.port}` : ""}`,
34
+ appBase: `${u.protocol}//app.${bare}${port ? `:${port}` : ""}`,
35
+ apiBase: `${u.protocol}//api.${bare}${port ? `:${port}` : ""}`,
34
36
  };
35
37
  }
36
38
  catch {
@@ -56,8 +58,8 @@ export function resolveServicePath(resource) {
56
58
  * Workers, libraries, SDKs and migrators have no Traefik route, so they are
57
59
  * skipped rather than reported as unreachable.
58
60
  */
59
- export function buildHealthTargets(resources) {
60
- const { appBase, apiBase } = resolveSubdomainBases();
61
+ export function buildHealthTargets(resources, ingressPort) {
62
+ const { appBase, apiBase } = resolveSubdomainBases(ingressPort);
61
63
  return resources
62
64
  .filter((resource) => resource.config?.appType === "frontend" || resource.config?.appType === "backend")
63
65
  .map((resource) => {
package/cli/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.80",
5
- "description": "TDK CLI - Tilt Development Kit command line interface",
4
+ "version": "1.3.82",
5
+ "description": "TDK CLI — start services on your laptop. Not a deploy or Compose file.",
6
6
  "keywords": [
7
7
  "tdk",
8
8
  "tilt",
@@ -10,7 +10,7 @@ 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(sablier_enabled = False):
13
+ def generate_standalone_traefik_compose(sablier_enabled = False, http_host_port = "8080", https_host_port = "8443"):
14
14
  """Minimal Traefik that routes Docker labels on api.{project}.localhost.
15
15
 
16
16
  The Sablier plugin, the sablier container and the wake gateway are only
@@ -48,8 +48,8 @@ services:
48
48
  - "--entrypoints.websecure.address=:443"
49
49
  - "--log.level=INFO"
50
50
  {sablier_flags} ports:
51
- - "80:80"
52
- - "8080:8080"
51
+ - "{http_host_port}:80"
52
+ - "{https_host_port}:443"
53
53
  volumes:
54
54
  - /var/run/docker.sock:/var/run/docker.sock:ro
55
55
  - ./{dynamic_dir_name}:/etc/traefik/dynamic:ro
@@ -65,6 +65,8 @@ services:
65
65
  """.format(
66
66
  name=name,
67
67
  network=network,
68
+ http_host_port=http_host_port,
69
+ https_host_port=https_host_port,
68
70
  healthcheck_timing=compose_healthcheck_timing(10),
69
71
  traefik_depends_on=_SABLIER_DEPENDS_ON if sablier_enabled else "",
70
72
  sablier_flags=_SABLIER_FLAGS if sablier_enabled else "",
@@ -70,7 +70,7 @@ services:
70
70
  POSTGRES_PASSWORD: ${{DB_PASSWORD}}
71
71
  POSTGRES_DB: postgres
72
72
  ports:
73
- - "5432:5432"
73
+ - "{host_port}:5432"
74
74
  volumes:
75
75
  - {name}_postgres_data:/var/lib/postgresql/data
76
76
  networks:
@@ -88,7 +88,7 @@ networks:
88
88
 
89
89
  volumes:
90
90
  {name}_postgres_data:
91
- """.format(name=name, database_network=database_network)
91
+ """.format(name=name, database_network=database_network, host_port=os.environ.get('TDK_POSTGRES_PORT', '15432'))
92
92
 
93
93
 
94
94
  def _ensure_database_management_compose(root_prefix, write_fn):
@@ -174,43 +174,18 @@ def _load_infisical(should_enable, root_prefix="", env_file=None):
174
174
  # 🌐 PROXY (Traefik)
175
175
  # =============================================================================
176
176
 
177
- def _stop_conflicting_traefik():
178
- """Auto-stop another TDK project's standalone Traefik if it already holds
179
- host port 80/8080, so `tdk up` on a new project always wins over a stale
180
- Traefik left running from a previous one.
181
-
182
- Each standalone project gets its own container name and Docker network
183
- (see PlatformDockerConstants.PROJECT_NAME), but the host ports are hardcoded
184
- to 80/8080 so `api.{project}.localhost` works without a port suffix. That
185
- means only one standalone project's Traefik can run at a time. Without this,
186
- starting a second project while another's Traefik is still running fails
187
- deep inside `docker compose up` with a cryptic "port is already allocated"
188
- networking error instead of just taking over the port.
189
- """
190
- own_container = PlatformDockerConstants.PROJECT_NAME + "_traefik"
191
- result = str(local(
192
- "docker ps --filter 'publish=80' --filter 'publish=8080' --format '{{.Names}}' 2>/dev/null || true",
193
- quiet=True, echo_off=True,
194
- )).strip()
195
- for container in result.split("\n"):
196
- container = container.strip()
197
- if container and container != own_container:
198
- print("🛑 Stopping Traefik from another TDK project ('{}') — it was holding host port 80/8080".format(container))
199
- local(
200
- "docker stop '{}' >/dev/null 2>&1 || true".format(container),
201
- quiet=True, echo_off=True,
202
- )
203
-
204
-
205
177
  def _load_standalone_traefik(root_prefix, env_file, write_fn):
206
178
  """Generate and load a self-contained Traefik compose for standalone projects."""
207
- _stop_conflicting_traefik()
208
179
  compose_rel = ".tdk/.tdk-out/docker-compose.traefik.yml"
209
180
  # The free-tier stub always reports "disabled"; the licensed module reports
210
181
  # enabled for a manifest that opts in. Probing it keeps this file free of any
211
182
  # license logic while starting the sablier containers only when they can work.
212
183
  _, sablier_enabled = sablier_middleware_suffix({"sablier": {"enable": True}}, "probe")
213
- content = generate_standalone_traefik_compose(sablier_enabled)
184
+ content = generate_standalone_traefik_compose(
185
+ sablier_enabled,
186
+ os.environ.get('TDK_HTTP_PORT', '8080'),
187
+ os.environ.get('TDK_HTTPS_PORT', '8443'),
188
+ )
214
189
  if write_fn:
215
190
  write_fn(compose_rel, content)
216
191
  compose_file = root_prefix + compose_rel if root_prefix else compose_rel
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.80",
4
- "description": "TDK runs many services on your laptop with Docker + Tilt. Helm still deploys the cluster.",
3
+ "version": "1.3.82",
4
+ "description": "TDK CLI — start services on your laptop. Not a deploy or Compose file.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {
7
7
  "type": "git",