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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +58 -12
  3. package/cli/bin/tdk.js +22 -2
  4. package/cli/dist/cli.js +9 -2
  5. package/cli/dist/commands/completion.js +30 -3
  6. package/cli/dist/commands/config.js +59 -1
  7. package/cli/dist/commands/doctor.js +286 -98
  8. package/cli/dist/commands/eject.js +77 -0
  9. package/cli/dist/commands/networks.js +17 -4
  10. package/cli/dist/commands/project.js +28 -5
  11. package/cli/dist/commands/resource.js +118 -10
  12. package/cli/dist/commands/up.js +93 -21
  13. package/cli/dist/commands/upgrade.js +163 -68
  14. package/cli/dist/generator/template-engine.js +77 -13
  15. package/cli/dist/types/index.js +6 -1
  16. package/cli/dist/utils/cold-preflight.js +198 -0
  17. package/cli/dist/utils/constants.js +4 -0
  18. package/cli/dist/utils/docker.js +15 -1
  19. package/cli/dist/utils/doctor-runtime.js +48 -16
  20. package/cli/dist/utils/doctor-wiring.js +15 -7
  21. package/cli/dist/utils/errors.js +5 -5
  22. package/cli/dist/utils/exec-async.js +30 -0
  23. package/cli/dist/utils/generated-paths.js +49 -0
  24. package/cli/dist/utils/platform.js +53 -0
  25. package/cli/dist/utils/port-assignment.js +13 -20
  26. package/cli/dist/utils/project-templates.js +3 -2
  27. package/cli/dist/utils/resource-features.js +6 -2
  28. package/cli/dist/utils/service-manifest.js +73 -0
  29. package/cli/dist/utils/services.js +8 -1
  30. package/cli/dist/utils/tar.js +27 -5
  31. package/cli/dist/utils/tilt-process.js +53 -0
  32. package/cli/dist/utils/tilt.js +3 -1
  33. package/cli/dist/utils/which.js +26 -0
  34. package/cli/dist/utils/windows-doctor.js +175 -0
  35. package/cli/package.json +1 -1
  36. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
  37. package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
  38. package/cli/templates/Tiltfile.hbs +0 -1
  39. package/cli/templates/spec.master.hbs +1 -2
  40. package/discovery/AGENTS.md +1 -1
  41. package/discovery/constants.star +2 -2
  42. package/discovery/discovery_orchestrator.star +2 -2
  43. package/discovery/manifest/constants.star +1 -1
  44. package/discovery/manifest/loading.star +7 -3
  45. package/discovery/registry.star +17 -3
  46. package/engine/README.md +2 -0
  47. package/engine/schemas/service-schema.json +47 -21
  48. package/engine/scripts/generate-manifest.sh +2 -1
  49. package/engine/topologies/platform/docker/compose/compose.star +37 -5
  50. package/engine/topologies/tilt/common/utils.star +54 -11
  51. package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
  52. package/engine/topologies/tilt/manifest/constants.star +3 -0
  53. package/engine/topologies/tilt/manifest/schema.star +2 -4
  54. package/engine/topologies/tilt/manifest/validator.star +1 -1
  55. package/engine/topologies/tilt/resources/infra-loader.star +14 -3
  56. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
  57. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
  58. package/examples/tdk-example/LICENSE +21 -0
  59. package/examples/tdk-example/README.md +23 -0
  60. package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
  61. package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
  62. package/examples/tdk-example/Tiltfile +25 -0
  63. package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
  64. package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
  65. package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
  66. package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
  67. package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
  68. package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
  69. package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
  70. package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
  71. package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
  72. package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
  73. package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
  74. package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
  75. package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
  76. package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
  77. package/package.json +2 -1
