@tdk-landscape/tdk-cli-core 1.3.121 → 1.3.123
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 +1 -0
- package/README.md +13 -8
- package/cli/dist/cli.js +1 -1
- package/cli/dist/commands/doctor.js +64 -7
- package/cli/dist/commands/help.js +17 -1
- package/cli/dist/commands/logs.js +28 -7
- package/cli/dist/commands/stack.js +94 -22
- package/cli/dist/commands/ui.js +81 -6
- package/cli/dist/commands/up.js +17 -3
- package/cli/dist/components/TabBar.js +3 -2
- package/cli/dist/generator/__tests__/golden-helpers.js +42 -0
- package/cli/dist/generator/template-engine.js +1 -1
- package/cli/dist/utils/bun-floor.js +8 -0
- package/cli/dist/utils/cold-preflight.js +6 -4
- package/cli/dist/utils/doctor-runtime.js +4 -1
- package/cli/dist/utils/doctor-wiring.js +62 -1
- package/cli/dist/utils/host-port-plan.js +3 -0
- package/cli/dist/utils/port-conflict-fix.js +24 -0
- package/cli/dist/utils/port-listener-probe.js +19 -0
- package/cli/dist/utils/secret-redaction.js +55 -0
- package/cli/dist/utils/service-config-checks.js +188 -0
- package/cli/dist/utils/startup-report.js +6 -1
- package/cli/dist/utils/tilt-events.js +195 -0
- package/cli/dist/utils/tilt-unreachable-message.js +8 -0
- package/cli/package.json +1 -1
- package/cli/templates/TILT_TECH_STACK.star.hbs +4 -4
- package/engine/topologies/platform/docker/compose/compose.star +1 -86
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +4 -4
- package/engine/topologies/platform/docker/networking/traefik.star +40 -7
- package/engine/topologies/platform/docker/networking/traefik_helpers.star +1 -1
- package/engine/topologies/tilt/resources/infra-loader.star +3 -2
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `tdk stack` can assign named resources in scripts with `--resources` (space- or comma-separated) and skip confirmation with `--yes`. Unknown names list valid resources, and non-TTY assignments without explicit resource names fail with a command hint. Invalid JSON encountered while writing now names the affected `service.json`.
|
|
7
8
|
- `tdk eject` no longer claims files it does not create or prints a command that cannot work. It still writes only `EJECTED.md` (no file is copied, moved or generated; the Tiltfile and engine stay in git-ignored `.tdk/.tdk-out/`, and the per-service Compose files, golden-layer Dockerfile and Traefik file only exist after Tilt has run). `EJECTED.md` no longer lists them under "Keep" and says the Tiltfile still reads `.tdk/project.json` and the `service.json` files. `Next: tilt up` (which finds no Tiltfile at the project root) is replaced by `tilt up -f .tdk/.tdk-out/Tiltfile -- --focus=<stack>`, the form `tdk up` runs. A project that already has an `EJECTED.md` keeps its old text (the file is never overwritten) and is now told so. `--dry-run` headings read `Generated files, left where they are (git-ignored):` and `Files written:`. A command that exports plain Compose files and Dockerfiles does not exist. Fixes #610.
|
|
8
9
|
- `tdk config regenerate` now restores a root `.tiltignore` that TDK wrote (it carries the `SYSTEM-GENERATED BY TDK CLI` header) and that was edited by hand, and prints `Restored: .tiltignore → project root (hand edits discarded)`. Before, it only copied the file when none existed, so `tdk config verify` kept failing with `Out of sync: .tiltignore (run 'tdk config regenerate')` after the advice was followed. A root `.tiltignore` without the header is still never overwritten, and plain `tdk project --yes` still does not overwrite an existing one. Docs: `docs/adopt-tdk.md` now quotes the current `tdk doctor` unknown-field wording.
|
|
9
10
|
- `tdk doctor` now checks the project's own resource configs for duplicates. Two resources with the same `appName` fail (`Duplicate resource names`, naming both `service.json` paths): the name becomes the Compose service and Traefik router, so they collide, and before nothing reported it. Two backend, mcp or routed bring-your-own resources on the same `port` give a warning, not a failure (`Duplicate resource ports`): service ports are not published on the host and each service has its own container, so the pair can run, but it is usually a copied `service.json`. Frontends, workers, libraries and a missing `port` are ignored. Fixes #76.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
English | [简体中文](README-zh_cn.md) | [繁體中文](README-zh_tw.md) | [日本語](README-ja.md) | [한국어](README-ko.md)
|
|
15
15
|
|
|
16
|
-
TDK CLI
|
|
16
|
+
TDK CLI — start services on your laptop. It is not a deploy and not a Compose file: define each service in `service.json`, then run `tdk up`. No Kubernetes is needed on the machine.
|
|
17
17
|
|
|
18
18
|
Stability: 1.x local dev. Write `service.json`; TDK generates the local runtime files. See [what TDK writes](docs/generated-files.md) and how to check for drift. Core CLI is MIT and needs no key. Premium is optional.
|
|
19
19
|
|
|
@@ -32,9 +32,11 @@ mkdir shop && cd shop
|
|
|
32
32
|
tdk project --yes
|
|
33
33
|
tdk resource orders-api --type backend --stack shop --yes
|
|
34
34
|
tdk up shop
|
|
35
|
-
curl http://api.shop.localhost/api/orders
|
|
35
|
+
curl http://api.shop.localhost:8080/api/orders/health
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
`tdk up` prints the host ports it chose. Port 8080 is the default; if it was taken, use the HTTP port `tdk up` reports (or set `TDK_HTTP_PORT`). A resource named `orders-api` is served at `/api/orders` (a trailing `-api` is dropped); `tdk up` prints the exact URLs.
|
|
39
|
+
|
|
38
40
|

