@tdk-landscape/tdk-cli-core 1.3.108 → 1.3.110
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 +2 -0
- package/README.md +1 -1
- package/cli/dist/commands/__tests__/ui-loading-screen.test.js +16 -0
- package/cli/dist/commands/doctor.js +56 -1
- package/cli/dist/commands/ui.js +26 -6
- package/cli/dist/commands/up.js +54 -5
- package/cli/dist/generator/template-engine.js +27 -7
- package/cli/dist/utils/search-status.js +13 -0
- package/cli/package.json +1 -1
- package/engine/assets/docker/golden-layers.Dockerfile +1 -1
- package/engine/schemas/project-schema.json +5 -0
- package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +1 -1
- package/engine/topologies/platform/docker/layers/l3_builder_layers.star +1 -1
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +1 -1
- package/engine/topologies/tilt/manifest/constants.star +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,8 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `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
|
+
- 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.
|
|
7
9
|
- `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.
|
|
8
10
|
- Go services can opt in to a live development loop with `"dev": { "liveReload": true }` in `service.json`: edits are copied into the running container and recompiled without an image rebuild, while `go.mod`, `go.sum` and the generated Dockerfile still rebuild the image. The watcher compiles into a staging binary while the old build keeps serving and swaps only on success, so a compile error shows in the Tilt log and the last good build keeps answering (Air, tried first, stopped the app before compiling and left it down for the whole compile; in 2 of 6 edits a Traefik health check then returned 503 for 10 s). Measured on one loaded Mac: median edit-to-ready 1.63 s for the scaffold. The default Go image (`golang:1.23`) now sets `GOTOOLCHAIN=auto`, so a `go.mod` that needs a newer Go, such as Gin 1.12 (`go 1.25`), downloads that toolchain instead of stopping with `requires go >= 1.25`. Offline or air-gapped builds of a go.mod newer than 1.23 still fail, but now at the toolchain download rather than at the version check. `scripts/e2e/go-dev-loop.sh` checks all of this through a real `tdk up`. Refs #369.
|
|
9
11
|
- `tdk up` keeps each smoke step's status and response body under `.tdk/smoke/<service>/<step>/` (`latest.json` and `body.txt`, body capped at 64 KiB), plus `last-success.json` and `last-success-body.txt` that a later failure never overwrites, so a regression can be diffed against the last good response. The failure line ends with the record path, and `tdk project` gitignores `.tdk/smoke/`. A new `smoke-e2e` workflow runs `scripts/verify-smoke.sh` and uploads the records. Before its first step a smoke check now waits for the service's health path (`healthCheckPath`, default `/health` for backends) with GETs until it answers something other than a connection error, 404 or 5xx, so a write is no longer the first request to hit a service that is still building; write retry rules are unchanged.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@tdk-landscape/tdk-cli-core)
|
|
4
4
|
[](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
|
|
5
|
-
[](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
|
|
6
6
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
|
|
8
8
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { renderToString } from "ink";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { LoadingScreen } from "../ui.js";
|
|
5
|
+
describe("ui LoadingScreen", () => {
|
|
6
|
+
it("shows honest text with no percentage", () => {
|
|
7
|
+
const out = renderToString(_jsx(LoadingScreen, { animated: false }));
|
|
8
|
+
expect(out).toContain("Discovering resources...");
|
|
9
|
+
expect(out).not.toContain("%");
|
|
10
|
+
});
|
|
11
|
+
it("renders a spinner frame only when animated", () => {
|
|
12
|
+
expect(renderToString(_jsx(LoadingScreen, { animated: true }))).toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Discovering/);
|
|
13
|
+
expect(renderToString(_jsx(LoadingScreen, { animated: false }))).not.toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=ui-loading-screen.test.js.map
|
|
@@ -15,7 +15,7 @@ import { execAsync, isExecTimeout } from "../utils/exec-async.js";
|
|
|
15
15
|
import { formatCount } from "../utils/formatting.js";
|
|
16
16
|
import { getHostPortPlan } from "../utils/host-port-config.js";
|
|
17
17
|
import { createHostPortPlan } from "../utils/host-port-plan.js";
|
|
18
|
-
import { findProjectRoot } from "../utils/paths.js";
|
|
18
|
+
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";
|
|
@@ -416,6 +416,60 @@ function checkMasterConfigs() {
|
|
|
416
416
|
fix: "Run: tdk project",
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* A repo can pin the oldest CLI it works with: `"minTdkVersion": "1.3.80"` in
|
|
421
|
+
* .tdk/project.json. An older `tdk` fails here, so a team sees one clear line
|
|
422
|
+
* instead of a half-working `tdk up`.
|
|
423
|
+
*/
|
|
424
|
+
export function checkTdkVersion(currentVersion = getPackageVersion()) {
|
|
425
|
+
const projectRoot = findProjectRoot();
|
|
426
|
+
if (!projectRoot) {
|
|
427
|
+
return {
|
|
428
|
+
name: "TDK version",
|
|
429
|
+
didPass: true,
|
|
430
|
+
isSkipped: true,
|
|
431
|
+
message: "Not in a project - skipping TDK version check",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
let minimum;
|
|
435
|
+
try {
|
|
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) {
|
|
443
|
+
return {
|
|
444
|
+
name: "TDK version",
|
|
445
|
+
didPass: true,
|
|
446
|
+
isSkipped: true,
|
|
447
|
+
message: `tdk ${currentVersion} (no minTdkVersion in .tdk/project.json)`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const required = typeof minimum === "string" ? parseVersion(minimum) : null;
|
|
451
|
+
if (!required || !/^\d+\.\d+\.\d+$/.test(String(minimum).trim())) {
|
|
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
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
name: "TDK version",
|
|
469
|
+
didPass: true,
|
|
470
|
+
message: `tdk ${currentVersion} meets minTdkVersion ${required.join(".")}`,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
419
473
|
/**
|
|
420
474
|
* Tilt only builds resources under `discovery.paths`, but the CLI finds every service.json,
|
|
421
475
|
* so a resource outside them is listed and given a URL by `tdk up` yet never started.
|
|
@@ -1092,6 +1146,7 @@ export const doctorCommand = new Command("doctor")
|
|
|
1092
1146
|
() => checkDockerNetworkCapacity(),
|
|
1093
1147
|
];
|
|
1094
1148
|
const projectChecks = [
|
|
1149
|
+
() => checkTdkVersion(),
|
|
1095
1150
|
checkMasterConfigs,
|
|
1096
1151
|
checkGeneratedProjectRuntimeAssets,
|
|
1097
1152
|
checkStarlarkLoadExports,
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -5,16 +5,27 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
5
5
|
import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, TabBar, } from "../components/index.js";
|
|
6
6
|
import { errorFactories, requireProjectRoot } from "../utils/errors.js";
|
|
7
7
|
import { findProjectRoot } from "../utils/paths.js";
|
|
8
|
+
import { describeSearch } from "../utils/search-status.js";
|
|
8
9
|
import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
|
|
9
10
|
import { createStatusMessageController } from "../utils/status-message.js";
|
|
10
11
|
import { isTiltAvailable } from "../utils/tilt.js";
|
|
11
12
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
12
13
|
const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q Quit \u2502 Esc Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close..." }) })] }));
|
|
13
|
-
const
|
|
14
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
15
|
+
export const LoadingScreen = ({ message = "Discovering resources...", animated = true, }) => {
|
|
16
|
+
const [frame, setFrame] = useState(0);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!animated)
|
|
19
|
+
return;
|
|
20
|
+
const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
21
|
+
return () => clearInterval(timer);
|
|
22
|
+
}, [animated]);
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: [animated && _jsxs(Text, { color: "cyan", children: [SPINNER_FRAMES[frame], " "] }), message] })] }));
|
|
24
|
+
};
|
|
14
25
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
15
26
|
const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
|
|
16
27
|
const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk project" }), _jsx(Text, { children: " 2. Run: tdk resource api --type backend" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
|
|
17
|
-
const TUIApp = () => {
|
|
28
|
+
const TUIApp = ({ animated = true }) => {
|
|
18
29
|
const { exit } = useApp();
|
|
19
30
|
const { stdout } = useStdout();
|
|
20
31
|
const { stdin, setRawMode } = useStdin();
|
|
@@ -149,6 +160,15 @@ const TUIApp = () => {
|
|
|
149
160
|
return [];
|
|
150
161
|
}, [activeTab, filteredStacks, filteredServices, selectedStackData, selectedServiceData]);
|
|
151
162
|
const items = getItems();
|
|
163
|
+
// Only the stack and service lists are filtered by the query; drilled-in views are not.
|
|
164
|
+
const searchList = activeTab === "overview" || (activeTab === "resources" && !selectedStackData)
|
|
165
|
+
? { total: stacks.length }
|
|
166
|
+
: (activeTab === "files" && !selectedServiceData) || activeTab === "config"
|
|
167
|
+
? { total: services.length }
|
|
168
|
+
: null;
|
|
169
|
+
const searchStatus = searchList
|
|
170
|
+
? describeSearch(searchQuery, items.length, searchList.total)
|
|
171
|
+
: null;
|
|
152
172
|
useEffect(() => {
|
|
153
173
|
if (highlightedIndex >= items.length && items.length > 0) {
|
|
154
174
|
setHighlightedIndex(items.length - 1);
|
|
@@ -401,7 +421,7 @@ const TUIApp = () => {
|
|
|
401
421
|
const compactTabBar = terminalWidth < 100;
|
|
402
422
|
const compact = terminalWidth < 80;
|
|
403
423
|
if (loading) {
|
|
404
|
-
return _jsx(LoadingScreen, {
|
|
424
|
+
return _jsx(LoadingScreen, { message: "Discovering resources...", animated: animated });
|
|
405
425
|
}
|
|
406
426
|
if (error) {
|
|
407
427
|
return _jsx(ErrorScreen, { error: error, onRetry: refresh });
|
|
@@ -409,7 +429,7 @@ const TUIApp = () => {
|
|
|
409
429
|
if (services.length === 0) {
|
|
410
430
|
return _jsx(EmptyState, { message: message });
|
|
411
431
|
}
|
|
412
|
-
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [_jsx(Box, { paddingX: 1, paddingY: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "white", children: projectRoot }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsxs(Text, { color: "green", children: [services.length, " services ready"] })] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "─".repeat(compact ? 60 : Math.min(terminalWidth - 4, 100)) }) }), isSearching && (
|
|
432
|
+
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [_jsx(Box, { paddingX: 1, paddingY: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "white", children: projectRoot }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsxs(Text, { color: "green", children: [services.length, " services ready"] })] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "─".repeat(compact ? 60 : Math.min(terminalWidth - 4, 100)) }) }), isSearching && (_jsxs(Box, { paddingX: 1, height: 1, children: [_jsxs(Text, { color: "yellow", children: ["Search: ", searchQuery, "_"] }), searchStatus && _jsx(Text, { color: "gray", dimColor: true, children: ` ${searchStatus.summary}` })] })), searchStatus?.emptyMessage && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: "gray", children: searchStatus.emptyMessage }) })), !isSearching && message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) })), !isSearching && !message && showTooltips && !showHelp && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: activeTab === "overview" && selectedStack
|
|
413
433
|
? `Stack "${selectedStack}" selected. [Enter] view │ [Esc] back │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
|
414
434
|
: activeTab === "overview" && !selectedStack
|
|
415
435
|
? `[↑/↓] Navigate │ [Enter] Select │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
|
@@ -431,12 +451,12 @@ export const uiCommand = new Command("ui")
|
|
|
431
451
|
.option("-v, --verbose", "Enable verbose output", false)
|
|
432
452
|
.option("--no-animations", "Disable animations")
|
|
433
453
|
.option("--high-contrast", "Enable high contrast mode")
|
|
434
|
-
.action(async () => {
|
|
454
|
+
.action(async (options) => {
|
|
435
455
|
const tiltAvailable = await isTiltAvailable();
|
|
436
456
|
if (!tiltAvailable) {
|
|
437
457
|
errorFactories.tiltNotInstalled().exit();
|
|
438
458
|
}
|
|
439
459
|
requireProjectRoot();
|
|
440
|
-
render(_jsx(TUIApp, {}));
|
|
460
|
+
render(_jsx(TUIApp, { animated: options.animations }));
|
|
441
461
|
});
|
|
442
462
|
//# sourceMappingURL=ui.js.map
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -2,7 +2,7 @@ import { connect } from "node:net";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
|
|
5
|
+
import { ensureProjectRuntimeAssets, ProjectConfigNotFoundError, verifyMasterConfigs, } from "../generator/template-engine.js";
|
|
6
6
|
import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
|
|
7
7
|
import { handleDryRun } from "../utils/command-helpers.js";
|
|
8
8
|
import { getDeferredResourceNames } from "../utils/doctor-runtime.js";
|
|
@@ -54,6 +54,48 @@ async function defaultDockerReachable() {
|
|
|
54
54
|
execFile("docker", ["ps"], { timeout: 10_000 }, (error) => resolve(!error));
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/** Exit status for `tdk up` when generated files no longer match service.json / project.json. */
|
|
58
|
+
export const DRIFT_EXIT_CODE = 2;
|
|
59
|
+
/**
|
|
60
|
+
* Generated service files edited by hand. Only these block `tdk up`: when service.json itself changed, `tdk up` regenerates
|
|
61
|
+
* the outputs, so stale outputs are the normal edit-then-up loop and not drift.
|
|
62
|
+
*/
|
|
63
|
+
export function driftReport(projectRoot) {
|
|
64
|
+
let handEdited;
|
|
65
|
+
try {
|
|
66
|
+
({ handEdited } = verifyMasterConfigs(projectRoot));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// A project with no .tdk/project.json has no snapshots to compare. Anything else (a corrupt config) must not skip the gate.
|
|
70
|
+
if (error instanceof ProjectConfigNotFoundError)
|
|
71
|
+
return null;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (handEdited.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
return [
|
|
77
|
+
"Generated files were edited by hand and no longer match service.json:",
|
|
78
|
+
...handEdited.map((file) => ` ${file}`),
|
|
79
|
+
"Run `tdk config regenerate` to discard the edits, or bypass with `tdk up --ignore-drift`.",
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Runs before Tilt is started: exits with DRIFT_EXIT_CODE on drift unless `ignoreDrift` is set. With `ignoreDrift` the
|
|
84
|
+
* warning is printed every time, whether or not anything drifted, because the check is skipped and cannot tell.
|
|
85
|
+
*/
|
|
86
|
+
export function enforceDriftGate(projectRoot, options, exit = process.exit) {
|
|
87
|
+
if (options.ignoreDrift) {
|
|
88
|
+
console.warn(chalk.yellow("Warning: --ignore-drift set; generated files were not checked."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const drift = driftReport(projectRoot);
|
|
92
|
+
if (!drift)
|
|
93
|
+
return;
|
|
94
|
+
options.onDrift?.(drift.join("\n"));
|
|
95
|
+
for (const line of drift)
|
|
96
|
+
console.error(chalk.red(line));
|
|
97
|
+
exit(DRIFT_EXIT_CODE);
|
|
98
|
+
}
|
|
57
99
|
function waitForTiltUi(port, timeoutMs = 30_000) {
|
|
58
100
|
return new Promise((resolve) => {
|
|
59
101
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -84,6 +126,7 @@ export const upCommand = new Command("up")
|
|
|
84
126
|
.option("-q, --quiet", "Suppress non-essential output", false)
|
|
85
127
|
.option("--dry-run", "Show what would be started without starting", false)
|
|
86
128
|
.option("-f, --force", "Kill existing Tilt process before starting", false)
|
|
129
|
+
.option("--ignore-drift", "Start even if generated files differ from service.json", false)
|
|
87
130
|
.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")
|
|
88
131
|
.option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
|
|
89
132
|
.action(async (stackName, options) => {
|
|
@@ -109,9 +152,8 @@ export const upCommand = new Command("up")
|
|
|
109
152
|
await assertMachineReadyOrExit();
|
|
110
153
|
}
|
|
111
154
|
const action = async () => {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
: (findProjectRoot() ?? process.cwd());
|
|
155
|
+
const foundRoot = options.dryRun ? requireProjectRoot() : findProjectRoot();
|
|
156
|
+
const projectRoot = foundRoot ?? process.cwd();
|
|
115
157
|
const discoveredResources = discoverResourcesStrict();
|
|
116
158
|
// Reject a bad request before anything below can write to the project (.env, runtime assets, .tdk/project.json).
|
|
117
159
|
if (options.only) {
|
|
@@ -127,13 +169,20 @@ export const upCommand = new Command("up")
|
|
|
127
169
|
showErrorAndExit(message, 2);
|
|
128
170
|
}
|
|
129
171
|
}
|
|
172
|
+
// Also under --dry-run: the check only reads, and a dry run should show what a real run would refuse.
|
|
173
|
+
if (foundRoot) {
|
|
174
|
+
enforceDriftGate(foundRoot, {
|
|
175
|
+
ignoreDrift: options.ignoreDrift,
|
|
176
|
+
onDrift: (message) => emit?.({ ok: false }, [{ code: "DRIFT_DETECTED", message }]),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
130
179
|
const hostPortPlan = await getHostPortPlan(projectRoot, {
|
|
131
180
|
inspectDocker: !options.dryRun,
|
|
132
181
|
});
|
|
133
182
|
if (!options.dryRun) {
|
|
134
183
|
// An older project's .env predates keys such as JWT_SECRET, which Compose now requires. Add what is missing (never
|
|
135
184
|
// changing an existing value) before anything starts. Only inside a real project, so a stray run never writes a .env.
|
|
136
|
-
if (
|
|
185
|
+
if (foundRoot) {
|
|
137
186
|
const addedEnvKeys = completeEnvFile(projectRoot);
|
|
138
187
|
if (addedEnvKeys.length > 0 && !options.quiet) {
|
|
139
188
|
console.log(chalk.gray(`Added ${addedEnvKeys.join(", ")} to .env (generated; existing values unchanged)`));
|
|
@@ -349,10 +349,13 @@ function normalizeProjectConfig(value) {
|
|
|
349
349
|
overrides: config.overrides,
|
|
350
350
|
};
|
|
351
351
|
}
|
|
352
|
+
/** The project has no .tdk/project.json yet, as opposed to one that exists but is unreadable or invalid. */
|
|
353
|
+
export class ProjectConfigNotFoundError extends Error {
|
|
354
|
+
}
|
|
352
355
|
export function readProjectConfig(projectRoot) {
|
|
353
356
|
const projectJsonPath = path.join(projectRoot, ".tdk", "project.json");
|
|
354
357
|
if (!fs.existsSync(projectJsonPath)) {
|
|
355
|
-
throw new
|
|
358
|
+
throw new ProjectConfigNotFoundError(`Project config not found: ${projectJsonPath}. Run 'tdk project --yes' first.`);
|
|
356
359
|
}
|
|
357
360
|
const jsonContent = fs.readFileSync(projectJsonPath, "utf-8");
|
|
358
361
|
const parsed = JSON.parse(jsonContent);
|
|
@@ -600,10 +603,12 @@ async function vendorTdkExtension(projectRoot) {
|
|
|
600
603
|
console.warn(`⚠️ Premium overlay skipped: ${err.message}`);
|
|
601
604
|
}
|
|
602
605
|
}
|
|
606
|
+
const TDK_GENERATED_MARKER = "SYSTEM-GENERATED BY TDK CLI";
|
|
603
607
|
export function verifyMasterConfigs(projectRoot) {
|
|
604
608
|
const errors = [];
|
|
605
609
|
const warnings = [];
|
|
606
610
|
const diffs = [];
|
|
611
|
+
const handEdited = [];
|
|
607
612
|
const addDrift = (displayPath, actual, expected) => {
|
|
608
613
|
if (actual === expected)
|
|
609
614
|
return;
|
|
@@ -622,16 +627,28 @@ export function verifyMasterConfigs(projectRoot) {
|
|
|
622
627
|
const filePath = path.join(outputDir, filename);
|
|
623
628
|
addDrift(`.tdk/.tdk-out/${filename}`, fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined, expectedContent);
|
|
624
629
|
}
|
|
630
|
+
// `tdk project` never overwrites a root .tiltignore the repo already has, so one
|
|
631
|
+
// without TDK's header belongs to the team: it is not drift.
|
|
625
632
|
const rootTiltIgnore = path.join(projectRoot, ".tiltignore");
|
|
626
|
-
|
|
633
|
+
const rootTiltIgnoreText = fs.existsSync(rootTiltIgnore)
|
|
634
|
+
? fs.readFileSync(rootTiltIgnore, "utf-8")
|
|
635
|
+
: undefined;
|
|
636
|
+
if (rootTiltIgnoreText !== undefined && !rootTiltIgnoreText.includes(TDK_GENERATED_MARKER)) {
|
|
637
|
+
warnings.push(".tiltignore: not generated by TDK, left as is");
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
addDrift(".tiltignore", rootTiltIgnoreText, expectedFiles[".tiltignore"]);
|
|
641
|
+
}
|
|
627
642
|
for (const manifestPath of discoverServiceManifestPaths(projectRoot)) {
|
|
628
643
|
const displayPath = path.relative(projectRoot, manifestPath).split(path.sep).join("/");
|
|
629
644
|
const validation = validateServiceManifestFile(manifestPath, displayPath);
|
|
630
645
|
errors.push(...validation.errors);
|
|
631
646
|
warnings.push(...validation.warnings);
|
|
632
|
-
|
|
647
|
+
const generated = verifyGeneratedResourceFiles(projectRoot, manifestPath);
|
|
648
|
+
errors.push(...generated.errors);
|
|
649
|
+
handEdited.push(...generated.handEdited);
|
|
633
650
|
}
|
|
634
|
-
return { valid: errors.length === 0, errors, warnings, diffs };
|
|
651
|
+
return { valid: errors.length === 0, errors, warnings, diffs, handEdited };
|
|
635
652
|
}
|
|
636
653
|
function createUnifiedDiff(file, actual, expected) {
|
|
637
654
|
const oldLines = actual.replace(/\n$/, "").split("\n");
|
|
@@ -651,7 +668,7 @@ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
|
|
|
651
668
|
const serviceDir = path.dirname(serviceJsonPath);
|
|
652
669
|
const generatedDir = path.join(serviceDir, ".autogenerated");
|
|
653
670
|
if (!fs.existsSync(generatedDir))
|
|
654
|
-
return [];
|
|
671
|
+
return { errors: [], handEdited: [] };
|
|
655
672
|
const generatedFiles = [];
|
|
656
673
|
const collect = (directory, relativeDir = "") => {
|
|
657
674
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
@@ -667,10 +684,11 @@ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
|
|
|
667
684
|
};
|
|
668
685
|
collect(generatedDir);
|
|
669
686
|
if (generatedFiles.length === 0)
|
|
670
|
-
return [];
|
|
687
|
+
return { errors: [], handEdited: [] };
|
|
671
688
|
const contractDir = path.join(generatedDir, ".tdk-contract");
|
|
672
689
|
const sourceSnapshot = path.join(contractDir, "service.json.source");
|
|
673
690
|
const errors = [];
|
|
691
|
+
const handEdited = [];
|
|
674
692
|
if (!fs.existsSync(sourceSnapshot)) {
|
|
675
693
|
errors.push(path.relative(projectRoot, sourceSnapshot) +
|
|
676
694
|
": generated-file contract snapshot is missing (run tdk up to regenerate)");
|
|
@@ -691,9 +709,11 @@ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
|
|
|
691
709
|
errors.push(`${display}: generated-file snapshot is missing (run tdk up to regenerate)`);
|
|
692
710
|
}
|
|
693
711
|
else if (fs.readFileSync(generated.fullPath, "utf-8") !== fs.readFileSync(snapshotPath, "utf-8")) {
|
|
712
|
+
// Windows path.relative uses backslashes; report project-relative paths with forward slashes everywhere.
|
|
713
|
+
handEdited.push(display.split(path.sep).join("/"));
|
|
694
714
|
errors.push(`${display}: generated file differs from its TDK snapshot (run tdk up to regenerate)`);
|
|
695
715
|
}
|
|
696
716
|
}
|
|
697
|
-
return errors;
|
|
717
|
+
return { errors, handEdited };
|
|
698
718
|
}
|
|
699
719
|
//# sourceMappingURL=template-engine.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Describes a `/` search result for the `tdk ui` header; null when there is no query. */
|
|
2
|
+
export function describeSearch(query, matches, total) {
|
|
3
|
+
if (!query)
|
|
4
|
+
return null;
|
|
5
|
+
if (matches === 0) {
|
|
6
|
+
return {
|
|
7
|
+
summary: "0 matches",
|
|
8
|
+
emptyMessage: `No resources match "${query}". Press Esc to clear.`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return { summary: `${matches} of ${total}` };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=search-status.js.map
|
package/cli/package.json
CHANGED
|
@@ -178,7 +178,7 @@ FROM l4_backend_bun AS l4_backend_golden
|
|
|
178
178
|
# =============================================================================
|
|
179
179
|
# L4-FRONTEND: FRONTEND PRODUCTION RUNTIME
|
|
180
180
|
# =============================================================================
|
|
181
|
-
FROM nginx:alpine AS l4_frontend_golden
|
|
181
|
+
FROM nginx:1.27-alpine AS l4_frontend_golden
|
|
182
182
|
|
|
183
183
|
LABEL layer="l4-frontend" description="Frontend production runtime with Nginx" maintainer="tdk-project"
|
|
184
184
|
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"version": {
|
|
17
17
|
"type": "string"
|
|
18
18
|
},
|
|
19
|
+
"minTdkVersion": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
22
|
+
"description": "Oldest tdk CLI this project works with, as MAJOR.MINOR.PATCH. tdk doctor fails on an older CLI."
|
|
23
|
+
},
|
|
19
24
|
"project": {
|
|
20
25
|
"type": "object",
|
|
21
26
|
"additionalProperties": true,
|
|
@@ -227,7 +227,7 @@ FROM l4_backend_bun AS l4_backend_golden
|
|
|
227
227
|
# =============================================================================
|
|
228
228
|
# L4-FRONTEND: FRONTEND PRODUCTION RUNTIME
|
|
229
229
|
# =============================================================================
|
|
230
|
-
FROM nginx:alpine AS l4_frontend_golden
|
|
230
|
+
FROM nginx:1.27-alpine AS l4_frontend_golden
|
|
231
231
|
|
|
232
232
|
LABEL layer="l4-frontend" \
|
|
233
233
|
description="Frontend production runtime with Nginx" \
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# === INLINED CONSTANTS for pure extension loading ===
|
|
2
2
|
WEB_FRAMEWORK = "hono"
|
|
3
|
-
RUNTIME_CONFIG = {"bun": {"image_base": "oven/bun:
|
|
3
|
+
RUNTIME_CONFIG = {"bun": {"image_base": "oven/bun:1.3.11-alpine", "package_manager": "bun", "lockfile": "bun.lock", "backend_start_command": "bun run start", "frontend_start_command": "bun run dev"}, "node": {"image_base": "node:20-alpine", "package_manager": "npm", "lockfile": "package-lock.json", "backend_start_command": "node dist/index.js"}}
|
|
4
4
|
OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
|
|
5
5
|
# === END INLINED CONSTANTS ===
|
|
6
6
|
|
|
@@ -215,7 +215,7 @@ def L4_generate_frontend_runtime(res_path, target_path = '/usr/share/nginx/html'
|
|
|
215
215
|
|
|
216
216
|
if use_nginx:
|
|
217
217
|
# Use golden L4-frontend image if enabled (Nginx pre-configured)
|
|
218
|
-
base_image = GOLDEN_L4_FRONTEND_IMAGE if use_golden else "nginx:alpine"
|
|
218
|
+
base_image = GOLDEN_L4_FRONTEND_IMAGE if use_golden else "nginx:1.27-alpine"
|
|
219
219
|
|
|
220
220
|
# If using golden image, skip curl installation (already there)
|
|
221
221
|
healthcheck_setup = "" if use_golden else "RUN apk add --no-cache curl\n"
|