@tdk-landscape/tdk-cli-core 1.3.80 → 1.3.81
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 +2 -0
- package/README.md +20 -1
- package/cli/dist/cli.js +2 -0
- package/cli/dist/commands/completion.js +15 -2
- package/cli/dist/commands/doctor.js +48 -9
- package/cli/dist/commands/help.js +5 -0
- package/cli/dist/commands/networks.js +8 -3
- package/cli/dist/commands/runtime.js +74 -0
- package/cli/dist/commands/up.js +12 -10
- package/cli/dist/generator/template-engine.js +2 -2
- package/cli/dist/utils/cold-preflight.js +12 -18
- package/cli/dist/utils/doctor-report.js +25 -1
- package/cli/dist/utils/doctor-runtime.js +4 -4
- package/cli/dist/utils/host-port-config.js +88 -0
- package/cli/dist/utils/host-port-plan.js +16 -4
- package/cli/dist/utils/service-urls.js +10 -8
- package/cli/package.json +1 -1
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +5 -3
- package/engine/topologies/tilt/resources/infra-loader.star +7 -32
- package/package.json +1 -1
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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
TDK runs many services on your laptop with Docker + Tilt. Helm still deploys the cluster.
|
|
4
4
|
|
|
5
|
+
[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)
|
|
6
|
+
|
|
5
7
|
[](https://www.npmjs.com/package/@tdk-landscape/tdk-cli-core)
|
|
6
8
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
|
|
7
9
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
|
|
@@ -33,6 +35,21 @@ tdk networks
|
|
|
33
35
|
|
|
34
36
|
`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
37
|
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
*Scaffolding a backend and frontend, then listing the stack.*
|
|
41
|
+
|
|
42
|
+
## Why TDK?
|
|
43
|
+
|
|
44
|
+
| | Compose | Local Kubernetes (kind, minikube) | Plain Tilt | **TDK** |
|
|
45
|
+
|---|---|---|---|---|
|
|
46
|
+
| Scaffold a service in one command | ❌ | ❌ | ❌ | ✅ `tdk resource` |
|
|
47
|
+
| Hot reload on file change | ⚠️ `compose watch` config | ⚠️ extra tooling | ✅ | ✅ |
|
|
48
|
+
| Health-checked startup order | ✅ | ✅ | ⚠️ configure it yourself | ✅ |
|
|
49
|
+
| Needs a cluster | No | Yes | Optional | **No** |
|
|
50
|
+
|
|
51
|
+
100 generated health-check services reached healthy in 112 s, using about 1.6 GiB in service-container memory on a 16 GB machine. [Details and caveats](docs/scale-bench.md).
|
|
52
|
+
|
|
36
53
|
## When not to use TDK
|
|
37
54
|
|
|
38
55
|
- Your existing Compose or Tilt workflow already gives you a working local environment.
|
|
@@ -52,7 +69,9 @@ tdk networks
|
|
|
52
69
|
|
|
53
70
|
## Requirements and support
|
|
54
71
|
|
|
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.
|
|
72
|
+
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).
|
|
73
|
+
|
|
74
|
+
**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
75
|
|
|
57
76
|
## Contributing and license
|
|
58
77
|
|
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,
|
|
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
|
-
() =>
|
|
1021
|
-
|
|
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(
|
|
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
|
|
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
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
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
|
-
- "
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
121
|
-
fix:
|
|
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,
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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] ??
|
|
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
|
|
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 ??
|
|
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}${
|
|
27
|
-
apiBase: `${u.protocol}//${host}${
|
|
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}${
|
|
33
|
-
apiBase: `${u.protocol}//api.${bare}${
|
|
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
|
@@ -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
|
-
- "
|
|
52
|
-
- "
|
|
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
|
-
- "
|
|
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(
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdk-landscape/tdk-cli-core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.81",
|
|
4
4
|
"description": "TDK runs many services on your laptop with Docker + Tilt. Helm still deploys the cluster.",
|
|
5
5
|
"homepage": "https://tdk-landscape.github.io/tdk-website",
|
|
6
6
|
"repository": {
|