|
|
39
41
|
|
|
40
42
|
*Scaffolding a backend and frontend, then listing the stack.*
|
|
@@ -63,6 +65,13 @@ If Helm, Compose, or your existing Tilt setup already gives you a working local
|
|
|
63
65
|
|
|
64
66
|
See [how TDK CLI works alongside Helm](https://tdk-landscape.github.io/tdk-website/docs/with-helm/), the [service schema](engine/schemas/service-schema.json), and the [project configuration schema](engine/schemas/project-schema.json).
|
|
65
67
|
|
|
68
|
+
## Terminal UI
|
|
69
|
+
|
|
70
|
+
Use `tdk ui` to inspect stacks and resources. Arrow keys or `j`/`k` move the
|
|
71
|
+
selection; `g`/`G` or Home/End jump to the first/last item, and PageUp/PageDown
|
|
72
|
+
move one visible page. `/` searches; navigation letters remain search text
|
|
73
|
+
while searching. The selected row stays visible when the terminal is resized.
|
|
74
|
+
|
|
66
75
|
## Installation
|
|
67
76
|
|
|
68
77
|
Install the CLI from npm (requires Node.js 22.12+) or use the prebuilt binary (no Node.js or Bun required):
|
|
@@ -83,6 +92,7 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
|
|
|
83
92
|
|
|
84
93
|
## Docs
|
|
85
94
|
|
|
95
|
+
- [CLI command cheat sheet](cli/README.md#cheat-sheet)
|
|
86
96
|
- [Documentation index](docs/README.md)
|
|
87
97
|
- [Pilot TDK on a real repository](docs/adopt-tdk.md)
|
|
88
98
|
- [FAQ for agencies and multi-client teams](docs/faq-teams.md)
|
|
@@ -121,7 +131,7 @@ published on 21 September 2026. Those dates describe the current repository and
|
|
|
121
131
|
|
|
122
132
|
## Requirements and support
|
|
123
133
|
|
|
124
|
-
For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness (supports `--no-ping` to skip service health checks and `--ping-timeout <ms>`). See [WSL2 setup](docs/wsl2.md).
|
|
134
|
+
For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20.2+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. Dev ports listen on `127.0.0.1` only, so other machines on your network cannot reach them. Set `TDK_BIND_ADDRESS` (for example `0.0.0.0`, to test from a phone) to change that; `tdk doctor` warns when it is a wildcard address. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness (supports `--no-ping` to skip service health checks and `--ping-timeout <ms>`). See [WSL2 setup](docs/wsl2.md).
|
|
125
135
|
|
|
126
136
|
On native Windows, `tdk --version`, `tdk doctor`, and `tdk up --dry-run` are inspect-only commands. `tdk up` exits 2 with “Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.”
|
|
127
137
|
|
|
@@ -144,8 +154,3 @@ Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
|
144
154
|
TDK is MIT-licensed; see [LICENSE](LICENSE). The [license boundary](GOVERNANCE.md#license-boundary-and-donation-scope) says which code is MIT and which is downloaded with a key.
|
|
145
155
|
|
|
146
156
|
Project governance: [GOVERNANCE.md](GOVERNANCE.md), [MAINTAINERS.md](MAINTAINERS.md), [ADOPTERS.md](ADOPTERS.md).
|
|
147
|
-
|
|
148
|
-
Use `tdk ui` to inspect stacks and resources. Arrow keys or `j`/`k` move the
|
|
149
|
-
selection; `g`/`G` or Home/End jump to the first/last item, and PageUp/PageDown
|
|
150
|
-
move one visible page. `/` searches; navigation letters remain search text
|
|
151
|
-
while searching. The selected row stays visible when the terminal is resized.
|
package/cli/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { unknownHelpTarget } from "./utils/help-command.js";
|
|
|
30
30
|
const program = new Command();
|
|
31
31
|
program
|
|
32
32
|
.name("tdk")
|
|
33
|
-
.description("
|
|
33
|
+
.description("TDK CLI — start services on your laptop.")
|
|
34
34
|
.version(pkg.version, "-v, --version", "Display version number")
|
|
35
35
|
.option("--verbose", "Enable verbose output", false)
|
|
36
36
|
.configureOutput({
|
|
@@ -9,7 +9,7 @@ import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constant
|
|
|
9
9
|
import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
|
|
10
10
|
import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
|
|
11
11
|
import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
12
|
-
import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkPrismaConsistency, checkResourcePackageJson, checkServiceUrlPorts, checkSharedPlatformPostgres, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
12
|
+
import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkPrismaConsistency, checkResourcePackageJson, checkServiceUrlPorts, checkSharedPlatformPostgres, checkSharedStackRoutes, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
13
13
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
14
14
|
import { execAsync, isExecTimeout } from "../utils/exec-async.js";
|
|
15
15
|
import { formatCount } from "../utils/formatting.js";
|
|
@@ -17,6 +17,8 @@ import { getHostPortPlan } from "../utils/host-port-config.js";
|
|
|
17
17
|
import { createHostPortPlan } from "../utils/host-port-plan.js";
|
|
18
18
|
import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
19
19
|
import { isApiServiceType } from "../utils/resource-kind.js";
|
|
20
|
+
import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
|
|
21
|
+
import { checkCircularDependencies, checkDependsOnShape, checkSchemaVersions, checkServicePorts, } from "../utils/service-config-checks.js";
|
|
20
22
|
import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
|
|
21
23
|
import { discoverResourcesFromRoot } from "../utils/services.js";
|
|
22
24
|
import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
|
|
@@ -70,6 +72,40 @@ export function checkWslProjectLocation(projectPath, strict, isWsl = process.pla
|
|
|
70
72
|
fix: "Move the repository under your WSL home directory (for example, ~/projects) and retry tdk doctor.",
|
|
71
73
|
};
|
|
72
74
|
}
|
|
75
|
+
/** True for IPv4 0.0.0.0 and any IPv6 unspecified form (::, 0:0::0, [::], ...). */
|
|
76
|
+
function isWildcardBindAddress(value) {
|
|
77
|
+
const host = value.replace(/^\[(.*)\]$/, "$1");
|
|
78
|
+
if (host === "0.0.0.0")
|
|
79
|
+
return true;
|
|
80
|
+
// IPv4-embedded unspecified address, e.g. ::0.0.0.0
|
|
81
|
+
if (/^[0:]*:0\.0\.0\.0$/.test(host))
|
|
82
|
+
return true;
|
|
83
|
+
return host.includes(":") && /^[0:]+$/.test(host);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Dev ports (Traefik, Postgres) bind to 127.0.0.1 by default. TDK_BIND_ADDRESS
|
|
87
|
+
* opts in to another address; a wildcard exposes them to every machine on the
|
|
88
|
+
* network (GHSA-3hj3-f39v-j2x5).
|
|
89
|
+
*/
|
|
90
|
+
export function checkPublishedBindAddress(env = process.env, strict = false) {
|
|
91
|
+
const bindAddress = (env.TDK_BIND_ADDRESS ?? "").trim();
|
|
92
|
+
if (!isWildcardBindAddress(bindAddress)) {
|
|
93
|
+
return {
|
|
94
|
+
name: "Dev Port Bind Address",
|
|
95
|
+
didPass: true,
|
|
96
|
+
message: bindAddress
|
|
97
|
+
? `Dev ports bind to ${bindAddress}`
|
|
98
|
+
: "Dev ports bind to 127.0.0.1 (this machine only)",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
name: "Dev Port Bind Address",
|
|
103
|
+
didPass: !strict,
|
|
104
|
+
isWarning: !strict,
|
|
105
|
+
message: `TDK_BIND_ADDRESS is ${bindAddress}: Traefik and Postgres are reachable from other machines on the network.`,
|
|
106
|
+
fix: "Unset TDK_BIND_ADDRESS to keep dev ports on 127.0.0.1. Keep it only when you test from another device on purpose.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
73
109
|
/** Failures are shown before passing statuses, with a 5432 conflict first. */
|
|
74
110
|
export function orderDoctorResults(results) {
|
|
75
111
|
const failures = results.filter((result) => !result.didPass && !result.isSkipped && !result.isWarning);
|
|
@@ -286,8 +322,8 @@ export async function checkTilt(exec = execAsync) {
|
|
|
286
322
|
return {
|
|
287
323
|
name: "Tilt CLI",
|
|
288
324
|
didPass: false,
|
|
289
|
-
message: `Tilt ${raw || "version unknown"} is below the required
|
|
290
|
-
fix:
|
|
325
|
+
message: `Tilt ${raw || "version unknown"} is below the required v${MIN_TILT_VERSION.join(".")} floor`,
|
|
326
|
+
fix: `Install Tilt v${MIN_TILT_VERSION.join(".")} or newer: https://docs.tilt.dev/install.html`,
|
|
291
327
|
};
|
|
292
328
|
}
|
|
293
329
|
return { name: "Tilt CLI", didPass: true, message: `Tilt ${raw} is available` };
|
|
@@ -345,8 +381,8 @@ export async function checkBun(exec = execAsync) {
|
|
|
345
381
|
return {
|
|
346
382
|
name: "Bun",
|
|
347
383
|
didPass: false,
|
|
348
|
-
message: `Bun ${rawVersion || "version unknown"} is below the required
|
|
349
|
-
fix:
|
|
384
|
+
message: `Bun ${rawVersion || "version unknown"} is below the required ${MIN_BUN_VERSION.join(".")} floor`,
|
|
385
|
+
fix: `Install Bun ${MIN_BUN_VERSION.join(".")} or newer: https://bun.sh/install`,
|
|
350
386
|
};
|
|
351
387
|
}
|
|
352
388
|
return {
|
|
@@ -1122,6 +1158,7 @@ export const doctorCommand = new Command("doctor")
|
|
|
1122
1158
|
checkDockerVersions,
|
|
1123
1159
|
checkBun,
|
|
1124
1160
|
() => checkWslProjectLocation(findProjectRoot() ?? process.cwd(), options.strict),
|
|
1161
|
+
() => checkPublishedBindAddress(process.env, options.strict),
|
|
1125
1162
|
// Each project needs several networks; a full address pool fails `tdk up` late.
|
|
1126
1163
|
() => checkDockerNetworkCapacity(),
|
|
1127
1164
|
];
|
|
@@ -1139,9 +1176,14 @@ export const doctorCommand = new Command("doctor")
|
|
|
1139
1176
|
// Wiring mistakes that otherwise surface minutes into `tdk up`.
|
|
1140
1177
|
() => checkResourcePackageJson(),
|
|
1141
1178
|
() => checkDuplicateResourceNames(),
|
|
1179
|
+
() => checkDependsOnShape(),
|
|
1180
|
+
() => checkCircularDependencies(),
|
|
1181
|
+
() => checkServicePorts(),
|
|
1182
|
+
() => checkSchemaVersions(undefined, options.strict),
|
|
1142
1183
|
() => checkDuplicateResourcePorts(),
|
|
1143
1184
|
() => checkServiceUrlPorts(),
|
|
1144
1185
|
() => checkFrontendBackendUrls(),
|
|
1186
|
+
() => checkSharedStackRoutes(),
|
|
1145
1187
|
() => checkNatsBroker(),
|
|
1146
1188
|
() => checkPrismaConsistency(),
|
|
1147
1189
|
// Shared platform Postgres: will-start report + unknown dependsOn names stay errors.
|
|
@@ -1182,9 +1224,24 @@ export const doctorCommand = new Command("doctor")
|
|
|
1182
1224
|
const exitCode = getDoctorExitCode(report);
|
|
1183
1225
|
const allPassed = report.data.ready;
|
|
1184
1226
|
if (options.json) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1227
|
+
let secrets;
|
|
1228
|
+
try {
|
|
1229
|
+
secrets = envSecretValues(findProjectRoot() ?? process.cwd());
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
if (!(error instanceof EnvUnreadableError))
|
|
1233
|
+
throw error;
|
|
1234
|
+
// Without the values the report cannot be published safely, so it is replaced by a structured error.
|
|
1235
|
+
console.log(JSON.stringify(createDoctorReport([], inProject, [
|
|
1236
|
+
...errors,
|
|
1237
|
+
{ code: "ENV_UNREADABLE", message: error.message },
|
|
1238
|
+
])));
|
|
1187
1239
|
console.error(error.message);
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
console.log(JSON.stringify(redactValue(report, secrets)));
|
|
1243
|
+
for (const error of errors)
|
|
1244
|
+
console.error(redactSecrets(error.message, secrets));
|
|
1188
1245
|
if (exitCode !== 0)
|
|
1189
1246
|
process.exit(exitCode);
|
|
1190
1247
|
return;
|
|
@@ -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("TDK
|
|
4
|
+
${chalk.bold.cyan("tdk")} ${chalk.gray(`v${pkg.version}`)} ${chalk.white("TDK CLI — start services on your laptop.")}
|
|
5
5
|
`;
|
|
6
6
|
const COMMAND_GROUPS = [
|
|
7
7
|
{
|
|
@@ -14,6 +14,16 @@ const COMMAND_GROUPS = [
|
|
|
14
14
|
alias: "",
|
|
15
15
|
},
|
|
16
16
|
{ name: "projects, info", desc: "Show project information and config status", alias: "" },
|
|
17
|
+
{
|
|
18
|
+
name: "import",
|
|
19
|
+
desc: "Import the services a directory describes",
|
|
20
|
+
alias: "[dir] --dry-run --yes",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "eject",
|
|
24
|
+
desc: "Write EJECTED.md describing the generated Tilt files",
|
|
25
|
+
alias: "--dry-run --yes",
|
|
26
|
+
},
|
|
17
27
|
],
|
|
18
28
|
},
|
|
19
29
|
{
|
|
@@ -39,6 +49,7 @@ const COMMAND_GROUPS = [
|
|
|
39
49
|
{ name: "up, deploy", desc: "Start services (optionally by stack)", alias: "[stack-name]" },
|
|
40
50
|
{ name: "down", desc: "Stop all tilt resources", alias: "" },
|
|
41
51
|
{ name: "status", desc: "Show status of services and stacks", alias: "" },
|
|
52
|
+
{ name: "logs", desc: "Print recent logs from the running stack", alias: "--json" },
|
|
42
53
|
],
|
|
43
54
|
},
|
|
44
55
|
{
|
|
@@ -63,6 +74,7 @@ const COMMAND_GROUPS = [
|
|
|
63
74
|
alias: "regenerate, verify, edit",
|
|
64
75
|
},
|
|
65
76
|
{ name: "doctor", desc: "Check environment readiness", alias: "" },
|
|
77
|
+
{ name: "mcp", desc: "Run an MCP server over stdio for coding agents", alias: "" },
|
|
66
78
|
{
|
|
67
79
|
name: "runtime",
|
|
68
80
|
desc: "Inspect packaged engine and template assets",
|
|
@@ -75,6 +87,10 @@ const COMMAND_GROUPS = [
|
|
|
75
87
|
],
|
|
76
88
|
},
|
|
77
89
|
];
|
|
90
|
+
/** Every command name and alias that the help table lists. */
|
|
91
|
+
export function listedCommandNames() {
|
|
92
|
+
return COMMAND_GROUPS.flatMap((group) => group.commands.flatMap((command) => command.name.split(",").map((name) => name.trim())));
|
|
93
|
+
}
|
|
78
94
|
function formatCommand(name, desc, alias, color) {
|
|
79
95
|
const nameFormatted = color(name.padEnd(20));
|
|
80
96
|
const descFormatted = chalk.white(desc);
|
|
@@ -2,8 +2,11 @@ import { Command } from "commander";
|
|
|
2
2
|
import { STANDARD_PORTS } from "../utils/constants.js";
|
|
3
3
|
import { errorFactories, runCommand, showErrorAndExit, TdkError } from "../utils/errors.js";
|
|
4
4
|
import { createMachineEnvelope } from "../utils/machine-output.js";
|
|
5
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
6
|
+
import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
|
|
5
7
|
import { isTiltAvailable, runTilt } from "../utils/tilt.js";
|
|
6
8
|
import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, } from "../utils/tilt-logs.js";
|
|
9
|
+
import { tiltUnreachableMessage } from "../utils/tilt-unreachable-message.js";
|
|
7
10
|
/** Resource names Tilt knows about, or null when they cannot be listed (the logs call then reports the real error). */
|
|
8
11
|
async function tiltResourceNames(port) {
|
|
9
12
|
try {
|
|
@@ -28,16 +31,24 @@ export const logsCommand = new Command("logs")
|
|
|
28
31
|
.option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
|
|
29
32
|
.option("--json", "Output one JSON object and exit", false)
|
|
30
33
|
.action(async (options) => {
|
|
31
|
-
|
|
34
|
+
let secrets = [];
|
|
35
|
+
const fail = (code, rawMessage, exitCode = 1, suggestions = []) => {
|
|
36
|
+
const message = redactSecrets(rawMessage, secrets);
|
|
37
|
+
const redactedSuggestions = redactValue(suggestions, secrets);
|
|
32
38
|
if (options.json) {
|
|
33
39
|
console.log(JSON.stringify(createMachineEnvelope(null, [
|
|
34
|
-
{
|
|
40
|
+
{
|
|
41
|
+
code,
|
|
42
|
+
message,
|
|
43
|
+
...(redactedSuggestions.length > 0 ? { suggestions: redactedSuggestions } : {}),
|
|
44
|
+
},
|
|
35
45
|
])));
|
|
36
46
|
console.error(message);
|
|
37
47
|
process.exit(exitCode);
|
|
38
48
|
}
|
|
39
|
-
if (
|
|
40
|
-
return new TdkError(message,
|
|
49
|
+
if (redactedSuggestions.length > 0) {
|
|
50
|
+
return new TdkError(message, redactedSuggestions, exitCode).exit();
|
|
51
|
+
}
|
|
41
52
|
return showErrorAndExit(message, exitCode);
|
|
42
53
|
};
|
|
43
54
|
if (!isValidTail(options.tail))
|
|
@@ -50,6 +61,16 @@ export const logsCommand = new Command("logs")
|
|
|
50
61
|
fail("USAGE", "--port must be a valid port number", 2);
|
|
51
62
|
const tail = Number(options.tail);
|
|
52
63
|
const services = options.service ?? [];
|
|
64
|
+
try {
|
|
65
|
+
secrets = envSecretValues(findProjectRoot() ?? process.cwd());
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!(error instanceof EnvUnreadableError))
|
|
69
|
+
throw error;
|
|
70
|
+
fail("ENV_UNREADABLE", error.message, 1, [
|
|
71
|
+
"Fix the read permission on .env, or move it out of the project",
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
53
74
|
const action = async () => {
|
|
54
75
|
if (!(await isTiltAvailable())) {
|
|
55
76
|
fail("TILT_MISSING", "Tilt is not installed. Run: tdk doctor");
|
|
@@ -75,16 +96,16 @@ export const logsCommand = new Command("logs")
|
|
|
75
96
|
if (result.exitCode !== 0) {
|
|
76
97
|
const detail = result.stderr.trim();
|
|
77
98
|
if (!detail || isTiltConnectionFailure(detail)) {
|
|
78
|
-
fail("TILT_NOT_RUNNING",
|
|
99
|
+
fail("TILT_NOT_RUNNING", tiltUnreachableMessage(portText));
|
|
79
100
|
}
|
|
80
101
|
fail("TILT_LOGS_FAILED", `tilt logs failed: ${detail}`);
|
|
81
102
|
}
|
|
82
103
|
if (!options.json) {
|
|
83
|
-
process.stdout.write(result.stdout);
|
|
104
|
+
process.stdout.write(redactSecrets(result.stdout, secrets));
|
|
84
105
|
return;
|
|
85
106
|
}
|
|
86
107
|
const lines = parseTiltLogLines(result.stdout, tail);
|
|
87
|
-
console.log(JSON.stringify(createMachineEnvelope({ services, tail, since: options.since ?? null, lines })));
|
|
108
|
+
console.log(JSON.stringify(redactValue(createMachineEnvelope({ services, tail, since: options.since ?? null, lines }), secrets)));
|
|
88
109
|
};
|
|
89
110
|
await runCommand(action);
|
|
90
111
|
});
|
|
@@ -3,21 +3,39 @@ import chalk from "chalk";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { assertValid, confirmOrCancel } from "../utils/command-helpers.js";
|
|
5
5
|
import { clearDiscoveryCache, createDiscoveryContext } from "../utils/discovery-context.js";
|
|
6
|
-
import { requireProjectRoot, runCommand } from "../utils/errors.js";
|
|
6
|
+
import { errorFactories, requireProjectRoot, runCommand, TdkError } from "../utils/errors.js";
|
|
7
7
|
import { writeJsonFile } from "../utils/file-helpers.js";
|
|
8
8
|
import { formatCount, showAllSatisfyCondition, showCommandHeader, showDetail, showSuccess, } from "../utils/formatting.js";
|
|
9
9
|
import { promptMultiSelect, promptText } from "../utils/prompt.js";
|
|
10
10
|
import { createKebabCaseValidator, validateStackName } from "../utils/validation.js";
|
|
11
|
+
export function parseStackResourceConfig(content, configPath) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
17
|
+
throw new TdkError(`Could not parse service.json at ${configPath}: ${reason}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function normalizeResourceNames(names) {
|
|
21
|
+
const normalizedNames = names.flatMap((name) => name
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((part) => part.trim())
|
|
24
|
+
.filter(Boolean));
|
|
25
|
+
return [...new Set(normalizedNames)];
|
|
26
|
+
}
|
|
11
27
|
export const stackCommand = new Command("stack")
|
|
12
28
|
.description("Organize resources into stacks (groups)")
|
|
13
29
|
.argument("[stack-name]", "Stack name to assign to resources")
|
|
14
30
|
.option("--list", "List resources without a stack", false)
|
|
31
|
+
.option("--resources <names...>", "Resource names to add (comma- or space-separated)")
|
|
32
|
+
.option("--yes", "Skip confirmation prompt", false)
|
|
15
33
|
.action(async (stackName, options) => {
|
|
16
34
|
await runCommand(async () => {
|
|
17
35
|
requireProjectRoot();
|
|
18
36
|
showCommandHeader("Stack Management");
|
|
19
37
|
const discovery = createDiscoveryContext();
|
|
20
|
-
if (discovery.resources.length === 0) {
|
|
38
|
+
if (discovery.resources.length === 0 && options.resources === undefined) {
|
|
21
39
|
console.log(chalk.yellow("No resources discovered. Make sure you're in a project with service.json files."));
|
|
22
40
|
return;
|
|
23
41
|
}
|
|
@@ -42,6 +60,24 @@ export const stackCommand = new Command("stack")
|
|
|
42
60
|
}
|
|
43
61
|
return;
|
|
44
62
|
}
|
|
63
|
+
if (!process.stdin.isTTY) {
|
|
64
|
+
if (options.resources === undefined) {
|
|
65
|
+
throw new TdkError("Interactive resource selection requires a TTY.", [
|
|
66
|
+
"Pass --resources <name...> to select resources without a prompt.",
|
|
67
|
+
"Use --list to inspect unassigned resources without changing them.",
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
if (!stackName) {
|
|
71
|
+
throw new TdkError("A stack name is required when stdin is not a TTY.", [
|
|
72
|
+
"Use: tdk stack <stack-name> --resources <name...> [--yes]",
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
if (!options.yes) {
|
|
76
|
+
throw new TdkError("Confirmation requires a TTY.", [
|
|
77
|
+
"Pass --yes to skip confirmation when assigning resources without a TTY.",
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
45
81
|
if (stackName)
|
|
46
82
|
assertValid(validateStackName(stackName));
|
|
47
83
|
let targetStack = stackName;
|
|
@@ -56,28 +92,64 @@ export const stackCommand = new Command("stack")
|
|
|
56
92
|
targetStack = name;
|
|
57
93
|
}
|
|
58
94
|
const resourcesToUpdate = discovery.unassignedResources;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
95
|
+
const selectedResources = [];
|
|
96
|
+
if (options.resources !== undefined) {
|
|
97
|
+
const requestedNames = normalizeResourceNames(options.resources);
|
|
98
|
+
if (requestedNames.length === 0) {
|
|
99
|
+
throw new TdkError("Pass at least one resource name to --resources.", [
|
|
100
|
+
"Example: `tdk stack api --resources users-api orders-api --yes`",
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
const configPathsByName = new Map();
|
|
104
|
+
for (const resource of discovery.resources) {
|
|
105
|
+
const configPaths = configPathsByName.get(resource.name) ?? [];
|
|
106
|
+
configPaths.push(resource.configPath);
|
|
107
|
+
configPathsByName.set(resource.name, configPaths);
|
|
108
|
+
}
|
|
109
|
+
const duplicates = [...configPathsByName].filter(([, configPaths]) => configPaths.length > 1);
|
|
110
|
+
if (duplicates.length > 0) {
|
|
111
|
+
throw new TdkError("Cannot assign resources while duplicate names are present.", duplicates.map(([name, configPaths]) => `Duplicate "${name}" found in ${configPaths.join(" and ")}`));
|
|
112
|
+
}
|
|
113
|
+
const validNames = [...configPathsByName.keys()].sort();
|
|
114
|
+
const resourceByName = new Map(discovery.resources.map((resource) => [resource.name, resource]));
|
|
115
|
+
for (const name of requestedNames) {
|
|
116
|
+
const resource = resourceByName.get(name);
|
|
117
|
+
if (!resource) {
|
|
118
|
+
const error = errorFactories.resourceNotFound(name, validNames);
|
|
119
|
+
error.suggestions.unshift(`Valid resources: ${validNames.join(", ")}`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
if (resource.stack) {
|
|
123
|
+
throw new TdkError(`Resource "${name}" is already assigned to stack "${resource.stack}".`, ["Run `tdk resources` to review current assignments."]);
|
|
124
|
+
}
|
|
125
|
+
selectedResources.push(resource.configPath);
|
|
126
|
+
}
|
|
62
127
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
else {
|
|
129
|
+
if (resourcesToUpdate.length === 0) {
|
|
130
|
+
console.log(chalk.yellow("\nNo resources available to add to this stack."));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const promptedResources = await promptMultiSelect({
|
|
134
|
+
message: `Select resources to add to stack "${targetStack}":`,
|
|
135
|
+
choices: resourcesToUpdate.map((resource) => ({
|
|
136
|
+
title: resource.name,
|
|
137
|
+
value: resource.configPath,
|
|
138
|
+
})),
|
|
139
|
+
validate: (input) => {
|
|
140
|
+
if (input.length === 0)
|
|
141
|
+
return "Select at least one resource";
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (promptedResources.length === 0) {
|
|
146
|
+
console.log(chalk.yellow("No resources selected. Exiting."));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
selectedResources.push(...promptedResources);
|
|
78
150
|
}
|
|
79
151
|
showDetail(`\nWill add "stack": "${targetStack}" to ${formatCount(selectedResources.length, "resource")}.`, 0);
|
|
80
|
-
const confirmed = await confirmOrCancel("Proceed?");
|
|
152
|
+
const confirmed = options.yes || (await confirmOrCancel("Proceed?"));
|
|
81
153
|
if (!confirmed)
|
|
82
154
|
return;
|
|
83
155
|
// Clear cache since we're about to modify resources
|
|
@@ -85,7 +157,7 @@ export const stackCommand = new Command("stack")
|
|
|
85
157
|
let updated = 0;
|
|
86
158
|
for (const configPath of selectedResources) {
|
|
87
159
|
const content = readFileSync(configPath, "utf-8");
|
|
88
|
-
const config =
|
|
160
|
+
const config = parseStackResourceConfig(content, configPath);
|
|
89
161
|
config.stack = targetStack;
|
|
90
162
|
writeJsonFile(configPath, config);
|
|
91
163
|
updated++;
|