@tdk-landscape/tdk-cli-core 1.3.143 → 1.3.145

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.
@@ -10,6 +10,7 @@ import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_DOCS
10
10
  import { projectNeedsBun } from "../utils/bun-requirement.js";
11
11
  import { AUTOGENERATED_DIR, CONTAINER_RUNTIME_CHECK, MASTER_CONFIG_FILES, PACKAGE_JSON, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
12
12
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
13
+ import { checkDiskSpace, dockerDesktopStuck } from "../utils/doctor-host.js";
13
14
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
14
15
  import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
15
16
  import { checkStarlarkLoadExports } from "../utils/doctor-starlark.js";
@@ -202,6 +203,10 @@ export async function checkDockerRuntime() {
202
203
  };
203
204
  }
204
205
  }
206
+ // Docker Desktop open but its engine down or stuck: say so instead of "not running".
207
+ const stuck = dockerDesktopStuck(await succeeds("pgrep -f com.docker.backend"));
208
+ if (stuck)
209
+ return stuck;
205
210
  // Docker not running: check for Colima, then Podman
206
211
  if (await succeeds("colima status")) {
207
212
  return {
@@ -1026,6 +1031,7 @@ export const doctorCommand = new Command("doctor")
1026
1031
  message: "Ingress port plan is unavailable",
1027
1032
  },
1028
1033
  checkDockerRuntime,
1034
+ () => checkDiskSpace(),
1029
1035
  checkDockerOperatingSystem,
1030
1036
  checkTilt,
1031
1037
  checkDockerCompose,
@@ -11,7 +11,8 @@ import { createMachineEnvelope, writeMachineError } from "../utils/machine-outpu
11
11
  import { findProjectRoot } from "../utils/paths.js";
12
12
  import { deriveServiceStates } from "../utils/service-runtime-state.js";
13
13
  import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
14
- import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
14
+ import { isTiltAvailable, runTilt } from "../utils/tilt.js";
15
+ import { getTiltPollingPort } from "../utils/tilt-startup.js";
15
16
  import { evaluateTiltReadiness, tiltGetUiResources } from "../utils/up-readiness.js";
16
17
  export const statusCommand = new Command("status")
17
18
  .description("Show status of resources and stacks")
@@ -42,30 +43,28 @@ export const statusCommand = new Command("status")
42
43
  }
43
44
  let tiltResources = null;
44
45
  let queryError = null;
45
- if (options.json && options.tilt && tiltAvailable) {
46
- const result = await runTilt("get", ["-f", getTiltfilePath(), "resources", "--output=json"], {
47
- inheritStdio: false,
48
- });
49
- if (result.exitCode === 0) {
50
- try {
51
- tiltResources = JSON.parse(result.stdout);
52
- }
53
- catch (error) {
54
- queryError = `Tilt returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`;
55
- }
56
- }
57
- else {
58
- queryError = result.stderr || "Could not retrieve Tilt resource status";
59
- }
60
- }
61
46
  // A single look at the running Tilt, so a caller that started `tdk up` detached can poll for readiness. Null when no
62
47
  // Tilt answers or its output cannot be read.
63
48
  let readiness = null;
64
49
  // The same look at Tilt feeds readiness and each service's state, so `tdk status` and `tdk ui` cannot disagree.
65
50
  let serviceStates = null;
51
+ const tiltPort = getTiltPollingPort(process.env.TILT_PORT);
66
52
  if (tiltAvailable) {
67
- const port = Number.parseInt(process.env.TILT_PORT ?? "", 10);
68
- const text = await tiltGetUiResources(Number.isInteger(port) ? port : 10350);
53
+ const text = await tiltGetUiResources(tiltPort);
54
+ // `tilt get` has no -f flag; resources come from the running Tilt's API on its port, the same source as readiness.
55
+ if (options.json && options.tilt) {
56
+ if (text) {
57
+ try {
58
+ tiltResources = JSON.parse(text);
59
+ }
60
+ catch (error) {
61
+ queryError = `Tilt returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`;
62
+ }
63
+ }
64
+ else {
65
+ queryError = `No Tilt is answering on port ${tiltPort}. Start it with: tdk up`;
66
+ }
67
+ }
69
68
  if (text) {
70
69
  const deferred = getDeferredResourceNames();
71
70
  try {
@@ -115,7 +114,7 @@ export const statusCommand = new Command("status")
115
114
  ? { blockedBy: serviceStates[resource.name]?.blockedBy }
116
115
  : {}),
117
116
  })),
