@tdk-landscape/tdk-cli-core 1.3.154 → 1.3.156

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.
@@ -122,6 +122,7 @@ _tdk() {
122
122
  upgrade)
123
123
  _arguments
124
124
  '--force[Force upgrade even if on latest]'
125
+ '--all[Upgrade every TDK installation found on PATH]'
125
126
  '--dry-run[Show what would be upgraded]'
126
127
  ;;
127
128
  esac
@@ -242,6 +243,7 @@ complete -c tdk -n '__fish_seen_subcommand_from runtime' -l json -d 'Print the c
242
243
 
243
244
  # upgrade command options
244
245
  complete -c tdk -n '__fish_seen_subcommand_from upgrade' -l force -d 'Force upgrade even if on latest'
246
+ complete -c tdk -n '__fish_seen_subcommand_from upgrade' -l all -d 'Upgrade every TDK installation found on PATH'
245
247
  complete -c tdk -n '__fish_seen_subcommand_from upgrade' -l dry-run -d 'Show what would be upgraded'
246
248
  `;
247
249
  const POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName tdk -ScriptBlock {
@@ -6,24 +6,29 @@ import { join, normalize, relative } from "node:path";
6
6
  import chalk from "chalk";
7
7
  import { Command } from "commander";
8
8
  import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
9
+ import { readProjectConfig } from "../generator/template-engine.js";
9
10
  import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_DOCS, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
10
11
  import { projectNeedsBun } from "../utils/bun-requirement.js";
11
12
  import { AUTOGENERATED_DIR, CONTAINER_RUNTIME_CHECK, MASTER_CONFIG_FILES, PACKAGE_JSON, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
12
13
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
14
+ import { checkCrashLoopingContainers } from "../utils/doctor-crashloop.js";
15
+ import { applyDoctorFixes, checkDuplicateTdkInstalls, hasFailedActions, planDoctorFixes, } from "../utils/doctor-fix.js";
13
16
  import { checkDiskSpace, dockerDesktopStuck } from "../utils/doctor-host.js";
14
17
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
15
18
  import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeAdvertisedProbes, summarizeServiceProbes, summarizeSmokeResults, } from "../utils/doctor-runtime.js";
16
19
  import { checkStarlarkLoadExports } from "../utils/doctor-starlark.js";
17
20
  import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkPrismaConsistency, checkResourcePackageJson, checkServiceUrlPorts, checkSharedPlatformPostgres, checkSharedStackRoutes, checkTiltInstances, } from "../utils/doctor-wiring.js";
21
+ import { findProjectTiltUpPids } from "../utils/down-cleanup.js";
18
22
  import { validateEnvFile } from "../utils/env-validator.js";
19
23
  import { execAsync, isExecTimeout } from "../utils/exec-async.js";
20
24
  import { formatCount } from "../utils/formatting.js";
21
25
  import { getHostPortPlan, resolveRuntimeIngressPort } from "../utils/host-port-config.js";
22
26
  import { createHostPortPlan } from "../utils/host-port-plan.js";
23
27
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
28
+ import { promptConfirm } from "../utils/prompt.js";
24
29
  import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
25
30
  import { checkCircularDependencies, checkDependsOnShape, checkSchemaVersions, checkServicePorts, } from "../utils/service-config-checks.js";
26
- import { buildAdvertisedTargets, buildHealthTargets, pingHealthTargets, } from "../utils/service-urls.js";
31
+ import { buildAdvertisedTargets, buildHealthTargets, getProjectName, pingHealthTargets, } from "../utils/service-urls.js";
27
32
  import { discoverResourcesFromRoot } from "../utils/services.js";
28
33
  import { buildSmokePlans, readOnlySmokePlans, runSmokePlans } from "../utils/smoke.js";
29
34
  import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
@@ -978,6 +983,26 @@ async function checkAdvertisedEndpoints(timeoutMs, ingressPort) {
978
983
  }
979
984
  return summarizeAdvertisedProbes(await pingHealthTargets(targets, timeoutMs));
980
985
  }
986
+ function printFixActions(actions, dryRun) {
987
+ if (actions.length === 0) {
988
+ console.log(`${chalk.green("✓")} Nothing to repair\n`);
989
+ return;
990
+ }
991
+ for (const action of actions) {
992
+ const label = action.status === "planned"
993
+ ? `${chalk.cyan("would run")}`
994
+ : action.status === "applied"
995
+ ? chalk.green("ran")
996
+ : action.status === "failed"
997
+ ? chalk.red("failed")
998
+ : chalk.gray("skipped");
999
+ const detail = action.status === "planned" ? "" : ` (${action.reason})`;
1000
+ console.log(`${label}: ${action.command}${detail}`);
1001
+ }
1002
+ if (dryRun)
1003
+ console.log(chalk.gray("\nDry run: nothing was changed."));
1004
+ console.log("");
1005
+ }
981
1006
  export const doctorCommand = new Command("doctor")
982
1007
  .description("Check environment readiness for TDK")
983
1008
  .option("--json", "Output a versioned JSON readiness report", false)
@@ -991,6 +1016,9 @@ export const doctorCommand = new Command("doctor")
991
1016
  })
992
1017
  .option("--no-ping", "Skip pinging running services' /health endpoints")
993
1018
  .option("--strict", "Fail warnings such as a WSL project under /mnt/c")
1019
+ .option("--fix", "Repair this project's unused Docker networks and leftover doctor probes", false)
1020
+ .option("--dry-run", "With --fix: list the repairs without changing anything", false)
1021
+ .option("-y, --yes", "With --fix: apply every repair without asking", false)
994
1022
  .option("--ping-timeout <ms>", "Per-service ping timeout in milliseconds", String(DEFAULT_PING_TIMEOUT_MS))
995
1023
  .action(async (options) => {
996
1024
  // Validate before running any probe; parseInt would silently accept "100ms" or "1.5".
@@ -1005,6 +1033,14 @@ export const doctorCommand = new Command("doctor")
1005
1033
  process.exit(2);
1006
1034
  return;
1007
1035
  }
1036
+ if ((options.dryRun || options.yes) && !options.fix) {
1037
+ const message = `${options.dryRun ? "--dry-run" : "--yes"} only applies with --fix`;
1038
+ if (options.json)
1039
+ console.log(JSON.stringify(createDoctorReport([], false, [{ code: "USAGE", message }])));
1040
+ console.error(message);
1041
+ process.exit(2);
1042
+ return;
1043
+ }
1008
1044
  if (!options.json) {
1009
1045
  console.log("TDK doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1010
1046
  }
@@ -1051,6 +1087,51 @@ export const doctorCommand = new Command("doctor")
1051
1087
  console.log("Checking environment...\n");
1052
1088
  }
1053
1089
  const projectRoot = findProjectRoot();
1090
+ // Repairs run before the checks so the checks report the state after the fix.
1091
+ let fixActions;
1092
+ if (options.fix) {
1093
+ try {
1094
+ let projectName;
1095
+ if (projectRoot) {
1096
+ try {
1097
+ projectName = readProjectConfig(projectRoot).project.name;
1098
+ }
1099
+ catch {
1100
+ // An unreadable config is reported by the project checks below.
1101
+ }
1102
+ }
1103
+ const tiltPids = projectRoot
1104
+ ? findProjectTiltUpPids(join(projectRoot, ".tdk", ".tdk-out", "Tiltfile"))
1105
+ : [];
1106
+ const planned = await planDoctorFixes(projectName, undefined, tiltPids.length > 0
1107
+ ? `Tilt is running for this project (pid ${tiltPids.join(", ")}); run tdk down first`
1108
+ : undefined);
1109
+ fixActions = await applyDoctorFixes(planned, {
1110
+ dryRun: options.dryRun,
1111
+ confirm: options.yes || (!options.json && process.stdin.isTTY)
1112
+ ? async (action) => options.yes ||
1113
+ (await promptConfirm({
1114
+ message: `Run \`${action.command}\` (${action.check}, ${action.reason})?`,
1115
+ }))
1116
+ : undefined,
1117
+ });
1118
+ }
1119
+ catch (error) {
1120
+ const reason = error instanceof Error ? error.message : String(error);
1121
+ fixActions = [
1122
+ {
1123
+ id: "plan-fixes",
1124
+ check: "Docker networks",
1125
+ scope: "project",
1126
+ command: "docker network ls",
1127
+ status: "failed",
1128
+ reason: `could not list Docker networks: ${reason.split(/\r?\n/)[0]}`,
1129
+ },
1130
+ ];
1131
+ }
1132
+ if (!options.json)
1133
+ printFixActions(fixActions, options.dryRun);
1134
+ }
1054
1135
  let hostPortPlan;
