@tdk-landscape/tdk-cli-core 1.3.79 → 1.3.81

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.
@@ -6,11 +6,14 @@ import { Command } from "commander";
6
6
  import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
7
7
  import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constants.js";
8
8
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
9
- import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
9
+ import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
10
+ import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
10
11
  import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
11
12
  import { validateEnvFile } from "../utils/env-validator.js";
12
13
  import { execAsync, isExecTimeout } from "../utils/exec-async.js";
13
14
  import { formatCount } from "../utils/formatting.js";
15
+ import { getHostPortPlan } from "../utils/host-port-config.js";
16
+ import { createHostPortPlan } from "../utils/host-port-plan.js";
14
17
  import { findProjectRoot } from "../utils/paths.js";
15
18
  import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
16
19
  import { discoverResourcesFromRoot } from "../utils/services.js";
@@ -66,8 +69,8 @@ export function checkWslProjectLocation(projectPath, strict, isWsl = process.pla
66
69
  }
67
70
  /** Failures are shown before passing statuses, with a 5432 conflict first. */
68
71
  export function orderDoctorResults(results) {
69
- const failures = results.filter((result) => !result.didPass && !result.isSkipped);
70
- const remaining = results.filter((result) => result.didPass || result.isSkipped);
72
+ const failures = results.filter((result) => !result.didPass && !result.isSkipped && !result.isWarning);
73
+ const remaining = results.filter((result) => result.didPass || result.isSkipped || result.isWarning);
71
74
  failures.sort((left, right) => {
72
75
  const rank = (result) => result.name === "Host Ports" && /\b5432\b/.test(result.message)
73
76
  ? 0
@@ -953,9 +956,9 @@ const DEFAULT_PING_TIMEOUT_MS = 5000;
953
956
  * it catches the case where a container is running but Traefik never routed
954
957
  * to it, which `docker ps` alone will not show.
955
958
  */
956
- async function checkServiceHealth(timeoutMs) {
959
+ async function checkServiceHealth(timeoutMs, ingressPort) {
957
960
  const projectRoot = findProjectRoot() ?? process.cwd();
958
- const targets = buildHealthTargets(discoverResourcesFromRoot(projectRoot));
961
+ const targets = buildHealthTargets(discoverResourcesFromRoot(projectRoot), ingressPort);
959
962
  if (targets.length === 0) {
960
963
  return {
961
964
  name: "Service Health",
@@ -968,25 +971,93 @@ async function checkServiceHealth(timeoutMs) {
968
971
  }
969
972
  export const doctorCommand = new Command("doctor")
970
973
  .description("Check environment readiness for TDK")
974
+ .option("--json", "Output a versioned JSON readiness report", false)
975
+ .exitOverride((error) => {
976
+ if (error.exitCode === 0)
977
+ process.exit(0);
978
+ if (doctorCommand.opts().json) {
979
+ console.log(JSON.stringify(createDoctorReport([], false, [{ code: "USAGE", message: error.message }])));
980
+ }
981
+ process.exit(2);
982
+ })
971
983
  .option("--no-ping", "Skip pinging running services' /health endpoints")
972
984
  .option("--strict", "Fail warnings such as a WSL project under /mnt/c")
973
985
  .option("--ping-timeout <ms>", "Per-service ping timeout in milliseconds", String(DEFAULT_PING_TIMEOUT_MS))
974
986
  .action(async (options) => {
987
+ // Validate before running any probe; parseInt would silently accept "100ms" or "1.5".
988
+ const pingTimeout = Number(options.pingTimeout);
989
+ if (!/^\d+$/.test(options.pingTimeout) ||
990
+ !Number.isSafeInteger(pingTimeout) ||
991
+ pingTimeout <= 0) {
992
+ const message = "--ping-timeout must be a positive integer number of milliseconds";
993
+ if (options.json)
994
+ console.log(JSON.stringify(createDoctorReport([], false, [{ code: "USAGE", message }])));
995
+ console.error(message);
996
+ process.exit(2);
997
+ return;
998
+ }
999
+ if (!options.json) {
1000
+ console.log("TDK doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1001
+ }
975
1002
  if (process.platform === "win32") {
1003
+ if (options.json) {
1004
+ let windowsPortPlan = null;
1005
+ try {
1006
+ windowsPortPlan = await createHostPortPlan();
1007
+ }
1008
+ catch {
1009
+ /* The native-Windows platform finding remains the primary failure. */
1010
+ }
1011
+ console.log(JSON.stringify(createDoctorReport([
1012
+ {
1013
+ name: "Native Windows support",
1014
+ didPass: false,
1015
+ message: NATIVE_WINDOWS_DOCTOR_MESSAGE,
1016
+ fix: "Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md",
1017
+ },
1018
+ ], Boolean(findProjectRoot()), [], windowsPortPlan)));
1019
+ }
976
1020
  console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
977
1021
  process.exit(1);
978
1022
  return;
979
1023
  }
980
- console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
981
- console.log("Checking environment...\n");
982
- const pingTimeout = Number.parseInt(options.pingTimeout, 10);
983
- if (!Number.isFinite(pingTimeout) || pingTimeout <= 0) {
984
- console.log(`${chalk.red("✗")} --ping-timeout must be a positive number of milliseconds`);
985
- process.exit(1);
1024
+ if (!options.json) {
1025
+ console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
1026
+ console.log("Checking environment...\n");
1027
+ }
1028
+ const projectRoot = findProjectRoot();
1029
+ let hostPortPlan;
1030
+ let portPlanError;
1031
+ try {
1032
+ hostPortPlan = await getHostPortPlan(projectRoot ?? process.cwd());
1033
+ }
1034
+ catch (error) {
1035
+ hostPortPlan = null;
1036
+ portPlanError = error instanceof Error ? error.message : String(error);
986
1037
  }
987
1038
  const machineChecks = [
988
- () => checkHostPorts(),
989
- () => checkIngressPorts(),
1039
+ () => hostPortPlan
1040
+ ? {
1041
+ name: "Host Ports",
1042
+ didPass: true,
1043
+ message: `HTTP ${hostPortPlan.ingressHttp}, HTTPS ${hostPortPlan.ingressHttps}, Postgres ${hostPortPlan.postgres}`,
1044
+ }
1045
+ : {
1046
+ name: "Host Ports",
1047
+ didPass: false,
1048
+ message: portPlanError ?? "Could not select host ports",
1049
+ fix: "Set TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT to available host ports.",
1050
+ },
1051
+ () => hostPortPlan
1052
+ ? checkIngressPorts(execSync, undefined, [
1053
+ hostPortPlan.ingressHttp,
1054
+ hostPortPlan.ingressHttps,
1055
+ ])
1056
+ : {
1057
+ name: "Ingress Ports",
1058
+ didPass: false,
1059
+ message: "Ingress port plan is unavailable",
1060
+ },
990
1061
  checkDockerRuntime,
991
1062
  checkDockerOperatingSystem,
992
1063
  checkTilt,
@@ -1029,26 +1100,28 @@ export const doctorCommand = new Command("doctor")
1029
1100
  ];
1030
1101
  // Runs last: needs routable services (and working Traefik) to mean anything.
1031
1102
  if (options.ping) {
1032
- projectChecks.push(() => checkServiceHealth(pingTimeout));
1103
+ projectChecks.push(() => checkServiceHealth(pingTimeout, hostPortPlan?.ingressHttp));
1033
1104
  }
1034
1105
  // Right after installing, people run `tdk doctor` before they have a
1035
1106
  // project. Only the machine checks mean anything there; the project checks
1036
1107
  // would all fail with "run tdk project".
1037
- const inProject = Boolean(findProjectRoot());
1038
- const checks = inProject ? [...machineChecks, ...projectChecks] : [...machineChecks];
1039
- // Machine checks are independent and mostly wait on child processes, so run
1040
- // them concurrently. Project checks stay sequential; rendering ranks failures first.
1041
- const startedMachineChecks = machineChecks.map((checkFn) => Promise.resolve().then(checkFn));
1042
- const results = [];
1043
- for (const [index, checkFn] of checks.entries()) {
1044
- const result = await (startedMachineChecks[index] ?? checkFn());
1045
- results.push(result);
1046
- }
1047
- // Let machine checks finish before process.exit so their probes can clean up.
1048
- await Promise.allSettled(startedMachineChecks);
1049
- const allPassed = results.every((result) => result.didPass || result.isSkipped);
1108
+ const inProject = Boolean(projectRoot);
1109
+ const { checks: results, errors } = await collectDoctorChecks(machineChecks, inProject ? projectChecks : []);
1110
+ const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan);
1111
+ const exitCode = getDoctorExitCode(report);
1112
+ const allPassed = report.data.ready;
1113
+ if (options.json) {
1114
+ console.log(JSON.stringify(report));
1115
+ for (const error of errors)
1116
+ console.error(error.message);
1117
+ if (exitCode !== 0)
1118
+ process.exit(exitCode);
1119
+ return;
1120
+ }
1121
+ for (const error of errors)
1122
+ console.error(`${chalk.red("✗")} Doctor check failed: ${error.message}`);
1050
1123
  for (const result of orderDoctorResults(results)) {
1051
- if (!result.didPass && !result.isSkipped) {
1124
+ if (!result.didPass && !result.isSkipped && !result.isWarning) {
1052
1125
  console.log(`${chalk.red("✗")} ${result.message}`);
1053
1126
  if (result.fix)
1054
1127
  console.log(`${chalk.blue("ℹ")} Fix: ${result.fix}`);
@@ -1074,7 +1147,7 @@ export const doctorCommand = new Command("doctor")
1074
1147
  }
1075
1148
  const outcome = getDoctorOutcomeMessage(inProject, allPassed);
1076
1149
  console.log(`${allPassed ? chalk.green(chalk.bold("✓")) : chalk.red(chalk.bold("✗"))} ${outcome}`);
1077
- if (!allPassed)
1078
- process.exit(1);
1150
+ if (exitCode !== 0)
1151
+ process.exit(exitCode);
1079
1152
  });
1080
1153
  //# sourceMappingURL=doctor.js.map
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import pkg from "../../package.json" with { type: "json" };
3
3
  const TDK_BANNER = `
4
- ${chalk.bold.cyan("tdk")} ${chalk.gray(`v${pkg.version}`)} ${chalk.white("Tilt Development Kit: run your microservices locally")}
4
+ ${chalk.bold.cyan("tdk")} ${chalk.gray(`v${pkg.version}`)} ${chalk.white("TDK runs many services locally with Docker + Tilt; Helm deploys the cluster")}
5
5
  `;
6
6
  const COMMAND_GROUPS = [
7
7
  {
@@ -63,6 +63,11 @@ const COMMAND_GROUPS = [
63
63
  alias: "regenerate, verify, edit",
64
64
  },
65
65
  { name: "doctor", desc: "Check environment readiness", alias: "" },
66
+ {
67
+ name: "runtime",
68
+ desc: "Inspect packaged engine and template assets",
69
+ alias: "--check-assets",
70
+ },
66
71
  { name: "completion", desc: "Generate shell completions", alias: "--install --shell zsh" },
67
72
  { name: "upgrade, update", desc: "Self-update to latest version", alias: "--force" },
68
73
  { name: "version, -v", desc: "Display version number", alias: "" },
@@ -78,7 +83,8 @@ function formatCommand(name, desc, alias, color) {
78
83
  }
79
84
  export function showHelp() {
80
85
  console.log(TDK_BANNER);
81
- console.log(chalk.bold.white("\n Project → Phase → Stack → Resource\n"));
86
+ console.log(chalk.gray("\n Local development inner loop; TDK does not replace Helm for cluster deployments.\n"));
87
+ console.log(chalk.bold.white(" Project → Phase → Stack → Resource\n"));
82
88
  console.log(chalk.gray(" Usage: tdk [command] [options]\n"));
83
89
  console.log(chalk.bold.yellow(" Global Options:"));
84
90
  console.log(` ${chalk.cyan("-v, --version".padEnd(20))} ${chalk.white("Display version number")}`);
@@ -6,6 +6,9 @@ import { getStackEmoji } from "../utils/constants.js";
6
6
  import { createDiscoveryContext } from "../utils/discovery-context.js";
7
7
  import { logVerbose, requireProjectRoot } from "../utils/errors.js";
8
8
  import { colorizeByStatus, DEFAULT_BOX_WIDTH, formatBoxLine, formatPadded, getStatusIcon, printBoxedHeader, } from "../utils/formatting.js";
9
+ import { readSavedHostPortPlan } from "../utils/host-port-config.js";
10
+ import { createHostPortPlan } from "../utils/host-port-plan.js";
11
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
9
12
  import { findProjectRoot } from "../utils/paths.js";
10
13
  import { checkPortStatus } from "../utils/port-assignment.js";
11
14
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
@@ -156,118 +159,140 @@ export const networksCommand = new Command("networks")
156
159
  .alias("urls")
157
160
  .alias("traefik")
158
161
  .option("-s, --stack <stack>", "Filter by stack name")
159
- .option("--json", "Output as JSON")
162
+ .option("--json", "Output a versioned JSON response")
163
+ .option("--json-legacy", "Output the legacy JSON array (deprecated)")
160
164
  .option("--raw", "Output raw URLs only")
161
165
  .action(async (options) => {
162
- const projectRoot = requireProjectRoot();
163
- const discovery = createDiscoveryContext();
164
- const baseDomain = determineDefaultDomain();
165
- const bareDomain = baseDomain.replace(/^(app|api)\./, "");
166
- const appDomain = `app.${bareDomain}`;
167
- const apiDomain = `api.${bareDomain}`;
168
- const services = discovery.resources;
169
- const servicesWithUrls = await Promise.all(services
170
- .filter((s) => typeof s.config?.basePath === "string")
171
- .map(async (s) => {
172
- const basePath = s.config.basePath.replace(/^\//, "");
173
- const isBackend = s.config?.appType === "backend";
174
- const host = isBackend ? apiDomain : appDomain;
175
- const url = `http://${host}/${basePath}`;
176
- const port = s.config.port;
177
- const status = await checkServiceStatus(s.name, port, url);
178
- return {
179
- name: s.name,
180
- stack: s.stack,
181
- basePath: s.config.basePath,
182
- url,
183
- ...(process.platform === "win32"
184
- ? { loopbackUrl: `http://127.0.0.1:80/${basePath}`.replace(/\/$/, "") }
185
- : {}),
186
- port,
187
- status,
188
- };
189
- }));
190
- const filteredServices = options.stack
191
- ? servicesWithUrls.filter((s) => s.stack === options.stack)
192
- : servicesWithUrls;
193
- if (filteredServices.length === 0) {
194
- if (options.stack) {
195
- console.log(chalk.yellow(`⚠️ No services with basePath found in stack "${options.stack}"`));
166
+ const action = async () => {
167
+ const projectRoot = options.json || options.jsonLegacy ? findProjectRoot() : requireProjectRoot();
168
+ if (!projectRoot) {
169
+ throw new Error("Could not find project root (no .tdk/project.json found)");
196
170
  }
197
- else {
198
- console.log(chalk.yellow("⚠️ No services with basePath found"));
199
- console.log(chalk.gray("\nAdd basePath to your service.json:"));
200
- console.log(chalk.gray(' "basePath": "/my-service"'));
171
+ const savedPlan = readSavedHostPortPlan(projectRoot);
172
+ const hostPortPlan = savedPlan ?? (await createHostPortPlan());
173
+ const httpPort = hostPortPlan.ingressHttp;
174
+ const discovery = createDiscoveryContext();
175
+ const baseDomain = determineDefaultDomain();
176
+ const bareDomain = baseDomain.replace(/^(app|api)\./, "");
177
+ const appDomain = `app.${bareDomain}`;
178
+ const apiDomain = `api.${bareDomain}`;
179
+ const services = discovery.resources;
180
+ const servicesWithUrls = await Promise.all(services
181
+ .filter((s) => typeof s.config?.basePath === "string")
182
+ .map(async (s) => {
183
+ const basePath = s.config.basePath.replace(/^\//, "");
184
+ const isBackend = s.config?.appType === "backend";
185
+ const host = isBackend ? apiDomain : appDomain;
186
+ const url = `http://${host}:${httpPort}/${basePath}`;
187
+ const port = s.config.port;
188
+ const status = await checkServiceStatus(s.name, port, url);
189
+ return {
190
+ name: s.name,
191
+ stack: s.stack,
192
+ basePath: s.config.basePath,
193
+ url,
194
+ ...(process.platform === "win32"
195
+ ? { loopbackUrl: `http://127.0.0.1:${httpPort}/${basePath}`.replace(/\/$/, "") }
196
+ : {}),
197
+ port,
198
+ status,
199
+ };
200
+ }));
201
+ const filteredServices = options.stack
202
+ ? servicesWithUrls.filter((s) => s.stack === options.stack)
203
+ : servicesWithUrls;
204
+ if (options.json || options.jsonLegacy) {
205
+ const output = options.jsonLegacy
206
+ ? filteredServices
207
+ : createMachineEnvelope({ services: filteredServices });
208
+ console.log(JSON.stringify(output, null, 2));
209
+ return;
201
210
  }
202
- process.exit(0);
203
- }
204
- if (options.json) {
205
- console.log(JSON.stringify(filteredServices, null, 2));
206
- process.exit(0);
207
- }
208
- if (options.raw) {
209
- for (const service of filteredServices) {
210
- console.log(service.url);
211
- if (service.loopbackUrl)
212
- console.log(service.loopbackUrl);
211
+ if (filteredServices.length === 0) {
212
+ if (options.stack) {
213
+ console.log(chalk.yellow(`⚠️ No services with basePath found in stack "${options.stack}"`));
214
+ }
215
+ else {
216
+ console.log(chalk.yellow("⚠️ No services with basePath found"));
217
+ console.log(chalk.gray("\nAdd basePath to your service.json:"));
218
+ console.log(chalk.gray(' "basePath": "/my-service"'));
219
+ }
220
+ process.exit(0);
213
221
  }
214
- process.exit(0);
215
- }
216
- printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}`, DEFAULT_BOX_WIDTH);
217
- // Group services by stack using discovery context's stack names for consistent ordering
218
- const stacks = new Map();
219
- for (const stackName of discovery.stackNames) {
220
- const stackServices = filteredServices.filter((s) => s.stack === stackName);
221
- if (stackServices.length > 0) {
222
- stacks.set(stackName, stackServices);
222
+ if (options.raw) {
223
+ for (const service of filteredServices) {
224
+ console.log(service.url);
225
+ if (service.loopbackUrl)
226
+ console.log(service.loopbackUrl);
227
+ }
228
+ process.exit(0);
223
229
  }
224
- }
225
- // Add unstacked services to 'default' group
226
- const unstackedServices = filteredServices.filter((s) => !s.stack);
227
- if (unstackedServices.length > 0) {
228
- stacks.set("default", unstackedServices);
229
- }
230
- let isFirstStack = true;
231
- for (const [stackName, stackServices] of stacks) {
232
- if (!isFirstStack) {
230
+ printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}:${httpPort}`, DEFAULT_BOX_WIDTH);
231
+ // Group services by stack using discovery context's stack names for consistent ordering
232
+ const stacks = new Map();
233
+ for (const stackName of discovery.stackNames) {
234
+ const stackServices = filteredServices.filter((s) => s.stack === stackName);
235
+ if (stackServices.length > 0) {
236
+ stacks.set(stackName, stackServices);
237
+ }
238
+ }
239
+ // Add unstacked services to 'default' group
240
+ const unstackedServices = filteredServices.filter((s) => !s.stack);
241
+ if (unstackedServices.length > 0) {
242
+ stacks.set("default", unstackedServices);
243
+ }
244
+ let isFirstStack = true;
245
+ for (const [stackName, stackServices] of stacks) {
246
+ if (!isFirstStack) {
247
+ console.log();
248
+ }
249
+ isFirstStack = false;
250
+ const emoji = getStackEmoji(stackName);
251
+ const stackTitle = `${emoji} ${stackName.toUpperCase()} STACK`;
233
252
  console.log();
253
+ console.log(chalk.bold.white(stackTitle));
254
+ console.log(chalk.gray(formatBoxLine("━", DEFAULT_BOX_WIDTH - 4)));
255
+ for (const service of stackServices) {
256
+ const statusSymbol = getStatusIcon(service.status);
257
+ const statusEmoji = colorizeByStatus(statusSymbol, service.status);
258
+ const namePart = formatPadded(service.name, 22);
259
+ const urlPart = service.status === "running"
260
+ ? chalk.cyan.underline(service.url)
261
+ : chalk.gray(service.url); // Gray out URL if stopped
262
+ const statusLabel = service.status !== "running" ? chalk.gray(` [${service.status}]`) : "";
263
+ console.log(` ${statusEmoji} ${chalk.white(namePart)} ${urlPart}${statusLabel}`);
264
+ if (service.loopbackUrl) {
265
+ console.log(` ${chalk.gray(`Loopback: ${service.loopbackUrl} (Host: ${new URL(service.url).hostname})`)}`);
266
+ }
267
+ }
234
268
  }
235
- isFirstStack = false;
236
- const emoji = getStackEmoji(stackName);
237
- const stackTitle = `${emoji} ${stackName.toUpperCase()} STACK`;
238
269
  console.log();
239
- console.log(chalk.bold.white(stackTitle));
240
- console.log(chalk.gray(formatBoxLine("━", DEFAULT_BOX_WIDTH - 4)));
241
- for (const service of stackServices) {
242
- const statusSymbol = getStatusIcon(service.status);
243
- const statusEmoji = colorizeByStatus(statusSymbol, service.status);
244
- const namePart = formatPadded(service.name, 22);
245
- const urlPart = service.status === "running"
246
- ? chalk.cyan.underline(service.url)
247
- : chalk.gray(service.url); // Gray out URL if stopped
248
- const statusLabel = service.status !== "running" ? chalk.gray(` [${service.status}]`) : "";
249
- console.log(` ${statusEmoji} ${chalk.white(namePart)} ${urlPart}${statusLabel}`);
250
- if (service.loopbackUrl) {
251
- console.log(` ${chalk.gray(`Loopback: ${service.loopbackUrl} (Host: ${new URL(service.url).hostname})`)}`);
252
- }
270
+ console.log(chalk.gray(formatBoxLine("─", DEFAULT_BOX_WIDTH - 2)));
271
+ console.log(chalk.gray("🖱️ Click any URL above to open in browser"));
272
+ console.log(chalk.gray("📊 Status: ") +
273
+ chalk.green("✓ Running") +
274
+ " | " +
275
+ chalk.red("✗ Stopped") +
276
+ " | " +
277
+ chalk.gray("? Unknown"));
278
+ if (baseDomain === "localhost") {
279
+ console.log();
280
+ const projectConfig = readProjectConfig(projectRoot);
281
+ const projectName = projectConfig.project.name;
282
+ console.log(chalk.yellow("💡 Tip: Set custom domain with:"));
283
+ console.log(chalk.cyan(` export TDK_PUBLIC_HOST=${projectName}.localhost`));
253
284
  }
254
- }
255
- console.log();
256
- console.log(chalk.gray(formatBoxLine("─", DEFAULT_BOX_WIDTH - 2)));
257
- console.log(chalk.gray("🖱️ Click any URL above to open in browser"));
258
- console.log(chalk.gray("📊 Status: ") +
259
- chalk.green("✓ Running") +
260
- " | " +
261
- chalk.red("✗ Stopped") +
262
- " | " +
263
- chalk.gray("? Unknown"));
264
- if (baseDomain === "localhost") {
265
285
  console.log();
266
- const projectConfig = readProjectConfig(projectRoot);
267
- const projectName = projectConfig.project.name;
268
- console.log(chalk.yellow("💡 Tip: Set custom domain with:"));
269
- console.log(chalk.cyan(` export TDK_PUBLIC_HOST=${projectName}.localhost`));
286
+ };
287
+ if (options.json || options.jsonLegacy) {
288
+ try {
289
+ await action();
290
+ }
291
+ catch (error) {
292
+ writeMachineError(error);
293
+ }
294
+ return;
270
295
  }
271
- console.log();
296
+ await action();
272
297
  });
273
298
  //# sourceMappingURL=networks.js.map
@@ -13,7 +13,7 @@ import { showCommandHeader } from "../utils/formatting.js";
13
13
  import { assignPort } from "../utils/port-assignment.js";
14
14
  import { promptSelect, promptText } from "../utils/prompt.js";
15
15
  import { getDefaultFeaturesForResourceType } from "../utils/resource-features.js";
16
- import { SERVICE_MANIFEST_SCHEMA_VERSION } from "../utils/service-manifest.js";
16
+ import { SERVICE_MANIFEST_SCHEMA_URL, SERVICE_MANIFEST_SCHEMA_VERSION, } from "../utils/service-manifest.js";
17
17
  import { discoverResources } from "../utils/services.js";
18
18
  import { createKebabCaseValidator, isPathSafe, validateResourceName } from "../utils/validation.js";
19
19
  export const BASE_TEMPLATE = {
@@ -77,6 +77,7 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
77
77
  }
78
78
  return {
79
79
  ...base,
80
+ $schema: SERVICE_MANIFEST_SCHEMA_URL,
80
81
  schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
81
82
  appName: name,
82
83
  appType: type,
@@ -494,6 +495,7 @@ export const resourceCommand = new Command("resource")
494
495
  // Update with new values while preserving existing fields
495
496
  const updatedServiceJson = {
496
497
  ...existingServiceJson,
498
+ $schema: existingServiceJson.$schema ?? SERVICE_MANIFEST_SCHEMA_URL,
497
499
  schemaVersion: existingServiceJson.schemaVersion ?? SERVICE_MANIFEST_SCHEMA_VERSION,
498
500
  appName: resourceName,
499
501
  appType: resourceType === "sdk" ? "sdk" : existingServiceJson.appType || resourceType,
@@ -525,6 +527,7 @@ export const resourceCommand = new Command("resource")
525
527
  mkdirSync(fullPath, { recursive: true });
526
528
  // Create service.json
527
529
  const byoServiceJson = {
530
+ $schema: SERVICE_MANIFEST_SCHEMA_URL,
528
531
  schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
529
532
  appName: resourceName,
530
533
  appType: "bring-your-own",
@@ -3,35 +3,56 @@ import { Command } from "commander";
3
3
  import { createDiscoveryContext } from "../utils/discovery-context.js";
4
4
  import { requireProjectRoot, runCommand } from "../utils/errors.js";
5
5
  import { formatCount, showAllSatisfyCondition, showDetail, showEmptyState, showStep, } from "../utils/formatting.js";
6
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
7
+ import { findProjectRoot } from "../utils/paths.js";
6
8
  export const resourcesCommand = new Command("resources")
7
9
  .description("List all resources (services) in the project")
8
10
  .option("-v, --verbose", "Show detailed information about each resource", false)
9
11
  .option("-s, --stack <stack>", "Filter resources by stack name")
10
12
  .option("--no-stack", "Show only resources without a stack")
11
13
  .option("--ports", "Show port assignments", false)
14
+ .option("--json", "Output a versioned JSON resource report", false)
12
15
  .action(async (options) => {
13
- await runCommand(async () => {
14
- requireProjectRoot();
15
- const discovery = createDiscoveryContext();
16
- if (discovery.resources.length === 0) {
17
- showEmptyState("resources");
18
- return;
16
+ const action = async () => {
17
+ if (options.json) {
18
+ if (!findProjectRoot()) {
19
+ writeMachineError(new Error("Could not find project root (no .tdk/project.json found)"));
20
+ }
19
21
  }
22
+ else {
23
+ requireProjectRoot();
24
+ }
25
+ const discovery = createDiscoveryContext();
20
26
  let resources = discovery.resources;
21
27
  if (options.stack) {
22
28
  resources = discovery.resourcesByStack.get(options.stack) || [];
23
- if (resources.length === 0) {
29
+ if (resources.length === 0 && !options.json) {
24
30
  showEmptyState("stack-services", ` in stack "${options.stack}"`);
25
31
  return;
26
32
  }
27
33
  }
28
34
  if (options.noStack) {
29
35
  resources = discovery.unassignedResources;
30
- if (resources.length === 0) {
36
+ if (resources.length === 0 && !options.json) {
31
37
  showAllSatisfyCondition("resources", "assigned to a stack");
32
38
  return;
33
39
  }
34
40
  }
41
+ if (options.json) {
42
+ const data = resources.map((resource) => ({
43
+ name: resource.name,
44
+ stack: resource.stack ?? null,
45
+ type: resource.config?.appType ?? resource.type ?? "unknown",
46
+ port: resource.port ?? null,
47
+ path: resource.configPath,
48
+ }));
49
+ console.log(JSON.stringify(createMachineEnvelope({ resources: data })));
50
+ return;
51
+ }
52
+ if (resources.length === 0) {
53
+ showEmptyState(options.stack ? "stack-services" : "resources", options.stack ? ` in stack "${options.stack}"` : "");
54
+ return;
55
+ }
35
56
  showStep(`Found ${formatCount(resources.length, "resource")}:\n`);
36
57
  if (options.verbose || options.ports) {
37
58
  for (const resource of resources) {
@@ -68,6 +89,16 @@ export const resourcesCommand = new Command("resources")
68
89
  console.log(chalk.yellow(`\n${formatCount(withoutStackCount, "resource")} not assigned to any stack.`));
69
90
  showDetail('Run "tdk resources --no-stack" to see them, or "tdk stack" to assign them.');
70
91
  }
71
- });
92
+ };
93
+ if (options.json) {
94
+ try {
95
+ await action();
96
+ }
97
+ catch (error) {
98
+ writeMachineError(error);
99
+ }
100
+ return;
101
+ }
102
+ await runCommand(action);
72
103
  });
73
104
  //# sourceMappingURL=resources.js.map