118
- ports: buildStackPorts(portPlan, process.env.TILT_PORT ? Number.parseInt(process.env.TILT_PORT, 10) : undefined),
117
+ ports: buildStackPorts(portPlan, tiltPort),
119
118
  stacks: discovery.stacks.map((stack) => ({
120
119
  name: stack.name,
121
120
  resourceCount: stack.resourceCount,
@@ -170,15 +169,14 @@ export const statusCommand = new Command("status")
170
169
  if (options.tilt && tiltAvailable) {
171
170
  console.log();
172
171
  showStep("Tilt Resources:");
173
- const tiltfilePath = getTiltfilePath();
174
- const result = await runTilt("get", ["-f", tiltfilePath, "resources"], {
172
+ const result = await runTilt("get", ["uiresources", "--port", String(tiltPort)], {
175
173
  inheritStdio: false,
176
174
  });
177
175
  if (result.exitCode === 0) {
178
176
  console.log(result.stdout || chalk.gray(" No active tilt resources"));
179
177
  }
180
178
  else {
181
- showDetail("Could not retrieve tilt resource status");
179
+ showDetail(`No Tilt is answering on port ${tiltPort}. Start it with: tdk up`);
182
180
  }
183
181
  }
184
182
  console.log();
@@ -16,6 +16,7 @@ import { createStatusMessageController } from "../utils/status-message.js";
16
16
  import { getListRowFromMouseY, getTerminalRuleWidth } from "../utils/terminal-layout.js";
17
17
  import { isTiltAvailable } from "../utils/tilt.js";
18
18
  import { loadTiltEvents, stripTerminalControls, TiltEventsLoadError, } from "../utils/tilt-events.js";
19
+ import { getTiltPollingPort } from "../utils/tilt-startup.js";
19
20
  import { formatUiKeyHint, formatUiKeyNames, UI_KEYMAP } from "../utils/ui-keymap.js";
20
21
  import { applyServiceStates, fetchServiceStates, } from "../utils/ui-service-state.js";
21
22
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
@@ -116,14 +117,14 @@ export const TUIApp = ({ animated = true }) => {
116
117
  const [serviceStates, setServiceStates] = useState({});
117
118
  useEffect(() => {
118
119
  let cancelled = false;
119
- const tiltPort = Number.parseInt(process.env.TILT_PORT ?? "", 10);
120
+ const tiltPort = getTiltPollingPort(process.env.TILT_PORT);
120
121
  // A Tilt call can outlast the 5 s interval; never start a poll while the last one is still running.
121
122
  const run = singleFlight((next) => {
122
123
  if (cancelled)
123
124
  return;
124
125
  setServiceStates((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
125
126
  });
126
- const poll = () => run(() => fetchServiceStates(services, Number.isInteger(tiltPort) ? tiltPort : 10350));
127
+ const poll = () => run(() => fetchServiceStates(services, tiltPort));
127
128
  void poll();
128
129
  const timer = setInterval(() => void poll(), 5000);
129
130
  return () => {
@@ -1,10 +1,11 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
+ import { existsSync } from "node:fs";
3
4
  import { connect } from "node:net";
4
5
  import { join } from "node:path";
5
6
  import chalk from "chalk";
6
7
  import { Command } from "commander";
7
- import { ensureProjectRuntimeAssets, ProjectConfigNotFoundError, verifyMasterConfigs, } from "../generator/template-engine.js";
8
+ import { ensureProjectRuntimeAssets, generateMasterConfigs, ProjectConfigNotFoundError, verifyMasterConfigs, } from "../generator/template-engine.js";
8
9
  import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
9
10
  import { handleDryRun } from "../utils/command-helpers.js";
10
11
  import { STANDARD_PORTS } from "../utils/constants.js";
@@ -256,6 +257,14 @@ export const upCommand = new Command("up")
256
257
  console.log(chalk.gray(`Added ${addedEnvKeys.join(", ")} to .env (generated; existing values unchanged)`));
257
258
  }
258
259
  }
260
+ // A project made by `tdk import` has .tdk/project.json but no generated files yet. Generate them rather than letting
261
+ // Tilt fail on a missing Tiltfile.
262
+ if (foundRoot && !existsSync(join(projectRoot, ".tdk", ".tdk-out", "Tiltfile"))) {
263
+ if (!options.quiet) {
264
+ console.log(chalk.gray("No generated Tiltfile yet; generating .tdk/.tdk-out/ ..."));
265
+ }
266
+ await generateMasterConfigs(projectRoot, { quiet: options.quiet });
267
+ }
259
268
  const copiedAssets = ensureProjectRuntimeAssets(projectRoot);
260
269
  if (copiedAssets.length > 0 && options.verbose && !options.quiet) {
261
270
  console.log(chalk.gray(`Refreshed runtime assets: ${copiedAssets.join(", ")}`));
@@ -428,7 +428,7 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
428
428
  throw new Error(`Refusing to generate config from unsafe service.json values:\n${unsafeFields.map((error) => ` ${error}`).join("\n")}`);
429
429
  }
430
430
  await warnIfSablierUnlicensed(projectRoot);
431
- if (ensureRootWorkspaceManifest(projectRoot, projectConfig)) {
431
+ if (ensureRootWorkspaceManifest(projectRoot, projectConfig) && !options.quiet) {
432
432
  console.log("✓ Generated: package.json (workspace root)");
433
433
  }
434
434
  // Verdaccio is Premium, but unlike the old posture here, this is NOT the
@@ -461,14 +461,17 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
461
461
  const content = files[filename];
462
462
  const filePath = assertTdkGeneratedPath(projectRoot, path.join(".tdk", ".tdk-out", filename));
463
463
  writeTextFileAtomic(filePath, content);
464
- console.log(`✓ Generated: .tdk/.tdk-out/${filename}`);
464
+ if (!options.quiet)
465
+ console.log(`✓ Generated: .tdk/.tdk-out/${filename}`);
465
466
  }
466
467
  if (isStackFeatureEnabledInStacks(projectConfig.phases, DATABASE_MANAGEMENT_FEATURE)) {
467
468
  const composePath = assertTdkGeneratedPath(projectRoot, path.join("services", "platform", DATABASE_MANAGEMENT_FEATURE, DOCKER_COMPOSE_FILE));
468
469
  const composeDir = path.dirname(composePath);
469
470
  fs.mkdirSync(composeDir, { recursive: true });
470
471
  writeTextFileAtomic(composePath, generateDatabaseManagementCompose(projectConfig));
471
- console.log("✓ Generated: services/platform/database-management/docker-compose.yml");
472
+ if (!options.quiet) {
473
+ console.log("✓ Generated: services/platform/database-management/docker-compose.yml");
474
+ }
472
475
  }
473
476
  // Copy .tiltignore to project root so Tilt uses it
474
477
  const tiltignoreSource = path.join(outputDir, TILTIGNORE_FILE);
@@ -0,0 +1,66 @@
1
+ // Copyright (c) 2026 TDK Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { statfsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ export const DISK_SPACE_CHECK = "Disk Space";
6
+ const GIB = 1024 ** 3;
7
+ // Docker Desktop keeps images and containers in one large disk image. When the host disk fills up, the engine
8
+ // stops answering while the Docker Desktop app keeps running, so `tdk up` waits with no visible error.
9
+ export const DISK_FAIL_GIB = 2;
10
+ export const DISK_WARN_GIB = 10;
11
+ export const DISK_CLEANUP_FIX = [
12
+ "Free disk space, then retry: tdk doctor",
13
+ " docker system prune -af # unused images and build cache (needs Docker running)",
14
+ " npm cache clean --force",
15
+ " bun pm cache rm",
16
+ " brew cleanup -s",
17
+ ].join("\n");
18
+ export function checkDiskSpace(statfs = statfsSync, path = homedir()) {
19
+ let freeGib;
20
+ try {
21
+ const stats = statfs(path);
22
+ freeGib = (Number(stats.bavail) * Number(stats.bsize)) / GIB;
23
+ }
24
+ catch {
25
+ return {
26
+ name: DISK_SPACE_CHECK,
27
+ didPass: true,
28
+ isSkipped: true,
29
+ message: "Could not read free disk space",
30
+ };
31
+ }
32
+ const shown = `${freeGib.toFixed(1)} GB free`;
33
+ if (freeGib < DISK_FAIL_GIB) {
34
+ return {
35
+ name: DISK_SPACE_CHECK,
36
+ didPass: false,
37
+ message: `${shown}: Docker cannot build or start containers`,
38
+ fix: DISK_CLEANUP_FIX,
39
+ };
40
+ }
41
+ if (freeGib < DISK_WARN_GIB) {
42
+ return {
43
+ name: DISK_SPACE_CHECK,
44
+ didPass: false,
45
+ isWarning: true,
46
+ message: `${shown}: image builds may fail`,
47
+ fix: DISK_CLEANUP_FIX,
48
+ };
49
+ }
50
+ return { name: DISK_SPACE_CHECK, didPass: true, message: shown };
51
+ }
52
+ export const DOCKER_DESKTOP_STUCK_FIX = "Quit Docker Desktop completely (whale icon > Quit Docker Desktop), reopen it, and wait until it shows Running. If it keeps getting stuck, check free disk space. Then retry: tdk doctor";
53
+ /**
54
+ * Docker Desktop's app process is up but `docker ps` fails: the engine is stopped or stuck, which needs a different fix
55
+ * than "start Docker". Returns the failing check, or null when that is not the situation.
56
+ */
57
+ export function dockerDesktopStuck(desktopRunning, platform = process.platform) {
58
+ if (platform !== "darwin" || !desktopRunning)
59
+ return null;
60
+ return {
61
+ name: "Container Runtime",
62
+ didPass: false,
63
+ message: "Docker Desktop is open, but its engine is not responding",
64
+ fix: DOCKER_DESKTOP_STUCK_FIX,
65
+ };
66
+ }
@@ -116,7 +116,12 @@ function readPath(value, path) {
116
116
  return undefined;
117
117
  if (!Object.hasOwn(current, part))
118
118
  return undefined;
119
- current = current[part];
119
+ // Resolve only own data properties. Besides avoiding inherited properties, this
120
+ // avoids invoking accessors on values supplied by smoke-test responses.
121
+ const descriptor = Object.getOwnPropertyDescriptor(current, part);
122
+ if (!descriptor || !("value" in descriptor))
123
+ return undefined;
124
+ current = descriptor.value;
120
125
  }
121
126
  return current;
122
127
  }
@@ -1,11 +1,12 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
+ import { STANDARD_PORTS } from "./constants.js";
3
4
  import { isValidPort } from "./validation.js";
4
5
  export function parseTiltPort(value) {
5
6
  if (value === undefined || value === "")
6
7
  return { ok: true, port: undefined };
7
8
  const port = Number(value);
8
- if (!/^[0-9]+$/.test(value) || !isValidPort(port)) {
9
+ if (/[^0-9]/.test(value) || !isValidPort(port)) {
9
10
  return {
10
11
  ok: false,
11
12
  message: `Invalid TILT_PORT "${value}": expected an integer from 1 to 65535.`,
@@ -13,6 +14,11 @@ export function parseTiltPort(value) {
13
14
  }
14
15
  return { ok: true, port };
15
16
  }
17
+ /** Resolve the port for read-only polling, falling back when TILT_PORT is absent or invalid. */
18
+ export function getTiltPollingPort(value) {
19
+ const result = parseTiltPort(value);
20
+ return result.ok ? (result.port ?? STANDARD_PORTS.tiltUi) : STANDARD_PORTS.tiltUi;
21
+ }
16
22
  export function secondUpAction(input) {
17
23
  if (input.runningPorts.length === 0 || input.force)
18
24
  return { action: "start" };