@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.
- package/CHANGELOG.md +6 -0
- package/README.md +59 -13
- package/cli/bin/tdk.js +22 -2
- package/cli/dist/cli.js +9 -2
- package/cli/dist/commands/completion.js +30 -3
- package/cli/dist/commands/config.js +59 -1
- package/cli/dist/commands/doctor.js +295 -98
- package/cli/dist/commands/eject.js +77 -0
- package/cli/dist/commands/networks.js +17 -4
- package/cli/dist/commands/project.js +28 -5
- package/cli/dist/commands/resource.js +118 -10
- package/cli/dist/commands/up.js +93 -21
- package/cli/dist/commands/upgrade.js +163 -68
- package/cli/dist/generator/template-engine.js +77 -13
- package/cli/dist/types/index.js +6 -1
- package/cli/dist/utils/cold-preflight.js +198 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/docker.js +15 -1
- package/cli/dist/utils/doctor-runtime.js +48 -16
- package/cli/dist/utils/doctor-wiring.js +269 -0
- package/cli/dist/utils/errors.js +5 -5
- package/cli/dist/utils/exec-async.js +30 -0
- package/cli/dist/utils/generated-paths.js +49 -0
- package/cli/dist/utils/platform.js +53 -0
- package/cli/dist/utils/port-assignment.js +13 -20
- package/cli/dist/utils/project-templates.js +7 -2
- package/cli/dist/utils/resource-features.js +6 -2
- package/cli/dist/utils/service-manifest.js +73 -0
- package/cli/dist/utils/services.js +8 -1
- package/cli/dist/utils/tar.js +27 -5
- package/cli/dist/utils/tilt-process.js +53 -0
- package/cli/dist/utils/tilt.js +3 -1
- package/cli/dist/utils/which.js +26 -0
- package/cli/dist/utils/windows-doctor.js +175 -0
- package/cli/package.json +1 -1
- package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
- package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
- package/cli/templates/Tiltfile.hbs +24 -4
- package/cli/templates/spec.master.hbs +1 -2
- package/discovery/AGENTS.md +1 -1
- package/discovery/constants.star +2 -2
- package/discovery/discovery_orchestrator.star +2 -2
- package/discovery/manifest/constants.star +1 -1
- package/discovery/manifest/loading.star +7 -3
- package/discovery/registry.star +17 -3
- package/engine/README.md +2 -0
- package/engine/schemas/service-schema.json +47 -21
- package/engine/scripts/generate-manifest.sh +2 -1
- package/engine/topologies/platform/docker/compose/compose.star +37 -5
- package/engine/topologies/tilt/common/utils.star +54 -11
- package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
- package/engine/topologies/tilt/manifest/constants.star +3 -0
- package/engine/topologies/tilt/manifest/schema.star +2 -4
- package/engine/topologies/tilt/manifest/validator.star +1 -1
- package/engine/topologies/tilt/resources/infra-loader.star +14 -3
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
- package/examples/tdk-example/LICENSE +21 -0
- package/examples/tdk-example/README.md +23 -0
- package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
- package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
- package/examples/tdk-example/Tiltfile +25 -0
- package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
- package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
- package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
- package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
- package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
- package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
- package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
- package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
- package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
- package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
- 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,
|
|
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
|
|
89
|
+
return isExecTimeout(err);
|
|
21
90
|
}
|
|
22
91
|
function createExecCheck(name, command, successMessage, failureMessage, fixInstructions) {
|
|
23
|
-
return () => {
|
|
92
|
+
return async () => {
|
|
24
93
|
try {
|
|
25
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
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:
|
|
59
|
-
fix:
|
|
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 =
|
|
127
|
-
const run = (command) => String(exec(command,
|
|
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 =
|
|
132
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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.
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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 {
|
|
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 =
|
|
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(
|
|
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();
|