@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
@@ -1,18 +1,23 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync, execSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { accessSync, constants, existsSync, readFileSync, realpathSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
3
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
4
+ import { basename, dirname, join, resolve, win32 } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import chalk from "chalk";
7
7
  import { Command } from "commander";
8
8
  import { getErrorMessage, logVerbose, showErrorAndExit } from "../utils/errors.js";
9
9
  import { showCancelled } from "../utils/formatting.js";
10
10
  import { getPackageVersion } from "../utils/paths.js";
11
+ import { binaryAssetName, executableName, isWindows } from "../utils/platform.js";
11
12
  import { promptConfirm } from "../utils/prompt.js";
12
13
  import { startSpinner } from "../utils/spinner.js";
14
+ import { extractTarball } from "../utils/tar.js";
15
+ import { findOnPath } from "../utils/which.js";
13
16
  function isStandaloneBinary(tdkPath) {
17
+ if (isWindows() && tdkPath.toLowerCase().endsWith(".exe"))
18
+ return true;
14
19
  try {
15
- const fileInfo = execSync(`file -b ${JSON.stringify(tdkPath)}`, { encoding: "utf-8" });
20
+ const fileInfo = execFileSync("file", ["-b", tdkPath], { encoding: "utf-8" });
16
21
  return /Mach-O|ELF/.test(fileInfo);
17
22
  }
18
23
  catch (err) {
@@ -20,9 +25,33 @@ function isStandaloneBinary(tdkPath) {
20
25
  return false;
21
26
  }
22
27
  }
28
+ export function classifyRunningInstall(execPath, scriptPath, platformName = process.platform) {
29
+ const runningName = (platformName === "win32" ? win32.basename(execPath) : basename(execPath)).toLowerCase();
30
+ if (runningName === "tdk" || runningName === "tdk.exe") {
31
+ return { method: "binary", path: execPath };
32
+ }
33
+ if (runningName === "node" || runningName === "node.exe") {
34
+ const normalizedScriptPath = scriptPath.toLowerCase();
35
+ if (normalizedScriptPath.includes("node_modules") || normalizedScriptPath.includes(".npm")) {
36
+ return { method: "npm", path: scriptPath };
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ export function windowsNpmUpgradeMessage(platformName, installInfo) {
42
+ return platformName === "win32" && installInfo.method === "npm"
43
+ ? "use npm install -g @tdk-landscape/tdk-cli-core@latest"
44
+ : null;
45
+ }
23
46
  function detectInstallation() {
47
+ const runningPath = process.execPath;
48
+ const runningInstall = classifyRunningInstall(runningPath, process.argv[1] ?? "");
49
+ if (runningInstall)
50
+ return runningInstall;
24
51
  try {
25
- const tdkPath = execSync("which tdk", { encoding: "utf-8" }).trim();
52
+ const tdkPath = findOnPath(executableName());
53
+ if (!tdkPath)
54
+ return { method: "unknown" };
26
55
  // `readlink -f` is not available on macOS. Use Node's cross-platform
27
56
  // realpath implementation so standalone binaries are detected there too.
28
57
  const realPath = realpathSync(tdkPath);
@@ -57,13 +86,6 @@ function detectInstallation() {
57
86
  }
58
87
  }
59
88
  const BINARY_RELEASE_REPO = "tdk-landscape/tdk-cli-releases";
60
- function binaryAssetName() {
61
- const osName = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
62
- const archName = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "amd64" : null;
63
- if (!osName || !archName)
64
- return null;
65
- return `tdk-${osName}-${archName}`;
66
- }
67
89
  const ENGINE_ASSET_NAME = "tdk-cli-engine.tar.gz";
68
90
  const CHECKSUMS_ASSET_NAME = "checksums.txt";
69
91
  /** Parses `shasum -a 256` / `sha256sum` output ("<hash> <name>" or "<hash> *<name>"). */
@@ -92,7 +114,7 @@ function verifyChecksum(filePath, assetName, sums, required) {
92
114
  }
93
115
  const actual = createHash("sha256").update(readFileSync(filePath)).digest("hex");
94
116
  if (actual !== expected) {
95
- throw new Error(`Checksum mismatch for ${assetName} (expected ${expected}, got ${actual})`);
117
+ throw new Error(`${isWindows() ? "checksum mismatch" : "Checksum mismatch"} for ${assetName} (expected ${expected}, got ${actual})`);
96
118
  }
97
119
  }
98
120
  async function getLatestBinaryRelease() {
@@ -100,8 +122,12 @@ async function getLatestBinaryRelease() {
100
122
  if (!assetName)
101
123
  return null;
102
124
  try {
103
- const raw = execSync(`curl -fsSL https://api.github.com/repos/${BINARY_RELEASE_REPO}/releases/latest`, { encoding: "utf-8", timeout: 10000 });
104
- const data = JSON.parse(raw);
125
+ const response = await fetch(`https://api.github.com/repos/${BINARY_RELEASE_REPO}/releases/latest`, {
126
+ signal: AbortSignal.timeout(10_000),
127
+ });
128
+ if (!response.ok)
129
+ return null;
130
+ const data = (await response.json());
105
131
  if (!data.tag_name)
106
132
  return null;
107
133
  return {
@@ -134,22 +160,31 @@ export async function upgradeViaBinary(tdkPath, release) {
134
160
  if (!isWritable(installDir)) {
135
161
  console.log(chalk.yellow(`\n🔒 ${installDir} isn't writable by your user.`));
136
162
  console.log(chalk.yellow("💡 Upgrade manually instead:"));
137
- console.log(chalk.cyan(` curl -fsSL -o ${tdkPath} ${release.downloadUrl}`));
138
- console.log(chalk.cyan(` chmod +x ${tdkPath}`));
163
+ if (isWindows()) {
164
+ console.log(chalk.cyan(" Install TDK in a user-writable directory, then retry the upgrade."));
165
+ }
166
+ else {
167
+ console.log(chalk.cyan(` curl -fsSL -o ${tdkPath} ${release.downloadUrl}`));
168
+ console.log(chalk.cyan(` chmod +x ${tdkPath}`));
169
+ }
139
170
  return false;
140
171
  }
141
172
  const spinner = startSpinner(`Downloading ${release.assetName} (${release.tag})...`);
142
- // Download to /tmp first (both the binary and the engine bundle below) so a
143
- // failed network step can't leave a half-written binary at tdkPath.
144
- const tmpPath = `/tmp/tdk-upgrade-${release.assetName}`;
145
- const engineTmpPath = `/tmp/tdk-upgrade-engine-${release.tag}.tar.gz`;
173
+ const tempDir = join(installDir, `.tdk-upgrade-${process.pid}-${Date.now()}`);
174
+ const tmpPath = join(tempDir, release.assetName);
175
+ const engineTmpPath = join(tempDir, "engine.tar.gz");
146
176
  const engineDir = join(installDir, "tdk-cli");
177
+ const engineStageDir = join(tempDir, "tdk-cli");
178
+ const binaryBackup = join(tempDir, `${basename(tdkPath)}.previous`);
179
+ const engineBackup = join(tempDir, "tdk-cli.previous");
147
180
  try {
148
- execSync(`curl -fsSL -o ${JSON.stringify(tmpPath)} ${JSON.stringify(release.downloadUrl)}`, {
149
- stdio: "pipe",
150
- timeout: 120000,
181
+ mkdirSync(tempDir, { recursive: true });
182
+ const binaryResponse = await fetch(release.downloadUrl, {
183
+ signal: AbortSignal.timeout(120_000),
151
184
  });
152
- execSync(`chmod +x ${JSON.stringify(tmpPath)}`);
185
+ if (!binaryResponse.ok)
186
+ throw new Error(`Download failed (${binaryResponse.status})`);
187
+ writeFileSync(tmpPath, Buffer.from(await binaryResponse.arrayBuffer()));
153
188
  // The compiled binary has no source checkout to find engine/ or
154
189
  // cli/templates/ in, so it looks for a tdk-cli/ folder next to itself
155
190
  // (see template-engine.ts loadTemplate() / vendorTdkExtension()).
@@ -157,41 +192,63 @@ export async function upgradeViaBinary(tdkPath, release) {
157
192
  // stale/missing after an upgrade - refresh it here too, every time,
158
193
  // the same way install.sh does on a fresh install.
159
194
  spinner.text = `Downloading bundled engine (${release.tag})...`;
160
- execSync(`curl -fsSL -o ${JSON.stringify(engineTmpPath)} ${JSON.stringify(release.engineDownloadUrl)}`, { stdio: "pipe", timeout: 120000 });
195
+ const engineResponse = await fetch(release.engineDownloadUrl, {
196
+ signal: AbortSignal.timeout(120_000),
197
+ });
198
+ if (!engineResponse.ok)
199
+ throw new Error(`Engine download failed (${engineResponse.status})`);
200
+ const engineBuffer = Buffer.from(await engineResponse.arrayBuffer());
201
+ writeFileSync(engineTmpPath, engineBuffer);
161
202
  // Verify both downloads against the release's checksums.txt before
162
203
  // touching the installed binary or engine.
163
204
  spinner.text = `Verifying checksums (${release.tag})...`;
164
- const sums = parseChecksums(execSync(`curl -fsSL ${JSON.stringify(release.checksumsUrl)}`, {
165
- encoding: "utf-8",
166
- stdio: "pipe",
167
- timeout: 30000,
168
- }));
205
+ const checksumResponse = await fetch(release.checksumsUrl, {
206
+ signal: AbortSignal.timeout(30_000),
207
+ });
208
+ if (!checksumResponse.ok)
209
+ throw new Error(`Checksum download failed (${checksumResponse.status})`);
210
+ const sums = parseChecksums(await checksumResponse.text());
169
211
  verifyChecksum(tmpPath, release.assetName, sums, true);
170
212
  verifyChecksum(engineTmpPath, ENGINE_ASSET_NAME, sums, false);
171
- execSync(`mv ${JSON.stringify(tmpPath)} ${JSON.stringify(tdkPath)}`);
172
- execSync(`rm -rf ${JSON.stringify(engineDir)}`);
173
- execSync(`mkdir -p ${JSON.stringify(engineDir)}`);
174
- execSync(`tar -xzf ${JSON.stringify(engineTmpPath)} -C ${JSON.stringify(engineDir)} --strip-components=1`);
213
+ mkdirSync(engineStageDir, { recursive: true });
214
+ extractTarball(engineBuffer, engineStageDir);
215
+ if (existsSync(tdkPath))
216
+ renameSync(tdkPath, binaryBackup);
217
+ try {
218
+ renameSync(tmpPath, tdkPath);
219
+ }
220
+ catch (err) {
221
+ if (existsSync(binaryBackup))
222
+ renameSync(binaryBackup, tdkPath);
223
+ throw err;
224
+ }
225
+ if (existsSync(engineDir))
226
+ renameSync(engineDir, engineBackup);
227
+ try {
228
+ renameSync(engineStageDir, engineDir);
229
+ }
230
+ catch (err) {
231
+ if (existsSync(engineBackup))
232
+ renameSync(engineBackup, engineDir);
233
+ if (existsSync(binaryBackup)) {
234
+ rmSync(tdkPath, { force: true });
235
+ renameSync(binaryBackup, tdkPath);
236
+ }
237
+ throw err;
238
+ }
239
+ rmSync(binaryBackup, { force: true });
240
+ rmSync(engineBackup, { recursive: true, force: true });
175
241
  spinner.succeed(`Upgraded to ${release.tag}`);
176
242
  return true;
177
243
  }
178
244
  catch (err) {
179
245
  spinner.fail(`Binary upgrade failed: ${getErrorMessage(err)}`);
180
246
  logVerbose("Binary upgrade error", err);
181
- console.log(chalk.yellow("\n💡 If this failed due to permissions, upgrade manually instead:"));
182
- console.log(chalk.cyan(` curl -fsSL -o ${tdkPath} ${release.downloadUrl}`));
183
- console.log(chalk.cyan(` chmod +x ${tdkPath}`));
184
- console.log(chalk.cyan(` rm -rf ${engineDir} && mkdir -p ${engineDir}`));
185
- console.log(chalk.cyan(` curl -fsSL ${release.engineDownloadUrl} | tar -xzf - -C ${engineDir} --strip-components=1`));
247
+ console.log(chalk.yellow("\n💡 If this failed due to permissions, use the platform installer or install command."));
186
248
  return false;
187
249
  }
188
250
  finally {
189
- try {
190
- execSync(`rm -f ${JSON.stringify(tmpPath)} ${JSON.stringify(engineTmpPath)}`);
191
- }
192
- catch {
193
- // best-effort cleanup
194
- }
251
+ rmSync(tempDir, { recursive: true, force: true });
195
252
  }
196
253
  }
197
254
  function getCurrentVersion() {
@@ -206,10 +263,24 @@ function getCurrentVersion() {
206
263
  async function getLatestVersion() {
207
264
  const spinner = startSpinner("Checking for latest version...");
208
265
  try {
209
- const result = execSync("npm view @tdk-landscape/tdk-cli-core version", {
210
- encoding: "utf-8",
211
- timeout: 10000,
212
- }).trim();
266
+ let result;
267
+ if (isWindows()) {
268
+ const response = await fetch("https://registry.npmjs.org/@tdk-landscape%2ftdk-cli-core/latest", {
269
+ signal: AbortSignal.timeout(10_000),
270
+ });
271
+ if (!response.ok)
272
+ throw new Error(`npm registry returned ${response.status}`);
273
+ const latest = (await response.json());
274
+ result = latest.version ?? "";
275
+ if (!result)
276
+ throw new Error("npm registry response did not include a version");
277
+ }
278
+ else {
279
+ result = execSync("npm view @tdk-landscape/tdk-cli-core version", {
280
+ encoding: "utf-8",
281
+ timeout: 10000,
282
+ }).trim();
283
+ }
213
284
  spinner.succeed(`Latest version: ${chalk.green(result)}`);
214
285
  return result;
215
286
  }
@@ -254,7 +325,7 @@ async function upgradeViaNpm() {
254
325
  async function upgradeViaBun() {
255
326
  const spinner = startSpinner("Upgrading via bun...");
256
327
  try {
257
- execSync("bun install -g @tdk-landscape/tdk-cli-core@latest", {
328
+ execFileSync(findOnPath("bun") ?? "bun", ["install", "-g", "@tdk-landscape/tdk-cli-core@latest"], {
258
329
  stdio: "inherit",
259
330
  timeout: 120000,
260
331
  });
@@ -266,7 +337,7 @@ async function upgradeViaBun() {
266
337
  spinner.text = "bun registry failed, trying GitHub...";
267
338
  logVerbose("bun registry error", err);
268
339
  try {
269
- execSync("bun install -g github:tdk-landscape/tdk-cli-core", {
340
+ execFileSync(findOnPath("bun") ?? "bun", ["install", "-g", "github:tdk-landscape/tdk-cli-core"], {
270
341
  stdio: "inherit",
271
342
  timeout: 120000,
272
343
  });
@@ -282,36 +353,41 @@ async function upgradeViaBun() {
282
353
  async function upgradeViaGit(path) {
283
354
  const spinner = startSpinner("Pulling latest changes from git...");
284
355
  try {
285
- execSync("git rev-parse --git-dir", {
356
+ execFileSync("git", ["rev-parse", "--git-dir"], {
286
357
  cwd: path,
287
358
  stdio: "pipe",
288
359
  });
289
360
  spinner.text = "Fetching from origin...";
290
- execSync("git fetch origin", {
361
+ execFileSync("git", ["fetch", "origin"], {
291
362
  cwd: path,
292
363
  stdio: "pipe",
293
364
  timeout: 30000,
294
365
  });
295
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
366
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
296
367
  cwd: path,
297
368
  encoding: "utf-8",
298
369
  }).trim();
299
370
  spinner.text = `Pulling latest on ${branch}...`;
300
- execSync(`git pull origin ${branch}`, {
371
+ execFileSync("git", ["pull", "origin", branch], {
301
372
  cwd: path,
302
373
  stdio: "pipe",
303
374
  timeout: 30000,
304
375
  });
305
376
  if (existsSync(join(path, "cli", "package.json"))) {
306
377
  spinner.text = "Rebuilding CLI...";
307
- execSync("bun install && bun run build", {
378
+ execFileSync(findOnPath("bun") ?? "bun", ["install"], {
379
+ cwd: join(path, "cli"),
380
+ stdio: "pipe",
381
+ timeout: 60000,
382
+ });
383
+ execFileSync(findOnPath("bun") ?? "bun", ["run", "build"], {
308
384
  cwd: join(path, "cli"),
309
385
  stdio: "pipe",
310
386
  timeout: 60000,
311
387
  });
312
388
  }
313
389
  spinner.text = "Re-linking CLI...";
314
- execSync("bun link --force", {
390
+ execFileSync(findOnPath("bun") ?? "bun", ["link", "--force"], {
315
391
  cwd: join(path, "cli"),
316
392
  stdio: "pipe",
317
393
  timeout: 30000,
@@ -337,6 +413,11 @@ export const upgradeCommand = new Command("upgrade")
337
413
  const installInfo = detectInstallation();
338
414
  console.log(chalk.gray(`Installation method: ${installInfo.method}`));
339
415
  console.log();
416
+ const windowsNpmMessage = windowsNpmUpgradeMessage(process.platform, installInfo);
417
+ if (windowsNpmMessage) {
418
+ console.log(windowsNpmMessage);
419
+ return;
420
+ }
340
421
  if (installInfo.method === "unknown") {
341
422
  console.error(chalk.red("❌ Could not detect installation method"));
342
423
  console.log(chalk.yellow("\n💡 Manual upgrade (package not on npm yet, use GitHub):"));
@@ -351,7 +432,13 @@ export const upgradeCommand = new Command("upgrade")
351
432
  console.log(chalk.blue("📦 Standalone binary installation detected"));
352
433
  binaryRelease = await getLatestBinaryRelease();
353
434
  if (!binaryRelease) {
354
- showErrorAndExit(`Could not determine latest release for this platform (${process.platform}/${process.arch})`);
435
+ if (process.platform === "win32" && process.arch === "arm64") {
436
+ showErrorAndExit("TDK Windows v1 supports AMD64 only. Use WSL2 Ubuntu or a 64-bit Intel/AMD PC.");
437
+ }
438
+ if (process.platform === "win32") {
439
+ showErrorAndExit("No Windows AMD64 binary in this TDK release. Need asset tdk-windows-amd64.exe.");
440
+ }
441
+ showErrorAndExit(`Unsupported OS: TDK does not support this OS: ${process.platform}. Supported: linux, darwin, win32.`);
355
442
  }
356
443
  latestVersion = binaryRelease.tag.replace(/^v/, "").split("-")[0];
357
444
  if (latestVersion === currentVersion && !options.force) {
@@ -369,12 +456,12 @@ export const upgradeCommand = new Command("upgrade")
369
456
  else if (installInfo.method === "git" && installInfo.path) {
370
457
  console.log(chalk.blue("📦 Git installation detected - will pull latest from origin"));
371
458
  try {
372
- execSync("git fetch origin", { cwd: installInfo.path, stdio: "pipe" });
373
- const localHash = execSync("git rev-parse HEAD", {
459
+ execFileSync("git", ["fetch", "origin"], { cwd: installInfo.path, stdio: "pipe" });
460
+ const localHash = execFileSync("git", ["rev-parse", "HEAD"], {
374
461
  cwd: installInfo.path,
375
462
  encoding: "utf-8",
376
463
  }).trim();
377
- const remoteHash = execSync("git rev-parse origin/main", {
464
+ const remoteHash = execFileSync("git", ["rev-parse", "origin/main"], {
378
465
  cwd: installInfo.path,
379
466
  encoding: "utf-8",
380
467
  }).trim();
@@ -484,17 +571,25 @@ export const upgradeCommand = new Command("upgrade")
484
571
  }
485
572
  else if (installInfo.method === "binary" && binaryRelease && installInfo.path) {
486
573
  const engineDir = join(dirname(installInfo.path), "tdk-cli");
487
- console.log(chalk.cyan(` curl -fsSL -o ${installInfo.path} ${binaryRelease.downloadUrl}`));
488
- console.log(chalk.cyan(` chmod +x ${installInfo.path}`));
489
- console.log(chalk.cyan(` rm -rf ${engineDir} && mkdir -p ${engineDir}`));
490
- console.log(chalk.cyan(` curl -fsSL ${binaryRelease.engineDownloadUrl} | tar -xzf - -C ${engineDir} --strip-components=1`));
574
+ if (isWindows()) {
575
+ console.log(chalk.cyan(" irm https://tdk-landscape.github.io/install.ps1 | iex"));
576
+ }
577
+ else {
578
+ console.log(chalk.cyan(` curl -fsSL -o ${installInfo.path} ${binaryRelease.downloadUrl}`));
579
+ console.log(chalk.cyan(` chmod +x ${installInfo.path}`));
580
+ console.log(chalk.cyan(` rm -rf ${engineDir} && mkdir -p ${engineDir}`));
581
+ console.log(chalk.cyan(` curl -fsSL ${binaryRelease.engineDownloadUrl} | tar -xzf - -C ${engineDir} --strip-components=1`));
582
+ }
491
583
  }
492
584
  process.exit(1);
493
585
  }
494
586
  console.log();
495
587
  const verifySpinner = startSpinner("Verifying upgrade...");
496
588
  try {
497
- const newVersion = execSync("tdk version", { encoding: "utf-8" }).trim();
589
+ const newVersion = execFileSync(findOnPath(executableName()) ?? executableName(), ["version"], {
590
+ encoding: "utf-8",
591
+ windowsHide: isWindows(),
592
+ }).trim();
498
593
  verifySpinner.succeed(`Verified: now running ${chalk.green(newVersion)}`);
499
594
  console.log();
500
595
  console.log(chalk.green.bold("✨ Upgrade complete!"));
@@ -508,7 +603,7 @@ export const upgradeCommand = new Command("upgrade")
508
603
  else {
509
604
  console.log(chalk.gray(` Method: ${installInfo.method}`));
510
605
  }
511
- console.log(chalk.gray(` Binary: ${execSync("which tdk", { encoding: "utf-8" }).trim()}`));
606
+ console.log(chalk.gray(` Binary: ${installInfo.path ?? findOnPath(executableName()) ?? "unknown"}`));
512
607
  console.log();
513
608
  console.log(chalk.cyan.bold("🚀 Quick Start:"));
514
609
  console.log(chalk.white(` tdk --help Show all commands`));
@@ -4,7 +4,9 @@ import { fileURLToPath } from "node:url";
4
4
  import Handlebars from "handlebars";
5
5
  import { PLATFORM_STANDARDS } from "../config/platform-standards.js";
6
6
  import { writeTextFile } from "../utils/file-helpers.js";
7
- import { discoverResourcesFromRoot } from "../utils/services.js";
7
+ import { assertTdkGeneratedPath } from "../utils/generated-paths.js";
8
+ import { validateServiceManifestFile } from "../utils/service-manifest.js";
9
+ import { discoverResourcesFromRoot, discoverServiceManifestPaths } from "../utils/services.js";
8
10
  import { isStackFeatureEnabledInStacks } from "../utils/stack-features.js";
9
11
  import { hasSablierLicense, hasVerdaccioLicense } from "./extension-fetch.js";
10
12
  const __filename = fileURLToPath(import.meta.url);
@@ -129,7 +131,6 @@ export class TemplateEngine {
129
131
  buildContext(projectConfig) {
130
132
  return {
131
133
  version: PLATFORM_STANDARDS.version,
132
- timestamp: new Date().toISOString(),
133
134
  tech: PLATFORM_STANDARDS.tech,
134
135
  ports: PLATFORM_STANDARDS.ports,
135
136
  health: PLATFORM_STANDARDS.health,
@@ -351,7 +352,7 @@ function normalizeProjectConfig(value) {
351
352
  export function readProjectConfig(projectRoot) {
352
353
  const projectJsonPath = path.join(projectRoot, ".tdk", "project.json");
353
354
  if (!fs.existsSync(projectJsonPath)) {
354
- throw new Error(`Project config not found: ${projectJsonPath}. Run 'tdk project init' first.`);
355
+ throw new Error(`Project config not found: ${projectJsonPath}. Run 'tdk project --yes' first.`);
355
356
  }
356
357
  const jsonContent = fs.readFileSync(projectJsonPath, "utf-8");
357
358
  const parsed = JSON.parse(jsonContent);
@@ -389,7 +390,7 @@ async function warnIfSablierUnlicensed(projectRoot) {
389
390
  "or remove the `sablier` block from the affected service.json file(s) to silence this warning.");
390
391
  }
391
392
  function ensureRootWorkspaceManifest(projectRoot, projectConfig) {
392
- const packageJsonPath = path.join(projectRoot, "package.json");
393
+ const packageJsonPath = assertTdkGeneratedPath(projectRoot, "package.json");
393
394
  if (fs.existsSync(packageJsonPath)) {
394
395
  return false;
395
396
  }
@@ -437,21 +438,21 @@ export async function generateMasterConfigs(projectRoot) {
437
438
  const files = engine.generateAll(projectConfig);
438
439
  for (const filename of ALL_GENERATED_FILES) {
439
440
  const content = files[filename];
440
- const filePath = path.join(outputDir, filename);
441
+ const filePath = assertTdkGeneratedPath(projectRoot, path.join(".tdk", ".tdk-out", filename));
441
442
  writeTextFile(filePath, content);
442
443
  console.log(`✓ Generated: .tdk/.tdk-out/${filename}`);
443
444
  }
444
445
  if (isStackFeatureEnabledInStacks(projectConfig.phases, "database-management")) {
445
- const composeDir = path.join(projectRoot, "services", "platform", "database-management");
446
+ const composePath = assertTdkGeneratedPath(projectRoot, path.join("services", "platform", "database-management", "docker-compose.yml"));
447
+ const composeDir = path.dirname(composePath);
446
448
  fs.mkdirSync(composeDir, { recursive: true });
447
- const composePath = path.join(composeDir, "docker-compose.yml");
448
449
  writeTextFile(composePath, generateDatabaseManagementCompose(projectConfig));
449
450
  console.log("✓ Generated: services/platform/database-management/docker-compose.yml");
450
451
  }
451
452
  // Copy .tiltignore to project root so Tilt uses it
452
453
  const tiltignoreSource = path.join(outputDir, ".tiltignore");
453
- const tiltignoreTarget = path.join(projectRoot, ".tiltignore");
454
- if (fs.existsSync(tiltignoreSource)) {
454
+ const tiltignoreTarget = assertTdkGeneratedPath(projectRoot, ".tiltignore");
455
+ if (fs.existsSync(tiltignoreSource) && !fs.existsSync(tiltignoreTarget)) {
455
456
  fs.copyFileSync(tiltignoreSource, tiltignoreTarget);
456
457
  console.log(`✓ Copied: .tiltignore → project root`);
457
458
  }
@@ -521,7 +522,7 @@ export function ensureProjectRuntimeAssets(projectRoot) {
521
522
  }
522
523
  for (const parts of PROJECT_RUNTIME_ASSET_DIRS) {
523
524
  const source = path.join(sourceRoot, ...parts);
524
- const target = path.join(projectRoot, ...parts);
525
+ const target = assertTdkGeneratedPath(projectRoot, path.join(...parts));
525
526
  if (!fs.existsSync(source)) {
526
527
  continue;
527
528
  }
@@ -538,8 +539,7 @@ export function ensureProjectRuntimeAssets(projectRoot) {
538
539
  return copied;
539
540
  }
540
541
  async function vendorTdkExtension(projectRoot) {
541
- const outputDir = path.join(projectRoot, ".tdk", ".tdk-out");
542
- const vendoredDir = path.join(outputDir, "tdk-cli-ext");
542
+ const vendoredDir = assertTdkGeneratedPath(projectRoot, path.join(".tdk", ".tdk-out", "tdk-cli-ext"));
543
543
  // Source resolution order:
544
544
  // 1. $TDK_EXTENSION_SOURCE (explicit override)
545
545
  // 2. Self-contained repo (engine/ ships next to cli/ - public tdk-cli-core)
@@ -602,6 +602,7 @@ async function vendorTdkExtension(projectRoot) {
602
602
  }
603
603
  export function verifyMasterConfigs(projectRoot) {
604
604
  const errors = [];
605
+ const warnings = [];
605
606
  const projectConfig = readProjectConfig(projectRoot);
606
607
  const engine = new TemplateEngine();
607
608
  const expectedFiles = engine.generateAll(projectConfig);
@@ -618,6 +619,69 @@ export function verifyMasterConfigs(projectRoot) {
618
619
  errors.push(`Out of sync: .tdk/.tdk-out/${filename} (run 'tdk config regenerate')`);
619
620
  }
620
621
  }
621
- return { valid: errors.length === 0, errors };
622
+ const rootTiltIgnore = path.join(projectRoot, ".tiltignore");
623
+ if (!fs.existsSync(rootTiltIgnore)) {
624
+ errors.push("Missing file: .tiltignore");
625
+ }
626
+ else if (fs.readFileSync(rootTiltIgnore, "utf-8") !== expectedFiles[".tiltignore"]) {
627
+ errors.push("Out of sync: .tiltignore (run 'tdk config regenerate')");
628
+ }
629
+ for (const manifestPath of discoverServiceManifestPaths(projectRoot)) {
630
+ const displayPath = path.relative(projectRoot, manifestPath).split(path.sep).join("/");
631
+ const validation = validateServiceManifestFile(manifestPath, displayPath);
632
+ errors.push(...validation.errors);
633
+ warnings.push(...validation.warnings);
634
+ errors.push(...verifyGeneratedResourceFiles(projectRoot, manifestPath));
635
+ }
636
+ return { valid: errors.length === 0, errors, warnings };
637
+ }
638
+ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
639
+ const serviceDir = path.dirname(serviceJsonPath);
640
+ const generatedDir = path.join(serviceDir, ".autogenerated");
641
+ if (!fs.existsSync(generatedDir))
642
+ return [];
643
+ const generatedFiles = [];
644
+ const collect = (directory, relativeDir = "") => {
645
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
646
+ if (relativeDir === "" && entry.name === ".tdk-contract")
647
+ continue;
648
+ const relativeFile = path.join(relativeDir, entry.name);
649
+ const fullPath = path.join(directory, entry.name);
650
+ if (entry.isDirectory())
651
+ collect(fullPath, relativeFile);
652
+ else if (entry.isFile())
653
+ generatedFiles.push({ fullPath, relativeFile });
654
+ }
655
+ };
656
+ collect(generatedDir);
657
+ if (generatedFiles.length === 0)
658
+ return [];
659
+ const contractDir = path.join(generatedDir, ".tdk-contract");
660
+ const sourceSnapshot = path.join(contractDir, "service.json.source");
661
+ const errors = [];
662
+ if (!fs.existsSync(sourceSnapshot)) {
663
+ errors.push(path.relative(projectRoot, sourceSnapshot) +
664
+ ": generated-file contract snapshot is missing (run tdk up to regenerate)");
665
+ }
666
+ else if (fs.readFileSync(sourceSnapshot, "utf-8") !== fs.readFileSync(serviceJsonPath, "utf-8")) {
667
+ errors.push(path.relative(projectRoot, serviceJsonPath) +
668
+ ": service.json changed since generated outputs were written (run tdk up)");
669
+ }
670
+ for (const generated of generatedFiles) {
671
+ // service.yaml is a runtime discovery shadow generated by the Tilt
672
+ // extension. It is not produced by `tdk config regenerate`, and its
673
+ // timestamp/UID content has no committed TDK snapshot to verify.
674
+ if (generated.relativeFile === "service.yaml")
675
+ continue;
676
+ const snapshotPath = path.join(contractDir, generated.relativeFile);
677
+ const display = path.relative(projectRoot, generated.fullPath);
678
+ if (!fs.existsSync(snapshotPath)) {
679
+ errors.push(`${display}: generated-file snapshot is missing (run tdk up to regenerate)`);
680
+ }
681
+ else if (fs.readFileSync(generated.fullPath, "utf-8") !== fs.readFileSync(snapshotPath, "utf-8")) {
682
+ errors.push(`${display}: generated file differs from its TDK snapshot (run tdk up to regenerate)`);
683
+ }
684
+ }
685
+ return errors;
622
686
  }
623
687
  //# sourceMappingURL=template-engine.js.map
@@ -1,4 +1,9 @@
1
- export const CREATABLE_RESOURCE_TYPES = ["backend", "frontend", "worker"];
1
+ export const CREATABLE_RESOURCE_TYPES = [
2
+ "backend",
3
+ "frontend",
4
+ "worker",
5
+ "bring-your-own",
6
+ ];
2
7
  export function isCreatableResourceType(value) {
3
8
  return (typeof value === "string" && CREATABLE_RESOURCE_TYPES.includes(value));
4
9
  }