@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.
- package/CHANGELOG.md +6 -0
- package/README.md +27 -213
- package/cli/dist/commands/config.js +34 -6
- package/cli/dist/commands/doctor.js +57 -23
- package/cli/dist/commands/help.js +3 -2
- package/cli/dist/commands/networks.js +121 -101
- package/cli/dist/commands/resource.js +4 -1
- package/cli/dist/commands/resources.js +40 -9
- package/cli/dist/commands/status.js +65 -7
- package/cli/dist/generator/template-engine.js +27 -15
- package/cli/dist/utils/doctor-report.js +40 -0
- package/cli/dist/utils/host-port-plan.js +62 -0
- package/cli/dist/utils/machine-output.js +24 -0
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/package.json +1 -1
- package/engine/schemas/project-schema.json +142 -0
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
240
|
-
console.log(chalk.gray(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|