@tdk-landscape/tdk-cli-core 1.3.55 ā 1.3.57
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 +9 -0
- package/README.md +2 -1
- package/cli/dist/commands/doctor.js +3 -1
- package/cli/dist/commands/project.js +13 -6
- package/cli/dist/commands/up.js +9 -0
- package/cli/dist/utils/doctor-runtime.js +85 -0
- package/cli/dist/utils/env-validator.js +5 -5
- package/cli/dist/utils/file-helpers.js +22 -1
- package/cli/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- Binary installs (`tdk-landscape.github.io/install.sh`) ship the Docker helper scripts generated Dockerfiles run. Before, `tdk project` warned "TDK runtime assets not found" and image builds could fail. The release script now smoke-tests the built binary.
|
|
8
|
+
|
|
9
|
+
## 1.3.56 (2026-09-27)
|
|
10
|
+
|
|
11
|
+
- `tdk project` adds `.env`, `.tdk/.tdk-out/`, `.tdk/.project-id` and `node_modules/` to `.gitignore`. `.env` holds a generated database password and was easy to commit by accident.
|
|
12
|
+
- `tdk doctor` checks that host ports 80, 443 and 5432 are free and names what holds them (usually a local Postgres). `tdk up` prints the same warning before starting Tilt.
|
|
13
|
+
- `tdk project` no longer tells you to edit `.env` first: the defaults work, and the Verdaccio variables are marked as belonging to the paid registry.
|
|
14
|
+
## 1.3.55 (2026-09-27)
|
|
15
|
+
|
|
7
16
|
- Dropped the `ora` dependency (only `tdk upgrade` used it) in favor of a small built-in spinner: 50 installed packages instead of 61, and no more duplicate versions of cli-cursor, restore-cursor, onetime and signal-exit.
|
|
8
17
|
- `tdk doctor` outside a project checks only Docker, Compose and Tilt, and reports "This machine is ready for TDK". Before, running it right after installing (as the installer suggests) failed with "Master configs missing ⦠Environment not ready".
|
|
9
18
|
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ tdk up shop # run it with hot reload
|
|
|
26
26
|
|
|
27
27
|

