@tdk-landscape/tdk-cli-core 1.3.75 → 1.3.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +59 -13
  3. package/cli/bin/tdk.js +22 -2
  4. package/cli/dist/cli.js +9 -2
  5. package/cli/dist/commands/completion.js +30 -3
  6. package/cli/dist/commands/config.js +59 -1
  7. package/cli/dist/commands/doctor.js +295 -98
  8. package/cli/dist/commands/eject.js +77 -0
  9. package/cli/dist/commands/networks.js +17 -4
  10. package/cli/dist/commands/project.js +28 -5
  11. package/cli/dist/commands/resource.js +118 -10
  12. package/cli/dist/commands/up.js +93 -21
  13. package/cli/dist/commands/upgrade.js +163 -68
  14. package/cli/dist/generator/template-engine.js +77 -13
  15. package/cli/dist/types/index.js +6 -1
  16. package/cli/dist/utils/cold-preflight.js +198 -0
  17. package/cli/dist/utils/constants.js +4 -0
  18. package/cli/dist/utils/docker.js +15 -1
  19. package/cli/dist/utils/doctor-runtime.js +48 -16
  20. package/cli/dist/utils/doctor-wiring.js +269 -0
  21. package/cli/dist/utils/errors.js +5 -5
  22. package/cli/dist/utils/exec-async.js +30 -0
  23. package/cli/dist/utils/generated-paths.js +49 -0
  24. package/cli/dist/utils/platform.js +53 -0
  25. package/cli/dist/utils/port-assignment.js +13 -20
  26. package/cli/dist/utils/project-templates.js +7 -2
  27. package/cli/dist/utils/resource-features.js +6 -2
  28. package/cli/dist/utils/service-manifest.js +73 -0
  29. package/cli/dist/utils/services.js +8 -1
  30. package/cli/dist/utils/tar.js +27 -5
  31. package/cli/dist/utils/tilt-process.js +53 -0
  32. package/cli/dist/utils/tilt.js +3 -1
  33. package/cli/dist/utils/which.js +26 -0
  34. package/cli/dist/utils/windows-doctor.js +175 -0
  35. package/cli/package.json +1 -1
  36. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
  37. package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
  38. package/cli/templates/Tiltfile.hbs +24 -4
  39. package/cli/templates/spec.master.hbs +1 -2
  40. package/discovery/AGENTS.md +1 -1
  41. package/discovery/constants.star +2 -2
  42. package/discovery/discovery_orchestrator.star +2 -2
  43. package/discovery/manifest/constants.star +1 -1
  44. package/discovery/manifest/loading.star +7 -3
  45. package/discovery/registry.star +17 -3
  46. package/engine/README.md +2 -0
  47. package/engine/schemas/service-schema.json +47 -21
  48. package/engine/scripts/generate-manifest.sh +2 -1
  49. package/engine/topologies/platform/docker/compose/compose.star +37 -5
  50. package/engine/topologies/tilt/common/utils.star +54 -11
  51. package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
  52. package/engine/topologies/tilt/manifest/constants.star +3 -0
  53. package/engine/topologies/tilt/manifest/schema.star +2 -4
  54. package/engine/topologies/tilt/manifest/validator.star +1 -1
  55. package/engine/topologies/tilt/resources/infra-loader.star +14 -3
  56. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
  57. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
  58. package/examples/tdk-example/LICENSE +21 -0
  59. package/examples/tdk-example/README.md +23 -0
  60. package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
  61. package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
  62. package/examples/tdk-example/Tiltfile +25 -0
  63. package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
  64. package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
  65. package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
  66. package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
  67. package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
  68. package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
  69. package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
  70. package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
  71. package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
  72. package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
  73. package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
  74. package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
  75. package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
  76. package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
  77. package/package.json +2 -1
@@ -1,28 +1,97 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync, execSync } from "node:child_process";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, normalize, relative } from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { Command } from "commander";
6
6
  import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
