@tdk-landscape/tdk-cli-core 1.3.110 ā 1.3.112
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 +11 -0
- package/README.md +9 -0
- package/cli/dist/commands/completion.js +14 -4
- package/cli/dist/commands/config.js +1 -1
- package/cli/dist/commands/doctor.js +9 -27
- package/cli/dist/commands/down.js +62 -2
- package/cli/dist/commands/eject.js +49 -20
- package/cli/dist/commands/resource.js +88 -29
- package/cli/dist/commands/resources.js +19 -5
- package/cli/dist/commands/up.js +29 -1
- package/cli/dist/commands/upgrade.js +21 -6
- package/cli/dist/generator/template-engine.js +16 -4
- package/cli/dist/utils/doctor-wiring.js +78 -2
- package/cli/dist/utils/down-cleanup.js +98 -0
- package/cli/dist/utils/tdk-version.js +49 -0
- package/cli/dist/utils/tilt.js +6 -0
- package/cli/package.json +1 -1
- package/engine/topologies/tilt/common/utils_docker_networks.star +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `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
|
+
- `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
|
+
- `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.
|
|
10
|
+
- `tdk doctor` no longer fails with `N resources without a package.json` for a bring-your-own resource. A bring-your-own resource (`--image`, `--dockerfile` or `buildContext`) is a plain container: its image is pulled or built from its own Dockerfile, never from the generated Dockerfile that copies a `package.json`, so the check was a false positive that made `tdk doctor` (and CI recipes that gate on it) exit non-zero right after wrapping an existing service. Backend, frontend, worker, migrator, mcp, library and sdk resources without a `package.json` still fail; Python, Go and Rust backends were already exempt. The other doctor checks were run on a project with only bring-your-own resources and report nothing. Fixes #599.
|
|
11
|
+
- A scaffolded frontend now says why `bun run build` / `bun run dev` cannot run on the host before `tdk up`: the `.autogenerated/vite.config.*.autogenerated.ts` files its scripts point at are written by the engine (Tilt) when `tdk up` loads the resource, and neither `tdk resource` nor `tdk config regenerate` creates them. Before, `vite` stopped with an esbuild `Could not resolve` stack trace. New `predev`/`prebuild` scripts print what is missing and how to get it; once the file exists (and inside the engine image build) they pass silently. Existing frontends need the two scripts added by hand. Backend, worker and mcp scaffolds are unchanged. Fixes #592.
|
|
12
|
+
- `tdk up` now honours `minTdkVersion` from `.tdk/project.json`, like `tdk doctor`. On an older CLI (or a malformed value) it exits 1 before the Docker checks and before Tilt starts, with `tdk X is older than the Y this project requires (minTdkVersion)` and `Run: tdk upgrade`; `--dry-run` reports it the same way. New `tdk up --ignore-version` skips the check with a warning. A project without `minTdkVersion` behaves as before. The check lives in `cli/src/utils/tdk-version.ts`, shared with doctor.
|
|
13
|
+
- `init-networks` now fails with Docker's own message when a network that does not exist cannot be created. It ran `docker network create X 2>/dev/null && echo ... || true`, so a failure such as `all predefined address pools have been fully subnetted` was reported as `Docker networks initialized` and Postgres and Traefik then failed with `network ... declared as external, but could not be found`. An already existing network (including one another process creates between the check and the create) is still not an error, and a network with incorrect compose labels is still removed first when unused. Fixes #587.
|
|
14
|
+
- A freshly scaffolded worker now type-checks: its `src/index.ts` typed `Job.payload` as `Record<string, JsonValue>`, but `JsonValue` is a type internal to the CLI that the generated project neither defines nor imports, so `bun run build` failed with `TS2304: Cannot find name 'JsonValue'`. The payload is now `Record<string, unknown>`. Existing workers need the same one-word edit. Fixes #588.
|
|
15
|
+
- A freshly scaffolded backend, worker or mcp service now builds on the host: its `tsconfig.json` sets `"types": ["node"]`, matching the generated Docker tsconfig. Before, `bun run build` failed with `TS2591: Cannot find name 'process'` because TypeScript 7 no longer auto-includes `@types/*`. Frontend scaffolds are unchanged. Existing services need `"types": ["node"]` added by hand. Closes #581.
|
|
16
|
+
- The `Dockerfile` that `tdk resource` writes next to `service.json` now follows the service: `PORT`, `EXPOSE` and the health check use the `port` (and `healthCheckPath`) from `service.json` instead of a fixed 3000, the health probe no longer needs `curl` (not in `oven/bun`), `bun.lock` is optional in the `COPY` (a TDK workspace keeps it at the project root, so the old file stopped at `"/bun.lock": not found`), and a header comment says `tdk up` does not build this file (it builds `.autogenerated/Dockerfile.app.autogenerated`). A worker, which declares no health path, gets no HEALTHCHECK. Fixes #579.
|
|
17
|
+
- `tdk down` now removes what `tdk up <stack>` started. It ran `tilt down` without `--focus`, so the Tiltfile fell back to its default phase and did not register the stack's own compose project: the generated service container (`shop-autogenerated-orders-api-1`) kept running and only Traefik and Postgres were removed. It now passes every stack to the Tiltfile, and it stops the project's own `tilt up` process first (it kept running, and a live Tilt re-creates removed containers). Only a `tilt up` started with this project's Tiltfile is stopped. New `tdk down --prune-networks` also removes the project's Docker networks (`<project>_backend`, `_database`, `_traefik-public`, ...) that no container is attached to; without the flag they are kept, as before. `tdk doctor`'s "all predefined address pools are in use" fix now names it.
|
|
7
18
|
- `tdk config verify` no longer fails on a root `.tiltignore` the repository already had. `tdk project` never overwrote it, but verify compared it with the generated one and reported `Out of sync: .tiltignore`; it now warns `not generated by TDK, left as is`. A `.tiltignore` carrying TDK's generated header that was edited is still reported. Refs #568.
|
|
8
19
|
- A repo can pin the oldest CLI it works with: add `"minTdkVersion": "1.3.80"` to `.tdk/project.json`, and `tdk doctor` fails with `Run: tdk upgrade` on an older `tdk` (a value that is not `MAJOR.MINOR.PATCH` also fails). It is a floor, not an exact pin. Documented in `docs/upgrading.md`. Refs #564.
|
|
9
20
|
- `tdk doctor` warns when a backend or worker applies database migrations from its own start-up (a `start`/`dev` script, a Dockerfile `CMD`/`ENTRYPOINT`, a shell entrypoint or its source: Prisma, Drizzle, Knex, TypeORM, MikroORM, Sequelize, dbmate, Atlas, Alembic). It is a warning, not a failure: such services work, but migrations belong in a `migrator` resource that runs once before the services that depend on it. `migrator` and bring-your-own resources are not checked.
|
package/README.md
CHANGED
|
@@ -69,6 +69,15 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
|
|
|
69
69
|
- [Pilot TDK on a real repository](docs/adopt-tdk.md)
|
|
70
70
|
- [FAQ for agencies and multi-client teams](docs/faq-teams.md)
|
|
71
71
|
- [Working alongside Helm](docs/with-helm.md)
|
|
72
|
+
- [Scope: local development, and what does not carry over to production](docs/scope.md)
|
|
73
|
+
- [Gradual adoption: adding TDK to an existing repository](docs/gradual-adoption.md)
|
|
74
|
+
- [Troubleshooting: tdk and Tilt error messages and their fixes](docs/troubleshooting.md)
|
|
75
|
+
- [Layout and ownership: services in several repositories](docs/layout.md)
|
|
76
|
+
- [Local data: reset, seed, snapshot](docs/data.md)
|
|
77
|
+
- [Upgrading and version pinning](docs/upgrading.md)
|
|
78
|
+
- [Security: network calls, secrets and supply chain](docs/security.md)
|
|
79
|
+
- [Pilot scorecard](docs/pilot-scorecard.md)
|
|
80
|
+
- [Bring-your-own resources](docs/byo.md)
|
|
72
81
|
- [Configuration and editor schemas](docs/configuration.md)
|
|
73
82
|
- [Runnable one-backend example](examples/one-backend/README.md)
|
|
74
83
|
- [Runnable Python backend example](examples/one-backend-python/README.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
const BASH_COMPLETION = `# TDK CLI Bash Completion
|
|
@@ -250,12 +250,22 @@ const POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName t
|
|
|
250
250
|
const POWERSHELL_START = "# TDK-CLI-COMPLETION-START";
|
|
251
251
|
const POWERSHELL_END = "# TDK-CLI-COMPLETION-END";
|
|
252
252
|
export const completionCommand = new Command("completion")
|
|
253
|
+
.argument("[shell]", "Target shell (bash, zsh, fish, powershell)")
|
|
253
254
|
.description("Generate shell completion scripts")
|
|
254
|
-
.option("-s, --shell <shell>", "Target shell (bash, zsh, fish, powershell)"
|
|
255
|
+
.option("-s, --shell <shell>", "Target shell (bash, zsh, fish, powershell)")
|
|
255
256
|
.option("-o, --output <path>", "Output file path (default: stdout)")
|
|
256
257
|
.option("--install", "Install to shell config automatically")
|
|
257
|
-
.action((options) => {
|
|
258
|
-
const
|
|
258
|
+
.action((shellArgument, options) => {
|
|
259
|
+
const requestedShell = options.shell ?? shellArgument;
|
|
260
|
+
const environmentShell = basename(process.env.SHELL ?? "")
|
|
261
|
+
.replace(/\.exe$/i, "")
|
|
262
|
+
.toLowerCase();
|
|
263
|
+
const supportedShells = ["bash", "zsh", "fish", "powershell"];
|
|
264
|
+
const defaultShell = supportedShells.includes(environmentShell) ? environmentShell : "bash";
|
|
265
|
+
const shell = (requestedShell ?? defaultShell).toLowerCase();
|
|
266
|
+
if (requestedShell === undefined) {
|
|
267
|
+
console.error(chalk.gray(`Using ${shell} completion`));
|
|
268
|
+
}
|
|
259
269
|
let completionScript;
|
|
260
270
|
let filename;
|
|
261
271
|
switch (shell) {
|
|
@@ -116,7 +116,7 @@ export const configCommand = new Command("config")
|
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
console.log(chalk.blue("š Regenerating master configuration files...\n"));
|
|
119
|
-
await generateMasterConfigs(projectRoot);
|
|
119
|
+
await generateMasterConfigs(projectRoot, { discardHandEdits: true });
|
|
120
120
|
console.log(chalk.green("\nā
Configuration regenerated!"));
|
|
121
121
|
});
|
|
122
122
|
}))
|
|
@@ -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, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
12
|
+
import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, 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";
|
|
@@ -19,6 +19,7 @@ import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
|
19
19
|
import { isApiServiceType } from "../utils/resource-kind.js";
|
|
20
20
|
import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
|
|
21
21
|
import { discoverResourcesFromRoot } from "../utils/services.js";
|
|
22
|
+
import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
|
|
22
23
|
import { findOnPath } from "../utils/which.js";
|
|
23
24
|
export { checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, summarizeTiltBuildError, } from "../utils/doctor-runtime.js";
|
|
24
25
|
// A wedged Docker daemon makes `docker ps` block forever instead of failing,
|
|
@@ -431,15 +432,8 @@ export function checkTdkVersion(currentVersion = getPackageVersion()) {
|
|
|
431
432
|
message: "Not in a project - skipping TDK version check",
|
|
432
433
|
};
|
|
433
434
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const parsed = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
|
|
437
|
-
minimum = parsed?.minTdkVersion;
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
minimum = undefined;
|
|
441
|
-
}
|
|
442
|
-
if (minimum === undefined) {
|
|
435
|
+
const floor = evaluateTdkVersionFloor(projectRoot, currentVersion);
|
|
436
|
+
if (floor.status === "none") {
|
|
443
437
|
return {
|
|
444
438
|
name: "TDK version",
|
|
445
439
|
didPass: true,
|
|
@@ -447,27 +441,13 @@ export function checkTdkVersion(currentVersion = getPackageVersion()) {
|
|
|
447
441
|
message: `tdk ${currentVersion} (no minTdkVersion in .tdk/project.json)`,
|
|
448
442
|
};
|
|
449
443
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
name: "TDK version",
|
|
454
|
-
didPass: false,
|
|
455
|
-
message: `.tdk/project.json minTdkVersion ${JSON.stringify(minimum)} is not a version like "1.3.80"`,
|
|
456
|
-
fix: 'Set minTdkVersion to a MAJOR.MINOR.PATCH string, for example "1.3.80".',
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
if (!versionMeetsMinimum(currentVersion, required)) {
|
|
460
|
-
return {
|
|
461
|
-
name: "TDK version",
|
|
462
|
-
didPass: false,
|
|
463
|
-
message: `tdk ${currentVersion} is older than the ${required.join(".")} this project requires (minTdkVersion)`,
|
|
464
|
-
fix: "Run: tdk upgrade",
|
|
465
|
-
};
|
|
444
|
+
if (floor.status === "malformed" || floor.status === "too-old") {
|
|
445
|
+
return { name: "TDK version", didPass: false, message: floor.message, fix: floor.fix };
|
|
466
446
|
}
|
|
467
447
|
return {
|
|
468
448
|
name: "TDK version",
|
|
469
449
|
didPass: true,
|
|
470
|
-
message: `tdk ${currentVersion} meets minTdkVersion ${required
|
|
450
|
+
message: `tdk ${currentVersion} meets minTdkVersion ${floor.required}`,
|
|
471
451
|
};
|
|
472
452
|
}
|
|
473
453
|
/**
|
|
@@ -1158,6 +1138,8 @@ export const doctorCommand = new Command("doctor")
|
|
|
1158
1138
|
checkResourceDiscovery,
|
|
1159
1139
|
// Wiring mistakes that otherwise surface minutes into `tdk up`.
|
|
1160
1140
|
() => checkResourcePackageJson(),
|
|
1141
|
+
() => checkDuplicateResourceNames(),
|
|
1142
|
+
() => checkDuplicateResourcePorts(),
|
|
1161
1143
|
() => checkServiceUrlPorts(),
|
|
1162
1144
|
() => checkFrontendBackendUrls(),
|
|
1163
1145
|
() => checkNatsBroker(),
|
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { readProjectConfig } from "../generator/template-engine.js";
|
|
3
4
|
import { handleDryRun } from "../utils/command-helpers.js";
|
|
5
|
+
import { pruneProjectNetworks, stopProjectTiltUp, waitForTiltUpExit, } from "../utils/down-cleanup.js";
|
|
4
6
|
import { handleTiltFailure, withTiltCheck } from "../utils/errors.js";
|
|
5
7
|
import { createJsonEmitter } from "../utils/json-output.js";
|
|
6
|
-
import {
|
|
8
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
9
|
+
import { discoverResources, discoverStacks } from "../utils/services.js";
|
|
10
|
+
import { buildTiltDownArgs, getTiltfilePath, runTilt } from "../utils/tilt.js";
|
|
11
|
+
function allStackNames() {
|
|
12
|
+
try {
|
|
13
|
+
const names = discoverStacks(discoverResources()).map((stack) => stack.name);
|
|
14
|
+
return names.length > 0 ? names : undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function pruneNetworksOrWarn(json) {
|
|
21
|
+
const root = findProjectRoot();
|
|
22
|
+
if (!root)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
const result = pruneProjectNetworks(readProjectConfig(root).project.name);
|
|
26
|
+
if (!json) {
|
|
27
|
+
if (result.removed.length > 0) {
|
|
28
|
+
console.log(chalk.gray(`Removed networks: ${result.removed.join(", ")}`));
|
|
29
|
+
}
|
|
30
|
+
for (const kept of result.kept) {
|
|
31
|
+
console.log(chalk.yellow(`Kept network ${kept.name}: ${kept.reason}`));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (!json) {
|
|
38
|
+
console.log(chalk.yellow(`Could not remove networks: ${err.message}`));
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
7
43
|
export const downCommand = new Command("down")
|
|
8
44
|
.description("Stop all tilt resources")
|
|
9
45
|
.option("-v, --verbose", "Enable verbose output", false)
|
|
10
46
|
.option("-f, --force", "Skip confirmation", false)
|
|
47
|
+
.option("--prune-networks", "Also remove this project's Docker networks (only those no container is attached to)", false)
|
|
11
48
|
.option("--dry-run", "Show what would be stopped without stopping", false)
|
|
12
49
|
.option("--json", "Print one JSON object on stdout; implies quiet human output", false)
|
|
13
50
|
.action(async (options) => {
|
|
@@ -22,7 +59,22 @@ export const downCommand = new Command("down")
|
|
|
22
59
|
}
|
|
23
60
|
const tiltArgs = buildTiltDownArgs({
|
|
24
61
|
force: options.force,
|
|
62
|
+
focusTargets: allStackNames(),
|
|
25
63
|
});
|
|
64
|
+
// Stop `tilt up` first: `tilt down` does not end it, and a live Tilt re-creates the containers `tilt down` just removed.
|
|
65
|
+
let stoppedTilt = [];
|
|
66
|
+
try {
|
|
67
|
+
const tiltfile = getTiltfilePath();
|
|
68
|
+
stoppedTilt = stopProjectTiltUp(tiltfile);
|
|
69
|
+
if (stoppedTilt.length > 0)
|
|
70
|
+
await waitForTiltUpExit(tiltfile);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Not fatal: the containers are already down.
|
|
74
|
+
}
|
|
75
|
+
if (stoppedTilt.length > 0 && !options.json) {
|
|
76
|
+
console.log(chalk.gray(`Stopped tilt up (pid ${stoppedTilt.join(", ")}).`));
|
|
77
|
+
}
|
|
26
78
|
if (!options.json)
|
|
27
79
|
console.log(chalk.blue("Stopping all tilt resources..."));
|
|
28
80
|
if (options.verbose && !options.json) {
|
|
@@ -35,7 +87,15 @@ export const downCommand = new Command("down")
|
|
|
35
87
|
if (result.exitCode !== 0) {
|
|
36
88
|
handleTiltFailure("down", result.exitCode);
|
|
37
89
|
}
|
|
38
|
-
|
|
90
|
+
let networks;
|
|
91
|
+
if (options.pruneNetworks) {
|
|
92
|
+
networks = pruneNetworksOrWarn(options.json);
|
|
93
|
+
}
|
|
94
|
+
emit?.({
|
|
95
|
+
ok: true,
|
|
96
|
+
stoppedTilt,
|
|
97
|
+
...(networks ? { removedNetworks: networks.removed, keptNetworks: networks.kept } : {}),
|
|
98
|
+
});
|
|
39
99
|
});
|
|
40
100
|
});
|
|
41
101
|
//# sourceMappingURL=down.js.map
|
|
@@ -5,25 +5,44 @@ import { Command } from "commander";
|
|
|
5
5
|
import { confirmOrCancel } from "../utils/command-helpers.js";
|
|
6
6
|
import { runCommand } from "../utils/errors.js";
|
|
7
7
|
import { findProjectRoot } from "../utils/paths.js";
|
|
8
|
+
// Same location `tdk up` hands to Tilt (see getTiltfilePath in utils/tilt.ts), relative to the project root.
|
|
9
|
+
export const TILTFILE_RELATIVE_PATH = ".tdk/.tdk-out/Tiltfile";
|
|
10
|
+
export const TILT_UP_COMMAND = `tilt up -f ${TILTFILE_RELATIVE_PATH} -- --focus=<stack>`;
|
|
8
11
|
export const EJECTED_CONTENT = `# Ejected from TDK
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
\`tdk eject\` wrote this file and nothing else. It did not copy, move or generate anything, and it did
|
|
14
|
+
not remove TDK. What exists is what TDK had already generated, and that is still TDK-owned output.
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
- Tiltfile
|
|
14
|
-
- Dockerfiles
|
|
15
|
-
- compose files TDK wrote
|
|
16
|
-
- Traefik / proxy config TDK wrote
|
|
16
|
+
## Where the generated files are
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- service
|
|
18
|
+
- \`.tdk/.tdk-out/\` holds the Tiltfile and the engine it loads (\`tdk-cli-ext/\`). It is git-ignored.
|
|
19
|
+
\`tdk config regenerate\` rewrites the master files in it.
|
|
20
|
+
- The per-service Compose files (\`services/<stack>/<name>/.autogenerated/\`), the golden-layer
|
|
21
|
+
Dockerfile and the Traefik file (\`.tdk/.tdk-out/\`) are written by Tilt when it runs, not by
|
|
22
|
+
\`tdk eject\`. Until Tilt has run they do not exist. They are git-ignored too.
|
|
23
|
+
- Your own Dockerfiles, \`service.json\` files and source code are untouched.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## Running the stack without the tdk CLI
|
|
26
|
+
|
|
27
|
+
There is no Tiltfile at the project root, so a plain \`tilt up\` does not find one. TDK runs:
|
|
28
|
+
|
|
29
|
+
${TILT_UP_COMMAND}
|
|
30
|
+
|
|
31
|
+
from the project root (\`<stack>\` is a stack name under \`services/\`; without \`--focus\` the Tiltfile
|
|
32
|
+
falls back to its default phase). The Tiltfile still reads \`.tdk/project.json\` and the
|
|
33
|
+
\`service.json\` files, so those must stay. \`tdk up\` also does steps before Tilt starts (it chooses
|
|
34
|
+
host ports and exports them to Tilt); running Tilt by hand skips them.
|
|
35
|
+
|
|
36
|
+
## Cleaning up
|
|
37
|
+
|
|
38
|
+
Optional to delete later: this file, and \`.tdk/\` plus the \`service.json\` files if you stop using TDK.
|
|
39
|
+
Deleting them also removes what the Tiltfile needs.
|
|
40
|
+
|
|
41
|
+
To go back to TDK: install it and run \`tdk config regenerate\`.
|
|
23
42
|
`;
|
|
24
43
|
export const ejectCommand = new Command("eject")
|
|
25
|
-
.description("
|
|
26
|
-
.option("--dry-run", "List generated files without changing the project", false)
|
|
44
|
+
.description("Write EJECTED.md describing the generated Tilt files (copies and generates nothing; they stay in .tdk/.tdk-out/)")
|
|
45
|
+
.option("--dry-run", "List the generated files and what would be written, without changing the project", false)
|
|
27
46
|
.option("--yes", "Skip the confirmation prompt", false)
|
|
28
47
|
.action(async (options) => {
|
|
29
48
|
await runCommand(async () => {
|
|
@@ -48,15 +67,19 @@ export const ejectCommand = new Command("eject")
|
|
|
48
67
|
throw new Error("Generated Tilt files are missing. Run `tdk project --yes` before ejecting.");
|
|
49
68
|
}
|
|
50
69
|
const rootTiltfile = join(projectRoot, "Tiltfile");
|
|
51
|
-
|
|
52
|
-
files.push("Tiltfile");
|
|
70
|
+
const userTiltfile = existsSync(rootTiltfile) ? ["Tiltfile"] : [];
|
|
53
71
|
const ejectedPath = join(projectRoot, "EJECTED.md");
|
|
54
72
|
const willCreate = existsSync(ejectedPath) ? [] : ["EJECTED.md"];
|
|
55
73
|
if (options.dryRun) {
|
|
56
|
-
console.log("
|
|
74
|
+
console.log("Generated files, left where they are (git-ignored):");
|
|
57
75
|
for (const path of files)
|
|
58
76
|
console.log(` ${path}`);
|
|
59
|
-
|
|
77
|
+
if (userTiltfile.length > 0) {
|
|
78
|
+
console.log("Root Tiltfile, left where it is (not generated, not git-ignored):");
|
|
79
|
+
for (const path of userTiltfile)
|
|
80
|
+
console.log(` ${path}`);
|
|
81
|
+
}
|
|
82
|
+
console.log("Files written:");
|
|
60
83
|
for (const path of willCreate)
|
|
61
84
|
console.log(` ${path}`);
|
|
62
85
|
if (willCreate.length === 0)
|
|
@@ -64,13 +87,19 @@ export const ejectCommand = new Command("eject")
|
|
|
64
87
|
return;
|
|
65
88
|
}
|
|
66
89
|
if (!options.yes &&
|
|
67
|
-
!(await confirmOrCancel("
|
|
90
|
+
!(await confirmOrCancel("Write EJECTED.md? (Generated files stay in .tdk/.tdk-out/; nothing is copied or moved.)"))) {
|
|
68
91
|
return;
|
|
69
92
|
}
|
|
70
|
-
if (
|
|
93
|
+
if (existsSync(ejectedPath)) {
|
|
94
|
+
console.log(chalk.green("EJECTED.md already exists and was left unchanged. Nothing was copied, moved or generated."));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
71
97
|
writeFileSync(ejectedPath, EJECTED_CONTENT, { flag: "wx" });
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
console.log(chalk.green("Wrote EJECTED.md. Nothing was copied, moved or generated."));
|
|
99
|
+
}
|
|
100
|
+
console.log("Generated files stay in .tdk/.tdk-out/ (git-ignored) and still need TDK inputs.");
|
|
101
|
+
console.log(`Run from the project root: ${TILT_UP_COMMAND}`);
|
|
102
|
+
console.log("Replace <stack> with a stack name under services/, or omit --focus to use the Tiltfile's default phase.");
|
|
74
103
|
console.log("Read EJECTED.md");
|
|
75
104
|
});
|
|
76
105
|
});
|
|
@@ -128,6 +128,19 @@ export function createByoServiceJson(name, stack, port, options) {
|
|
|
128
128
|
...(options.restart ? { restart: options.restart } : {}),
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The Vite configs a frontend's `dev`/`build` scripts point at are written by the engine
|
|
133
|
+
* (Tilt loads the Starlark generator in `tdk up`), not by `tdk resource`. Without them
|
|
134
|
+
* `vite` stops with an esbuild "Could not resolve" stack trace, so the pre-scripts say what
|
|
135
|
+
* is missing instead. Inside the engine's image build the file exists and the check passes.
|
|
136
|
+
*/
|
|
137
|
+
function requireGeneratedViteConfig(configPath) {
|
|
138
|
+
// The text sits inside single quotes in a shell command: no quotes, backticks or parentheses.
|
|
139
|
+
const message = `${configPath} does not exist yet. TDK engine writes it when tdk up loads this resource ` +
|
|
140
|
+
"(needs Docker); tdk resource and tdk config regenerate do not create it. " +
|
|
141
|
+
"Run tdk up <stack> once, or point this script at your own Vite config to build on the host.";
|
|
142
|
+
return `bun -e 'if(!require("fs").existsSync(${JSON.stringify(configPath)})){console.error(${JSON.stringify(message)});process.exit(1)}'`;
|
|
143
|
+
}
|
|
131
144
|
export function createPackageJson(name, type, frameworkId) {
|
|
132
145
|
const isFrontend = type === "frontend";
|
|
133
146
|
const framework = resolveFrontendFramework(type, frameworkId);
|
|
@@ -137,6 +150,12 @@ export function createPackageJson(name, type, frameworkId) {
|
|
|
137
150
|
version: "0.0.1",
|
|
138
151
|
type: "module",
|
|
139
152
|
scripts: {
|
|
153
|
+
...(isFrontend
|
|
154
|
+
? {
|
|
155
|
+
predev: requireGeneratedViteConfig(".autogenerated/vite.config.frontend.autogenerated.ts"),
|
|
156
|
+
prebuild: requireGeneratedViteConfig(".autogenerated/vite.config.build.autogenerated.ts"),
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
140
159
|
dev: isFrontend
|
|
141
160
|
? "vite --config .autogenerated/vite.config.frontend.autogenerated.ts"
|
|
142
161
|
: "bun run --watch src/index.ts",
|
|
@@ -174,6 +193,10 @@ export function createResourceTsconfig(resourceType, frameworkId) {
|
|
|
174
193
|
...TSCONFIG_TEMPLATE,
|
|
175
194
|
compilerOptions: {
|
|
176
195
|
...TSCONFIG_TEMPLATE.compilerOptions,
|
|
196
|
+
// TypeScript 7 does not auto-include @types/*: without this, `tsc` fails with TS2591 on
|
|
197
|
+
// `process`/`Buffer`. "node" matches the generated Docker tsconfig. Frontends target the
|
|
198
|
+
// DOM and keep the default.
|
|
199
|
+
...(resourceType === "frontend" ? {} : { types: ["node"] }),
|
|
177
200
|
...(framework?.compilerOptions ?? {}),
|
|
178
201
|
...(backendFramework?.compilerOptions ?? {}),
|
|
179
202
|
},
|
|
@@ -197,15 +220,33 @@ export const TSCONFIG_TEMPLATE = {
|
|
|
197
220
|
include: ["src/**/*"],
|
|
198
221
|
exclude: ["node_modules", "dist"],
|
|
199
222
|
};
|
|
200
|
-
|
|
223
|
+
/**
|
|
224
|
+
* The plain `Dockerfile` written next to service.json. `tdk up` does not build it: for generated
|
|
225
|
+
* resources the engine builds `.autogenerated/Dockerfile.app.autogenerated`. It is a starting point
|
|
226
|
+
* for building the service image outside TDK, so its port follows the service's `port`.
|
|
227
|
+
*/
|
|
228
|
+
export function getDockerfileTemplate(port, healthCheckPath) {
|
|
229
|
+
// `oven/bun` has no curl, so the probe uses bun itself.
|
|
230
|
+
const healthcheck = healthCheckPath
|
|
231
|
+
? `# Health check
|
|
232
|
+
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \\
|
|
233
|
+
CMD bun -e "fetch('http://localhost:${port}${healthCheckPath}').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
|
|
234
|
+
|
|
235
|
+
`
|
|
236
|
+
: "";
|
|
237
|
+
return `# Scaffolded by \`tdk resource\`. \`tdk up\` does NOT build this file: it builds
|
|
238
|
+
# .autogenerated/Dockerfile.app.autogenerated (golden layers, project root as context).
|
|
239
|
+
# Use this one as a starting point for building the image outside TDK, and keep its
|
|
240
|
+
# port in step with "port" in service.json.
|
|
241
|
+
FROM oven/bun:1.2
|
|
201
242
|
|
|
202
243
|
WORKDIR /app
|
|
203
244
|
|
|
204
|
-
# Copy package files
|
|
205
|
-
COPY package.json bun.lock ./
|
|
245
|
+
# Copy package files (bun.lock is optional: a TDK workspace keeps it at the project root)
|
|
246
|
+
COPY package.json bun.lock* ./
|
|
206
247
|
|
|
207
248
|
# Install dependencies
|
|
208
|
-
RUN bun install
|
|
249
|
+
RUN bun install
|
|
209
250
|
|
|
210
251
|
# Copy source
|
|
211
252
|
COPY . .
|
|
@@ -213,14 +254,12 @@ COPY . .
|
|
|
213
254
|
# Build if needed
|
|
214
255
|
RUN bun run build
|
|
215
256
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
CMD curl -f http://localhost:3000/health || exit 1
|
|
219
|
-
|
|
220
|
-
EXPOSE 3000
|
|
257
|
+
${healthcheck}ENV PORT=${port}
|
|
258
|
+
EXPOSE ${port}
|
|
221
259
|
|
|
222
260
|
CMD ["bun", "run", "start"]
|
|
223
261
|
`;
|
|
262
|
+
}
|
|
224
263
|
export { getBackendIndexTemplate };
|
|
225
264
|
function getShutdownHandlerTemplate(signal) {
|
|
226
265
|
return `process.on('${signal}', () => {
|
|
@@ -234,7 +273,7 @@ export function getWorkerIndexTemplate(name) {
|
|
|
234
273
|
interface Job {
|
|
235
274
|
id: string;
|
|
236
275
|
type: string;
|
|
237
|
-
payload: Record<string,
|
|
276
|
+
payload: Record<string, unknown>;
|
|
238
277
|
priority?: number;
|
|
239
278
|
timestamp?: string;
|
|
240
279
|
}
|
|
@@ -328,7 +367,7 @@ export const resourceCommand = new Command("resource")
|
|
|
328
367
|
.option("-p, --path <path>", "Custom path for resource directory")
|
|
329
368
|
.option("--resource-path <path>", "Alias for --path (for backward compatibility)")
|
|
330
369
|
.option("--register-existing", "Register an existing resource without creating templates")
|
|
331
|
-
.option("-y, --yes", "
|
|
370
|
+
.option("-y, --yes", "Run without prompts; use defaults", false)
|
|
332
371
|
.option("--ddd", "Scaffold DDD (domain-driven design) folders + path aliases (Premium - requires TDK_LICENSE_KEY)")
|
|
333
372
|
.option("--dockerfile <path>", "Path to Dockerfile relative to resource dir (for bring-your-own)")
|
|
334
373
|
.option("--health-path <path>", "HTTP health check path (for bring-your-own)", "/health")
|
|
@@ -350,6 +389,11 @@ export const resourceCommand = new Command("resource")
|
|
|
350
389
|
showCommandHeader("Resource Creation");
|
|
351
390
|
const allResources = discoverResources();
|
|
352
391
|
let resourceName = name;
|
|
392
|
+
if (!resourceName && options.yes) {
|
|
393
|
+
throw new TdkError("A resource name is required when using --yes.", [
|
|
394
|
+
"Pass the name explicitly, for example: tdk resource api --yes.",
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
353
397
|
if (!resourceName) {
|
|
354
398
|
const inputName = await promptText({
|
|
355
399
|
message: "Resource name (kebab-case):",
|
|
@@ -411,27 +455,40 @@ export const resourceCommand = new Command("resource")
|
|
|
411
455
|
}
|
|
412
456
|
const existingStacks = Array.from(stackSet);
|
|
413
457
|
if (existingStacks.length > 0) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (selectedStack === "__new__") {
|
|
422
|
-
const newStack = await promptText({
|
|
423
|
-
message: "New stack name:",
|
|
424
|
-
validate: (input) => {
|
|
425
|
-
const validation = createKebabCaseValidator("stack")(input);
|
|
426
|
-
return validation === true || validation;
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
stackName = newStack;
|
|
458
|
+
if (options.yes) {
|
|
459
|
+
if (existingStacks.length > 1) {
|
|
460
|
+
throw new TdkError("Multiple stacks exist; specify --stack when using --yes.", [
|
|
461
|
+
`Available stacks: ${existingStacks.join(", ")}`,
|
|
462
|
+
]);
|
|
463
|
+
}
|
|
464
|
+
stackName = existingStacks[0] ?? "main";
|
|
430
465
|
}
|
|
431
466
|
else {
|
|
432
|
-
|
|
467
|
+
const selectedStack = await promptSelect({
|
|
468
|
+
message: "Assign to stack:",
|
|
469
|
+
choices: [
|
|
470
|
+
...existingStacks.map((s) => ({ title: s, value: s })),
|
|
471
|
+
{ title: "Create new stack", value: "__new__" },
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
if (selectedStack === "__new__") {
|
|
475
|
+
const newStack = await promptText({
|
|
476
|
+
message: "New stack name:",
|
|
477
|
+
validate: (input) => {
|
|
478
|
+
const validation = createKebabCaseValidator("stack")(input);
|
|
479
|
+
return validation === true || validation;
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
stackName = newStack;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
stackName = selectedStack;
|
|
486
|
+
}
|
|
433
487
|
}
|
|
434
488
|
}
|
|
489
|
+
else if (options.yes) {
|
|
490
|
+
stackName = "main";
|
|
491
|
+
}
|
|
435
492
|
else {
|
|
436
493
|
const newStack = await promptText({
|
|
437
494
|
message: "Stack name (first resource):",
|
|
@@ -651,7 +708,9 @@ This file contains the resource configuration for TDK.
|
|
|
651
708
|
{
|
|
652
709
|
type: "text",
|
|
653
710
|
filename: "Dockerfile",
|
|
654
|
-
content:
|
|
711
|
+
content: getDockerfileTemplate(serviceJson.port, typeof serviceJson.healthCheckPath === "string"
|
|
712
|
+
? serviceJson.healthCheckPath
|
|
713
|
+
: undefined),
|
|
655
714
|
description: "Generating Dockerfile",
|
|
656
715
|
emoji: "š³",
|
|
657
716
|
},
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { VALID_RESOURCE_TYPES } from "../utils/constants.js";
|
|
3
4
|
import { createDiscoveryContext } from "../utils/discovery-context.js";
|
|
4
|
-
import { errorFactories, requireProjectRoot, runCommand } from "../utils/errors.js";
|
|
5
|
+
import { errorFactories, requireProjectRoot, runCommand, TdkError } from "../utils/errors.js";
|
|
5
6
|
import { formatCount, showAllSatisfyCondition, showDetail, showEmptyState, showStep, } from "../utils/formatting.js";
|
|
6
7
|
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
7
8
|
import { findProjectRoot } from "../utils/paths.js";
|
|
8
9
|
import { stackExists } from "../utils/services.js";
|
|
10
|
+
import { includes } from "../utils/validation.js";
|
|
9
11
|
export const resourcesCommand = new Command("resources")
|
|
10
12
|
.description("List all resources (services) in the project")
|
|
11
13
|
.option("-v, --verbose", "Show detailed information about each resource", false)
|
|
12
14
|
.option("-s, --stack <stack>", "Filter resources by stack name")
|
|
15
|
+
.option("-t, --type <type>", "Filter resources by type")
|
|
13
16
|
.option("--no-stack", "Show only resources without a stack")
|
|
14
17
|
.option("--ports", "Show port assignments", false)
|
|
15
18
|
.option("--json", "Output a versioned JSON resource report", false)
|
|
@@ -23,6 +26,11 @@ export const resourcesCommand = new Command("resources")
|
|
|
23
26
|
else {
|
|
24
27
|
requireProjectRoot();
|
|
25
28
|
}
|
|
29
|
+
if (options.type !== undefined && !includes(VALID_RESOURCE_TYPES, options.type)) {
|
|
30
|
+
throw new TdkError(`Invalid resource type "${options.type}"`, [
|
|
31
|
+
`Valid types: ${VALID_RESOURCE_TYPES.join(", ")}`,
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
26
34
|
const discovery = createDiscoveryContext();
|
|
27
35
|
if (options.stack && !stackExists(options.stack, discovery.resources)) {
|
|
28
36
|
const error = errorFactories.stackNotFound(options.stack);
|
|
@@ -34,18 +42,21 @@ export const resourcesCommand = new Command("resources")
|
|
|
34
42
|
let resources = discovery.resources;
|
|
35
43
|
if (options.stack) {
|
|
36
44
|
resources = discovery.resourcesByStack.get(options.stack) || [];
|
|
37
|
-
if (resources.length === 0 && !options.json) {
|
|
45
|
+
if (resources.length === 0 && !options.json && options.type === undefined) {
|
|
38
46
|
showEmptyState("stack-services", ` in stack "${options.stack}"`);
|
|
39
47
|
return;
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
if (withoutStackFilter) {
|
|
43
51
|
resources = discovery.unassignedResources;
|
|
44
|
-
if (resources.length === 0 && !options.json) {
|
|
52
|
+
if (resources.length === 0 && !options.json && options.type === undefined) {
|
|
45
53
|
showAllSatisfyCondition("resources", "assigned to a stack");
|
|
46
54
|
return;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
57
|
+
if (options.type !== undefined) {
|
|
58
|
+
resources = resources.filter((resource) => (resource.config?.appType ?? resource.type) === options.type);
|
|
59
|
+
}
|
|
49
60
|
if (options.json) {
|
|
50
61
|
const data = resources.map((resource) => ({
|
|
51
62
|
name: resource.name,
|
|
@@ -58,7 +69,10 @@ export const resourcesCommand = new Command("resources")
|
|
|
58
69
|
return;
|
|
59
70
|
}
|
|
60
71
|
if (resources.length === 0) {
|
|
61
|
-
|
|
72
|
+
const stackContext = options.stack ? ` in stack "${options.stack}"` : "";
|
|
73
|
+
const noStackContext = withoutStackFilter && !options.stack ? " without a stack" : "";
|
|
74
|
+
const typeContext = options.type !== undefined ? ` with type "${options.type}"` : "";
|
|
75
|
+
showEmptyState(options.stack ? "stack-services" : "resources", stackContext + noStackContext + typeContext);
|
|
62
76
|
return;
|
|
63
77
|
}
|
|
64
78
|
showStep(`Found ${formatCount(resources.length, "resource")}:\n`);
|
|
@@ -90,7 +104,7 @@ export const resourcesCommand = new Command("resources")
|
|
|
90
104
|
}
|
|
91
105
|
showDetail("\nRun with --verbose for more details or --ports to see port assignments.", 0);
|
|
92
106
|
}
|
|
93
|
-
const withoutStackCount = options.stack
|
|
107
|
+
const withoutStackCount = options.stack || options.type !== undefined
|
|
94
108
|
? resources.filter((r) => !r.stack).length
|
|
95
109
|
: discovery.unassignedResources.length;
|
|
96
110
|
if (withoutStackCount > 0 && !withoutStackFilter && !options.stack) {
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -12,12 +12,13 @@ import { formatCount } from "../utils/formatting.js";
|
|
|
12
12
|
import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "../utils/host-port-config.js";
|
|
13
13
|
import { formatHostPortPlan } from "../utils/host-port-plan.js";
|
|
14
14
|
import { createJsonEmitter } from "../utils/json-output.js";
|
|
15
|
-
import { findProjectRoot } from "../utils/paths.js";
|
|
15
|
+
import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
16
16
|
import { findAvailablePort } from "../utils/port-assignment.js";
|
|
17
17
|
import { isApiServiceType } from "../utils/resource-kind.js";
|
|
18
18
|
import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
|
|
19
19
|
import { discoverResources, discoverResourcesStrict, discoverStacks, stackExists, } from "../utils/services.js";
|
|
20
20
|
import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smoke.js";
|
|
21
|
+
import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
|
|
21
22
|
import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
|
|
22
23
|
import { stopTiltOnPort } from "../utils/tilt-process.js";
|
|
23
24
|
import { parseTiltPort, resolveTiltPort, stopTiltForUp } from "../utils/tilt-startup.js";
|
|
@@ -96,6 +97,24 @@ export function enforceDriftGate(projectRoot, options, exit = process.exit) {
|
|
|
96
97
|
console.error(chalk.red(line));
|
|
97
98
|
exit(DRIFT_EXIT_CODE);
|
|
98
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Runs before anything starts (also under --dry-run, like the drift gate): a project's `minTdkVersion` in
|
|
102
|
+
* .tdk/project.json that this CLI does not meet, or cannot read, exits with 1 and the same text as `tdk doctor`.
|
|
103
|
+
* `ignoreVersion` skips the check and says so. No minTdkVersion means no check.
|
|
104
|
+
*/
|
|
105
|
+
export function enforceVersionFloor(projectRoot, options, currentVersion = getPackageVersion(), exit = process.exit) {
|
|
106
|
+
if (options.ignoreVersion) {
|
|
107
|
+
console.warn(chalk.yellow("Warning: --ignore-version set; minTdkVersion was not checked."));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const floor = evaluateTdkVersionFloor(projectRoot, currentVersion);
|
|
111
|
+
if (floor.status === "none" || floor.status === "ok")
|
|
112
|
+
return;
|
|
113
|
+
options.onFail?.(`${floor.message}\n${floor.fix}`);
|
|
114
|
+
console.error(chalk.red(floor.message));
|
|
115
|
+
console.error(chalk.red(floor.fix));
|
|
116
|
+
exit(1);
|
|
117
|
+
}
|
|
99
118
|
function waitForTiltUi(port, timeoutMs = 30_000) {
|
|
100
119
|
return new Promise((resolve) => {
|
|
101
120
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -127,6 +146,7 @@ export const upCommand = new Command("up")
|
|
|
127
146
|
.option("--dry-run", "Show what would be started without starting", false)
|
|
128
147
|
.option("-f, --force", "Kill existing Tilt process before starting", false)
|
|
129
148
|
.option("--ignore-drift", "Start even if generated files differ from service.json", false)
|
|
149
|
+
.option("--ignore-version", "Start even if this tdk is older than minTdkVersion in .tdk/project.json", false)
|
|
130
150
|
.option("--only <services...>", "Start only these services plus what the Tiltfile enables for them (their dependsOn services and shared infrastructure). With a stack, names must belong to that stack; dependencies may cross stacks")
|
|
131
151
|
.option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
|
|
132
152
|
.action(async (stackName, options) => {
|
|
@@ -139,6 +159,14 @@ export const upCommand = new Command("up")
|
|
|
139
159
|
showErrorAndExit(portSetting.message);
|
|
140
160
|
}
|
|
141
161
|
const configuredTiltPort = portSetting.port;
|
|
162
|
+
// First, before the host/Docker checks below: an old CLI must stop with one clear line, not a half-working stack.
|
|
163
|
+
const versionRoot = findProjectRoot();
|
|
164
|
+
if (versionRoot) {
|
|
165
|
+
enforceVersionFloor(versionRoot, {
|
|
166
|
+
ignoreVersion: options.ignoreVersion,
|
|
167
|
+
onFail: (message) => emit?.({ ok: false }, [{ code: "TDK_VERSION_TOO_OLD", message }]),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
142
170
|
const hostRefusal = options.dryRun ? null : await hostUpRefusal();
|
|
143
171
|
if (hostRefusal) {
|
|
144
172
|
emit?.({ ok: false }, [{ code: "HOST_UNSUPPORTED", message: hostRefusal }]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { accessSync, chmodSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, resolve, win32 } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import chalk from "chalk";
|
|
@@ -143,6 +143,10 @@ async function getLatestBinaryRelease() {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
function isPermissionError(err) {
|
|
147
|
+
const code = err?.code;
|
|
148
|
+
return code === "EACCES" || code === "EPERM";
|
|
149
|
+
}
|
|
146
150
|
export function isWritable(path) {
|
|
147
151
|
try {
|
|
148
152
|
accessSync(path, constants.W_OK);
|
|
@@ -210,6 +214,10 @@ export async function upgradeViaBinary(tdkPath, release) {
|
|
|
210
214
|
const sums = parseChecksums(await checksumResponse.text());
|
|
211
215
|
verifyChecksum(tmpPath, release.assetName, sums, true);
|
|
212
216
|
verifyChecksum(engineTmpPath, ENGINE_ASSET_NAME, sums, false);
|
|
217
|
+
// writeFileSync creates the file 0644; without this the swapped-in binary
|
|
218
|
+
// is not executable and the shell reports "Permission denied".
|
|
219
|
+
if (!isWindows())
|
|
220
|
+
chmodSync(tmpPath, 0o755);
|
|
213
221
|
mkdirSync(engineStageDir, { recursive: true });
|
|
214
222
|
extractTarball(engineBuffer, engineStageDir);
|
|
215
223
|
if (existsSync(tdkPath))
|
|
@@ -622,11 +630,18 @@ export const upgradeCommand = new Command("upgrade")
|
|
|
622
630
|
console.log();
|
|
623
631
|
console.log(chalk.yellow("ā ļø Upgrade status unknown - verification failed"));
|
|
624
632
|
console.log();
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
633
|
+
if (isPermissionError(err) && installInfo.method !== "git" && installInfo.path) {
|
|
634
|
+
console.log(chalk.yellow("š” The installed binary isn't executable. Fix it with:"));
|
|
635
|
+
console.log(chalk.cyan(` chmod +x ${installInfo.path}`));
|
|
636
|
+
console.log(chalk.white(" then run: tdk version"));
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
console.log(chalk.yellow("š” Verify manually:"));
|
|
640
|
+
console.log(chalk.white(" 1. Restart your terminal"));
|
|
641
|
+
console.log(chalk.white(" 2. Run: tdk version"));
|
|
642
|
+
if (installInfo.method === "git" && installInfo.path) {
|
|
643
|
+
console.log(chalk.white(` 3. Compare with: git -C ${installInfo.path} rev-parse HEAD`));
|
|
644
|
+
}
|
|
630
645
|
}
|
|
631
646
|
}
|
|
632
647
|
});
|
|
@@ -407,7 +407,7 @@ function ensureRootWorkspaceManifest(projectRoot, projectConfig) {
|
|
|
407
407
|
writeTextFile(packageJsonPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
408
408
|
return true;
|
|
409
409
|
}
|
|
410
|
-
export async function generateMasterConfigs(projectRoot) {
|
|
410
|
+
export async function generateMasterConfigs(projectRoot, options = {}) {
|
|
411
411
|
const projectConfig = readProjectConfig(projectRoot);
|
|
412
412
|
await warnIfSablierUnlicensed(projectRoot);
|
|
413
413
|
if (ensureRootWorkspaceManifest(projectRoot, projectConfig)) {
|
|
@@ -455,9 +455,21 @@ export async function generateMasterConfigs(projectRoot) {
|
|
|
455
455
|
// Copy .tiltignore to project root so Tilt uses it
|
|
456
456
|
const tiltignoreSource = path.join(outputDir, ".tiltignore");
|
|
457
457
|
const tiltignoreTarget = assertTdkGeneratedPath(projectRoot, ".tiltignore");
|
|
458
|
-
if (fs.existsSync(tiltignoreSource)
|
|
459
|
-
fs.
|
|
460
|
-
|
|
458
|
+
if (fs.existsSync(tiltignoreSource)) {
|
|
459
|
+
if (!fs.existsSync(tiltignoreTarget)) {
|
|
460
|
+
fs.copyFileSync(tiltignoreSource, tiltignoreTarget);
|
|
461
|
+
console.log(`ā Copied: .tiltignore ā project root`);
|
|
462
|
+
}
|
|
463
|
+
else if (options.discardHandEdits) {
|
|
464
|
+
// Only a file TDK wrote (it carries the header) may be restored; one without the
|
|
465
|
+
// header is the team's own and `verifyMasterConfigs` reports it as a warning.
|
|
466
|
+
const current = fs.readFileSync(tiltignoreTarget, "utf-8");
|
|
467
|
+
if (current.includes(TDK_GENERATED_MARKER) &&
|
|
468
|
+
current !== fs.readFileSync(tiltignoreSource, "utf-8")) {
|
|
469
|
+
fs.copyFileSync(tiltignoreSource, tiltignoreTarget);
|
|
470
|
+
console.log("ā Restored: .tiltignore ā project root (hand edits discarded)");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
461
473
|
}
|
|
462
474
|
const copiedAssets = ensureProjectRuntimeAssets(projectRoot);
|
|
463
475
|
for (const asset of copiedAssets) {
|
|
@@ -53,9 +53,18 @@ function ownsRuntimeFiles(resource) {
|
|
|
53
53
|
Object.hasOwn(BACKEND_LANGUAGES, language) &&
|
|
54
54
|
BACKEND_LANGUAGES[language].createFiles !== undefined);
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A bring-your-own resource is a plain container: the engine builds the image from the resource's own `dockerfile`
|
|
58
|
+
* (or its `buildContext`), or pulls `image`, and never generates the golden-layer Dockerfile that COPYs a package.json.
|
|
59
|
+
*/
|
|
60
|
+
function isBringYourOwn(resource) {
|
|
61
|
+
return (resource.config?.appType ?? resource.type) === "bring-your-own";
|
|
62
|
+
}
|
|
56
63
|
export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
57
64
|
const missing = discoverResourcesFromRoot(projectRoot)
|
|
58
|
-
.filter((resource) => !
|
|
65
|
+
.filter((resource) => !isBringYourOwn(resource) &&
|
|
66
|
+
!ownsRuntimeFiles(resource) &&
|
|
67
|
+
!existsSync(join(resource.path, "package.json")))
|
|
59
68
|
.map((resource) => resource.name);
|
|
60
69
|
if (missing.length === 0) {
|
|
61
70
|
return {
|
|
@@ -71,6 +80,73 @@ export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? proc
|
|
|
71
80
|
fix: "Add a package.json next to each service.json (copy one from a working resource), or delete the folder's service.json if it is not a service",
|
|
72
81
|
};
|
|
73
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Two resources with one appName share a Compose service name and Traefik router and service names
|
|
85
|
+
* (all built from the name, none from the directory), so one of them is lost or misrouted.
|
|
86
|
+
*/
|
|
87
|
+
export function checkDuplicateResourceNames(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
88
|
+
const byName = new Map();
|
|
89
|
+
for (const resource of discoverResourcesFromRoot(projectRoot)) {
|
|
90
|
+
const paths = byName.get(resource.name) ?? [];
|
|
91
|
+
paths.push(relative(projectRoot, resource.configPath) || resource.configPath);
|
|
92
|
+
byName.set(resource.name, paths);
|
|
93
|
+
}
|
|
94
|
+
const duplicates = [...byName].filter(([, paths]) => paths.length > 1);
|
|
95
|
+
if (duplicates.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
name: "Duplicate resource names",
|
|
98
|
+
didPass: true,
|
|
99
|
+
message: "Every resource has a unique appName",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const lines = duplicates.map(([name, paths]) => `"${name}" in ${paths.join(" and ")}`);
|
|
103
|
+
return {
|
|
104
|
+
name: "Duplicate resource names",
|
|
105
|
+
didPass: false,
|
|
106
|
+
message: `${formatCount(duplicates.length, "appName")} used by more than one resource:\n ${lines.join("\n ")}`,
|
|
107
|
+
fix: 'Give each resource its own "appName" in its service.json. The name becomes the Compose service and the Traefik router, so two resources with one name collide',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** A resource that gets a Traefik route to its own `port`: a backend, an mcp server, or a bring-your-own resource that is not opted out. */
|
|
111
|
+
function isRoutedToPort(resource) {
|
|
112
|
+
const appType = resource.config?.appType;
|
|
113
|
+
if (isApiServiceType(appType))
|
|
114
|
+
return true;
|
|
115
|
+
return appType === "bring-your-own" && resource.config?.exposeViaProxy !== false;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Two routed resources on one `port`. This is a warning, not a failure: the engine publishes no service
|
|
119
|
+
* port on the host and gives each resource its own container, which Traefik reaches as <container>:<port>,
|
|
120
|
+
* so two containers can listen on 4000 at once. The pair still confuses the checks and URLs that identify
|
|
121
|
+
* a backend by its port, and usually means a copied service.json.
|
|
122
|
+
*/
|
|
123
|
+
export function checkDuplicateResourcePorts(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
124
|
+
const byPort = new Map();
|
|
125
|
+
for (const resource of discoverResourcesFromRoot(projectRoot)) {
|
|
126
|
+
const port = resource.config?.port;
|
|
127
|
+
if (typeof port !== "number" || !isRoutedToPort(resource))
|
|
128
|
+
continue;
|
|
129
|
+
const names = byPort.get(port) ?? [];
|
|
130
|
+
names.push(resource.name);
|
|
131
|
+
byPort.set(port, names);
|
|
132
|
+
}
|
|
133
|
+
const shared = [...byPort].filter(([, names]) => names.length > 1);
|
|
134
|
+
if (shared.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
name: "Duplicate resource ports",
|
|
137
|
+
didPass: true,
|
|
138
|
+
message: "No two routed resources use the same port",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const lines = shared.map(([port, names]) => `port ${port}: ${names.join(", ")}`);
|
|
142
|
+
return {
|
|
143
|
+
name: "Duplicate resource ports",
|
|
144
|
+
didPass: false,
|
|
145
|
+
isWarning: true,
|
|
146
|
+
message: `${formatCount(shared.length, "port")} used by more than one routed resource:\n ${lines.join("\n ")}`,
|
|
147
|
+
fix: 'Usually a copied service.json. Each container can listen on the same port, but pick distinct "port" values (tdk resource assigns free ones) so URLs like http://<resource>:<port> and localhost checks stay unambiguous',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
74
150
|
const CONTAINER_URL = /https?:\/\/([a-z][a-z0-9-]*):(\d+)/g;
|
|
75
151
|
export function checkServiceUrlPorts(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
76
152
|
const resources = discoverResourcesFromRoot(projectRoot);
|
|
@@ -345,7 +421,7 @@ export async function checkDockerNetworkCapacity(exec = execAsync) {
|
|
|
345
421
|
name: "Docker networks",
|
|
346
422
|
didPass: false,
|
|
347
423
|
message: "Docker cannot create another network: all predefined address pools are in use. Each TDK project needs several",
|
|
348
|
-
fix: "
|
|
424
|
+
fix: "run tdk down --prune-networks in projects you are not using, or docker network prune (removes all unused networks)",
|
|
349
425
|
};
|
|
350
426
|
}
|
|
351
427
|
return {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const run = (command, args) => execFileSync(command, args, { encoding: "utf-8", stdio: "pipe", windowsHide: true });
|
|
3
|
+
/** Network names the engine creates per project (see engine/.../docker/constants.star and init-networks). */
|
|
4
|
+
const PROJECT_NETWORK_SUFFIXES = [
|
|
5
|
+
"traefik-public",
|
|
6
|
+
"traefik-private",
|
|
7
|
+
"backend",
|
|
8
|
+
"database",
|
|
9
|
+
"infisical-network",
|
|
10
|
+
"verdaccio-network",
|
|
11
|
+
"proxy",
|
|
12
|
+
];
|
|
13
|
+
/** The engine prefixes networks with the project name, hyphens turned into underscores. */
|
|
14
|
+
export function projectNetworkNames(projectName) {
|
|
15
|
+
const prefix = projectName.replace(/-/g, "_");
|
|
16
|
+
return PROJECT_NETWORK_SUFFIXES.map((suffix) => `${prefix}_${suffix}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Remove this project's own networks. Only the exact names the engine creates are touched, and `docker network rm`
|
|
20
|
+
* itself refuses a network that still has containers attached, so another project's networks and in-use networks
|
|
21
|
+
* are never removed.
|
|
22
|
+
*/
|
|
23
|
+
export function pruneProjectNetworks(projectName, commandRunner = run) {
|
|
24
|
+
const existing = new Set(commandRunner("docker", ["network", "ls", "--format", "{{.Name}}"])
|
|
25
|
+
.split(/\r?\n/)
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean));
|
|
28
|
+
const result = { removed: [], kept: [] };
|
|
29
|
+
for (const name of projectNetworkNames(projectName)) {
|
|
30
|
+
if (!existing.has(name))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
commandRunner("docker", ["network", "rm", name]);
|
|
34
|
+
result.removed.push(name);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const stderr = String(err.stderr ?? err).trim();
|
|
38
|
+
result.kept.push({ name, reason: stderr.split(/\r?\n/)[0] ?? "" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* PIDs of `tilt up` processes started for this project's Tiltfile. Matching on the Tiltfile path keeps another
|
|
45
|
+
* project's Tilt (or a Tilt the user started by hand) untouched.
|
|
46
|
+
*/
|
|
47
|
+
export function findProjectTiltUpPids(tiltfilePath, platform = process.platform, commandRunner = run) {
|
|
48
|
+
if (platform === "win32")
|
|
49
|
+
return [];
|
|
50
|
+
let output;
|
|
51
|
+
try {
|
|
52
|
+
output = commandRunner("ps", ["-axo", "pid=,command="]);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const pids = [];
|
|
58
|
+
for (const line of output.split(/\r?\n/)) {
|
|
59
|
+
const match = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
60
|
+
if (!match)
|
|
61
|
+
continue;
|
|
62
|
+
const command = match[2] ?? "";
|
|
63
|
+
const exe = (command.split(/\s+/)[0] ?? "").split("/").at(-1);
|
|
64
|
+
const target = ` -f ${tiltfilePath}`;
|
|
65
|
+
const at = command.indexOf(target);
|
|
66
|
+
const boundary = command[at + target.length];
|
|
67
|
+
if (exe === "tilt" &&
|
|
68
|
+
/^\S+\s+up\s/.test(command) &&
|
|
69
|
+
at > 0 &&
|
|
70
|
+
(boundary === undefined || boundary === " ")) {
|
|
71
|
+
pids.push(Number(match[1]));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return pids;
|
|
75
|
+
}
|
|
76
|
+
export function stopProjectTiltUp(tiltfilePath, platform = process.platform, commandRunner = run) {
|
|
77
|
+
const pids = findProjectTiltUpPids(tiltfilePath, platform, commandRunner);
|
|
78
|
+
for (const pid of pids) {
|
|
79
|
+
try {
|
|
80
|
+
commandRunner("kill", ["-TERM", String(pid)]);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Already gone.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return pids;
|
|
87
|
+
}
|
|
88
|
+
/** Wait (up to `timeoutMs`) for the `tilt up` processes to exit after TERM, so `tilt down` does not race a live Tilt. */
|
|
89
|
+
export async function waitForTiltUpExit(tiltfilePath, timeoutMs = 8000, findPids = findProjectTiltUpPids) {
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
while (findPids(tiltfilePath).length > 0) {
|
|
92
|
+
if (Date.now() >= deadline)
|
|
93
|
+
return false;
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=down-cleanup.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function toParts(raw) {
|
|
4
|
+
const match = raw.trim().match(/(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
5
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)] : null;
|
|
6
|
+
}
|
|
7
|
+
function readMinTdkVersion(projectRoot) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
|
|
10
|
+
return parsed?.minTdkVersion;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function evaluateTdkVersionFloor(projectRoot, currentVersion) {
|
|
17
|
+
const minimum = readMinTdkVersion(projectRoot);
|
|
18
|
+
if (minimum === undefined)
|
|
19
|
+
return { status: "none" };
|
|
20
|
+
const required = typeof minimum === "string" ? toParts(minimum) : null;
|
|
21
|
+
if (!required || !/^\d+\.\d+\.\d+$/.test(String(minimum).trim())) {
|
|
22
|
+
return {
|
|
23
|
+
status: "malformed",
|
|
24
|
+
message: `.tdk/project.json minTdkVersion ${JSON.stringify(minimum)} is not a version like "1.3.80"`,
|
|
25
|
+
fix: 'Set minTdkVersion to a MAJOR.MINOR.PATCH string, for example "1.3.80".',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const current = toParts(currentVersion);
|
|
29
|
+
const meets = current !== null &&
|
|
30
|
+
(() => {
|
|
31
|
+
for (const [index, need] of required.entries()) {
|
|
32
|
+
const actual = current[index] ?? 0;
|
|
33
|
+
if (actual !== need)
|
|
34
|
+
return actual > need;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
})();
|
|
38
|
+
const requiredText = required.join(".");
|
|
39
|
+
if (!meets) {
|
|
40
|
+
return {
|
|
41
|
+
status: "too-old",
|
|
42
|
+
required: requiredText,
|
|
43
|
+
message: `tdk ${currentVersion} is older than the ${requiredText} this project requires (minTdkVersion)`,
|
|
44
|
+
fix: "Run: tdk upgrade",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { status: "ok", required: requiredText };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=tdk-version.js.map
|
package/cli/dist/utils/tilt.js
CHANGED
|
@@ -92,6 +92,12 @@ export function buildTiltDownArgs(options = {}) {
|
|
|
92
92
|
if (options.force) {
|
|
93
93
|
args.push("--force");
|
|
94
94
|
}
|
|
95
|
+
// `tilt down` re-evaluates the Tiltfile and removes only the resources that evaluation registers. Without `--focus`
|
|
96
|
+
// the Tiltfile falls back to its default phase and never registers a stack's own compose project, so `tdk up shop`
|
|
97
|
+
// containers survived `tdk down`. Pass the same selection `tdk up` used (every stack) after `--`.
|
|
98
|
+
if (options.focusTargets && options.focusTargets.length > 0) {
|
|
99
|
+
args.push("--", `--focus=${options.focusTargets.join(",")}`);
|
|
100
|
+
}
|
|
95
101
|
return args;
|
|
96
102
|
}
|
|
97
103
|
//# sourceMappingURL=tilt.js.map
|
package/cli/package.json
CHANGED
|
@@ -7,7 +7,8 @@ def fix_docker_networks(platform_networks):
|
|
|
7
7
|
"""
|
|
8
8
|
Generates a shell command that:
|
|
9
9
|
1. Removes networks with incorrect labels (if they exist and are not in use)
|
|
10
|
-
2. Creates all platform networks if they don't exist
|
|
10
|
+
2. Creates all platform networks if they don't exist, and fails with Docker's own
|
|
11
|
+
error if one that does not exist cannot be created
|
|
11
12
|
"""
|
|
12
13
|
commands = []
|
|
13
14
|
commands.append('echo "Initializing Docker networks..."')
|
|
@@ -19,7 +20,12 @@ def fix_docker_networks(platform_networks):
|
|
|
19
20
|
check_and_remove += 'docker network rm ' + network + ' 2>/dev/null) || true'
|
|
20
21
|
commands.append(check_and_remove)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
# Create the network. A failed create is an error unless the network exists
|
|
24
|
+
# afterwards (already there, or another process created it first). Docker's own
|
|
25
|
+
# message is printed and the command exits non-zero, so Tilt marks init-networks failed.
|
|
26
|
+
create_cmd = 'if _out=$(docker network create ' + network + ' 2>&1); then echo "Created network ' + network + '"; '
|
|
27
|
+
create_cmd += 'elif docker network inspect ' + network + ' >/dev/null 2>&1; then :; '
|
|
28
|
+
create_cmd += 'else echo "Failed to create Docker network ' + network + ': $_out" >&2; exit 1; fi'
|
|
23
29
|
commands.append(create_cmd)
|
|
24
30
|
|
|
25
31
|
commands.append('echo "Docker networks initialized"')
|