1055
1136
  let portPlanError;
1056
1137
  try {
@@ -1094,6 +1175,7 @@ export const doctorCommand = new Command("doctor")
1094
1175
  () => checkPublishedBindAddress(process.env, options.strict),
1095
1176
  // Each project needs several networks; a full address pool fails `tdk up` late.
1096
1177
  () => checkDockerNetworkCapacity(),
1178
+ () => checkDuplicateTdkInstalls(),
1097
1179
  ];
1098
1180
  const projectChecks = [
1099
1181
  () => checkTdkVersion(),
@@ -1134,6 +1216,24 @@ export const doctorCommand = new Command("doctor")
1134
1216
  },
1135
1217
  // Runtime checks: skip gracefully if the stack isn't started yet.
1136
1218
  checkContainerResourceHealth,
1219
+ // A container restarted thousands of times is invisible in `docker ps` until someone looks.
1220
+ () => {
1221
+ const root = findProjectRoot() ?? process.cwd();
1222
+ let secrets = [];
1223
+ try {
1224
+ secrets = envSecretValues(root);
1225
+ }
1226
+ catch {
1227
+ // Without the secret values, log lines cannot be shown safely.
1228
+ return {
1229
+ name: "Crash-looping containers",
1230
+ didPass: true,
1231
+ isSkipped: true,
1232
+ message: "Could not read .env - skipped crash-loop check",
1233
+ };
1234
+ }
1235
+ return checkCrashLoopingContainers(getProjectName(), secrets);
1236
+ },
1137
1237
  // When Tilt is up, surface red resources (Traefik/apps never started).