7
- import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
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 { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
10
+ import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
10
11
  import { validateEnvFile } from "../utils/env-validator.js";
12
+ import { execAsync, isExecTimeout } from "../utils/exec-async.js";
11
13
  import { formatCount } from "../utils/formatting.js";
12
14
  import { findProjectRoot } from "../utils/paths.js";
13
15
  import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
14
16
  import { discoverResourcesFromRoot } from "../utils/services.js";
17
+ import { findOnPath } from "../utils/which.js";
15
18
  export { checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, summarizeTiltBuildError, } from "../utils/doctor-runtime.js";
16
19
  // A wedged Docker daemon makes `docker ps` block forever instead of failing,
17
20
  // and doctor is exactly the tool people run when their environment is broken.
18
21
  const EXEC_TIMEOUT_MS = 10_000;
22
+ export const DOCTOR_FIXES = {
23
+ dockerMissing: "See https://docs.docker.com/get-docker/",
24
+ dockerDaemonDown: "Start Docker Desktop, OrbStack, or Colima, then retry: tdk doctor",
25
+ tiltMissing: "curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash",
26
+ bunMissing: "curl -fsSL https://bun.sh/install | bash",
27
+ port80: "Stop the process bound to port 80, or stop local nginx/caddy. Then: tdk doctor",
28
+ port5432: "Stop local Postgres or change the host port. Then: tdk doctor",
29
+ notProject: "tdk project --yes",
30
+ };
31
+ export const WSL2_DOCTOR_MESSAGE = "WSL2 detected. Use Docker Desktop WSL integration. Guide: docs/wsl2.md";
32
+ export const NATIVE_WINDOWS_DOCTOR_MESSAGE = "Native Windows landscape boot is unsupported. Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md";
33
+ export const MIN_TILT_VERSION = [0, 25, 0];
34
+ export const MIN_BUN_VERSION = [1, 2, 0];
35
+ export function versionMeetsMinimum(raw, minimum) {
36
+ const version = parseVersion(raw);
37
+ if (!version)
38
+ return false;
39
+ return isAtLeast(version, minimum);
40
+ }
41
+ export function checkWslProjectLocation(projectPath, strict, isWsl = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME)) {
42
+ if (!isWsl) {
43
+ return {
44
+ name: "WSL project location",
45
+ didPass: true,
46
+ isSkipped: true,
47
+ message: "Not running in WSL2 - skipped project filesystem check",
48
+ };
49
+ }
50
+ const normalized = projectPath.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
51
+ if (normalized !== "/mnt/c" && !normalized.startsWith("/mnt/c/")) {
52
+ return {
53
+ name: "WSL project location",
54
+ didPass: true,
55
+ message: `${WSL2_DOCTOR_MESSAGE} Project is on the WSL filesystem.`,
56
+ };
57
+ }
58
+ const message = `${WSL2_DOCTOR_MESSAGE} Project is under /mnt/c (${projectPath}); hot reload may be broken. Move it into the WSL filesystem, such as ~/projects.`;
59
+ return {
60
+ name: "WSL project location",
61
+ didPass: !strict,
62
+ isWarning: !strict,
63
+ message,
64
+ fix: "Move the repository under your WSL home directory (for example, ~/projects) and retry tdk doctor.",
65
+ };
66
+ }
67
+ /** Failures are shown before passing statuses, with a 5432 conflict first. */
68
+ export function orderDoctorResults(results) {
69
+ const failures = results.filter((result) => !result.didPass && !result.isSkipped);
70
+ const remaining = results.filter((result) => result.didPass || result.isSkipped);
71
+ failures.sort((left, right) => {
72
+ const rank = (result) => result.name === "Host Ports" && /\b5432\b/.test(result.message)
73
+ ? 0
74
+ : result.name === "Host Ports"
75
+ ? 1
76
+ : result.name === "Ingress Ports"
77
+ ? 2
78
+ : 3;
79
+ return rank(left) - rank(right);
80
+ });
81
+ return [...failures, ...remaining];
82
+ }
83
+ export function getDoctorOutcomeMessage(inProject, allPassed) {
84
+ if (!allPassed)
85
+ return "Doctor failed. Fix the items above, then run: tdk doctor";
86
+ return inProject ? "Doctor passed. Next: tdk up" : "Doctor passed. Next: tdk project example";
87
+ }
19
88
  function isTimeout(err) {
20
- return err?.code === "ETIMEDOUT";
89
+ return isExecTimeout(err);
21
90
  }
