@tdk-landscape/tdk-cli-core 1.3.75 → 1.3.77
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 +59 -13
- package/cli/bin/tdk.js +22 -2
- package/cli/dist/cli.js +9 -2
- package/cli/dist/commands/completion.js +30 -3
- package/cli/dist/commands/config.js +59 -1
- package/cli/dist/commands/doctor.js +295 -98
- package/cli/dist/commands/eject.js +77 -0
- package/cli/dist/commands/networks.js +17 -4
- package/cli/dist/commands/project.js +28 -5
- package/cli/dist/commands/resource.js +118 -10
- package/cli/dist/commands/up.js +93 -21
- package/cli/dist/commands/upgrade.js +163 -68
- package/cli/dist/generator/template-engine.js +77 -13
- package/cli/dist/types/index.js +6 -1
- package/cli/dist/utils/cold-preflight.js +198 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/docker.js +15 -1
- package/cli/dist/utils/doctor-runtime.js +48 -16
- package/cli/dist/utils/doctor-wiring.js +269 -0
- package/cli/dist/utils/errors.js +5 -5
- package/cli/dist/utils/exec-async.js +30 -0
- package/cli/dist/utils/generated-paths.js +49 -0
- package/cli/dist/utils/platform.js +53 -0
- package/cli/dist/utils/port-assignment.js +13 -20
- package/cli/dist/utils/project-templates.js +7 -2
- package/cli/dist/utils/resource-features.js +6 -2
- package/cli/dist/utils/service-manifest.js +73 -0
- package/cli/dist/utils/services.js +8 -1
- package/cli/dist/utils/tar.js +27 -5
- package/cli/dist/utils/tilt-process.js +53 -0
- package/cli/dist/utils/tilt.js +3 -1
- package/cli/dist/utils/which.js +26 -0
- package/cli/dist/utils/windows-doctor.js +175 -0
- package/cli/package.json +1 -1
- package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
- package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
- package/cli/templates/Tiltfile.hbs +24 -4
- package/cli/templates/spec.master.hbs +1 -2
- package/discovery/AGENTS.md +1 -1
- package/discovery/constants.star +2 -2
- package/discovery/discovery_orchestrator.star +2 -2
- package/discovery/manifest/constants.star +1 -1
- package/discovery/manifest/loading.star +7 -3
- package/discovery/registry.star +17 -3
- package/engine/README.md +2 -0
- package/engine/schemas/service-schema.json +47 -21
- package/engine/scripts/generate-manifest.sh +2 -1
- package/engine/topologies/platform/docker/compose/compose.star +37 -5
- package/engine/topologies/tilt/common/utils.star +54 -11
- package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
- package/engine/topologies/tilt/manifest/constants.star +3 -0
- package/engine/topologies/tilt/manifest/schema.star +2 -4
- package/engine/topologies/tilt/manifest/validator.star +1 -1
- package/engine/topologies/tilt/resources/infra-loader.star +14 -3
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
- package/examples/tdk-example/LICENSE +21 -0
- package/examples/tdk-example/README.md +23 -0
- package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
- package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
- package/examples/tdk-example/Tiltfile +25 -0
- package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
- package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
- package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
- package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
- package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
- package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
- package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
- package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
- package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
- package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
- package/package.json +2 -1
package/cli/dist/utils/errors.js
CHANGED
|
@@ -63,16 +63,16 @@ export const errorFactories = {
|
|
|
63
63
|
"Path must be within the project directory",
|
|
64
64
|
'Path cannot contain special characters like <>:"|?*',
|
|
65
65
|
]),
|
|
66
|
-
notInProject: () => new TdkError("Could not find project root (no
|
|
67
|
-
"Run this from within a project
|
|
68
|
-
"Run `tdk project` to initialize a new project",
|
|
66
|
+
notInProject: () => new TdkError("Could not find project root (no .tdk/project.json found)", [
|
|
67
|
+
"Run this from within a TDK project",
|
|
68
|
+
"Run `tdk project --yes` to initialize a new project",
|
|
69
69
|
]),
|
|
70
70
|
};
|
|
71
71
|
export function requireProjectRoot() {
|
|
72
72
|
const projectRoot = findProjectRoot();
|
|
73
73
|
if (!projectRoot) {
|
|
74
|
-
console.error(chalk.red("Error: Could not find project root (no
|
|
75
|
-
console.error(chalk.gray("Run
|
|
74
|
+
console.error(chalk.red("Error: Could not find project root (no .tdk/project.json found)."));
|
|
75
|
+
console.error(chalk.gray("Run `tdk project --yes` to initialize a new project."));
|
|
76
76
|
process.exit(1);
|
|
77
77
|
}
|
|
78
78
|
return projectRoot;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { exec, execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { findOnPath } from "./which.js";
|
|
4
|
+
const execPromise = promisify(exec);
|
|
5
|
+
export const execAsync = async (command, timeoutMs) => {
|
|
6
|
+
if (process.platform === "win32") {
|
|
7
|
+
// Doctor commands are simple executable/argument invocations. Tokenize
|
|
8
|
+
// their quoted arguments and bypass cmd.exe/PowerShell entirely.
|
|
9
|
+
const parts = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
10
|
+
const [parsedFile, ...args] = parts.map((part) => part.startsWith('"') || part.startsWith("'") ? part.slice(1, -1) : part);
|
|
11
|
+
if (!parsedFile)
|
|
12
|
+
throw new Error("Command is empty");
|
|
13
|
+
const file = findOnPath(parsedFile) ?? parsedFile;
|
|
14
|
+
const { stdout } = await promisify(execFile)(file, args, {
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
timeout: timeoutMs,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
shell: false,
|
|
19
|
+
});
|
|
20
|
+
return stdout;
|
|
21
|
+
}
|
|
22
|
+
const { stdout } = await execPromise(command, { encoding: "utf-8", timeout: timeoutMs });
|
|
23
|
+
return stdout;
|
|
24
|
+
};
|
|
25
|
+
/** `exec` kills a timed-out child and sets `killed`; `execSync` reports ETIMEDOUT. */
|
|
26
|
+
export function isExecTimeout(err) {
|
|
27
|
+
const e = err;
|
|
28
|
+
return e?.code === "ETIMEDOUT" || e?.killed === true;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=exec-async.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, normalize, relative, sep } from "node:path";
|
|
3
|
+
/** Reviewed TDK-owned destinations written by `tdk config regenerate` or Tilt generation. */
|
|
4
|
+
export const TDK_GENERATED_PATH_ALLOWLIST = [
|
|
5
|
+
".autogenerated/**",
|
|
6
|
+
".tdk/.tdk-out/**",
|
|
7
|
+
"Tiltfile",
|
|
8
|
+
"TILT_*.star",
|
|
9
|
+
".tiltignore",
|
|
10
|
+
"package.json (create only)",
|
|
11
|
+
"shared-platform-engineering/docker-templates/**",
|
|
12
|
+
"services/platform/database-management/docker-compose.yml",
|
|
13
|
+
"product_autogenerated_resource_manifests/**",
|
|
14
|
+
"product_autogenerated_services_list/**",
|
|
15
|
+
];
|
|
16
|
+
export function isTdkGeneratedPath(path) {
|
|
17
|
+
const normalized = normalize(path).split(sep).join("/");
|
|
18
|
+
if (isAbsolute(path) || normalized === ".." || normalized.startsWith("../"))
|
|
19
|
+
return false;
|
|
20
|
+
if (normalized.split("/").includes(".autogenerated") ||
|
|
21
|
+
normalized.startsWith(".tdk/.tdk-out/") ||
|
|
22
|
+
normalized === "shared-platform-engineering/docker-templates" ||
|
|
23
|
+
normalized.startsWith("shared-platform-engineering/docker-templates/") ||
|
|
24
|
+
normalized.startsWith("product_autogenerated_resource_manifests/") ||
|
|
25
|
+
normalized.startsWith("product_autogenerated_services_list/")) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return (normalized === "Tiltfile" ||
|
|
29
|
+
/^TILT_[^/]+\.star$/.test(normalized) ||
|
|
30
|
+
normalized === ".tiltignore" ||
|
|
31
|
+
normalized === "package.json" ||
|
|
32
|
+
normalized === "services/platform/database-management/docker-compose.yml");
|
|
33
|
+
}
|
|
34
|
+
export function assertTdkGeneratedPath(projectRoot, path) {
|
|
35
|
+
const absolute = isAbsolute(path) ? normalize(path) : normalize(`${projectRoot}/${path}`);
|
|
36
|
+
const projectRelative = relative(projectRoot, absolute).split(sep).join("/");
|
|
37
|
+
if (!isTdkGeneratedPath(projectRelative)) {
|
|
38
|
+
throw new Error(`Refusing to write outside TDK's generated-file allowlist: ${projectRelative}`);
|
|
39
|
+
}
|
|
40
|
+
let cursor = projectRoot;
|
|
41
|
+
for (const segment of projectRelative.split("/")) {
|
|
42
|
+
cursor = normalize(`${cursor}/${segment}`);
|
|
43
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) {
|
|
44
|
+
throw new Error(`Refusing to write through a symbolic link in generated path: ${projectRelative}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return absolute;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=generated-paths.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { delimiter, extname } from "node:path";
|
|
2
|
+
export function isWindows() {
|
|
3
|
+
return process.platform === "win32";
|
|
4
|
+
}
|
|
5
|
+
export function tdkOsFor(platformName) {
|
|
6
|
+
if (platformName === "linux")
|
|
7
|
+
return "linux";
|
|
8
|
+
if (platformName === "darwin")
|
|
9
|
+
return "darwin";
|
|
10
|
+
if (platformName === "win32")
|
|
11
|
+
return "windows";
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export function tdkOs() {
|
|
15
|
+
return tdkOsFor(process.platform);
|
|
16
|
+
}
|
|
17
|
+
export function tdkArchFor(architecture) {
|
|
18
|
+
if (architecture === "x64")
|
|
19
|
+
return "amd64";
|
|
20
|
+
if (architecture === "arm64")
|
|
21
|
+
return "arm64";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export function tdkArch() {
|
|
25
|
+
return tdkArchFor(process.arch);
|
|
26
|
+
}
|
|
27
|
+
export function binaryAssetNameFor(platformName, architecture) {
|
|
28
|
+
const osName = tdkOsFor(platformName);
|
|
29
|
+
const archName = tdkArchFor(architecture);
|
|
30
|
+
if (!osName || !archName)
|
|
31
|
+
return null;
|
|
32
|
+
if (osName === "windows" && archName !== "amd64")
|
|
33
|
+
return null;
|
|
34
|
+
const base = `tdk-${osName}-${archName}`;
|
|
35
|
+
return osName === "windows" ? `${base}.exe` : base;
|
|
36
|
+
}
|
|
37
|
+
export function binaryAssetName() {
|
|
38
|
+
return binaryAssetNameFor(process.platform, process.arch);
|
|
39
|
+
}
|
|
40
|
+
export function executableName() {
|
|
41
|
+
return isWindows() ? "tdk.exe" : "tdk";
|
|
42
|
+
}
|
|
43
|
+
export function pathLookups(binName, windows = isWindows()) {
|
|
44
|
+
if (!windows)
|
|
45
|
+
return [binName];
|
|
46
|
+
if (extname(binName))
|
|
47
|
+
return [binName];
|
|
48
|
+
return [binName, `${binName}.exe`, `${binName}.cmd`, `${binName}.bat`];
|
|
49
|
+
}
|
|
50
|
+
export function pathDelimiter(windows = isWindows()) {
|
|
51
|
+
return windows ? ";" : delimiter;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createConnection } from "node:net";
|
|
1
|
+
import { createConnection, createServer } from "node:net";
|
|
3
2
|
import { PORT_RANGES } from "./constants.js";
|
|
4
3
|
function isPortAvailable(port) {
|
|
5
4
|
return new Promise((resolve) => {
|
|
@@ -20,26 +19,20 @@ function isPortAvailable(port) {
|
|
|
20
19
|
}
|
|
21
20
|
export async function checkPortStatus(port) {
|
|
22
21
|
return new Promise((resolve) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
let hasOutput = false;
|
|
28
|
-
child.stdout?.on("data", () => {
|
|
29
|
-
hasOutput = true;
|
|
30
|
-
});
|
|
31
|
-
child.on("close", (code) => {
|
|
32
|
-
// lsof returns 0 if it found something, 1 if nothing found
|
|
33
|
-
if (code === 0) {
|
|
34
|
-
resolve("running");
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
resolve(hasOutput ? "running" : "stopped");
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
child.on("error", () => {
|
|
22
|
+
const server = createServer();
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
server.close();
|
|
41
25
|
resolve("unknown");
|
|
26
|
+
}, 3000);
|
|
27
|
+
server.once("error", (error) => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
resolve(error.code === "EADDRINUSE" ? "running" : "unknown");
|
|
30
|
+
});
|
|
31
|
+
server.once("listening", () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
server.close(() => resolve("stopped"));
|
|
42
34
|
});
|
|
35
|
+
server.listen(port, "127.0.0.1");
|
|
43
36
|
});
|
|
44
37
|
}
|
|
45
38
|
export async function findAvailablePort(basePort, maxAttempts = 10) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Public starter repositories plus the bundled default product example. */
|
|
2
2
|
export const PROJECT_TEMPLATES = {
|
|
3
3
|
restaurant: {
|
|
4
4
|
repo: "https://github.com/tdk-landscape/tdk-restaurant-example.git",
|
|
@@ -16,9 +16,14 @@ export const PROJECT_TEMPLATES = {
|
|
|
16
16
|
repo: "https://github.com/tdk-landscape/tdk-user-management.git",
|
|
17
17
|
description: "User/auth management services",
|
|
18
18
|
},
|
|
19
|
+
ecommerce: {
|
|
20
|
+
repo: "https://github.com/tdk-landscape/tdk-ecommerce-example.git",
|
|
21
|
+
description: "Vue 3 storefront and Hono catalog API (needs TDK 1.3.75+ for --framework vue)",
|
|
22
|
+
},
|
|
19
23
|
example: {
|
|
20
24
|
repo: "https://github.com/tdk-landscape/tdk-example.git",
|
|
21
|
-
|
|
25
|
+
bundledPath: "examples/tdk-example",
|
|
26
|
+
description: "Hono + Postgres + NATS worker + Vite example with a routed write path",
|
|
22
27
|
},
|
|
23
28
|
};
|
|
24
29
|
//# sourceMappingURL=project-templates.js.map
|
|
@@ -150,14 +150,18 @@ export const RESOURCE_FEATURES = {
|
|
|
150
150
|
applies_to: ["backend", "frontend"],
|
|
151
151
|
enabled_by_default: false,
|
|
152
152
|
category: "premium",
|
|
153
|
-
generator_file: "engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star, engine/topologies/platform/docker/compose/traefik_standalone.star (deferStart wake gateway + static route), engine/topologies/
|
|
154
|
-
output_file: "(Traefik + Sablier Docker labels on the resource's own compose service; with deferStart, also a generated Traefik file-provider dynamic-config entry and
|
|
153
|
+
generator_file: "engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star, engine/topologies/platform/docker/compose/traefik_standalone.star (deferStart wake gateway + static route), engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star (deferred image prebuild + wake route wiring)",
|
|
154
|
+
output_file: "(Traefik + Sablier Docker labels on the resource's own compose service; with deferStart, also a generated Traefik file-provider dynamic-config entry, wake gateway, and image prebuild resource)",
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
157
|
/**
|
|
158
158
|
* Get default features for a resource type
|
|
159
159
|
*/
|
|
160
160
|
export function getDefaultFeaturesForResourceType(appType) {
|
|
161
|
+
// bring-your-own doesn't generate application code, so no features
|
|
162
|
+
if (appType === "bring-your-own") {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
161
165
|
const defaults = [];
|
|
162
166
|
for (const [key, feature] of Object.entries(RESOURCE_FEATURES)) {
|
|
163
167
|
if (feature.enabled_by_default &&
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
export const SERVICE_MANIFEST_SCHEMA_VERSION = 1;
|
|
3
|
+
const KNOWN_SERVICE_FIELDS = new Set([
|
|
4
|
+
"$schema",
|
|
5
|
+
"apiBasePath",
|
|
6
|
+
"appName",
|
|
7
|
+
"appType",
|
|
8
|
+
"backendName",
|
|
9
|
+
"basePath",
|
|
10
|
+
"build",
|
|
11
|
+
"databaseName",
|
|
12
|
+
"dependencies",
|
|
13
|
+
"dev",
|
|
14
|
+
"dependsOn",
|
|
15
|
+
"dockerfile",
|
|
16
|
+
"enabled",
|
|
17
|
+
"exposeViaProxy",
|
|
18
|
+
"featuresEnabled",
|
|
19
|
+
"framework",
|
|
20
|
+
"healthCheckPath",
|
|
21
|
+
"image",
|
|
22
|
+
"name",
|
|
23
|
+
"nats",
|
|
24
|
+
"params",
|
|
25
|
+
"playwright",
|
|
26
|
+
"port",
|
|
27
|
+
"replicas",
|
|
28
|
+
"runtime",
|
|
29
|
+
"sablier",
|
|
30
|
+
"schemaVersion",
|
|
31
|
+
"secrets",
|
|
32
|
+
"stack",
|
|
33
|
+
"syncs",
|
|
34
|
+
"traefik",
|
|
35
|
+
"type",
|
|
36
|
+
]);
|
|
37
|
+
export function validateServiceManifest(value, displayPath) {
|
|
38
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
39
|
+
return { errors: [`${displayPath}: expected a JSON object`], warnings: [] };
|
|
40
|
+
}
|
|
41
|
+
const manifest = value;
|
|
42
|
+
const errors = [];
|
|
43
|
+
for (const field of ["appName", "appType", "stack", "schemaVersion"]) {
|
|
44
|
+
if (!(field in manifest))
|
|
45
|
+
errors.push(`${displayPath}.${field}: required field is missing`);
|
|
46
|
+
}
|
|
47
|
+
for (const field of ["appName", "appType", "stack"]) {
|
|
48
|
+
if (field in manifest && typeof manifest[field] !== "string") {
|
|
49
|
+
errors.push(`${displayPath}.${field}: expected a string`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if ("schemaVersion" in manifest && manifest.schemaVersion !== SERVICE_MANIFEST_SCHEMA_VERSION) {
|
|
53
|
+
errors.push(`${displayPath}.schemaVersion: unsupported version ${String(manifest.schemaVersion)} (supported: ${SERVICE_MANIFEST_SCHEMA_VERSION})`);
|
|
54
|
+
}
|
|
55
|
+
const warnings = Object.keys(manifest)
|
|
56
|
+
.filter((field) => !KNOWN_SERVICE_FIELDS.has(field))
|
|
57
|
+
.map((field) => `${displayPath}.${field}: unknown field is preserved`);
|
|
58
|
+
return { errors, warnings, manifest };
|
|
59
|
+
}
|
|
60
|
+
export function validateServiceManifestFile(filePath, displayPath) {
|
|
61
|
+
if (!existsSync(filePath)) {
|
|
62
|
+
return { errors: [`${displayPath}: file is missing`], warnings: [] };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
66
|
+
return validateServiceManifest(parsed, displayPath);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return { errors: [`${displayPath}: invalid JSON (${message})`], warnings: [] };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=service-manifest.js.map
|
|
@@ -4,6 +4,7 @@ import { Cache, createCacheValidator } from "./cache.js";
|
|
|
4
4
|
import { AUTOGENERATED_FILE_PATTERNS, SKIP_DIRECTORIES } from "./constants.js";
|
|
5
5
|
import { formatCount } from "./formatting.js";
|
|
6
6
|
import { findProjectRoot } from "./paths.js";
|
|
7
|
+
import { validateServiceManifest } from "./service-manifest.js";
|
|
7
8
|
import { includes } from "./validation.js";
|
|
8
9
|
const RESOURCE_JSON_FILENAME = "service.json";
|
|
9
10
|
function findServiceJsonFiles(dir, maxDepth = 5, currentDepth = 0) {
|
|
@@ -25,6 +26,9 @@ function findServiceJsonFiles(dir, maxDepth = 5, currentDepth = 0) {
|
|
|
25
26
|
}
|
|
26
27
|
return results;
|
|
27
28
|
}
|
|
29
|
+
export function discoverServiceManifestPaths(projectRoot) {
|
|
30
|
+
return findServiceJsonFiles(projectRoot);
|
|
31
|
+
}
|
|
28
32
|
function shouldSkipDirectory(name) {
|
|
29
33
|
return includes(SKIP_DIRECTORIES, name) || name.startsWith(".");
|
|
30
34
|
}
|
|
@@ -45,6 +49,9 @@ function parseResource(serviceJsonPath) {
|
|
|
45
49
|
throw new Error(`Invalid service.json at ${serviceJsonPath}: missing required fields (appName, runtime or appType)`);
|
|
46
50
|
}
|
|
47
51
|
const parsedConfig = parsed;
|
|
52
|
+
for (const warning of validateServiceManifest(parsed, serviceJsonPath).warnings) {
|
|
53
|
+
console.warn(`⚠️ Unknown service.json field: ${warning}`);
|
|
54
|
+
}
|
|
48
55
|
const config = {
|
|
49
56
|
...parsedConfig,
|
|
50
57
|
appType: (parsedConfig.appType ||
|
|
@@ -70,7 +77,7 @@ export function discoverResourcesFromRoot(projectRoot) {
|
|
|
70
77
|
export function discoverResources() {
|
|
71
78
|
const projectRoot = findProjectRoot();
|
|
72
79
|
if (!projectRoot) {
|
|
73
|
-
throw new Error("Could not find project root (no
|
|
80
|
+
throw new Error("Could not find project root (no .tdk/project.json found). Run `tdk project --yes` first.");
|
|
74
81
|
}
|
|
75
82
|
return discoverResourcesFromRoot(projectRoot);
|
|
76
83
|
}
|
package/cli/dist/utils/tar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
/**
|
|
@@ -8,14 +8,36 @@ import { join } from "node:path";
|
|
|
8
8
|
* implementation - available by default on macOS and Linux.
|
|
9
9
|
*/
|
|
10
10
|
export function extractTarball(data, destDir) {
|
|
11
|
-
const
|
|
11
|
+
const tempDir = mkdtempSync(join(tmpdir(), "tdk-ext-"));
|
|
12
|
+
const tmpFile = join(tempDir, "bundle.tar.gz");
|
|
12
13
|
writeFileSync(tmpFile, data);
|
|
13
|
-
|
|
14
|
+
mkdirSync(destDir, { recursive: true });
|
|
15
|
+
const tarBin = process.platform === "win32" ? "tar.exe" : "tar";
|
|
16
|
+
const result = spawnSync(tarBin, ["xzf", tmpFile, "-C", destDir, "--strip-components=1"], {
|
|
14
17
|
stdio: "pipe",
|
|
18
|
+
windowsHide: true,
|
|
15
19
|
});
|
|
16
|
-
rmSync(join(tmpFile, ".."), { recursive: true, force: true });
|
|
17
20
|
if (result.status !== 0) {
|
|
18
|
-
|
|
21
|
+
const fallbackDir = join(tempDir, "fallback");
|
|
22
|
+
mkdirSync(fallbackDir, { recursive: true });
|
|
23
|
+
const fallback = spawnSync(tarBin, ["xzf", tmpFile, "-C", fallbackDir], {
|
|
24
|
+
stdio: "pipe",
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
});
|
|
27
|
+
if (fallback.status !== 0) {
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
throw new Error(`tar extraction failed: ${fallback.stderr?.toString() || result.stderr?.toString() || "unknown error"}`);
|
|
30
|
+
}
|
|
31
|
+
const entries = readdirSync(fallbackDir);
|
|
32
|
+
const sourceDir = entries.length === 1 &&
|
|
33
|
+
existsSync(join(fallbackDir, entries[0])) &&
|
|
34
|
+
statSync(join(fallbackDir, entries[0])).isDirectory()
|
|
35
|
+
? join(fallbackDir, entries[0])
|
|
36
|
+
: fallbackDir;
|
|
37
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
38
|
+
renameSync(join(sourceDir, entry), join(destDir, entry));
|
|
39
|
+
}
|
|
19
40
|
}
|
|
41
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
42
|
}
|
|
21
43
|
//# sourceMappingURL=tar.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const run = (command, args) => execFileSync(command, args, { encoding: "utf-8", stdio: "pipe", windowsHide: true });
|
|
3
|
+
/** Refuse to target arbitrary listeners: only stop a Tilt process on the requested UI port. */
|
|
4
|
+
export function findTiltProcessIdsOnPort(port, platform = process.platform, commandRunner = run) {
|
|
5
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
6
|
+
return [];
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
const script = `$port=${port}; Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $p=Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue; if ($p -and $p.ProcessName -eq 'tilt') { $_.OwningProcess } } | Sort-Object -Unique`;
|
|
9
|
+
try {
|
|
10
|
+
return commandRunner("powershell.exe", ["-NoProfile", "-Command", script])
|
|
11
|
+
.split(/\s+/)
|
|
12
|
+
.map(Number)
|
|
13
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`Unable to inspect Tilt listeners on port ${port} with PowerShell.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let pids;
|
|
20
|
+
try {
|
|
21
|
+
pids = commandRunner("lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"])
|
|
22
|
+
.split(/\s+/)
|
|
23
|
+
.map(Number)
|
|
24
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const commandErr = err;
|
|
28
|
+
const stderr = commandErr.stderr?.toString().trim();
|
|
29
|
+
if (commandErr?.status === 1 && !stderr)
|
|
30
|
+
return [];
|
|
31
|
+
throw new Error(`Unable to inspect listeners on port ${port}. Install lsof or resolve the lsof error before using --force.`, { cause: commandErr });
|
|
32
|
+
}
|
|
33
|
+
return pids.filter((pid) => {
|
|
34
|
+
try {
|
|
35
|
+
const name = commandRunner("ps", ["-p", String(pid), "-o", "comm="]).trim();
|
|
36
|
+
return name.split(/[\\/]/).at(-1) === "tilt";
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new Error(`Unable to identify the process listening on port ${port}.`, { cause: err });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function stopTiltOnPort(port, platform = process.platform, commandRunner = run) {
|
|
44
|
+
const pids = findTiltProcessIdsOnPort(port, platform, commandRunner);
|
|
45
|
+
for (const pid of pids) {
|
|
46
|
+
if (platform === "win32")
|
|
47
|
+
commandRunner("taskkill.exe", ["/PID", String(pid), "/F"]);
|
|
48
|
+
else
|
|
49
|
+
commandRunner("kill", ["-TERM", String(pid)]);
|
|
50
|
+
}
|
|
51
|
+
return pids;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=tilt-process.js.map
|
package/cli/dist/utils/tilt.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { findProjectRoot } from "./paths.js";
|
|
4
|
+
import { findOnPath } from "./which.js";
|
|
4
5
|
export function runTilt(command, args = [], options = {}) {
|
|
5
6
|
return new Promise((resolve, reject) => {
|
|
6
7
|
const tiltArgs = [command, ...args];
|
|
7
8
|
if (options.verbose) {
|
|
8
9
|
console.log(`Executing: tilt ${tiltArgs.join(" ")}`);
|
|
9
10
|
}
|
|
10
|
-
const child = spawn("tilt", tiltArgs, {
|
|
11
|
+
const child = spawn(findOnPath("tilt") ?? "tilt", tiltArgs, {
|
|
11
12
|
stdio: options.inheritStdio ? "inherit" : "pipe",
|
|
12
13
|
shell: false,
|
|
14
|
+
windowsHide: process.platform === "win32",
|
|
13
15
|
timeout: options.timeoutMs,
|
|
14
16
|
});
|
|
15
17
|
let stdout = "";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { delimiter, extname, join } from "node:path";
|
|
3
|
+
import { isWindows, pathLookups } from "./platform.js";
|
|
4
|
+
export function findOnPath(command) {
|
|
5
|
+
const pathEnv = process.env.PATH ?? process.env.Path ?? "";
|
|
6
|
+
const windows = isWindows();
|
|
7
|
+
const names = pathLookups(command, windows);
|
|
8
|
+
if (windows && !extname(command)) {
|
|
9
|
+
for (const extension of (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")) {
|
|
10
|
+
const suffix = extension.toLowerCase();
|
|
11
|
+
if (suffix && !names.includes(`${command}${suffix}`))
|
|
12
|
+
names.push(`${command}${suffix}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
16
|
+
if (!dir)
|
|
17
|
+
continue;
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
const candidate = join(dir, name);
|
|
20
|
+
if (existsSync(candidate))
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=which.js.map
|