1138
1238
  checkTiltResourceHealth,
1139
1239
  ];
@@ -1160,6 +1260,11 @@ export const doctorCommand = new Command("doctor")
1160
1260
  }
1161
1261
  }
1162
1262
  const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan, host);
1263
+ if (fixActions)
1264
+ report.data.actions = fixActions;
1265
+ const fixFailed = fixActions ? hasFailedActions(fixActions) : false;
1266
+ if (fixFailed)
1267
+ report.data.ready = false;
1163
1268
  const exitCode = getDoctorExitCode(report);
1164
1269
  const allPassed = report.data.ready;
1165
1270
  if (options.json) {
@@ -83,7 +83,7 @@ const COMMAND_GROUPS = [
83
83
  alias: "--check-assets",
84
84
  },
85
85
  { name: "completion", desc: "Generate shell completions", alias: "--install --shell zsh" },
86
- { name: "upgrade, update", desc: "Self-update to latest version", alias: "--force" },
86
+ { name: "upgrade, update", desc: "Self-update to latest version", alias: "--force --all" },
87
87
  { name: "version, -v", desc: "Display version number", alias: "" },
88
88
  { name: "help", desc: "Show help", alias: "[command]" },
89
89
  ],
@@ -136,6 +136,7 @@ export function showHelp() {
136
136
  ${chalk.cyan("$")} tdk networks # Show Traefik-routed URLs
137
137
  ${chalk.cyan("$")} tdk doctor # Check your environment
138
138
  ${chalk.cyan("$")} tdk upgrade # Self-update
139
+ ${chalk.cyan("$")} tdk upgrade --all # Update all TDK installs on PATH
139
140
  `));
140
141
  console.log(chalk.gray(` ${"═".repeat(50)}`));
141
142
  console.log(chalk.gray(` For more help: ${chalk.cyan("tdk help [command]")}`));
@@ -348,11 +348,13 @@ export const TUIApp = ({ animated = true }) => {
348
348
  // Parse SGR 1006 mouse protocol: ESC[<btn;x;yM or ESC[<btn;x;ym
349
349
  // biome-ignore lint/suspicious/noControlCharactersInRegex: \\x1b ESC is intentional (ANSI SGR/CSI sequence parsing)
350
350
  const sgrMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
351
- if (sgrMatch) {
352
- const btn = parseInt(sgrMatch[1], 10);
353
- // sgrMatch[2] is the x coordinate, which the handler does not use
354
- const y = parseInt(sgrMatch[3], 10);
355
- const release = sgrMatch[4] === "m";
351
+ const btnText = sgrMatch?.[1];
352
+ const yText = sgrMatch?.[3];
353
+ const action = sgrMatch?.[4];
354
+ if (btnText !== undefined && yText !== undefined && action !== undefined) {
355
+ const btn = parseInt(btnText, 10);
356
+ const y = parseInt(yText, 10);
357
+ const release = action === "m";
356
358
  // Check if it's a left click (btn & 0b11 == 0 means left button)
357
359
  const isLeftClick = (btn & 0b11) === 0;
358
360
  if (isLeftClick && !release) {
@@ -29,6 +29,7 @@ import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smo
29
29
  import { buildStartupReport, formatStartupReport, isStartupStalled, } from "../utils/startup-report.js";
30
30
  import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
31
31
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
32
+ import { findTiltPortOwner, isForeignTilt } from "../utils/tilt-port-owner.js";
32
33
  import { stopTiltOnPort } from "../utils/tilt-process.js";
33
34
  import { parseTiltPort, resolveTiltPort, secondUpAction, stopTiltForUp, } from "../utils/tilt-startup.js";
34
35
  import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
@@ -369,13 +370,35 @@ export const upCommand = new Command("up")
369
370
  const watchedPort = configuredTiltPort ?? basePort;
370
371
  const candidatePorts = options.only ? [...new Set([basePort, watchedPort])] : [watchedPort];
371
372
  const running = [];
373
+ const foreign = [];
372
374
  for (const candidate of candidatePorts) {
373
- if ((await tiltGetUiResources(candidate)) !== null)
375
+ if ((await tiltGetUiResources(candidate)) === null)
376
+ continue;
377
+ // A Tilt on this port may belong to another project; that is not "this environment is running".
378
+ const owner = findTiltPortOwner(candidate);
379
+ if (owner && isForeignTilt(owner, projectRoot)) {
380
+ foreign.push({ port: candidate, root: owner.root });
381
+ }
382
+ else {
374
383
  running.push(candidate);
384
+ }
375
385
  }
386
+ if (foreign.length > 0 && configuredTiltPort !== undefined) {
387
+ const other = foreign[0];
388
+ const message = `TILT_PORT=${other?.port} is used by another project's Tilt (${other?.root}). Choose a free port, or run tdk down in that project.`;
389
+ emit?.({ ok: false }, [{ code: "TILT_PORT_IN_USE", message }]);
390
+ showErrorAndExit(message);
391
+ }
392
+ if (!options.quiet) {
393
+ for (const other of foreign) {
394
+ console.log(chalk.yellow(`⚠️ Tilt on port ${other.port} belongs to another project (${other.root}); this project will use another port.`));
395
+ }
396
+ }
397
+ // --force replaces this project's Tilt; it must never stop another project's.
398
+ const force = options.force && foreign.length === 0;
376
399
  const decision = secondUpAction({
377
400
  runningPorts: running,
378
- force: options.force,
401
+ force,
379
402
  only: Boolean(options.only),
380
403
  });
