@tdk-landscape/tdk-cli-core 1.3.98 → 1.3.100
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/README.md +2 -0
- package/cli/dist/commands/ui.js +5 -4
- package/cli/dist/utils/service-manifest.js +10 -0
- package/cli/package.json +1 -1
- package/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +33 -2
- package/engine/topologies/platform/docker/index.star +2 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +17 -2
- package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +3 -2
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# TDK CLI — start services on your laptop
|
|
2
2
|
|
|
3
|
+
English | [简体中文](README-zh_cn.md) | [繁體中文](README-zh_tw.md) | [日本語](README-ja.md) | [한국어](README-ko.md)
|
|
4
|
+
|
|
3
5
|
TDK CLI starts your services on your laptop. It is not a deploy and not a Compose file: define each service in `service.json`, then run `tdk up`. No Kubernetes is needed on the machine.
|
|
4
6
|
|
|
5
7
|
Stability: 1.x local dev. Generated files are a contract; verify with `tdk config verify`. Core CLI is MIT and needs no key. Premium is optional.
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -8,11 +8,11 @@ import { findProjectRoot } from "../utils/paths.js";
|
|
|
8
8
|
import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
|
|
9
9
|
import { isTiltAvailable } from "../utils/tilt.js";
|
|
10
10
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
11
|
-
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
|
|
11
|
+
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..." }) })] }));
|
|
12
12
|
const LoadingScreen = ({ progress, message }) => (_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: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
|
|
13
13
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
14
14
|
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" })] }));
|
|
15
|
-
const EmptyState = () => (_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 init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" })] }));
|
|
15
|
+
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 init" }), _jsx(Text, { children: " 2. Or create services manually" }), _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"] }) }))] }));
|
|
16
16
|
const TUIApp = () => {
|
|
17
17
|
const { exit } = useApp();
|
|
18
18
|
const { stdout } = useStdout();
|
|
@@ -297,7 +297,8 @@ const TUIApp = () => {
|
|
|
297
297
|
setSelectedStack(null);
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
setMessage("Press q to quit");
|
|
301
|
+
setTimeout(() => setMessage(""), 2000);
|
|
301
302
|
return;
|
|
302
303
|
}
|
|
303
304
|
if (input === "?") {
|
|
@@ -413,7 +414,7 @@ const TUIApp = () => {
|
|
|
413
414
|
return _jsx(ErrorScreen, { error: error, onRetry: refresh });
|
|
414
415
|
}
|
|
415
416
|
if (services.length === 0) {
|
|
416
|
-
return _jsx(EmptyState, {});
|
|
417
|
+
return _jsx(EmptyState, { message: message });
|
|
417
418
|
}
|
|
418
419
|
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 && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "yellow", children: ["Search: ", searchQuery, "_"] }) })), !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
|
|
419
420
|
? `Stack "${selectedStack}" selected. [Enter] view │ [Esc] back │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
|
@@ -66,6 +66,13 @@ export function validateServiceManifest(value, displayPath) {
|
|
|
66
66
|
if ("schemaVersion" in manifest && manifest.schemaVersion !== SERVICE_MANIFEST_SCHEMA_VERSION) {
|
|
67
67
|
errors.push(`${displayPath}.schemaVersion: unsupported version ${String(manifest.schemaVersion)} (supported: ${SERVICE_MANIFEST_SCHEMA_VERSION})`);
|
|
68
68
|
}
|
|
69
|
+
const dev = manifest.dev;
|
|
70
|
+
const liveReload = dev && typeof dev === "object" && !Array.isArray(dev)
|
|
71
|
+
? dev.liveReload
|
|
72
|
+
: undefined;
|
|
73
|
+
if (liveReload !== undefined && typeof liveReload !== "boolean") {
|
|
74
|
+
errors.push(`${displayPath}.dev.liveReload: expected true or false`);
|
|
75
|
+
}
|
|
69
76
|
if ("smoke" in manifest) {
|
|
70
77
|
for (const message of validateSmoke(manifest.smoke))
|
|
71
78
|
errors.push(`${displayPath}.${message}`);
|
|
@@ -78,6 +85,9 @@ export function validateServiceManifest(value, displayPath) {
|
|
|
78
85
|
return [`${displayPath}.${field}: unknown field is preserved`];
|
|
79
86
|
return [];
|
|
80
87
|
});
|
|
88
|
+
if (liveReload === true && manifest.language !== "go") {
|
|
89
|
+
warnings.push(`${displayPath}.dev.liveReload: only applies to Go services (language "go"); it is ignored here`);
|
|
90
|
+
}
|
|
81
91
|
return { errors, warnings, manifest };
|
|
82
92
|
}
|
|
83
93
|
export function validateServiceManifestFile(filePath, displayPath) {
|
package/cli/package.json
CHANGED
|
@@ -62,7 +62,7 @@ RUN CGO_ENABLED=0 go build -o /out/app .
|
|
|
62
62
|
|
|
63
63
|
FROM build AS test
|
|
64
64
|
RUN go test ./...
|
|
65
|
-
|
|
65
|
+
{go_development}
|
|
66
66
|
FROM debian:bookworm-slim AS production
|
|
67
67
|
|
|
68
68
|
{curl}
|
|
@@ -102,14 +102,35 @@ EXPOSE {port}
|
|
|
102
102
|
CMD ["app"]
|
|
103
103
|
"""
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
# Opt-in Go development target (`dev.liveReload`): the toolchain image with Air, which rebuilds and restarts the app when a `.go`
|
|
106
|
+
# file changes. Dependencies are downloaded in their own layer and the module and build caches live in BuildKit cache mounts, so an
|
|
107
|
+
# ordinary source edit recompiles incrementally. `go.mod`, `go.sum` and the Dockerfile still rebuild the image.
|
|
108
|
+
_GO_DEVELOPMENT = """
|
|
109
|
+
FROM golang:1.23-bookworm AS development
|
|
110
|
+
RUN go install github.com/air-verse/air@v1.61.7
|
|
111
|
+
WORKDIR /app/{res_path}
|
|
112
|
+
COPY {res_path}/go.* ./
|
|
113
|
+
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
|
114
|
+
COPY {res_path}/ ./
|
|
115
|
+
ENV GOFLAGS=-buildvcs=false
|
|
116
|
+
# Prime the build cache inside the image (a cache mount would not be kept), so the first edit recompiles only what changed.
|
|
117
|
+
RUN go build -o /tmp/app .
|
|
118
|
+
EXPOSE {port}
|
|
119
|
+
CMD ["sh", "-c", "exec air --build.cmd 'go build -o /tmp/app .' --build.bin /tmp/app --build.include_ext go"]
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND, dev = False):
|
|
106
123
|
"""Dockerfile text for a provider-owned backend language, or fail for an unknown one."""
|
|
107
124
|
template = {"python": _PYTHON, "go": _GO, "rust": _RUST}.get(language)
|
|
108
125
|
if template == None:
|
|
109
126
|
fail("No Dockerfile generator for backend language '{}'".format(language))
|
|
127
|
+
go_development = ""
|
|
128
|
+
if dev and language == "go":
|
|
129
|
+
go_development = _GO_DEVELOPMENT.format(res_path = res_path, port = port)
|
|
110
130
|
return template.format(
|
|
111
131
|
res_path = res_path,
|
|
112
132
|
port = port,
|
|
133
|
+
go_development = go_development,
|
|
113
134
|
curl = _CURL,
|
|
114
135
|
healthcheck = _HEALTHCHECK.format(port = port),
|
|
115
136
|
)
|
|
@@ -120,3 +141,13 @@ def is_provider_owned_language(manifest):
|
|
|
120
141
|
if not manifest:
|
|
121
142
|
return False
|
|
122
143
|
return manifest.get("appType", "backend") == "backend" and manifest.get("language", "bun") in PROVIDER_OWNED_LANGUAGES
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def go_live_reload_enabled(manifest):
|
|
147
|
+
"""True for a Go service that opted in with `dev.liveReload: true` (see _GO_DEVELOPMENT); anything else stays off."""
|
|
148
|
+
if not manifest or manifest.get("language", "bun") != "go":
|
|
149
|
+
return False
|
|
150
|
+
dev = manifest.get("dev")
|
|
151
|
+
if type(dev) != "dict":
|
|
152
|
+
return False
|
|
153
|
+
return dev.get("liveReload") == True
|
|
@@ -8,6 +8,7 @@ load(
|
|
|
8
8
|
"./dockerfile/language_dockerfile.star",
|
|
9
9
|
"generate_language_dockerfile",
|
|
10
10
|
"is_provider_owned_language",
|
|
11
|
+
"go_live_reload_enabled",
|
|
11
12
|
"LANGUAGE_REQUIRED_FILES",
|
|
12
13
|
)
|
|
13
14
|
load("./config/dockerignore.star", "generate_dockerignore")
|
|
@@ -26,6 +27,7 @@ Docker = struct(
|
|
|
26
27
|
migrator = generate_migrator_dockerfile,
|
|
27
28
|
backend_language = generate_language_dockerfile,
|
|
28
29
|
is_provider_owned_language = is_provider_owned_language,
|
|
30
|
+
go_live_reload = go_live_reload_enabled,
|
|
29
31
|
language_required_files = LANGUAGE_REQUIRED_FILES,
|
|
30
32
|
generate_dockerignore = generate_dockerignore,
|
|
31
33
|
frontend_compose = generate_frontend_compose,
|
package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star
CHANGED
|
@@ -215,7 +215,7 @@ def _live_update_sync_source(project_root, full_res_path, sync_path):
|
|
|
215
215
|
return relative_path
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun'):
|
|
218
|
+
def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun', go_live_reload=False):
|
|
219
219
|
"""Build live update sync and run rules for a resource.
|
|
220
220
|
|
|
221
221
|
Python backends reload their own process after a sync (uvicorn --reload), so they have
|
|
@@ -225,9 +225,22 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
|
|
|
225
225
|
# Go and Rust are compiled into the image and have no reload process, so syncing sources would leave the
|
|
226
226
|
# old binary running. Return before any sync() step is built (Tilt rejects sync steps that are created
|
|
227
227
|
# but never used): no live-update rules means Tilt rebuilds the image when a watched file changes.
|
|
228
|
-
if language
|
|
228
|
+
if language == 'rust' or (language == 'go' and not go_live_reload):
|
|
229
229
|
return []
|
|
230
230
|
|
|
231
|
+
if go_live_reload:
|
|
232
|
+
# Opted-in Go development loop: sync the sources into the development image, where Air rebuilds and restarts.
|
|
233
|
+
# Dependency and Dockerfile changes cannot be picked up by a restart, so they fall back to a full image rebuild.
|
|
234
|
+
source_dir = _live_update_sync_source(project_root, full_res_path, '')
|
|
235
|
+
return [
|
|
236
|
+
fall_back_on([
|
|
237
|
+
source_dir + 'go.mod',
|
|
238
|
+
source_dir + 'go.sum',
|
|
239
|
+
source_dir + OUTPUT.AUTOGENERATED_FOLDER,
|
|
240
|
+
]),
|
|
241
|
+
sync(source_dir.rstrip('/'), '/app/' + full_res_path),
|
|
242
|
+
]
|
|
243
|
+
|
|
231
244
|
live_update_rules = []
|
|
232
245
|
for sync_path in syncs:
|
|
233
246
|
# Python images never read the npm/Bun registry config, and the engine does not
|
|
@@ -285,6 +298,7 @@ def _register_docker_build(config, live_update_rules):
|
|
|
285
298
|
_resolve_dockerfile(config),
|
|
286
299
|
live_update_rules,
|
|
287
300
|
config['res_deps'],
|
|
301
|
+
target = 'development' if Docker.go_live_reload(config.get('manifest', {})) else 'production',
|
|
288
302
|
)
|
|
289
303
|
|
|
290
304
|
|
|
@@ -408,6 +422,7 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
|
|
|
408
422
|
config['syncs'],
|
|
409
423
|
project_root=config.get('project_root', ''),
|
|
410
424
|
language=config.get('manifest', {}).get('language', 'bun'),
|
|
425
|
+
go_live_reload=Docker.go_live_reload(config.get('manifest', {})),
|
|
411
426
|
)
|
|
412
427
|
_register_docker_build(config, live_update_rules)
|
|
413
428
|
if defers_start:
|
|
@@ -105,7 +105,7 @@ def _get_resource_only_paths(resource_path):
|
|
|
105
105
|
|
|
106
106
|
return only_paths
|
|
107
107
|
|
|
108
|
-
def _build_typescript_service(name, context, dockerfile, live_update_rules, deps, env=None):
|
|
108
|
+
def _build_typescript_service(name, context, dockerfile, live_update_rules, deps, env=None, target='production'):
|
|
109
109
|
"""Build a TypeScript service with Docker.
|
|
110
110
|
|
|
111
111
|
Args:
|
|
@@ -115,6 +115,7 @@ def _build_typescript_service(name, context, dockerfile, live_update_rules, deps
|
|
|
115
115
|
live_update_rules: Live update configuration
|
|
116
116
|
deps: Resource dependencies
|
|
117
117
|
env: Dict of environment variables to pass to container (optional)
|
|
118
|
+
target: Dockerfile target to build (`development` for an opted-in Go service, otherwise `production`)
|
|
118
119
|
"""
|
|
119
120
|
# Extract service path from dockerfile path (e.g., 'services/product/{domain}/{service-name}')
|
|
120
121
|
# The dockerfile parameter looks like: 'services/product/{domain}/{service-name}/autogenerated/Dockerfile.autogenerated'
|
|
@@ -154,7 +155,7 @@ def _build_typescript_service(name, context, dockerfile, live_update_rules, deps
|
|
|
154
155
|
name,
|
|
155
156
|
context,
|
|
156
157
|
dockerfile=dockerfile,
|
|
157
|
-
target=
|
|
158
|
+
target=target,
|
|
158
159
|
live_update=live_update_rules,
|
|
159
160
|
only=_get_resource_only_paths(resource_path),
|
|
160
161
|
ignore=ignore_patterns,
|