@tdk-landscape/tdk-cli-core 1.3.74 → 1.3.76
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 +6 -0
- package/README.md +5 -2
- package/cli/dist/commands/doctor.js +67 -2
- package/cli/dist/commands/resource.js +62 -62
- package/cli/dist/commands/ui.js +27 -24
- package/cli/dist/frontend-frameworks/react.js +70 -0
- package/cli/dist/frontend-frameworks/registry.js +29 -0
- package/cli/dist/frontend-frameworks/types.js +2 -0
- package/cli/dist/frontend-frameworks/vue.js +73 -0
- package/cli/dist/utils/discovery-paths.js +67 -0
- package/cli/dist/utils/doctor-wiring.js +261 -0
- package/cli/dist/utils/errors.js +4 -1
- package/cli/dist/utils/project-templates.js +4 -0
- package/cli/package.json +1 -1
- package/cli/templates/Tiltfile.hbs +24 -3
- package/engine/AGENTS.md +5 -4
- package/engine/schemas/service-schema.json +9 -1
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +26 -19
- package/engine/topologies/tilt/generators/validators.star +2 -1
- package/engine/topologies/tilt/generators/vite/frameworks/react.star +126 -0
- package/engine/topologies/tilt/generators/vite/frameworks/vue.star +92 -0
- package/engine/topologies/tilt/generators/vite/frontend.star +21 -4
- package/engine/topologies/tilt/generators/vite/templates.star +0 -283
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `tdk up` no longer leaves `nats` (and any other service in `services/platform/messaging/docker-compose.yml`, e.g. a local mail catcher) disabled in Tilt. The pre-alpha default always goes through a focus-filter path that only re-enabled `init-networks`/`postgres`/`traefik`/`golden-layers-build`, so a resource with the `nats` feature retried its broker connection forever until you ran `tilt enable nats` by hand. Fixes #155.
|
|
8
|
+
|
|
9
|
+
- `tdk doctor` now catches wiring mistakes that used to surface minutes into `tdk up`: a resource with no `package.json` (the image build stops at "COPY ... not found"), a `params` URL such as `http://auth-emulator:4000` whose port is not the one the target listens on (its `service.json` `port`), a frontend calling a backend through `localhost:<port>` (TDK publishes no backend ports on localhost; use the Traefik route), the `nats` feature without `services/platform/messaging/docker-compose.yml` (TDK does not generate it, so no NATS server starts), two Tilt processes running for one project, and Docker having no address pool left for the networks a project needs. Found while building [tdk-ecommerce-example](https://github.com/tdk-landscape/tdk-ecommerce-example) and a queue/auth example.
|
|
10
|
+
|
|
11
|
+
- `tdk project ecommerce` clones [tdk-ecommerce-example](https://github.com/tdk-landscape/tdk-ecommerce-example), a Vue 3 storefront and Hono catalog API. Before, only restaurant, saas, erp, user-management and example were accepted.
|
|
12
|
+
|
|
7
13
|
## 1.3.73 (2026-09-28)
|
|
8
14
|
|
|
9
15
|
- Free-tier projects no longer start the `sablier` and `wake-gateway` containers, and Traefik no longer downloads the Sablier plugin from GitHub on every start. They were always launched (the `sablier` one with a read-write Docker socket) even though the license only gated the per-service labels, so nothing could use them. They are now generated only when a license grants Sablier. Fewer containers, one image build less, and no network fetch at Traefik boot. Spotted in the ERP CI run log.
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ One CLI that scaffolds your services and runs the whole landscape locally with h
|
|
|
10
10
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
|
|
11
11
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
|
|
12
12
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
|
|
13
|
+
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/examples-e2e.yml)
|
|
13
14
|
[](https://snyk.io/test/github/tdk-landscape/tdk-cli-core)
|
|
14
15
|
[](https://socket.dev/npm/package/@tdk-landscape/tdk-cli-core/overview)
|
|
15
16
|
[](LICENSE)
|
|
@@ -28,6 +29,8 @@ tdk up shop # run it with hot reload
|
|
|
28
29
|
|
|
29
30
|

|
|
30
31
|
|
|
32
|
+

|
|
33
|
+
|
|
31
34
|
**Requirements:** Docker (Desktop, OrbStack or Colima; Engine 25+, Compose 2.20+), [Tilt](https://docs.tilt.dev/install.html), [Bun](https://bun.sh) for the generated services, and Node.js 22.12+ for the CLI. Ports 80, 443 and 5432 must be free (a local Postgres is the usual conflict). macOS and Linux are supported. Windows is untested (WSL2 may work). Run `tdk doctor` to check all of it.
|
|
32
35
|
|
|
33
36
|
This is the core monorepo for TDK: the `tdk` CLI, the Starlark-based Tilt orchestration engine that powers it, and the discovery system that turns a directory of services into a running local landscape.
|
|
@@ -184,14 +187,14 @@ make pre-commit-run # run pre-commit hooks on all files
|
|
|
184
187
|
tdk doctor
|
|
185
188
|
```
|
|
186
189
|
|
|
187
|
-
Checks Docker, Compose and Tilt. Inside a project it also checks the generated configs, `.env`, service scripts and that the ingress ports are free, before you run `tdk up`.
|
|
190
|
+
Checks Docker, Compose and Tilt. Inside a project it also checks the generated configs, `.env`, service scripts and that the ingress ports are free, before you run `tdk up`. It also catches wiring mistakes: a resource without `package.json`, a `params` URL that uses the wrong port for another service, a frontend calling a backend on `localhost:<port>`, the `nats` feature without a broker to start, duplicate Tilt processes, and Docker running out of network address space.
|
|
188
191
|
|
|
189
192
|
## Community
|
|
190
193
|
|
|
191
194
|
- **Questions and ideas:** [open an issue](https://github.com/tdk-landscape/tdk-cli-core/issues/new/choose).
|
|
192
195
|
- **Contributing:** see [CONTRIBUTING.md](CONTRIBUTING.md). Issues labeled [`good first issue`](https://github.com/tdk-landscape/tdk-cli-core/labels/good%20first%20issue) are a good place to start.
|
|
193
196
|
- **Security:** see [SECURITY.md](SECURITY.md). Please don't report vulnerabilities in public issues.
|
|
194
|
-
- **Examples:** [ERP system (100 services)](https://github.com/tdk-landscape/tdk-erp-system), [SaaS starter](https://github.com/tdk-landscape/tdk-saas-starter), [restaurant](https://github.com/tdk-landscape/tdk-restaurant-example).
|
|
197
|
+
- **Examples:** [ERP system (100 services)](https://github.com/tdk-landscape/tdk-erp-system), [SaaS starter](https://github.com/tdk-landscape/tdk-saas-starter), [restaurant](https://github.com/tdk-landscape/tdk-restaurant-example), [ecommerce (Vue + Hono)](https://github.com/tdk-landscape/tdk-ecommerce-example).
|
|
195
198
|
|
|
196
199
|
## License
|
|
197
200
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, normalize } from "node:path";
|
|
3
|
+
import { dirname, join, normalize, relative } from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
|
|
7
7
|
import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
|
|
8
|
+
import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
|
|
8
9
|
import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
10
|
+
import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
9
11
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
10
12
|
import { formatCount } from "../utils/formatting.js";
|
|
11
13
|
import { findProjectRoot } from "../utils/paths.js";
|
|
@@ -218,6 +220,31 @@ function checkMasterConfigs() {
|
|
|
218
220
|
fix: "Run: tdk project",
|
|
219
221
|
};
|
|
220
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Tilt only builds resources under `discovery.paths`, but the CLI finds every service.json,
|
|
225
|
+
* so a resource outside them is listed and given a URL by `tdk up` yet never started.
|
|
226
|
+
*/
|
|
227
|
+
export function checkResourceDiscovery() {
|
|
228
|
+
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
229
|
+
const patterns = readDiscoveryPaths(projectRoot);
|
|
230
|
+
const stranded = discoverResourcesFromRoot(projectRoot).filter((resource) => !isPathDiscovered(projectRoot, resource.path, patterns));
|
|
231
|
+
if (stranded.length === 0) {
|
|
232
|
+
return {
|
|
233
|
+
name: "Resource Discovery",
|
|
234
|
+
didPass: true,
|
|
235
|
+
message: `All resources are covered by discovery.paths ${JSON.stringify(patterns)}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const list = stranded
|
|
239
|
+
.map((resource) => normalize(relative(projectRoot, resource.path)))
|
|
240
|
+
.join(", ");
|
|
241
|
+
return {
|
|
242
|
+
name: "Resource Discovery",
|
|
243
|
+
didPass: false,
|
|
244
|
+
message: `${formatCount(stranded.length, "resource")} outside discovery.paths ${JSON.stringify(patterns)}: ${list}. \`tdk up\` will not start them`,
|
|
245
|
+
fix: "Move each under services/<stack>/<name>, or add a matching pattern to discovery.paths in .tdk/project.json and run: tdk config regenerate",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
221
248
|
// The scripts `tdk project` copies into every project. Generated Dockerfiles
|
|
222
249
|
// may reference others; those are checked by scanning the Dockerfiles below.
|
|
223
250
|
const REQUIRED_DOCKER_TEMPLATE_FILES = [
|
|
@@ -491,6 +518,26 @@ const KNOWN_TYPE_PACKAGE_VERSIONS = {
|
|
|
491
518
|
"@types/bun": "^1.4.2",
|
|
492
519
|
"@types/node": "^20.0.0",
|
|
493
520
|
};
|
|
521
|
+
/**
|
|
522
|
+
* `@types/bun` (via its underlying `bun-types` package) ships a hardcoded
|
|
523
|
+
* `/// <reference types="node" />` in its own index.d.ts. TypeScript always
|
|
524
|
+
* honors an explicit triple-slash reference, regardless of what the
|
|
525
|
+
* tsconfig's own `"types"` array restricts to -- so a Docker tsconfig with
|
|
526
|
+
* `"types": ["bun"]` still requires `@types/node` to resolve, even though
|
|
527
|
+
* "node" never appears in that array.
|
|
528
|
+
*
|
|
529
|
+
* When `@types/node` isn't a direct dependency, it can only resolve as a
|
|
530
|
+
* *transitive* dependency of `bun-types`, hoisted wherever Bun's installer
|
|
531
|
+
* happens to place it. That is not guaranteed in the isolated, per-service
|
|
532
|
+
* `bun install` that Docker's build layers run (unlike a full workspace-root
|
|
533
|
+
* install), so the same tsconfig can build locally and fail in Docker with
|
|
534
|
+
* `TS2688: Cannot find type definition file for 'node'`. Declaring
|
|
535
|
+
* `@types/node` directly makes the install deterministic either way.
|
|
536
|
+
*/
|
|
537
|
+
function needsNodeTypesForBun(installed, types) {
|
|
538
|
+
const usesBunTypes = types.includes("bun") || installed.has("@types/bun");
|
|
539
|
+
return usesBunTypes && !installed.has("@types/node");
|
|
540
|
+
}
|
|
494
541
|
export function checkTypeScriptTypeDependencies() {
|
|
495
542
|
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
496
543
|
const resources = discoverResourcesFromRoot(projectRoot);
|
|
@@ -527,6 +574,10 @@ export function checkTypeScriptTypeDependencies() {
|
|
|
527
574
|
const described = missing.map((m) => `"${m}" (needs ${neededTypePackage(m)})`).join(", ");
|
|
528
575
|
problems.push(`${resource.name}: "types" lists ${described}`);
|
|
529
576
|
}
|
|
577
|
+
if (needsNodeTypesForBun(installed, types)) {
|
|
578
|
+
missingPackages.add("@types/node");
|
|
579
|
+
problems.push(`${resource.name}: uses @types/bun, which references node types internally, but @types/node is not a direct dependency`);
|
|
580
|
+
}
|
|
530
581
|
}
|
|
531
582
|
if (problems.length === 0) {
|
|
532
583
|
return {
|
|
@@ -542,7 +593,7 @@ export function checkTypeScriptTypeDependencies() {
|
|
|
542
593
|
return {
|
|
543
594
|
name: "TypeScript Type Dependencies",
|
|
544
595
|
didPass: false,
|
|
545
|
-
message:
|
|
596
|
+
message: `\`bun run build\` will fail with TS2688 (Cannot find type definition file) because a @types package it needs is not a direct dependency:\n ${problems.join("\n ")}`,
|
|
546
597
|
fix: `Add to each service's package.json devDependencies: ${lines.join(", ")}`,
|
|
547
598
|
};
|
|
548
599
|
}
|
|
@@ -573,6 +624,11 @@ export function checkFrontendDockerPreflight() {
|
|
|
573
624
|
}
|
|
574
625
|
const problems = [];
|
|
575
626
|
for (const resource of resources) {
|
|
627
|
+
// The frontend Docker builder runs `COPY <resource>/public ./public`, so a missing
|
|
628
|
+
// folder fails the image build with "/public: not found".
|
|
629
|
+
if (!existsSync(join(resource.path, "public"))) {
|
|
630
|
+
problems.push(`${resource.name}: no public/ folder, but the Docker build copies it. Create it: mkdir -p ${normalize(relative(projectRoot, join(resource.path, "public")))} && touch ${normalize(relative(projectRoot, join(resource.path, "public", ".gitkeep")))}`);
|
|
631
|
+
}
|
|
576
632
|
const dockerfilePath = join(resource.path, ".autogenerated", "Dockerfile.app.autogenerated");
|
|
577
633
|
const dockerTsconfigPath = join(resource.path, ".autogenerated", "tsconfig.docker.autogenerated.json");
|
|
578
634
|
if (existsSync(dockerfilePath)) {
|
|
@@ -740,6 +796,8 @@ export const doctorCommand = new Command("doctor")
|
|
|
740
796
|
checkTilt,
|
|
741
797
|
checkDockerCompose,
|
|
742
798
|
checkDockerVersions,
|
|
799
|
+
// Each project needs several networks; a full address pool fails `tdk up` late.
|
|
800
|
+
() => checkDockerNetworkCapacity(),
|
|
743
801
|
];
|
|
744
802
|
const projectChecks = [
|
|
745
803
|
checkMasterConfigs,
|
|
@@ -750,6 +808,13 @@ export const doctorCommand = new Command("doctor")
|
|
|
750
808
|
() => checkHealthRoutes(),
|
|
751
809
|
checkTypeScriptTypeDependencies,
|
|
752
810
|
checkFrontendDockerPreflight,
|
|
811
|
+
checkResourceDiscovery,
|
|
812
|
+
// Wiring mistakes that otherwise surface minutes into `tdk up`.
|
|
813
|
+
() => checkResourcePackageJson(),
|
|
814
|
+
() => checkServiceUrlPorts(),
|
|
815
|
+
() => checkFrontendBackendUrls(),
|
|
816
|
+
() => checkNatsBroker(),
|
|
817
|
+
() => checkTiltInstances(),
|
|
753
818
|
checkEnvironmentVariables,
|
|
754
819
|
// Preflight: catch "port 80 already allocated" BEFORE claiming ready.
|
|
755
820
|
checkIngressPorts,
|
|
@@ -2,9 +2,11 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { resolveFrontendFramework } from "../frontend-frameworks/registry.js";
|
|
5
6
|
import { hasDddLicense } from "../generator/extension-fetch.js";
|
|
6
7
|
import { CREATABLE_RESOURCE_TYPES } from "../types/index.js";
|
|
7
8
|
import { assertValid, confirmOrCancel } from "../utils/command-helpers.js";
|
|
9
|
+
import { chooseResourcePath, isPathDiscovered, readDiscoveryPaths, } from "../utils/discovery-paths.js";
|
|
8
10
|
import { errorFactories, requireProjectRoot, runCommand } from "../utils/errors.js";
|
|
9
11
|
import { writeFilesWithProgress } from "../utils/file-helpers.js";
|
|
10
12
|
import { showCommandHeader } from "../utils/formatting.js";
|
|
@@ -42,7 +44,8 @@ export const TYPE_SPECIFIC = {
|
|
|
42
44
|
},
|
|
43
45
|
},
|
|
44
46
|
};
|
|
45
|
-
export function createServiceJson(name, type, stack, port, extraFeatures = []) {
|
|
47
|
+
export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId) {
|
|
48
|
+
const framework = resolveFrontendFramework(type, frameworkId);
|
|
46
49
|
const typeSpecific = TYPE_SPECIFIC[type];
|
|
47
50
|
const base = JSON.parse(JSON.stringify(BASE_TEMPLATE));
|
|
48
51
|
for (const [key, value] of Object.entries(typeSpecific)) {
|
|
@@ -62,17 +65,23 @@ export function createServiceJson(name, type, stack, port, extraFeatures = []) {
|
|
|
62
65
|
type,
|
|
63
66
|
stack,
|
|
64
67
|
port,
|
|
68
|
+
...(framework ? { framework: framework.id } : {}),
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
|
-
export function createPackageJson(name, type) {
|
|
71
|
+
export function createPackageJson(name, type, frameworkId) {
|
|
68
72
|
const isFrontend = type === "frontend";
|
|
73
|
+
const framework = resolveFrontendFramework(type, frameworkId);
|
|
69
74
|
return {
|
|
70
75
|
name: `@project/${name}`,
|
|
71
76
|
version: "0.0.1",
|
|
72
77
|
type: "module",
|
|
73
78
|
scripts: {
|
|
74
|
-
dev: isFrontend
|
|
75
|
-
|
|
79
|
+
dev: isFrontend
|
|
80
|
+
? "vite --config .autogenerated/vite.config.frontend.autogenerated.ts"
|
|
81
|
+
: "bun run --watch src/index.ts",
|
|
82
|
+
build: isFrontend
|
|
83
|
+
? "tsc && vite build --config .autogenerated/vite.config.build.autogenerated.ts"
|
|
84
|
+
: "tsc",
|
|
76
85
|
// Backends and workers run `start` in their production image; doctor checks for it.
|
|
77
86
|
...(isFrontend ? {} : { start: "bun run dist/index.js" }),
|
|
78
87
|
test: "vitest",
|
|
@@ -81,6 +90,7 @@ export function createPackageJson(name, type) {
|
|
|
81
90
|
},
|
|
82
91
|
dependencies: {
|
|
83
92
|
...(isFrontend ? {} : { hono: "^4.0.0" }),
|
|
93
|
+
...(framework?.dependencies ?? {}),
|
|
84
94
|
},
|
|
85
95
|
devDependencies: {
|
|
86
96
|
"@types/bun": "^1.4.2",
|
|
@@ -91,6 +101,17 @@ export function createPackageJson(name, type) {
|
|
|
91
101
|
vitest: "^5.0.0",
|
|
92
102
|
"@biomejs/biome": "^2.5.13",
|
|
93
103
|
...(isFrontend ? { vite: "^5.0.0" } : {}),
|
|
104
|
+
...(framework?.devDependencies ?? {}),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function createResourceTsconfig(resourceType, frameworkId) {
|
|
109
|
+
const framework = resolveFrontendFramework(resourceType, frameworkId);
|
|
110
|
+
return {
|
|
111
|
+
...TSCONFIG_TEMPLATE,
|
|
112
|
+
compilerOptions: {
|
|
113
|
+
...TSCONFIG_TEMPLATE.compilerOptions,
|
|
114
|
+
...(framework?.compilerOptions ?? {}),
|
|
94
115
|
},
|
|
95
116
|
};
|
|
96
117
|
}
|
|
@@ -179,44 +200,6 @@ export default {
|
|
|
179
200
|
};
|
|
180
201
|
`;
|
|
181
202
|
}
|
|
182
|
-
function getFrontendIndexTemplate(name) {
|
|
183
|
-
return `<!DOCTYPE html>
|
|
184
|
-
<html lang="en">
|
|
185
|
-
<head>
|
|
186
|
-
<meta charset="UTF-8" />
|
|
187
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
188
|
-
<title>${name}</title>
|
|
189
|
-
</head>
|
|
190
|
-
<body>
|
|
191
|
-
<div id="root"></div>
|
|
192
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
193
|
-
</body>
|
|
194
|
-
</html>
|
|
195
|
-
`;
|
|
196
|
-
}
|
|
197
|
-
const FRONTEND_MAIN_TEMPLATE = `import React from 'react';
|
|
198
|
-
import ReactDOM from 'react-dom/client';
|
|
199
|
-
import App from './App';
|
|
200
|
-
|
|
201
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
202
|
-
<React.StrictMode>
|
|
203
|
-
<App />
|
|
204
|
-
</React.StrictMode>,
|
|
205
|
-
);
|
|
206
|
-
`;
|
|
207
|
-
function getFrontendAppTemplate(name) {
|
|
208
|
-
return `function App() {
|
|
209
|
-
return (
|
|
210
|
-
<div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
211
|
-
<h1>${name}</h1>
|
|
212
|
-
<p>Frontend resource created with TDK</p>
|
|
213
|
-
</div>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export default App;
|
|
218
|
-
`;
|
|
219
|
-
}
|
|
220
203
|
function getShutdownHandlerTemplate(signal) {
|
|
221
204
|
return `process.on('${signal}', () => {
|
|
222
205
|
console.log('[Worker] ${signal} received, shutting down gracefully...');
|
|
@@ -307,6 +290,7 @@ export const resourceCommand = new Command("resource")
|
|
|
307
290
|
.description("Create a new resource (service) from scratch, or register an existing one")
|
|
308
291
|
.argument("[name]", "Resource name (kebab-case)")
|
|
309
292
|
.option("-t, --type <type>", "Resource type: backend, frontend, worker, sdk", "backend")
|
|
293
|
+
.option("--framework <id>", "Frontend framework: react (default), vue")
|
|
310
294
|
.option("-s, --stack <stack>", "Stack to assign resource to", "default")
|
|
311
295
|
.option("-p, --path <path>", "Custom path for resource directory")
|
|
312
296
|
.option("--resource-path <path>", "Alias for --path (for backward compatibility)")
|
|
@@ -351,6 +335,7 @@ export const resourceCommand = new Command("resource")
|
|
|
351
335
|
else {
|
|
352
336
|
resourceType = options.type;
|
|
353
337
|
}
|
|
338
|
+
const frontendFramework = resolveFrontendFramework(resourceType, options.framework);
|
|
354
339
|
let dddEnabled = false;
|
|
355
340
|
if (options.ddd) {
|
|
356
341
|
if (resourceType !== "backend" && resourceType !== "worker") {
|
|
@@ -409,6 +394,7 @@ export const resourceCommand = new Command("resource")
|
|
|
409
394
|
}
|
|
410
395
|
}
|
|
411
396
|
let finalResourcePath = resourcePath;
|
|
397
|
+
let pathAdjustedFrom;
|
|
412
398
|
if (!finalResourcePath) {
|
|
413
399
|
const defaultPaths = {
|
|
414
400
|
backend: `services/${stackName}/${resourceName}`,
|
|
@@ -417,6 +403,12 @@ export const resourceCommand = new Command("resource")
|
|
|
417
403
|
sdk: `packages/${resourceName}`,
|
|
418
404
|
};
|
|
419
405
|
finalResourcePath = defaultPaths[resourceType];
|
|
406
|
+
// An SDK is registered where it already lives, so its folder is never moved.
|
|
407
|
+
if (resourceType !== "sdk") {
|
|
408
|
+
const chosen = chooseResourcePath(projectRoot, finalResourcePath, stackName, resourceName);
|
|
409
|
+
finalResourcePath = chosen.path;
|
|
410
|
+
pathAdjustedFrom = chosen.adjustedFrom;
|
|
411
|
+
}
|
|
420
412
|
}
|
|
421
413
|
const fullPath = resolve(projectRoot, finalResourcePath);
|
|
422
414
|
// Prevent path traversal attacks
|
|
@@ -482,14 +474,17 @@ export const resourceCommand = new Command("resource")
|
|
|
482
474
|
mkdirSync(fullPath, { recursive: true });
|
|
483
475
|
mkdirSync(resolve(fullPath, "src"), { recursive: true });
|
|
484
476
|
mkdirSync(resolve(fullPath, "tests"), { recursive: true });
|
|
477
|
+
if (resourceType === "frontend") {
|
|
478
|
+
mkdirSync(resolve(fullPath, "public"), { recursive: true });
|
|
479
|
+
}
|
|
485
480
|
if (dddEnabled) {
|
|
486
481
|
for (const layer of ["domain", "application", "infrastructure", "presentation"]) {
|
|
487
482
|
mkdirSync(resolve(fullPath, "src", layer), { recursive: true });
|
|
488
483
|
}
|
|
489
484
|
}
|
|
490
485
|
// Prepare file generation tasks
|
|
491
|
-
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : []);
|
|
492
|
-
const packageJson = createPackageJson(resourceName, resourceType);
|
|
486
|
+
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id);
|
|
487
|
+
const packageJson = createPackageJson(resourceName, resourceType, frontendFramework?.id);
|
|
493
488
|
const tasks = [
|
|
494
489
|
{
|
|
495
490
|
type: "json",
|
|
@@ -508,7 +503,7 @@ export const resourceCommand = new Command("resource")
|
|
|
508
503
|
{
|
|
509
504
|
type: "json",
|
|
510
505
|
filename: "tsconfig.json",
|
|
511
|
-
content:
|
|
506
|
+
content: createResourceTsconfig(resourceType, frontendFramework?.id),
|
|
512
507
|
description: "Generating tsconfig.json",
|
|
513
508
|
emoji: "⚙️",
|
|
514
509
|
},
|
|
@@ -531,26 +526,19 @@ export const resourceCommand = new Command("resource")
|
|
|
531
526
|
});
|
|
532
527
|
}
|
|
533
528
|
else if (resourceType === "frontend") {
|
|
534
|
-
tasks.push({
|
|
529
|
+
tasks.push(...(frontendFramework?.createFiles(resourceName).map((file) => ({
|
|
530
|
+
...file,
|
|
535
531
|
type: "text",
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
});
|
|
532
|
+
})) ?? []));
|
|
533
|
+
// The shared frontend Docker builder unconditionally runs `COPY <resource>/public`
|
|
534
|
+
// (the Vite static-assets folder), and an empty folder can't be committed, so
|
|
535
|
+
// without a placeholder the image build fails for every framework.
|
|
541
536
|
tasks.push({
|
|
542
537
|
type: "text",
|
|
543
|
-
filename: "
|
|
544
|
-
content:
|
|
545
|
-
description: "Generating
|
|
546
|
-
emoji: "
|
|
547
|
-
});
|
|
548
|
-
tasks.push({
|
|
549
|
-
type: "text",
|
|
550
|
-
filename: "src/App.tsx",
|
|
551
|
-
content: getFrontendAppTemplate(resourceName),
|
|
552
|
-
description: "Generating React app",
|
|
553
|
-
emoji: "💻",
|
|
538
|
+
filename: "public/.gitkeep",
|
|
539
|
+
content: "",
|
|
540
|
+
description: "Generating public/ assets folder",
|
|
541
|
+
emoji: "📁",
|
|
554
542
|
});
|
|
555
543
|
}
|
|
556
544
|
else if (resourceType === "worker") {
|
|
@@ -575,6 +563,18 @@ export const resourceCommand = new Command("resource")
|
|
|
575
563
|
writeFilesWithProgress(fullPath, tasks);
|
|
576
564
|
console.log(chalk.green("\n✅ Resource created successfully!"));
|
|
577
565
|
console.log(chalk.gray(`\nLocation: ${fullPath}`));
|
|
566
|
+
const discoveryPaths = readDiscoveryPaths(projectRoot);
|
|
567
|
+
if (pathAdjustedFrom) {
|
|
568
|
+
console.log(chalk.yellow(`\n📁 Placed in ${finalResourcePath} instead of the usual ${pathAdjustedFrom}: ` +
|
|
569
|
+
`discovery.paths in .tdk/project.json is ${JSON.stringify(discoveryPaths)}, ` +
|
|
570
|
+
`so ${pathAdjustedFrom} would never be started by \`tdk up\`.`));
|
|
571
|
+
console.log(chalk.gray(" Use --path to choose another folder."));
|
|
572
|
+
}
|
|
573
|
+
else if (!isPathDiscovered(projectRoot, finalResourcePath, discoveryPaths)) {
|
|
574
|
+
console.log(chalk.yellow(`\n⚠️ ${finalResourcePath} is outside discovery.paths ${JSON.stringify(discoveryPaths)}, ` +
|
|
575
|
+
"so `tdk up` will not start this resource."));
|
|
576
|
+
console.log(chalk.gray(" Add a matching pattern to discovery.paths in .tdk/project.json and run `tdk config regenerate`."));
|
|
577
|
+
}
|
|
578
578
|
console.log(chalk.gray(`\nNext steps:`));
|
|
579
579
|
console.log(chalk.gray(` cd ${finalResourcePath}`));
|
|
580
580
|
console.log(chalk.gray(` bun install`));
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -11,8 +11,8 @@ import { isTiltAvailable } from "../utils/tilt.js";
|
|
|
11
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: " a Toggle all/pre-alpha services" }), _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/Esc Quit / 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
|
-
const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "
|
|
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: "
|
|
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" })] }));
|
|
16
16
|
const TUIApp = () => {
|
|
17
17
|
const { exit } = useApp();
|
|
18
18
|
const { stdout } = useStdout();
|
|
@@ -33,21 +33,27 @@ const TUIApp = () => {
|
|
|
33
33
|
const [showTooltips, setShowTooltips] = useState(true);
|
|
34
34
|
const [showEnabledOnly, setShowEnabledOnly] = useState(true);
|
|
35
35
|
const projectRoot = findProjectRoot() || "unknown";
|
|
36
|
-
const { stacks, services } =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (services.length === 0) {
|
|
45
|
-
setError('No services found. Run "tdk init" to get started.');
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
36
|
+
const [{ stacks, services }, setDiscovered] = useState({ stacks: [], services: [] });
|
|
37
|
+
// Re-read service.json files from disk. An empty project is not an error:
|
|
38
|
+
// it renders EmptyState. Only a failed discovery shows ErrorScreen.
|
|
39
|
+
const refresh = useCallback(() => {
|
|
40
|
+
clearMetadataCache();
|
|
41
|
+
try {
|
|
42
|
+
setDiscovered({ stacks: discoverStacks(), services: discoverResources() });
|
|
48
43
|
setError(null);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
48
|
+
return false;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
refresh();
|
|
56
|
+
}, [refresh]);
|
|
51
57
|
const selectedStackData = useMemo(() => {
|
|
52
58
|
if (!selectedStack)
|
|
53
59
|
return null;
|
|
@@ -241,8 +247,7 @@ const TUIApp = () => {
|
|
|
241
247
|
useInput((input, key) => {
|
|
242
248
|
if (error) {
|
|
243
249
|
if (input === "r" || input === "R") {
|
|
244
|
-
|
|
245
|
-
setLoading(true);
|
|
250
|
+
refresh();
|
|
246
251
|
return;
|
|
247
252
|
}
|
|
248
253
|
if (input === "q" || key.escape) {
|
|
@@ -300,9 +305,10 @@ const TUIApp = () => {
|
|
|
300
305
|
return;
|
|
301
306
|
}
|
|
302
307
|
if (input === "r") {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
if (refresh()) {
|
|
309
|
+
setMessage("Data refreshed");
|
|
310
|
+
setTimeout(() => setMessage(""), 1500);
|
|
311
|
+
}
|
|
306
312
|
return;
|
|
307
313
|
}
|
|
308
314
|
if (input === "/") {
|
|
@@ -404,10 +410,7 @@ const TUIApp = () => {
|
|
|
404
410
|
return _jsx(LoadingScreen, { progress: 100, message: "Initializing..." });
|
|
405
411
|
}
|
|
406
412
|
if (error) {
|
|
407
|
-
return
|
|
408
|
-
setError(null);
|
|
409
|
-
setLoading(true);
|
|
410
|
-
} }));
|
|
413
|
+
return _jsx(ErrorScreen, { error: error, onRetry: refresh });
|
|
411
414
|
}
|
|
412
415
|
if (services.length === 0) {
|
|
413
416
|
return _jsx(EmptyState, {});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const reactFrontendProvider = {
|
|
2
|
+
id: "react",
|
|
3
|
+
label: "React + Vite",
|
|
4
|
+
dependencies: {
|
|
5
|
+
react: "^19.0.0",
|
|
6
|
+
"react-dom": "^19.0.0",
|
|
7
|
+
},
|
|
8
|
+
devDependencies: {
|
|
9
|
+
"@types/react": "^19.0.0",
|
|
10
|
+
"@types/react-dom": "^19.0.0",
|
|
11
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
12
|
+
},
|
|
13
|
+
compilerOptions: {
|
|
14
|
+
jsx: "react-jsx",
|
|
15
|
+
},
|
|
16
|
+
createFiles(name) {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
filename: "index.html",
|
|
20
|
+
content: `<!DOCTYPE html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="UTF-8" />
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
25
|
+
<title>${name}</title>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="root"></div>
|
|
29
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
`,
|
|
33
|
+
description: "Generating HTML template",
|
|
34
|
+
emoji: "💻",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
filename: "src/main.tsx",
|
|
38
|
+
content: `import React from 'react';
|
|
39
|
+
import ReactDOM from 'react-dom/client';
|
|
40
|
+
import App from './App';
|
|
41
|
+
|
|
42
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
43
|
+
<React.StrictMode>
|
|
44
|
+
<App />
|
|
45
|
+
</React.StrictMode>,
|
|
46
|
+
);
|
|
47
|
+
`,
|
|
48
|
+
description: "Generating React entry",
|
|
49
|
+
emoji: "💻",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
filename: "src/App.tsx",
|
|
53
|
+
content: `function App() {
|
|
54
|
+
return (
|
|
55
|
+
<div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
56
|
+
<h1>${name}</h1>
|
|
57
|
+
<p>Frontend resource created with TDK</p>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default App;
|
|
63
|
+
`,
|
|
64
|
+
description: "Generating React app",
|
|
65
|
+
emoji: "💻",
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TdkError } from "../utils/errors.js";
|
|
2
|
+
import { reactFrontendProvider } from "./react.js";
|
|
3
|
+
import { vueFrontendProvider } from "./vue.js";
|
|
4
|
+
export const DEFAULT_FRONTEND_FRAMEWORK = "react";
|
|
5
|
+
export const FRONTEND_FRAMEWORKS = {
|
|
6
|
+
react: reactFrontendProvider,
|
|
7
|
+
vue: vueFrontendProvider,
|
|
8
|
+
};
|
|
9
|
+
export function getFrontendFramework(frameworkId) {
|
|
10
|
+
const id = (frameworkId ?? DEFAULT_FRONTEND_FRAMEWORK).trim().toLowerCase();
|
|
11
|
+
const provider = Object.hasOwn(FRONTEND_FRAMEWORKS, id) ? FRONTEND_FRAMEWORKS[id] : undefined;
|
|
12
|
+
if (!provider) {
|
|
13
|
+
const supportedIds = Object.keys(FRONTEND_FRAMEWORKS).join(", ");
|
|
14
|
+
throw new TdkError(`Unknown frontend framework "${frameworkId}". Supported frameworks: ${supportedIds}.`, [`Use one of: ${supportedIds}`, `Omit --framework to use ${DEFAULT_FRONTEND_FRAMEWORK}`]);
|
|
15
|
+
}
|
|
16
|
+
return provider;
|
|
17
|
+
}
|
|
18
|
+
export function resolveFrontendFramework(resourceType, frameworkId) {
|
|
19
|
+
if (resourceType !== "frontend") {
|
|
20
|
+
if (frameworkId !== undefined) {
|
|
21
|
+
throw new TdkError("--framework can only be used with --type frontend.", [
|
|
22
|
+
"Add --type frontend, or drop --framework for this resource type",
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return getFrontendFramework(frameworkId);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=registry.js.map
|