|
|
28
28
|
|
|
29
|
-
**Requirements:** Docker (Desktop, OrbStack or Colima; Engine 25+, Compose 2.20+), [Tilt](https://docs.tilt.dev/install.html), [Bun](https://bun.sh) for the generated services, and Node.js 22.12+ for the CLI. macOS and Linux are supported. Windows is untested (WSL2 may work). Run `tdk doctor` to check all of it.
|
|
29
|
+
**Requirements:** Docker (Desktop, OrbStack or Colima; Engine 25+, Compose 2.20+), [Tilt](https://docs.tilt.dev/install.html), [Bun](https://bun.sh) for the generated services, and Node.js 22.12+ for the CLI. Ports 80, 443 and 5432 must be free (a local Postgres is the usual conflict). macOS and Linux are supported. Windows is untested (WSL2 may work). Run `tdk doctor` to check all of it.
|
|
30
30
|
|
|
31
31
|
This is the core monorepo for TDK: the `tdk` CLI, the Starlark-based Tilt orchestration engine that powers it, and the discovery system that turns a directory of services into a running local landscape.
|
|
32
32
|
|
|
@@ -144,6 +144,7 @@ TDK has no telemetry or analytics. The CLI only goes online when you ask it to:
|
|
|
144
144
|
- `tdk upgrade` checks the latest release on GitHub.
|
|
145
145
|
- With `TDK_LICENSE_KEY` set, `tdk project` and `tdk up` download the paid bundle from `tdk-extension-dist.oranguman.workers.dev` and re-check the key every 12 hours. Without a key, nothing is sent.
|
|
146
146
|
- Docker and Tilt pull images and packages as they normally would.
|
|
147
|
+
- Tilt, which TDK runs, has its own anonymous usage analytics. Turn them off with `tilt analytics opt out`.
|
|
147
148
|
|
|
148
149
|
## FAQ
|
|
149
150
|
|
|
@@ -4,7 +4,7 @@ import { dirname, join, normalize } from "node:path";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
|
|
7
|
-
import { checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, } from "../utils/doctor-runtime.js";
|
|
7
|
+
import { checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, } from "../utils/doctor-runtime.js";
|
|
8
8
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
9
9
|
import { formatCount } from "../utils/formatting.js";
|
|
10
10
|
import { findProjectRoot } from "../utils/paths.js";
|
|
@@ -697,6 +697,8 @@ export const doctorCommand = new Command("doctor")
|
|
|
697
697
|
checkEnvironmentVariables,
|
|
698
698
|
// Preflight: catch "port 80 already allocated" BEFORE claiming ready.
|
|
699
699
|
checkIngressPorts,
|
|
700
|
+
// Preflight: a local Postgres or web server on 5432/80/443.
|
|
701
|
+
() => checkHostPorts(),
|
|
700
702
|
// Preflight: Verdaccio down causes ImageBuild bun install ConnectionRefused.
|
|
701
703
|
checkPrivateNpmRegistry,
|
|
702
704
|
// Runtime checks: skip gracefully if the stack isn't started yet.
|
|
@@ -8,7 +8,7 @@ import { generateMasterConfigs, readProjectConfig } from "../generator/template-
|
|
|
8
8
|
import { MASTER_CONFIG_FILES } from "../utils/constants.js";
|
|
9
9
|
import { ensureEnvFile, validateEnvFile } from "../utils/env-validator.js";
|
|
10
10
|
import { errorFactories, runCommand, showErrorAndExit } from "../utils/errors.js";
|
|
11
|
-
import { ensureDirectory, writeJsonFile } from "../utils/file-helpers.js";
|
|
11
|
+
import { ensureDirectory, ensureGitignore, writeJsonFile } from "../utils/file-helpers.js";
|
|
12
12
|
import { showCancelled, showCommandHeader, showDetail, showStep, showSuccess, } from "../utils/formatting.js";
|
|
13
13
|
import { findProjectRoot } from "../utils/paths.js";
|
|
14
14
|
import { PROJECT_FEATURES } from "../utils/project-features.js";
|
|
@@ -218,6 +218,10 @@ export const projectCommand = new Command("project")
|
|
|
218
218
|
}
|
|
219
219
|
showStep("\nš Regenerating master configuration files...\n");
|
|
220
220
|
await generateMasterConfigs(projectRoot);
|
|
221
|
+
const ignored = ensureGitignore(projectRoot);
|
|
222
|
+
if (ignored.length > 0) {
|
|
223
|
+
showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
|
|
224
|
+
}
|
|
221
225
|
console.log(chalk.green("\nā
Project configuration regenerated!"));
|
|
222
226
|
showDetail("\nGenerated in .tdk/.tdk-out/:", 0);
|
|
223
227
|
for (const file of MASTER_CONFIG_FILES) {
|
|
@@ -354,8 +358,7 @@ export const projectCommand = new Command("project")
|
|
|
354
358
|
showStep("\nš§ Setting up environment variables...\n");
|
|
355
359
|
const envCreated = ensureEnvFile(projectRoot);
|
|
356
360
|
if (envCreated) {
|
|
357
|
-
showSuccess("Created: .env (
|
|
358
|
-
showDetail("ā Edit .env to set VERDACCIO_URL_DOCKER, TILT_ENV, etc.");
|
|
361
|
+
showSuccess("Created: .env (the defaults work as-is)");
|
|
359
362
|
}
|
|
360
363
|
else {
|
|
361
364
|
showSuccess("Found: .env (environment already configured)");
|
|
@@ -368,6 +371,10 @@ export const projectCommand = new Command("project")
|
|
|
368
371
|
}
|
|
369
372
|
showDetail("ā Edit .env and fill in these values before running `tdk up`", 1);
|
|
370
373
|
}
|
|
374
|
+
const ignored = ensureGitignore(projectRoot);
|
|
375
|
+
if (ignored.length > 0) {
|
|
376
|
+
showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
|
|
377
|
+
}
|
|
371
378
|
console.log(chalk.green("\nā
Project configuration complete!"));
|
|
372
379
|
showDetail("\nGenerated files in .tdk/.tdk-out/:", 0);
|
|
373
380
|
for (const file of MASTER_CONFIG_FILES) {
|
|
@@ -378,9 +385,9 @@ export const projectCommand = new Command("project")
|
|
|
378
385
|
showDetail("\nSource file:", 0);
|
|
379
386
|
showDetail("- .tdk/project.json (edit this to change project structure)");
|
|
380
387
|
showDetail("\nNext steps:", 0);
|
|
381
|
-
showDetail("1.
|
|
382
|
-
showDetail("2.
|
|
383
|
-
showDetail("
|
|
388
|
+
showDetail("1. tdk resource api --type backend --stack app # scaffold a service");
|
|
389
|
+
showDetail("2. tdk up app # start it");
|
|
390
|
+
showDetail("\nAfter editing .tdk/project.json, run `tdk config regenerate`.", 0);
|
|
384
391
|
});
|
|
385
392
|
});
|
|
386
393
|
//# sourceMappingURL=project.js.map
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
|
|
5
5
|
import { handleDryRun } from "../utils/command-helpers.js";
|
|
6
|
+
import { checkHostPorts } from "../utils/doctor-runtime.js";
|
|
6
7
|
import { errorFactories, handleTiltFailure, withTiltCheck } from "../utils/errors.js";
|
|
7
8
|
import { formatCount } from "../utils/formatting.js";
|
|
8
9
|
import { findProjectRoot } from "../utils/paths.js";
|
|
@@ -84,6 +85,14 @@ export const upCommand = new Command("up")
|
|
|
84
85
|
if (handleDryRun(options, "not starting services", dryRunCommand)) {
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
88
|
+
// Warn rather than block: a stale local Postgres on 5432 makes Tilt fail
|
|
89
|
+
// halfway through startup with a bind error that's hard to read.
|
|
90
|
+
const hostPorts = await checkHostPorts();
|
|
91
|
+
if (!hostPorts.didPass && !options.quiet) {
|
|
92
|
+
console.log(chalk.yellow(`\nā ļø ${hostPorts.message}`));
|
|
93
|
+
if (hostPorts.fix)
|
|
94
|
+
console.log(chalk.gray(` ${hostPorts.fix}\n`));
|
|
95
|
+
}
|
|
87
96
|
if (options.force && !options.quiet) {
|
|
88
97
|
console.log(chalk.yellow("Force flag set - killing any existing Tilt processes..."));
|
|
89
98
|
execSync("killall tilt 2>/dev/null || true", { shell: "/bin/sh", stdio: "pipe" });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:net";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { formatCount } from "./formatting.js";
|
|
5
6
|
import { findProjectRoot } from "./paths.js";
|
|
@@ -322,6 +323,90 @@ export function checkIngressPorts(exec = execSync, projectName = getProjectName(
|
|
|
322
323
|
fix: `Stop the foreign container(s), e.g. \`docker stop ${foreign[0]?.name}\`, or shut down the other TDK/Tilt project using that Traefik. Then re-run \`tdk up\`.`,
|
|
323
324
|
};
|
|
324
325
|
}
|
|
326
|
+
/** Host ports the generated stack publishes: Traefik (80, 443) and Postgres (5432). */
|
|
327
|
+
export const HOST_PORTS = { 80: "HTTP", 443: "HTTPS", 5432: "Postgres" };
|
|
328
|
+
function probeAddress(port, host) {
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
const server = createServer();
|
|
331
|
+
server.once("error", (err) =>
|
|
332
|
+
// EACCES: ports below 1024 need root on Linux, so we can't tell.
|
|
333
|
+
resolve(err.code === "EADDRINUSE" ? "in-use" : "unknown"));
|
|
334
|
+
server.once("listening", () => server.close(() => resolve("free")));
|
|
335
|
+
server.listen(port, host);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Probes both the wildcard and loopback address: a Homebrew Postgres listens on
|
|
340
|
+
* 127.0.0.1 only, which a wildcard bind alone doesn't detect.
|
|
341
|
+
*/
|
|
342
|
+
export async function probeHostPort(port) {
|
|
343
|
+
const results = await Promise.all([
|
|
344
|
+
probeAddress(port, "0.0.0.0"),
|
|
345
|
+
probeAddress(port, "127.0.0.1"),
|
|
346
|
+
]);
|
|
347
|
+
if (results.includes("in-use"))
|
|
348
|
+
return "in-use";
|
|
349
|
+
return results.includes("free") ? "free" : "unknown";
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Catches host ports taken by something other than this project's containers,
|
|
353
|
+
* most often a local Postgres on 5432 or a web server on 80. Docker-held 80/443
|
|
354
|
+
* are reported by checkIngressPorts, so only 5432 is checked against other
|
|
355
|
+
* containers here.
|
|
356
|
+
*/
|
|
357
|
+
export async function checkHostPorts(exec = execSync, projectName = getProjectName(), probe = probeHostPort) {
|
|
358
|
+
const ports = Object.keys(HOST_PORTS).map(Number);
|
|
359
|
+
const inUse = [];
|
|
360
|
+
for (const port of ports) {
|
|
361
|
+
if ((await probe(port)) === "in-use")
|
|
362
|
+
inUse.push(port);
|
|
363
|
+
}
|
|
364
|
+
if (inUse.length === 0) {
|
|
365
|
+
return {
|
|
366
|
+
name: "Host Ports",
|
|
367
|
+
didPass: true,
|
|
368
|
+
message: `Host ports ${ports.join(", ")} are free`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
let dockerPs = "";
|
|
372
|
+
try {
|
|
373
|
+
dockerPs = exec("docker ps --format '{{.Names}}\\t{{.Ports}}'", {
|
|
374
|
+
stdio: "pipe",
|
|
375
|
+
encoding: "utf-8",
|
|
376
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Can't attribute ports without Docker; treat them as held by the host.
|
|
381
|
+
}
|
|
382
|
+
const holders = parsePublishedPortHolders(dockerPs, inUse);
|
|
383
|
+
const prefix = toComposeProjectPrefix(projectName).toLowerCase();
|
|
384
|
+
const isOwn = (name) => {
|
|
385
|
+
const lower = name.toLowerCase();
|
|
386
|
+
return lower.startsWith(`${prefix}_`) || lower.startsWith(`${prefix}-`);
|
|
387
|
+
};
|
|
388
|
+
const problems = [];
|
|
389
|
+
for (const port of inUse) {
|
|
390
|
+
const holder = holders.find((h) => h.publishedPorts.includes(port));
|
|
391
|
+
if (!holder) {
|
|
392
|
+
problems.push(`${port} (${HOST_PORTS[port]}) is used by a program on this machine`);
|
|
393
|
+
}
|
|
394
|
+
else if (port === 5432 && !isOwn(holder.name)) {
|
|
395
|
+
problems.push(`${port} (${HOST_PORTS[port]}) is published by container ${holder.name}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (problems.length === 0) {
|
|
399
|
+
return { name: "Host Ports", didPass: true, message: "Host ports are held by this project" };
|
|
400
|
+
}
|
|
401
|
+
const first = inUse.find((port) => problems.some((p) => p.startsWith(`${port} `))) ?? inUse[0];
|
|
402
|
+
return {
|
|
403
|
+
name: "Host Ports",
|
|
404
|
+
didPass: false,
|
|
405
|
+
message: `Ports TDK needs are taken:\n ${problems.join("\n ")}`,
|
|
406
|
+
fix: `Find what holds a port with \`lsof -nP -iTCP:${first} -sTCP:LISTEN\` and stop it ` +
|
|
407
|
+
"(for a local Postgres: `brew services stop postgresql` or quit Postgres.app), then run `tdk up`.",
|
|
408
|
+
};
|
|
409
|
+
}
|
|
325
410
|
/**
|
|
326
411
|
* When a Tilt session is active, surface resource update errors (Traefik port
|
|
327
412
|
* binds, image builds, etc.) instead of claiming the environment is ready
|
|
@@ -4,14 +4,14 @@ import { join } from "node:path";
|
|
|
4
4
|
const REQUIRED_ENV_VARS = [
|
|
5
5
|
{
|
|
6
6
|
name: "VERDACCIO_URL_DOCKER",
|
|
7
|
-
description: "Docker-internal URL
|
|
7
|
+
description: "Docker-internal URL of the private npm registry (Verdaccio, a paid feature)",
|
|
8
8
|
required: true,
|
|
9
9
|
default: "http://verdaccio:4873",
|
|
10
10
|
example: "http://verdaccio:4873",
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
name: "VERDACCIO_URL",
|
|
14
|
-
description: "Local
|
|
14
|
+
description: "Local URL of the private npm registry (Verdaccio, a paid feature)",
|
|
15
15
|
required: false,
|
|
16
16
|
default: "http://localhost:4873",
|
|
17
17
|
},
|
|
@@ -44,8 +44,8 @@ function generateDbPassword() {
|
|
|
44
44
|
export function generateEnvFile(_projectRoot) {
|
|
45
45
|
const lines = [
|
|
46
46
|
"# š§ TDK Environment Configuration",
|
|
47
|
-
"#
|
|
48
|
-
"#
|
|
47
|
+
"# Generated by `tdk project`. The defaults work as-is; edit only to change them.",
|
|
48
|
+
"# Keep this file out of git (it holds a generated database password).",
|
|
49
49
|
"#",
|
|
50
50
|
"# After editing, run: tdk config regenerate && tdk up",
|
|
51
51
|
"",
|
|
@@ -53,7 +53,7 @@ export function generateEnvFile(_projectRoot) {
|
|
|
53
53
|
for (const envVar of REQUIRED_ENV_VARS) {
|
|
54
54
|
lines.push(`# ${envVar.description}`);
|
|
55
55
|
if (envVar.required) {
|
|
56
|
-
lines.push(
|
|
56
|
+
lines.push("# Required (the value below is a working default)");
|
|
57
57
|
}
|
|
58
58
|
if (envVar.example) {
|
|
59
59
|
lines.push(`# Example: ${envVar.example}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
export function writeFilesWithProgress(basePath, tasks, onProgress) {
|
|
@@ -42,4 +42,25 @@ export function ensureDirectory(dirPath) {
|
|
|
42
42
|
mkdirSync(dirPath, { recursive: true });
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
/** Generated files that must never be committed: .env holds a generated DB password. */
|
|
46
|
+
const GITIGNORE_ENTRIES = [".env", ".tdk/.tdk-out/", ".tdk/.project-id", "node_modules/"];
|
|
47
|
+
/**
|
|
48
|
+
* Appends the TDK entries missing from the project's .gitignore (creating it
|
|
49
|
+
* if needed), so the first `git add .` doesn't commit .env or generated
|
|
50
|
+
* output. Returns the entries it added.
|
|
51
|
+
*/
|
|
52
|
+
export function ensureGitignore(projectRoot) {
|
|
53
|
+
const gitignorePath = resolve(projectRoot, ".gitignore");
|
|
54
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
55
|
+
const lines = new Set(existing.split("\n").map((line) => line.trim().replace(/\/$/, "")));
|
|
56
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry.replace(/\/$/, "")));
|
|
57
|
+
if (missing.length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
60
|
+
const header = existing
|
|
61
|
+
? "\n# TDK (generated files and secrets)\n"
|
|
62
|
+
: "# TDK (generated files and secrets)\n";
|
|
63
|
+
appendFileSync(gitignorePath, `${prefix}${header}${missing.join("\n")}\n`);
|
|
64
|
+
return missing;
|
|
65
|
+
}
|
|
45
66
|
//# sourceMappingURL=file-helpers.js.map
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdk-landscape/tdk-cli-core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.57",
|
|
4
4
|
"description": "Run 100 microservices on a 16 GB laptop without Kubernetes. Scaffold services and run them locally with hot reload, built on Tilt.",
|
|
5
5
|
"homepage": "https://tdk-landscape.github.io/tdk-website",
|
|
6
6
|
"repository": {
|