@@ -1,4 +1,4 @@
1
- /** Starter projects `tdk project <name>` can clone. Keep in sync with the public tdk-landscape examples. */
1
+ /** Public starter repositories plus the bundled default product example. */
2
2
  export const PROJECT_TEMPLATES = {
3
3
  restaurant: {
4
4
  repo: "https://github.com/tdk-landscape/tdk-restaurant-example.git",
@@ -22,7 +22,8 @@ export const PROJECT_TEMPLATES = {
22
22
  },
23
23
  example: {
24
24
  repo: "https://github.com/tdk-landscape/tdk-example.git",
25
- description: "Minimal Project-Stack-Resource (PSR) demonstration",
25
+ bundledPath: "examples/tdk-example",
26
+ description: "Hono + Postgres + NATS worker + Vite example with a routed write path",
26
27
  },
27
28
  };
28
29
  //# sourceMappingURL=project-templates.js.map
@@ -150,14 +150,18 @@ export const RESOURCE_FEATURES = {
150
150
  applies_to: ["backend", "frontend"],
151
151
  enabled_by_default: false,
152
152
  category: "premium",
153
- generator_file: "engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star, engine/topologies/platform/docker/compose/traefik_standalone.star (deferStart wake gateway + static route), engine/topologies/platform/docker/compose/compose.star (deferStart depends_on)",
154
- output_file: "(Traefik + Sablier Docker labels on the resource's own compose service; with deferStart, also a generated Traefik file-provider dynamic-config entry and a depends_on block)",
153
+ generator_file: "engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star, engine/topologies/platform/docker/compose/traefik_standalone.star (deferStart wake gateway + static route), engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star (deferred image prebuild + wake route wiring)",
154
+ output_file: "(Traefik + Sablier Docker labels on the resource's own compose service; with deferStart, also a generated Traefik file-provider dynamic-config entry, wake gateway, and image prebuild resource)",
155
155
  },
156
156
  };
157
157
  /**
158
158
  * Get default features for a resource type
159
159
  */