381
404
  if (decision.action === "already-running") {
@@ -400,7 +423,7 @@ export const upCommand = new Command("up")
400
423
  }
401
424
  const resolution = await resolveTiltPort({
402
425
  configuredPort: configuredTiltPort,
403
- force: options.force,
426
+ force,
404
427
  basePort,
405
428
  findAvailablePort,
406
429
  });
@@ -410,7 +433,7 @@ export const upCommand = new Command("up")
410
433
  console.log(chalk.blue(`🔄 Auto-switching to port ${port}\n`));
411
434
  }
412
435
  process.env.TILT_PORT = port.toString();
413
- await stopTiltForUp({ force: options.force, quiet: options.quiet, port }, {
436
+ await stopTiltForUp({ force, quiet: options.quiet, port }, {
414
437
  stop: stopTiltOnPort,
415
438
  log: (message) => console.log(chalk.yellow(message)),
416
439
  wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
@@ -15,7 +15,7 @@ import { binaryAssetName, executableName, isWindows } from "../utils/platform.js
15
15
  import { promptConfirm } from "../utils/prompt.js";
16
16
  import { startSpinner } from "../utils/spinner.js";
17
17
  import { extractTarball } from "../utils/tar.js";
18
- import { findOnPath } from "../utils/which.js";
18
+ import { findAllOnPath, findOnPath } from "../utils/which.js";
19
19
  function isStandaloneBinary(tdkPath) {
20
20
  if (isWindows() && tdkPath.toLowerCase().endsWith(".exe"))
21
21
  return true;
@@ -88,6 +88,45 @@ function detectInstallation() {
88
88
  return { method: "unknown" };
89
89
  }
90
90
  }
91
+ function detectInstallationForPath(tdkPath) {
92
+ try {
93
+ const realPath = realpathSync(tdkPath);
94
+ if (realPath.includes("tdk-cli")) {
95
+ const possibleGitRoot = resolve(realPath, "..", "..", "..");
96
+ if (existsSync(join(possibleGitRoot, ".git"))) {
97
+ return { method: "git", path: possibleGitRoot };
98
+ }
99
+ }
100
+ if (tdkPath.includes("node_modules") || tdkPath.includes(".npm") || tdkPath.includes(".bun")) {
101
+ return { method: tdkPath.includes(".bun") ? "bun" : "npm", path: tdkPath };
102
+ }
103
+ if (isStandaloneBinary(realPath))
104
+ return { method: "binary", path: realPath };
105
+ }
106
+ catch (err) {
107
+ logVerbose(`Installation detection failed for ${tdkPath}`, err);
108
+ }
109
+ return { method: "unknown", path: tdkPath };
110
+ }
111
+ function detectAllInstallations() {
112
+ const candidates = findAllOnPath(executableName()).map(detectInstallationForPath);
113
+ const current = detectInstallation();
114
+ if (current.method !== "unknown")
115
+ candidates.unshift(current);
116
+ const unique = new Map();
117
+ for (const install of candidates) {
118
+ if (install.method === "unknown" || !install.path)
119
+ continue;
120
+ let key = install.path;
121
+ try {
122
+ if (install.method === "binary")
123
+ key = realpathSync(install.path);
124
+ }
125
+ catch { }
126
+ unique.set(`${install.method}:${key}`, install);
127
+ }
128
+ return [...unique.values()];
129
+ }
91
130
  const BINARY_RELEASE_REPO = "tdk-landscape/tdk-cli-releases";
92
131
  const ENGINE_ASSET_NAME = "tdk-cli-engine.tar.gz";
93
132
  const CHECKSUMS_ASSET_NAME = "checksums.txt";
@@ -355,8 +394,75 @@ export async function upgradeViaBun() {
355
394
  return false;
356
395
  }
357
396
  }
397
+ async function upgradeAllInstallations(dryRun) {
398
+ const installations = detectAllInstallations();
399
+ if (!installations.length) {
400
+ console.error(chalk.red("Could not find any TDK installations on PATH."));
401
+ return false;
402
+ }
403
+ console.log(chalk.blue(`Found ${installations.length} TDK installation${installations.length === 1 ? "" : "s"}:`));
404
+ for (const install of installations) {
405
+ console.log(chalk.gray(` ${install.method}: ${install.path}`));
406
+ }
407
+ if (dryRun) {
408
+ console.log(chalk.yellow("\nDry run: no installations were changed."));
409
+ return true;
410
+ }
411
+ const binaryInstallations = installations.filter((install) => install.method === "binary" && install.path);
412
+ const release = binaryInstallations.length ? await getLatestBinaryRelease() : null;
413
+ if (binaryInstallations.length && !release) {
414
+ console.error(chalk.red("Could not find a compatible TDK binary release for this platform."));
415
+ return false;
416
+ }
417
+ let success = true;
418
+ const completed = new Set();
419
+ for (const install of installations) {
420
+ const key = install.method === "npm" || install.method === "bun"
421
+ ? install.method
422
+ : `${install.method}:${install.path}`;
423
+ if (completed.has(key))
424
+ continue;
425
+ completed.add(key);
426
+ if (install.method === "git" && install.path) {
427
+ console.log(chalk.cyan(`\nUpdating git install at ${install.path}`));
428
+ success = (await upgradeViaGit(install.path)) && success;
429
+ }
430
+ else if (install.method === "binary" && install.path && release) {
431
+ const latest = release.tag.replace(/^v/, "").split("-")[0];
432
+ let current = "";
433
+ try {
434
+ current = execFileSync(install.path, ["version"], {
435
+ encoding: "utf-8",
436
+ timeout: 10_000,
437
+ }).trim();
438
+ }
439
+ catch { }
440
+ if (current === latest) {
441
+ console.log(chalk.green(`\n${install.path} is already current (${current}).`));
442
+ }
443
+ else {
444
+ console.log(chalk.cyan(`\nUpdating ${install.path} (${current || "unknown"} → ${latest})`));
445
+ success = (await upgradeViaBinary(install.path, release)) && success;
446
+ }
447
+ }
448
+ else if (install.method === "npm") {
449
+ console.log(chalk.cyan("\nUpdating the npm installation..."));
450
+ success = (await upgradeViaNpm()) && success;
451
+ }
452
+ else if (install.method === "bun") {
453
+ console.log(chalk.cyan("\nUpdating the Bun installation..."));
454
+ success = (await upgradeViaBun()) && success;
455
+ }
456
+ }
457
+ if (success)
458
+ console.log(chalk.green.bold("\nAll TDK installations on PATH are up to date."));
459
+ else
460
+ console.error(chalk.red("\nAt least one TDK installation could not be updated."));
461
+ return success;
462
+ }
358
463
  async function upgradeViaGit(path) {
359
- const spinner = startSpinner("Pulling latest changes from git...");
464
+ const spinner = startSpinner("Updating the git installation from the default branch...");
465
+ let rebaseInProgress = false;
360
466
  try {
361
467
  execFileSync("git", ["rev-parse", "--git-dir"], {
362
468
  cwd: path,
@@ -368,16 +474,57 @@ async function upgradeViaGit(path) {
368
474
  stdio: "pipe",
369
475
  timeout: 30000,
370
476
  });
371
- const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
477
+ const dirty = execFileSync("git", ["status", "--porcelain"], {
372
478
  cwd: path,
373
479
  encoding: "utf-8",
374
480
  }).trim();
375
- spinner.text = `Pulling latest on ${branch}...`;
376
- execFileSync("git", ["pull", "origin", branch], {
481
+ if (dirty) {
482
+ throw new Error("The TDK source checkout has local changes. Commit or stash them, then run `tdk upgrade` again.");
483
+ }
484
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
377
485
  cwd: path,
378
- stdio: "pipe",
379
- timeout: 30000,
380
- });
486
+ encoding: "utf-8",
487
+ }).trim();
488
+ let defaultRef = "";
489
+ try {
490
+ defaultRef = execFileSync("git", ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], {
491
+ cwd: path,
492
+ encoding: "utf-8",
493
+ stdio: "pipe",
494
+ }).trim();
495
+ }
496
+ catch {
497
+ for (const candidate of ["origin/main", "origin/master"]) {
498
+ try {
499
+ execFileSync("git", ["rev-parse", "--verify", candidate], { cwd: path, stdio: "pipe" });
500
+ defaultRef = candidate;
501
+ break;
502
+ }
503
+ catch { }
504
+ }
505
+ }
506
+ if (!defaultRef?.startsWith("origin/")) {
507
+ throw new Error("Could not find the default branch for origin (expected origin/HEAD, origin/main, or origin/master).");
508
+ }
509
+ const defaultBranch = defaultRef.slice("origin/".length);
510
+ if (branch === defaultBranch) {
511
+ spinner.text = `Fast-forwarding ${branch} from origin/${defaultBranch}...`;
512
+ execFileSync("git", ["pull", "--ff-only", "origin", defaultBranch], {
513
+ cwd: path,
514
+ stdio: "pipe",
515
+ timeout: 30000,
516
+ });
517
+ }
518
+ else {
519
+ spinner.text = `Rebasing ${branch} onto origin/${defaultBranch}...`;
520
+ rebaseInProgress = true;
521
+ execFileSync("git", ["rebase", defaultRef], {
522
+ cwd: path,
523
+ stdio: "pipe",
524
+ timeout: 60000,
525
+ });
526
+ rebaseInProgress = false;
527
+ }
381
528
  if (existsSync(join(path, "cli", PACKAGE_JSON))) {
382
529
  spinner.text = "Rebuilding CLI...";
383
530
  execFileSync(findOnPath("bun") ?? "bun", ["install"], {
@@ -397,10 +544,16 @@ async function upgradeViaGit(path) {
397
544
  stdio: "pipe",
398
545
  timeout: 30000,
399
546
  });
400
- spinner.succeed("Upgraded successfully via git pull");
547
+ spinner.succeed(`Upgraded successfully from origin/${defaultBranch}`);
401
548
  return true;
402
549
  }
403
550
  catch (err) {
551
+ if (rebaseInProgress) {
552
+ try {
553
+ execFileSync("git", ["rebase", "--abort"], { cwd: path, stdio: "pipe" });
554
+ }
555
+ catch { }
556
+ }
404
557
  spinner.fail(`Git upgrade failed: ${getErrorMessage(err)}`);
405
558
  return false;
406
559
  }
@@ -409,6 +562,7 @@ export const upgradeCommand = new Command("upgrade")
409
562
  .alias("update")
410
563
  .description("Upgrade TDK CLI to the latest version")
411
564
  .option("-f, --force", "Force upgrade even if already on latest", false)
565
+ .option("--all", "Upgrade every TDK installation found on PATH", false)
412
566
  .option("--dry-run", "Show what would be upgraded without actually doing it", false)
413
567
  .option("-y, --yes", "Skip confirmation prompt", false)
414
568
  .action(async (options) => {
@@ -418,6 +572,27 @@ export const upgradeCommand = new Command("upgrade")
418
572
  const installInfo = detectInstallation();
419
573
  console.log(chalk.gray(`Installation method: ${installInfo.method}`));
420
574
  console.log();
575
+ if (options.all) {
576
+ const installations = detectAllInstallations();
577
+ if (!installations.length) {
578
+ console.error(chalk.red("Could not find any TDK installations on PATH."));
579
+ process.exit(1);
580
+ }
581
+ if (!options.dryRun && !options.yes) {
582
+ const confirm = await promptConfirm({
583
+ message: `Upgrade all ${installations.length} TDK installations on PATH?`,
584
+ initial: true,
585
+ });
586
+ if (!confirm) {
587
+ showCancelled();
588
+ process.exit(0);
589
+ }
590
+ }
591
+ const success = await upgradeAllInstallations(options.dryRun);
592
+ if (!success)
593
+ process.exit(1);
594
+ return;
595
+ }
421
596
  const windowsNpmMessage = windowsNpmUpgradeMessage(process.platform, installInfo);
422
597
  if (windowsNpmMessage) {
423
598
  console.log(windowsNpmMessage);
@@ -459,7 +634,7 @@ export const upgradeCommand = new Command("upgrade")
459
634
  }
460
635
  }
461
636
  else if (installInfo.method === "git" && installInfo.path) {
462
- console.log(chalk.blue("📦 Git installation detected - will pull latest from origin"));
637
+ console.log(chalk.blue("📦 Git installation detected - will update from origin's default branch"));
463
638
  try {
464
639
  execFileSync("git", ["fetch", "origin"], { cwd: installInfo.path, stdio: "pipe" });
465
640
  const localHash = execFileSync("git", ["rev-parse", "HEAD"], {
@@ -516,7 +691,7 @@ export const upgradeCommand = new Command("upgrade")
516
691
  console.log(chalk.gray(` Path: ${installInfo.path}`));
517
692
  }
518
693
  if (installInfo.method === "git") {
519
- console.log(chalk.gray(" Action: git pull origin main && bun install && bun link --force"));
694
+ console.log(chalk.gray(" Action: fetch origin, update from origin/HEAD, rebuild, and relink"));
520
695
  }
521
696
  else if (installInfo.method === "binary" && binaryRelease) {
522
697
  console.log(chalk.gray(` Action: Download and replace with ${binaryRelease.downloadUrl}`));
@@ -565,7 +740,8 @@ export const upgradeCommand = new Command("upgrade")
565
740
  if (!success) {
566
741
  console.error(chalk.red("\nUpgrade failed"));
567
742
  if (installInfo.method === "git") {
568
- console.log(chalk.cyan(` cd ${installInfo.path} && git pull && bun link --force`));
743
+ console.log(chalk.cyan(` Check local changes with: git -C ${installInfo.path} status --short`));
744
+ console.log(chalk.cyan(" Commit or stash them, then retry: tdk upgrade"));
569
745
  }
570
746
  else if (installInfo.method === "binary" && binaryRelease && installInfo.path) {
571
747
  const engineDir = join(dirname(installInfo.path), "tdk-cli");
@@ -505,6 +505,26 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
505
505
  // vendor a working copy into .tdk/.tdk-out/tdk-cli-ext/ at generation time.
506
506
  // This makes `tdk up` work offline and on any machine - no hardcoded paths,
507
507
  // no dependency on a private GitHub repo.
508
+ // The engine's own unit tests (for example wake-gateway/decide.test.ts, which imports bun:test) are
509
+ // not needed to run a stack. Vendoring them under .tdk/.tdk-out makes a Node project's test runner
510
+ // (Vitest, Jest) discover them and fail, so they are left out of the vendored copy.
511
+ const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
512
+ export function isTestFile(filePath) {
513
+ return TEST_FILE_PATTERN.test(filePath);
514
+ }
515
+ function removeVendoredTestFiles(dir) {
516
+ if (!fs.existsSync(dir))
517
+ return;
518
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
519
+ const entryPath = path.join(dir, entry.name);
520
+ if (entry.isDirectory()) {
521
+ removeVendoredTestFiles(entryPath);
522
+ }
523
+ else if (isTestFile(entryPath)) {
524
+ fs.rmSync(entryPath, { force: true });
525
+ }
526
+ }
527
+ }
508
528
  const TDK_EXTENSION_DIRS = ["engine", "discovery", "specs", "ext"];
509
529
  const PROJECT_RUNTIME_ASSET_DIRS = [["shared-platform-engineering", "docker-templates"]];
510
530
  const REQUIRED_DOCKER_TEMPLATE_FILES = [
@@ -618,8 +638,11 @@ async function vendorTdkExtension(projectRoot) {
618
638
  }
619
639
  fs.copyFileSync(path.join(sourceRoot, "Tiltfile"), path.join(vendoredDir, "Tiltfile"));
620
640
  for (const dir of TDK_EXTENSION_DIRS) {
641
+ // A project vendored by an older TDK already holds test files; cpSync only adds, so drop them.
642
+ removeVendoredTestFiles(path.join(vendoredDir, dir));
621
643
  fs.cpSync(path.join(sourceRoot, dir), path.join(vendoredDir, dir), {
622
644
  recursive: true,
645
+ filter: (source) => !isTestFile(source),
623
646
  });
624
647
  }
625
648
  console.log(`✓ Vendored TDK extension → .tdk/.tdk-out/tdk-cli-ext/`);