@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +59 -13
  3. package/cli/bin/tdk.js +22 -2
  4. package/cli/dist/cli.js +9 -2
  5. package/cli/dist/commands/completion.js +30 -3
  6. package/cli/dist/commands/config.js +59 -1
  7. package/cli/dist/commands/doctor.js +295 -98
  8. package/cli/dist/commands/eject.js +77 -0
  9. package/cli/dist/commands/networks.js +17 -4
  10. package/cli/dist/commands/project.js +28 -5
  11. package/cli/dist/commands/resource.js +118 -10
  12. package/cli/dist/commands/up.js +93 -21
  13. package/cli/dist/commands/upgrade.js +163 -68
  14. package/cli/dist/generator/template-engine.js +77 -13
  15. package/cli/dist/types/index.js +6 -1
  16. package/cli/dist/utils/cold-preflight.js +198 -0
  17. package/cli/dist/utils/constants.js +4 -0
  18. package/cli/dist/utils/docker.js +15 -1
  19. package/cli/dist/utils/doctor-runtime.js +48 -16
  20. package/cli/dist/utils/doctor-wiring.js +269 -0
  21. package/cli/dist/utils/errors.js +5 -5
  22. package/cli/dist/utils/exec-async.js +30 -0
  23. package/cli/dist/utils/generated-paths.js +49 -0
  24. package/cli/dist/utils/platform.js +53 -0
  25. package/cli/dist/utils/port-assignment.js +13 -20
  26. package/cli/dist/utils/project-templates.js +7 -2
  27. package/cli/dist/utils/resource-features.js +6 -2
  28. package/cli/dist/utils/service-manifest.js +73 -0
  29. package/cli/dist/utils/services.js +8 -1
  30. package/cli/dist/utils/tar.js +27 -5
  31. package/cli/dist/utils/tilt-process.js +53 -0
  32. package/cli/dist/utils/tilt.js +3 -1
  33. package/cli/dist/utils/which.js +26 -0
  34. package/cli/dist/utils/windows-doctor.js +175 -0
  35. package/cli/package.json +1 -1
  36. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
  37. package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
  38. package/cli/templates/Tiltfile.hbs +24 -4
  39. package/cli/templates/spec.master.hbs +1 -2
  40. package/discovery/AGENTS.md +1 -1
  41. package/discovery/constants.star +2 -2
  42. package/discovery/discovery_orchestrator.star +2 -2
  43. package/discovery/manifest/constants.star +1 -1
  44. package/discovery/manifest/loading.star +7 -3
  45. package/discovery/registry.star +17 -3
  46. package/engine/README.md +2 -0
  47. package/engine/schemas/service-schema.json +47 -21
  48. package/engine/scripts/generate-manifest.sh +2 -1
  49. package/engine/topologies/platform/docker/compose/compose.star +37 -5
  50. package/engine/topologies/tilt/common/utils.star +54 -11
  51. package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
  52. package/engine/topologies/tilt/manifest/constants.star +3 -0
  53. package/engine/topologies/tilt/manifest/schema.star +2 -4
  54. package/engine/topologies/tilt/manifest/validator.star +1 -1
  55. package/engine/topologies/tilt/resources/infra-loader.star +14 -3
  56. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
  57. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
  58. package/examples/tdk-example/LICENSE +21 -0
  59. package/examples/tdk-example/README.md +23 -0
  60. package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
  61. package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
  62. package/examples/tdk-example/Tiltfile +25 -0
  63. package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
  64. package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
  65. package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
  66. package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
  67. package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
  68. package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
  69. package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
  70. package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
  71. package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
  72. package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
  73. package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
  74. package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
  75. package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
  76. package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
  77. package/package.json +2 -1
@@ -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 Tiltfile found)", [
67
- "Run this from within a project that has a Tiltfile",
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 Tiltfile found)."));
75
- console.error(chalk.gray("Run this from within a project that has a Tiltfile."));
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 { spawn } from "node:child_process";
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 child = spawn("lsof", ["-Pi", `:${port}`, "-sTCP:LISTEN"], {
24
- timeout: 3000,
25
- stdio: "pipe",
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
- /** Starter projects `tdk project <name>` can clone. Keep in sync with the public tdk-landscape examples. */
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
- description: "Minimal Project-Stack-Resource (PSR) demonstration",
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/platform/docker/compose/compose.star (deferStart depends_on)",
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 a depends_on block)",
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 Tiltfile found). Make sure you're in a root project.");
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
  }
@@ -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 tmpFile = join(mkdtempSync(join(tmpdir(), "tdk-ext-")), "bundle.tar.gz");
11
+ const tempDir = mkdtempSync(join(tmpdir(), "tdk-ext-"));
12
+ const tmpFile = join(tempDir, "bundle.tar.gz");
12
13
  writeFileSync(tmpFile, data);
13
- const result = spawnSync("tar", ["xzf", tmpFile, "-C", destDir, "--strip-components=1"], {
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
- throw new Error(`tar extraction failed: ${result.stderr?.toString() || "unknown error"}`);
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
@@ -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