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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +58 -12
  3. package/cli/bin/tdk.js +22 -2
  4. package/cli/dist/cli.js +9 -2
  5. package/cli/dist/commands/completion.js +30 -3
  6. package/cli/dist/commands/config.js +59 -1
  7. package/cli/dist/commands/doctor.js +286 -98
  8. package/cli/dist/commands/eject.js +77 -0
  9. package/cli/dist/commands/networks.js +17 -4
  10. package/cli/dist/commands/project.js +28 -5
  11. package/cli/dist/commands/resource.js +118 -10
  12. package/cli/dist/commands/up.js +93 -21
  13. package/cli/dist/commands/upgrade.js +163 -68
  14. package/cli/dist/generator/template-engine.js +77 -13
  15. package/cli/dist/types/index.js +6 -1
  16. package/cli/dist/utils/cold-preflight.js +198 -0
  17. package/cli/dist/utils/constants.js +4 -0
  18. package/cli/dist/utils/docker.js +15 -1
  19. package/cli/dist/utils/doctor-runtime.js +48 -16
  20. package/cli/dist/utils/doctor-wiring.js +15 -7
  21. package/cli/dist/utils/errors.js +5 -5
  22. package/cli/dist/utils/exec-async.js +30 -0
  23. package/cli/dist/utils/generated-paths.js +49 -0
  24. package/cli/dist/utils/platform.js +53 -0
  25. package/cli/dist/utils/port-assignment.js +13 -20
  26. package/cli/dist/utils/project-templates.js +3 -2
  27. package/cli/dist/utils/resource-features.js +6 -2
  28. package/cli/dist/utils/service-manifest.js +73 -0
  29. package/cli/dist/utils/services.js +8 -1
  30. package/cli/dist/utils/tar.js +27 -5
  31. package/cli/dist/utils/tilt-process.js +53 -0
  32. package/cli/dist/utils/tilt.js +3 -1
  33. package/cli/dist/utils/which.js +26 -0
  34. package/cli/dist/utils/windows-doctor.js +175 -0
  35. package/cli/package.json +1 -1
  36. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
  37. package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
  38. package/cli/templates/Tiltfile.hbs +0 -1
  39. package/cli/templates/spec.master.hbs +1 -2
  40. package/discovery/AGENTS.md +1 -1
  41. package/discovery/constants.star +2 -2
  42. package/discovery/discovery_orchestrator.star +2 -2
  43. package/discovery/manifest/constants.star +1 -1
  44. package/discovery/manifest/loading.star +7 -3
  45. package/discovery/registry.star +17 -3
  46. package/engine/README.md +2 -0
  47. package/engine/schemas/service-schema.json +47 -21
  48. package/engine/scripts/generate-manifest.sh +2 -1
  49. package/engine/topologies/platform/docker/compose/compose.star +37 -5
  50. package/engine/topologies/tilt/common/utils.star +54 -11
  51. package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
  52. package/engine/topologies/tilt/manifest/constants.star +3 -0
  53. package/engine/topologies/tilt/manifest/schema.star +2 -4
  54. package/engine/topologies/tilt/manifest/validator.star +1 -1
  55. package/engine/topologies/tilt/resources/infra-loader.star +14 -3
  56. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
  57. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
  58. package/examples/tdk-example/LICENSE +21 -0
  59. package/examples/tdk-example/README.md +23 -0
  60. package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
  61. package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
  62. package/examples/tdk-example/Tiltfile +25 -0
  63. package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
  64. package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
  65. package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
  66. package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
  67. package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
  68. package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
  69. package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
  70. package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
  71. package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
  72. package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
  73. package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
  74. package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
  75. package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
  76. package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
  77. package/package.json +2 -1
@@ -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" },
@@ -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("docker ps --format '{{.Names}}'", {
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 = exec(`curl -fsS -o /dev/null -w '%{http_code}' --max-time 2 ${JSON.stringify(registryUrl)}`, {
250
- stdio: "pipe",
251
- encoding: "utf-8",
252
- timeout: REGISTRY_PROBE_TIMEOUT_MS + 1_000,
253
- }).trim();
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("docker ps --format '{{.Names}}\\t{{.Ports}}'", {
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
- const ports = Object.keys(HOST_PORTS).map(Number);
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("docker ps --format '{{.Names}}\\t{{.Ports}}'", {
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
- problems.push(`${port} (${HOST_PORTS[port]}) is used by a program on this machine`);
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: `Find what holds a port with \`lsof -nP -iTCP:${first} -sTCP:LISTEN\` and stop it ` +
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("tilt get uiresources -o json", {
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("docker ps -a --format '{{.Names}}'", {
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(`docker logs --tail 120 ${JSON.stringify(match)} 2>&1`, {
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("/.tdk/")[0] });
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 = exec("ps -Ao pid=,command=", { encoding: "utf-8", timeout: EXEC_TIMEOUT_MS });
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 = execSync) {
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}`, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
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}`, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
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.
@@ -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 Tiltfile found)", [
67
- "Run this from within a project that has a Tiltfile",
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 Tiltfile found)."));
75
- console.error(chalk.gray("Run this from within a project that has a Tiltfile."));
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 { spawn } from "node:child_process";
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 child = spawn("lsof", ["-Pi", `:${port}`, "-sTCP:LISTEN"], {
24
- timeout: 3000,
25
- stdio: "pipe",
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) {