@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.
- package/CHANGELOG.md +2 -0
- 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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { checkDockerCompose, checkDockerRuntime, checkDockerVersions, checkTilt, } from "../commands/doctor.js";
|
|
5
|
+
import { checkHostPorts, checkIngressPorts } from "./doctor-runtime.js";
|
|
6
|
+
import { checkNatsBroker } from "./doctor-wiring.js";
|
|
7
|
+
import { findProjectRoot } from "./paths.js";
|
|
8
|
+
import { discoverResourcesFromRoot } from "./services.js";
|
|
9
|
+
import { findOnPath } from "./which.js";
|
|
10
|
+
/** Converts a doctor check result into the preflight's compact display item. */
|
|
11
|
+
function item(id, result) {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
ok: result.didPass || Boolean(result.isSkipped),
|
|
15
|
+
message: result.message,
|
|
16
|
+
fix: result.fix,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Returns whether a Node version is below the CLI's 22.12 minimum. */
|
|
20
|
+
export function nodeTooOld(version) {
|
|
21
|
+
const [major = 0, minor = 0] = String(version)
|
|
22
|
+
.split(".")
|
|
23
|
+
.map((part) => Number.parseInt(part, 10));
|
|
24
|
+
return (!Number.isFinite(major) || !Number.isFinite(minor) || major < 22 || (major === 22 && minor < 12));
|
|
25
|
+
}
|
|
26
|
+
async function safeCheck(id, check, fallback) {
|
|
27
|
+
try {
|
|
28
|
+
return item(id, await check());
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { id, ok: false, message: fallback };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Requires a Docker engine and Compose version check to complete successfully. */
|
|
35
|
+
export function requireVerifiedDockerVersions(result) {
|
|
36
|
+
if (!result.isSkipped)
|
|
37
|
+
return result;
|
|
38
|
+
return {
|
|
39
|
+
...result,
|
|
40
|
+
didPass: false,
|
|
41
|
+
isSkipped: false,
|
|
42
|
+
message: `Could not verify Docker Engine/Compose minimum versions: ${result.message}`,
|
|
43
|
+
fix: result.fix ?? "Start Docker and ensure Docker Engine 25+ and Compose 2.20.2+ are installed.",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function usesPrisma(projectRoot) {
|
|
47
|
+
try {
|
|
48
|
+
return discoverResourcesFromRoot(projectRoot).some((resource) => {
|
|
49
|
+
return resource.config?.featuresEnabled?.includes("prisma") ?? false;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function runColdPreflight(opts = {}) {
|
|
57
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
58
|
+
const projectRoot = findProjectRoot(cwd);
|
|
59
|
+
const inProject = Boolean(projectRoot);
|
|
60
|
+
const items = [];
|
|
61
|
+
const nodeVersion = process.versions.node;
|
|
62
|
+
items.push({
|
|
63
|
+
id: "node",
|
|
64
|
+
ok: !nodeTooOld(nodeVersion),
|
|
65
|
+
message: nodeVersion,
|
|
66
|
+
fix: nodeTooOld(nodeVersion)
|
|
67
|
+
? `Install Node.js 22.12+ (current: ${nodeVersion}). https://nodejs.org`
|
|
68
|
+
: undefined,
|
|
69
|
+
});
|
|
70
|
+
items.push(await safeCheck("docker", checkDockerRuntime, "Docker daemon is not running"));
|
|
71
|
+
items.push(await safeCheck("compose", async () => {
|
|
72
|
+
const [compose, versions] = await Promise.all([
|
|
73
|
+
checkDockerCompose(),
|
|
74
|
+
checkDockerVersions(),
|
|
75
|
+
]);
|
|
76
|
+
if (!compose.didPass)
|
|
77
|
+
return compose;
|
|
78
|
+
return requireVerifiedDockerVersions(versions);
|
|
79
|
+
}, "Docker Engine or Compose version check failed"));
|
|
80
|
+
items.push(await safeCheck("tilt", checkTilt, "Tilt CLI not found"));
|
|
81
|
+
let bun = false;
|
|
82
|
+
const bunPath = findOnPath("bun");
|
|
83
|
+
if (bunPath) {
|
|
84
|
+
try {
|
|
85
|
+
const version = execFileSync(bunPath, ["--version"], {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
timeout: 3000,
|
|
88
|
+
}).trim();
|
|
89
|
+
const [major = 0, minor = 0] = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
90
|
+
bun = major > 1 || (major === 1 && minor >= 2);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* reported as a failed runtime check */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
items.push({
|
|
97
|
+
id: "bun",
|
|
98
|
+
ok: bun,
|
|
99
|
+
message: bun ? "Bun 1.2+ is available on PATH" : "Bun 1.2+ not found on PATH",
|
|
100
|
+
fix: bun ? undefined : "curl -fsSL https://bun.sh/install | bash",
|
|
101
|
+
});
|
|
102
|
+
const [hostPorts, ingressPorts] = await Promise.all([
|
|
103
|
+
safeCheck("ports", checkHostPorts, "Could not check required host ports"),
|
|
104
|
+
safeCheck("ports", checkIngressPorts, "Could not check ingress ports"),
|
|
105
|
+
]);
|
|
106
|
+
if (!hostPorts.ok || !ingressPorts.ok) {
|
|
107
|
+
const port5432 = !hostPorts.ok && /(?:^|\n)\s*5432\s*\(/.test(hostPorts.message);
|
|
108
|
+
const messages = [
|
|
109
|
+
!hostPorts.ok
|
|
110
|
+
? port5432
|
|
111
|
+
? "Port 5432 is taken (usually local Postgres). Stop it or TDK cannot bind the bundled Postgres."
|
|
112
|
+
: hostPorts.message
|
|
113
|
+
: undefined,
|
|
114
|
+
!ingressPorts.ok ? ingressPorts.message : undefined,
|
|
115
|
+
].filter((message) => Boolean(message));
|
|
116
|
+
const fixes = [hostPorts.fix, ingressPorts.fix].filter((fix) => Boolean(fix));
|
|
117
|
+
items.push({
|
|
118
|
+
id: "ports",
|
|
119
|
+
ok: false,
|
|
120
|
+
message: messages.join("\n"),
|
|
121
|
+
fix: fixes.join(" "),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (projectRoot) {
|
|
125
|
+
const nats = await safeCheck("nats", () => checkNatsBroker(projectRoot), "NATS broker check failed");
|
|
126
|
+
if (!nats.ok)
|
|
127
|
+
items.push(nats);
|
|
128
|
+
if (usesPrisma(projectRoot)) {
|
|
129
|
+
let dbUp = false;
|
|
130
|
+
try {
|
|
131
|
+
const config = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
|
|
132
|
+
const projectName = String(config?.project?.name ?? "").replace(/-/g, "_");
|
|
133
|
+
if (projectName) {
|
|
134
|
+
const running = execFileSync(findOnPath("docker") ?? "docker", [
|
|
135
|
+
"ps",
|
|
136
|
+
"--filter",
|
|
137
|
+
`label=com.docker.compose.project=${projectName}`,
|
|
138
|
+
"--filter",
|
|
139
|
+
"label=com.docker.compose.service=postgres",
|
|
140
|
+
"--filter",
|
|
141
|
+
"health=healthy",
|
|
142
|
+
"--format",
|
|
143
|
+
"{{.Names}}",
|
|
144
|
+
], { encoding: "utf-8", timeout: 3000 }).trim();
|
|
145
|
+
dbUp = running.length > 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* Docker unavailable is already represented by the machine checks. */
|
|
150
|
+
}
|
|
151
|
+
if (!dbUp)
|
|
152
|
+
items.push({
|
|
153
|
+
id: "prisma",
|
|
154
|
+
ok: false,
|
|
155
|
+
message: "A Prisma resource is configured, but the project Postgres service is not healthy",
|
|
156
|
+
fix: "Run `tdk up` to start the bundled Postgres, then check `tdk doctor`.",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const failures = items.filter((check) => !check.ok).slice(0, 8);
|
|
161
|
+
const machineFailed = failures.some((check) => ["node", "docker", "compose", "tilt", "bun", "ports"].includes(check.id));
|
|
162
|
+
const bunFailed = failures.some((check) => check.id === "bun");
|
|
163
|
+
const header = failures.length
|
|
164
|
+
? machineFailed
|
|
165
|
+
? bunFailed
|
|
166
|
+
? "Cold start blocked. Bun 1.2+ must be installed on PATH.\nPrisma/NATS configuration is project-specific and generated after `tdk project`. Fix the machine checks below."
|
|
167
|
+
: "Cold start blocked. Bun/Prisma/NATS are not the first failure.\nThey are generated after `tdk project`. Fix the machine checks below."
|
|
168
|
+
: "Machine is ready. Project wiring is not."
|
|
169
|
+
: "";
|
|
170
|
+
const footer = failures.length
|
|
171
|
+
? ""
|
|
172
|
+
: inProject
|
|
173
|
+
? "Environment ready for TDK"
|
|
174
|
+
: "This machine is ready for TDK.\nNext: tdk project --yes";
|
|
175
|
+
return { ok: failures.length === 0, inProject, items: failures, header, footer };
|
|
176
|
+
}
|
|
177
|
+
/** Formats only failed preflight checks and the applicable readiness message. */
|
|
178
|
+
export function formatColdPreflight(result, options = {}) {
|
|
179
|
+
const lines = [result.header];
|
|
180
|
+
for (const check of result.items) {
|
|
181
|
+
lines.push(`FAIL ${check.id.padEnd(7)} ${check.message}`);
|
|
182
|
+
if (check.fix)
|
|
183
|
+
lines.push(` ${check.fix}`);
|
|
184
|
+
}
|
|
185
|
+
if (result.footer && (options.includeSuccessFooter ?? true))
|
|
186
|
+
lines.push(result.footer);
|
|
187
|
+
return lines.filter(Boolean).join("\n");
|
|
188
|
+
}
|
|
189
|
+
/** Exits before machine-dependent command work when any machine check fails. */
|
|
190
|
+
export async function assertMachineReadyOrExit() {
|
|
191
|
+
const result = await runColdPreflight();
|
|
192
|
+
const machineFailure = result.items.some((check) => ["node", "docker", "compose", "tilt", "bun", "ports"].includes(check.id));
|
|
193
|
+
if (machineFailure) {
|
|
194
|
+
console.error(formatColdPreflight(result));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=cold-preflight.js.map
|
|
@@ -10,10 +10,12 @@ export const VALID_RESOURCE_TYPES = [
|
|
|
10
10
|
"sdk",
|
|
11
11
|
"worker",
|
|
12
12
|
"migrator",
|
|
13
|
+
"bring-your-own",
|
|
13
14
|
];
|
|
14
15
|
// Frontend/library/sdk services are served statically or consumed as
|
|
15
16
|
// packages, so they never need a "start" script - only backend/worker/
|
|
16
17
|
// migrator processes are run in production via `<runtime> run start`.
|
|
18
|
+
// bring-your-own doesn't generate application code, so no scripts required.
|
|
17
19
|
export const REQUIRED_PACKAGE_SCRIPTS = {
|
|
18
20
|
backend: ["dev", "build", "start"],
|
|
19
21
|
worker: ["dev", "build", "start"],
|
|
@@ -21,6 +23,7 @@ export const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
21
23
|
frontend: ["dev", "build"],
|
|
22
24
|
library: ["dev", "build"],
|
|
23
25
|
sdk: ["dev", "build"],
|
|
26
|
+
"bring-your-own": [],
|
|
24
27
|
};
|
|
25
28
|
// Version specifiers that pin nothing at all - a fresh `bun install`/`npm install`
|
|
26
29
|
// can pull a different (possibly breaking) version on every run.
|
|
@@ -35,6 +38,7 @@ export const OPTIONAL_INFRA_SERVICES = [
|
|
|
35
38
|
export const PORT_RANGES = {
|
|
36
39
|
frontend: { base: 3000, min: 3000, max: 3999, range: "3000-3999" },
|
|
37
40
|
backend: { base: 4000, min: 4000, max: 4999, range: "4000-4999" },
|
|
41
|
+
"bring-your-own": { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
|
|
38
42
|
worker: { base: 6000, min: 6000, max: 6999, range: "6000-6999" },
|
|
39
43
|
health: { base: 5000, min: 5000, max: 5999, range: "5000-5999" },
|
|
40
44
|
migrator: { base: 7000, min: 7000, max: 7999, range: "7000-7999" },
|
package/cli/dist/utils/docker.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { findOnPath } from "./which.js";
|
|
2
3
|
// A wedged daemon makes `docker ps` block forever instead of failing.
|
|
3
4
|
const RUNTIME_CHECK_TIMEOUT_MS = 10_000;
|
|
4
5
|
/** Whether Docker, Colima, or Podman is reachable, hung, or absent. */
|
|
5
6
|
export function getContainerRuntimeStatus() {
|
|
6
7
|
let sawTimeout = false;
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
try {
|
|
10
|
+
execFileSync(findOnPath("docker") ?? "docker", ["ps"], {
|
|
11
|
+
stdio: "ignore",
|
|
12
|
+
timeout: RUNTIME_CHECK_TIMEOUT_MS,
|
|
13
|
+
windowsHide: true,
|
|
14
|
+
});
|
|
15
|
+
return "running";
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return err.code === "ETIMEDOUT" ? "unresponsive" : "missing";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
7
21
|
for (const command of ["docker ps", "colima status", "podman ps"]) {
|
|
8
22
|
try {
|
|
9
23
|
execSync(command, { stdio: "ignore", timeout: RUNTIME_CHECK_TIMEOUT_MS });
|
|
@@ -1,4 +1,4 @@
|
|
|
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 { createServer } from "node:net";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -6,9 +6,23 @@ import { formatCount } from "./formatting.js";
|
|
|
6
6
|
import { findProjectRoot } from "./paths.js";
|
|
7
7
|
import { getProjectName } from "./service-urls.js";
|
|
8
8
|
import { discoverResources, discoverResourcesFromRoot } from "./services.js";
|
|
9
|
+
import { findOnPath } from "./which.js";
|
|
9
10
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
10
11
|
const REGISTRY_PROBE_TIMEOUT_MS = 3_000;
|
|
11
12
|
const DEFAULT_VERDACCIO_URL = "http://localhost:4873";
|
|
13
|
+
export const HOST_PORT_FIXES = {
|
|
14
|
+
port80: "Stop the process bound to port 80, or stop local nginx/caddy. Then: tdk doctor",
|
|
15
|
+
port5432: "Stop local Postgres or change the host port. Then: tdk doctor",
|
|
16
|
+
};
|
|
17
|
+
function runHostCommand(exec, command, args, shellCommand, options) {
|
|
18
|
+
if (process.platform === "win32") {
|
|
19
|
+
return execFileSync(findOnPath(command) ?? command, args, {
|
|
20
|
+
...options,
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return exec(shellCommand, options);
|
|
25
|
+
}
|
|
12
26
|
/** Host ports Traefik publishes for local ingress. Without these, app routes never come up. */
|
|
13
27
|
export const INGRESS_PORTS = [80, 443];
|
|
14
28
|
export function toComposeProjectPrefix(projectName) {
|
|
@@ -232,7 +246,7 @@ export function checkPrivateNpmRegistry(exec = execSync, projectRoot = findProje
|
|
|
232
246
|
}
|
|
233
247
|
let containerRunning = false;
|
|
234
248
|
try {
|
|
235
|
-
const names = exec
|
|
249
|
+
const names = runHostCommand(exec, "docker", ["ps", "--format", "{{.Names}}"], "docker ps --format '{{.Names}}'", {
|
|
236
250
|
stdio: "pipe",
|
|
237
251
|
encoding: "utf-8",
|
|
238
252
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -246,11 +260,18 @@ export function checkPrivateNpmRegistry(exec = execSync, projectRoot = findProje
|
|
|
246
260
|
let httpDetail = "";
|
|
247
261
|
try {
|
|
248
262
|
// curl is more reliable than fetch in the compiled CLI binary environments.
|
|
249
|
-
const probe =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
263
|
+
const probe = process.platform === "win32"
|
|
264
|
+
? execFileSync("curl.exe", ["-fsS", "-o", "NUL", "-w", "%{http_code}", "--max-time", "2", registryUrl], {
|
|
265
|
+
stdio: "pipe",
|
|
266
|
+
encoding: "utf-8",
|
|
267
|
+
timeout: REGISTRY_PROBE_TIMEOUT_MS + 1_000,
|
|
268
|
+
windowsHide: true,
|
|
269
|
+
}).trim()
|
|
270
|
+
: exec(`curl -fsS -o /dev/null -w '%{http_code}' --max-time 2 ${JSON.stringify(registryUrl)}`, {
|
|
271
|
+
stdio: "pipe",
|
|
272
|
+
encoding: "utf-8",
|
|
273
|
+
timeout: REGISTRY_PROBE_TIMEOUT_MS + 1_000,
|
|
274
|
+
}).trim();
|
|
254
275
|
httpOk = /^[23]\d\d$/.test(probe);
|
|
255
276
|
httpDetail = `HTTP ${probe}`;
|
|
256
277
|
}
|
|
@@ -288,7 +309,7 @@ export function checkIngressPorts(exec = execSync, projectName = getProjectName(
|
|
|
288
309
|
const projectPrefix = toComposeProjectPrefix(projectName);
|
|
289
310
|
let dockerPs = "";
|
|
290
311
|
try {
|
|
291
|
-
dockerPs = exec
|
|
312
|
+
dockerPs = runHostCommand(exec, "docker", ["ps", "--format", "{{.Names}}\\t{{.Ports}}"], "docker ps --format '{{.Names}}\\t{{.Ports}}'", {
|
|
292
313
|
stdio: "pipe",
|
|
293
314
|
encoding: "utf-8",
|
|
294
315
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -422,7 +443,8 @@ export function summarizeServiceProbes(probes) {
|
|
|
422
443
|
* containers here.
|
|
423
444
|
*/
|
|
424
445
|
export async function checkHostPorts(exec = execSync, projectName = getProjectName(), probe = probeHostPort) {
|
|
425
|
-
|
|
446
|
+
// Postgres is the most common environment blocker and must be surfaced first.
|
|
447
|
+
const ports = [5432, 80, 443];
|
|
426
448
|
const inUse = [];
|
|
427
449
|
for (const port of ports) {
|
|
428
450
|
if ((await probe(port)) === "in-use")
|
|
@@ -437,7 +459,7 @@ export async function checkHostPorts(exec = execSync, projectName = getProjectNa
|
|
|
437
459
|
}
|
|
438
460
|
let dockerPs = "";
|
|
439
461
|
try {
|
|
440
|
-
dockerPs = exec
|
|
462
|
+
dockerPs = runHostCommand(exec, "docker", ["ps", "--format", "{{.Names}}\\t{{.Ports}}"], "docker ps --format '{{.Names}}\\t{{.Ports}}'", {
|
|
441
463
|
stdio: "pipe",
|
|
442
464
|
encoding: "utf-8",
|
|
443
465
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -456,7 +478,8 @@ export async function checkHostPorts(exec = execSync, projectName = getProjectNa
|
|
|
456
478
|
for (const port of inUse) {
|
|
457
479
|
const holder = holders.find((h) => h.publishedPorts.includes(port));
|
|
458
480
|
if (!holder) {
|
|
459
|
-
|
|
481
|
+
const detail = port === 5432 ? "; inspect it with lsof -nP -iTCP:5432" : "";
|
|
482
|
+
problems.push(`${port} (${HOST_PORTS[port]}) is used by a program on this machine${detail}`);
|
|
460
483
|
}
|
|
461
484
|
else if (port === 5432 && !isOwn(holder.name)) {
|
|
462
485
|
problems.push(`${port} (${HOST_PORTS[port]}) is published by container ${holder.name}`);
|
|
@@ -466,12 +489,21 @@ export async function checkHostPorts(exec = execSync, projectName = getProjectNa
|
|
|
466
489
|
return { name: "Host Ports", didPass: true, message: "Host ports are held by this project" };
|
|
467
490
|
}
|
|
468
491
|
const first = inUse.find((port) => problems.some((p) => p.startsWith(`${port} `))) ?? inUse[0];
|
|
492
|
+
let fix;
|
|
493
|
+
if (first === 80) {
|
|
494
|
+
fix = HOST_PORT_FIXES.port80;
|
|
495
|
+
}
|
|
496
|
+
else if (first === 5432) {
|
|
497
|
+
fix = HOST_PORT_FIXES.port5432;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
fix = `Stop the process bound to port ${first}. Then: tdk doctor`;
|
|
501
|
+
}
|
|
469
502
|
return {
|
|
470
503
|
name: "Host Ports",
|
|
471
504
|
didPass: false,
|
|
472
505
|
message: `Ports TDK needs are taken:\n ${problems.join("\n ")}`,
|
|
473
|
-
fix
|
|
474
|
-
"(for a local Postgres: `brew services stop postgresql` or quit Postgres.app), then run `tdk up`.",
|
|
506
|
+
fix,
|
|
475
507
|
};
|
|
476
508
|
}
|
|
477
509
|
/**
|
|
@@ -482,7 +514,7 @@ export async function checkHostPorts(exec = execSync, projectName = getProjectNa
|
|
|
482
514
|
export function checkTiltResourceHealth(exec = execSync) {
|
|
483
515
|
let jsonText = "";
|
|
484
516
|
try {
|
|
485
|
-
jsonText = exec
|
|
517
|
+
jsonText = runHostCommand(exec, "tilt", ["get", "uiresources", "-o", "json"], "tilt get uiresources -o json", {
|
|
486
518
|
stdio: "pipe",
|
|
487
519
|
encoding: "utf-8",
|
|
488
520
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -588,7 +620,7 @@ export function orderTiltFailures(failures) {
|
|
|
588
620
|
*/
|
|
589
621
|
export function probeContainerRuntimeError(resourceName, exec = execSync) {
|
|
590
622
|
try {
|
|
591
|
-
const names = exec
|
|
623
|
+
const names = runHostCommand(exec, "docker", ["ps", "-a", "--format", "{{.Names}}"], "docker ps -a --format '{{.Names}}'", {
|
|
592
624
|
stdio: "pipe",
|
|
593
625
|
encoding: "utf-8",
|
|
594
626
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -600,7 +632,7 @@ export function probeContainerRuntimeError(resourceName, exec = execSync) {
|
|
|
600
632
|
names.find((name) => name.includes(resourceName));
|
|
601
633
|
if (!match)
|
|
602
634
|
return null;
|
|
603
|
-
const logs = exec
|
|
635
|
+
const logs = runHostCommand(exec, "docker", ["logs", "--tail", "120", match], `docker logs --tail 120 ${JSON.stringify(match)} 2>&1`, {
|
|
604
636
|
stdio: "pipe",
|
|
605
637
|
encoding: "utf-8",
|
|
606
638
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { join, relative } from "node:path";
|
|
4
|
+
import { execAsync } from "./exec-async.js";
|
|
4
5
|
import { formatCount } from "./formatting.js";
|
|
5
6
|
import { findProjectRoot } from "./paths.js";
|
|
6
7
|
import { discoverResourcesFromRoot } from "./services.js";
|
|
@@ -185,17 +186,24 @@ export function checkNatsBroker(projectRoot = findProjectRoot() ?? process.cwd()
|
|
|
185
186
|
export function parseTiltProcesses(psOutput) {
|
|
186
187
|
const processes = [];
|
|
187
188
|
for (const line of psOutput.split("\n")) {
|
|
188
|
-
const match = line.match(/^\s*(\d+)\s+.*\btilt up\b.*?\s-f\s+(\S+)/);
|
|
189
|
+
const match = line.match(/^\s*(\d+)\s+.*\btilt up\b.*?\s-f\s+(\S+)/i);
|
|
189
190
|
if (!match)
|
|
190
191
|
continue;
|
|
191
|
-
processes.push({ pid: Number(match[1]), root: match[2].split(
|
|
192
|
+
processes.push({ pid: Number(match[1]), root: match[2].split(/[\\/]\.tdk[\\/]/i)[0] });
|
|
192
193
|
}
|
|
193
194
|
return processes;
|
|
194
195
|
}
|
|
195
196
|
export function checkTiltInstances(projectRoot = findProjectRoot() ?? process.cwd(), exec = execSync) {
|
|
196
197
|
let output;
|
|
197
198
|
try {
|
|
198
|
-
output =
|
|
199
|
+
output =
|
|
200
|
+
process.platform === "win32"
|
|
201
|
+
? execFileSync("powershell.exe", [
|
|
202
|
+
"-NoProfile",
|
|
203
|
+
"-Command",
|
|
204
|
+
"Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'tilt.exe' } | ForEach-Object { '{0} {1}' -f $_.ProcessId, $_.CommandLine }",
|
|
205
|
+
], { encoding: "utf-8", timeout: EXEC_TIMEOUT_MS, windowsHide: true })
|
|
206
|
+
: exec("ps -Ao pid=,command=", { encoding: "utf-8", timeout: EXEC_TIMEOUT_MS });
|
|
199
207
|
}
|
|
200
208
|
catch {
|
|
201
209
|
return {
|
|
@@ -228,10 +236,10 @@ export function checkTiltInstances(projectRoot = findProjectRoot() ?? process.cw
|
|
|
228
236
|
}
|
|
229
237
|
return { name: "Tilt processes", didPass: true, message: "No stray Tilt processes" };
|
|
230
238
|
}
|
|
231
|
-
export function checkDockerNetworkCapacity(exec =
|
|
239
|
+
export async function checkDockerNetworkCapacity(exec = execAsync) {
|
|
232
240
|
const probe = `tdk_doctor_probe_${process.pid}`;
|
|
233
241
|
try {
|
|
234
|
-
exec(`docker network create ${probe}`,
|
|
242
|
+
await exec(`docker network create ${probe}`, EXEC_TIMEOUT_MS);
|
|
235
243
|
}
|
|
236
244
|
catch (error) {
|
|
237
245
|
const stderr = String(error.stderr ?? error);
|
|
@@ -251,7 +259,7 @@ export function checkDockerNetworkCapacity(exec = execSync) {
|
|
|
251
259
|
};
|
|
252
260
|
}
|
|
253
261
|
try {
|
|
254
|
-
exec(`docker network rm ${probe}`,
|
|
262
|
+
await exec(`docker network rm ${probe}`, EXEC_TIMEOUT_MS);
|
|
255
263
|
}
|
|
256
264
|
catch {
|
|
257
265
|
// A leftover probe network is harmless and tdk_doctor_probe_* is easy to prune.
|
package/cli/dist/utils/errors.js
CHANGED
|
@@ -63,16 +63,16 @@ export const errorFactories = {
|
|
|
63
63
|
"Path must be within the project directory",
|
|
64
64
|
'Path cannot contain special characters like <>:"|?*',
|
|
65
65
|
]),
|
|
66
|
-
notInProject: () => new TdkError("Could not find project root (no
|
|
67
|
-
"Run this from within a project
|
|
68
|
-
"Run `tdk project` to initialize a new project",
|
|
66
|
+
notInProject: () => new TdkError("Could not find project root (no .tdk/project.json found)", [
|
|
67
|
+
"Run this from within a TDK project",
|
|
68
|
+
"Run `tdk project --yes` to initialize a new project",
|
|
69
69
|
]),
|
|
70
70
|
};
|
|
71
71
|
export function requireProjectRoot() {
|
|
72
72
|
const projectRoot = findProjectRoot();
|
|
73
73
|
if (!projectRoot) {
|
|
74
|
-
console.error(chalk.red("Error: Could not find project root (no
|
|
75
|
-
console.error(chalk.gray("Run
|
|
74
|
+
console.error(chalk.red("Error: Could not find project root (no .tdk/project.json found)."));
|
|
75
|
+
console.error(chalk.gray("Run `tdk project --yes` to initialize a new project."));
|
|
76
76
|
process.exit(1);
|
|
77
77
|
}
|
|
78
78
|
return projectRoot;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { exec, execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { findOnPath } from "./which.js";
|
|
4
|
+
const execPromise = promisify(exec);
|
|
5
|
+
export const execAsync = async (command, timeoutMs) => {
|
|
6
|
+
if (process.platform === "win32") {
|
|
7
|
+
// Doctor commands are simple executable/argument invocations. Tokenize
|
|
8
|
+
// their quoted arguments and bypass cmd.exe/PowerShell entirely.
|
|
9
|
+
const parts = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
10
|
+
const [parsedFile, ...args] = parts.map((part) => part.startsWith('"') || part.startsWith("'") ? part.slice(1, -1) : part);
|
|
11
|
+
if (!parsedFile)
|
|
12
|
+
throw new Error("Command is empty");
|
|
13
|
+
const file = findOnPath(parsedFile) ?? parsedFile;
|
|
14
|
+
const { stdout } = await promisify(execFile)(file, args, {
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
timeout: timeoutMs,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
shell: false,
|
|
19
|
+
});
|
|
20
|
+
return stdout;
|
|
21
|
+
}
|
|
22
|
+
const { stdout } = await execPromise(command, { encoding: "utf-8", timeout: timeoutMs });
|
|
23
|
+
return stdout;
|
|
24
|
+
};
|
|
25
|
+
/** `exec` kills a timed-out child and sets `killed`; `execSync` reports ETIMEDOUT. */
|
|
26
|
+
export function isExecTimeout(err) {
|
|
27
|
+
const e = err;
|
|
28
|
+
return e?.code === "ETIMEDOUT" || e?.killed === true;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=exec-async.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, normalize, relative, sep } from "node:path";
|
|
3
|
+
/** Reviewed TDK-owned destinations written by `tdk config regenerate` or Tilt generation. */
|
|
4
|
+
export const TDK_GENERATED_PATH_ALLOWLIST = [
|
|
5
|
+
".autogenerated/**",
|
|
6
|
+
".tdk/.tdk-out/**",
|
|
7
|
+
"Tiltfile",
|
|
8
|
+
"TILT_*.star",
|
|
9
|
+
".tiltignore",
|
|
10
|
+
"package.json (create only)",
|
|
11
|
+
"shared-platform-engineering/docker-templates/**",
|
|
12
|
+
"services/platform/database-management/docker-compose.yml",
|
|
13
|
+
"product_autogenerated_resource_manifests/**",
|
|
14
|
+
"product_autogenerated_services_list/**",
|
|
15
|
+
];
|
|
16
|
+
export function isTdkGeneratedPath(path) {
|
|
17
|
+
const normalized = normalize(path).split(sep).join("/");
|
|
18
|
+
if (isAbsolute(path) || normalized === ".." || normalized.startsWith("../"))
|
|
19
|
+
return false;
|
|
20
|
+
if (normalized.split("/").includes(".autogenerated") ||
|
|
21
|
+
normalized.startsWith(".tdk/.tdk-out/") ||
|
|
22
|
+
normalized === "shared-platform-engineering/docker-templates" ||
|
|
23
|
+
normalized.startsWith("shared-platform-engineering/docker-templates/") ||
|
|
24
|
+
normalized.startsWith("product_autogenerated_resource_manifests/") ||
|
|
25
|
+
normalized.startsWith("product_autogenerated_services_list/")) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return (normalized === "Tiltfile" ||
|
|
29
|
+
/^TILT_[^/]+\.star$/.test(normalized) ||
|
|
30
|
+
normalized === ".tiltignore" ||
|
|
31
|
+
normalized === "package.json" ||
|
|
32
|
+
normalized === "services/platform/database-management/docker-compose.yml");
|
|
33
|
+
}
|
|
34
|
+
export function assertTdkGeneratedPath(projectRoot, path) {
|
|
35
|
+
const absolute = isAbsolute(path) ? normalize(path) : normalize(`${projectRoot}/${path}`);
|
|
36
|
+
const projectRelative = relative(projectRoot, absolute).split(sep).join("/");
|
|
37
|
+
if (!isTdkGeneratedPath(projectRelative)) {
|
|
38
|
+
throw new Error(`Refusing to write outside TDK's generated-file allowlist: ${projectRelative}`);
|
|
39
|
+
}
|
|
40
|
+
let cursor = projectRoot;
|
|
41
|
+
for (const segment of projectRelative.split("/")) {
|
|
42
|
+
cursor = normalize(`${cursor}/${segment}`);
|
|
43
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) {
|
|
44
|
+
throw new Error(`Refusing to write through a symbolic link in generated path: ${projectRelative}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return absolute;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=generated-paths.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { delimiter, extname } from "node:path";
|
|
2
|
+
export function isWindows() {
|
|
3
|
+
return process.platform === "win32";
|
|
4
|
+
}
|
|
5
|
+
export function tdkOsFor(platformName) {
|
|
6
|
+
if (platformName === "linux")
|
|
7
|
+
return "linux";
|
|
8
|
+
if (platformName === "darwin")
|
|
9
|
+
return "darwin";
|
|
10
|
+
if (platformName === "win32")
|
|
11
|
+
return "windows";
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export function tdkOs() {
|
|
15
|
+
return tdkOsFor(process.platform);
|
|
16
|
+
}
|
|
17
|
+
export function tdkArchFor(architecture) {
|
|
18
|
+
if (architecture === "x64")
|
|
19
|
+
return "amd64";
|
|
20
|
+
if (architecture === "arm64")
|
|
21
|
+
return "arm64";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export function tdkArch() {
|
|
25
|
+
return tdkArchFor(process.arch);
|
|
26
|
+
}
|
|
27
|
+
export function binaryAssetNameFor(platformName, architecture) {
|
|
28
|
+
const osName = tdkOsFor(platformName);
|
|
29
|
+
const archName = tdkArchFor(architecture);
|
|
30
|
+
if (!osName || !archName)
|
|
31
|
+
return null;
|
|
32
|
+
if (osName === "windows" && archName !== "amd64")
|
|
33
|
+
return null;
|
|
34
|
+
const base = `tdk-${osName}-${archName}`;
|
|
35
|
+
return osName === "windows" ? `${base}.exe` : base;
|
|
36
|
+
}
|
|
37
|
+
export function binaryAssetName() {
|
|
38
|
+
return binaryAssetNameFor(process.platform, process.arch);
|
|
39
|
+
}
|
|
40
|
+
export function executableName() {
|
|
41
|
+
return isWindows() ? "tdk.exe" : "tdk";
|
|
42
|
+
}
|
|
43
|
+
export function pathLookups(binName, windows = isWindows()) {
|
|
44
|
+
if (!windows)
|
|
45
|
+
return [binName];
|
|
46
|
+
if (extname(binName))
|
|
47
|
+
return [binName];
|
|
48
|
+
return [binName, `${binName}.exe`, `${binName}.cmd`, `${binName}.bat`];
|
|
49
|
+
}
|
|
50
|
+
export function pathDelimiter(windows = isWindows()) {
|
|
51
|
+
return windows ? ";" : delimiter;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createConnection } from "node:net";
|
|
1
|
+
import { createConnection, createServer } from "node:net";
|
|
3
2
|
import { PORT_RANGES } from "./constants.js";
|
|
4
3
|
function isPortAvailable(port) {
|
|
5
4
|
return new Promise((resolve) => {
|
|
@@ -20,26 +19,20 @@ function isPortAvailable(port) {
|
|
|
20
19
|
}
|
|
21
20
|
export async function checkPortStatus(port) {
|
|
22
21
|
return new Promise((resolve) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
let hasOutput = false;
|
|
28
|
-
child.stdout?.on("data", () => {
|
|
29
|
-
hasOutput = true;
|
|
30
|
-
});
|
|
31
|
-
child.on("close", (code) => {
|
|
32
|
-
// lsof returns 0 if it found something, 1 if nothing found
|
|
33
|
-
if (code === 0) {
|
|
34
|
-
resolve("running");
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
resolve(hasOutput ? "running" : "stopped");
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
child.on("error", () => {
|
|
22
|
+
const server = createServer();
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
server.close();
|
|
41
25
|
resolve("unknown");
|
|
26
|
+
}, 3000);
|
|
27
|
+
server.once("error", (error) => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
resolve(error.code === "EADDRINUSE" ? "running" : "unknown");
|
|
30
|
+
});
|
|
31
|
+
server.once("listening", () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
server.close(() => resolve("stopped"));
|
|
42
34
|
});
|
|
35
|
+
server.listen(port, "127.0.0.1");
|
|
43
36
|
});
|
|
44
37
|
}
|
|
45
38
|
export async function findAvailablePort(basePort, maxAttempts = 10) {
|