@tdk-landscape/tdk-cli-core 1.3.76 → 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/README.md +58 -12
- 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 +286 -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 +15 -7
- 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 +3 -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 +0 -1
- 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,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,
|
|
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
|
|
89
|
+
return isExecTimeout(err);
|
|
22
90
|
}
|
|
23
91
|
function createExecCheck(name, command, successMessage, failureMessage, fixInstructions) {
|
|
24
|
-
return () => {
|
|
92
|
+
return async () => {
|
|
25
93
|
try {
|
|
26
|
-
|
|
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
|
|
45
|
-
|
|
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
|
-
|
|
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:
|
|
60
|
-
fix:
|
|
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 =
|
|
128
|
-
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();
|
|
129
219
|
let engineRaw;
|
|
130
220
|
let composeRaw;
|
|
131
221
|
try {
|
|
132
|
-
engineRaw =
|
|
133
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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.
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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 {
|
|
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();
|