160
160
  export function getDefaultFeaturesForResourceType(appType) {
161
+ // bring-your-own doesn't generate application code, so no features
162
+ if (appType === "bring-your-own") {
163
+ return [];
164
+ }
161
165
  const defaults = [];
162
166
  for (const [key, feature] of Object.entries(RESOURCE_FEATURES)) {
163
167
  if (feature.enabled_by_default &&
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ export const SERVICE_MANIFEST_SCHEMA_VERSION = 1;
3
+ const KNOWN_SERVICE_FIELDS = new Set([
4
+ "$schema",
5
+ "apiBasePath",
6
+ "appName",
7
+ "appType",
8
+ "backendName",
9
+ "basePath",
10
+ "build",
11
+ "databaseName",
12
+ "dependencies",
13
+ "dev",
14
+ "dependsOn",
15
+ "dockerfile",
16
+ "enabled",
17
+ "exposeViaProxy",
18
+ "featuresEnabled",
19
+ "framework",
20
+ "healthCheckPath",
21
+ "image",
22
+ "name",
23
+ "nats",
24
+ "params",
25
+ "playwright",
26
+ "port",
27
+ "replicas",
28
+ "runtime",
29
+ "sablier",
30
+ "schemaVersion",
31
+ "secrets",
32
+ "stack",
33
+ "syncs",
34
+ "traefik",
35
+ "type",
36
+ ]);
37
+ export function validateServiceManifest(value, displayPath) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
39
+ return { errors: [`${displayPath}: expected a JSON object`], warnings: [] };
40
+ }
41
+ const manifest = value;
42
+ const errors = [];
43
+ for (const field of ["appName", "appType", "stack", "schemaVersion"]) {
44
+ if (!(field in manifest))
45
+ errors.push(`${displayPath}.${field}: required field is missing`);
46
+ }
47
+ for (const field of ["appName", "appType", "stack"]) {
48
+ if (field in manifest && typeof manifest[field] !== "string") {
49
+ errors.push(`${displayPath}.${field}: expected a string`);
50
+ }
51
+ }
52
+ if ("schemaVersion" in manifest && manifest.schemaVersion !== SERVICE_MANIFEST_SCHEMA_VERSION) {
53
+ errors.push(`${displayPath}.schemaVersion: unsupported version ${String(manifest.schemaVersion)} (supported: ${SERVICE_MANIFEST_SCHEMA_VERSION})`);
54
+ }
55
+ const warnings = Object.keys(manifest)
56
+ .filter((field) => !KNOWN_SERVICE_FIELDS.has(field))
57
+ .map((field) => `${displayPath}.${field}: unknown field is preserved`);
58
+ return { errors, warnings, manifest };
59
+ }
60
+ export function validateServiceManifestFile(filePath, displayPath) {
61
+ if (!existsSync(filePath)) {
62
+ return { errors: [`${displayPath}: file is missing`], warnings: [] };
63
+ }
64
+ try {
65
+ const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
66
+ return validateServiceManifest(parsed, displayPath);
67
+ }
68
+ catch (error) {
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ return { errors: [`${displayPath}: invalid JSON (${message})`], warnings: [] };
71
+ }
72
+ }
73
+ //# sourceMappingURL=service-manifest.js.map
@@ -4,6 +4,7 @@ import { Cache, createCacheValidator } from "./cache.js";
4
4
  import { AUTOGENERATED_FILE_PATTERNS, SKIP_DIRECTORIES } from "./constants.js";
5
5
  import { formatCount } from "./formatting.js";
6
6
  import { findProjectRoot } from "./paths.js";
7
+ import { validateServiceManifest } from "./service-manifest.js";
7
8
  import { includes } from "./validation.js";
8
9
  const RESOURCE_JSON_FILENAME = "service.json";
9
10
  function findServiceJsonFiles(dir, maxDepth = 5, currentDepth = 0) {
@@ -25,6 +26,9 @@ function findServiceJsonFiles(dir, maxDepth = 5, currentDepth = 0) {
25
26
  }
26
27
  return results;
27
28
  }
29
+ export function discoverServiceManifestPaths(projectRoot) {
30
+ return findServiceJsonFiles(projectRoot);
31
+ }
28
32
  function shouldSkipDirectory(name) {
29
33
  return includes(SKIP_DIRECTORIES, name) || name.startsWith(".");
30
34
  }
@@ -45,6 +49,9 @@ function parseResource(serviceJsonPath) {
45
49
  throw new Error(`Invalid service.json at ${serviceJsonPath}: missing required fields (appName, runtime or appType)`);
46
50
  }
47
51
  const parsedConfig = parsed;
52
+ for (const warning of validateServiceManifest(parsed, serviceJsonPath).warnings) {
53
+ console.warn(`⚠️ Unknown service.json field: ${warning}`);
54
+ }
48
55
  const config = {
49
56
  ...parsedConfig,
50
57
  appType: (parsedConfig.appType ||
@@ -70,7 +77,7 @@ export function discoverResourcesFromRoot(projectRoot) {
70
77
  export function discoverResources() {
71
78
  const projectRoot = findProjectRoot();
72
79
  if (!projectRoot) {
73
- throw new Error("Could not find project root (no Tiltfile found). Make sure you're in a root project.");
80
+ throw new Error("Could not find project root (no .tdk/project.json found). Run `tdk project --yes` first.");
74
81
  }
75
82
  return discoverResourcesFromRoot(projectRoot);
76
83
  }
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  /**
@@ -8,14 +8,36 @@ import { join } from "node:path";
8
8
  * implementation - available by default on macOS and Linux.
9
9
  */
10
10
  export function extractTarball(data, destDir) {
11
- const tmpFile = join(mkdtempSync(join(tmpdir(), "tdk-ext-")), "bundle.tar.gz");
11
+ const tempDir = mkdtempSync(join(tmpdir(), "tdk-ext-"));
12
+ const tmpFile = join(tempDir, "bundle.tar.gz");
12
13
  writeFileSync(tmpFile, data);
13
- const result = spawnSync("tar", ["xzf", tmpFile, "-C", destDir, "--strip-components=1"], {
14
+ mkdirSync(destDir, { recursive: true });
15
+ const tarBin = process.platform === "win32" ? "tar.exe" : "tar";
16
+ const result = spawnSync(tarBin, ["xzf", tmpFile, "-C", destDir, "--strip-components=1"], {
14
17
  stdio: "pipe",
18
+ windowsHide: true,
15
19
  });
16
- rmSync(join(tmpFile, ".."), { recursive: true, force: true });
17
20
  if (result.status !== 0) {
18
- throw new Error(`tar extraction failed: ${result.stderr?.toString() || "unknown error"}`);
21
+ const fallbackDir = join(tempDir, "fallback");
22
+ mkdirSync(fallbackDir, { recursive: true });
23
+ const fallback = spawnSync(tarBin, ["xzf", tmpFile, "-C", fallbackDir], {
24
+ stdio: "pipe",
25
+ windowsHide: true,
26
+ });
27
+ if (fallback.status !== 0) {
28
+ rmSync(tempDir, { recursive: true, force: true });
29
+ throw new Error(`tar extraction failed: ${fallback.stderr?.toString() || result.stderr?.toString() || "unknown error"}`);
30
+ }
31
+ const entries = readdirSync(fallbackDir);
32
+ const sourceDir = entries.length === 1 &&
33
+ existsSync(join(fallbackDir, entries[0])) &&
34
+ statSync(join(fallbackDir, entries[0])).isDirectory()
35
+ ? join(fallbackDir, entries[0])
36
+ : fallbackDir;
37
+ for (const entry of readdirSync(sourceDir)) {
38
+ renameSync(join(sourceDir, entry), join(destDir, entry));
39
+ }
19
40
  }
41
+ rmSync(tempDir, { recursive: true, force: true });
20
42
  }
21
43
  //# sourceMappingURL=tar.js.map
@@ -0,0 +1,53 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const run = (command, args) => execFileSync(command, args, { encoding: "utf-8", stdio: "pipe", windowsHide: true });
3
+ /** Refuse to target arbitrary listeners: only stop a Tilt process on the requested UI port. */
4
+ export function findTiltProcessIdsOnPort(port, platform = process.platform, commandRunner = run) {
5
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
6
+ return [];
7
+ if (platform === "win32") {
8
+ const script = `$port=${port}; Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $p=Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue; if ($p -and $p.ProcessName -eq 'tilt') { $_.OwningProcess } } | Sort-Object -Unique`;
9
+ try {
10
+ return commandRunner("powershell.exe", ["-NoProfile", "-Command", script])
11
+ .split(/\s+/)
12
+ .map(Number)
13
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
14
+ }
15
+ catch {
16
+ throw new Error(`Unable to inspect Tilt listeners on port ${port} with PowerShell.`);
17
+ }
18
+ }
19
+ let pids;
20
+ try {
21
+ pids = commandRunner("lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"])
22
+ .split(/\s+/)
23
+ .map(Number)
24
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
25
+ }
26
+ catch (err) {
27
+ const commandErr = err;
28
+ const stderr = commandErr.stderr?.toString().trim();
29
+ if (commandErr?.status === 1 && !stderr)
30
+ return [];
31
+ throw new Error(`Unable to inspect listeners on port ${port}. Install lsof or resolve the lsof error before using --force.`, { cause: commandErr });
32
+ }
33
+ return pids.filter((pid) => {
34
+ try {
35
+ const name = commandRunner("ps", ["-p", String(pid), "-o", "comm="]).trim();
36
+ return name.split(/[\\/]/).at(-1) === "tilt";
37
+ }
38
+ catch (err) {
39
+ throw new Error(`Unable to identify the process listening on port ${port}.`, { cause: err });
40
+ }
41
+ });
42
+ }
43
+ export function stopTiltOnPort(port, platform = process.platform, commandRunner = run) {
44
+ const pids = findTiltProcessIdsOnPort(port, platform, commandRunner);
45
+ for (const pid of pids) {
46
+ if (platform === "win32")
47
+ commandRunner("taskkill.exe", ["/PID", String(pid), "/F"]);
48
+ else
49
+ commandRunner("kill", ["-TERM", String(pid)]);
50
+ }
51
+ return pids;
52
+ }
53
+ //# sourceMappingURL=tilt-process.js.map
@@ -1,15 +1,17 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { join } from "node:path";
3
3
  import { findProjectRoot } from "./paths.js";
4
+ import { findOnPath } from "./which.js";
4
5
  export function runTilt(command, args = [], options = {}) {
5
6
  return new Promise((resolve, reject) => {
6
7
  const tiltArgs = [command, ...args];
7
8
  if (options.verbose) {
8
9
  console.log(`Executing: tilt ${tiltArgs.join(" ")}`);
9
10
  }
10
- const child = spawn("tilt", tiltArgs, {
11
+ const child = spawn(findOnPath("tilt") ?? "tilt", tiltArgs, {
11
12
  stdio: options.inheritStdio ? "inherit" : "pipe",
12
13
  shell: false,
14
+ windowsHide: process.platform === "win32",
13
15
  timeout: options.timeoutMs,
14
16
  });
15
17
  let stdout = "";
@@ -0,0 +1,26 @@
1
+ import { existsSync } from "node:fs";
2
+ import { delimiter, extname, join } from "node:path";
3
+ import { isWindows, pathLookups } from "./platform.js";
4
+ export function findOnPath(command) {
5
+ const pathEnv = process.env.PATH ?? process.env.Path ?? "";
6
+ const windows = isWindows();
7
+ const names = pathLookups(command, windows);
8
+ if (windows && !extname(command)) {
9
+ for (const extension of (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")) {
10
+ const suffix = extension.toLowerCase();
11
+ if (suffix && !names.includes(`${command}${suffix}`))
12
+ names.push(`${command}${suffix}`);
13
+ }
14
+ }
15
+ for (const dir of pathEnv.split(delimiter)) {
16
+ if (!dir)
17
+ continue;
18
+ for (const name of names) {
19
+ const candidate = join(dir, name);
20
+ if (existsSync(candidate))
21
+ return candidate;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ //# sourceMappingURL=which.js.map
@@ -0,0 +1,175 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { lookup } from "node:dns/promises";
3
+ import { parsePublishedPortHolders, probeHostPort } from "./doctor-runtime.js";
4
+ import { findProjectRoot } from "./paths.js";
5
+ import { getProjectName } from "./service-urls.js";
6
+ import { findOnPath } from "./which.js";
7
+ const WINDOWS_DNS_FAILURE = "Windows did not resolve *.tdk.localhost to 127.0.0.1. Browser URLs with hostnames will fail until you fix hosts or use the 127.0.0.1 URLs printed by tdk networks.";
8
+ function loopbackAddresses(addresses) {
9
+ return (addresses.length > 0 &&
10
+ addresses.every((address) => address === "127.0.0.1" || address === "::1"));
11
+ }
12
+ export function parseExcludedPortRanges(output) {
13
+ return output.split(/\r?\n/).flatMap((line) => {
14
+ const match = line.match(/^\s*(\d+)\s+(\d+)(?:\s|$)/);
15
+ return match ? [[Number(match[1]), Number(match[2])]] : [];
16
+ });
17
+ }
18
+ export function findExternallyOccupiedPorts(occupied, dockerHolders, projectName) {
19
+ const prefix = projectName.replace(/-/g, "_").toLowerCase();
20
+ return occupied.filter((port) => {
21
+ const holder = dockerHolders.find((candidate) => candidate.publishedPorts.includes(port));
22
+ if (!holder || !prefix)
23
+ return true;
24
+ const name = holder.name.toLowerCase();
25
+ return !name.startsWith(`${prefix}_`) && !name.startsWith(`${prefix}-`);
26
+ });
27
+ }
28
+ function checkDockerTool() {
29
+ return findOnPath("docker");
30
+ }
31
+ export function checkWindowsRuntimeTools() {
32
+ const docker = checkDockerTool();
33
+ if (!docker) {
34
+ return {
35
+ name: "Windows Runtime Tools",
36
+ didPass: false,
37
+ message: "Docker is not running. Start Docker Desktop, wait until it is Running, then retry tdk doctor.",
38
+ fix: "Install Docker Desktop and start it in Linux container mode.",
39
+ };
40
+ }
41
+ if (!findOnPath("tilt")) {
42
+ return {
43
+ name: "Windows Runtime Tools",
44
+ didPass: false,
45
+ message: "tilt.exe not found on PATH. Install Tilt from https://docs.tilt.dev/install.html",
46
+ };
47
+ }
48
+ const bun = findOnPath("bun");
49
+ const npmInstall = (process.argv[1] ?? "").toLowerCase().includes("node_modules");
50
+ const node = npmInstall ? findOnPath("node") : true;
51
+ if (!node) {
52
+ return {
53
+ name: "Windows Runtime Tools",
54
+ didPass: false,
55
+ message: "node.exe not found on PATH for this npm installation.",
56
+ };
57
+ }
58
+ return {
59
+ name: "Windows Runtime Tools",
60
+ didPass: true,
61
+ message: bun
62
+ ? `Docker (${docker}), Tilt, and Bun found on PATH`
63
+ : `Docker (${docker}) and Tilt found on PATH; ⚠️ Bun was not found (generated services need Bun)`,
64
+ };
65
+ }
66
+ export function checkWindowsDockerMode() {
67
+ try {
68
+ const osType = execFileSync(findOnPath("docker") ?? "docker", ["info", "--format", "{{.OSType}}"], {
69
+ encoding: "utf-8",
70
+ timeout: 10_000,
71
+ windowsHide: true,
72
+ }).trim();
73
+ if (osType.toLowerCase() === "windows") {
74
+ return {
75
+ name: "Docker container mode",
76
+ didPass: false,
77
+ message: "Docker Desktop is using Windows containers. Switch to Linux containers. TDK stacks are Linux images.",
78
+ fix: "Switch Docker Desktop to Linux containers. TDK does not run Windows containers.",
79
+ };
80
+ }
81
+ return {
82
+ name: "Docker container mode",
83
+ didPass: true,
84
+ message: "Docker Desktop is using Linux containers",
85
+ };
86
+ }
87
+ catch {
88
+ return {
89
+ name: "Docker container mode",
90
+ didPass: false,
91
+ message: "Docker is not running. Start Docker Desktop, wait until it is Running, then retry tdk doctor.",
92
+ };
93
+ }
94
+ }
95
+ export async function checkWindowsHostConfiguration() {
96
+ const warnings = [];
97
+ let excludedRanges = [];
98
+ try {
99
+ const output = execFileSync("netsh", ["interface", "ipv4", "show", "excludedportrange", "protocol=tcp"], { encoding: "utf-8", timeout: 10_000, windowsHide: true });
100
+ excludedRanges = parseExcludedPortRanges(output);
101
+ }
102
+ catch {
103
+ warnings.push("Could not inspect Windows Hyper-V excluded TCP port ranges.");
104
+ }
105
+ const requiredPorts = [80, 443, 5432];
106
+ const excluded = requiredPorts.filter((port) => excludedRanges.some(([start, end]) => port >= start && port <= end));
107
+ if (excluded.length > 0) {
108
+ warnings.push(`⚠️ Windows reserved port${excluded.length > 1 ? "s" : ""} ${excluded.join(", ")} for Hyper-V/WSL. Fix: disable the conflicting Windows feature or change TDK ports in project config. Common fix: run \`net stop winnat\` then \`net start winnat\` in an Administrator terminal, then retry \`tdk doctor\`.`);
109
+ }
110
+ const occupied = [];
111
+ for (const port of requiredPorts) {
112
+ if ((await probeHostPort(port)) === "in-use")
113
+ occupied.push(port);
114
+ }
115
+ let dockerHolders = [];
116
+ try {
117
+ const dockerPs = execFileSync(findOnPath("docker") ?? "docker", ["ps", "--format", "{{.Names}}\\t{{.Ports}}"], { encoding: "utf-8", timeout: 10_000, windowsHide: true });
118
+ dockerHolders = parsePublishedPortHolders(dockerPs, requiredPorts);
119
+ }
120
+ catch {
121
+ // Runtime checks report Docker daemon errors independently.
122
+ }
123
+ const projectName = findProjectRoot() ? getProjectName() : "";
124
+ const externallyOccupied = findExternallyOccupiedPorts(occupied, dockerHolders, projectName);
125
+ if (externallyOccupied.length > 0) {
126
+ try {
127
+ const owners = execFileSync("powershell.exe", [
128
+ "-NoProfile",
129
+ "-Command",
130
+ `$ports = '${externallyOccupied.join(",")}'.Split(','); foreach ($port in $ports) { Get-NetTCPConnection -LocalPort ([int]$port) -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $connection = $_; $service = Get-CimInstance Win32_Service | Where-Object { $_.ProcessId -eq $connection.OwningProcess } | Select-Object -First 1; if ([int]$port -eq 80 -and ($service.Name -eq 'W3SVC' -or $connection.OwningProcess -eq 4)) { '80: IIS / World Wide Web Publishing (HTTP.sys)' } elseif ($service) { '{0}: {1}' -f $port, $service.DisplayName } else { '{0}: PID {1}' -f $port, $connection.OwningProcess } } }`,
131
+ ], { encoding: "utf-8", timeout: 10_000, windowsHide: true }).trim();
132
+ warnings.push(`⚠️ Ports ${externallyOccupied.join(", ")} are already listening${owners ? `:\n${owners}` : "."}`);
133
+ }
134
+ catch {
135
+ warnings.push(`⚠️ Ports ${externallyOccupied.join(", ")} are already in use.`);
136
+ }
137
+ }
138
+ let dnsOk = true;
139
+ for (const host of ["tdk.localhost", "example.tdk.localhost"]) {
140
+ try {
141
+ const records = await lookup(host, { all: true, verbatim: true });
142
+ if (!loopbackAddresses(records.map((record) => record.address)))
143
+ dnsOk = false;
144
+ }
145
+ catch {
146
+ dnsOk = false;
147
+ }
148
+ }
149
+ if (!dnsOk) {
150
+ warnings.push(`${WINDOWS_DNS_FAILURE}\nAdd these lines to C:\\Windows\\System32\\drivers\\etc\\hosts (Administrator Notepad):\n127.0.0.1 tdk.localhost\n127.0.0.1 example.tdk.localhost`);
151
+ }
152
+ try {
153
+ const autocrlf = execFileSync("git", ["config", "core.autocrlf"], {
154
+ encoding: "utf-8",
155
+ timeout: 5_000,
156
+ windowsHide: true,
157
+ }).trim();
158
+ if (autocrlf.toLowerCase() === "true") {
159
+ warnings.push("git core.autocrlf is true; generated files may get CRLF while Docker Linux containers expect LF. Recommended: git config core.autocrlf input in TDK repos.");
160
+ }
161
+ }
162
+ catch {
163
+ // No repository-local setting to report.
164
+ }
165
+ warnings.push("Docker Desktop Settings → Resources → File sharing must include this project directory.");
166
+ return {
167
+ name: "Windows ports and DNS",
168
+ didPass: dnsOk && externallyOccupied.length === 0,
169
+ message: warnings.length > 0
170
+ ? warnings.join("\n")
171
+ : "Required ports are available and TDK localhost DNS resolves to loopback",
172
+ ...(dnsOk ? {} : { fix: WINDOWS_DNS_FAILURE }),
173
+ };
174
+ }
175
+ //# sourceMappingURL=windows-doctor.js.map
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.76",
4
+ "version": "1.3.78",
5
5
  "description": "TDK CLI - Tilt Development Kit command line interface",
6
6
  "keywords": [
7
7
  "tdk",
@@ -6,7 +6,6 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Generated: {{timestamp}}
10
9
  # Run 'tdk project' to update
11
10
  # =============================================================================
12
11
 
@@ -218,4 +217,4 @@ __all__ = [
218
217
  "HEALTH_CHECK_TIMEOUT",
219
218
  "NAMING_PATTERNS",
220
219
  "TRAEFIK_CONFIG",
221
- ]
220
+ ]
@@ -6,7 +6,6 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Generated: {{timestamp}}
10
9
  # Run 'tdk project' to update
11
10
  # =============================================================================
12
11
 
@@ -5,7 +5,6 @@
5
5
  # =============================================================================
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: .tdk/project.json
8
- # Generated: {{timestamp}}
9
8
  # Run 'tdk project' to regenerate
10
9
  # =============================================================================
11
10
 
@@ -6,7 +6,6 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: .tdk/project.json + TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Generated: {{timestamp}}
10
9
  # Run 'tdk project' to update
11
10
  #
12
11
  # 🏗️ ARCHITECTURE: Project → Stack → Service (PSR Model)
@@ -169,7 +168,7 @@ BETA = FOCUS_BETA,
169
168
  # =============================================================================
170
169
  # 📝 CHANGE LOG
171
170
  # =============================================================================
172
- # {{timestamp}}: Generated by TDK CLI v{{version}}
171
+ # Generated by TDK CLI v{{version}}
173
172
  # =============================================================================
174
173
 
175
174
  # =============================================================================
@@ -195,7 +195,7 @@ The discovery system now validates all manifests during the scan process:
195
195
  | Field | Validation |
196
196
  |-------|------------|
197
197
  | `appName` | Required, 3-64 chars, kebab-case (lowercase, numbers, hyphens) |
198
- | `appType` | Required, must be: backend, frontend, library, migrator, sdk, worker |
198
+ | `appType` | Required, must be: backend, frontend, library, migrator, sdk, worker, bring-your-own |
199
199
  | `domain` | Required, must be a valid domain from `constants.star` |
200
200
  | `port` | Required, integer 3000-9999, appType-specific ranges enforced |
201
201
  | `features` | Optional, each must be a valid feature from `constants.star` |
@@ -149,7 +149,7 @@ MAX_SCAN_TIMEOUT_MS = 5000 # 5 seconds per root (warn if exceeded)
149
149
  MAX_MANIFESTS_PER_ROOT = 110 # Warn if root has more manifests
150
150
 
151
151
  # Autogenerated files directory (relative to this file's location)
152
- PRODUCT_SNAPSHOT_DIR = "../../product_autogenerated_resource_manifests"
152
+ PRODUCT_SNAPSHOT_DIR = ".tdk/.tdk-out/product_autogenerated_resource_manifests"
153
153
 
154
154
  PRODUCT_SNAPSHOT_FILES = {
155
155
  "services": "services.autogenerated.star",
@@ -160,6 +160,6 @@ PRODUCT_SNAPSHOT_FILES = {
160
160
  "readme": "README.autogenerated.md",
161
161
  }
162
162
 
163
- PRODUCT_TOPOLOGY_DIR = "../../product_autogenerated_services_list"
163
+ PRODUCT_TOPOLOGY_DIR = ".tdk/.tdk-out/product_autogenerated_services_list"
164
164
  PRODUCT_STACKS_INDEX_FILE = "stacks.autogenerated.star"
165
165
  PRODUCT_STACK_RESOURCES_FILE = "resources.autogenerated.star"
@@ -77,7 +77,7 @@ def _determine_app_type(manifest, resource_path):
77
77
  Determine app type from manifest or service path.
78
78
  """
79
79
  # Check if manifest has explicit appType
80
- if manifest.get("appType") in ["frontend", "backend", "library", "migrator", "sdk", "worker", "infra"]:
80
+ if manifest.get("appType") in ["frontend", "backend", "library", "migrator", "sdk", "worker", "infra", "bring-your-own"]:
81
81
  return manifest["appType"]
82
82
 
83
83
  # Extract from path
@@ -120,7 +120,7 @@ def normalize_manifest(manifest, resource_path):
120
120
 
121
121
  resource = {
122
122
  "name": app_name,
123
- "dockerfile": app_name + "/" + dockerfile,
123
+ "dockerfile": dockerfile if app_type == "bring-your-own" else app_name + "/" + dockerfile,
124
124
  "syncs": syncs,
125
125
  "port": port,
126
126
  "stack": stack,
@@ -28,7 +28,7 @@ MANIFEST_DEFAULTS = {
28
28
  }
29
29
 
30
30
  # Valid app types
31
- VALID_APP_TYPES = ["backend", "frontend", "library", "migrator", "sdk", "worker"]
31
+ VALID_APP_TYPES = ["backend", "frontend", "library", "migrator", "sdk", "worker", "bring-your-own"]
32
32
 
33
33
  # Default syncs
34
34
  DEFAULT_SYNCS = ["src", "prisma"]
@@ -39,7 +39,7 @@ def get_default_syncs_for_type(app_type, features=None):
39
39
  if features == None:
40
40
  features = []
41
41
 
42
- syncs = list(DEFAULT_SYNCS.get(app_type, ['src', 'package.json']))
42
+ syncs = list(DEFAULT_SYNCS.get(app_type, [] if app_type == 'bring-your-own' else ['src', 'package.json']))
43
43
 
44
44
  # Add prisma to syncs if using prisma feature
45
45
  if app_type == 'backend' and 'prisma' in features and 'prisma' not in syncs:
@@ -261,7 +261,11 @@ def _apply_manifest_defaults(manifest, resource_path):
261
261
  features = result.get('featuresEnabled', [])
262
262
  result['usePrisma'] = 'prisma' in features
263
263
  result['useNats'] = 'nats' in features
264
- result['useTraefik'] = 'traefik' in features or app_type in ['backend', 'sdk']
264
+ result['useTraefik'] = (
265
+ 'traefik' in features
266
+ or app_type in ['backend', 'sdk']
267
+ or (app_type == 'bring-your-own' and result.get('exposeViaProxy', True))
268
+ )
265
269
 
266
270
  # Extract stack from path if not set
267
271
  if 'stack' not in result:
@@ -278,7 +282,7 @@ def _apply_manifest_defaults(manifest, resource_path):
278
282
 
279
283
  # 🎯 SMART TRAEFIK CONFIG (Convention over Configuration)
280
284
  # Apply to backends and SDKs that need Traefik routing
281
- if result['useTraefik'] and app_type in ['backend', 'sdk']:
285
+ if result['useTraefik'] and app_type in ['backend', 'sdk', 'bring-your-own']:
282
286
  existing_traefik = result.get('traefik', {})
283
287
  # Use apiPath from root level if available, otherwise use default pattern
284
288
  root_api_path = result.get('apiPath')