@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.
- package/CHANGELOG.md +8 -0
- package/README.md +39 -206
- package/cli/dist/cli.js +2 -0
- package/cli/dist/commands/completion.js +15 -2
- package/cli/dist/commands/config.js +34 -6
- package/cli/dist/commands/doctor.js +103 -30
- package/cli/dist/commands/help.js +8 -2
- package/cli/dist/commands/networks.js +126 -101
- package/cli/dist/commands/resource.js +4 -1
- package/cli/dist/commands/resources.js +40 -9
- package/cli/dist/commands/runtime.js +74 -0
- package/cli/dist/commands/status.js +65 -7
- package/cli/dist/commands/up.js +12 -10
- package/cli/dist/generator/template-engine.js +29 -17
- package/cli/dist/utils/cold-preflight.js +12 -18
- package/cli/dist/utils/doctor-report.js +64 -0
- package/cli/dist/utils/doctor-runtime.js +4 -4
- package/cli/dist/utils/host-port-config.js +88 -0
- package/cli/dist/utils/host-port-plan.js +74 -0
- package/cli/dist/utils/machine-output.js +24 -0
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/dist/utils/service-urls.js +10 -8
- package/cli/package.json +1 -1
- package/engine/schemas/project-schema.json +142 -0
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +5 -3
- package/engine/topologies/tilt/resources/infra-loader.star +7 -32
- package/package.json +2 -2
|
@@ -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 {
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
() =>
|
|
989
|
-
|
|
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(
|
|
1038
|
-
const checks = inProject ?
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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 (
|
|
1078
|
-
process.exit(
|
|
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("
|
|
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.
|
|
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
|
|
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
|
|
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}"`));
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
240
|
-
console.log(chalk.gray(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|