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

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,6 +6,7 @@ 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 { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
9
10
  import { findProjectRoot } from "../utils/paths.js";
10
11
  import { checkPortStatus } from "../utils/port-assignment.js";
11
12
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
@@ -156,118 +157,137 @@ export const networksCommand = new Command("networks")
156
157
  .alias("urls")
157
158
  .alias("traefik")
158
159
  .option("-s, --stack <stack>", "Filter by stack name")
159
- .option("--json", "Output as JSON")
160
+ .option("--json", "Output a versioned JSON response")
161
+ .option("--json-legacy", "Output the legacy JSON array (deprecated)")
160
162
  .option("--raw", "Output raw URLs only")
161
163
  .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}"`));
164
+ const action = async () => {
165
+ const projectRoot = options.json || options.jsonLegacy ? findProjectRoot() : requireProjectRoot();
166
+ if (!projectRoot) {
167
+ throw new Error("Could not find project root (no .tdk/project.json found)");
196
168
  }
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"'));
169
+ const discovery = createDiscoveryContext();
170
+ const baseDomain = determineDefaultDomain();
171
+ const bareDomain = baseDomain.replace(/^(app|api)\./, "");
172
+ const appDomain = `app.${bareDomain}`;
173
+ const apiDomain = `api.${bareDomain}`;
174
+ const services = discovery.resources;
175
+ const servicesWithUrls = await Promise.all(services
176
+ .filter((s) => typeof s.config?.basePath === "string")
177
+ .map(async (s) => {
178
+ const basePath = s.config.basePath.replace(/^\//, "");
179
+ const isBackend = s.config?.appType === "backend";
180
+ const host = isBackend ? apiDomain : appDomain;
181
+ const url = `http://${host}/${basePath}`;
182
+ const port = s.config.port;
183
+ const status = await checkServiceStatus(s.name, port, url);
184
+ return {
185
+ name: s.name,
186
+ stack: s.stack,
187
+ basePath: s.config.basePath,
188
+ url,
189
+ ...(process.platform === "win32"
190
+ ? { loopbackUrl: `http://127.0.0.1:80/${basePath}`.replace(/\/$/, "") }
191
+ : {}),
192
+ port,
193
+ status,
194
+ };
195
+ }));
196
+ const filteredServices = options.stack
197
+ ? servicesWithUrls.filter((s) => s.stack === options.stack)
198
+ : servicesWithUrls;
199
+ if (options.json || options.jsonLegacy) {
200
+ const output = options.jsonLegacy
201
+ ? filteredServices
202
+ : createMachineEnvelope({ services: filteredServices });
203
+ console.log(JSON.stringify(output, null, 2));
204
+ return;
201
205
  }
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);
206
+ if (filteredServices.length === 0) {
207
+ if (options.stack) {
208
+ console.log(chalk.yellow(`⚠️ No services with basePath found in stack "${options.stack}"`));
209
+ }
210
+ else {
211
+ console.log(chalk.yellow("⚠️ No services with basePath found"));
212
+ console.log(chalk.gray("\nAdd basePath to your service.json:"));
213
+ console.log(chalk.gray(' "basePath": "/my-service"'));
214
+ }
215
+ process.exit(0);
213
216
  }
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);
217
+ if (options.raw) {
218
+ for (const service of filteredServices) {
219
+ console.log(service.url);
220
+ if (service.loopbackUrl)
221
+ console.log(service.loopbackUrl);
222
+ }
223
+ process.exit(0);
223
224
  }
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) {
225
+ printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}`, DEFAULT_BOX_WIDTH);
226
+ // Group services by stack using discovery context's stack names for consistent ordering
227
+ const stacks = new Map();
228
+ for (const stackName of discovery.stackNames) {
229
+ const stackServices = filteredServices.filter((s) => s.stack === stackName);
230
+ if (stackServices.length > 0) {
231
+ stacks.set(stackName, stackServices);
232
+ }
233
+ }
234
+ // Add unstacked services to 'default' group
235
+ const unstackedServices = filteredServices.filter((s) => !s.stack);
236
+ if (unstackedServices.length > 0) {
237
+ stacks.set("default", unstackedServices);
238
+ }
239
+ let isFirstStack = true;
240
+ for (const [stackName, stackServices] of stacks) {
241
+ if (!isFirstStack) {
242
+ console.log();
243
+ }
244
+ isFirstStack = false;
245
+ const emoji = getStackEmoji(stackName);
246
+ const stackTitle = `${emoji} ${stackName.toUpperCase()} STACK`;
233
247
  console.log();
248
+ console.log(chalk.bold.white(stackTitle));
249
+ console.log(chalk.gray(formatBoxLine("━", DEFAULT_BOX_WIDTH - 4)));
250
+ for (const service of stackServices) {
251
+ const statusSymbol = getStatusIcon(service.status);
252
+ const statusEmoji = colorizeByStatus(statusSymbol, service.status);
253
+ const namePart = formatPadded(service.name, 22);
254
+ const urlPart = service.status === "running"
255
+ ? chalk.cyan.underline(service.url)
256
+ : chalk.gray(service.url); // Gray out URL if stopped
257
+ const statusLabel = service.status !== "running" ? chalk.gray(` [${service.status}]`) : "";
258
+ console.log(` ${statusEmoji} ${chalk.white(namePart)} ${urlPart}${statusLabel}`);
259
+ if (service.loopbackUrl) {
260
+ console.log(` ${chalk.gray(`Loopback: ${service.loopbackUrl} (Host: ${new URL(service.url).hostname})`)}`);
261
+ }
262
+ }
234
263
  }
235
- isFirstStack = false;
236
- const emoji = getStackEmoji(stackName);
237
- const stackTitle = `${emoji} ${stackName.toUpperCase()} STACK`;
238
264
  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
- }
265
+ console.log(chalk.gray(formatBoxLine("─", DEFAULT_BOX_WIDTH - 2)));
266
+ console.log(chalk.gray("🖱️ Click any URL above to open in browser"));
267
+ console.log(chalk.gray("📊 Status: ") +
268
+ chalk.green("✓ Running") +
269
+ " | " +
270
+ chalk.red("✗ Stopped") +
271
+ " | " +
272
+ chalk.gray("? Unknown"));
273
+ if (baseDomain === "localhost") {
274
+ console.log();
275
+ const projectConfig = readProjectConfig(projectRoot);
276
+ const projectName = projectConfig.project.name;
277
+ console.log(chalk.yellow("💡 Tip: Set custom domain with:"));
278
+ console.log(chalk.cyan(` export TDK_PUBLIC_HOST=${projectName}.localhost`));
253
279
  }
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
280
  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`));
