@tdk-landscape/tdk-cli-core 1.3.103 → 1.3.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@tdk-landscape/tdk-cli-core.svg?style=flat&color=blue)](https://www.npmjs.com/package/@tdk-landscape/tdk-cli-core)
4
4
  [![unpacked size](https://img.shields.io/npm/unpacked-size/@tdk-landscape/tdk-cli-core)](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
5
- [![install size](https://packagephobia.com/badge?p=@tdk-landscape/tdk-cli-core@1.3.81)](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core@1.3.81)
5
+ [![install size](https://packagephobia.com/badge?p=@tdk-landscape/tdk-cli-core)](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
6
6
  [![CI](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
7
7
  [![Quickstart E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
8
8
  [![ERP fixture scale E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
@@ -4,6 +4,7 @@ import { dirname, join, normalize, relative } from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { Command } from "commander";
6
6
  import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
7
+ import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_DOCS, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
7
8
  import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constants.js";
8
9
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
9
10
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
@@ -1005,6 +1006,7 @@ export const doctorCommand = new Command("doctor")
1005
1006
  if (!options.json) {
1006
1007
  console.log("TDK doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1007
1008
  }
1009
+ const host = detectHost();
1008
1010
  if (process.platform === "win32") {
1009
1011
  if (options.json) {
1010
1012
  let windowsPortPlan = null;
@@ -1021,12 +1023,27 @@ export const doctorCommand = new Command("doctor")
1021
1023
  message: NATIVE_WINDOWS_DOCTOR_MESSAGE,
1022
1024
  fix: "Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md",
1023
1025
  },
1024
- ], Boolean(findProjectRoot()), [], windowsPortPlan)));
1026
+ ], Boolean(findProjectRoot()), [], windowsPortPlan, { ...host, canUp: false })));
1025
1027
  }
1026
1028
  console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
1027
1029
  process.exit(1);
1028
1030
  return;
1029
1031
  }
1032
+ if (host.kind === "webcontainer") {
1033
+ const report = createDoctorReport([
1034
+ {
1035
+ name: "Host",
1036
+ didPass: false,
1037
+ message: WEBCONTAINER_UP_MESSAGE,
1038
+ fix: `Use a machine with Docker. Guide: ${WEBCONTAINER_DOCS}`,
1039
+ },
1040
+ ], Boolean(findProjectRoot()), [], undefined, host);
1041
+ if (options.json)
1042
+ console.log(JSON.stringify(report));
1043
+ console.error(WEBCONTAINER_UP_MESSAGE);
1044
+ process.exit(1);
1045
+ return;
1046
+ }
1030
1047
  if (!options.json) {
1031
1048
  console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
1032
1049
  console.log("Checking environment...\n");
@@ -1113,7 +1130,14 @@ export const doctorCommand = new Command("doctor")
1113
1130
  // would all fail with "run tdk project".
1114
1131
  const inProject = Boolean(projectRoot);
1115
1132
  const { checks: results, errors } = await collectDoctorChecks(machineChecks, inProject ? projectChecks : []);
1116
- const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan);
1133
+ if (isContainerHost(host.kind)) {
1134
+ for (const result of results) {
1135
+ if (result.name === "Container Runtime" && !result.didPass) {
1136
+ result.fix = DEVCONTAINER_DOCKER_FIX;
1137
+ }
1138
+ }
1139
+ }
1140
+ const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan, host);
1117
1141
  const exitCode = getDoctorExitCode(report);
1118
1142
  const allPassed = report.data.ready;
1119
1143
  if (options.json) {
@@ -2,31 +2,40 @@ import chalk from "chalk";
2
2
  import { Command } from "commander";
3
3
  import { handleDryRun } from "../utils/command-helpers.js";
4
4
  import { handleTiltFailure, withTiltCheck } from "../utils/errors.js";
5
+ import { createJsonEmitter } from "../utils/json-output.js";
5
6
  import { buildTiltDownArgs, runTilt } from "../utils/tilt.js";
6
7
  export const downCommand = new Command("down")
7
8
  .description("Stop all tilt resources")
8
9
  .option("-v, --verbose", "Enable verbose output", false)
9
10
  .option("-f, --force", "Skip confirmation", false)
10
11
  .option("--dry-run", "Show what would be stopped without stopping", false)
12
+ .option("--json", "Print one JSON object on stdout; implies quiet human output", false)
11
13
  .action(async (options) => {
14
+ const emit = options.json ? createJsonEmitter("DOWN_FAILED", "tdk down") : undefined;
12
15
  await withTiltCheck(async () => {
16
+ if (emit && options.dryRun) {
17
+ emit({ ok: true, dryRun: true });
18
+ return;
19
+ }
13
20
  if (handleDryRun(options, "not stopping resources", "tilt down")) {
14
21
  return;
15
22
  }
16
23
  const tiltArgs = buildTiltDownArgs({
17
24
  force: options.force,
18
25
  });
19
- console.log(chalk.blue("Stopping all tilt resources..."));
20
- if (options.verbose) {
26
+ if (!options.json)
27
+ console.log(chalk.blue("Stopping all tilt resources..."));
28
+ if (options.verbose && !options.json) {
21
29
  console.log(chalk.gray("Running: tilt down -f .tdk/.tdk-out/Tiltfile"));
22
30
  }
23
31
  const result = await runTilt("down", tiltArgs, {
24
- verbose: options.verbose,
25
- inheritStdio: true,
32
+ verbose: options.verbose && !options.json,
33
+ inheritStdio: !options.json,
26
34
  });
27
35
  if (result.exitCode !== 0) {
28
36
  handleTiltFailure("down", result.exitCode);
29
37
  }
38
+ emit?.({ ok: true });
30
39
  });
31
40
  });
32
41
  //# sourceMappingURL=down.js.map
@@ -3,7 +3,10 @@ import { Command } from "commander";
3
3
  import { createDiscoveryContext } from "../utils/discovery-context.js";
4
4
  import { runCommand } from "../utils/errors.js";
5
5
  import { formatCount, showDetail, showStep } from "../utils/formatting.js";
6
+ import { getHostPortPlan } from "../utils/host-port-config.js";
6
7
  import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
8
+ import { findProjectRoot } from "../utils/paths.js";
9
+ import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
7
10
  import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
8
11
  export const statusCommand = new Command("status")
9
12
  .description("Show status of resources and stacks")
@@ -51,6 +54,14 @@ export const statusCommand = new Command("status")
51
54
  }
52
55
  }
53
56
  if (options.json) {
57
+ let portPlan = null;
58
+ try {
59
+ const root = findProjectRoot();
60
+ portPlan = root ? await getHostPortPlan(root, { inspectDocker: false }) : null;
61
+ }
62
+ catch {
63
+ // Port planning is best effort; stack ports then list only the Tilt UI.
64
+ }
54
65
  const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
55
66
  const data = {
56
67
  tilt: {
@@ -63,7 +74,9 @@ export const statusCommand = new Command("status")
63
74
  stack: resource.stack ?? null,
64
75
  type: resource.type ?? "unknown",
65
76
  port: resource.port ?? null,
77
+ ...buildServicePorts(resource, portPlan?.ingressHttp),
66
78
  })),
79
+ ports: buildStackPorts(portPlan, process.env.TILT_PORT ? Number.parseInt(process.env.TILT_PORT, 10) : undefined),
67
80
  stacks: discovery.stacks.map((stack) => ({
68
81
  name: stack.name,
69
82
  resourceCount: stack.resourceCount,
@@ -12,7 +12,7 @@ const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderCo
12
12
  const LoadingScreen = ({ progress, message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
13
13
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
14
14
  const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
15
- const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
15
+ const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk project" }), _jsx(Text, { children: " 2. Run: tdk resource api --type backend" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
16
16
  const TUIApp = () => {
17
17
  const { exit } = useApp();
18
18
  const { stdout } = useStdout();
@@ -3,12 +3,14 @@ import { join } from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { Command } from "commander";
5
5
  import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
6
+ import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
6
7
  import { handleDryRun } from "../utils/command-helpers.js";
7
8
  import { completeEnvFile } from "../utils/env-validator.js";
8
9
  import { errorFactories, handleTiltFailure, requireProjectRoot, runCommand, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
9
10
  import { formatCount } from "../utils/formatting.js";
10
11
  import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "../utils/host-port-config.js";
11
12
  import { formatHostPortPlan } from "../utils/host-port-plan.js";
13
+ import { createJsonEmitter } from "../utils/json-output.js";
12
14
  import { findProjectRoot } from "../utils/paths.js";
13
15
  import { findAvailablePort } from "../utils/port-assignment.js";
14
16
  import { isApiServiceType } from "../utils/resource-kind.js";
@@ -17,6 +19,7 @@ import { discoverResources, discoverStacks, getResourcesForStack, stackExists, }
17
19
  import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smoke.js";
18
20
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
19
21
  import { stopTiltOnPort } from "../utils/tilt-process.js";
22
+ import { waitForTiltResourcesReady } from "../utils/up-readiness.js";
20
23
  import { enableDiscoveredStacks } from "./project.js";
21
24
  export function formatUpSuccess(port, appUrls = []) {
22
25
  return [
@@ -34,6 +37,20 @@ export function nativeWindowsUpRefusal(platform, allowNativeWindows) {
34
37
  return null;
35
38
  return "Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.";
36
39
  }
40
+ /** Why `tdk up` cannot work on this host, or null when it may proceed. Docker reachability is only probed in container hosts. */
41
+ export async function hostUpRefusal(host = detectHost(), dockerReachable = defaultDockerReachable) {
42
+ if (host.kind === "webcontainer")
43
+ return WEBCONTAINER_UP_MESSAGE;
44
+ if (isContainerHost(host.kind) && !(await dockerReachable()))
45
+ return DEVCONTAINER_DOCKER_FIX;
46
+ return null;
47
+ }
48
+ async function defaultDockerReachable() {
49
+ const { execFile } = await import("node:child_process");
50
+ return new Promise((resolve) => {
51
+ execFile("docker", ["ps"], { timeout: 10_000 }, (error) => resolve(!error));
52
+ });
53
+ }
37
54
  function waitForTiltUi(port, timeoutMs = 30_000) {
38
55
  return new Promise((resolve) => {
39
56
  const deadline = Date.now() + timeoutMs;
@@ -64,7 +81,16 @@ export const upCommand = new Command("up")
64
81
  .option("-q, --quiet", "Suppress non-essential output", false)
65
82
  .option("--dry-run", "Show what would be started without starting", false)
66
83
  .option("-f, --force", "Kill existing Tilt process before starting", false)
84
+ .option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
67
85
  .action(async (stackName, options) => {
86
+ const emit = options.json ? createJsonEmitter("UP_FAILED", "tdk up") : undefined;
87
+ if (options.json)
88
+ options.quiet = true;
89
+ const hostRefusal = options.dryRun ? null : await hostUpRefusal();
90
+ if (hostRefusal) {
91
+ emit?.({ ok: false }, [{ code: "HOST_UNSUPPORTED", message: hostRefusal }]);
92
+ showErrorAndExit(hostRefusal);
93
+ }
68
94
  const platformRefusal = nativeWindowsUpRefusal(process.platform, process.env.TDK_ALLOW_NATIVE_WINDOWS);
69
95
  if (platformRefusal)
70
96
  showErrorAndExit(platformRefusal);
@@ -152,6 +178,16 @@ export const upCommand = new Command("up")
152
178
  console.log(chalk.blue(formatHostPortPlan(hostPortPlan)));
153
179
  console.log(chalk.gray("Override with TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT."));
154
180
  }
181
+ if (emit && options.dryRun) {
182
+ emit({
183
+ ok: true,
184
+ dryRun: true,
185
+ stack: stackName ?? null,
186
+ services: serviceNames,
187
+ command: dryRunCommand,
188
+ });
189
+ return;
190
+ }
155
191
  if (handleDryRun(options, "not starting services", dryRunCommand)) {
156
192
  return;
157
193
  }
@@ -191,7 +227,7 @@ export const upCommand = new Command("up")
191
227
  console.log(chalk.gray(`Using Tiltfile: .tdk/.tdk-out/Tiltfile`));
192
228
  }
193
229
  const tiltRun = runTilt("up", tiltArgs, {
194
- verbose: options.verbose,
230
+ verbose: options.verbose && !options.json,
195
231
  quiet: options.quiet,
196
232
  inheritStdio: !options.quiet, // Suppress tilt output in quiet mode
197
233
  });
@@ -202,12 +238,29 @@ export const upCommand = new Command("up")
202
238
  let printedSuccess = false;
203
239
  const successOutput = (async () => {
204
240
  const uiReady = await waitForTiltUi(port);
241
+ // JSON consumers get success only after every non-deferred resource is built and running, not when the UI port opens.
242
+ let jsonReady = false;
243
+ if (uiReady && emit) {
244
+ const readiness = await waitForTiltResourcesReady(port);
245
+ if (readiness.ready) {
246
+ jsonReady = true;
247
+ }
248
+ else {
249
+ const detail = readiness.failures.map((f) => `${f.name}: ${f.message}`).join("; ");
250
+ emit({ ok: false, tiltUrl: `http://localhost:${port}`, failures: readiness.failures }, [
251
+ {
252
+ code: readiness.timedOut ? "UP_TIMEOUT" : "UP_RESOURCE_FAILED",
253
+ message: `${readiness.timedOut ? "Timed out waiting for resources" : "Resources failed"}${detail ? ` (${detail})` : ""}. Tilt is still running; inspect it or run: tdk down`,
254
+ },
255
+ ]);
256
+ }
257
+ }
205
258
  if (uiReady && !options.quiet) {
206
259
  for (const line of formatUpSuccess(port))
207
260
  console.log(chalk.blue(line));
208
261
  printedSuccess = true;
209
262
  }
210
- if (uiReady && smokePlans.length > 0) {
263
+ if (uiReady && smokePlans.length > 0 && (!emit || jsonReady)) {
211
264
  if (!options.quiet) {
212
265
  console.log(chalk.gray(`Smoke check: ${smokePlans.map((p) => p.name).join(", ")}`));
213
266
  }
@@ -224,8 +277,20 @@ export const upCommand = new Command("up")
224
277
  console.error(chalk.red(formatSmokeFailure(smokeResult)));
225
278
  }
226
279
  }
227
- if (smokeFailed)
280
+ if (smokeFailed) {
228
281
  stopTiltOnPort(port);
282
+ emit?.({ ok: false }, [
283
+ { code: "UP_SMOKE_FAILED", message: "A smoke check failed; Tilt was stopped" },
284
+ ]);
285
+ }
286
+ }
287
+ if (uiReady && jsonReady && !smokeFailed) {
288
+ emit?.({
289
+ ok: true,
290
+ stack: stackName ?? null,
291
+ services: serviceNames,
292
+ tiltUrl: `http://localhost:${port}`,
293
+ });
229
294
  }
230
295
  })();
231
296
  const result = await tiltRun;
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ export const WEBCONTAINER_DOCS = "docs/agent-hosts.md";
3
+ export const WEBCONTAINER_UP_MESSAGE = `WebContainers cannot run TDK: there is no Docker daemon or Tilt. Run \`tdk up\` on a machine with Docker. See ${WEBCONTAINER_DOCS}.`;
4
+ export const DEVCONTAINER_DOCKER_FIX = `Docker is not reachable from this Dev Container. Mount the host Docker socket (-v /var/run/docker.sock:/var/run/docker.sock) or add the docker-outside-of-docker feature to devcontainer.json, then retry. Docker-in-docker is not supported. See ${WEBCONTAINER_DOCS}.`;
5
+ function defaultInputs() {
6
+ return {
7
+ env: process.env,
8
+ versions: process.versions,
9
+ platform: process.platform,
10
+ fileExists: existsSync,
11
+ };
12
+ }
13
+ /** Classify where the CLI is running. Informational: Docker reachability is still decided by the runtime checks. */
14
+ export function detectHost(inputs = defaultInputs()) {
15
+ const { env, versions, platform, fileExists } = inputs;
16
+ if (versions.webcontainer) {
17
+ return { kind: "webcontainer", canUp: false };
18
+ }
19
+ if (env.CODESPACES === "true")
20
+ return { kind: "codespaces", canUp: true };
21
+ if (env.REMOTE_CONTAINERS || (fileExists("/.dockerenv") && env.DEVCONTAINER)) {
22
+ return { kind: "devcontainer", canUp: true };
23
+ }
24
+ // Mirrors nativeWindowsUpRefusal in commands/up.ts: native Windows is inspect-only unless explicitly overridden.
25
+ if (platform === "win32") {
26
+ return { kind: "native-windows", canUp: env.TDK_ALLOW_NATIVE_WINDOWS === "1" };
27
+ }
28
+ if (platform === "linux" && env.WSL_DISTRO_NAME)
29
+ return { kind: "wsl2", canUp: true };
30
+ return { kind: "local", canUp: true };
31
+ }
32
+ export function isContainerHost(kind) {
33
+ return kind === "devcontainer" || kind === "codespaces";
34
+ }
35
+ //# sourceMappingURL=agent-host.js.map
@@ -20,8 +20,12 @@ export async function collectDoctorChecks(machineChecks, projectChecks) {
20
20
  }
21
21
  return { checks, errors };
22
22
  }
23
+ function containerRuntimeReachable(checks) {
24
+ const runtime = checks.find((check) => check.name === "Container Runtime");
25
+ return runtime ? runtime.didPass : null;
26
+ }
23
27
  /** Produce the same readiness decision for human and machine consumers. */
24
- export function createDoctorReport(checks, inProject, errors = [], portPlan) {
28
+ export function createDoctorReport(checks, inProject, errors = [], portPlan, host) {
25
29
  return {
26
30
  schemaVersion: 1,
27
31
  data: {
@@ -29,6 +33,15 @@ export function createDoctorReport(checks, inProject, errors = [], portPlan) {
29
33
  checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
30
34
  inProject,
31
35
  checks,
36
+ ...(host
37
+ ? {
38
+ host: {
39
+ ...host,
40
+ containerRuntimeReachable: containerRuntimeReachable(checks),
41
+ canUp: host.canUp && containerRuntimeReachable(checks) !== false,
42
+ },
43
+ }
44
+ : {}),
32
45
  ...(portPlan !== undefined
33
46
  ? {
34
47
  ports: {
@@ -0,0 +1,27 @@
1
+ import { writeSync } from "node:fs";
2
+ /**
3
+ * Returns an emitter that writes at most one `{schemaVersion, data, errors}` object to stdout. If the process
4
+ * exits before anything was emitted (a preflight `process.exit`, Tilt ending early), a failure object is written
5
+ * so a JSON consumer never sees an empty stdout. Uses writeSync because `process.exit` does not flush async writes.
6
+ */
7
+ export function createJsonEmitter(failureCode, command) {
8
+ let emitted = false;
9
+ const emit = (data, errors = []) => {
10
+ if (emitted)
11
+ return;
12
+ emitted = true;
13
+ writeSync(1, `${JSON.stringify({ schemaVersion: 1, data, errors })}\n`);
14
+ };
15
+ process.once("exit", (code) => {
16
+ emit({ ok: false }, [
17
+ {
18
+ code: failureCode,
19
+ message: code === 0
20
+ ? `${command} exited before reporting a result`
21
+ : `${command} exited with code ${code}`,
22
+ },
23
+ ]);
24
+ });
25
+ return emit;
26
+ }
27
+ //# sourceMappingURL=json-output.js.map
@@ -19,6 +19,7 @@ const KNOWN_SERVICE_FIELDS = new Set([
19
19
  "backendName",
20
20
  "basePath",
21
21
  "build",
22
+ "buildContext",
22
23
  "databaseName",
23
24
  "dependencies",
24
25
  "dev",
@@ -0,0 +1,32 @@
1
+ import { STANDARD_PORTS } from "./constants.js";
2
+ import { isApiServiceType } from "./resource-kind.js";
3
+ import { resolveServicePath, resolveSubdomainBases } from "./service-urls.js";
4
+ export function buildServicePorts(resource, ingressHttp) {
5
+ const appType = resource.config?.appType;
6
+ const routable = appType === "frontend" || isApiServiceType(appType);
7
+ let url = null;
8
+ if (routable) {
9
+ const { appBase, apiBase } = resolveSubdomainBases(ingressHttp);
10
+ url = `${isApiServiceType(appType) ? apiBase : appBase}${resolveServicePath(resource)}`;
11
+ }
12
+ return { url, containerPort: resource.port ?? resource.config?.port ?? null, hostPort: null };
13
+ }
14
+ /** Ports a client should forward or open: ingress, Tilt UI, and the published datastore. Not one per service. */
15
+ export function buildStackPorts(plan, tiltPort) {
16
+ const ports = [];
17
+ if (plan) {
18
+ ports.push({ name: "ingress-http", hostPort: plan.ingressHttp, purpose: "Traefik HTTP ingress" }, { name: "ingress-https", hostPort: plan.ingressHttps, purpose: "Traefik HTTPS ingress" });
19
+ }
20
+ // `tdk up` may move Tilt off 10350 and records that only in its own process, so this is the default or TILT_PORT.
21
+ const validTiltPort = tiltPort !== undefined && Number.isInteger(tiltPort) && tiltPort > 0;
22
+ ports.push({
23
+ name: "tilt-ui",
24
+ hostPort: validTiltPort ? tiltPort : STANDARD_PORTS.tiltUi,
25
+ purpose: "Tilt UI",
26
+ });
27
+ if (plan) {
28
+ ports.push({ name: "postgres", hostPort: plan.postgres, purpose: "Published Postgres" });
29
+ }
30
+ return ports;
31
+ }
32
+ //# sourceMappingURL=status-ports.js.map
@@ -0,0 +1,48 @@
1
+ import { execFile } from "node:child_process";
2
+ import { getDeferredResourceNames, parseTiltResourceFailures } from "./doctor-runtime.js";
3
+ import { findOnPath } from "./which.js";
4
+ function tiltGetUiResources(port) {
5
+ return new Promise((resolve) => {
6
+ execFile(findOnPath("tilt") ?? "tilt", ["get", "uiresources", "-o", "json", "--port", String(port)], { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error ? null : stdout));
7
+ });
8
+ }
9
+ /**
10
+ * Waits until every non-deferred Tilt resource is built and running. Resolves not-ready as soon as the rest has settled
11
+ * with an errored resource, or at the deadline. Sablier-deferred resources are excluded: they stay idle by design.
12
+ */
13
+ export async function waitForTiltResourcesReady(port, options = {}) {
14
+ const timeoutMs = options.timeoutMs ?? Number(process.env.TDK_UP_READY_TIMEOUT_MS ?? 900_000);
15
+ const intervalMs = options.intervalMs ?? 2_000;
16
+ const fetchJson = options.fetchJson ?? tiltGetUiResources;
17
+ const deferred = options.deferred ?? getDeferredResourceNames();
18
+ const deadline = Date.now() + timeoutMs;
19
+ let last = { ready: false, failures: [], pending: 0, timedOut: false };
20
+ for (;;) {
21
+ const text = await fetchJson(port);
22
+ if (text) {
23
+ try {
24
+ const parsed = parseTiltResourceFailures(text, deferred);
25
+ last = {
26
+ ready: parsed.failures.length === 0 && parsed.pendingCount === 0 && parsed.okCount > 0,
27
+ failures: parsed.failures.map((failure) => ({
28
+ name: failure.name,
29
+ message: failure.error || `${failure.updateStatus}/${failure.runtimeStatus}`,
30
+ })),
31
+ pending: parsed.pendingCount,
32
+ timedOut: false,
33
+ };
34
+ if (last.ready)
35
+ return last;
36
+ if (last.failures.length > 0 && last.pending === 0)
37
+ return last;
38
+ }
39
+ catch {
40
+ /* Tilt may emit partial output while starting; retry. */
41
+ }
42
+ }
43
+ if (Date.now() >= deadline)
44
+ return { ...last, ready: false, timedOut: true };
45
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
46
+ }
47
+ }
48
+ //# sourceMappingURL=up-readiness.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.103",
4
+ "version": "1.3.105",
5
5
  "description": "TDK CLI — start services on your laptop.",
6
6
  "keywords": [
7
7
  "tdk",
@@ -233,7 +233,12 @@ def _apply_manifest_defaults(manifest, resource_path):
233
233
  # Validate custom dockerfile exists if specified
234
234
  if 'dockerfile' in result:
235
235
  custom_dockerfile = result['dockerfile']
236
- dockerfile_path = resource_path + '/' + custom_dockerfile
236
+ # With a bring-your-own buildContext the dockerfile is relative to that context.
237
+ build_context = result.get('buildContext')
238
+ if build_context and build_context != '.':
239
+ dockerfile_path = resource_path + '/' + build_context + '/' + custom_dockerfile
240
+ else:
241
+ dockerfile_path = resource_path + '/' + custom_dockerfile
237
242
  # BUG FIX: Run check from project root since Tilt may run from .tdk/.tdk-out/
238
243
  project_root = os.environ.get('TDK_PROJECT_ROOT', '')
239
244
  if project_root:
@@ -162,6 +162,10 @@
162
162
  "description": "Custom Dockerfile path relative to resource directory. Optional for bring-your-own type.",
163
163
  "type": "string"
164
164
  },
165
+ "buildContext": {
166
+ "description": "Docker build context for a bring-your-own resource, relative to the resource directory. Defaults to the resource directory. When set, dockerfile is relative to this context.",
167
+ "type": "string"
168
+ },
165
169
  "exposeViaProxy": {
166
170
  "description": "Whether this bring-your-own resource receives a Traefik route. Defaults to true.",
167
171
  "type": "boolean"
@@ -207,6 +207,12 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
207
207
  # context instead of the project root used by generated TDK apps.
208
208
  context_prefix = '/'.join(['..'] * (len(resource_path.split('/')) + 1))
209
209
  build_context = context_prefix + '/' + full_resource_path
210
+ # `buildContext` (relative to the service directory) moves the context
211
+ # to the source tree an imported Dockerfile lives in; `dockerfile` is then
212
+ # relative to that context, as in Compose.
213
+ custom_context = manifest.get('buildContext')
214
+ if custom_context and custom_context != '.':
215
+ build_context = build_context + '/' + custom_context
210
216
  dockerfile = manifest.get('dockerfile', 'Dockerfile')
211
217
  if dockerfile.startswith('./'):
212
218
  dockerfile = dockerfile[2:]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.103",
3
+ "version": "1.3.105",
4
4
  "description": "TDK CLI — start services on your laptop.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {