@tdk-landscape/tdk-cli-core 1.3.76 → 1.3.78

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 +2 -0
  2. package/README.md +58 -12
  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 +286 -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 +15 -7
  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 +3 -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 +0 -1
  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,29 +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
10
  import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
11
11
  import { validateEnvFile } from "../utils/env-validator.js";
12
+ import { execAsync, isExecTimeout } from "../utils/exec-async.js";
12
13
  import { formatCount } from "../utils/formatting.js";
13
14
  import { findProjectRoot } from "../utils/paths.js";
14
15
  import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
15
16
  import { discoverResourcesFromRoot } from "../utils/services.js";
17
+ import { findOnPath } from "../utils/which.js";
16
18
  export { checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, summarizeTiltBuildError, } from "../utils/doctor-runtime.js";
17
19
  // A wedged Docker daemon makes `docker ps` block forever instead of failing,
18
20
  // and doctor is exactly the tool people run when their environment is broken.
19
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
+ }
20
88
  function isTimeout(err) {
21
- return err?.code === "ETIMEDOUT";
89
+ return isExecTimeout(err);
22
90
  }
23
91
  function createExecCheck(name, command, successMessage, failureMessage, fixInstructions) {
24
- return () => {
92
+ return async () => {
25
93
  try {
26
- execSync(command, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
94
+ await execAsync(command, EXEC_TIMEOUT_MS);
27
95
  return {
28
96
  name,
29
97
  didPass: true,
@@ -41,10 +109,37 @@ function createExecCheck(name, command, successMessage, failureMessage, fixInstr
41
109
  }
42
110
  };
43
111
  }
44
- function checkDockerRuntime() {
45
- // 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"));
46
141
  try {
47
- execSync("docker ps", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
142
+ await execAsync("docker ps", EXEC_TIMEOUT_MS);
48
143
  return {
49
144
  name: "Container Runtime",
50
145
  didPass: true,
@@ -56,55 +151,50 @@ function checkDockerRuntime() {
56
151
  return {
57
152
  name: "Container Runtime",
58
153
  didPass: false,
59
- message: `Docker daemon is not responding (\`docker ps\` hung for ${EXEC_TIMEOUT_MS / 1000}s)`,
60
- fix: "Restart the runtime: quit and reopen Docker Desktop, or run `colima restart`",
61
- };
62
- }
63
- // Docker not running, check for Colima
64
- try {
65
- execSync("colima status", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
66
- // Colima is running
67
- return {
68
- name: "Container Runtime",
69
- didPass: true,
70
- message: "Colima (Docker runtime) is running",
154
+ message: "Docker daemon is not responding",
155
+ fix: DOCTOR_FIXES.dockerDaemonDown,
71
156
  };
72
157
  }
73
- catch {
74
- // Check if Colima is installed but not running
75
- try {
76
- execSync("which colima", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
77
- return {
78
- name: "Container Runtime",
79
- didPass: false,
80
- message: "Colima is installed but not running",
81
- fix: "Start Colima: colima start",
82
- };
83
- }
84
- catch {
85
- // Check for Podman
86
- try {
87
- execSync("podman ps", { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
88
- return {
89
- name: "Container Runtime",
90
- didPass: true,
91
- message: "Podman is running",
92
- };
93
- }
94
- catch {
95
- // No container runtime found
96
- return {
97
- name: "Container Runtime",
98
- didPass: false,
99
- message: "No container runtime (Docker/Colima/Podman) is running",
100
- fix: `Start: colima start (recommended) OR open -a Docker (macOS) OR sudo systemctl start docker (Linux). Setup guide: ${QUICKSTART_DOCS_URL}`,
101
- };
102
- }
103
- }
104
- }
105
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
+ };
106
196
  }
107
- 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/");
108
198
  // Generated healthchecks use `start_interval`, which older engines/compose reject.
109
199
  const MIN_DOCKER_ENGINE_VERSION = [25, 0, 0];
110
200
  const MIN_DOCKER_COMPOSE_VERSION = [2, 20, 2];
@@ -124,13 +214,15 @@ function isAtLeast(version, minimum) {
124
214
  }
125
215
  return true;
126
216
  }
127
- export function checkDockerVersions(exec = execSync) {
128
- 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();
129
219
  let engineRaw;
130
220
  let composeRaw;
131
221
  try {
132
- engineRaw = run("docker version --format '{{.Server.Version}}'");
133
- 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
+ ]);
134
226
  }
135
227
  catch {
136
228
  return {
@@ -171,7 +263,101 @@ export function checkDockerVersions(exec = execSync) {
171
263
  fix: "Update Docker Desktop, or on Linux update docker-ce and the docker-compose-plugin package",
172
264
  };
173
265
  }
174
- 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
+ }
175
361
  function checkEnvironmentVariables() {
176
362
  const projectRoot = findProjectRoot() ?? process.cwd();
177
363
  const validation = validateEnvFile(projectRoot);
@@ -713,10 +899,11 @@ function parseDockerStats(raw) {
713
899
  function checkContainerResourceHealth() {
714
900
  let raw;
715
901
  try {
716
- raw = execSync('docker stats --no-stream --format "{{.Name}},{{.MemPerc}},{{.CPUPerc}}"', {
902
+ raw = execFileSync(findOnPath("docker") ?? "docker", ["stats", "--no-stream", "--format", "{{.Name}},{{.MemPerc}},{{.CPUPerc}}"], {
717
903
  stdio: "pipe",
718
904
  encoding: "utf-8",
719
905
  timeout: EXEC_TIMEOUT_MS,
906
+ windowsHide: process.platform === "win32",
720
907
  });
721
908
  }
722
909
  catch (err) {
@@ -782,8 +969,14 @@ async function checkServiceHealth(timeoutMs) {
782
969
  export const doctorCommand = new Command("doctor")
783
970
  .description("Check environment readiness for TDK")
784
971
  .option("--no-ping", "Skip pinging running services' /health endpoints")
972
+ .option("--strict", "Fail warnings such as a WSL project under /mnt/c")
785
973
  .option("--ping-timeout <ms>", "Per-service ping timeout in milliseconds", String(DEFAULT_PING_TIMEOUT_MS))
786
974
  .action(async (options) => {
975
+ if (process.platform === "win32") {
976
+ console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
977
+ process.exit(1);
978
+ return;
979
+ }
787
980
  console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
788
981
  console.log("Checking environment...\n");
789
982
  const pingTimeout = Number.parseInt(options.pingTimeout, 10);
@@ -792,10 +985,15 @@ export const doctorCommand = new Command("doctor")
792
985
  process.exit(1);
793
986
  }
794
987
  const machineChecks = [
988
+ () => checkHostPorts(),
989
+ () => checkIngressPorts(),
795
990
  checkDockerRuntime,
991
+ checkDockerOperatingSystem,
796
992
  checkTilt,
797
993
  checkDockerCompose,
798
994
  checkDockerVersions,
995
+ checkBun,
996
+ () => checkWslProjectLocation(findProjectRoot() ?? process.cwd(), options.strict),
799
997
  // Each project needs several networks; a full address pool fails `tdk up` late.
800
998
  () => checkDockerNetworkCapacity(),
801
999
  ];
@@ -816,10 +1014,6 @@ export const doctorCommand = new Command("doctor")
816
1014
  () => checkNatsBroker(),
817
1015
  () => checkTiltInstances(),
818
1016
  checkEnvironmentVariables,
819
- // Preflight: catch "port 80 already allocated" BEFORE claiming ready.
820
- checkIngressPorts,
821
- // Preflight: a local Postgres or web server on 5432/80/443.
822
- () => checkHostPorts(),
823
1017
  // Preflight: Verdaccio down causes ImageBuild bun install ConnectionRefused.
824
1018
  async () => {
825
1019
  const root = findProjectRoot() ?? process.cwd();
@@ -841,52 +1035,46 @@ export const doctorCommand = new Command("doctor")
841
1035
  // project. Only the machine checks mean anything there; the project checks
842
1036
  // would all fail with "run tdk project".
843
1037
  const inProject = Boolean(findProjectRoot());
844
- const checks = inProject ? [...machineChecks, ...projectChecks] : machineChecks;
845
- let allPassed = true;
846
- for (const checkFn of checks) {
847
- const result = await checkFn();
848
- 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) {
849
1062
  console.log(`${chalk.gray("○")} ${chalk.gray(result.message)}`);
850
- if (result.fix) {
1063
+ if (result.fix)
851
1064
  console.log(`${chalk.blue("ℹ")} ${result.fix}`);
852
- }
853
- }
854
- else if (result.didPass) {
855
- console.log(`${chalk.green("✓")} ${result.message}`);
856
1065
  }
857
1066
  else {
858
- console.log(`${chalk.red("✗")} ${result.message}`);
859
- if (result.fix) {
860
- console.log(`${chalk.blue("ℹ")} Fix: ${result.fix}`);
861
- }
862
- allPassed = false;
863
- break;
1067
+ console.log(`${chalk.green("✓")} ${result.message}`);
864
1068
  }
865
1069
  }
866
1070
  console.log("");
867
1071
  if (allPassed && !inProject) {
868
1072
  console.log(`${chalk.gray("○")} ${chalk.gray("Not in a TDK project, so project checks were skipped")}`);
869
1073
  console.log("");
870
- console.log(`${chalk.green(chalk.bold("✓"))} This machine is ready for TDK`);
871
- console.log("");
872
- console.log("Next steps:");
873
- console.log(" 1. mkdir my-app && cd my-app");
874
- console.log(" 2. tdk project --yes");
875
- console.log(" 3. tdk resource api --type backend --stack my-app");
876
- console.log(" 4. tdk up my-app");
877
- }
878
- else if (allPassed) {
879
- console.log(`${chalk.green(chalk.bold("✓"))} Environment ready for TDK`);
880
- console.log("");
881
- console.log("Next steps:");
882
- console.log(" 1. Run: tdk up");
883
- console.log(" 2. Open: http://localhost:10350");
884
1074
  }
885
- else {
886
- console.log(`${chalk.red(chalk.bold("✗"))} Environment not ready`);
887
- console.log("");
888
- 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)
889
1078
  process.exit(1);
890
- }
891
1079
  });
892
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();