281
+ };
282
+ if (options.json || options.jsonLegacy) {
283
+ try {
284
+ await action();
285
+ }
286
+ catch (error) {
287
+ writeMachineError(error);
288
+ }
289
+ return;
270
290
  }
271
- console.log();
291
+ await action();
272
292
  });
273
293
  //# 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
@@ -3,15 +3,17 @@ 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 { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
6
7
  import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
7
8
  export const statusCommand = new Command("status")
8
9
  .description("Show status of resources and stacks")
10
+ .option("--json", "Output a versioned JSON status report", false)
9
11
  .option("-v, --verbose", "Show detailed information", false)
10
12
  .option("--stacks", "Show stack information (default)", true)
11
13
  .option("--resources", "Show all discovered resources", false)
12
14
  .option("--tilt", "Show tilt resource status", false)
13
15
  .action(async (options) => {
14
- await runCommand(async () => {
16
+ const action = async () => {
15
17
  let tiltAvailable = false;
16
18
  try {
17
19
  tiltAvailable = await isTiltAvailable();
@@ -20,13 +22,59 @@ export const statusCommand = new Command("status")
20
22
  // Tilt not available (spawn error)
21
23
  tiltAvailable = false;
22
24
  }
23
- showStep("TDK Status\n");
24
- console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
25
- if (!tiltAvailable) {
26
- showDetail("Install Tilt: https://docs.tilt.dev/install.html");
25
+ if (!options.json) {
26
+ showStep("TDK Status\n");
27
+ console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
28
+ if (!tiltAvailable) {
29
+ showDetail("Install Tilt: https://docs.tilt.dev/install.html");
30
+ }
31
+ console.log();
27
32
  }
28
- console.log();
29
33
  const discovery = createDiscoveryContext();
34
+ let tiltResources = null;
35
+ let queryError = null;
36
+ if (options.json && options.tilt && tiltAvailable) {
37
+ const result = await runTilt("get", ["-f", getTiltfilePath(), "resources", "--output=json"], {
38
+ inheritStdio: false,
39
+ });
40
+ if (result.exitCode === 0) {
41
+ try {
42
+ tiltResources = JSON.parse(result.stdout);
43
+ }
44
+ catch (error) {
45
+ queryError = `Tilt returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`;
46
+ }
47
+ }
48
+ else {
49
+ queryError = result.stderr || "Could not retrieve Tilt resource status";
50
+ }
51
+ }
52
+ if (options.json) {
53
+ const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
54
+ const data = {
55
+ tilt: {
56
+ available: tiltAvailable,
57
+ resourcesQueried: options.tilt && tiltAvailable,
58
+ resources: tiltResources,
59
+ },
60
+ resources: discovery.resources.map((resource) => ({
61
+ name: resource.name,
62
+ stack: resource.stack ?? null,
63
+ type: resource.type ?? "unknown",
64
+ port: resource.port ?? null,
65
+ })),
66
+ stacks: discovery.stacks.map((stack) => ({
67
+ name: stack.name,
68
+ resourceCount: stack.resourceCount,
69
+ })),
70
+ };
71
+ console.log(JSON.stringify(createMachineEnvelope(data, errors)));
72
+ if (errors.length > 0) {
73
+ console.error(queryError);
74
+ process.exit(1);
75
+ }
76
+ return;
77
+ }
30
78
  console.log(chalk.bold("Resources:"), `${discovery.resources.length} discovered`);
31
79
  console.log(chalk.bold("Stacks:"), `${discovery.stacks.length} defined`);
32
80
  if (discovery.stacks.length > 0) {
@@ -66,6 +114,16 @@ export const statusCommand = new Command("status")
66
114
  console.log();
67
115
  showDetail('Run "tdk list-stacks" to see all stacks.');
68
116
  showDetail('Run "tdk up <stack-name>" to start a stack.');
69
- });
117
+ };
118
+ if (options.json) {
119
+ try {
120
+ await action();
121
+ }
122
+ catch (error) {
123
+ writeMachineError(error);
124
+ }
125
+ return;
126
+ }
127
+ await runCommand(action);
70
128
  });
71
129
  //# sourceMappingURL=status.js.map
@@ -603,6 +603,16 @@ async function vendorTdkExtension(projectRoot) {
603
603
  export function verifyMasterConfigs(projectRoot) {
604
604
  const errors = [];
605
605
  const warnings = [];
606
+ const diffs = [];
607
+ const addDrift = (displayPath, actual, expected) => {
608
+ if (actual === expected)
609
+ return;
610
+ errors.push(`${actual === undefined ? "Missing file" : "Out of sync"}: ${displayPath} (run 'tdk config regenerate')`);
611
+ diffs.push({
612
+ file: displayPath,
613
+ diff: createUnifiedDiff(displayPath, actual ?? "", expected),
614
+ });
615
+ };
606
616
  const projectConfig = readProjectConfig(projectRoot);
607
617
  const engine = new TemplateEngine();
608
618
  const expectedFiles = engine.generateAll(projectConfig);
@@ -610,22 +620,10 @@ export function verifyMasterConfigs(projectRoot) {
610
620
  for (const filename of ALL_GENERATED_FILES) {
611
621
  const expectedContent = expectedFiles[filename];
612
622
  const filePath = path.join(outputDir, filename);
613
- if (!fs.existsSync(filePath)) {
614
- errors.push(`Missing file: .tdk/.tdk-out/${filename}`);
615
- continue;
616
- }
617
- const actualContent = fs.readFileSync(filePath, "utf-8");
618
- if (actualContent !== expectedContent) {
619
- errors.push(`Out of sync: .tdk/.tdk-out/${filename} (run 'tdk config regenerate')`);
620
- }
623
+ addDrift(`.tdk/.tdk-out/${filename}`, fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined, expectedContent);
621
624
  }
622
625
  const rootTiltIgnore = path.join(projectRoot, ".tiltignore");
623
- if (!fs.existsSync(rootTiltIgnore)) {
624
- errors.push("Missing file: .tiltignore");
625
- }
626
- else if (fs.readFileSync(rootTiltIgnore, "utf-8") !== expectedFiles[".tiltignore"]) {
627
- errors.push("Out of sync: .tiltignore (run 'tdk config regenerate')");
628
- }
626
+ addDrift(".tiltignore", fs.existsSync(rootTiltIgnore) ? fs.readFileSync(rootTiltIgnore, "utf-8") : undefined, expectedFiles[".tiltignore"]);
629
627
  for (const manifestPath of discoverServiceManifestPaths(projectRoot)) {
630
628
  const displayPath = path.relative(projectRoot, manifestPath).split(path.sep).join("/");
631
629
  const validation = validateServiceManifestFile(manifestPath, displayPath);
@@ -633,7 +631,21 @@ export function verifyMasterConfigs(projectRoot) {
633
631
  warnings.push(...validation.warnings);
634
632
  errors.push(...verifyGeneratedResourceFiles(projectRoot, manifestPath));
635
633
  }
636
- return { valid: errors.length === 0, errors, warnings };
634
+ return { valid: errors.length === 0, errors, warnings, diffs };
635
+ }
636
+ function createUnifiedDiff(file, actual, expected) {
637
+ const oldLines = actual.replace(/\n$/, "").split("\n");
638
+ const newLines = expected.replace(/\n$/, "").split("\n");
639
+ const lines = [
640
+ `--- a/${file}`,
641
+ `+++ b/${file}`,
642
+ `@@ -1,${oldLines.length} +1,${newLines.length} @@`,
643
+ ];
644
+ for (const line of oldLines)
645
+ lines.push(`-${line}`);
646
+ for (const line of newLines)
647
+ lines.push(`+${line}`);
648
+ return lines.join("\n");
637
649
  }
638
650
  function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
639
651
  const serviceDir = path.dirname(serviceJsonPath);
@@ -0,0 +1,40 @@
1
+ /** Wait for concurrent probes to finish even if one throws, so they can release resources. */
2
+ export async function collectDoctorChecks(machineChecks, projectChecks) {
3
+ const checks = [];
4
+ const errors = [];
5
+ const append = (result) => {
6
+ if (result.status === "fulfilled")
7
+ checks.push(result.value);
8
+ else
9
+ errors.push({
10
+ code: "INTERNAL",
11
+ message: result.reason instanceof Error ? result.reason.message : String(result.reason),
12
+ });
13
+ };
14
+ for (const result of await Promise.allSettled(machineChecks.map((check) => Promise.resolve().then(check)))) {
15
+ append(result);
16
+ }
17
+ for (const check of projectChecks) {
18
+ for (const result of await Promise.allSettled([Promise.resolve().then(check)]))
19
+ append(result);
20
+ }
21
+ return { checks, errors };
22
+ }
23
+ /** Produce the same readiness decision for human and machine consumers. */
24
+ export function createDoctorReport(checks, inProject, errors = []) {
25
+ return {
26
+ schemaVersion: 1,
27
+ data: {
28
+ ready: errors.length === 0 &&
29
+ checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
30
+ inProject,
31
+ checks,
32
+ },
33
+ errors,
34
+ };
35
+ }
36
+ /** 0: ready (warnings permitted), 1: blocking findings, 2: usage/internal failure. */
37
+ export function getDoctorExitCode(report) {
38
+ return report.errors.length > 0 ? 2 : report.data.ready ? 0 : 1;
39
+ }
40
+ //# sourceMappingURL=doctor-report.js.map
@@ -0,0 +1,62 @@
1
+ import { createServer } from "node:net";
2
+ const DEFAULT_RANGES = {
3
+ ingressHttp: { start: 8080, end: 8180 },
4
+ ingressHttps: { start: 8443, end: 8543 },
5
+ postgres: { start: 15432, end: 15532 },
6
+ };
7
+ const ENV_KEYS = {
8
+ ingressHttp: "TDK_HTTP_PORT",
9
+ ingressHttps: "TDK_HTTPS_PORT",
10
+ postgres: "TDK_POSTGRES_PORT",
11
+ };
12
+ async function isHostPortAvailable(port) {
13
+ return new Promise((resolve) => {
14
+ const server = createServer();
15
+ server.once("error", () => resolve(false));
16
+ // Docker Desktop and Windows publish these host ports through IPv4 loopback.
17
+ // Match that target instead of probing wildcard binds that may differ by OS.
18
+ server.listen(port, "127.0.0.1", () => server.close(() => resolve(true)));
19
+ });
20
+ }
21
+ function parsePort(name, value) {
22
+ const port = Number(value);
23
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
24
+ throw new Error(`${name} must be an integer between 1 and 65535 (received ${JSON.stringify(value)})`);
25
+ }
26
+ return port;
27
+ }
28
+ export async function createHostPortPlan(options = {}) {
29
+ const env = options.env ?? process.env;
30
+ const isAvailable = options.isAvailable ?? isHostPortAvailable;
31
+ const explicitFlags = { ingressHttp: false, ingressHttps: false, postgres: false };
32
+ const selected = { explicit: explicitFlags };
33
+ const reserved = new Set();
34
+ for (const key of ["ingressHttp", "ingressHttps", "postgres"]) {
35
+ const envValue = env[ENV_KEYS[key]];
36
+ const explicit = envValue !== undefined && envValue.trim() !== "";
37
+ const range = options.ranges?.[key] ?? DEFAULT_RANGES[key];
38
+ const candidates = explicit
39
+ ? [parsePort(ENV_KEYS[key], envValue)]
40
+ : Array.from({ length: range.end - range.start + 1 }, (_, index) => range.start + index);
41
+ let chosen;
42
+ for (const port of candidates) {
43
+ if (reserved.has(port))
44
+ continue;
45
+ if (await isAvailable(port)) {
46
+ chosen = port;
47
+ break;
48
+ }
49
+ if (explicit) {
50
+ throw new Error(`${ENV_KEYS[key]}=${port} is already in use. Choose a free host port and retry.`);
51
+ }
52
+ }
53
+ if (chosen === undefined) {
54
+ throw new Error(`No available host port in ${range.start}-${range.end} for ${key}. Set ${ENV_KEYS[key]} to a free port and retry.`);
55
+ }
56
+ reserved.add(chosen);
57
+ selected[key] = chosen;
58
+ explicitFlags[key] = explicit;
59
+ }
60
+ return selected;
61
+ }
62
+ //# sourceMappingURL=host-port-plan.js.map
@@ -0,0 +1,24 @@
1
+ import { TdkError } from "./errors.js";
2
+ export function createMachineEnvelope(data, errors = []) {
3
+ return { schemaVersion: 1, data, errors };
4
+ }
5
+ export function toMachineError(error) {
6
+ if (error instanceof TdkError) {
7
+ return { error: { code: "COMMAND_FAILED", message: error.message }, exitCode: 1 };
8
+ }
9
+ return {
10
+ error: {
11
+ code: "INTERNAL",
12
+ message: error instanceof Error ? error.message : String(error),
13
+ },
14
+ exitCode: 2,
15
+ };
16
+ }
17
+ /** Keep machine stdout parseable when a command fails before it has data to return. */
18
+ export function writeMachineError(error) {
19
+ const result = toMachineError(error);
20
+ console.log(JSON.stringify(createMachineEnvelope(null, [result.error])));
21
+ console.error(result.error.message);
22
+ process.exit(result.exitCode);
23
+ }
24
+ //# sourceMappingURL=machine-output.js.map