22
91
  function createExecCheck(name, command, successMessage, failureMessage, fixInstructions) {
23
- return () => {
92
+ return async () => {
24
93
  try {
25
- execSync(command, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
94
+ await execAsync(command, EXEC_TIMEOUT_MS);
26
95
  return {
27
96
  name,
28
97
  didPass: true,
@@ -40,10 +109,37 @@ function createExecCheck(name, command, successMessage, failureMessage, fixInstr
40
109
  }
41
110
  };
42
111
  }
43
- function checkDockerRuntime() {
44
- // Check for Docker
112
+ async function succeeds(command) {
113
+ try {
114
+ await execAsync(command, EXEC_TIMEOUT_MS);
115
+ return true;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ export async function checkDockerRuntime() {
122
+ if (process.platform === "win32") {
123
+ try {
124
+ await execAsync("docker ps", EXEC_TIMEOUT_MS);
125
+ return {
126
+ name: "Container Runtime",
127
+ didPass: true,
128
+ message: "Docker Desktop Linux container engine is running",
129
+ };
130
+ }
131
+ catch {
132
+ return {
133
+ name: "Container Runtime",
134
+ didPass: false,
135
+ message: "Docker is not running",
136
+ fix: DOCTOR_FIXES.dockerDaemonDown,
137
+ };
138
+ }
139
+ }
140
+ const hasDockerCli = Boolean(findOnPath("docker"));
45
141
  try {
46
- execSync("docker ps", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
142
+ await execAsync("docker ps", EXEC_TIMEOUT_MS);
47
143
  return {
48
144
  name: "Container Runtime",
49
145
  didPass: true,
@@ -55,55 +151,50 @@ function checkDockerRuntime() {
55
151
  return {
56
152
  name: "Container Runtime",
57
153
  didPass: false,
58
- message: `Docker daemon is not responding (\`docker ps\` hung for ${EXEC_TIMEOUT_MS / 1000}s)`,
59
- fix: "Restart the runtime: quit and reopen Docker Desktop, or run `colima restart`",
60
- };
61
- }
62
- // Docker not running, check for Colima
63
- try {
64
- execSync("colima status", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
65
- // Colima is running
66
- return {
67
- name: "Container Runtime",
68
- didPass: true,
69
- message: "Colima (Docker runtime) is running",
154
+ message: "Docker daemon is not responding",
155
+ fix: DOCTOR_FIXES.dockerDaemonDown,
70
156
  };
71
157
  }
72
- catch {
73
- // Check if Colima is installed but not running
74
- try {
75
- execSync("which colima", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
76
- return {
77
- name: "Container Runtime",
78
- didPass: false,
79
- message: "Colima is installed but not running",
80
- fix: "Start Colima: colima start",
81
- };
82
- }
83
- catch {
84
- // Check for Podman
85
- try {
86
- execSync("podman ps", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
87
- return {
88
- name: "Container Runtime",
89
- didPass: true,
90
- message: "Podman is running",
91
- };
92
- }
93
- catch {
94
- // No container runtime found
95
- return {
96
- name: "Container Runtime",
97
- didPass: false,
98
- message: "No container runtime (Docker/Colima/Podman) is running",
99
- fix: `Start: colima start (recommended) OR open -a Docker (macOS) OR sudo systemctl start docker (Linux). Setup guide: ${QUICKSTART_DOCS_URL}`,
100
- };
101
- }
102
- }
103
- }
104
158
  }
159
+ // Docker not running: check for Colima, then Podman
160
+ if (await succeeds("colima status")) {
161
+ return {
162
+ name: "Container Runtime",
163
+ didPass: true,
164
+ message: "Colima (Docker runtime) is running",
165
+ };
166
+ }
167
+ if (findOnPath("colima")) {
168
+ return {
169
+ name: "Container Runtime",
170
+ didPass: false,
171
+ message: "Colima is installed but not running",
172
+ fix: DOCTOR_FIXES.dockerDaemonDown,
173
+ };
174
+ }
175
+ if (await succeeds("podman ps")) {
176
+ return {
177
+ name: "Container Runtime",
178
+ didPass: true,
179
+ message: "Podman is running",
180
+ };
181
+ }
182
+ if (hasDockerCli) {
183
+ return {
184
+ name: "Container Runtime",
185
+ didPass: false,
186
+ message: "Docker daemon is not running",
187
+ fix: DOCTOR_FIXES.dockerDaemonDown,
188
+ };
189
+ }
190
+ return {
191
+ name: "Container Runtime",
192
+ didPass: false,
193
+ message: "Docker is not running",
194
+ fix: DOCTOR_FIXES.dockerMissing,
195
+ };
105
196
  }
106
- const checkDockerCompose = createExecCheck("Docker Compose", "docker compose version", "Docker Compose plugin available", "Docker Compose plugin not found", "Install Docker Compose: https://docs.docker.com/compose/install/");
197
+ export const checkDockerCompose = createExecCheck("Docker Compose", "docker compose version", "Docker Compose plugin available", "Docker Compose plugin not found", "Install Docker Compose: https://docs.docker.com/compose/install/");
107
198
  // Generated healthchecks use `start_interval`, which older engines/compose reject.
108
199
  const MIN_DOCKER_ENGINE_VERSION = [25, 0, 0];
109
200
  const MIN_DOCKER_COMPOSE_VERSION = [2, 20, 2];
@@ -123,13 +214,15 @@ function isAtLeast(version, minimum) {
123
214
  }
124
215
  return true;
125
216
  }
126
- export function checkDockerVersions(exec = execSync) {
127
- const run = (command) => String(exec(command, { stdio: "pipe", encoding: "utf-8", timeout: EXEC_TIMEOUT_MS })).trim();
217
+ export async function checkDockerVersions(exec = execAsync) {
218
+ const run = async (command) => String(await exec(command, EXEC_TIMEOUT_MS)).trim();
128
219
  let engineRaw;
129
220
  let composeRaw;
130
221
  try {
131
- engineRaw = run("docker version --format '{{.Server.Version}}'");
132
- composeRaw = run("docker compose version --short");
222
+ [engineRaw, composeRaw] = await Promise.all([
223
+ run("docker version --format '{{.Server.Version}}'"),
224
+ run("docker compose version --short"),
225
+ ]);
133
226
  }
134
227
  catch {
135
228
  return {
@@ -170,7 +263,101 @@ export function checkDockerVersions(exec = execSync) {
170
263
  fix: "Update Docker Desktop, or on Linux update docker-ce and the docker-compose-plugin package",
171
264
  };
172
265
  }
173
- const checkTilt = createExecCheck("Tilt CLI", "tilt version", "Tilt CLI installed", "Tilt CLI not found", `Install Tilt: brew install tilt (macOS) or see https://docs.tilt.dev/install.html. Setup guide: ${QUICKSTART_DOCS_URL}`);
266
+ export async function checkTilt(exec = execAsync) {
267
+ let raw;
268
+ try {
269
+ raw = String(await exec("tilt version", EXEC_TIMEOUT_MS)).trim();
270
+ }
271
+ catch {
272
+ return {
273
+ name: "Tilt CLI",
274
+ didPass: false,
275
+ message: "Tilt CLI not found",
276
+ fix: DOCTOR_FIXES.tiltMissing,
277
+ };
278
+ }
279
+ if (!versionMeetsMinimum(raw, MIN_TILT_VERSION)) {
280
+ return {
281
+ name: "Tilt CLI",
282
+ didPass: false,
283
+ message: `Tilt ${raw || "version unknown"} is below the required v0.25.0 floor`,
284
+ fix: "Install Tilt v0.25.0 or newer: https://docs.tilt.dev/install.html",
285
+ };
286
+ }
287
+ return { name: "Tilt CLI", didPass: true, message: `Tilt ${raw} is available` };
288
+ }
289
+ export async function checkDockerOperatingSystem(exec = execAsync) {
290
+ let osType;
291
+ try {
292
+ osType = String(await exec("docker info --format '{{.OSType}}'", EXEC_TIMEOUT_MS))
293
+ .trim()
294
+ .toLowerCase();
295
+ }
296
+ catch {
297
+ return {
298
+ name: "Docker OS",
299
+ didPass: true,
300
+ isSkipped: true,
301
+ message: "Docker OS unavailable - daemon failure reported separately",
302
+ };
303
+ }
304
+ if (osType !== "linux") {
305
+ return {
306
+ name: "Docker OS",
307
+ didPass: false,
308
+ message: `Docker is using the ${osType || "unknown"} engine; TDK requires Linux containers`,
309
+ fix: "Switch Docker Desktop to Linux containers, enable WSL2 integration, then retry tdk doctor.",
310
+ };
311
+ }
312
+ return { name: "Docker OS", didPass: true, message: "Docker is using Linux containers" };
313
+ }
314
+ export async function checkBun(exec = execAsync) {
315
+ const projectRoot = findProjectRoot();
316
+ if (!projectRoot) {
317
+ return {
318
+ name: "Bun",
319
+ didPass: true,
320
+ isSkipped: true,
321
+ message: "Not in a project - skipping Bun check",
322
+ };
323
+ }
324
+ const resources = discoverResourcesFromRoot(projectRoot);
325
+ const hasGeneratedJsServices = resources.some((resource) => resource.config?.appType === "backend" ||
326
+ resource.config?.appType === "frontend" ||
327
+ resource.config?.appType === "worker");
328
+ if (!hasGeneratedJsServices) {
329
+ return {
330
+ name: "Bun",
331
+ didPass: true,
332
+ isSkipped: true,
333
+ message: "No generated JS services - skipping Bun check",
334
+ };
335
+ }
336
+ try {
337
+ const rawVersion = String(await exec("bun --version", EXEC_TIMEOUT_MS)).trim();
338
+ if (!versionMeetsMinimum(rawVersion, MIN_BUN_VERSION)) {
339
+ return {
340
+ name: "Bun",
341
+ didPass: false,
342
+ message: `Bun ${rawVersion || "version unknown"} is below the required 1.2.0 floor`,
343
+ fix: "Install Bun 1.2.0 or newer: https://bun.sh/install",
344
+ };
345
+ }
346
+ return {
347
+ name: "Bun",
348
+ didPass: true,
349
+ message: `Bun ${rawVersion} is available`,
350
+ };
351
+ }
352
+ catch {
353
+ return {
354
+ name: "Bun",
355
+ didPass: false,
356
+ message: "Bun is not available",
357
+ fix: DOCTOR_FIXES.bunMissing,
358
+ };
359
+ }
360
+ }
174
361
  function checkEnvironmentVariables() {
175
362
  const projectRoot = findProjectRoot() ?? process.cwd();
176
363
  const validation = validateEnvFile(projectRoot);
@@ -712,10 +899,11 @@ function parseDockerStats(raw) {
712
899
  function checkContainerResourceHealth() {
713
900
  let raw;
714
901
  try {
715
- raw = execSync('docker stats --no-stream --format "{{.Name}},{{.MemPerc}},{{.CPUPerc}}"', {
902
+ raw = execFileSync(findOnPath("docker") ?? "docker", ["stats", "--no-stream", "--format", "{{.Name}},{{.MemPerc}},{{.CPUPerc}}"], {
716
903
  stdio: "pipe",
717
904
  encoding: "utf-8",
718
905
  timeout: EXEC_TIMEOUT_MS,
906
+ windowsHide: process.platform === "win32",
719
907
  });
720
908
  }
721
909
  catch (err) {
@@ -781,8 +969,14 @@ async function checkServiceHealth(timeoutMs) {
781
969
  export const doctorCommand = new Command("doctor")
782
970
  .description("Check environment readiness for TDK")
783
971
  .option("--no-ping", "Skip pinging running services' /health endpoints")
972
+ .option("--strict", "Fail warnings such as a WSL project under /mnt/c")
784
973
  .option("--ping-timeout <ms>", "Per-service ping timeout in milliseconds", String(DEFAULT_PING_TIMEOUT_MS))
785
974
  .action(async (options) => {
975
+ if (process.platform === "win32") {
976
+ console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
977
+ process.exit(1);
978
+ return;
979
+ }
786
980
  console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
787
981
  console.log("Checking environment...\n");
788
982
  const pingTimeout = Number.parseInt(options.pingTimeout, 10);
@@ -791,10 +985,17 @@ export const doctorCommand = new Command("doctor")
791
985
  process.exit(1);
792
986
  }
793
987
  const machineChecks = [
988
+ () => checkHostPorts(),
989
+ () => checkIngressPorts(),
794
990
  checkDockerRuntime,
991
+ checkDockerOperatingSystem,
795
992
  checkTilt,
796
993
  checkDockerCompose,
797
994
  checkDockerVersions,
995
+ checkBun,
996
+ () => checkWslProjectLocation(findProjectRoot() ?? process.cwd(), options.strict),
997
+ // Each project needs several networks; a full address pool fails `tdk up` late.
998
+ () => checkDockerNetworkCapacity(),
798
999
  ];
799
1000
  const projectChecks = [
800
1001
  checkMasterConfigs,
@@ -806,11 +1007,13 @@ export const doctorCommand = new Command("doctor")
806
1007
  checkTypeScriptTypeDependencies,
807
1008
  checkFrontendDockerPreflight,
808
1009
  checkResourceDiscovery,
1010
+ // Wiring mistakes that otherwise surface minutes into `tdk up`.
1011
+ () => checkResourcePackageJson(),
1012
+ () => checkServiceUrlPorts(),
1013
+ () => checkFrontendBackendUrls(),
1014
+ () => checkNatsBroker(),
1015
+ () => checkTiltInstances(),
809
1016
  checkEnvironmentVariables,
810
- // Preflight: catch "port 80 already allocated" BEFORE claiming ready.
811
- checkIngressPorts,
812
- // Preflight: a local Postgres or web server on 5432/80/443.
813
- () => checkHostPorts(),
814
1017
  // Preflight: Verdaccio down causes ImageBuild bun install ConnectionRefused.
815
1018
  async () => {
816
1019
  const root = findProjectRoot() ?? process.cwd();
@@ -832,52 +1035,46 @@ export const doctorCommand = new Command("doctor")
832
1035
  // project. Only the machine checks mean anything there; the project checks
833
1036
  // would all fail with "run tdk project".
834
1037
  const inProject = Boolean(findProjectRoot());
835
- const checks = inProject ? [...machineChecks, ...projectChecks] : machineChecks;
836
- let allPassed = true;
837
- for (const checkFn of checks) {
838
- const result = await checkFn();
839
- if (result.isSkipped) {
1038
+ const checks = inProject ? [...machineChecks, ...projectChecks] : [...machineChecks];
1039
+ // Machine checks are independent and mostly wait on child processes, so run
1040
+ // them concurrently. Project checks stay sequential; rendering ranks failures first.
1041
+ const startedMachineChecks = machineChecks.map((checkFn) => Promise.resolve().then(checkFn));
1042
+ const results = [];
1043
+ for (const [index, checkFn] of checks.entries()) {
1044
+ const result = await (startedMachineChecks[index] ?? checkFn());
1045
+ results.push(result);
1046
+ }
1047
+ // Let machine checks finish before process.exit so their probes can clean up.
1048
+ await Promise.allSettled(startedMachineChecks);
1049
+ const allPassed = results.every((result) => result.didPass || result.isSkipped);
1050
+ for (const result of orderDoctorResults(results)) {
1051
+ if (!result.didPass && !result.isSkipped) {
1052
+ console.log(`${chalk.red("✗")} ${result.message}`);
1053
+ if (result.fix)
1054
+ console.log(`${chalk.blue("ℹ")} Fix: ${result.fix}`);
1055
+ }
1056
+ else if (result.isWarning) {
1057
+ console.log(`${chalk.yellow("⚠")} ${chalk.yellow(result.message)}`);
1058
+ if (result.fix)
1059
+ console.log(`${chalk.blue("ℹ")} Fix: ${result.fix}`);
1060
+ }
1061
+ else if (result.isSkipped) {
840
1062
  console.log(`${chalk.gray("○")} ${chalk.gray(result.message)}`);
841
- if (result.fix) {
1063
+ if (result.fix)
842
1064
  console.log(`${chalk.blue("ℹ")} ${result.fix}`);
843
- }
844
- }
845
- else if (result.didPass) {
846
- console.log(`${chalk.green("✓")} ${result.message}`);
847
1065
  }
848
1066
  else {
849
- console.log(`${chalk.red("✗")} ${result.message}`);
850
- if (result.fix) {
851
- console.log(`${chalk.blue("ℹ")} Fix: ${result.fix}`);
852
- }
853
- allPassed = false;
854
- break;
1067
+ console.log(`${chalk.green("✓")} ${result.message}`);
855
1068
  }
856
1069
  }
857
1070
  console.log("");
858
1071
  if (allPassed && !inProject) {
859
1072
  console.log(`${chalk.gray("○")} ${chalk.gray("Not in a TDK project, so project checks were skipped")}`);
860
1073
  console.log("");
861
- console.log(`${chalk.green(chalk.bold("✓"))} This machine is ready for TDK`);
862
- console.log("");
863
- console.log("Next steps:");
864
- console.log(" 1. mkdir my-app && cd my-app");
865
- console.log(" 2. tdk project --yes");
866
- console.log(" 3. tdk resource api --type backend --stack my-app");
867
- console.log(" 4. tdk up my-app");
868
- }
869
- else if (allPassed) {
870
- console.log(`${chalk.green(chalk.bold("✓"))} Environment ready for TDK`);
871
- console.log("");
872
- console.log("Next steps:");
873
- console.log(" 1. Run: tdk up");
874
- console.log(" 2. Open: http://localhost:10350");
875
1074
  }
876
- else {
877
- console.log(`${chalk.red(chalk.bold("✗"))} Environment not ready`);
878
- console.log("");
879
- console.log("Fix the issues above, then run: tdk doctor");
1075
+ const outcome = getDoctorOutcomeMessage(inProject, allPassed);
1076
+ console.log(`${allPassed ? chalk.green(chalk.bold("✓")) : chalk.red(chalk.bold("✗"))} ${outcome}`);
1077
+ if (!allPassed)
880
1078
  process.exit(1);
881
- }
882
1079
  });
883
1080
  //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,77 @@
1
+ import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { confirmOrCancel } from "../utils/command-helpers.js";
6
+ import { runCommand } from "../utils/errors.js";
7
+ import { findProjectRoot } from "../utils/paths.js";
8
+ export const EJECTED_CONTENT = `# Ejected from TDK
9
+
10
+ These files were generated by TDK. You can keep using Docker and Tilt without the tdk CLI.
11
+
12
+ Keep:
13
+ - Tiltfile
14
+ - Dockerfiles
15
+ - compose files TDK wrote
16
+ - Traefik / proxy config TDK wrote
17
+
18
+ Optional to delete later:
19
+ - .tdk/
20
+ - service.json files (only if you no longer want tdk up)
21
+
22
+ To go back: install TDK and run \`tdk config regenerate\`.
23
+ `;
24
+ export const ejectCommand = new Command("eject")
25
+ .description("Take ownership of the generated Tilt and Docker files")
26
+ .option("--dry-run", "List generated files without changing the project", false)
27
+ .option("--yes", "Skip the confirmation prompt", false)
28
+ .action(async (options) => {
29
+ await runCommand(async () => {
30
+ const projectRoot = findProjectRoot();
31
+ if (!projectRoot) {
32
+ throw new Error("tdk eject: no .tdk/project.json in this directory or parents");
33
+ }
34
+ const generatedPaths = [".tdk/.tdk-out"];
35
+ const presentPaths = generatedPaths.filter((path) => existsSync(join(projectRoot, path)));
36
+ const files = presentPaths.flatMap((path) => {
37
+ const absolutePath = join(projectRoot, path);
38
+ if (statSync(absolutePath).isFile())
39
+ return [path];
40
+ try {
41
+ return readdirSync(absolutePath, { recursive: true }).map((entry) => join(path, entry.toString()));
42
+ }
43
+ catch {
44
+ return [path];
45
+ }
46
+ });
47
+ if (!existsSync(join(projectRoot, ".tdk", ".tdk-out", "Tiltfile"))) {
48
+ throw new Error("Generated Tilt files are missing. Run `tdk project --yes` before ejecting.");
49
+ }
50
+ const rootTiltfile = join(projectRoot, "Tiltfile");
51
+ if (existsSync(rootTiltfile))
52
+ files.push("Tiltfile");
53
+ const ejectedPath = join(projectRoot, "EJECTED.md");
54
+ const willCreate = existsSync(ejectedPath) ? [] : ["EJECTED.md"];
55
+ if (options.dryRun) {
56
+ console.log("Files kept:");
57
+ for (const path of files)
58
+ console.log(` ${path}`);
59
+ console.log("Files created:");
60
+ for (const path of willCreate)
61
+ console.log(` ${path}`);
62
+ if (willCreate.length === 0)
63
+ console.log(" (none)");
64
+ return;
65
+ }
66
+ if (!options.yes &&
67
+ !(await confirmOrCancel("Take ownership of the generated Tilt and Docker files?"))) {
68
+ return;
69
+ }
70
+ if (!existsSync(ejectedPath))
71
+ writeFileSync(ejectedPath, EJECTED_CONTENT, { flag: "wx" });
72
+ console.log(chalk.green("Ejected. Tilt and Docker files are yours."));
73
+ console.log("Next: tilt up");
74
+ console.log("Read EJECTED.md");
75
+ });
76
+ });
77
+ //# sourceMappingURL=eject.js.map
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { execFileSync, spawn } from "node:child_process";
2
2
  import chalk from "chalk";
3
3
  import { Command } from "commander";
4
4
  import { readProjectConfig } from "../generator/template-engine.js";
@@ -9,10 +9,13 @@ import { colorizeByStatus, DEFAULT_BOX_WIDTH, formatBoxLine, formatPadded, getSt
9
9
  import { findProjectRoot } from "../utils/paths.js";
10
10
  import { checkPortStatus } from "../utils/port-assignment.js";
11
11
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
12
+ import { findOnPath } from "../utils/which.js";
12
13
  function execSafe(command, args, options = {}) {
13
14
  return new Promise((resolve, reject) => {
14
- const child = spawn(command, args, {
15
+ const child = spawn(findOnPath(command) ?? command, args, {
15
16
  timeout: options.timeout || 5000,
17
+ shell: false,
18
+ windowsHide: process.platform === "win32",
16
19
  });
17
20
  let stdout = "";
18
21
  let stderr = "";
@@ -46,7 +49,7 @@ function determineDefaultDomain() {
46
49
  }
47
50
  const domains = new Set();
48
51
  try {
49
- const traefikLabels = execSync('docker ps --filter "label=traefik.enable=true" --format "{{.Labels}}" 2>/dev/null', { encoding: "utf-8" });
52
+ const traefikLabels = execFileSync(findOnPath("docker") ?? "docker", ["ps", "--filter", "label=traefik.enable=true", "--format", "{{.Labels}}"], { encoding: "utf-8", windowsHide: process.platform === "win32" });
50
53
  // Capture every Host(`...`) in a rule (multi-host rules included), avoid ReDoS with bounded classes
51
54
  const domainRegex = /Host\(`([a-zA-Z0-9_.-]{1,100})`\)/g;
52
55
  for (let match = domainRegex.exec(traefikLabels); match !== null; match = domainRegex.exec(traefikLabels)) {
@@ -130,7 +133,9 @@ async function checkServiceStatus(serviceName, port, url) {
130
133
  return "running";
131
134
  }
132
135
  if (portStatus === "unknown") {
133
- console.warn(chalk.yellow(`⚠️ Could not check port ${port} (lsof unavailable)`));
136
+ console.warn(chalk.yellow(process.platform === "win32"
137
+ ? `⚠️ Could not check port ${port} with the Windows TCP probe`
138
+ : `⚠️ Could not check port ${port} (port probe unavailable)`));
134
139
  }
135
140
  }
136
141
  try {
@@ -175,6 +180,9 @@ export const networksCommand = new Command("networks")
175
180
  stack: s.stack,
176
181
  basePath: s.config.basePath,
177
182
  url,
183
+ ...(process.platform === "win32"
184
+ ? { loopbackUrl: `http://127.0.0.1:80/${basePath}`.replace(/\/$/, "") }
185
+ : {}),
178
186
  port,
179
187
  status,
180
188
  };
@@ -200,6 +208,8 @@ export const networksCommand = new Command("networks")
200
208
  if (options.raw) {
201
209
  for (const service of filteredServices) {
202
210
  console.log(service.url);
211
+ if (service.loopbackUrl)
212
+ console.log(service.loopbackUrl);
203
213
  }
204
214
  process.exit(0);
205
215
  }
@@ -237,6 +247,9 @@ export const networksCommand = new Command("networks")
237
247
  : chalk.gray(service.url); // Gray out URL if stopped
238
248
  const statusLabel = service.status !== "running" ? chalk.gray(` [${service.status}]`) : "";
239
249
  console.log(` ${statusEmoji} ${chalk.white(namePart)} ${urlPart}${statusLabel}`);
250
+ if (service.loopbackUrl) {
251
+ console.log(` ${chalk.gray(`Loopback: ${service.loopbackUrl} (Host: ${new URL(service.url).hostname})`)}`);
252
+ }
240
253
  }
241
254
  }
242
255
  console.log();