@tdk-landscape/tdk-cli-core 1.3.74 → 1.3.75
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 +3 -0
- package/cli/dist/commands/doctor.js +58 -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/errors.js +4 -1
- package/cli/package.json +1 -1
- 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/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.
|
|
@@ -1,10 +1,11 @@
|
|
|
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";
|
|
9
10
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
10
11
|
import { formatCount } from "../utils/formatting.js";
|
|
@@ -218,6 +219,31 @@ function checkMasterConfigs() {
|
|
|
218
219
|
fix: "Run: tdk project",
|
|
219
220
|
};
|
|
220
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Tilt only builds resources under `discovery.paths`, but the CLI finds every service.json,
|
|
224
|
+
* so a resource outside them is listed and given a URL by `tdk up` yet never started.
|
|
225
|
+
*/
|
|
226
|
+
export function checkResourceDiscovery() {
|
|
227
|
+
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
228
|
+
const patterns = readDiscoveryPaths(projectRoot);
|
|
229
|
+
const stranded = discoverResourcesFromRoot(projectRoot).filter((resource) => !isPathDiscovered(projectRoot, resource.path, patterns));
|
|
230
|
+
if (stranded.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
name: "Resource Discovery",
|
|
233
|
+
didPass: true,
|
|
234
|
+
message: `All resources are covered by discovery.paths ${JSON.stringify(patterns)}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const list = stranded
|
|
238
|
+
.map((resource) => normalize(relative(projectRoot, resource.path)))
|
|
239
|
+
.join(", ");
|
|
240
|
+
return {
|
|
241
|
+
name: "Resource Discovery",
|
|
242
|
+
didPass: false,
|
|
243
|
+
message: `${formatCount(stranded.length, "resource")} outside discovery.paths ${JSON.stringify(patterns)}: ${list}. \`tdk up\` will not start them`,
|
|
244
|
+
fix: "Move each under services/<stack>/<name>, or add a matching pattern to discovery.paths in .tdk/project.json and run: tdk config regenerate",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
221
247
|
// The scripts `tdk project` copies into every project. Generated Dockerfiles
|
|
222
248
|
// may reference others; those are checked by scanning the Dockerfiles below.
|
|
223
249
|
const REQUIRED_DOCKER_TEMPLATE_FILES = [
|
|
@@ -491,6 +517,26 @@ const KNOWN_TYPE_PACKAGE_VERSIONS = {
|
|
|
491
517
|
"@types/bun": "^1.4.2",
|
|
492
518
|
"@types/node": "^20.0.0",
|
|
493
519
|
};
|
|
520
|
+
/**
|
|
521
|
+
* `@types/bun` (via its underlying `bun-types` package) ships a hardcoded
|
|
522
|
+
* `/// <reference types="node" />` in its own index.d.ts. TypeScript always
|
|
523
|
+
* honors an explicit triple-slash reference, regardless of what the
|
|
524
|
+
* tsconfig's own `"types"` array restricts to -- so a Docker tsconfig with
|
|
525
|
+
* `"types": ["bun"]` still requires `@types/node` to resolve, even though
|
|
526
|
+
* "node" never appears in that array.
|
|
527
|
+
*
|
|
528
|
+
* When `@types/node` isn't a direct dependency, it can only resolve as a
|
|
529
|
+
* *transitive* dependency of `bun-types`, hoisted wherever Bun's installer
|
|
530
|
+
* happens to place it. That is not guaranteed in the isolated, per-service
|
|
531
|
+
* `bun install` that Docker's build layers run (unlike a full workspace-root
|
|
532
|
+
* install), so the same tsconfig can build locally and fail in Docker with
|
|
533
|
+
* `TS2688: Cannot find type definition file for 'node'`. Declaring
|
|
534
|
+
* `@types/node` directly makes the install deterministic either way.
|
|
535
|
+
*/
|
|
536
|
+
function needsNodeTypesForBun(installed, types) {
|
|
537
|
+
const usesBunTypes = types.includes("bun") || installed.has("@types/bun");
|
|
538
|
+
return usesBunTypes && !installed.has("@types/node");
|
|
539
|
+
}
|
|
494
540
|
export function checkTypeScriptTypeDependencies() {
|
|
495
541
|
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
496
542
|
const resources = discoverResourcesFromRoot(projectRoot);
|
|
@@ -527,6 +573,10 @@ export function checkTypeScriptTypeDependencies() {
|
|
|
527
573
|
const described = missing.map((m) => `"${m}" (needs ${neededTypePackage(m)})`).join(", ");
|
|
528
574
|
problems.push(`${resource.name}: "types" lists ${described}`);
|
|
529
575
|
}
|
|
576
|
+
if (needsNodeTypesForBun(installed, types)) {
|
|
577
|
+
missingPackages.add("@types/node");
|
|
578
|
+
problems.push(`${resource.name}: uses @types/bun, which references node types internally, but @types/node is not a direct dependency`);
|
|
579
|
+
}
|
|
530
580
|
}
|
|
531
581
|
if (problems.length === 0) {
|
|
532
582
|
return {
|
|
@@ -542,7 +592,7 @@ export function checkTypeScriptTypeDependencies() {
|
|
|
542
592
|
return {
|
|
543
593
|
name: "TypeScript Type Dependencies",
|
|
544
594
|
didPass: false,
|
|
545
|
-
message:
|
|
595
|
+
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
596
|
fix: `Add to each service's package.json devDependencies: ${lines.join(", ")}`,
|
|
547
597
|
};
|
|
548
598
|
}
|
|
@@ -573,6 +623,11 @@ export function checkFrontendDockerPreflight() {
|
|
|
573
623
|
}
|
|
574
624
|
const problems = [];
|
|
575
625
|
for (const resource of resources) {
|
|
626
|
+
// The frontend Docker builder runs `COPY <resource>/public ./public`, so a missing
|
|
627
|
+
// folder fails the image build with "/public: not found".
|
|
628
|
+
if (!existsSync(join(resource.path, "public"))) {
|
|
629
|
+
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")))}`);
|
|
630
|
+
}
|
|
576
631
|
const dockerfilePath = join(resource.path, ".autogenerated", "Dockerfile.app.autogenerated");
|
|
577
632
|
const dockerTsconfigPath = join(resource.path, ".autogenerated", "tsconfig.docker.autogenerated.json");
|
|
578
633
|
if (existsSync(dockerfilePath)) {
|
|
@@ -750,6 +805,7 @@ export const doctorCommand = new Command("doctor")
|
|
|
750
805
|
() => checkHealthRoutes(),
|
|
751
806
|
checkTypeScriptTypeDependencies,
|
|
752
807
|
checkFrontendDockerPreflight,
|
|
808
|
+
checkResourceDiscovery,
|
|
753
809
|
checkEnvironmentVariables,
|
|
754
810
|
// Preflight: catch "port 80 already allocated" BEFORE claiming ready.
|
|
755
811
|
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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const vueFrontendProvider = {
|
|
2
|
+
id: "vue",
|
|
3
|
+
label: "Vue + Vite",
|
|
4
|
+
dependencies: {
|
|
5
|
+
vue: "^3.5.0",
|
|
6
|
+
},
|
|
7
|
+
devDependencies: {
|
|
8
|
+
"@vitejs/plugin-vue": "^5.2.4",
|
|
9
|
+
},
|
|
10
|
+
compilerOptions: {},
|
|
11
|
+
createFiles(name) {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
filename: "index.html",
|
|
15
|
+
content: `<!DOCTYPE html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
+
<title>${name}</title>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div id="app"></div>
|
|
24
|
+
<script type="module" src="/src/main.ts"></script>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
`,
|
|
28
|
+
description: "Generating HTML template",
|
|
29
|
+
emoji: "💻",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
filename: "src/main.ts",
|
|
33
|
+
content: `import { createApp } from 'vue';
|
|
34
|
+
import App from './App.vue';
|
|
35
|
+
|
|
36
|
+
createApp(App).mount('#app');
|
|
37
|
+
`,
|
|
38
|
+
description: "Generating Vue entry",
|
|
39
|
+
emoji: "💻",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
filename: "src/App.vue",
|
|
43
|
+
content: `<script setup lang="ts">
|
|
44
|
+
const message = 'Frontend resource created with TDK';
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<main style="padding: 2rem; font-family: system-ui">
|
|
49
|
+
<h1>${name}</h1>
|
|
50
|
+
<p>{{ message }}</p>
|
|
51
|
+
</main>
|
|
52
|
+
</template>
|
|
53
|
+
`,
|
|
54
|
+
description: "Generating Vue app",
|
|
55
|
+
emoji: "💻",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
filename: "src/vite-env.d.ts",
|
|
59
|
+
content: `/// <reference types="vite/client" />
|
|
60
|
+
|
|
61
|
+
declare module '*.vue' {
|
|
62
|
+
import type { Component } from 'vue';
|
|
63
|
+
const component: Component;
|
|
64
|
+
export default component;
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
description: "Generating Vue module types",
|
|
68
|
+
emoji: "⚙️",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=vue.js.map
|