@mesh-tech/mesh-cli 0.39.0 → 0.40.1

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 (96) hide show
  1. package/dist/bin/{mesh.auth-preflight-JO5G7SOF.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
  2. package/dist/bin/{mesh.chunk-CNLSVKYY.js → mesh.chunk-2ASXJ5DO.js} +233 -581
  3. package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
  4. package/dist/bin/{mesh.chunk-WODQO6TP.js → mesh.chunk-6AIG5OJM.js} +3 -3
  5. package/dist/bin/{mesh.chunk-OSV3EZQ3.js → mesh.chunk-6SSOVZOI.js} +1 -1
  6. package/dist/bin/{mesh.chunk-2ZJR5LGN.js → mesh.chunk-7HTKSUWR.js} +1507 -548
  7. package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
  8. package/dist/bin/{mesh.chunk-UI6EFAMD.js → mesh.chunk-F44A3ACW.js} +2 -2
  9. package/dist/bin/{mesh.chunk-C2TKCYHP.js → mesh.chunk-HJGMOEUG.js} +1 -1
  10. package/dist/bin/{mesh.chunk-O6WRZU4X.js → mesh.chunk-NSQJEX36.js} +1 -1
  11. package/dist/bin/{mesh.chunk-C4ZM7DM2.js → mesh.chunk-PZVI7V3J.js} +1 -1
  12. package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
  13. package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
  14. package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
  15. package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
  16. package/dist/bin/{mesh.create-app-SOHBL75U.js → mesh.create-app-Z3RKSTBO.js} +3 -3
  17. package/dist/bin/{mesh.dev-token-server-SS7QEQOQ.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
  18. package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
  19. package/dist/bin/mesh.js +9 -8
  20. package/dist/bin/mesh.js.map +1 -1
  21. package/dist/bin/{mesh.login-6VPMWHHU.js → mesh.login-3AWVGPCM.js} +2 -2
  22. package/dist/bin/{mesh.platform-setup-DJJB2PWL.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
  23. package/dist/bin/{mesh.program-Y4HUNBFP.js → mesh.program-K5JQXEHX.js} +9 -8
  24. package/dist/bin/{mesh.registry-5P4ZLFLJ.js → mesh.registry-JLQYFUNN.js} +4 -4
  25. package/dist/bin/{mesh.registry-identity-ONGUJQAY.js → mesh.registry-identity-3AR27UGO.js} +3 -3
  26. package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
  27. package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
  28. package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
  29. package/dist/bin/{mesh.start-5CPWOHDJ.js → mesh.start-KZ5L6SOJ.js} +2 -2
  30. package/dist/bin/{mesh.wizard-FNRTSGVN.js → mesh.wizard-FCTT55KI.js} +7 -7
  31. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
  32. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
  33. package/dist/build-info.json +2 -2
  34. package/dist/src/commands/dev-doctor.d.ts +22 -0
  35. package/dist/src/commands/dev-doctor.js +149 -0
  36. package/dist/src/commands/dev-env-leaks.d.ts +24 -0
  37. package/dist/src/commands/dev-env-leaks.js +178 -0
  38. package/dist/src/commands/dev-launch.d.ts +17 -1
  39. package/dist/src/commands/dev-launch.js +130 -5
  40. package/dist/src/commands/dev-readiness.d.ts +49 -0
  41. package/dist/src/commands/dev-readiness.js +78 -0
  42. package/dist/src/commands/dev.d.ts +60 -0
  43. package/dist/src/commands/dev.js +539 -140
  44. package/dist/src/commands/hub/index.js +10 -7
  45. package/dist/src/commands/mcp/generate.d.ts +11 -0
  46. package/dist/src/commands/mcp/generate.js +78 -0
  47. package/dist/src/commands/mcp/index.d.ts +2 -0
  48. package/dist/src/commands/mcp/index.js +70 -0
  49. package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
  50. package/dist/src/commands/mcp/manifest-file.js +24 -0
  51. package/dist/src/commands/mcp/review-command.d.ts +24 -0
  52. package/dist/src/commands/mcp/review-command.js +66 -0
  53. package/dist/src/commands/mcp/review.d.ts +41 -0
  54. package/dist/src/commands/mcp/review.js +140 -0
  55. package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
  56. package/dist/src/commands/mcp/workspace-membership.js +77 -0
  57. package/dist/src/commands/secrets/reindex.js +2 -7
  58. package/dist/src/commands/secrets/set.d.ts +22 -1
  59. package/dist/src/commands/secrets/set.js +31 -8
  60. package/dist/src/commands/temporal.js +35 -4
  61. package/dist/src/commands/temporal.js.map +2 -2
  62. package/dist/src/program.js +2 -0
  63. package/dist/src/utils/aws-auth.d.ts +4 -0
  64. package/dist/src/utils/aws-auth.js +7 -3
  65. package/dist/src/utils/dev-sessions-root.d.ts +3 -0
  66. package/dist/src/utils/dev-sessions-root.js +45 -0
  67. package/dist/src/utils/kubeconfig.js +2 -2
  68. package/dist/src/utils/tailscale.js +7 -1
  69. package/dist/src/utils/temporal-auth.js +2 -1
  70. package/dist/src/utils/workflow-fingerprint.js +23 -12
  71. package/fragments/agents/AGENTS.md.hbs +3 -1
  72. package/fragments/base/index.ts.hbs +3 -0
  73. package/package.json +7 -5
  74. package/skills/mesh/SKILL.md +10 -6
  75. package/templates/api-auth/index.ts.hbs +1 -0
  76. package/templates/api-role-gating/index.ts.hbs +1 -0
  77. package/templates/external-service/index.ts.hbs +1 -0
  78. package/templates/workflow/index.ts.hbs +2 -0
  79. package/dist/bin/mesh.chunk-2ZJR5LGN.js.map +0 -6
  80. package/dist/bin/mesh.chunk-CNLSVKYY.js.map +0 -6
  81. /package/dist/bin/{mesh.auth-preflight-JO5G7SOF.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
  82. /package/dist/bin/{mesh.chunk-WODQO6TP.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
  83. /package/dist/bin/{mesh.chunk-OSV3EZQ3.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
  84. /package/dist/bin/{mesh.chunk-UI6EFAMD.js.map → mesh.chunk-F44A3ACW.js.map} +0 -0
  85. /package/dist/bin/{mesh.chunk-C2TKCYHP.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
  86. /package/dist/bin/{mesh.chunk-O6WRZU4X.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
  87. /package/dist/bin/{mesh.chunk-C4ZM7DM2.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
  88. /package/dist/bin/{mesh.create-app-SOHBL75U.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
  89. /package/dist/bin/{mesh.dev-token-server-SS7QEQOQ.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
  90. /package/dist/bin/{mesh.login-6VPMWHHU.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
  91. /package/dist/bin/{mesh.platform-setup-DJJB2PWL.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
  92. /package/dist/bin/{mesh.program-Y4HUNBFP.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
  93. /package/dist/bin/{mesh.registry-5P4ZLFLJ.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
  94. /package/dist/bin/{mesh.registry-identity-ONGUJQAY.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
  95. /package/dist/bin/{mesh.start-5CPWOHDJ.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
  96. /package/dist/bin/{mesh.wizard-FNRTSGVN.js.map → mesh.wizard-FCTT55KI.js.map} +0 -0
@@ -49,14 +49,14 @@ import {
49
49
  isExpiredAwsTokenMessage,
50
50
  probeRegistryToken,
51
51
  registryLoginFix
52
- } from "./mesh.chunk-OSV3EZQ3.js";
52
+ } from "./mesh.chunk-6SSOVZOI.js";
53
53
 
54
54
  // packages/mesh-cli/src/commands/login.ts
55
55
  import * as http from "http";
56
56
  import * as crypto3 from "crypto";
57
- import * as fs19 from "fs";
58
- import * as path20 from "path";
59
- import { execFileSync as execFileSync15 } from "child_process";
57
+ import * as fs21 from "fs";
58
+ import * as path22 from "path";
59
+ import { execFileSync as execFileSync16 } from "child_process";
60
60
 
61
61
  // packages/mesh-cli/src/utils/aws-auth.ts
62
62
  import * as fs2 from "fs";
@@ -167,7 +167,7 @@ function pulumiStackOutput(appRoot, key, extraArgs, env) {
167
167
 
168
168
  // packages/mesh-cli/src/utils/aws-auth.ts
169
169
  var DEFAULT_REGION = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
170
- function readStackPlatform(appRoot, stack) {
170
+ function derivePlatformRef(appRoot, stack) {
171
171
  const configFile = path2.join(appRoot, `Pulumi.${stack}.yaml`);
172
172
  if (!fs2.existsSync(configFile)) return null;
173
173
  const content = fs2.readFileSync(configFile, "utf-8");
@@ -175,17 +175,21 @@ function readStackPlatform(appRoot, stack) {
175
175
  if (jsonMatch) {
176
176
  try {
177
177
  const parsed = JSON.parse(jsonMatch[1]);
178
- if (parsed.name && parsed.env) return `${parsed.name}.${parsed.env}`;
178
+ if (parsed.name && parsed.env) return { name: parsed.name, env: parsed.env };
179
179
  } catch {
180
180
  }
181
181
  }
182
182
  const nameMatch = content.match(/mesh:platform:[\s\S]*?name:\s*(\S+)/);
183
183
  const envMatch = content.match(/mesh:platform:[\s\S]*?env:\s*(\S+)/);
184
184
  if (nameMatch && envMatch) {
185
- return `${nameMatch[1]}.${envMatch[1]}`;
185
+ return { name: nameMatch[1], env: envMatch[1] };
186
186
  }
187
187
  return null;
188
188
  }
189
+ function readStackPlatform(appRoot, stack) {
190
+ const ref = derivePlatformRef(appRoot, stack);
191
+ return ref ? `${ref.name}.${ref.env}` : null;
192
+ }
189
193
  function derivePlatformContext(appRoot, stack) {
190
194
  const explicit = readStackConfig(appRoot, stack, "mesh:loginContext");
191
195
  if (explicit) return explicit;
@@ -562,7 +566,7 @@ async function resolveAwsCredentials(roleArn, appRoot, stack) {
562
566
  }
563
567
 
564
568
  // packages/mesh-cli/src/commands/vpn/index.ts
565
- import { execFileSync as execFileSync14 } from "child_process";
569
+ import { execFileSync as execFileSync15 } from "child_process";
566
570
  import * as net8 from "node:net";
567
571
 
568
572
  // packages/mesh-cli/src/utils/context.ts
@@ -1194,11 +1198,11 @@ function unregisterDnsRecords(cfg, suffix) {
1194
1198
  import * as net7 from "node:net";
1195
1199
 
1196
1200
  // packages/mesh-cli/src/utils/tailscale.ts
1197
- import * as fs18 from "node:fs";
1201
+ import * as fs20 from "node:fs";
1198
1202
  import * as net6 from "node:net";
1199
- import * as os4 from "node:os";
1200
- import * as path19 from "node:path";
1201
- import { spawn as spawn2, execFileSync as execFileSync13 } from "node:child_process";
1203
+ import * as os3 from "node:os";
1204
+ import * as path21 from "node:path";
1205
+ import { spawn as spawn2, execFileSync as execFileSync14 } from "node:child_process";
1202
1206
 
1203
1207
  // packages/mesh-cli/src/utils/pid.ts
1204
1208
  var MIN_SIGNALABLE_PID = 2;
@@ -1236,16 +1240,16 @@ function assertSignalablePid(pid, source) {
1236
1240
  }
1237
1241
 
1238
1242
  // packages/mesh-cli/src/utils/tailscale-targets.ts
1239
- import { execFileSync as execFileSync11 } from "node:child_process";
1240
- import * as fs16 from "node:fs";
1241
- import * as path17 from "node:path";
1243
+ import { execFileSync as execFileSync12 } from "node:child_process";
1244
+ import * as fs18 from "node:fs";
1245
+ import * as path19 from "node:path";
1242
1246
 
1243
1247
  // packages/mesh-cli/src/commands/dev.ts
1244
- import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync2 } from "child_process";
1245
- import * as fs15 from "fs";
1248
+ import { execFileSync as execFileSync11, spawn, spawnSync as spawnSync2 } from "child_process";
1249
+ import { createHash as createHash5 } from "crypto";
1250
+ import * as fs17 from "fs";
1246
1251
  import * as net3 from "net";
1247
- import * as os3 from "os";
1248
- import * as path16 from "path";
1252
+ import * as path18 from "path";
1249
1253
  import { Option } from "commander";
1250
1254
  import { SecretsManagerClient as SecretsManagerClient2, GetSecretValueCommand as GetSecretValueCommand2 } from "@aws-sdk/client-secrets-manager";
1251
1255
 
@@ -1360,9 +1364,48 @@ async function resolvePulumiEnv(opts) {
1360
1364
  }
1361
1365
 
1362
1366
  // packages/mesh-cli/src/utils/kubeconfig.ts
1363
- import * as fs5 from "fs";
1364
- import * as os from "os";
1365
- import * as path5 from "path";
1367
+ import * as fs6 from "fs";
1368
+ import * as path6 from "path";
1369
+
1370
+ // packages/mesh-cli/src/utils/dev-sessions-root.ts
1371
+ import fs5 from "node:fs";
1372
+ import os from "node:os";
1373
+ import path5 from "node:path";
1374
+ var migrated = false;
1375
+ function devSessionsRoot(env = process.env) {
1376
+ const configured = env.MESH_DEV_SESSIONS_DIR?.trim();
1377
+ const root = configured || path5.join(env.XDG_CONFIG_HOME?.trim() || path5.join(os.homedir(), ".config"), "mesh", "dev-sessions");
1378
+ fs5.mkdirSync(root, { recursive: true });
1379
+ if (!configured && !migrated) {
1380
+ migrated = true;
1381
+ migrateLegacySessionsDir(path5.join(env.TMPDIR?.trim() || os.tmpdir(), "mesh-dev-sessions"), root);
1382
+ }
1383
+ return root;
1384
+ }
1385
+ function migrateLegacySessionsDir(legacy, root) {
1386
+ try {
1387
+ if (path5.resolve(legacy) === path5.resolve(root)) return;
1388
+ let stat;
1389
+ try {
1390
+ stat = fs5.lstatSync(legacy);
1391
+ } catch {
1392
+ stat = void 0;
1393
+ }
1394
+ if (stat?.isSymbolicLink()) return;
1395
+ if (stat?.isDirectory()) {
1396
+ for (const entry of fs5.readdirSync(legacy)) {
1397
+ const to = path5.join(root, entry);
1398
+ if (!fs5.existsSync(to)) fs5.cpSync(path5.join(legacy, entry), to, { recursive: true, preserveTimestamps: true });
1399
+ }
1400
+ fs5.renameSync(legacy, `${legacy}.migrated-${Date.now()}`);
1401
+ }
1402
+ fs5.mkdirSync(path5.dirname(legacy), { recursive: true });
1403
+ fs5.symlinkSync(root, legacy, "dir");
1404
+ } catch {
1405
+ }
1406
+ }
1407
+
1408
+ // packages/mesh-cli/src/utils/kubeconfig.ts
1366
1409
  function buildKubeconfig(cluster, region) {
1367
1410
  return `apiVersion: v1
1368
1411
  kind: Config
@@ -1393,7 +1436,7 @@ users:
1393
1436
  `;
1394
1437
  }
1395
1438
  function sessionKubeconfigPath(sessionName) {
1396
- return path5.join(os.tmpdir(), "mesh-dev-sessions", `${sessionName}.kubeconfig`);
1439
+ return path6.join(devSessionsRoot(), `${sessionName}.kubeconfig`);
1397
1440
  }
1398
1441
  function resolveHubPlatformName(platform) {
1399
1442
  return platform?.name ?? "mesh";
@@ -1426,10 +1469,10 @@ async function ensureKubeconfig(platformName, env, sessionName, deps = {}) {
1426
1469
  }
1427
1470
  const region = process.env.AWS_REGION || "us-east-2";
1428
1471
  const kubeconfigPath = sessionKubeconfigPath(sessionName);
1429
- if (!fs5.existsSync(path5.dirname(kubeconfigPath))) {
1430
- fs5.mkdirSync(path5.dirname(kubeconfigPath), { recursive: true });
1472
+ if (!fs6.existsSync(path6.dirname(kubeconfigPath))) {
1473
+ fs6.mkdirSync(path6.dirname(kubeconfigPath), { recursive: true });
1431
1474
  }
1432
- fs5.writeFileSync(kubeconfigPath, buildKubeconfig(cluster, region), { mode: 384 });
1475
+ fs6.writeFileSync(kubeconfigPath, buildKubeconfig(cluster, region), { mode: 384 });
1433
1476
  return kubeconfigPath;
1434
1477
  }
1435
1478
 
@@ -1508,7 +1551,7 @@ async function resolveTemporalAuth(tenant, env, platformName = tenant) {
1508
1551
  const stderr = String(err?.stderr ?? "");
1509
1552
  if (/\.kube[/\\]config|error loading config file/i.test(`${detail} ${stderr}`)) {
1510
1553
  logWarn(
1511
- "kubectl could not read a kubeconfig and fell back to ~/.kube/config (macOS blocks it for shells without Full Disk Access). Run inside the mesh dev tmux session, or export KUBECONFIG=$TMPDIR/mesh-dev-sessions/<session>.kubeconfig."
1554
+ "kubectl could not read a kubeconfig and fell back to ~/.kube/config (macOS blocks it for shells without Full Disk Access). Run inside the mesh dev tmux session, or export KUBECONFIG=~/.config/mesh/dev-sessions/<session>.kubeconfig ($MESH_DEV_SESSIONS_DIR/<session>.kubeconfig when that is set)."
1512
1555
  );
1513
1556
  }
1514
1557
  }
@@ -1610,6 +1653,7 @@ var WORKFLOW_PACKAGE_DIRS = [
1610
1653
  "packages/agent-contracts/src"
1611
1654
  ];
1612
1655
  var EXCLUDE = /(^|\/)(node_modules|dist|__tests__)(\/|$)|\.(test|spec)\.[cm]?tsx?$/;
1656
+ var EXCLUDED_DIR_NAMES = /* @__PURE__ */ new Set(["node_modules", "dist", "__tests__"]);
1613
1657
  function findRepoRoot2(startDir) {
1614
1658
  let dir = startDir;
1615
1659
  for (; ; ) {
@@ -1633,19 +1677,27 @@ function resolveWorkerSourceDirs(repoRoot, services = {}) {
1633
1677
  }
1634
1678
  return dirs;
1635
1679
  }
1680
+ function listDir(dir) {
1681
+ try {
1682
+ return readdirSync3(dir, { withFileTypes: true });
1683
+ } catch {
1684
+ return [];
1685
+ }
1686
+ }
1687
+ function collectSourceFiles(dir, files) {
1688
+ for (const entry of listDir(dir)) {
1689
+ if (entry.isSymbolicLink()) continue;
1690
+ if (entry.isDirectory()) {
1691
+ if (!EXCLUDED_DIR_NAMES.has(entry.name)) collectSourceFiles(join6(dir, entry.name), files);
1692
+ } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !EXCLUDE.test(entry.name)) {
1693
+ files.push(join6(dir, entry.name));
1694
+ }
1695
+ }
1696
+ }
1636
1697
  function computeWorkflowFingerprint(dirs, relativeTo) {
1637
1698
  const files = [];
1638
1699
  for (const dir of dirs) {
1639
- let rels;
1640
- try {
1641
- rels = readdirSync3(dir, { recursive: true });
1642
- } catch {
1643
- continue;
1644
- }
1645
- for (const rel of rels) {
1646
- const p = String(rel);
1647
- if (/\.(ts|tsx)$/.test(p) && !EXCLUDE.test(p)) files.push(join6(dir, p));
1648
- }
1700
+ collectSourceFiles(dir, files);
1649
1701
  }
1650
1702
  files.sort();
1651
1703
  if (files.length === 0) return "";
@@ -1669,9 +1721,9 @@ function fingerprintWorkflowSource(appRoot, services = {}) {
1669
1721
  }
1670
1722
 
1671
1723
  // packages/mesh-cli/src/commands/dev-launch.ts
1672
- import * as fs6 from "fs";
1724
+ import * as fs7 from "fs";
1673
1725
  import * as net from "net";
1674
- import * as path6 from "path";
1726
+ import * as path7 from "path";
1675
1727
  var ENV_FILE_MODE = 384;
1676
1728
  var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1677
1729
  function quoteShellValue(value) {
@@ -1694,15 +1746,158 @@ function renderEnvFile(env) {
1694
1746
  function envFileName(serviceName) {
1695
1747
  return `${serviceName.replace(/[^A-Za-z0-9._-]/g, "-")}.env.sh`;
1696
1748
  }
1749
+ function getSessionEnvDir(sessionName) {
1750
+ return path7.join(devSessionsRoot(), sessionName);
1751
+ }
1752
+ function serviceOverridesPaths(sessionName, serviceName) {
1753
+ const dir = getSessionEnvDir(sessionName);
1754
+ return [path7.join(dir, "_all.overrides.env.sh"), path7.join(dir, `${serviceName}.overrides.env.sh`)];
1755
+ }
1756
+ function readServiceOverrides(sessionName, serviceName) {
1757
+ const merged = {};
1758
+ for (const file of serviceOverridesPaths(sessionName, serviceName)) {
1759
+ try {
1760
+ Object.assign(merged, parseEnvFile(fs7.readFileSync(file, "utf8")));
1761
+ } catch {
1762
+ }
1763
+ }
1764
+ return merged;
1765
+ }
1697
1766
  function writeEnvFile(filePath, env) {
1698
- fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
1699
- fs6.writeFileSync(filePath, renderEnvFile(env), { mode: ENV_FILE_MODE });
1700
- fs6.chmodSync(filePath, ENV_FILE_MODE);
1767
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
1768
+ fs7.writeFileSync(filePath, renderEnvFile(env), { mode: ENV_FILE_MODE });
1769
+ fs7.chmodSync(filePath, ENV_FILE_MODE);
1770
+ }
1771
+ var LAUNCH_HOST_ENV_ALLOW_LIST = [
1772
+ // Who the process is. Without HOME nothing finds ~/.aws (the SSO profile
1773
+ // `buildChildAwsEnv` hands over by name), ~/.npmrc (the registry token the
1774
+ // service's `pnpm dev` needs), ~/.config/mesh (the credentials the
1775
+ // token-server window reads), ~/.ssh, ~/.cache, or the user's shell rc files.
1776
+ "HOME",
1777
+ "USER",
1778
+ "LOGNAME",
1779
+ // The launching shell's PATH — the one that found `mesh` — so `node`, `pnpm`,
1780
+ // `npx`, `aws` and whatever the dev command names resolve the same way they
1781
+ // did for the launcher, not the way a bare tmux pane's login shell would.
1782
+ "PATH",
1783
+ // The interpreter the launch runs under (see `launchShell`) and what tools
1784
+ // that spawn `$SHELL` get.
1785
+ "SHELL",
1786
+ // macOS gives each user a private temp dir; pnpm, node, and the AWS CLI's
1787
+ // session manager plugin read it.
1788
+ "TMPDIR",
1789
+ // Where `mesh dev` keeps session state and env files (`devSessionsRoot`). A
1790
+ // `mesh` command run inside a window (the token-server, `mesh dev restart`
1791
+ // from a pane) must resolve the same root the launcher used.
1792
+ "MESH_DEV_SESSIONS_DIR",
1793
+ // Locale and timezone: text encoding for the service's stdout, and the
1794
+ // timestamps in its logs.
1795
+ "LANG",
1796
+ "LANGUAGE",
1797
+ "TZ",
1798
+ // XDG base dirs: `mesh login` stores credentials under XDG_CONFIG_HOME
1799
+ // (login.ts), the docs cache under XDG_CACHE_HOME — the token-server window
1800
+ // and any `npx mesh …` a service runs must agree with the launcher on where
1801
+ // those are. LC_* is matched by prefix below.
1802
+ "XDG_CONFIG_HOME",
1803
+ "XDG_CACHE_HOME",
1804
+ "XDG_DATA_HOME",
1805
+ "XDG_STATE_HOME",
1806
+ "XDG_RUNTIME_DIR",
1807
+ // The CLI's own cache-root override (cache-home.ts): the status and
1808
+ // token-server windows run `npx mesh`, and must read the same cache the
1809
+ // launcher wrote.
1810
+ "MESH_CACHE_HOME",
1811
+ // ssh-agent, for a service that runs git over ssh (the workspaces manager
1812
+ // cloning its mirrors). tmux's own `update-environment` default forwards
1813
+ // this into new sessions for the same reason.
1814
+ "SSH_AUTH_SOCK",
1815
+ // Docker: a service that drives the daemon (the workspaces manager starting
1816
+ // sidecars) must reach the SAME daemon the launcher's `docker` did — Colima
1817
+ // and Docker Desktop are selected by these, not by anything on disk.
1818
+ "DOCKER_HOST",
1819
+ "DOCKER_CONTEXT",
1820
+ "DOCKER_CONFIG",
1821
+ "DOCKER_CERT_PATH",
1822
+ "DOCKER_TLS_VERIFY",
1823
+ // Egress: corporate proxies and the extra CA they terminate TLS with. Both
1824
+ // casings, because Node reads the lower-case ones and curl/aws either.
1825
+ "HTTP_PROXY",
1826
+ "HTTPS_PROXY",
1827
+ "NO_PROXY",
1828
+ "http_proxy",
1829
+ "https_proxy",
1830
+ "no_proxy",
1831
+ "NODE_EXTRA_CA_CERTS",
1832
+ "SSL_CERT_FILE",
1833
+ "SSL_CERT_DIR"
1834
+ ];
1835
+ var LAUNCH_HOST_ENV_ALLOW_PREFIXES = [
1836
+ // LC_ALL, LC_CTYPE, LC_TIME, … — the locale family, alongside LANG.
1837
+ "LC_"
1838
+ ];
1839
+ var LAUNCH_PANE_ENV_FORWARDED = ["TERM", "TMUX", "TMUX_PANE"];
1840
+ function isAllowedLaunchHostVar(name) {
1841
+ return LAUNCH_HOST_ENV_ALLOW_LIST.includes(name) || LAUNCH_HOST_ENV_ALLOW_PREFIXES.some((prefix) => name.startsWith(prefix));
1842
+ }
1843
+ function pickLaunchHostEnv(env) {
1844
+ const picked = {};
1845
+ for (const name of Object.keys(env).sort()) {
1846
+ const value = env[name];
1847
+ if (value && isAllowedLaunchHostVar(name)) picked[name] = value;
1848
+ }
1849
+ return picked;
1850
+ }
1851
+ function launchShell(hostEnv) {
1852
+ const shell = hostEnv.SHELL;
1853
+ if (shell && path7.isAbsolute(shell) && /^(bash|zsh)$/.test(path7.basename(shell))) return shell;
1854
+ return "/bin/sh";
1855
+ }
1856
+ function buildLaunchCommand(envFilePath, dir, command, logShipper, hostEnv) {
1857
+ const sourced = `. ${quoteShellValue(envFilePath)} && `;
1858
+ const script = logShipper ? `${sourced}{ ${command}; } 2>&1 | NODE_OPTIONS= node ${quoteShellValue(logShipper)}` : `${sourced}${command}`;
1859
+ const forwarded = LAUNCH_PANE_ENV_FORWARDED.map((name) => `${name}="$${name}"`).join(" ");
1860
+ return `cd ${quoteShellValue(dir)} && env -i ${forwarded} ${quoteShellValue(launchShell(hostEnv))} -c ${quoteShellValue(script)}`;
1861
+ }
1862
+ function envFileKeys(content) {
1863
+ const keys = [];
1864
+ for (const line of content.split("\n")) {
1865
+ const match = /^export ([A-Za-z_][A-Za-z0-9_]*)=/.exec(line);
1866
+ if (match) keys.push(match[1]);
1867
+ }
1868
+ return keys;
1869
+ }
1870
+ function parseEnvFile(content) {
1871
+ const env = {};
1872
+ const entry = /^export ([A-Za-z_][A-Za-z0-9_]*)='((?:[^']|'\\'')*)'$/gm;
1873
+ for (const match of content.matchAll(entry)) {
1874
+ env[match[1]] = match[2].replace(/'\\''/g, "'");
1875
+ }
1876
+ return env;
1701
1877
  }
1702
- function buildLaunchCommand(envFilePath, dir, command, logShipper) {
1703
- const base = `cd ${quoteShellValue(dir)} && source ${quoteShellValue(envFilePath)} && `;
1704
- if (!logShipper) return `${base}${command}`;
1705
- return `${base}{ ${command}; } 2>&1 | NODE_OPTIONS= node ${quoteShellValue(logShipper)}`;
1878
+ var SECRET_KEY_RE = /TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|COOKIE|AUTH/i;
1879
+ var PRINTABLE_VALUE_MAX = 80;
1880
+ function diffEnvFiles(before, after) {
1881
+ const show = (key, value) => {
1882
+ if (SECRET_KEY_RE.test(key)) return "(secret)";
1883
+ if (value.length > PRINTABLE_VALUE_MAX) return `(${value.length} chars)`;
1884
+ return JSON.stringify(value);
1885
+ };
1886
+ const changes = [];
1887
+ const keys = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort();
1888
+ for (const key of keys) {
1889
+ const was = before[key];
1890
+ const now = after[key];
1891
+ if (was === void 0 && now !== void 0) {
1892
+ changes.push({ key, kind: "added", detail: `+ ${key} = ${show(key, now)}` });
1893
+ } else if (was !== void 0 && now === void 0) {
1894
+ changes.push({ key, kind: "removed", detail: `- ${key} (was ${show(key, was)})` });
1895
+ } else if (was !== void 0 && now !== void 0 && was !== now) {
1896
+ const detail = SECRET_KEY_RE.test(key) ? `~ ${key} (secret; value changed)` : `~ ${key}: ${show(key, was)} \u2192 ${show(key, now)}`;
1897
+ changes.push({ key, kind: "changed", detail });
1898
+ }
1899
+ }
1900
+ return changes;
1706
1901
  }
1707
1902
  async function waitForPort(host, port, timeoutMs, intervalMs = 500) {
1708
1903
  const deadline = Date.now() + timeoutMs;
@@ -1730,13 +1925,13 @@ function tryConnect(host, port, timeoutMs) {
1730
1925
  }
1731
1926
 
1732
1927
  // packages/mesh-cli/src/commands/local/dev-local.ts
1733
- import * as fs10 from "fs";
1734
- import * as path10 from "path";
1928
+ import * as fs11 from "fs";
1929
+ import * as path11 from "path";
1735
1930
 
1736
1931
  // packages/mesh-cli/src/commands/local/mocks.ts
1737
1932
  import { execFileSync as execFileSync4 } from "child_process";
1738
- import * as fs7 from "fs";
1739
- import * as path7 from "path";
1933
+ import * as fs8 from "fs";
1934
+ import * as path8 from "path";
1740
1935
  var EXTERNAL_MODES = ["mock", "local", "remote"];
1741
1936
  var DECL_DOCS = { docs: 'package.json \u2192 "mesh": { "externals": { \u2026 } }' };
1742
1937
  function externalMode(name, decl) {
@@ -1868,8 +2063,8 @@ var dockerProbes = {
1868
2063
  portPublisher: composeFilePortPublisher
1869
2064
  };
1870
2065
  async function composeExternalUp(appRoot, sessionName, name, decl, probes = dockerProbes) {
1871
- const composeFile = path7.resolve(appRoot, decl.compose);
1872
- if (!fs7.existsSync(composeFile)) {
2066
+ const composeFile = path8.resolve(appRoot, decl.compose);
2067
+ if (!fs8.existsSync(composeFile)) {
1873
2068
  throw new MeshCliError(`External '${name}': compose file not found at ${composeFile}.`, {
1874
2069
  remediation: { docs: 'package.json \u2192 "mesh": { "externals": { \u2026 } }' }
1875
2070
  });
@@ -2007,10 +2202,10 @@ function composeExternalsDown(refs) {
2007
2202
  }
2008
2203
  }
2009
2204
  }
2010
- var SECRET_KEY_RE = /key|secret|password|token/i;
2205
+ var SECRET_KEY_RE2 = /key|secret|password|token/i;
2011
2206
  function readLocalMocks(appRoot) {
2012
2207
  try {
2013
- const pkg = JSON.parse(fs7.readFileSync(path7.join(appRoot, "package.json"), "utf-8"));
2208
+ const pkg = JSON.parse(fs8.readFileSync(path8.join(appRoot, "package.json"), "utf-8"));
2014
2209
  const mocks = pkg?.mesh?.mocks;
2015
2210
  const externals = pkg?.mesh?.externals;
2016
2211
  return {
@@ -2105,12 +2300,12 @@ function externalDocs(decl) {
2105
2300
  return typeof decl.docs === "string" ? { url: decl.docs } : decl.docs;
2106
2301
  }
2107
2302
  function localProbeFile(tenant, app, external) {
2108
- return path7.join(localProbesDir(), `${tenant}-${app}-${external}.json`);
2303
+ return path8.join(localProbesDir(), `${tenant}-${app}-${external}.json`);
2109
2304
  }
2110
2305
  function localProbesRemove(files) {
2111
2306
  for (const file of files ?? []) {
2112
2307
  try {
2113
- fs7.rmSync(file, { force: true });
2308
+ fs8.rmSync(file, { force: true });
2114
2309
  } catch {
2115
2310
  }
2116
2311
  }
@@ -2146,7 +2341,7 @@ async function seedLocalMock(args) {
2146
2341
  await upsertLocalSecret(secretName, value);
2147
2342
  await upsertLocalSecret(
2148
2343
  `${secretName}/.config`,
2149
- Object.fromEntries(Object.entries(value).filter(([key]) => !SECRET_KEY_RE.test(key)))
2344
+ Object.fromEntries(Object.entries(value).filter(([key]) => !SECRET_KEY_RE2.test(key)))
2150
2345
  );
2151
2346
  const { SSMClient: SSMClient2, PutParameterCommand } = await import("@aws-sdk/client-ssm");
2152
2347
  const ssm = new SSMClient2(LOCAL_AWS_CONFIG);
@@ -2179,7 +2374,7 @@ async function seedLocalMock(args) {
2179
2374
  fields: Object.fromEntries(
2180
2375
  Object.keys(value).map((key) => [
2181
2376
  key,
2182
- { type: "field", name: key, description: "", secret: SECRET_KEY_RE.test(key), optional: false }
2377
+ { type: "field", name: key, description: "", secret: SECRET_KEY_RE2.test(key), optional: false }
2183
2378
  ])
2184
2379
  ),
2185
2380
  keyedBy: null
@@ -2199,7 +2394,7 @@ async function seedLocalMock(args) {
2199
2394
  );
2200
2395
  const probeFile = localProbeFile(tenant, app, decl.external);
2201
2396
  if (probe) {
2202
- fs7.writeFileSync(
2397
+ fs8.writeFileSync(
2203
2398
  probeFile,
2204
2399
  JSON.stringify(
2205
2400
  [
@@ -2233,7 +2428,7 @@ async function seedLocalMock(args) {
2233
2428
  }
2234
2429
 
2235
2430
  // packages/mesh-cli/src/commands/local/seed-zitadel.ts
2236
- import * as fs9 from "fs";
2431
+ import * as fs10 from "fs";
2237
2432
 
2238
2433
  // packages/mesh-cli/src/commands/local/act-as-local.ts
2239
2434
  import { createPublicKey } from "node:crypto";
@@ -2276,12 +2471,12 @@ function actAsEnv(publication, tenant, env, surface) {
2276
2471
 
2277
2472
  // packages/mesh-cli/src/commands/local/seed-zitadel.ts
2278
2473
  import * as os2 from "os";
2279
- import * as path9 from "path";
2474
+ import * as path10 from "path";
2280
2475
 
2281
2476
  // packages/mesh-cli/src/commands/local/mock-idps.ts
2282
2477
  import { execFileSync as execFileSync5 } from "child_process";
2283
- import * as fs8 from "fs";
2284
- import * as path8 from "path";
2478
+ import * as fs9 from "fs";
2479
+ import * as path9 from "path";
2285
2480
  var MOCK_SAML_PORT = 4569;
2286
2481
  var MOCK_OIDC_PORT = 5556;
2287
2482
  var MOCK_OIDC_SERVICE = "mock-oidc";
@@ -2314,13 +2509,13 @@ var mockOidcEndpoints = () => ({
2314
2509
  token: `http://${MOCK_OIDC_SERVICE}:${MOCK_OIDC_PORT}/dex/token`,
2315
2510
  user: `http://${MOCK_OIDC_SERVICE}:${MOCK_OIDC_PORT}/dex/userinfo`
2316
2511
  });
2317
- var keyDir = () => path8.join(meshCacheDir("mesh-local"), "mock-idp");
2512
+ var keyDir = () => path9.join(meshCacheDir("mesh-local"), "mock-idp");
2318
2513
  function ensureMockSamlSigningKey() {
2319
2514
  const dir = keyDir();
2320
- const certPath = path8.join(dir, "cert.pem");
2321
- const keyPath = path8.join(dir, "key.pem");
2322
- if (!fs8.existsSync(certPath) || !fs8.existsSync(keyPath)) {
2323
- fs8.mkdirSync(dir, { recursive: true });
2515
+ const certPath = path9.join(dir, "cert.pem");
2516
+ const keyPath = path9.join(dir, "key.pem");
2517
+ if (!fs9.existsSync(certPath) || !fs9.existsSync(keyPath)) {
2518
+ fs9.mkdirSync(dir, { recursive: true });
2324
2519
  try {
2325
2520
  execFileSync5(
2326
2521
  "openssl",
@@ -2351,8 +2546,8 @@ function ensureMockSamlSigningKey() {
2351
2546
  }
2352
2547
  }
2353
2548
  return {
2354
- publicKeyB64: fs8.readFileSync(certPath).toString("base64"),
2355
- privateKeyB64: fs8.readFileSync(keyPath).toString("base64")
2549
+ publicKeyB64: fs9.readFileSync(certPath).toString("base64"),
2550
+ privateKeyB64: fs9.readFileSync(keyPath).toString("base64")
2356
2551
  };
2357
2552
  }
2358
2553
  var mockIdpComposeEnv = () => {
@@ -2449,10 +2644,10 @@ function buildCliAppPayload() {
2449
2644
  };
2450
2645
  }
2451
2646
  function readSeederPat() {
2452
- const tmp = path9.join(fs9.mkdtempSync(path9.join(os2.tmpdir(), "mesh-zitadel-")), "pat.txt");
2647
+ const tmp = path10.join(fs10.mkdtempSync(path10.join(os2.tmpdir(), "mesh-zitadel-")), "pat.txt");
2453
2648
  try {
2454
2649
  compose(["cp", "zitadel:/machinekey/pat.txt", tmp]);
2455
- const pat = fs9.readFileSync(tmp, "utf-8").trim();
2650
+ const pat = fs10.readFileSync(tmp, "utf-8").trim();
2456
2651
  if (!pat) throw new Error("empty PAT file");
2457
2652
  return pat;
2458
2653
  } catch {
@@ -2461,7 +2656,7 @@ function readSeederPat() {
2461
2656
  { remediation: { command: "mesh stop --destroy && mesh start" } }
2462
2657
  );
2463
2658
  } finally {
2464
- fs9.rmSync(path9.dirname(tmp), { recursive: true, force: true });
2659
+ fs10.rmSync(path10.dirname(tmp), { recursive: true, force: true });
2465
2660
  }
2466
2661
  }
2467
2662
  async function api(pat, method, apiPath, body, orgId) {
@@ -2565,11 +2760,11 @@ async function ensureTestUsers(pat) {
2565
2760
  }
2566
2761
  }
2567
2762
  function hubAuthPath() {
2568
- return path9.join(meshCacheDir("mesh-local"), "hub-auth.json");
2763
+ return path10.join(meshCacheDir("mesh-local"), "hub-auth.json");
2569
2764
  }
2570
2765
  function readHubAuth() {
2571
2766
  try {
2572
- const parsed = JSON.parse(fs9.readFileSync(hubAuthPath(), "utf-8"));
2767
+ const parsed = JSON.parse(fs10.readFileSync(hubAuthPath(), "utf-8"));
2573
2768
  if (parsed?.clientId && parsed?.clientSecret && parsed?.cookieSecret) return parsed;
2574
2769
  return null;
2575
2770
  } catch {
@@ -2709,7 +2904,7 @@ async function seedHubAuth(pat) {
2709
2904
  clientSecret: app.clientSecret ?? persisted?.clientSecret ?? "",
2710
2905
  cookieSecret: persistedCookie ?? (await import("crypto")).randomBytes(16).toString("hex")
2711
2906
  };
2712
- fs9.writeFileSync(hubAuthPath(), JSON.stringify(config, null, 2), { mode: 384 });
2907
+ fs10.writeFileSync(hubAuthPath(), JSON.stringify(config, null, 2), { mode: 384 });
2713
2908
  return config;
2714
2909
  }
2715
2910
  async function publishHubAuthzPointer(projectId, orgId, awsConfig) {
@@ -3331,11 +3526,11 @@ function localAppNamespace(tenant, appName) {
3331
3526
  return `${tenant}-${LOCAL_ENV}-${appName}`;
3332
3527
  }
3333
3528
  function logShipperPath() {
3334
- return path10.join(findPackageRoot(), "assets", "log-shipper.mjs");
3529
+ return path11.join(findPackageRoot(), "assets", "log-shipper.mjs");
3335
3530
  }
3336
3531
  function autoInstrumentationEnv(appRoot, src) {
3337
- const pkgDir = path10.join("node_modules", "@opentelemetry", "auto-instrumentations-node");
3338
- const present = fs10.existsSync(path10.join(appRoot, src, pkgDir)) || fs10.existsSync(path10.join(appRoot, pkgDir));
3532
+ const pkgDir = path11.join("node_modules", "@opentelemetry", "auto-instrumentations-node");
3533
+ const present = fs11.existsSync(path11.join(appRoot, src, pkgDir)) || fs11.existsSync(path11.join(appRoot, pkgDir));
3339
3534
  if (!present) return {};
3340
3535
  return {
3341
3536
  NODE_OPTIONS: { value: "--import @opentelemetry/auto-instrumentations-node/register" },
@@ -3391,26 +3586,26 @@ function localPlatformEnv(tenant, appName, serviceName) {
3391
3586
  return env;
3392
3587
  }
3393
3588
  function isDagsterWorkspaceRoot(appRoot) {
3394
- if (fs10.existsSync(path10.join(appRoot, "dg.toml"))) return true;
3395
- const pyproject = path10.join(appRoot, "pyproject.toml");
3396
- if (!fs10.existsSync(pyproject)) return false;
3589
+ if (fs11.existsSync(path11.join(appRoot, "dg.toml"))) return true;
3590
+ const pyproject = path11.join(appRoot, "pyproject.toml");
3591
+ if (!fs11.existsSync(pyproject)) return false;
3397
3592
  try {
3398
- return /^\s*\[tool\.dg[\].]/m.test(fs10.readFileSync(pyproject, "utf-8"));
3593
+ return /^\s*\[tool\.dg[\].]/m.test(fs11.readFileSync(pyproject, "utf-8"));
3399
3594
  } catch {
3400
3595
  return false;
3401
3596
  }
3402
3597
  }
3403
3598
  function resolveDgBinary(appRoot) {
3404
3599
  for (const candidate of ["deployments/local/.venv/bin/dg", ".venv/bin/dg"]) {
3405
- if (fs10.existsSync(path10.join(appRoot, candidate))) return candidate;
3600
+ if (fs11.existsSync(path11.join(appRoot, candidate))) return candidate;
3406
3601
  }
3407
3602
  return "dg";
3408
3603
  }
3409
3604
  function hasDevScript(dir) {
3410
- const pkgPath = path10.join(dir, "package.json");
3411
- if (!fs10.existsSync(pkgPath)) return false;
3605
+ const pkgPath = path11.join(dir, "package.json");
3606
+ if (!fs11.existsSync(pkgPath)) return false;
3412
3607
  try {
3413
- const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
3608
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
3414
3609
  return typeof pkg?.scripts?.dev === "string";
3415
3610
  } catch {
3416
3611
  return false;
@@ -3418,14 +3613,14 @@ function hasDevScript(dir) {
3418
3613
  }
3419
3614
  function detectLocalServices(appRoot) {
3420
3615
  const services = {};
3421
- for (const entry of fs10.readdirSync(appRoot, { withFileTypes: true })) {
3616
+ for (const entry of fs11.readdirSync(appRoot, { withFileTypes: true })) {
3422
3617
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
3423
- if (hasDevScript(path10.join(appRoot, entry.name))) {
3618
+ if (hasDevScript(path11.join(appRoot, entry.name))) {
3424
3619
  services[entry.name] = { src: entry.name };
3425
3620
  }
3426
3621
  }
3427
3622
  if (Object.keys(services).length === 0 && hasDevScript(appRoot)) {
3428
- services[path10.basename(appRoot)] = { src: "." };
3623
+ services[path11.basename(appRoot)] = { src: "." };
3429
3624
  }
3430
3625
  return services;
3431
3626
  }
@@ -3439,15 +3634,15 @@ function buildLocalDevOutput(appRoot, tenant, opts = {}) {
3439
3634
  { remediation: { command: "mesh create-app" } }
3440
3635
  );
3441
3636
  }
3442
- let appName = path10.basename(appRoot);
3637
+ let appName = path11.basename(appRoot);
3443
3638
  try {
3444
- const pkg = JSON.parse(fs10.readFileSync(path10.join(appRoot, "package.json"), "utf-8"));
3639
+ const pkg = JSON.parse(fs11.readFileSync(path11.join(appRoot, "package.json"), "utf-8"));
3445
3640
  if (typeof pkg?.name === "string" && pkg.name) appName = pkg.name.replace(/^@[^/]+\//, "");
3446
3641
  } catch {
3447
3642
  }
3448
- const pulumiYaml = path10.join(appRoot, "Pulumi.yaml");
3449
- if (fs10.existsSync(pulumiYaml)) {
3450
- const match = fs10.readFileSync(pulumiYaml, "utf-8").match(/^name:\s*(.+)$/m);
3643
+ const pulumiYaml = path11.join(appRoot, "Pulumi.yaml");
3644
+ if (fs11.existsSync(pulumiYaml)) {
3645
+ const match = fs11.readFileSync(pulumiYaml, "utf-8").match(/^name:\s*(.+)$/m);
3451
3646
  if (match?.[1]?.trim()) appName = match[1].trim();
3452
3647
  }
3453
3648
  const services = {};
@@ -3497,7 +3692,7 @@ function buildLocalDevOutput(appRoot, tenant, opts = {}) {
3497
3692
  command: [resolveDgBinary(appRoot), "dev", "-h", "127.0.0.1", "-p", '"$PORT"'],
3498
3693
  env: {
3499
3694
  PORT: { value: String(port) },
3500
- DAGSTER_HOME: { value: path10.join(appRoot, ".dagster") },
3695
+ DAGSTER_HOME: { value: path11.join(appRoot, ".dagster") },
3501
3696
  ...localPlatformEnv(tenant, appName, "dagster")
3502
3697
  }
3503
3698
  };
@@ -3529,23 +3724,23 @@ function buildLocalDevOutput(appRoot, tenant, opts = {}) {
3529
3724
  };
3530
3725
  }
3531
3726
  function hasStackBacking(appRoot) {
3532
- const hasOwn = fs10.existsSync(path10.join(appRoot, "Pulumi.yaml")) || fs10.readdirSync(appRoot).some((f) => /^Pulumi\..+\.yaml$/.test(f));
3727
+ const hasOwn = fs11.existsSync(path11.join(appRoot, "Pulumi.yaml")) || fs11.readdirSync(appRoot).some((f) => /^Pulumi\..+\.yaml$/.test(f));
3533
3728
  if (hasOwn) return true;
3534
- let dir = path10.dirname(appRoot);
3729
+ let dir = path11.dirname(appRoot);
3535
3730
  while (true) {
3536
- if (fs10.existsSync(path10.join(dir, "Pulumi.yaml"))) return true;
3537
- if (fs10.existsSync(path10.join(dir, ".git")) || fs10.existsSync(path10.join(dir, "pnpm-workspace.yaml"))) {
3538
- return fs10.existsSync(path10.join(dir, "Pulumi.yaml"));
3731
+ if (fs11.existsSync(path11.join(dir, "Pulumi.yaml"))) return true;
3732
+ if (fs11.existsSync(path11.join(dir, ".git")) || fs11.existsSync(path11.join(dir, "pnpm-workspace.yaml"))) {
3733
+ return fs11.existsSync(path11.join(dir, "Pulumi.yaml"));
3539
3734
  }
3540
- const parent = path10.dirname(dir);
3735
+ const parent = path11.dirname(dir);
3541
3736
  if (parent === dir) return false;
3542
3737
  dir = parent;
3543
3738
  }
3544
3739
  }
3545
3740
  function detectLocalTenant(appRoot) {
3546
- for (const file of fs10.readdirSync(appRoot)) {
3741
+ for (const file of fs11.readdirSync(appRoot)) {
3547
3742
  if (!/^Pulumi\..*\.yaml$/.test(file)) continue;
3548
- const match = fs10.readFileSync(path10.join(appRoot, file), "utf-8").match(/^\s*mesh:tenant:\s*["']?([A-Za-z0-9-]+)["']?\s*$/m);
3743
+ const match = fs11.readFileSync(path11.join(appRoot, file), "utf-8").match(/^\s*mesh:tenant:\s*["']?([A-Za-z0-9-]+)["']?\s*$/m);
3549
3744
  if (match) return match[1];
3550
3745
  }
3551
3746
  const recorded = findMeshJson(appRoot);
@@ -3566,8 +3761,8 @@ async function ensureLocalPlatformRunning() {
3566
3761
  import { spawnSync } from "node:child_process";
3567
3762
  import { createHash as createHash2 } from "node:crypto";
3568
3763
  import { createRequire } from "node:module";
3569
- import fs11 from "node:fs";
3570
- import path11 from "node:path";
3764
+ import fs12 from "node:fs";
3765
+ import path12 from "node:path";
3571
3766
  import { pathToFileURL } from "node:url";
3572
3767
  var LOCAL_APP_SPICEDB_IMAGE = "authzed/spicedb:v1.42.1";
3573
3768
  var LOCAL_APP_SPICEDB_KEY = "local-dev-key";
@@ -3579,7 +3774,7 @@ var HTTP_PORT_BASE = 18100;
3579
3774
  var PORT_SLOTS = 100;
3580
3775
  function readLocalAuthzConfig(appRoot) {
3581
3776
  try {
3582
- const pkg = JSON.parse(fs11.readFileSync(path11.join(appRoot, "package.json"), "utf-8"));
3777
+ const pkg = JSON.parse(fs12.readFileSync(path12.join(appRoot, "package.json"), "utf-8"));
3583
3778
  const schema = pkg?.mesh?.authz?.schema;
3584
3779
  if (typeof schema !== "string" || !schema.trim()) return null;
3585
3780
  return { schema: schema.trim(), prebuilt: pkg?.mesh?.authz?.metadata !== void 0 };
@@ -3786,7 +3981,7 @@ var tsxImporter = async (specifier, parentFile) => {
3786
3981
  api2 = createRequire(parentFile).resolve("tsx/esm/api");
3787
3982
  } catch {
3788
3983
  throw new MeshCliError(
3789
- `Cannot load the app's authz schema: \`tsx\` is not installed in ${path11.dirname(parentFile)}. Add it to the app's devDependencies (\`pnpm add -D tsx\`).`
3984
+ `Cannot load the app's authz schema: \`tsx\` is not installed in ${path12.dirname(parentFile)}. Add it to the app's devDependencies (\`pnpm add -D tsx\`).`
3790
3985
  );
3791
3986
  }
3792
3987
  const { tsImport } = await import(pathToFileURL(api2).href);
@@ -3797,8 +3992,8 @@ var unwrapDefault = (mod) => {
3797
3992
  return d && typeof d === "object" && "default" in d && !("namespace" in d) ? d.default : d;
3798
3993
  };
3799
3994
  async function loadAppAuthz(appRoot, schemaRel, importer = tsxImporter) {
3800
- const file = path11.resolve(appRoot, schemaRel);
3801
- if (!fs11.existsSync(file)) {
3995
+ const file = path12.resolve(appRoot, schemaRel);
3996
+ if (!fs12.existsSync(file)) {
3802
3997
  throw new MeshCliError(`mesh.authz.schema names ${schemaRel}, which does not exist under ${appRoot}.`);
3803
3998
  }
3804
3999
  const schema = unwrapDefault(await importer(pathToFileURL(file).href, file));
@@ -3887,8 +4082,8 @@ async function publishLocalAppAuthz(args, deps = {}) {
3887
4082
 
3888
4083
  // packages/mesh-cli/src/commands/local/publish-docs.ts
3889
4084
  import { createHash as createHash3 } from "node:crypto";
3890
- import * as fs12 from "node:fs";
3891
- import * as path12 from "node:path";
4085
+ import * as fs13 from "node:fs";
4086
+ import * as path13 from "node:path";
3892
4087
  import { execFileSync as execFileSync6 } from "node:child_process";
3893
4088
  import { pathToFileURL as pathToFileURL2 } from "node:url";
3894
4089
 
@@ -4041,14 +4236,14 @@ function zedOf(value) {
4041
4236
  if (typeof zed !== "string" || !zed.trim()) throw new Error("the spicedbSchema export returned no zed schema");
4042
4237
  return zed;
4043
4238
  }
4044
- async function spicedbHttp(path21, body) {
4045
- const res = await fetch(`${HOST_HTTP}${path21}`, {
4239
+ async function spicedbHttp(path23, body) {
4240
+ const res = await fetch(`${HOST_HTTP}${path23}`, {
4046
4241
  method: "POST",
4047
4242
  headers: { Authorization: `Bearer ${LOCAL_SPICEDB_KEY}`, "Content-Type": "application/json" },
4048
4243
  body: JSON.stringify(body)
4049
4244
  });
4050
4245
  const parsed = await res.json().catch(() => ({}));
4051
- if (!res.ok) throw Object.assign(new Error(`SpiceDB ${path21}: ${res.status} ${parsed.message ?? ""}`.trim()), { status: res.status });
4246
+ if (!res.ok) throw Object.assign(new Error(`SpiceDB ${path23}: ${res.status} ${parsed.message ?? ""}`.trim()), { status: res.status });
4052
4247
  return parsed;
4053
4248
  }
4054
4249
  async function readLocalSpicedbSchema() {
@@ -4185,7 +4380,7 @@ function readLocalPublishDeclaration(pkg, appRoot) {
4185
4380
  }
4186
4381
  if (typeof docs?.config === "string" && docs.config) {
4187
4382
  out.docs = {
4188
- config: path12.resolve(appRoot, docs.config),
4383
+ config: path13.resolve(appRoot, docs.config),
4189
4384
  actAs: docs.actAs === true,
4190
4385
  ...sandbox?.success ? { sandbox: sandbox.data } : {},
4191
4386
  ...typeof docs.local === "string" && docs.local.includes("#") ? { local: docs.local } : {},
@@ -4215,7 +4410,7 @@ function readLocalPublishDeclaration(pkg, appRoot) {
4215
4410
  }
4216
4411
  async function callExport(appRoot, spec, arg) {
4217
4412
  const [modulePath, exportName] = spec.split("#");
4218
- const file = path12.resolve(appRoot, modulePath);
4413
+ const file = path13.resolve(appRoot, modulePath);
4219
4414
  let mod;
4220
4415
  if (/\.m?js$/.test(file)) {
4221
4416
  mod = await import(pathToFileURL2(file).href);
@@ -4394,12 +4589,12 @@ async function publishLocalDocs(input) {
4394
4589
  ...local.events ? { events: local.events } : {}
4395
4590
  });
4396
4591
  result.problems.push(...built.problems);
4397
- const config = parseDocsConfig(fs12.readFileSync(docs.config, "utf8"), docs.config);
4592
+ const config = parseDocsConfig(fs13.readFileSync(docs.config, "utf8"), docs.config);
4398
4593
  const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
4399
4594
  const s3 = new S3Client({ ...awsConfig, forcePathStyle: true });
4400
4595
  const specKey = `api-docs/${app}/openapi.json`;
4401
4596
  const sourceKey = docsSourceKey(app);
4402
- const specText = fs12.readFileSync(built.specPath, "utf8");
4597
+ const specText = fs13.readFileSync(built.specPath, "utf8");
4403
4598
  await s3.send(new PutObjectCommand({ Bucket: ARTIFACTS_BUCKET, Key: specKey, Body: specText, ContentType: "application/json" }));
4404
4599
  await s3.send(new PutObjectCommand({ Bucket: ARTIFACTS_BUCKET, Key: sourceKey, Body: built.json, ContentType: "application/json" }));
4405
4600
  if (docs.actAs) {
@@ -4565,8 +4760,8 @@ async function ensureLocalApiKey(input) {
4565
4760
 
4566
4761
  // packages/mesh-cli/src/commands/local/docker-runner.ts
4567
4762
  import { execFileSync as execFileSync7 } from "child_process";
4568
- import * as fs13 from "fs";
4569
- import * as path13 from "path";
4763
+ import * as fs14 from "fs";
4764
+ import * as path14 from "path";
4570
4765
  var DEV_RUNNER_IMAGE = "node:22-bookworm@sha256:5647be709086c696ff32edaaf1c70cd26d1da6ab2b39c32f3c7b4c4a31957e37";
4571
4766
  function sessionDir(sessionName) {
4572
4767
  return meshCacheDir("mesh-local", "dev", sessionName);
@@ -4589,8 +4784,8 @@ function writeDevCompose(sessionName, appRoot, services) {
4589
4784
  ];
4590
4785
  for (const [name, service] of Object.entries(services)) {
4591
4786
  const env = service.env ?? {};
4592
- fs13.writeFileSync(path13.join(dir, `${name}.env`), renderDockerEnvFile(env), { mode: 384 });
4593
- const workdir = path13.posix.join("/workspace", service.src === "." ? "" : service.src);
4787
+ fs14.writeFileSync(path14.join(dir, `${name}.env`), renderDockerEnvFile(env), { mode: 384 });
4788
+ const workdir = path14.posix.join("/workspace", service.src === "." ? "" : service.src);
4594
4789
  const ship = env.OTEL_RESOURCE_ATTRIBUTES ? ` 2>&1 | NODE_OPTIONS= node /mesh-assets/log-shipper.mjs` : "";
4595
4790
  blocks.push(
4596
4791
  ` ${name}:`,
@@ -4599,7 +4794,7 @@ function writeDevCompose(sessionName, appRoot, services) {
4599
4794
  ` working_dir: ${workdir}`,
4600
4795
  ` volumes:`,
4601
4796
  ` - '${appRoot}:/workspace'`,
4602
- ` - '${path13.dirname(shipper)}:/mesh-assets:ro'`,
4797
+ ` - '${path14.dirname(shipper)}:/mesh-assets:ro'`,
4603
4798
  ` environment:`,
4604
4799
  // node_modules is the HOST's bind-mounted install — pnpm inside the
4605
4800
  // container must never "verify" it and try a purge/reinstall (pnpm
@@ -4615,12 +4810,12 @@ function writeDevCompose(sessionName, appRoot, services) {
4615
4810
  ` restart: unless-stopped`
4616
4811
  );
4617
4812
  }
4618
- const composePath = path13.join(dir, "compose.yml");
4619
- fs13.writeFileSync(composePath, blocks.join("\n") + "\n");
4813
+ const composePath = path14.join(dir, "compose.yml");
4814
+ fs14.writeFileSync(composePath, blocks.join("\n") + "\n");
4620
4815
  return composePath;
4621
4816
  }
4622
4817
  function composeArgs(sessionName, args) {
4623
- return ["compose", "-p", sessionName, "-f", path13.join(sessionDir(sessionName), "compose.yml"), ...args];
4818
+ return ["compose", "-p", sessionName, "-f", path14.join(sessionDir(sessionName), "compose.yml"), ...args];
4624
4819
  }
4625
4820
  function dockerDevUp(sessionName) {
4626
4821
  execFileSync7("docker", composeArgs(sessionName, ["up", "-d"]), {
@@ -4693,10 +4888,173 @@ function rewriteClusterHostsToLocal(devOutput) {
4693
4888
  }
4694
4889
 
4695
4890
  // packages/mesh-cli/src/commands/dev-doctor.ts
4696
- import { execFileSync as execFileSync8 } from "node:child_process";
4697
- import * as fs14 from "node:fs";
4891
+ import { execFileSync as execFileSync9 } from "node:child_process";
4892
+ import * as fs16 from "node:fs";
4698
4893
  import * as net2 from "node:net";
4699
- import * as path14 from "node:path";
4894
+ import * as path16 from "node:path";
4895
+
4896
+ // packages/mesh-cli/src/commands/dev-env-leaks.ts
4897
+ import { execFileSync as execFileSync8 } from "node:child_process";
4898
+ import * as fs15 from "node:fs";
4899
+ import * as path15 from "node:path";
4900
+ var LEAK_NAME_PATTERNS = [
4901
+ /^WORKSPACES_/,
4902
+ /^AWS_/,
4903
+ /^TEMPORAL_/,
4904
+ /^VCS_/,
4905
+ /_TOKEN$/,
4906
+ /_SECRET$/,
4907
+ /_KEY$/,
4908
+ /_PASSWORD$/
4909
+ ];
4910
+ function matchesLeakPattern(name) {
4911
+ return LEAK_NAME_PATTERNS.some((re) => re.test(name));
4912
+ }
4913
+ function parseTmuxEnvironment(text) {
4914
+ const out = {};
4915
+ for (const line of text.split("\n")) {
4916
+ if (!line || line.startsWith("-")) continue;
4917
+ const eq = line.indexOf("=");
4918
+ if (eq <= 0) continue;
4919
+ const name = line.slice(0, eq);
4920
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
4921
+ out[name] = line.slice(eq + 1);
4922
+ }
4923
+ return out;
4924
+ }
4925
+ function classifyLeak(name, envFileKeysByService, cleanLaunch) {
4926
+ const services = Object.keys(envFileKeysByService).sort();
4927
+ const shadowedIn = services.filter((svc) => envFileKeysByService[svc].includes(name));
4928
+ const unshadowedIn = services.filter((svc) => !shadowedIn.includes(svc));
4929
+ if (cleanLaunch === false) {
4930
+ if (services.length > 0 && unshadowedIn.length === 0) return { fate: "shadowed", shadowedIn, unshadowedIn };
4931
+ return { fate: "reaches", shadowedIn, unshadowedIn };
4932
+ }
4933
+ if (isAllowedLaunchHostVar(name)) return { fate: "reaches", shadowedIn, unshadowedIn };
4934
+ if (services.length === 0) return { fate: "dropped", shadowedIn, unshadowedIn };
4935
+ if (unshadowedIn.length === 0) return { fate: "shadowed", shadowedIn, unshadowedIn };
4936
+ if (shadowedIn.length === 0) return { fate: "dropped", shadowedIn, unshadowedIn };
4937
+ return { fate: "partial", shadowedIn, unshadowedIn };
4938
+ }
4939
+ function collectLeakCandidates(input) {
4940
+ const names = /* @__PURE__ */ new Map();
4941
+ for (const name of Object.keys(input.tmuxGlobal)) {
4942
+ if (matchesLeakPattern(name)) names.set(name, ["tmux-global"]);
4943
+ }
4944
+ for (const [name, value] of Object.entries(input.shell)) {
4945
+ if (value === void 0 || !matchesLeakPattern(name)) continue;
4946
+ const sources = names.get(name) ?? [];
4947
+ if (!sources.includes("shell")) sources.push("shell");
4948
+ names.set(name, sources);
4949
+ }
4950
+ return [...names.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, sources]) => ({
4951
+ name,
4952
+ sources,
4953
+ ...classifyLeak(name, input.envFileKeysByService, input.cleanLaunch)
4954
+ }));
4955
+ }
4956
+ function redactValue(value) {
4957
+ if (value.length === 0) return "(empty)";
4958
+ if (value.length <= 8) return "\u2022\u2022\u2022\u2022";
4959
+ return `${value.slice(0, 4)}\u2026(${value.length} chars)`;
4960
+ }
4961
+ var FATE_LABEL = {
4962
+ reaches: "REACHES SERVICES",
4963
+ shadowed: "shadowed by the env file",
4964
+ dropped: "dropped by the clean launch",
4965
+ partial: "shadowed for some services, dropped for others"
4966
+ };
4967
+ var SOURCE_LABEL = {
4968
+ "tmux-global": "tmux -g",
4969
+ shell: "shell"
4970
+ };
4971
+ function renderLeakLines(candidates, input) {
4972
+ return candidates.map((c) => {
4973
+ const values = c.sources.map((src) => {
4974
+ const value = src === "tmux-global" ? input.tmuxGlobal[c.name] : input.shell[c.name];
4975
+ return `${SOURCE_LABEL[src]}: ${redactValue(value ?? "")}`;
4976
+ });
4977
+ let fate = FATE_LABEL[c.fate];
4978
+ if (c.fate === "partial") {
4979
+ fate = `shadowed for ${c.shadowedIn.join(", ")}; dropped for ${c.unshadowedIn.join(", ")}`;
4980
+ } else if (c.fate === "reaches" && input.cleanLaunch !== false && isAllowedLaunchHostVar(c.name)) {
4981
+ fate = `${fate} (allow-listed host variable)`;
4982
+ }
4983
+ return `${c.name} [${values.join("; ")}] \u2192 ${fate}`;
4984
+ });
4985
+ }
4986
+ function readTmuxGlobalEnvironment() {
4987
+ try {
4988
+ const text = execFileSync8("tmux", ["show-environment", "-g"], {
4989
+ encoding: "utf8",
4990
+ stdio: ["ignore", "pipe", "ignore"]
4991
+ });
4992
+ return parseTmuxEnvironment(text);
4993
+ } catch {
4994
+ return {};
4995
+ }
4996
+ }
4997
+ function readSessionEnvFileKeys(sessionName, serviceNames) {
4998
+ const dir = getSessionEnvDir(sessionName);
4999
+ const out = {};
5000
+ for (const name of serviceNames) {
5001
+ const file = path15.join(dir, envFileName(name));
5002
+ if (!fs15.existsSync(file)) continue;
5003
+ out[name] = envFileKeys(fs15.readFileSync(file, "utf8"));
5004
+ }
5005
+ return out;
5006
+ }
5007
+ var envLeaksCheck = {
5008
+ id: "env-leaks",
5009
+ title: "Inherited environment",
5010
+ // On demand only: launch preflight has no env files yet, and the launch is
5011
+ // clean regardless — the question this answers is "what would a service
5012
+ // have seen", which is worth asking when something looks wrong, not on
5013
+ // every start.
5014
+ phases: ["ondemand"],
5015
+ async run(ctx) {
5016
+ const state = ctx.sessionState;
5017
+ const services = state?.devOutput.services ? Object.entries(state.devOutput.services).filter(([, svc]) => svc.command && svc.command.length > 0).map(([name]) => name) : [];
5018
+ const input = {
5019
+ tmuxGlobal: readTmuxGlobalEnvironment(),
5020
+ shell: process.env,
5021
+ envFileKeysByService: state ? readSessionEnvFileKeys(ctx.sessionName, services) : {},
5022
+ cleanLaunch: state ? state.cleanEnv === true : null
5023
+ };
5024
+ const candidates = collectLeakCandidates(input);
5025
+ if (candidates.length === 0) {
5026
+ return {
5027
+ status: "ok",
5028
+ summary: "no WORKSPACES_*/AWS_*/TEMPORAL_*/VCS_*/*_TOKEN/*_SECRET/*_KEY in the tmux global env or this shell"
5029
+ };
5030
+ }
5031
+ const detail = renderLeakLines(candidates, input).join("\n ");
5032
+ const reaching = candidates.filter((c) => c.fate === "reaches");
5033
+ if (reaching.length > 0 && input.cleanLaunch === false) {
5034
+ return {
5035
+ status: "warn",
5036
+ summary: `${reaching.length} of ${candidates.length} inherited variable(s) reach the services \u2014 this session predates the clean launch`,
5037
+ detail,
5038
+ remediation: "mesh dev # relaunch: services then start under env -i with only their env file"
5039
+ };
5040
+ }
5041
+ if (reaching.length > 0) {
5042
+ return {
5043
+ status: "warn",
5044
+ summary: `${reaching.length} of ${candidates.length} inherited variable(s) reach the services via the host allow-list`,
5045
+ detail
5046
+ };
5047
+ }
5048
+ const where = state ? "services launched clean" : "no session; the next launch is clean";
5049
+ return {
5050
+ status: "ok",
5051
+ summary: `${candidates.length} inherited variable(s) found, none reach the services (${where}) \u2014 each is shadowed by the env file or dropped by env -i`,
5052
+ detail
5053
+ };
5054
+ }
5055
+ };
5056
+
5057
+ // packages/mesh-cli/src/commands/dev-doctor.ts
4700
5058
  function aggregateStatus(results) {
4701
5059
  if (results.some((r) => r.status === "error")) return "error";
4702
5060
  if (results.some((r) => r.status === "warn")) return "warn";
@@ -4864,9 +5222,9 @@ function parsePortSquatter(lsofOut, psOut) {
4864
5222
  return { pid, command };
4865
5223
  }
4866
5224
  function sharesWorktreeRoot(serviceSrc, repoRoot) {
4867
- const src = path14.resolve(serviceSrc);
4868
- const root = path14.resolve(repoRoot);
4869
- return src === root || src.startsWith(root + path14.sep);
5225
+ const src = path16.resolve(serviceSrc);
5226
+ const root = path16.resolve(repoRoot);
5227
+ return src === root || src.startsWith(root + path16.sep);
4870
5228
  }
4871
5229
  function classifyPortListener(listenerPid, sessionPids) {
4872
5230
  if (listenerPid === null) return "dead";
@@ -4895,7 +5253,7 @@ function collectSessionPids(panePids, psOut) {
4895
5253
  }
4896
5254
  function whoHasPort(port) {
4897
5255
  try {
4898
- const lsof = execFileSync8("lsof", ["-ti", `:${port}`], {
5256
+ const lsof = execFileSync9("lsof", ["-ti", `:${port}`], {
4899
5257
  encoding: "utf8",
4900
5258
  stdio: ["ignore", "pipe", "ignore"]
4901
5259
  });
@@ -4903,7 +5261,7 @@ function whoHasPort(port) {
4903
5261
  if (!pid) return null;
4904
5262
  let ps = "";
4905
5263
  try {
4906
- ps = execFileSync8("ps", ["-o", "comm=", "-p", pid], {
5264
+ ps = execFileSync9("ps", ["-o", "comm=", "-p", pid], {
4907
5265
  encoding: "utf8",
4908
5266
  stdio: ["ignore", "pipe", "ignore"]
4909
5267
  });
@@ -4917,7 +5275,7 @@ function whoHasPort(port) {
4917
5275
  function sessionProcessTree(sessionName) {
4918
5276
  let panePids;
4919
5277
  try {
4920
- const out = execFileSync8(
5278
+ const out = execFileSync9(
4921
5279
  "tmux",
4922
5280
  ["list-panes", "-s", "-t", sessionName, "-F", "#{pane_pid}"],
4923
5281
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -4929,7 +5287,7 @@ function sessionProcessTree(sessionName) {
4929
5287
  if (panePids.length === 0) return /* @__PURE__ */ new Set();
4930
5288
  let psOut = "";
4931
5289
  try {
4932
- psOut = execFileSync8("ps", ["-eo", "pid=,ppid="], {
5290
+ psOut = execFileSync9("ps", ["-eo", "pid=,ppid="], {
4933
5291
  encoding: "utf8",
4934
5292
  stdio: ["ignore", "pipe", "ignore"]
4935
5293
  });
@@ -4938,13 +5296,144 @@ function sessionProcessTree(sessionName) {
4938
5296
  }
4939
5297
  return collectSessionPids(panePids, psOut);
4940
5298
  }
5299
+ function paneProcessQuery(command, args) {
5300
+ return execFileSync9(command, args, {
5301
+ encoding: "utf8",
5302
+ stdio: ["ignore", "pipe", "ignore"]
5303
+ });
5304
+ }
5305
+ function listSessionWindowNames(sessionName, query) {
5306
+ try {
5307
+ const raw = query("tmux", ["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
5308
+ return new Set(
5309
+ raw.trim().split("\n").map((line) => line.trim()).filter(Boolean)
5310
+ );
5311
+ } catch {
5312
+ return null;
5313
+ }
5314
+ }
5315
+ function parseOptionalNumber(field) {
5316
+ if (field === void 0 || field === "") return null;
5317
+ const n = Number(field);
5318
+ return Number.isFinite(n) ? n : null;
5319
+ }
5320
+ function classifyPaneLiveness(target, query) {
5321
+ let raw;
5322
+ try {
5323
+ raw = query("tmux", [
5324
+ "display",
5325
+ "-p",
5326
+ "-t",
5327
+ target,
5328
+ "#{pane_dead}|#{pane_dead_status}|#{pane_pid}|#{window_activity}"
5329
+ ]);
5330
+ } catch {
5331
+ return { state: "unknown", reason: `tmux could not report the pane at '${target}'` };
5332
+ }
5333
+ const [dead, deadStatus, pidStr, activityStr] = raw.trim().split("|");
5334
+ const lastActivityEpoch = parseOptionalNumber(activityStr);
5335
+ if (dead === "1") {
5336
+ return { state: "exited", exitCode: parseOptionalNumber(deadStatus), lastActivityEpoch };
5337
+ }
5338
+ const pid = Number(pidStr);
5339
+ if (!Number.isInteger(pid) || pid <= 0) {
5340
+ return { state: "unknown", reason: `tmux reported no pane pid for '${target}'` };
5341
+ }
5342
+ let psOut;
5343
+ try {
5344
+ psOut = query("ps", ["-eo", "pid=,ppid="]);
5345
+ } catch {
5346
+ return paneChildrenViaPgrep(pid, lastActivityEpoch, query);
5347
+ }
5348
+ return collectSessionPids([pid], psOut).size > 1 ? { state: "running" } : { state: "exited", exitCode: null, lastActivityEpoch };
5349
+ }
5350
+ function paneChildrenViaPgrep(pid, lastActivityEpoch, query) {
5351
+ try {
5352
+ return query("pgrep", ["-P", String(pid)]).trim().length > 0 ? { state: "running" } : { state: "exited", exitCode: null, lastActivityEpoch };
5353
+ } catch (err) {
5354
+ if (err.status === 1) {
5355
+ return { state: "exited", exitCode: null, lastActivityEpoch };
5356
+ }
5357
+ return { state: "unknown", reason: "could not read the process table (ps, pgrep)" };
5358
+ }
5359
+ }
5360
+ function classifySessionServices(sessionName, services, query) {
5361
+ const windows = listSessionWindowNames(sessionName, query);
5362
+ const result = /* @__PURE__ */ new Map();
5363
+ for (const [name, svc] of Object.entries(services)) {
5364
+ if (!svc.command || svc.command.length === 0) continue;
5365
+ if (windows === null) {
5366
+ result.set(name, {
5367
+ state: "unknown",
5368
+ reason: `could not list the windows of session '${sessionName}' (tmux)`
5369
+ });
5370
+ continue;
5371
+ }
5372
+ if (!windows.has(name)) {
5373
+ result.set(name, { state: "exited", exitCode: null, lastActivityEpoch: null });
5374
+ continue;
5375
+ }
5376
+ result.set(name, classifyPaneLiveness(`${sessionName}:${name}`, query));
5377
+ }
5378
+ return result;
5379
+ }
5380
+ function describePaneExit(liveness) {
5381
+ if (liveness.exitCode !== null) return `exit ${liveness.exitCode}`;
5382
+ if (liveness.lastActivityEpoch !== null) {
5383
+ return `no live process (last output ${new Date(liveness.lastActivityEpoch * 1e3).toISOString()})`;
5384
+ }
5385
+ return "no live process";
5386
+ }
5387
+ function servicesLivenessToResult(liveness) {
5388
+ const total = liveness.size;
5389
+ const dead = [];
5390
+ const unknown = [];
5391
+ for (const [name, l] of liveness.entries()) {
5392
+ if (l.state === "exited") dead.push([name, l]);
5393
+ else if (l.state === "unknown") unknown.push([name, l]);
5394
+ }
5395
+ if (dead.length === 0 && unknown.length === 0) {
5396
+ return { status: "ok", summary: `all ${total} service process(es) running` };
5397
+ }
5398
+ if (dead.length > 0) {
5399
+ return {
5400
+ status: "error",
5401
+ summary: `${dead.length} of ${total} service process(es) not running`,
5402
+ detail: dead.map(([name, l]) => `${name} \u2014 ${describePaneExit(l)}`).join("; "),
5403
+ remediation: dead.length === 1 ? `mesh dev logs ${dead[0][0]}` : "mesh dev logs <svc> for each service above, then mesh dev restart <svc>"
5404
+ };
5405
+ }
5406
+ return {
5407
+ status: "warn",
5408
+ summary: `could not determine liveness for ${unknown.length} of ${total} service(s)`,
5409
+ detail: unknown.map(([name, l]) => `${name} \u2014 ${l.reason}`).join("; ")
5410
+ };
5411
+ }
5412
+ var servicesCheck = {
5413
+ // On-demand only: at preflight (launch) the services are only starting, and
5414
+ // their liveness is what `mesh dev --status` reports live.
5415
+ id: "services",
5416
+ title: "Service processes",
5417
+ phases: ["ondemand"],
5418
+ async run(ctx) {
5419
+ const services = ctx.sessionState?.devOutput.services;
5420
+ if (!services || Object.keys(services).length === 0) {
5421
+ return { status: "ok", summary: "no session \u2014 services start at launch" };
5422
+ }
5423
+ const liveness = classifySessionServices(ctx.sessionName, services, paneProcessQuery);
5424
+ if (liveness.size === 0) {
5425
+ return { status: "ok", summary: "no locally-run services in this session" };
5426
+ }
5427
+ return servicesLivenessToResult(liveness);
5428
+ }
5429
+ };
4941
5430
  var tmuxCheck = {
4942
5431
  id: "tmux",
4943
5432
  title: "tmux",
4944
5433
  phases: ["preflight", "ondemand"],
4945
5434
  async run() {
4946
5435
  try {
4947
- execFileSync8("which", ["tmux"], { stdio: "ignore" });
5436
+ execFileSync9("which", ["tmux"], { stdio: "ignore" });
4948
5437
  return { status: "ok", summary: "installed" };
4949
5438
  } catch {
4950
5439
  return {
@@ -5022,14 +5511,14 @@ var configStalenessCheck = {
5022
5511
  if (!state) {
5023
5512
  return { status: "ok", summary: "no session \u2014 config read fresh at launch" };
5024
5513
  }
5025
- const configPath = path14.join(ctx.appRoot, `Pulumi.${ctx.stack}.yaml`);
5026
- if (!fs14.existsSync(configPath)) {
5514
+ const configPath = path16.join(ctx.appRoot, `Pulumi.${ctx.stack}.yaml`);
5515
+ if (!fs16.existsSync(configPath)) {
5027
5516
  return {
5028
5517
  status: "ok",
5029
5518
  summary: `no Pulumi.${ctx.stack}.yaml to compare`
5030
5519
  };
5031
5520
  }
5032
- const mtimeMs = fs14.statSync(configPath).mtimeMs;
5521
+ const mtimeMs = fs16.statSync(configPath).mtimeMs;
5033
5522
  if (isConfigStale(mtimeMs, state.startedAt)) {
5034
5523
  return {
5035
5524
  status: "warn",
@@ -5052,7 +5541,7 @@ var worktreeCheck = {
5052
5541
  }
5053
5542
  let repoRoot;
5054
5543
  try {
5055
- repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
5544
+ repoRoot = execFileSync9("git", ["rev-parse", "--show-toplevel"], {
5056
5545
  cwd: ctx.appRoot,
5057
5546
  encoding: "utf8",
5058
5547
  stdio: ["ignore", "pipe", "ignore"]
@@ -5200,9 +5689,11 @@ var ALL_CHECKS = [
5200
5689
  registryCheck,
5201
5690
  configStalenessCheck,
5202
5691
  portsCheck,
5692
+ servicesCheck,
5203
5693
  worktreeCheck,
5204
5694
  tunnelsCheck,
5205
- temporalCheck
5695
+ temporalCheck,
5696
+ envLeaksCheck
5206
5697
  ];
5207
5698
  async function runDoctor(ctx, opts) {
5208
5699
  const results = await runChecks(ctx, "ondemand", ALL_CHECKS);
@@ -5217,10 +5708,75 @@ ${ICONS[status]} overall: ${status}`);
5217
5708
  return status;
5218
5709
  }
5219
5710
 
5711
+ // packages/mesh-cli/src/commands/dev-readiness.ts
5712
+ var TEMPORAL_WORKER_RUNNING = /Worker state changed[\s\S]{0,400}?state: 'RUNNING'/;
5713
+ var RESTART_READY_TIMEOUT_MS = 6e4;
5714
+ var RESTART_ALIVE_GRACE_MS = 1e4;
5715
+ var RESTART_START_WINDOW_MS = 5e3;
5716
+ async function awaitServiceReadiness(target, probe) {
5717
+ const timeoutMs = target.timeoutMs ?? RESTART_READY_TIMEOUT_MS;
5718
+ const intervalMs = target.intervalMs ?? 500;
5719
+ const graceMs = Math.min(target.graceMs ?? RESTART_ALIVE_GRACE_MS, timeoutMs);
5720
+ const startWindowMs = Math.min(target.startWindowMs ?? RESTART_START_WINDOW_MS, timeoutMs);
5721
+ const portIsSignal = target.port > 0 && !target.portHeldBeforeStart;
5722
+ const hasSignal = portIsSignal || target.readyMarker !== void 0;
5723
+ const start = probe.now();
5724
+ for (; ; ) {
5725
+ const afterMs = probe.now() - start;
5726
+ const liveness = probe.liveness();
5727
+ if (liveness.state === "exited") {
5728
+ if (afterMs >= startWindowMs) {
5729
+ return { state: "exited", liveness, afterMs, output: probe.paneOutput() };
5730
+ }
5731
+ await probe.sleep(Math.min(intervalMs, startWindowMs - afterMs));
5732
+ continue;
5733
+ }
5734
+ const blind = liveness.state === "unknown";
5735
+ if (portIsSignal && await probe.portAccepts(target.port)) {
5736
+ return { state: "ready", via: "port", afterMs };
5737
+ }
5738
+ if (target.readyMarker && target.readyMarker.test(probe.paneOutput())) {
5739
+ return { state: "ready", via: "marker", afterMs };
5740
+ }
5741
+ if (!hasSignal) {
5742
+ if (blind) return { state: "unknown", reason: liveness.reason };
5743
+ if (afterMs >= graceMs) {
5744
+ return target.portHeldBeforeStart ? { state: "port-held", afterMs, output: probe.paneOutput() } : { state: "ready", via: "alive", afterMs };
5745
+ }
5746
+ }
5747
+ if (afterMs >= timeoutMs) {
5748
+ if (blind && liveness.state === "unknown") {
5749
+ return { state: "unknown", reason: `${liveness.reason}; and no readiness signal in ${timeoutMs}ms` };
5750
+ }
5751
+ return hasSignal ? { state: "pending", afterMs, output: probe.paneOutput() } : { state: "ready", via: "alive", afterMs };
5752
+ }
5753
+ await probe.sleep(Math.min(intervalMs, timeoutMs - afterMs));
5754
+ }
5755
+ }
5756
+ async function portHeldByAnother(port, probe, settleMs = 3e3, intervalMs = 250) {
5757
+ if (port <= 0) return false;
5758
+ const start = probe.now();
5759
+ for (; ; ) {
5760
+ if (!await probe.portAccepts(port)) return false;
5761
+ if (probe.now() - start >= settleMs) return true;
5762
+ await probe.sleep(intervalMs);
5763
+ }
5764
+ }
5765
+ function paneTail(output, n = 20) {
5766
+ return output.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-n);
5767
+ }
5768
+ function parseWaitSeconds(raw, defaultSeconds) {
5769
+ if (raw === false) return 0;
5770
+ if (raw === void 0 || raw === true) return defaultSeconds;
5771
+ const n = Number(raw);
5772
+ if (!Number.isFinite(n) || n < 0) return defaultSeconds;
5773
+ return n;
5774
+ }
5775
+
5220
5776
  // packages/mesh-cli/src/utils/worktree-identity.ts
5221
- import { execFileSync as execFileSync9 } from "node:child_process";
5777
+ import { execFileSync as execFileSync10 } from "node:child_process";
5222
5778
  import * as crypto from "node:crypto";
5223
- import * as path15 from "node:path";
5779
+ import * as path17 from "node:path";
5224
5780
  var NUM_PORT_BLOCKS = 64;
5225
5781
  var PORT_BLOCK_BASE = 4e4;
5226
5782
  var PORT_BLOCK_SIZE = 40;
@@ -5242,7 +5798,7 @@ function blockBasePort(block) {
5242
5798
  }
5243
5799
  function withAppScopedPortBlock(wt, appRoot) {
5244
5800
  if (wt.isPrimary) return wt;
5245
- const appHash = worktreeHash(`${wt.worktreeRoot}#${path15.resolve(appRoot)}`);
5801
+ const appHash = worktreeHash(`${wt.worktreeRoot}#${path17.resolve(appRoot)}`);
5246
5802
  return { ...wt, portBlock: portBlockFor(appHash, false) };
5247
5803
  }
5248
5804
  var STACK_SLUG_MAX = 16;
@@ -5254,27 +5810,27 @@ function worktreeStackName(baseName, wt) {
5254
5810
  function stackNeedsWorktreeIsolation(stackName, wt) {
5255
5811
  return !!wt.token && !stackName.endsWith(`-${wt.hash}`);
5256
5812
  }
5257
- var defaultGitRunner = (args, cwd) => execFileSync9("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5813
+ var defaultGitRunner = (args, cwd) => execFileSync10("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5258
5814
  function resolveWorktreeIdentity(cwd = process.cwd(), gitRunner = defaultGitRunner) {
5259
5815
  let worktreeRoot;
5260
5816
  let isPrimary = true;
5261
5817
  try {
5262
- worktreeRoot = path15.resolve(gitRunner(["rev-parse", "--show-toplevel"], cwd));
5818
+ worktreeRoot = path17.resolve(gitRunner(["rev-parse", "--show-toplevel"], cwd));
5263
5819
  const commonDir = gitRunner(["rev-parse", "--git-common-dir"], cwd);
5264
- const primaryRoot = path15.resolve(path15.dirname(path15.resolve(cwd, commonDir)));
5820
+ const primaryRoot = path17.resolve(path17.dirname(path17.resolve(cwd, commonDir)));
5265
5821
  isPrimary = worktreeRoot === primaryRoot;
5266
5822
  } catch {
5267
5823
  return {
5268
- worktreeRoot: path15.resolve(cwd),
5824
+ worktreeRoot: path17.resolve(cwd),
5269
5825
  isPrimary: true,
5270
- slug: sanitizeSlug(path15.basename(path15.resolve(cwd))),
5271
- hash: worktreeHash(path15.resolve(cwd)),
5826
+ slug: sanitizeSlug(path17.basename(path17.resolve(cwd))),
5827
+ hash: worktreeHash(path17.resolve(cwd)),
5272
5828
  token: "",
5273
5829
  portBlock: 0,
5274
5830
  taskQueueSuffix: ""
5275
5831
  };
5276
5832
  }
5277
- const slug2 = sanitizeSlug(path15.basename(worktreeRoot));
5833
+ const slug2 = sanitizeSlug(path17.basename(worktreeRoot));
5278
5834
  const hash = worktreeHash(worktreeRoot);
5279
5835
  const token = isPrimary ? "" : `${slug2}-${hash}`;
5280
5836
  return {
@@ -5312,6 +5868,9 @@ function registerServiceProbes(devOutput, tenant, probeFiles) {
5312
5868
  function deriveSessionName(projectName, wt) {
5313
5869
  return wt.isPrimary ? `${projectName}-dev` : `${projectName}-${wt.slug}`;
5314
5870
  }
5871
+ function sessionNameFor(appRoot, explicit) {
5872
+ return explicit ?? deriveSessionName(getProjectName(appRoot), resolveWorktreeIdentity(appRoot));
5873
+ }
5315
5874
  function printDevPlan(sessionName, appRoot, wt, devOutput) {
5316
5875
  const kind = wt.isPrimary ? "primary checkout" : `linked worktree (block ${wt.portBlock})`;
5317
5876
  console.log(`
@@ -5334,9 +5893,9 @@ mesh dev plan \u2014 ${kind}
5334
5893
  console.log(` (none \u2014 all services are deployed/K8s)`);
5335
5894
  } else {
5336
5895
  const monorepoRoot = findMonorepoRoot();
5337
- const wtWithSep = wt.worktreeRoot.endsWith(path16.sep) ? wt.worktreeRoot : wt.worktreeRoot + path16.sep;
5896
+ const wtWithSep = wt.worktreeRoot.endsWith(path18.sep) ? wt.worktreeRoot : wt.worktreeRoot + path18.sep;
5338
5897
  for (const [name, s] of locals) {
5339
- const dir = path16.resolve(appRoot, rebaseServiceSrc(s.src, monorepoRoot));
5898
+ const dir = path18.resolve(appRoot, rebaseServiceSrc(s.src, monorepoRoot));
5340
5899
  const outside = dir !== wt.worktreeRoot && !dir.startsWith(wtWithSep);
5341
5900
  const port = s.port > 0 ? `:${s.port}` : "(no port)";
5342
5901
  console.log(` ${name.padEnd(18)} ${port.padEnd(7)} ${dir}${outside ? " \u26A0 OUTSIDE worktree" : ""}`);
@@ -5345,18 +5904,17 @@ mesh dev plan \u2014 ${kind}
5345
5904
  console.log("");
5346
5905
  }
5347
5906
  function getSessionStatePath(sessionName) {
5348
- const dir = path16.join(os3.tmpdir(), "mesh-dev-sessions");
5349
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
5350
- return path16.join(dir, `${sessionName}.json`);
5907
+ const dir = devSessionsRoot();
5908
+ return path18.join(dir, `${sessionName}.json`);
5351
5909
  }
5352
5910
  function saveSessionState(sessionName, state) {
5353
- fs15.writeFileSync(getSessionStatePath(sessionName), JSON.stringify(state, null, 2));
5911
+ fs17.writeFileSync(getSessionStatePath(sessionName), JSON.stringify(state, null, 2));
5354
5912
  }
5355
5913
  function loadSessionState(sessionName) {
5356
5914
  const filePath = getSessionStatePath(sessionName);
5357
- if (!fs15.existsSync(filePath)) return null;
5915
+ if (!fs17.existsSync(filePath)) return null;
5358
5916
  try {
5359
- return JSON.parse(fs15.readFileSync(filePath, "utf-8"));
5917
+ return JSON.parse(fs17.readFileSync(filePath, "utf-8"));
5360
5918
  } catch {
5361
5919
  return null;
5362
5920
  }
@@ -5370,7 +5928,7 @@ function workflowChangeWouldStrand(priorState, appRoot, force) {
5370
5928
  const current = fingerprintWorkflowSource(appRoot, priorState?.devOutput?.services);
5371
5929
  if (!workflowChanged(prior, current)) return false;
5372
5930
  logWarn(
5373
- "Workflow code changed since this session started.\n Restarting the worker replays any in-flight conversations against the NEW code \u2014\n a replay-incompatible change strands them (they get stuck and become unviewable).\n Keep it replay-compatible: gate the change with wf.patched() and regenerate the replay\n goldens (see the temporal-workflow-safety skill), or accept the risk."
5931
+ "Workflow code changed since this session started.\n Restarting the worker replays any in-flight conversations against the NEW code \u2014\n a replay-incompatible change strands them (they get stuck and become unviewable).\n Keep it replay-compatible: gate the change with wf.patched() and commit a frozen\n taken/not-taken fixture pair for the gate (the temporal-replay skill's patch rule;\n never regenerate an existing golden), or accept the risk."
5374
5932
  );
5375
5933
  if (force) {
5376
5934
  logWarn(" Proceeding anyway (--force).");
@@ -5379,19 +5937,56 @@ function workflowChangeWouldStrand(priorState, appRoot, force) {
5379
5937
  logError(" Refusing to restart the worker. Re-run with --force once the change is replay-safe.");
5380
5938
  return true;
5381
5939
  }
5382
- function getSessionEnvDir(sessionName) {
5383
- return path16.join(os3.tmpdir(), "mesh-dev-sessions", sessionName);
5384
- }
5385
5940
  function getServiceEnvFilePath(sessionName, serviceName) {
5386
- return path16.join(getSessionEnvDir(sessionName), envFileName(serviceName));
5941
+ return path18.join(getSessionEnvDir(sessionName), envFileName(serviceName));
5387
5942
  }
5388
5943
  function removeSessionState(sessionName) {
5389
5944
  try {
5390
- fs15.unlinkSync(getSessionStatePath(sessionName));
5945
+ fs17.unlinkSync(getSessionStatePath(sessionName));
5391
5946
  } catch {
5392
5947
  }
5393
5948
  try {
5394
- fs15.rmSync(getSessionEnvDir(sessionName), { recursive: true, force: true });
5949
+ fs17.rmSync(getSessionEnvDir(sessionName), { recursive: true, force: true });
5950
+ } catch {
5951
+ }
5952
+ }
5953
+ function getSessionPortMemoryPath(sessionName) {
5954
+ return path18.join(path18.dirname(getSessionStatePath(sessionName)), `${sessionName}.ports.json`);
5955
+ }
5956
+ function loadSessionPortMemory(sessionName) {
5957
+ try {
5958
+ const raw = JSON.parse(fs17.readFileSync(getSessionPortMemoryPath(sessionName), "utf-8"));
5959
+ return rememberedPortsOf(raw);
5960
+ } catch {
5961
+ return {};
5962
+ }
5963
+ }
5964
+ function sessionPortPlan(sessionName) {
5965
+ const remembered = loadSessionPortMemory(sessionName);
5966
+ const ownPorts = /* @__PURE__ */ new Set();
5967
+ if (sessionExists(sessionName)) {
5968
+ const running = loadSessionState(sessionName)?.devOutput?.services ?? {};
5969
+ for (const [name, port] of Object.entries(remembered)) {
5970
+ if (running[name]?.port === port) ownPorts.add(port);
5971
+ }
5972
+ }
5973
+ return { remembered, ownPorts };
5974
+ }
5975
+ function rememberedPortsOf(memory) {
5976
+ const out = {};
5977
+ for (const [name, port] of Object.entries(memory?.services ?? {})) {
5978
+ if (Number.isInteger(port) && port > 0) out[name] = port;
5979
+ }
5980
+ return out;
5981
+ }
5982
+ function saveSessionPortMemory(sessionName, devOutput) {
5983
+ const services = {};
5984
+ for (const [name, svc] of Object.entries(devOutput.services)) {
5985
+ if (svc.port > 0 && svc.command && svc.command.length > 0) services[name] = svc.port;
5986
+ }
5987
+ const memory = { services, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
5988
+ try {
5989
+ fs17.writeFileSync(getSessionPortMemoryPath(sessionName), JSON.stringify(memory, null, 2));
5395
5990
  } catch {
5396
5991
  }
5397
5992
  }
@@ -5440,7 +6035,7 @@ function rewriteOwnServicePort(service, oldPort, newPort) {
5440
6035
  }
5441
6036
  }
5442
6037
  }
5443
- async function allocatePorts(devOutput, worktree) {
6038
+ async function allocatePorts(devOutput, worktree, remembered = {}, probes = { isPortFree, findFreePort }, ownPorts = /* @__PURE__ */ new Set()) {
5444
6039
  const allocated = JSON.parse(JSON.stringify(devOutput));
5445
6040
  const usedPorts = /* @__PURE__ */ new Set();
5446
6041
  const portRemaps = /* @__PURE__ */ new Map();
@@ -5450,25 +6045,35 @@ async function allocatePorts(devOutput, worktree) {
5450
6045
  if (service.port <= 0) continue;
5451
6046
  if (!service.command || service.command.length === 0) continue;
5452
6047
  const preferred = service.port;
6048
+ const last = remembered[name];
6049
+ const lastIsUsable = async () => last !== void 0 && last !== preferred && !usedPorts.has(last) && (ownPorts.has(last) || await probes.isPortFree(last));
5453
6050
  let target = -1;
5454
6051
  if (worktree.isPrimary) {
5455
- if (!usedPorts.has(preferred) && await isPortFree(preferred)) {
6052
+ if (!usedPorts.has(preferred) && await probes.isPortFree(preferred)) {
5456
6053
  target = preferred;
6054
+ } else if (await lastIsUsable()) {
6055
+ target = last;
6056
+ logInfo(`Port ${preferred} busy for service '${name}', reusing last launch's ${target}`);
5457
6057
  } else {
5458
- target = await findFreePort();
6058
+ target = await probes.findFreePort();
5459
6059
  logWarn(`Port ${preferred} busy for service '${name}', using ${target}`);
5460
6060
  }
6061
+ } else if (await lastIsUsable()) {
6062
+ target = last;
5461
6063
  } else {
6064
+ if (last !== void 0 && last !== preferred) {
6065
+ logWarn(`Port ${last} (this session's last launch) is not available for service '${name}'; allocating another`);
6066
+ }
5462
6067
  for (let i = blockCursor; i < PORT_BLOCK_SERVICE_SUBRANGE; i++) {
5463
6068
  const candidate = blockBase + i;
5464
- if (!usedPorts.has(candidate) && await isPortFree(candidate)) {
6069
+ if (!usedPorts.has(candidate) && await probes.isPortFree(candidate)) {
5465
6070
  target = candidate;
5466
6071
  blockCursor = i + 1;
5467
6072
  break;
5468
6073
  }
5469
6074
  }
5470
6075
  if (target < 0) {
5471
- target = await findFreePort();
6076
+ target = await probes.findFreePort();
5472
6077
  logWarn(`Worktree port block exhausted for '${name}', using ${target}`);
5473
6078
  }
5474
6079
  }
@@ -5506,12 +6111,15 @@ function mergePinnedPorts(fresh, pinned) {
5506
6111
  if (!pinnedSvc || typeof pinnedSvc.port !== "number") continue;
5507
6112
  const freshPort = mergedSvc.port;
5508
6113
  mergedSvc.port = pinnedSvc.port;
6114
+ if (typeof freshPort === "number" && freshPort > 0) {
6115
+ rewriteOwnServicePort(mergedSvc, freshPort, pinnedSvc.port);
6116
+ if (freshPort !== pinnedSvc.port) {
6117
+ portRemaps.set(freshPort, pinnedSvc.port);
6118
+ }
6119
+ }
5509
6120
  if (mergedSvc.env?.PORT) {
5510
6121
  mergedSvc.env.PORT.value = String(pinnedSvc.port);
5511
6122
  }
5512
- if (typeof freshPort === "number" && freshPort > 0 && freshPort !== pinnedSvc.port) {
5513
- portRemaps.set(freshPort, pinnedSvc.port);
5514
- }
5515
6123
  }
5516
6124
  rewriteCrossServicePorts(merged, portRemaps);
5517
6125
  rewriteClusterHostsToLocal(merged);
@@ -5519,16 +6127,16 @@ function mergePinnedPorts(fresh, pinned) {
5519
6127
  }
5520
6128
  function findMonorepoRoot() {
5521
6129
  let dir = process.cwd();
5522
- const root = path16.parse(dir).root;
6130
+ const root = path18.parse(dir).root;
5523
6131
  while (dir !== root) {
5524
- if (fs15.existsSync(path16.join(dir, "pnpm-workspace.yaml")) || fs15.existsSync(path16.join(dir, "turbo.json"))) {
6132
+ if (fs17.existsSync(path18.join(dir, "pnpm-workspace.yaml")) || fs17.existsSync(path18.join(dir, "turbo.json"))) {
5525
6133
  return dir;
5526
6134
  }
5527
- const gitPath = path16.join(dir, ".git");
5528
- if (fs15.existsSync(gitPath)) {
6135
+ const gitPath = path18.join(dir, ".git");
6136
+ if (fs17.existsSync(gitPath)) {
5529
6137
  return dir;
5530
6138
  }
5531
- dir = path16.dirname(dir);
6139
+ dir = path18.dirname(dir);
5532
6140
  }
5533
6141
  return null;
5534
6142
  }
@@ -5536,40 +6144,40 @@ function discoverApps(monorepoRoot) {
5536
6144
  const apps = [];
5537
6145
  function addApp(appDir, tenant) {
5538
6146
  const projectName = getProjectName(appDir);
5539
- const stacks = fs15.readdirSync(appDir).filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml").map((f) => f.replace(/^Pulumi\./, "").replace(/\.yaml$/, ""));
6147
+ const stacks = fs17.readdirSync(appDir).filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml").map((f) => f.replace(/^Pulumi\./, "").replace(/\.yaml$/, ""));
5540
6148
  apps.push({
5541
6149
  name: projectName,
5542
6150
  tenant,
5543
6151
  appPath: appDir,
5544
- relativePath: path16.relative(monorepoRoot, appDir),
6152
+ relativePath: path18.relative(monorepoRoot, appDir),
5545
6153
  stacks
5546
6154
  });
5547
6155
  }
5548
6156
  const searchDirs = ["tenants", "tests/tenants"];
5549
6157
  for (const searchDir of searchDirs) {
5550
- const base = path16.join(monorepoRoot, searchDir);
5551
- if (!fs15.existsSync(base)) continue;
5552
- for (const tenant of fs15.readdirSync(base)) {
5553
- const appsDir = path16.join(base, tenant, "apps");
5554
- if (!fs15.existsSync(appsDir) || !fs15.statSync(appsDir).isDirectory()) continue;
5555
- for (const app of fs15.readdirSync(appsDir)) {
5556
- const appDir = path16.join(appsDir, app);
5557
- if (!fs15.existsSync(path16.join(appDir, "Pulumi.yaml"))) continue;
6158
+ const base = path18.join(monorepoRoot, searchDir);
6159
+ if (!fs17.existsSync(base)) continue;
6160
+ for (const tenant of fs17.readdirSync(base)) {
6161
+ const appsDir = path18.join(base, tenant, "apps");
6162
+ if (!fs17.existsSync(appsDir) || !fs17.statSync(appsDir).isDirectory()) continue;
6163
+ for (const app of fs17.readdirSync(appsDir)) {
6164
+ const appDir = path18.join(appsDir, app);
6165
+ if (!fs17.existsSync(path18.join(appDir, "Pulumi.yaml"))) continue;
5558
6166
  addApp(appDir, tenant);
5559
6167
  }
5560
6168
  }
5561
6169
  }
5562
- const flatAppsDir = path16.join(monorepoRoot, "apps");
5563
- if (fs15.existsSync(flatAppsDir) && fs15.statSync(flatAppsDir).isDirectory()) {
5564
- for (const app of fs15.readdirSync(flatAppsDir)) {
5565
- const appDir = path16.join(flatAppsDir, app);
5566
- if (!fs15.statSync(appDir).isDirectory()) continue;
5567
- if (!fs15.existsSync(path16.join(appDir, "Pulumi.yaml"))) continue;
6170
+ const flatAppsDir = path18.join(monorepoRoot, "apps");
6171
+ if (fs17.existsSync(flatAppsDir) && fs17.statSync(flatAppsDir).isDirectory()) {
6172
+ for (const app of fs17.readdirSync(flatAppsDir)) {
6173
+ const appDir = path18.join(flatAppsDir, app);
6174
+ if (!fs17.statSync(appDir).isDirectory()) continue;
6175
+ if (!fs17.existsSync(path18.join(appDir, "Pulumi.yaml"))) continue;
5568
6176
  let tenant = "unknown";
5569
- const stackConfigs = fs15.readdirSync(appDir).filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml");
6177
+ const stackConfigs = fs17.readdirSync(appDir).filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml");
5570
6178
  if (stackConfigs.length > 0) {
5571
6179
  try {
5572
- const content = fs15.readFileSync(path16.join(appDir, stackConfigs[0]), "utf-8");
6180
+ const content = fs17.readFileSync(path18.join(appDir, stackConfigs[0]), "utf-8");
5573
6181
  const tenantMatch = content.match(/mesh:tenant:\s*(\S+)/);
5574
6182
  if (tenantMatch) tenant = tenantMatch[1];
5575
6183
  } catch {
@@ -5582,38 +6190,38 @@ function discoverApps(monorepoRoot) {
5582
6190
  }
5583
6191
  function findAppRoot2(appPath) {
5584
6192
  if (appPath) {
5585
- const resolved = path16.resolve(appPath);
5586
- if (fs15.existsSync(path16.join(resolved, "Pulumi.yaml"))) return resolved;
6193
+ const resolved = path18.resolve(appPath);
6194
+ if (fs17.existsSync(path18.join(resolved, "Pulumi.yaml"))) return resolved;
5587
6195
  const mono = findMonorepoRoot();
5588
6196
  if (mono) {
5589
- const fromMono = path16.resolve(mono, appPath);
5590
- if (fs15.existsSync(path16.join(fromMono, "Pulumi.yaml"))) return fromMono;
6197
+ const fromMono = path18.resolve(mono, appPath);
6198
+ if (fs17.existsSync(path18.join(fromMono, "Pulumi.yaml"))) return fromMono;
5591
6199
  }
5592
6200
  logError(`No Pulumi.yaml found at: ${appPath}`);
5593
6201
  process.exit(1);
5594
6202
  }
5595
6203
  let dir = process.cwd();
5596
- const root = path16.parse(dir).root;
6204
+ const root = path18.parse(dir).root;
5597
6205
  while (dir !== root) {
5598
- if (fs15.existsSync(path16.join(dir, "Pulumi.yaml"))) {
6206
+ if (fs17.existsSync(path18.join(dir, "Pulumi.yaml"))) {
5599
6207
  return dir;
5600
6208
  }
5601
- dir = path16.dirname(dir);
6209
+ dir = path18.dirname(dir);
5602
6210
  }
5603
6211
  return process.cwd();
5604
6212
  }
5605
6213
  function getProjectName(appRoot) {
5606
- const yamlPath = path16.join(appRoot, "Pulumi.yaml");
5607
- if (!fs15.existsSync(yamlPath)) return path16.basename(appRoot);
5608
- const content = fs15.readFileSync(yamlPath, "utf-8");
6214
+ const yamlPath = path18.join(appRoot, "Pulumi.yaml");
6215
+ if (!fs17.existsSync(yamlPath)) return path18.basename(appRoot);
6216
+ const content = fs17.readFileSync(yamlPath, "utf-8");
5609
6217
  const match = content.match(/^name:\s*(.+)$/m);
5610
- return match?.[1]?.trim() ?? path16.basename(appRoot);
6218
+ return match?.[1]?.trim() ?? path18.basename(appRoot);
5611
6219
  }
5612
6220
  function detectStack(appRoot, stageArg) {
5613
6221
  if (stageArg) return stageArg;
5614
6222
  if (process.env.MESH_STAGE) return process.env.MESH_STAGE;
5615
6223
  try {
5616
- const result = execFileSync10("pulumi", ["stack", "--show-name"], {
6224
+ const result = execFileSync11("pulumi", ["stack", "--show-name"], {
5617
6225
  cwd: appRoot,
5618
6226
  encoding: "utf-8",
5619
6227
  stdio: ["ignore", "pipe", "ignore"]
@@ -5622,7 +6230,7 @@ function detectStack(appRoot, stageArg) {
5622
6230
  } catch {
5623
6231
  }
5624
6232
  try {
5625
- const files = fs15.readdirSync(appRoot).filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml");
6233
+ const files = fs17.readdirSync(appRoot).filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml");
5626
6234
  if (files.length === 1) {
5627
6235
  const match = files[0].match(/^Pulumi\.(.+)\.yaml$/);
5628
6236
  if (match?.[1]) {
@@ -5636,7 +6244,7 @@ function detectStack(appRoot, stageArg) {
5636
6244
  }
5637
6245
  function stackArgs(appRoot, stack) {
5638
6246
  try {
5639
- const selected = execFileSync10("pulumi", ["stack", "--show-name"], {
6247
+ const selected = execFileSync11("pulumi", ["stack", "--show-name"], {
5640
6248
  cwd: appRoot,
5641
6249
  encoding: "utf-8",
5642
6250
  stdio: ["ignore", "pipe", "ignore"]
@@ -5645,7 +6253,7 @@ function stackArgs(appRoot, stack) {
5645
6253
  } catch {
5646
6254
  }
5647
6255
  try {
5648
- execFileSync10("pulumi", ["stack", "select", stack], {
6256
+ execFileSync11("pulumi", ["stack", "select", stack], {
5649
6257
  cwd: appRoot,
5650
6258
  encoding: "utf-8",
5651
6259
  stdio: ["ignore", "pipe", "ignore"]
@@ -5679,9 +6287,85 @@ function getDevOutput(appRoot, stack, awsEnv) {
5679
6287
  throw new MissingStackOutputError(stack, err);
5680
6288
  }
5681
6289
  }
6290
+ function gitCommitOfDir(dir) {
6291
+ const run = (...args) => execFileSync11("git", ["-C", dir, ...args], {
6292
+ encoding: "utf-8",
6293
+ stdio: ["ignore", "pipe", "ignore"],
6294
+ timeout: 2e3
6295
+ }).trim();
6296
+ try {
6297
+ const commit = run("rev-parse", "--short=12", "HEAD");
6298
+ if (!commit) return null;
6299
+ const dirty = run("status", "--porcelain").length > 0;
6300
+ return dirty ? `${commit}-dirty` : commit;
6301
+ } catch {
6302
+ return null;
6303
+ }
6304
+ }
6305
+ function worktreeDigestOfDir(dir) {
6306
+ const run = (cwd, ...args) => execFileSync11("git", ["-C", cwd, ...args], {
6307
+ stdio: ["ignore", "pipe", "ignore"],
6308
+ timeout: 1e4,
6309
+ maxBuffer: 256 * 1024 * 1024
6310
+ });
6311
+ try {
6312
+ const top = run(dir, "rev-parse", "--show-toplevel").toString().trim();
6313
+ if (!top) return null;
6314
+ const diff = run(top, "diff", "HEAD", "--no-color", "--no-ext-diff");
6315
+ const untracked = run(top, "ls-files", "--others", "--exclude-standard", "-z").toString().split("\0").filter((f) => f.length > 0).sort();
6316
+ if (diff.length === 0 && untracked.length === 0) return null;
6317
+ const hash = createHash5("sha256").update(diff);
6318
+ for (const file of untracked) {
6319
+ hash.update("\0").update(file).update("\0");
6320
+ try {
6321
+ hash.update(fs17.readFileSync(path18.join(top, file)));
6322
+ } catch {
6323
+ }
6324
+ }
6325
+ return hash.digest("hex").slice(0, 16);
6326
+ } catch {
6327
+ return null;
6328
+ }
6329
+ }
6330
+ function currentProgramProvenance(appRoot, stack) {
6331
+ let configSha256 = null;
6332
+ try {
6333
+ configSha256 = createHash5("sha256").update(fs17.readFileSync(path18.join(appRoot, `Pulumi.${stack}.yaml`))).digest("hex");
6334
+ } catch {
6335
+ }
6336
+ const gitCommit = gitCommitOfDir(appRoot);
6337
+ const worktreeDigest = gitCommit?.endsWith("-dirty") ? worktreeDigestOfDir(appRoot) : null;
6338
+ return { gitCommit, configSha256, worktreeDigest };
6339
+ }
6340
+ function devOutputStaleness(recorded, current) {
6341
+ const reasons = [];
6342
+ if (recorded?.gitCommit && current.gitCommit && recorded.gitCommit !== current.gitCommit) {
6343
+ reasons.push(
6344
+ `program is at ${current.gitCommit} but the outputs were deployed from ${recorded.gitCommit}`
6345
+ );
6346
+ } else if (recorded?.gitCommit && current.gitCommit && recorded.worktreeDigest && current.worktreeDigest && recorded.worktreeDigest !== current.worktreeDigest) {
6347
+ reasons.push(
6348
+ `program is at ${current.gitCommit} with different uncommitted changes than the outputs were deployed from`
6349
+ );
6350
+ }
6351
+ if (recorded?.configSha256 && current.configSha256 && recorded.configSha256 !== current.configSha256) {
6352
+ reasons.push("the stack config (Pulumi.<stack>.yaml) changed since the deploy");
6353
+ }
6354
+ return reasons;
6355
+ }
6356
+ function warnIfDevOutputStale(devOutput, appRoot, stack) {
6357
+ const reasons = devOutputStaleness(
6358
+ devOutput.provenance,
6359
+ currentProgramProvenance(appRoot, stack)
6360
+ );
6361
+ if (reasons.length === 0) return;
6362
+ logWarn(
6363
+ `Stack outputs are older than the program (${reasons.join("; ")}); run \`mesh deploy up --stack ${stack}\` to refresh the dev env.`
6364
+ );
6365
+ }
5682
6366
  function hasTmux() {
5683
6367
  try {
5684
- execFileSync10("which", ["tmux"], { stdio: "ignore" });
6368
+ execFileSync11("which", ["tmux"], { stdio: "ignore" });
5685
6369
  return true;
5686
6370
  } catch {
5687
6371
  return false;
@@ -5689,7 +6373,7 @@ function hasTmux() {
5689
6373
  }
5690
6374
  function sessionExists(name) {
5691
6375
  try {
5692
- execFileSync10("tmux", ["has-session", "-t", name], { stdio: "ignore" });
6376
+ execFileSync11("tmux", ["has-session", "-t", name], { stdio: "ignore" });
5693
6377
  return true;
5694
6378
  } catch {
5695
6379
  return false;
@@ -5697,7 +6381,7 @@ function sessionExists(name) {
5697
6381
  }
5698
6382
  function killSession(name) {
5699
6383
  try {
5700
- execFileSync10("tmux", ["kill-session", "-t", name], { stdio: "ignore" });
6384
+ execFileSync11("tmux", ["kill-session", "-t", name], { stdio: "ignore" });
5701
6385
  return true;
5702
6386
  } catch {
5703
6387
  return false;
@@ -5743,9 +6427,9 @@ function buildChildAwsEnv(awsEnv, method, profile, opts) {
5743
6427
  if (method === "zitadel" && opts) {
5744
6428
  const sanitizedContext = opts.context.replace(/[^A-Za-z0-9_-]/g, "-");
5745
6429
  const profileName = `mesh-dev-${sanitizedContext}`;
5746
- const configPath = path16.join(opts.sessionScratchDir, "aws-config");
6430
+ const configPath = path18.join(opts.sessionScratchDir, "aws-config");
5747
6431
  const region = awsEnv.AWS_REGION ?? awsEnv.AWS_DEFAULT_REGION ?? "";
5748
- fs15.mkdirSync(opts.sessionScratchDir, { recursive: true });
6432
+ fs17.mkdirSync(opts.sessionScratchDir, { recursive: true });
5749
6433
  const profileBlock = renderCredentialProcessProfile({
5750
6434
  profileName,
5751
6435
  context: opts.context,
@@ -5753,7 +6437,7 @@ function buildChildAwsEnv(awsEnv, method, profile, opts) {
5753
6437
  region,
5754
6438
  meshBin: opts.meshBin
5755
6439
  });
5756
- const existing = fs15.existsSync(configPath) ? fs15.readFileSync(configPath, "utf8") : "";
6440
+ const existing = fs17.existsSync(configPath) ? fs17.readFileSync(configPath, "utf8") : "";
5757
6441
  const next = upsertManagedAwsConfigSection(
5758
6442
  stripBareProfile(existing, profileName),
5759
6443
  `${opts.context} (mesh dev)`,
@@ -5778,7 +6462,7 @@ function buildChildAwsEnv(awsEnv, method, profile, opts) {
5778
6462
  function readDotEnvFile(file) {
5779
6463
  let text;
5780
6464
  try {
5781
- text = fs15.readFileSync(file, "utf-8");
6465
+ text = fs17.readFileSync(file, "utf-8");
5782
6466
  } catch {
5783
6467
  return {};
5784
6468
  }
@@ -5811,37 +6495,42 @@ function rebaseServiceSrc(src, monorepoRoot) {
5811
6495
  if (src === "." || src === "./") return src;
5812
6496
  const rel = monorepoRelativeSrc(src);
5813
6497
  if (rel === null) return src;
5814
- const rebased = path16.join(monorepoRoot, rel);
5815
- return fs15.existsSync(rebased) ? rebased : src;
6498
+ const rebased = path18.join(monorepoRoot, rel);
6499
+ return fs17.existsSync(rebased) ? rebased : src;
5816
6500
  }
5817
6501
  function isLinkedDependencyDir(serviceDir, appRoot) {
5818
- let pkgRoot = null;
5819
- let name = "";
5820
- for (let dir = serviceDir; ; dir = path16.dirname(dir)) {
6502
+ const pkg = enclosingPackage(serviceDir);
6503
+ return Boolean(pkg && appResolvedPackageDir(pkg.name, appRoot) === pkg.root);
6504
+ }
6505
+ function enclosingPackage(dir) {
6506
+ for (let at = dir; ; at = path18.dirname(at)) {
5821
6507
  try {
5822
- const manifest = JSON.parse(fs15.readFileSync(path16.join(dir, "package.json"), "utf8"));
5823
- if (typeof manifest?.name === "string" && manifest.name) {
5824
- pkgRoot = dir;
5825
- name = manifest.name;
5826
- break;
5827
- }
6508
+ const manifest = JSON.parse(fs17.readFileSync(path18.join(at, "package.json"), "utf8"));
6509
+ if (typeof manifest?.name === "string" && manifest.name) return { root: at, name: manifest.name };
5828
6510
  } catch {
5829
6511
  }
5830
- const parent = path16.dirname(dir);
5831
- if (parent === dir) break;
6512
+ if (path18.dirname(at) === at) return null;
5832
6513
  }
5833
- if (!pkgRoot) return false;
5834
- for (let dir = appRoot; ; dir = path16.dirname(dir)) {
6514
+ }
6515
+ function appResolvedPackageDir(name, appRoot) {
6516
+ for (let dir = appRoot; ; dir = path18.dirname(dir)) {
5835
6517
  try {
5836
- if (fs15.realpathSync(path16.join(dir, "node_modules", name)) === pkgRoot) return true;
6518
+ return fs17.realpathSync(path18.join(dir, "node_modules", name));
5837
6519
  } catch {
5838
6520
  }
5839
- const parent = path16.dirname(dir);
5840
- if (parent === dir) return false;
6521
+ if (path18.dirname(dir) === dir) return null;
5841
6522
  }
5842
6523
  }
6524
+ function relinkedServiceDir(serviceDir, appRoot) {
6525
+ const pkg = enclosingPackage(serviceDir);
6526
+ if (!pkg) return null;
6527
+ const own = appResolvedPackageDir(pkg.name, appRoot);
6528
+ if (!own || own === pkg.root) return null;
6529
+ const candidate = path18.join(own, path18.relative(pkg.root, serviceDir));
6530
+ return fs17.existsSync(candidate) ? candidate : null;
6531
+ }
5843
6532
  function monorepoRelativeSrc(src) {
5844
- if (!path16.isAbsolute(src)) return src.replace(/^\.\//, "") || ".";
6533
+ if (!path18.isAbsolute(src)) return src.replace(/^\.\//, "") || ".";
5845
6534
  const m = src.match(/^.*\/mesh-platform(?:\/(.*))?$/);
5846
6535
  if (!m) return null;
5847
6536
  const rest = (m[1] ?? "").replace(/^\.worktrees\/[^/]+(?:\/|$)/, "");
@@ -5855,30 +6544,51 @@ function getServiceEnvVars(service, tunnels) {
5855
6544
  if (spec.tunnel) {
5856
6545
  const tunnel = tunnels[spec.tunnel];
5857
6546
  if (tunnel) {
5858
- try {
5859
- const url = new URL(value);
5860
- if (url.hostname) {
5861
- url.hostname = tunnel.host;
5862
- url.port = String(tunnel.port);
5863
- value = url.toString();
5864
- } else {
5865
- value = `${tunnel.host}:${tunnel.port}`;
5866
- }
5867
- } catch {
5868
- value = `${tunnel.host}:${tunnel.port}`;
5869
- }
6547
+ value = pointAtTunnel(key, value, tunnel);
6548
+ const portKey = portSiblingOf(key);
6549
+ if (portKey && service.env[portKey]) result[portKey] = String(tunnel.port);
5870
6550
  }
5871
6551
  }
5872
- result[key] = value;
6552
+ if (!(key in result)) result[key] = value;
5873
6553
  }
5874
6554
  }
5875
6555
  return result;
5876
6556
  }
6557
+ function portSiblingOf(key) {
6558
+ return key.endsWith("_HOST") ? `${key.slice(0, -"_HOST".length)}_PORT` : void 0;
6559
+ }
6560
+ function pointAtTunnel(key, value, tunnel) {
6561
+ try {
6562
+ const url = new URL(value);
6563
+ if (url.hostname) {
6564
+ url.hostname = tunnel.host;
6565
+ url.port = String(tunnel.port);
6566
+ return url.toString();
6567
+ }
6568
+ } catch {
6569
+ }
6570
+ if (key.endsWith("_HOST") && !value.includes(":")) return tunnel.host;
6571
+ return `${tunnel.host}:${tunnel.port}`;
6572
+ }
5877
6573
  function setTmuxEnv(sessionName, vars) {
5878
6574
  for (const [key, value] of Object.entries(vars)) {
5879
- execFileSync10("tmux", ["set-environment", "-t", sessionName, key, value], { stdio: "ignore" });
6575
+ execFileSync11("tmux", ["set-environment", "-t", sessionName, key, value], { stdio: "ignore" });
5880
6576
  }
5881
6577
  }
6578
+ function readTmuxSessionEnv(sessionName, keys) {
6579
+ const out = {};
6580
+ for (const key of keys) {
6581
+ try {
6582
+ const line = execFileSync11("tmux", ["show-environment", "-t", sessionName, key], {
6583
+ encoding: "utf-8",
6584
+ stdio: ["ignore", "pipe", "ignore"]
6585
+ }).trim();
6586
+ if (line.startsWith(`${key}=`)) out[key] = line.slice(key.length + 1);
6587
+ } catch {
6588
+ }
6589
+ }
6590
+ return out;
6591
+ }
5882
6592
  function envPrefix(vars) {
5883
6593
  const entries = Object.entries(vars);
5884
6594
  if (entries.length === 0) return "";
@@ -5889,7 +6599,32 @@ function shellEscape(s) {
5889
6599
  if (/^[a-zA-Z0-9_./:@=+,-]+$/.test(s)) return s;
5890
6600
  return `'${s.replace(/'/g, "'\\''")}'`;
5891
6601
  }
5892
- async function resolveSecrets(secrets) {
6602
+ function tunnelForSecret(secretName, tunnels) {
6603
+ return tunnels[secretName] ?? (secretName === "database" ? tunnels["db"] : void 0);
6604
+ }
6605
+ function pointSecretUrlsAtTunnel(values, tunnel) {
6606
+ if (!tunnel) return values;
6607
+ const out = {};
6608
+ for (const [key, value] of Object.entries(values)) {
6609
+ let rewritten = value;
6610
+ try {
6611
+ const url = new URL(value);
6612
+ if (url.hostname) {
6613
+ url.hostname = tunnel.host === "localhost" ? "127.0.0.1" : tunnel.host;
6614
+ url.port = String(tunnel.port);
6615
+ if (/^postgres(ql)?:$/.test(url.protocol) && !url.searchParams.has("sslmode")) {
6616
+ url.searchParams.set("sslmode", "no-verify");
6617
+ url.searchParams.set("sslaccept", "accept_invalid_certs");
6618
+ }
6619
+ rewritten = url.toString();
6620
+ }
6621
+ } catch {
6622
+ }
6623
+ out[key] = rewritten;
6624
+ }
6625
+ return out;
6626
+ }
6627
+ async function resolveSecrets(secrets, tunnels) {
5893
6628
  const client = new SecretsManagerClient2({});
5894
6629
  const resolved = {};
5895
6630
  for (const [name, secret] of Object.entries(secrets)) {
@@ -5899,11 +6634,13 @@ async function resolveSecrets(secrets) {
5899
6634
  );
5900
6635
  if (!response.SecretString) continue;
5901
6636
  const values = JSON.parse(response.SecretString);
6637
+ const mapped = {};
5902
6638
  for (const [secretKey, envVar] of Object.entries(secret.envMapping)) {
5903
6639
  const value = values[secretKey];
5904
6640
  if (value === void 0) continue;
5905
- resolved[envVar] = value;
6641
+ mapped[envVar] = value;
5906
6642
  }
6643
+ Object.assign(resolved, pointSecretUrlsAtTunnel(mapped, tunnelForSecret(name, tunnels)));
5907
6644
  logSuccess(`Secret resolved: ${name} (${secret.secretName})`);
5908
6645
  } catch (err) {
5909
6646
  logWarn(
@@ -5913,11 +6650,15 @@ async function resolveSecrets(secrets) {
5913
6650
  }
5914
6651
  return resolved;
5915
6652
  }
5916
- function resolveTemporalEncodingKey(tenant, env, appName) {
5917
- const namespace = `${tenant}-${env}-${appName}`;
6653
+ function stackNamespace(devOutput) {
6654
+ const tenant = devOutput.platform?.tenant ?? "mesh";
6655
+ const stack = devOutput.stack ?? devOutput.platform?.env ?? "dev";
6656
+ return devOutput.app ? `${tenant}-${stack}-${devOutput.app}` : void 0;
6657
+ }
6658
+ function resolveTemporalEncodingKey(namespace) {
5918
6659
  const secretName = `${namespace}-temporal-encoding-key`;
5919
6660
  try {
5920
- const b64 = execFileSync10(
6661
+ const b64 = execFileSync11(
5921
6662
  "kubectl",
5922
6663
  [
5923
6664
  "get",
@@ -5953,18 +6694,20 @@ async function resolveTemporalAuthVars(sessionName, devOutput) {
5953
6694
  setTmuxEnv(sessionName, { KUBECONFIG: kubeconfigPath });
5954
6695
  } else if (!process.env.KUBECONFIG) {
5955
6696
  const existing = sessionKubeconfigPath(sessionName);
5956
- if (fs15.existsSync(existing)) {
6697
+ if (fs17.existsSync(existing)) {
5957
6698
  process.env.KUBECONFIG = existing;
5958
6699
  }
5959
6700
  }
5960
6701
  let temporalAuthVars = {};
5961
6702
  if (devOutput.tunnels["temporal"]) {
5962
6703
  temporalAuthVars = await resolveTemporalAuth(tenant, platformEnv, devOutput.platform?.name ?? tenant);
5963
- const appName = devOutput.app ?? "";
5964
- if (appName) {
5965
- const encodingKey = resolveTemporalEncodingKey(tenant, platformEnv, appName);
6704
+ const namespace = stackNamespace(devOutput);
6705
+ if (namespace) {
6706
+ const encodingKey = resolveTemporalEncodingKey(namespace);
5966
6707
  if (encodingKey) {
5967
6708
  temporalAuthVars.TEMPORAL_ENCODING_KEY = encodingKey;
6709
+ } else {
6710
+ logInfo(`No deployed encoding key in ${namespace} \u2014 using the stack's dev key from the dev block.`);
5968
6711
  }
5969
6712
  }
5970
6713
  const temporalTunnel = devOutput.tunnels["temporal"];
@@ -6124,13 +6867,29 @@ async function allocateSsmLocalPort(remotePort, reservedPorts) {
6124
6867
  }
6125
6868
  function hasSessionManagerPlugin() {
6126
6869
  try {
6127
- execFileSync10("which", ["session-manager-plugin"], { stdio: "ignore" });
6870
+ execFileSync11("which", ["session-manager-plugin"], { stdio: "ignore" });
6128
6871
  return true;
6129
6872
  } catch {
6130
6873
  return false;
6131
6874
  }
6132
6875
  }
6133
- async function startSsmTunnels(sessionName, devOutput) {
6876
+ var TUNNEL_TO_BASTION_KEY = {
6877
+ temporal: "temporal-frontend",
6878
+ "temporal-ui": "temporal-ui",
6879
+ rds: "rds",
6880
+ db: "rds",
6881
+ database: "rds"
6882
+ };
6883
+ function bastionServiceForTunnel(tunnelName, tunnel, services) {
6884
+ for (const key of [TUNNEL_TO_BASTION_KEY[tunnelName], tunnelName]) {
6885
+ if (key && services[key]) return { key, endpoint: services[key] };
6886
+ }
6887
+ const byEndpoint = Object.entries(services).find(
6888
+ ([, svc]) => svc.host === tunnel.host && svc.port === tunnel.port
6889
+ );
6890
+ return byEndpoint ? { key: byEndpoint[0], endpoint: byEndpoint[1] } : void 0;
6891
+ }
6892
+ async function startSsmTunnels(sessionName, devOutput, appRoot, awsEnv) {
6134
6893
  const platformEnv = devOutput.platform?.env ?? "dev";
6135
6894
  const bastionTenant = devOutput.platform?.name ?? "mesh";
6136
6895
  const bastion = await getPlatformBastionInfo(bastionTenant, platformEnv).catch(() => null);
@@ -6147,13 +6906,6 @@ async function startSsmTunnels(sessionName, devOutput) {
6147
6906
  return rewritten;
6148
6907
  }
6149
6908
  logInfo(`Starting ${tunnelCount} SSM tunnel(s) via bastion ${bastion.instanceId}...`);
6150
- const tunnelToBastionKey = {
6151
- temporal: "temporal-frontend",
6152
- "temporal-ui": "temporal-ui",
6153
- rds: "rds",
6154
- db: "rds",
6155
- database: "rds"
6156
- };
6157
6909
  const OPTIONAL_TUNNEL_NAMES = /* @__PURE__ */ new Set([
6158
6910
  "loki",
6159
6911
  "prometheus",
@@ -6165,9 +6917,10 @@ async function startSsmTunnels(sessionName, devOutput) {
6165
6917
  const skippedOptional = [];
6166
6918
  const reservedPorts = /* @__PURE__ */ new Set();
6167
6919
  const plannedTunnels = [];
6168
- for (const [tunnelName] of Object.entries(devOutput.tunnels)) {
6169
- const bastionKey = tunnelToBastionKey[tunnelName] ?? tunnelName;
6170
- const endpoint = bastion.services[bastionKey];
6920
+ for (const [tunnelName, tunnel] of Object.entries(devOutput.tunnels)) {
6921
+ const matched = bastionServiceForTunnel(tunnelName, tunnel, bastion.services);
6922
+ const bastionKey = matched?.key ?? tunnelName;
6923
+ const endpoint = matched?.endpoint;
6171
6924
  if (!endpoint) {
6172
6925
  if (OPTIONAL_TUNNEL_NAMES.has(tunnelName)) {
6173
6926
  skippedOptional.push(`${tunnelName} (bastion key: ${bastionKey})`);
@@ -6206,6 +6959,7 @@ async function startSsmTunnels(sessionName, devOutput) {
6206
6959
  );
6207
6960
  throw new Error("SSM tunnel fallback unavailable: missing bastion endpoints");
6208
6961
  }
6962
+ const hostEnv = pickLaunchHostEnv(process.env);
6209
6963
  for (const { tunnelName, endpoint, localPort } of plannedTunnels) {
6210
6964
  const windowName = `tunnel-${tunnelName}`;
6211
6965
  const ssmCmd = [
@@ -6223,13 +6977,21 @@ async function startSsmTunnels(sessionName, devOutput) {
6223
6977
  localPortNumber: [String(localPort)]
6224
6978
  })}'`
6225
6979
  ].join(" ");
6226
- execFileSync10("tmux", ["new-window", "-t", sessionName, "-n", windowName]);
6227
- execFileSync10(
6980
+ execFileSync11("tmux", ["new-window", "-t", sessionName, "-n", windowName]);
6981
+ execFileSync11(
6228
6982
  "tmux",
6229
6983
  ["set-option", "-t", `${sessionName}:${windowName}`, "remain-on-exit", "on"],
6230
6984
  { stdio: "ignore" }
6231
6985
  );
6232
- execFileSync10("tmux", ["send-keys", "-t", `${sessionName}:${windowName}`, ssmCmd, "Enter"]);
6986
+ const envFilePath = getServiceEnvFilePath(sessionName, windowName);
6987
+ writeEnvFile(envFilePath, { ...hostEnv, ...awsEnv });
6988
+ execFileSync11("tmux", [
6989
+ "send-keys",
6990
+ "-t",
6991
+ `${sessionName}:${windowName}`,
6992
+ buildLaunchCommand(envFilePath, appRoot, ssmCmd, void 0, hostEnv),
6993
+ "Enter"
6994
+ ]);
6233
6995
  rewritten.tunnels[tunnelName] = {
6234
6996
  host: "localhost",
6235
6997
  port: localPort
@@ -6241,7 +7003,7 @@ async function startSsmTunnels(sessionName, devOutput) {
6241
7003
  return rewritten;
6242
7004
  }
6243
7005
  async function updateDevboxProxy(devOutput, appRoot) {
6244
- const appName = devOutput.app ?? path16.basename(appRoot);
7006
+ const appName = devOutput.app ?? path18.basename(appRoot);
6245
7007
  const tenant = devOutput.platform?.tenant ?? "mesh";
6246
7008
  const env = devOutput.platform?.env ?? "dev";
6247
7009
  const stack = devOutput.stack ?? "dev";
@@ -6273,7 +7035,7 @@ async function updateDevboxProxy(devOutput, appRoot) {
6273
7035
  lines.push("");
6274
7036
  }
6275
7037
  const caddyfile = lines.join("\n");
6276
- execFileSync10(
7038
+ execFileSync11(
6277
7039
  "docker",
6278
7040
  [
6279
7041
  "run",
@@ -6289,7 +7051,7 @@ CADDYEOF`
6289
7051
  ],
6290
7052
  { stdio: "pipe" }
6291
7053
  );
6292
- execFileSync10(
7054
+ execFileSync11(
6293
7055
  "docker",
6294
7056
  [
6295
7057
  "run",
@@ -6319,7 +7081,7 @@ CADDYEOF`
6319
7081
  async function setupSubdomainRouting(devOutput, appRoot) {
6320
7082
  const tsInfo = await getTailscaleInfo();
6321
7083
  if (!tsInfo) return null;
6322
- const appName = devOutput.app ?? path16.basename(appRoot);
7084
+ const appName = devOutput.app ?? path18.basename(appRoot);
6323
7085
  const stack = devOutput.stack ?? "dev";
6324
7086
  const baseDomain = `${appName}.${stack}.${tsInfo.hostname}.vpn.internal`;
6325
7087
  const platform = devOutput.platform ?? { tenant: "mesh", env: "dev" };
@@ -6338,8 +7100,8 @@ async function setupSubdomainRouting(devOutput, appRoot) {
6338
7100
  } catch {
6339
7101
  }
6340
7102
  const caddyDir = "/tmp/mesh-dev-caddy";
6341
- const caddyfile = path16.join(caddyDir, "Caddyfile");
6342
- if (!fs15.existsSync(caddyDir)) fs15.mkdirSync(caddyDir, { recursive: true });
7103
+ const caddyfile = path18.join(caddyDir, "Caddyfile");
7104
+ if (!fs17.existsSync(caddyDir)) fs17.mkdirSync(caddyDir, { recursive: true });
6343
7105
  const caddyLines = [`# mesh-dev: ${appName}/${stack} (${tsInfo.hostname})`, ""];
6344
7106
  for (const [name, service] of Object.entries(devOutput.services)) {
6345
7107
  if (service.port) {
@@ -6357,16 +7119,16 @@ async function setupSubdomainRouting(devOutput, appRoot) {
6357
7119
  caddyLines.push("}");
6358
7120
  caddyLines.push("");
6359
7121
  }
6360
- fs15.writeFileSync(caddyfile, caddyLines.join("\n"));
7122
+ fs17.writeFileSync(caddyfile, caddyLines.join("\n"));
6361
7123
  let caddyRunning = false;
6362
7124
  try {
6363
- execFileSync10("pgrep", ["-f", "caddy run.*mesh-dev-caddy"], { stdio: "pipe" });
7125
+ execFileSync11("pgrep", ["-f", "caddy run.*mesh-dev-caddy"], { stdio: "pipe" });
6364
7126
  caddyRunning = true;
6365
7127
  } catch {
6366
7128
  }
6367
7129
  if (caddyRunning) {
6368
7130
  try {
6369
- execFileSync10("caddy", ["reload", "--config", caddyfile, "--adapter", "caddyfile"], {
7131
+ execFileSync11("caddy", ["reload", "--config", caddyfile, "--adapter", "caddyfile"], {
6370
7132
  stdio: "pipe"
6371
7133
  });
6372
7134
  } catch {
@@ -6375,7 +7137,7 @@ async function setupSubdomainRouting(devOutput, appRoot) {
6375
7137
  }
6376
7138
  } else {
6377
7139
  try {
6378
- execFileSync10("which", ["caddy"], { stdio: "pipe" });
7140
+ execFileSync11("which", ["caddy"], { stdio: "pipe" });
6379
7141
  } catch {
6380
7142
  logWarn(
6381
7143
  "Caddy not found. Install for subdomain routing: curl -fsSL https://caddyserver.com/api/download?os=linux&arch=arm64 -o /usr/local/bin/caddy && chmod +x /usr/local/bin/caddy"
@@ -6417,8 +7179,14 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6417
7179
  return devOutput;
6418
7180
  }
6419
7181
  logInfo(`Creating tmux session: ${sessionName}`);
6420
- execFileSync10("tmux", ["new-session", "-d", "-s", sessionName, "-n", "status", "-c", appRoot]);
6421
- setTmuxEnv(sessionName, awsEnv);
7182
+ execFileSync11("tmux", ["new-session", "-d", "-s", sessionName, "-n", "status", "-c", appRoot]);
7183
+ const sessionEnv = {};
7184
+ const shareOnSession = (vars) => {
7185
+ Object.assign(sessionEnv, vars);
7186
+ setTmuxEnv(sessionName, vars);
7187
+ };
7188
+ const hostEnv = pickLaunchHostEnv(process.env);
7189
+ shareOnSession(awsEnv);
6422
7190
  const hasTunnels = Object.keys(devOutput.tunnels).length > 0;
6423
7191
  let effectiveTransport = tunnelPlan.transport;
6424
7192
  if (hasTunnels && effectiveTransport === "tailscale" && tunnelPlan.tailscale) {
@@ -6448,7 +7216,7 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6448
7216
  }
6449
7217
  }
6450
7218
  if (hasTunnels && effectiveTransport === "ssm") {
6451
- devOutput = await startSsmTunnels(sessionName, devOutput);
7219
+ devOutput = await startSsmTunnels(sessionName, devOutput, appRoot, awsEnv);
6452
7220
  }
6453
7221
  if (hasTunnels && effectiveTransport !== "vpn-direct") {
6454
7222
  for (const [tName, tunnel] of Object.entries(devOutput.tunnels)) {
@@ -6485,7 +7253,7 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6485
7253
  }
6486
7254
  let secretEnvVars = {};
6487
7255
  if (devOutput.secrets && Object.keys(devOutput.secrets).length > 0) {
6488
- secretEnvVars = await resolveSecrets(devOutput.secrets);
7256
+ secretEnvVars = await resolveSecrets(devOutput.secrets, devOutput.tunnels);
6489
7257
  }
6490
7258
  const tenant = devOutput.platform?.tenant ?? "mesh";
6491
7259
  const platformEnv = devOutput.platform?.env ?? "dev";
@@ -6499,7 +7267,7 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6499
7267
  });
6500
7268
  if (kubeconfigPath) {
6501
7269
  process.env.KUBECONFIG = kubeconfigPath;
6502
- setTmuxEnv(sessionName, { KUBECONFIG: kubeconfigPath });
7270
+ shareOnSession({ KUBECONFIG: kubeconfigPath });
6503
7271
  logSuccess(`Kubeconfig resolved from SSM (hub ${platformName}/${platformEnv}) \u2192 ${kubeconfigPath}`);
6504
7272
  } else {
6505
7273
  logWarn("Could not resolve EKS kubeconfig from SSM; kubectl calls will use ambient config (if any).");
@@ -6508,37 +7276,47 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6508
7276
  let temporalAuthVars = {};
6509
7277
  if (devOutput.tunnels["temporal"]) {
6510
7278
  temporalAuthVars = await resolveTemporalAuth(tenant, platformEnv, devOutput.platform?.name ?? tenant);
6511
- const appName = devOutput.app ?? "";
6512
- if (appName) {
6513
- const encodingKey = resolveTemporalEncodingKey(tenant, platformEnv, appName);
7279
+ const namespace = stackNamespace(devOutput);
7280
+ if (namespace) {
7281
+ const encodingKey = resolveTemporalEncodingKey(namespace);
6514
7282
  if (encodingKey) {
6515
7283
  temporalAuthVars.TEMPORAL_ENCODING_KEY = encodingKey;
7284
+ } else {
7285
+ logInfo(`No deployed encoding key in ${namespace} \u2014 using the stack's dev key from the dev block.`);
6516
7286
  }
6517
7287
  }
6518
7288
  const temporalTunnel = devOutput.tunnels["temporal"];
6519
7289
  temporalAuthVars.TEMPORAL_ADDRESS = tunnelClientAddress(temporalTunnel);
6520
7290
  }
6521
- setTmuxEnv(sessionName, secretEnvVars);
6522
- setTmuxEnv(sessionName, temporalAuthVars);
7291
+ shareOnSession(secretEnvVars);
7292
+ shareOnSession(temporalAuthVars);
6523
7293
  const credContext = devOutput.platform ? isLocalPlatform ? "local" : loginContextFor(devOutput.platform) : null;
6524
7294
  const devUserVars = resolveDevUserVars(devOutput);
6525
7295
  if (Object.keys(devUserVars).length > 0) {
6526
- setTmuxEnv(sessionName, devUserVars);
7296
+ shareOnSession(devUserVars);
6527
7297
  logSuccess(`Dev user injected from mesh login: ${credContext}`);
6528
7298
  const tokenPort = await findFreePort();
6529
7299
  const tokenUrl = `http://127.0.0.1:${tokenPort}`;
6530
- setTmuxEnv(sessionName, { DEV_USER_TOKEN_URL: tokenUrl });
6531
- execFileSync10("tmux", ["new-window", "-t", sessionName, "-n", "token-server"]);
6532
- execFileSync10(
7300
+ shareOnSession({ DEV_USER_TOKEN_URL: tokenUrl });
7301
+ execFileSync11("tmux", ["new-window", "-t", sessionName, "-n", "token-server"]);
7302
+ execFileSync11(
6533
7303
  "tmux",
6534
7304
  ["set-option", "-t", `${sessionName}:token-server`, "remain-on-exit", "on"],
6535
7305
  { stdio: "ignore" }
6536
7306
  );
6537
- execFileSync10("tmux", [
7307
+ const tokenServerEnvFile = getServiceEnvFilePath(sessionName, "token-server");
7308
+ writeEnvFile(tokenServerEnvFile, hostEnv);
7309
+ execFileSync11("tmux", [
6538
7310
  "send-keys",
6539
7311
  "-t",
6540
7312
  `${sessionName}:token-server`,
6541
- `npx mesh dev __token-server ${tokenPort} ${credContext}`,
7313
+ buildLaunchCommand(
7314
+ tokenServerEnvFile,
7315
+ appRoot,
7316
+ `npx mesh dev __token-server ${tokenPort} ${credContext}`,
7317
+ void 0,
7318
+ hostEnv
7319
+ ),
6542
7320
  "Enter"
6543
7321
  ]);
6544
7322
  for (let i = 0; i < 15 && !await isPortListening(tokenPort); i++) {
@@ -6551,33 +7329,55 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6551
7329
  logInfo(" Hub UI will show unauthenticated state without valid credentials.");
6552
7330
  }
6553
7331
  const monorepoRoot = findMonorepoRoot();
7332
+ const notLaunched = [];
6554
7333
  for (const [name, service] of Object.entries(devOutput.services)) {
6555
7334
  const cmd = service.command.join(" ");
6556
7335
  if (!cmd) {
6557
7336
  logInfo(`Skipped: ${name} (no dev command, deployed to K8s)`);
6558
7337
  continue;
6559
7338
  }
6560
- const serviceDir = path16.resolve(appRoot, rebaseServiceSrc(service.src, monorepoRoot));
6561
- if (worktreeRoot && fs15.existsSync(serviceDir)) {
6562
- const wtRootWithSep = worktreeRoot.endsWith(path16.sep) ? worktreeRoot : worktreeRoot + path16.sep;
7339
+ let serviceDir = path18.resolve(appRoot, rebaseServiceSrc(service.src, monorepoRoot));
7340
+ if (worktreeRoot && fs17.existsSync(serviceDir)) {
7341
+ const wtRootWithSep = worktreeRoot.endsWith(path18.sep) ? worktreeRoot : worktreeRoot + path18.sep;
6563
7342
  const outside = serviceDir !== worktreeRoot && !serviceDir.startsWith(wtRootWithSep);
6564
7343
  if (outside && isLinkedDependencyDir(serviceDir, appRoot)) {
6565
7344
  logInfo(`${name}: source is a dev-linked package (${serviceDir})`);
6566
7345
  } else if (outside) {
6567
- logError(
6568
- `Refusing to launch '${name}': its source resolved to ${serviceDir}, outside this worktree (${worktreeRoot}), and it is not a package this app resolves there. The stack was likely deployed from another worktree. Re-run \`mesh deploy up\` here, or pass --app.`
6569
- );
6570
- process.exit(1);
7346
+ const own = relinkedServiceDir(serviceDir, appRoot);
7347
+ if (own) {
7348
+ logWarn(
7349
+ `${name}: the stack recorded its source in another copy of its package (${serviceDir}); running this app's own copy (${own}).`
7350
+ );
7351
+ serviceDir = own;
7352
+ } else {
7353
+ logError(
7354
+ `Not launching '${name}': its source resolved to ${serviceDir}, outside this worktree (${worktreeRoot}), and it is not a package this app resolves. The stack was likely deployed from another worktree; re-run \`mesh deploy up\` here, or pass --app. Every other service still starts.`
7355
+ );
7356
+ notLaunched.push(name);
7357
+ continue;
7358
+ }
6571
7359
  }
6572
7360
  }
6573
- if (!fs15.existsSync(serviceDir)) {
7361
+ if (!fs17.existsSync(serviceDir)) {
6574
7362
  logWarn(
6575
7363
  `Skipped: ${name} (source directory not found: ${serviceDir}${serviceDir !== service.src ? ` \u2014 rebased from ${service.src}` : ""})`
6576
7364
  );
6577
7365
  continue;
6578
7366
  }
6579
7367
  const serviceVars = {
7368
+ // The allow-listed host variables come FIRST, so anything below wins over
7369
+ // them; they are in the file rather than on the launch command so that a
7370
+ // restart (which sources this same file) reproduces the launch PATH and
7371
+ // HOME byte for byte, and so the send-keys string stays short — inlining
7372
+ // a long PATH is the same truncation hazard that moved the env into a
7373
+ // file in the first place.
7374
+ ...hostEnv,
6580
7375
  ...getServiceEnvVars(service, devOutput.tunnels),
7376
+ // The resolved secrets — DATABASE_URL, DATABASE_PASSWORD — for the same reason as
7377
+ // every overlay below: the service starts from its env file alone. Without them a
7378
+ // service gets DATABASE_HOST/PORT/NAME/USER from its link and no DATABASE_URL, and
7379
+ // Prisma falls back to localhost:5432, whatever happens to listen there.
7380
+ ...secretEnvVars,
6581
7381
  ...temporalAuthVars,
6582
7382
  // Overlay the resolved dev-user identity for the same reason as the
6583
7383
  // Temporal vars: the dev output may carry a placeholder (e.g. hub ui's
@@ -6600,21 +7400,31 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6600
7400
  // no AWS_* here it silently has none to give. `buildChildAwsEnv` has already
6601
7401
  // chosen the SELF-REFRESHING shape (a profile with `credential_process`, not a
6602
7402
  // frozen STS triple), which is exactly what a long-lived child needs.
6603
- ...awsEnv
7403
+ ...awsEnv,
7404
+ // Whatever else was shared on the session (KUBECONFIG and
7405
+ // DEV_USER_TOKEN_URL), everything above being already overlaid — because
7406
+ // under `env -i` the session environment no longer reaches the service.
7407
+ // The explicit overlays above stay: each names the failure
7408
+ // it prevents, and this catch-all must not be the only thing carrying them.
7409
+ ...sessionEnv
6604
7410
  };
6605
7411
  const envFilePath = getServiceEnvFilePath(sessionName, name);
6606
- writeEnvFile(envFilePath, serviceVars);
7412
+ const overrides = readServiceOverrides(sessionName, name);
7413
+ if (Object.keys(overrides).length)
7414
+ logInfo(`${name}: applying ${Object.keys(overrides).length} override(s) from ${serviceOverridesPaths(sessionName, name).join(" / ")}`);
7415
+ writeEnvFile(envFilePath, { ...serviceVars, ...overrides });
6607
7416
  const launchCmd = buildLaunchCommand(
6608
7417
  envFilePath,
6609
7418
  serviceDir,
6610
7419
  cmd,
6611
- service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0
7420
+ service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0,
7421
+ hostEnv
6612
7422
  );
6613
- execFileSync10("tmux", ["new-window", "-t", sessionName, "-n", name, "-c", serviceDir]);
6614
- execFileSync10("tmux", ["set-option", "-t", `${sessionName}:${name}`, "remain-on-exit", "on"], {
7423
+ execFileSync11("tmux", ["new-window", "-t", sessionName, "-n", name, "-c", serviceDir]);
7424
+ execFileSync11("tmux", ["set-option", "-t", `${sessionName}:${name}`, "remain-on-exit", "on"], {
6615
7425
  stdio: "ignore"
6616
7426
  });
6617
- execFileSync10("tmux", ["send-keys", "-t", `${sessionName}:${name}`, launchCmd, "Enter"]);
7427
+ execFileSync11("tmux", ["send-keys", "-t", `${sessionName}:${name}`, launchCmd, "Enter"]);
6618
7428
  logSuccess(`Started: ${name} (${serviceDir}, port ${service.port})`);
6619
7429
  }
6620
7430
  if (process.env.DEVCONTAINER === "1") {
@@ -6625,9 +7435,13 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6625
7435
  }
6626
7436
  }
6627
7437
  const statusCmd = `watch -n2 -t npx mesh dev --status --session '${sessionName}'`;
6628
- execFileSync10("tmux", ["send-keys", "-t", `${sessionName}:status`, statusCmd, "Enter"]);
7438
+ execFileSync11("tmux", ["send-keys", "-t", `${sessionName}:status`, statusCmd, "Enter"]);
6629
7439
  console.log("");
6630
7440
  logSuccess(`Dev session started: ${sessionName}`);
7441
+ if (notLaunched.length > 0)
7442
+ logWarn(
7443
+ `Not launched (source outside this worktree, see above): ${notLaunched.join(", ")}. The rest of the session is up.`
7444
+ );
6631
7445
  console.log("");
6632
7446
  if (tunnelNames.length > 0) {
6633
7447
  console.log("Connections (VPN direct):");
@@ -6680,31 +7494,20 @@ async function showStatus(sessionName, devOutput, asJson) {
6680
7494
  }
6681
7495
  return;
6682
7496
  }
6683
- let windows = [];
6684
- try {
6685
- const raw = execFileSync10(
6686
- "tmux",
6687
- ["list-windows", "-t", sessionName, "-F", "#{window_name} #{pane_dead}"],
6688
- { encoding: "utf-8" }
6689
- );
6690
- windows = raw.trim().split("\n");
6691
- } catch {
6692
- }
6693
- const windowStatus = {};
6694
- for (const line of windows) {
6695
- const [name, dead] = line.split(" ");
6696
- if (name) windowStatus[name] = dead === "1" ? "exited" : "running";
6697
- }
7497
+ const liveness = classifySessionServices(sessionName, devOutput.services, paneProcessQuery);
6698
7498
  const tunnelNames = Object.keys(devOutput.tunnels);
6699
7499
  const tunnelHealth = await probeTunnelHealth(devOutput.tunnels);
6700
7500
  const healthByName = new Map(tunnelHealth.map((h) => [h.name, h]));
6701
7501
  if (asJson) {
6702
7502
  const services = {};
6703
7503
  for (const [name, svc] of Object.entries(devOutput.services)) {
7504
+ const l = liveness.get(name);
6704
7505
  services[name] = {
6705
- status: windowStatus[name] ?? "unknown",
7506
+ status: l?.state ?? "unknown",
6706
7507
  port: svc.port,
6707
- src: svc.src
7508
+ src: svc.src,
7509
+ ...l?.state === "exited" && l.exitCode !== null ? { exitCode: l.exitCode } : {},
7510
+ ...l?.state === "exited" && l.lastActivityEpoch !== null ? { lastActivity: new Date(l.lastActivityEpoch * 1e3).toISOString() } : {}
6708
7511
  };
6709
7512
  }
6710
7513
  const tunnels = {};
@@ -6734,10 +7537,16 @@ async function showStatus(sessionName, devOutput, asJson) {
6734
7537
  console.log(` \u2601 ${name.padEnd(16)} deployed ${svc.src}`);
6735
7538
  continue;
6736
7539
  }
6737
- const status = windowStatus[name] ?? "unknown";
7540
+ const l = liveness.get(name);
7541
+ const status = l?.state ?? "unknown";
6738
7542
  const icon = status === "running" ? "\u25CF" : status === "exited" ? "\u2717" : "?";
7543
+ const statusCol = l?.state === "exited" && l.exitCode !== null ? `exited(${l.exitCode})` : status;
6739
7544
  const addr = svc.port ? `:${svc.port}` : "";
6740
- console.log(` ${icon} ${name.padEnd(16)} ${status.padEnd(10)} ${addr.padEnd(8)} ${svc.src}`);
7545
+ let line = ` ${icon} ${name.padEnd(16)} ${statusCol.padEnd(10)} ${addr.padEnd(8)} ${svc.src}`;
7546
+ if (l?.state === "exited" && l.lastActivityEpoch !== null) {
7547
+ line += ` (last output ${new Date(l.lastActivityEpoch * 1e3).toLocaleTimeString()})`;
7548
+ }
7549
+ console.log(line);
6741
7550
  }
6742
7551
  console.log("");
6743
7552
  console.log("Commands:");
@@ -6759,10 +7568,15 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6759
7568
  logError(`No active session: ${sessionName}. Run 'mesh dev' first.`);
6760
7569
  process.exit(1);
6761
7570
  }
6762
- if (opts?.refreshEnv) {
7571
+ const waitMs = opts?.waitMs ?? RESTART_READY_TIMEOUT_MS;
7572
+ const neverLaunched = !fs17.existsSync(getServiceEnvFilePath(sessionName, serviceName));
7573
+ if (neverLaunched && !opts?.refreshEnv && opts?.stack)
7574
+ logInfo(`'${serviceName}' has no launch env file (never started) \u2014 rendering it from the stack.`);
7575
+ const refreshEnv = opts?.refreshEnv || neverLaunched && Boolean(opts?.stack);
7576
+ if (refreshEnv && opts) {
6763
7577
  if (!opts.stack) {
6764
7578
  logError("--refresh-env: no stack resolved; keeping existing env and NOT restarting.");
6765
- return;
7579
+ return null;
6766
7580
  }
6767
7581
  const pinned = opts.sessionState?.devOutput ?? devOutput;
6768
7582
  let fresh;
@@ -6773,20 +7587,26 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6773
7587
  `--refresh-env: failed to read stack output for '${opts.stack}'. Keeping the existing env file for '${serviceName}' and NOT restarting.`
6774
7588
  );
6775
7589
  logInfo(String(err?.message ?? err));
6776
- return;
7590
+ return null;
6777
7591
  }
7592
+ warnIfDevOutputStale(fresh, appRoot, opts.stack);
6778
7593
  const merged = mergePinnedPorts(fresh, pinned);
6779
7594
  const mergedService = merged.services[serviceName];
6780
7595
  if (!mergedService) {
6781
7596
  logError(
6782
7597
  `--refresh-env: service '${serviceName}' is absent from the fresh stack output. Keeping the existing env file and NOT restarting.`
6783
7598
  );
6784
- return;
7599
+ return null;
6785
7600
  }
6786
7601
  const temporalAuthVars = await resolveTemporalAuthVars(sessionName, merged);
7602
+ const secretEnvVars = merged.secrets && Object.keys(merged.secrets).length > 0 ? await resolveSecrets(merged.secrets, merged.tunnels) : {};
6787
7603
  const { taskQueueSuffix } = resolveWorktreeIdentity(appRoot);
6788
7604
  const serviceVars = {
7605
+ // The allow-listed host variables first, as at launch: the rewritten
7606
+ // file is the whole environment the restarted service gets.
7607
+ ...pickLaunchHostEnv(process.env),
6789
7608
  ...getServiceEnvVars(mergedService, merged.tunnels),
7609
+ ...secretEnvVars,
6790
7610
  ...temporalAuthVars,
6791
7611
  // Same overlay as launch: without it, --refresh-env rewrites the env
6792
7612
  // file from dev output and its DEV_USER_* placeholders clobber the real
@@ -6796,30 +7616,75 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6796
7616
  // Same overlay as launch — a refresh that dropped these would take a working
6797
7617
  // service's credentials away on restart, which is the hardest kind of change to
6798
7618
  // attribute afterwards.
6799
- ...awsEnv
7619
+ ...awsEnv,
7620
+ // The two launch set on the session and nowhere the refresh can re-derive
7621
+ // them from: the token-server's URL (its port was picked at launch) and the
7622
+ // session kubeconfig. Before the clean launch the window's shell inherited
7623
+ // them from the session; now only the file reaches the service.
7624
+ ...readTmuxSessionEnv(sessionName, ["KUBECONFIG", "DEV_USER_TOKEN_URL"])
6800
7625
  };
6801
- writeEnvFile(getServiceEnvFilePath(sessionName, serviceName), serviceVars);
7626
+ const envFilePathToRefresh = getServiceEnvFilePath(sessionName, serviceName);
7627
+ let previousVars = null;
7628
+ try {
7629
+ previousVars = parseEnvFile(fs17.readFileSync(envFilePathToRefresh, "utf-8"));
7630
+ } catch {
7631
+ }
7632
+ if (previousVars) {
7633
+ const changes = diffEnvFiles(previousVars, { ...serviceVars, ...readServiceOverrides(sessionName, serviceName) });
7634
+ if (changes.length === 0) {
7635
+ logInfo(`--refresh-env: the regenerated env for '${serviceName}' is identical to the running one.`);
7636
+ } else {
7637
+ logInfo(`--refresh-env: '${serviceName}' env changes (${changes.length}):`);
7638
+ for (const change of changes) console.log(` ${change.detail}`);
7639
+ }
7640
+ }
7641
+ const overrides = readServiceOverrides(sessionName, serviceName);
7642
+ if (Object.keys(overrides).length)
7643
+ logInfo(`'${serviceName}': applying ${Object.keys(overrides).length} override(s).`);
7644
+ writeEnvFile(envFilePathToRefresh, { ...serviceVars, ...overrides });
6802
7645
  service = mergedService;
6803
7646
  logSuccess(`Regenerated env for '${serviceName}' from current stack/SSM outputs.`);
6804
7647
  }
6805
7648
  const target = `${sessionName}:${serviceName}`;
6806
7649
  try {
6807
- execFileSync10("tmux", ["respawn-pane", "-k", "-t", target], { stdio: "ignore" });
7650
+ execFileSync11("tmux", ["respawn-pane", "-k", "-t", target], { stdio: "ignore" });
6808
7651
  } catch {
6809
- logError(`Window '${serviceName}' not found in session.`);
6810
- process.exit(1);
7652
+ try {
7653
+ execFileSync11("tmux", ["new-window", "-d", "-t", sessionName, "-n", serviceName], { stdio: "ignore" });
7654
+ logInfo(`Window '${serviceName}' did not exist \u2014 created it.`);
7655
+ } catch {
7656
+ logError(`Window '${serviceName}' not found in session, and could not be created.`);
7657
+ process.exit(1);
7658
+ }
7659
+ }
7660
+ try {
7661
+ execFileSync11("tmux", ["clear-history", "-t", target], { stdio: "ignore" });
7662
+ } catch {
7663
+ }
7664
+ const portHeldBeforeStart = waitMs > 0 && await portHeldByAnother(service.port, {
7665
+ portAccepts: isPortListening,
7666
+ now: () => Date.now(),
7667
+ sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms))
7668
+ });
7669
+ if (portHeldBeforeStart) {
7670
+ logWarn(
7671
+ `Port :${service.port} is still accepting connections with '${serviceName}' stopped \u2014 another process holds it. The restarted service cannot bind it; whatever answers on :${service.port} is not this session's ${serviceName}.`
7672
+ );
7673
+ logInfo(`Find the holder: lsof -nP -iTCP:${service.port} -sTCP:LISTEN`);
6811
7674
  }
6812
7675
  setTmuxEnv(sessionName, awsEnv);
6813
7676
  const cmd = service.command.join(" ");
6814
7677
  const envFilePath = getServiceEnvFilePath(sessionName, serviceName);
6815
7678
  let restartCmd;
6816
- if (fs15.existsSync(envFilePath)) {
6817
- const serviceDir = path16.resolve(appRoot, rebaseServiceSrc(service.src, findMonorepoRoot()));
7679
+ if (fs17.existsSync(envFilePath)) {
7680
+ let serviceDir = path18.resolve(appRoot, rebaseServiceSrc(service.src, findMonorepoRoot()));
7681
+ if (!isLinkedDependencyDir(serviceDir, appRoot)) serviceDir = relinkedServiceDir(serviceDir, appRoot) ?? serviceDir;
6818
7682
  restartCmd = buildLaunchCommand(
6819
7683
  envFilePath,
6820
7684
  serviceDir,
6821
7685
  cmd,
6822
- service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0
7686
+ service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0,
7687
+ pickLaunchHostEnv(process.env)
6823
7688
  );
6824
7689
  } else {
6825
7690
  logWarn(
@@ -6829,8 +7694,80 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6829
7694
  const serviceVars = getServiceEnvVars(service, devOutput.tunnels);
6830
7695
  restartCmd = `${envPrefix(serviceVars)}${cmd}`;
6831
7696
  }
6832
- execFileSync10("tmux", ["send-keys", "-t", target, restartCmd, "Enter"]);
6833
- logSuccess(`Restarted: ${serviceName}`);
7697
+ execFileSync11("tmux", ["send-keys", "-t", target, restartCmd, "Enter"]);
7698
+ if (waitMs <= 0) {
7699
+ logSuccess(`Restarted: ${serviceName} (not waiting for readiness \u2014 check \`mesh dev --status\`)`);
7700
+ return null;
7701
+ }
7702
+ const readiness = await awaitServiceReadiness(
7703
+ {
7704
+ port: service.port,
7705
+ portHeldBeforeStart,
7706
+ readyMarker: service.port > 0 ? void 0 : TEMPORAL_WORKER_RUNNING,
7707
+ timeoutMs: waitMs
7708
+ },
7709
+ {
7710
+ liveness: () => classifyPaneLiveness(target, paneProcessQuery),
7711
+ portAccepts: isPortListening,
7712
+ paneOutput: () => capturePane(target),
7713
+ now: () => Date.now(),
7714
+ sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms))
7715
+ }
7716
+ );
7717
+ reportRestartReadiness(serviceName, service.port, readiness);
7718
+ return readiness;
7719
+ }
7720
+ function capturePane(target, lines = 2e3) {
7721
+ try {
7722
+ return execFileSync11("tmux", ["capture-pane", "-p", "-t", target, "-S", `-${lines}`], {
7723
+ encoding: "utf-8",
7724
+ stdio: ["ignore", "pipe", "ignore"]
7725
+ });
7726
+ } catch {
7727
+ return "";
7728
+ }
7729
+ }
7730
+ function reportRestartReadiness(serviceName, port, readiness) {
7731
+ const secs = (ms) => `${(ms / 1e3).toFixed(1)}s`;
7732
+ switch (readiness.state) {
7733
+ case "ready": {
7734
+ const how = readiness.via === "port" ? `listening on :${port}` : readiness.via === "marker" ? "worker RUNNING" : "process up";
7735
+ logSuccess(`Restarted: ${serviceName} (${how} after ${secs(readiness.afterMs)})`);
7736
+ return;
7737
+ }
7738
+ case "exited": {
7739
+ logError(
7740
+ `Restart FAILED: '${serviceName}' ${describePaneExit(readiness.liveness)} ${secs(readiness.afterMs)} after its command was typed.`
7741
+ );
7742
+ const tail = paneTail(readiness.output);
7743
+ if (tail.length > 0) {
7744
+ logInfo(`Last output from '${serviceName}':`);
7745
+ for (const line of tail) console.log(` ${line}`);
7746
+ }
7747
+ logInfo(`Full output: mesh dev logs ${serviceName}`);
7748
+ return;
7749
+ }
7750
+ case "pending": {
7751
+ const what = port > 0 ? `:${port} is not accepting connections` : "no ready marker printed";
7752
+ logWarn(
7753
+ `Restarted: ${serviceName}, but readiness is UNCONFIRMED \u2014 ${what} after ${secs(readiness.afterMs)} (process still up).`
7754
+ );
7755
+ logInfo(`Watch it: mesh dev logs ${serviceName} \xB7 mesh dev --status`);
7756
+ return;
7757
+ }
7758
+ case "port-held": {
7759
+ logError(
7760
+ `Restart NOT confirmed: '${serviceName}' is up ${secs(readiness.afterMs)} after its command was typed, but :${port} was already held by another process \u2014 it either failed to bind and is exiting, or moved to a port nothing dials.`
7761
+ );
7762
+ const tail = paneTail(readiness.output, 8);
7763
+ for (const line of tail) console.log(` ${line}`);
7764
+ logInfo(`Free :${port} (lsof -nP -iTCP:${port} -sTCP:LISTEN) and restart again, or relaunch: mesh dev --kill && mesh dev`);
7765
+ return;
7766
+ }
7767
+ case "unknown":
7768
+ logWarn(`Restarted: ${serviceName}, but readiness could not be checked (${readiness.reason}).`);
7769
+ return;
7770
+ }
6834
7771
  }
6835
7772
  function showLogs(sessionName, serviceName, tail) {
6836
7773
  if (!sessionExists(sessionName)) {
@@ -6839,7 +7776,7 @@ function showLogs(sessionName, serviceName, tail) {
6839
7776
  }
6840
7777
  const target = `${sessionName}:${serviceName}`;
6841
7778
  try {
6842
- const result = execFileSync10("tmux", ["capture-pane", "-t", target, "-p", "-S", `-${tail}`], {
7779
+ const result = execFileSync11("tmux", ["capture-pane", "-t", target, "-p", "-S", `-${tail}`], {
6843
7780
  encoding: "utf-8"
6844
7781
  });
6845
7782
  process.stdout.write(result);
@@ -6874,7 +7811,9 @@ function buildDoctorContext(appRoot, stack, sessionName) {
6874
7811
  };
6875
7812
  }
6876
7813
  function registerDevCommand(program) {
6877
- const dev = program.command("dev").description("Start local dev environment (reads Pulumi stack outputs)").option("--app <path>", "Path to a Pulumi app (relative to monorepo root or cwd)").option("--stack <stack>", "Pulumi stack name (default: auto-detect)").addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp()).option("--headless", "Start without attaching to tmux").option("--kill", "Kill existing dev session").option("--status", "Show service status").option("--json", "Output status as JSON (with --status)").option(
7814
+ const dev = program.command("dev").description(
7815
+ "Start local dev environment (reads Pulumi stack outputs). The env is rendered from the stack's STORED outputs \u2014 when they are older than the program (git HEAD) or the stack config, mesh dev warns to run `mesh deploy up --stack <stack>` first"
7816
+ ).option("--app <path>", "Path to a Pulumi app (relative to monorepo root or cwd)").option("--stack <stack>", "Pulumi stack name (default: auto-detect)").addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp()).option("--headless", "Start without attaching to tmux").option("--kill", "Kill existing dev session").option("--status", "Show service status").option("--json", "Output status as JSON (with --status)").option(
6878
7817
  "--session <name>",
6879
7818
  "Override the tmux session name (auto-derived per git worktree by default \u2014 omit to keep concurrent worktrees isolated)"
6880
7819
  ).option("--dry-run", "Print the resolved worktree/session/port plan and exit without launching").option("--force", "Relaunch even if workflow code changed since launch (may strand in-flight conversations)").option("--profile <name>", "AWS SSO profile to use (e.g., mesh-dev)").addOption(
@@ -6894,7 +7833,7 @@ function registerDevCommand(program) {
6894
7833
  "tmux"
6895
7834
  );
6896
7835
  dev.command("__token-server <port> <context>", { hidden: true }).action(async (port, context) => {
6897
- const { startTokenServer } = await import("./mesh.dev-token-server-SS7QEQOQ.js");
7836
+ const { startTokenServer } = await import("./mesh.dev-token-server-XG26C6IQ.js");
6898
7837
  await startTokenServer(Number(port), context);
6899
7838
  });
6900
7839
  dev.action(async (options) => {
@@ -7047,7 +7986,13 @@ function registerDevCommand(program) {
7047
7986
  mocks: selectedMocks
7048
7987
  });
7049
7988
  if (options.dryRun) {
7050
- printDevPlan(sessionName, appRoot, worktree, await allocatePorts(rawDevOutput2, worktree));
7989
+ const plan3 = sessionPortPlan(sessionName);
7990
+ printDevPlan(
7991
+ sessionName,
7992
+ appRoot,
7993
+ worktree,
7994
+ await allocatePorts(rawDevOutput2, worktree, plan3.remembered, void 0, plan3.ownPorts)
7995
+ );
7051
7996
  for (const [name, decl] of Object.entries(selectedMocks)) {
7052
7997
  const mode = externalMode(name, decl);
7053
7998
  if (mode === "local") {
@@ -7072,7 +8017,7 @@ function registerDevCommand(program) {
7072
8017
  signInServices = [];
7073
8018
  try {
7074
8019
  const appPkg = JSON.parse(
7075
- fs15.readFileSync(path16.join(appRoot, "package.json"), "utf-8")
8020
+ fs17.readFileSync(path18.join(appRoot, "package.json"), "utf-8")
7076
8021
  );
7077
8022
  if (Array.isArray(appPkg?.mesh?.auth?.roles)) {
7078
8023
  authRoles = appPkg.mesh.auth.roles.filter((r) => typeof r === "string");
@@ -7124,7 +8069,8 @@ function registerDevCommand(program) {
7124
8069
  );
7125
8070
  }
7126
8071
  }
7127
- const devOutput2 = await allocatePorts(rawDevOutput2, worktree);
8072
+ const plan2 = sessionPortPlan(sessionName);
8073
+ const devOutput2 = await allocatePorts(rawDevOutput2, worktree, plan2.remembered, void 0, plan2.ownPorts);
7128
8074
  for (const service of signInServices) {
7129
8075
  const port = devOutput2.services[service]?.port;
7130
8076
  if (!port) {
@@ -7168,7 +8114,7 @@ function registerDevCommand(program) {
7168
8114
  }
7169
8115
  let localDocs;
7170
8116
  try {
7171
- const appPkg = JSON.parse(fs15.readFileSync(path16.join(appRoot, "package.json"), "utf-8"));
8117
+ const appPkg = JSON.parse(fs17.readFileSync(path18.join(appRoot, "package.json"), "utf-8"));
7172
8118
  const declaration = readLocalPublishDeclaration(appPkg, appRoot);
7173
8119
  if (declaration.docs || declaration.authzMetadata || declaration.problems.length > 0) {
7174
8120
  const app = devOutput2.app ?? projectName;
@@ -7178,10 +8124,10 @@ function registerDevCommand(program) {
7178
8124
  if (!httpService || !service?.port) {
7179
8125
  logWarn(`mesh.docs names service '${httpService ?? ""}', which this app does not run \u2014 docs not published.`);
7180
8126
  } else {
7181
- const serviceDir = path16.resolve(appRoot, service.src);
8127
+ const serviceDir = path18.resolve(appRoot, service.src);
7182
8128
  const env = service.env ?? (service.env = {});
7183
8129
  const serviceEnv = {
7184
- ...readDotEnvFile(path16.join(serviceDir, ".env")),
8130
+ ...readDotEnvFile(path18.join(serviceDir, ".env")),
7185
8131
  ...Object.fromEntries(Object.entries(process.env).filter((e) => typeof e[1] === "string")),
7186
8132
  ...Object.fromEntries(Object.entries(env).map(([k, v]) => [k, v.value]))
7187
8133
  };
@@ -7326,6 +8272,7 @@ function registerDevCommand(program) {
7326
8272
  externalProbeFiles,
7327
8273
  ...localDocs ? { localDocs } : {}
7328
8274
  });
8275
+ saveSessionPortMemory(sessionName, devOutput2);
7329
8276
  console.log("");
7330
8277
  logSuccess(`Dev session started (docker): ${sessionName}`);
7331
8278
  console.log("");
@@ -7355,10 +8302,12 @@ function registerDevCommand(program) {
7355
8302
  devOutput: finalDevOutput2,
7356
8303
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7357
8304
  runner: "tmux",
8305
+ cleanEnv: true,
7358
8306
  composeExternals,
7359
8307
  externalProbeFiles,
7360
8308
  ...localDocs ? { localDocs } : {}
7361
8309
  });
8310
+ saveSessionPortMemory(sessionName, finalDevOutput2);
7362
8311
  return;
7363
8312
  }
7364
8313
  if (options.mock || options.externals) {
@@ -7370,9 +8319,9 @@ function registerDevCommand(program) {
7370
8319
  logInfo(`Project: ${projectName}, Stack: ${stack}`);
7371
8320
  const monorepoRoot = findMonorepoRoot();
7372
8321
  const looksLikeStandaloneApp = ["api", "worker", "web"].some(
7373
- (dir) => fs15.existsSync(path16.join(appRoot, dir, "package.json"))
8322
+ (dir) => fs17.existsSync(path18.join(appRoot, dir, "package.json"))
7374
8323
  );
7375
- if (monorepoRoot && path16.resolve(appRoot) === path16.resolve(monorepoRoot) && !looksLikeStandaloneApp) {
8324
+ if (monorepoRoot && path18.resolve(appRoot) === path18.resolve(monorepoRoot) && !looksLikeStandaloneApp) {
7376
8325
  logError(`mesh dev should be run from an app directory, not the monorepo root.
7377
8326
  `);
7378
8327
  logInfo("Try:");
@@ -7503,7 +8452,9 @@ function registerDevCommand(program) {
7503
8452
  await showStatus(sessionName, state?.devOutput ?? rawDevOutput, !!options.json);
7504
8453
  return;
7505
8454
  }
7506
- const devOutput = await allocatePorts(rawDevOutput, worktree);
8455
+ warnIfDevOutputStale(rawDevOutput, appRoot, stack);
8456
+ const plan = sessionPortPlan(sessionName);
8457
+ const devOutput = await allocatePorts(rawDevOutput, worktree, plan.remembered, void 0, plan.ownPorts);
7507
8458
  transport = await gateVpnDirectReachability(transport, devOutput);
7508
8459
  if (options.dryRun) {
7509
8460
  printDevPlan(sessionName, appRoot, worktree, devOutput);
@@ -7571,112 +8522,113 @@ function registerDevCommand(program) {
7571
8522
  stack,
7572
8523
  devOutput: finalDevOutput,
7573
8524
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8525
+ cleanEnv: true,
7574
8526
  workflowFingerprint: fingerprintWorkflowSource(appRoot, finalDevOutput.services)
7575
8527
  });
8528
+ saveSessionPortMemory(sessionName, finalDevOutput);
7576
8529
  });
7577
8530
  dev.command("logs <service>").description("Show logs for a service").option("--tail <lines>", "Number of lines", "100").action((service, opts) => {
7578
- const sessionName = dev.opts().session;
7579
- const resolvedSession = sessionName ?? `${getProjectName(findAppRoot2())}-dev`;
7580
- if (loadSessionState(resolvedSession)?.runner === "docker") {
7581
- console.log(dockerDevLogs(resolvedSession, service, parseInt(opts.tail, 10)));
8531
+ const sessionName = sessionNameFor(findAppRoot2(), dev.opts().session);
8532
+ if (loadSessionState(sessionName)?.runner === "docker") {
8533
+ console.log(dockerDevLogs(sessionName, service, parseInt(opts.tail, 10)));
7582
8534
  return;
7583
8535
  }
7584
- if (sessionName) {
7585
- showLogs(sessionName, service, parseInt(opts.tail, 10));
7586
- } else {
7587
- const appRoot = findAppRoot2();
7588
- const projectName = getProjectName(appRoot);
7589
- showLogs(`${projectName}-dev`, service, parseInt(opts.tail, 10));
7590
- }
8536
+ showLogs(sessionName, service, parseInt(opts.tail, 10));
7591
8537
  });
7592
8538
  dev.command("restart <service>").description("Restart a service").option("--stack <stack>", "Pulumi stack name").addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp()).option(
7593
8539
  "--refresh-env",
7594
8540
  "Regenerate this service's env from current stack/SSM outputs before restarting (for config/deploy changes)"
7595
- ).option("--force", "Restart the worker even if workflow code changed since launch (may strand in-flight conversations)").action(async (service, opts) => {
7596
- let ctx = null;
7597
- try {
7598
- const parentSession = dev.opts().session;
7599
- const parentProfile = dev.opts().profile;
7600
- if (parentProfile) {
7601
- applyAwsProfileOverride(parentProfile);
7602
- }
7603
- const appRoot = findAppRoot2();
7604
- const projectName = getProjectName(appRoot);
7605
- const sessionName = parentSession ?? `${projectName}-dev`;
7606
- const state = loadSessionState(sessionName);
7607
- if (state?.runner === "docker") {
7608
- dockerDevRestart(sessionName, service);
7609
- logSuccess(`Restarted: ${service}`);
7610
- return;
7611
- }
7612
- if (/worker/i.test(service) && workflowChangeWouldStrand(state, appRoot, !!opts.force)) {
7613
- process.exit(1);
7614
- }
7615
- const stack = state?.stack ?? detectStack(appRoot, resolveStackOption(opts));
7616
- ctx = derivePlatformContext(appRoot, stack);
7617
- const defaultDeployerRole = readStackConfig(appRoot, stack, "mesh:deployerRole");
7618
- const adminDeployerRole = readStackConfig(appRoot, stack, "mesh:adminDeployerRole");
7619
- let deployerRole = defaultDeployerRole;
7620
- if (defaultDeployerRole) {
7621
- const meshCreds = ctx ? readCredentials(ctx) : null;
7622
- const idToken = meshCreds && new Date(meshCreds.expiresAt) > /* @__PURE__ */ new Date() ? meshCreds.idToken : null;
7623
- deployerRole = selectRoleForCaller(idToken, {
7624
- defaultRole: defaultDeployerRole,
7625
- adminRole: adminDeployerRole ?? void 0
8541
+ ).option("--force", "Restart the worker even if workflow code changed since launch (may strand in-flight conversations)").option(
8542
+ "--wait <seconds>",
8543
+ "How long to wait for the service to prove ready (port accepting, or a Temporal worker RUNNING) before reporting; an exit in that window fails the restart",
8544
+ String(RESTART_READY_TIMEOUT_MS / 1e3)
8545
+ ).option("--no-wait", "Return as soon as the command is typed, without checking readiness").action(
8546
+ async (service, opts) => {
8547
+ let ctx = null;
8548
+ try {
8549
+ const parentSession = dev.opts().session;
8550
+ const parentProfile = dev.opts().profile;
8551
+ if (parentProfile) {
8552
+ applyAwsProfileOverride(parentProfile);
8553
+ }
8554
+ const appRoot = findAppRoot2();
8555
+ const sessionName = sessionNameFor(appRoot, parentSession);
8556
+ const state = loadSessionState(sessionName);
8557
+ if (state?.runner === "docker") {
8558
+ dockerDevRestart(sessionName, service);
8559
+ logSuccess(`Restarted: ${service}`);
8560
+ return;
8561
+ }
8562
+ if (/worker/i.test(service) && workflowChangeWouldStrand(state, appRoot, !!opts.force)) {
8563
+ process.exit(1);
8564
+ }
8565
+ const stack = state?.stack ?? detectStack(appRoot, resolveStackOption(opts));
8566
+ ctx = derivePlatformContext(appRoot, stack);
8567
+ const defaultDeployerRole = readStackConfig(appRoot, stack, "mesh:deployerRole");
8568
+ const adminDeployerRole = readStackConfig(appRoot, stack, "mesh:adminDeployerRole");
8569
+ let deployerRole = defaultDeployerRole;
8570
+ if (defaultDeployerRole) {
8571
+ const meshCreds = ctx ? readCredentials(ctx) : null;
8572
+ const idToken = meshCreds && new Date(meshCreds.expiresAt) > /* @__PURE__ */ new Date() ? meshCreds.idToken : null;
8573
+ deployerRole = selectRoleForCaller(idToken, {
8574
+ defaultRole: defaultDeployerRole,
8575
+ adminRole: adminDeployerRole ?? void 0
8576
+ });
8577
+ }
8578
+ const resolved = deployerRole ? await resolveAwsCredentials(deployerRole, appRoot, stack) : null;
8579
+ if (deployerRole && !resolved && !process.env.AWS_ACCESS_KEY_ID) {
8580
+ const pf = ctx ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx, readStackPlatform(appRoot, stack)) : null;
8581
+ logError(
8582
+ pf?.message ?? `Couldn't resolve AWS credentials to restart '${service}'. Run: mesh login ${ctx ?? "mesh.dev"} --device`
8583
+ );
8584
+ process.exit(1);
8585
+ }
8586
+ const baseAwsEnv = resolved?.env ?? getAwsEnvVars();
8587
+ const awsRegion = readStackConfig(appRoot, stack, "aws:region");
8588
+ if (awsRegion && !baseAwsEnv.AWS_REGION) {
8589
+ baseAwsEnv.AWS_REGION = awsRegion;
8590
+ }
8591
+ Object.assign(process.env, baseAwsEnv);
8592
+ const profileHint = parentProfile ?? process.env.AWS_PROFILE ?? "mesh-dev";
8593
+ const zitadelOpts = resolved?.method === "zitadel" && deployerRole && ctx ? {
8594
+ context: ctx,
8595
+ roleArn: deployerRole,
8596
+ sessionScratchDir: getSessionEnvDir(sessionName),
8597
+ meshBin: resolveStableMeshBin(process.argv[1])
8598
+ } : void 0;
8599
+ const awsEnv = buildChildAwsEnv(baseAwsEnv, resolved?.method ?? "ambient", profileHint, zitadelOpts);
8600
+ const devOutput = state?.devOutput ?? getDevOutput(appRoot, stack, baseAwsEnv);
8601
+ const readiness = await restartService(sessionName, service, appRoot, devOutput, awsEnv, {
8602
+ refreshEnv: !!opts.refreshEnv,
8603
+ stack,
8604
+ sessionState: state,
8605
+ waitMs: parseWaitSeconds(opts.wait, RESTART_READY_TIMEOUT_MS / 1e3) * 1e3
7626
8606
  });
7627
- }
7628
- const resolved = deployerRole ? await resolveAwsCredentials(deployerRole, appRoot, stack) : null;
7629
- if (deployerRole && !resolved && !process.env.AWS_ACCESS_KEY_ID) {
7630
- const pf = ctx ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx, readStackPlatform(appRoot, stack)) : null;
7631
- logError(
7632
- pf?.message ?? `Couldn't resolve AWS credentials to restart '${service}'. Run: mesh login ${ctx ?? "mesh.dev"} --device`
8607
+ if (/worker/i.test(service) && state) {
8608
+ saveSessionState(sessionName, {
8609
+ ...state,
8610
+ workflowFingerprint: fingerprintWorkflowSource(appRoot, state.devOutput?.services)
8611
+ });
8612
+ }
8613
+ if (readiness?.state === "exited" || readiness?.state === "port-held") process.exit(1);
8614
+ } catch (err) {
8615
+ logError(`Failed to restart '${service}': ${err?.message ?? String(err)}`);
8616
+ logInfo(
8617
+ `If this is a credential/kubeconfig issue, run \`mesh login ${ctx ?? "<your platform context>"} --device\`, or \`mesh dev\` to relaunch the session cleanly.`
7633
8618
  );
7634
8619
  process.exit(1);
7635
8620
  }
7636
- const baseAwsEnv = resolved?.env ?? getAwsEnvVars();
7637
- const awsRegion = readStackConfig(appRoot, stack, "aws:region");
7638
- if (awsRegion && !baseAwsEnv.AWS_REGION) {
7639
- baseAwsEnv.AWS_REGION = awsRegion;
7640
- }
7641
- Object.assign(process.env, baseAwsEnv);
7642
- const profileHint = parentProfile ?? process.env.AWS_PROFILE ?? "mesh-dev";
7643
- const zitadelOpts = resolved?.method === "zitadel" && deployerRole && ctx ? {
7644
- context: ctx,
7645
- roleArn: deployerRole,
7646
- sessionScratchDir: getSessionEnvDir(sessionName),
7647
- meshBin: resolveStableMeshBin(process.argv[1])
7648
- } : void 0;
7649
- const awsEnv = buildChildAwsEnv(baseAwsEnv, resolved?.method ?? "ambient", profileHint, zitadelOpts);
7650
- const devOutput = state?.devOutput ?? getDevOutput(appRoot, stack, baseAwsEnv);
7651
- await restartService(sessionName, service, appRoot, devOutput, awsEnv, {
7652
- refreshEnv: !!opts.refreshEnv,
7653
- stack,
7654
- sessionState: state
7655
- });
7656
- if (/worker/i.test(service) && state) {
7657
- saveSessionState(sessionName, {
7658
- ...state,
7659
- workflowFingerprint: fingerprintWorkflowSource(appRoot, state.devOutput?.services)
7660
- });
7661
- }
7662
- } catch (err) {
7663
- logError(`Failed to restart '${service}': ${err?.message ?? String(err)}`);
7664
- logInfo(
7665
- `If this is a credential/kubeconfig issue, run \`mesh login ${ctx ?? "<your platform context>"} --device\`, or \`mesh dev\` to relaunch the session cleanly.`
7666
- );
7667
- process.exit(1);
7668
8621
  }
7669
- });
8622
+ );
7670
8623
  dev.command("doctor").description(
7671
- "Diagnose the dev session (creds, tunnels, config, ports, worktree, Temporal) \u2014 names the fix"
8624
+ "Diagnose the dev session (creds, registry, config, ports, service processes, worktree, tunnels, Temporal) \u2014 names the fix"
7672
8625
  ).option("--stack <stack>", "Pulumi stack name").addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp()).option("--json", "Machine-readable output").action(async (opts) => {
7673
8626
  const parentProfile = dev.opts().profile;
7674
8627
  if (parentProfile) {
7675
8628
  applyAwsProfileOverride(parentProfile);
7676
8629
  }
7677
8630
  const appRoot = findAppRoot2();
7678
- const projectName = getProjectName(appRoot);
7679
- const sessionName = dev.opts().session ?? `${projectName}-dev`;
8631
+ const sessionName = sessionNameFor(appRoot, dev.opts().session);
7680
8632
  const state = loadSessionState(sessionName);
7681
8633
  const stack = state?.stack ?? detectStack(appRoot, resolveStackOption(opts));
7682
8634
  const asJson = !!opts.json || !!dev.opts().json;
@@ -7804,17 +8756,17 @@ function registerDevCommand(program) {
7804
8756
  // packages/mesh-cli/src/utils/tailscale-targets.ts
7805
8757
  var PORT_BLOCK = 3e3;
7806
8758
  function tenantPortIndex(tenant, registryPath) {
7807
- const p = registryPath ?? path17.join(CONFIG_DIR, "tailscale", "port-registry.json");
8759
+ const p = registryPath ?? path19.join(CONFIG_DIR, "tailscale", "port-registry.json");
7808
8760
  let reg;
7809
8761
  try {
7810
- reg = JSON.parse(fs16.readFileSync(p, "utf8"));
8762
+ reg = JSON.parse(fs18.readFileSync(p, "utf8"));
7811
8763
  } catch {
7812
8764
  reg = { mesh: 0 };
7813
8765
  }
7814
8766
  if (tenant in reg) return reg[tenant];
7815
8767
  const idx = Math.max(-1, ...Object.values(reg)) + 1;
7816
8768
  reg[tenant] = idx;
7817
- fs16.mkdirSync(path17.dirname(p), { recursive: true });
8769
+ fs18.mkdirSync(path19.dirname(p), { recursive: true });
7818
8770
  atomicWriteFileSync(p, JSON.stringify(reg, null, 2), 384);
7819
8771
  return idx;
7820
8772
  }
@@ -7842,7 +8794,7 @@ function resolveTunnelTargets(services, resolveIp, portOffset = 0) {
7842
8794
  }
7843
8795
  function resolveElbIp(host) {
7844
8796
  try {
7845
- const out = execFileSync11("dig", ["+short", host, "@1.1.1.1"], {
8797
+ const out = execFileSync12("dig", ["+short", host, "@1.1.1.1"], {
7846
8798
  encoding: "utf8",
7847
8799
  stdio: ["ignore", "pipe", "ignore"]
7848
8800
  });
@@ -7926,35 +8878,35 @@ function createForwarder(opts) {
7926
8878
 
7927
8879
  // packages/mesh-cli/src/utils/tunnel-ownership.ts
7928
8880
  import * as crypto2 from "node:crypto";
7929
- import * as fs17 from "node:fs";
8881
+ import * as fs19 from "node:fs";
7930
8882
  import * as net5 from "node:net";
7931
- import * as path18 from "node:path";
7932
- import { execFileSync as execFileSync12 } from "node:child_process";
8883
+ import * as path20 from "node:path";
8884
+ import { execFileSync as execFileSync13 } from "node:child_process";
7933
8885
  function mintRunnerToken() {
7934
8886
  return crypto2.randomBytes(16).toString("hex");
7935
8887
  }
7936
8888
  function readRunnerManifest(file) {
7937
- if (!fs17.existsSync(file)) return null;
8889
+ if (!fs19.existsSync(file)) return null;
7938
8890
  try {
7939
- return JSON.parse(fs17.readFileSync(file, "utf8"));
8891
+ return JSON.parse(fs19.readFileSync(file, "utf8"));
7940
8892
  } catch {
7941
8893
  return null;
7942
8894
  }
7943
8895
  }
7944
8896
  function writeRunnerManifest(file, manifest) {
7945
- fs17.mkdirSync(path18.dirname(file), { recursive: true });
7946
- fs17.writeFileSync(file, JSON.stringify(manifest, null, 2), { mode: 384 });
8897
+ fs19.mkdirSync(path20.dirname(file), { recursive: true });
8898
+ fs19.writeFileSync(file, JSON.stringify(manifest, null, 2), { mode: 384 });
7947
8899
  }
7948
8900
  function clearRunnerManifest(file, token) {
7949
8901
  if (token !== void 0 && readRunnerManifest(file)?.token !== token) return;
7950
8902
  try {
7951
- fs17.unlinkSync(file);
8903
+ fs19.unlinkSync(file);
7952
8904
  } catch {
7953
8905
  }
7954
8906
  }
7955
8907
  function getProcessGroupId(pid) {
7956
8908
  try {
7957
- const out = execFileSync12("ps", ["-o", "pgid=", "-p", String(pid)], {
8909
+ const out = execFileSync13("ps", ["-o", "pgid=", "-p", String(pid)], {
7958
8910
  encoding: "utf8",
7959
8911
  stdio: ["ignore", "pipe", "ignore"]
7960
8912
  }).trim();
@@ -7980,7 +8932,7 @@ function parseRunnerPidsFromPs(psOutput, tenant, exclude) {
7980
8932
  }
7981
8933
  function discoverRunnerPids(tenant, exclude) {
7982
8934
  try {
7983
- const out = execFileSync12("ps", ["ax", "-o", "pid=,command="], {
8935
+ const out = execFileSync13("ps", ["ax", "-o", "pid=,command="], {
7984
8936
  encoding: "utf8",
7985
8937
  stdio: ["ignore", "pipe", "ignore"]
7986
8938
  });
@@ -8072,7 +9024,7 @@ async function pollOwnership(check, deadlineMs) {
8072
9024
  }
8073
9025
  function describePortOwner(port) {
8074
9026
  try {
8075
- const out = execFileSync12("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fcp"], {
9027
+ const out = execFileSync13("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fcp"], {
8076
9028
  encoding: "utf8",
8077
9029
  stdio: ["ignore", "pipe", "ignore"]
8078
9030
  });
@@ -8114,58 +9066,58 @@ function buildSquatterError(tenant, squatted, detail) {
8114
9066
  // packages/mesh-cli/src/utils/tailscale.ts
8115
9067
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
8116
9068
  function tenantStateDir(tenant) {
8117
- return path19.join(CONFIG_DIR, "tailscale", tenant);
9069
+ return path21.join(CONFIG_DIR, "tailscale", tenant);
8118
9070
  }
8119
9071
  function socketPath(tenant) {
8120
- return path19.join(tenantStateDir(tenant), "tailscaled.sock");
9072
+ return path21.join(tenantStateDir(tenant), "tailscaled.sock");
8121
9073
  }
8122
9074
  function logPath(tenant) {
8123
- return path19.join(tenantStateDir(tenant), "tailscaled.log");
9075
+ return path21.join(tenantStateDir(tenant), "tailscaled.log");
8124
9076
  }
8125
9077
  function statePath(tenant) {
8126
- return path19.join(tenantStateDir(tenant), "tunnel-state.json");
9078
+ return path21.join(tenantStateDir(tenant), "tunnel-state.json");
8127
9079
  }
8128
9080
  function daemonMetaPath(tenant) {
8129
- return path19.join(tenantStateDir(tenant), "daemon.json");
9081
+ return path21.join(tenantStateDir(tenant), "daemon.json");
8130
9082
  }
8131
9083
  function runnerManifestPath(tenant) {
8132
- return path19.join(tenantStateDir(tenant), "runner.json");
9084
+ return path21.join(tenantStateDir(tenant), "runner.json");
8133
9085
  }
8134
9086
  function readTunnelState(tenant) {
8135
9087
  const p = statePath(tenant);
8136
- if (!fs18.existsSync(p)) return null;
9088
+ if (!fs20.existsSync(p)) return null;
8137
9089
  try {
8138
- return JSON.parse(fs18.readFileSync(p, "utf8"));
9090
+ return JSON.parse(fs20.readFileSync(p, "utf8"));
8139
9091
  } catch {
8140
9092
  return null;
8141
9093
  }
8142
9094
  }
8143
9095
  function writeTunnelState(tenant, state) {
8144
- fs18.mkdirSync(tenantStateDir(tenant), { recursive: true });
9096
+ fs20.mkdirSync(tenantStateDir(tenant), { recursive: true });
8145
9097
  atomicWriteFileSync(statePath(tenant), JSON.stringify(state, null, 2), 384);
8146
9098
  }
8147
9099
  function clearTunnelState(tenant) {
8148
9100
  try {
8149
- fs18.unlinkSync(statePath(tenant));
9101
+ fs20.unlinkSync(statePath(tenant));
8150
9102
  } catch {
8151
9103
  }
8152
9104
  }
8153
9105
  function readDaemonMeta(tenant) {
8154
9106
  const p = daemonMetaPath(tenant);
8155
- if (!fs18.existsSync(p)) return null;
9107
+ if (!fs20.existsSync(p)) return null;
8156
9108
  try {
8157
- return JSON.parse(fs18.readFileSync(p, "utf8"));
9109
+ return JSON.parse(fs20.readFileSync(p, "utf8"));
8158
9110
  } catch {
8159
9111
  return null;
8160
9112
  }
8161
9113
  }
8162
9114
  function writeDaemonMeta(tenant, meta) {
8163
- fs18.mkdirSync(tenantStateDir(tenant), { recursive: true });
9115
+ fs20.mkdirSync(tenantStateDir(tenant), { recursive: true });
8164
9116
  atomicWriteFileSync(daemonMetaPath(tenant), JSON.stringify(meta, null, 2), 384);
8165
9117
  }
8166
9118
  function clearDaemonMeta(tenant) {
8167
9119
  try {
8168
- fs18.unlinkSync(daemonMetaPath(tenant));
9120
+ fs20.unlinkSync(daemonMetaPath(tenant));
8169
9121
  } catch {
8170
9122
  }
8171
9123
  }
@@ -8185,18 +9137,18 @@ function tailscaleBinPath() {
8185
9137
  }
8186
9138
  function resolveBrewBin(name) {
8187
9139
  const brew = `/opt/homebrew/bin/${name}`;
8188
- if (fs18.existsSync(brew)) return brew;
9140
+ if (fs20.existsSync(brew)) return brew;
8189
9141
  try {
8190
- const p = execFileSync13("which", [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9142
+ const p = execFileSync14("which", [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8191
9143
  if (p) return p;
8192
9144
  } catch {
8193
9145
  }
8194
9146
  throw new Error(`${name} not found \u2014 install the standalone binary: brew install tailscale`);
8195
9147
  }
8196
9148
  function daemonState(tenant) {
8197
- if (!fs18.existsSync(socketPath(tenant))) return { backendState: "Down" };
9149
+ if (!fs20.existsSync(socketPath(tenant))) return { backendState: "Down" };
8198
9150
  try {
8199
- const out = execFileSync13(
9151
+ const out = execFileSync14(
8200
9152
  tailscaleBinPath(),
8201
9153
  ["--socket", socketPath(tenant), "status", "--json"],
8202
9154
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -8216,7 +9168,7 @@ function findRunningDaemon(tenant) {
8216
9168
  const sock = socketPath(tenant);
8217
9169
  let out;
8218
9170
  try {
8219
- out = execFileSync13("ps", ["ax", "-o", "pid=,command="], {
9171
+ out = execFileSync14("ps", ["ax", "-o", "pid=,command="], {
8220
9172
  encoding: "utf8",
8221
9173
  stdio: ["ignore", "pipe", "ignore"]
8222
9174
  });
@@ -8268,8 +9220,8 @@ async function ensureDaemon(tenant, opts) {
8268
9220
  }
8269
9221
  }
8270
9222
  const dir = tenantStateDir(tenant);
8271
- fs18.mkdirSync(dir, { recursive: true });
8272
- const log = fs18.openSync(logPath(tenant), "a");
9223
+ fs20.mkdirSync(dir, { recursive: true });
9224
+ const log = fs20.openSync(logPath(tenant), "a");
8273
9225
  try {
8274
9226
  const child = spawn2(
8275
9227
  tailscaledBinPath(),
@@ -8289,7 +9241,7 @@ async function ensureDaemon(tenant, opts) {
8289
9241
  child.unref();
8290
9242
  if (child.pid) writeDaemonMeta(tenant, { socksPort: opts.socksPort, pid: child.pid });
8291
9243
  } finally {
8292
- fs18.closeSync(log);
9244
+ fs20.closeSync(log);
8293
9245
  }
8294
9246
  for (let i = 0; i < 40; i++) {
8295
9247
  await sleep(250);
@@ -8315,7 +9267,7 @@ function stopDaemon(tenant) {
8315
9267
  }
8316
9268
  for (const p of [socketPath(tenant)]) {
8317
9269
  try {
8318
- fs18.unlinkSync(p);
9270
+ fs20.unlinkSync(p);
8319
9271
  } catch {
8320
9272
  }
8321
9273
  }
@@ -8337,7 +9289,7 @@ function buildTailscaleUpArgs(opts) {
8337
9289
  }
8338
9290
  async function joinHeadscale(tenant, loginServer, opts = {}) {
8339
9291
  if (daemonState(tenant).backendState === "Running") return;
8340
- const hostname2 = `mesh-${tenant}-${os4.hostname().split(".")[0]}`;
9292
+ const hostname2 = `mesh-${tenant}-${os3.hostname().split(".")[0]}`;
8341
9293
  const child = spawn2(
8342
9294
  tailscaleBinPath(),
8343
9295
  buildTailscaleUpArgs({ socket: socketPath(tenant), loginServer, hostname: hostname2, preAuthKey: opts.preAuthKey }),
@@ -8380,7 +9332,7 @@ async function joinHeadscale(tenant, loginServer, opts = {}) {
8380
9332
  }
8381
9333
  function logout(tenant) {
8382
9334
  try {
8383
- execFileSync13(tailscaleBinPath(), ["--socket", socketPath(tenant), "logout"], { stdio: "ignore" });
9335
+ execFileSync14(tailscaleBinPath(), ["--socket", socketPath(tenant), "logout"], { stdio: "ignore" });
8384
9336
  } catch {
8385
9337
  }
8386
9338
  }
@@ -8460,7 +9412,7 @@ async function assertTunnelPortsFree(tenant, ports) {
8460
9412
  async function ensureForwarders(state) {
8461
9413
  const prior = readTunnelState(state.tenant);
8462
9414
  const expected = { socksPort: state.socksPort, ports: state.tunnels.map((t) => t.localPort) };
8463
- const runnerAlive = prior?.managed === "launchd" ? fs18.existsSync(launchAgentPath(state.tenant)) : pidAlive(prior?.supervisorPid);
9415
+ const runnerAlive = prior?.managed === "launchd" ? fs20.existsSync(launchAgentPath(state.tenant)) : pidAlive(prior?.supervisorPid);
8464
9416
  if (prior && runnerAlive && tunnelTargetsEqual(prior.tunnels, state.tunnels) && // A prior supervisor spawned for a DIFFERENT SOCKS port still binds the same
8465
9417
  // local ports but dials a dead upstream — reusing it silently strands every
8466
9418
  // tunnel. The port must match, and the runner must PROVE it is
@@ -8485,7 +9437,7 @@ async function ensureForwarders(state) {
8485
9437
  if (launchdAvailable()) {
8486
9438
  state.managed = "launchd";
8487
9439
  writeTunnelState(state.tenant, state);
8488
- if (path19.isAbsolute(meshBin) && installLaunchAgent(state.tenant, meshBin)) {
9440
+ if (path21.isAbsolute(meshBin) && installLaunchAgent(state.tenant, meshBin)) {
8489
9441
  if ((await awaitOwnership(state.tenant, expected, 8e3)).ok) return 0;
8490
9442
  uninstallLaunchAgent(state.tenant);
8491
9443
  await assertTunnelPortsFree(state.tenant, expected.ports);
@@ -8520,8 +9472,12 @@ async function ensureForwarders(state) {
8520
9472
  }
8521
9473
  function rewriteTunnelsForTargets(devOutput, targets) {
8522
9474
  const tunnels = { ...devOutput.tunnels };
9475
+ for (const [name, tunnel] of Object.entries(devOutput.tunnels)) {
9476
+ const target = targets.find((t) => t.name === name) ?? targets.find((t) => t.targetHost === tunnel.host && t.remotePort === tunnel.port);
9477
+ if (target) tunnels[name] = { host: "localhost", port: target.localPort };
9478
+ }
8523
9479
  for (const t of targets) {
8524
- tunnels[t.name] = { host: "localhost", port: t.localPort };
9480
+ tunnels[t.name] ??= { host: "localhost", port: t.localPort };
8525
9481
  }
8526
9482
  return { ...devOutput, tunnels };
8527
9483
  }
@@ -8613,7 +9569,7 @@ function forwardersNeedingRebind(targets, listening) {
8613
9569
  return targets.filter((t) => !listening.has(t.localPort));
8614
9570
  }
8615
9571
  function launchAgentPath(tenant) {
8616
- return path19.join(os4.homedir(), "Library", "LaunchAgents", `${launchAgentLabel(tenant)}.plist`);
9572
+ return path21.join(os3.homedir(), "Library", "LaunchAgents", `${launchAgentLabel(tenant)}.plist`);
8617
9573
  }
8618
9574
  function guiDomain() {
8619
9575
  return `gui/${process.getuid?.() ?? 0}`;
@@ -8622,8 +9578,8 @@ var _launchdAvailable;
8622
9578
  function launchdAvailable() {
8623
9579
  if (_launchdAvailable !== void 0) return _launchdAvailable;
8624
9580
  try {
8625
- execFileSync13("which", ["launchctl"], { stdio: "ignore" });
8626
- execFileSync13("launchctl", ["print", guiDomain()], { stdio: "ignore" });
9581
+ execFileSync14("which", ["launchctl"], { stdio: "ignore" });
9582
+ execFileSync14("launchctl", ["print", guiDomain()], { stdio: "ignore" });
8627
9583
  _launchdAvailable = true;
8628
9584
  } catch {
8629
9585
  _launchdAvailable = false;
@@ -8635,30 +9591,30 @@ function installLaunchAgent(tenant, meshBin) {
8635
9591
  try {
8636
9592
  let realMeshBin = meshBin;
8637
9593
  try {
8638
- realMeshBin = fs18.realpathSync(meshBin);
9594
+ realMeshBin = fs20.realpathSync(meshBin);
8639
9595
  } catch {
8640
9596
  }
8641
9597
  let pathEnv = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
8642
9598
  try {
8643
- pathEnv = `${path19.dirname(tailscaledBinPath())}:${pathEnv}`;
9599
+ pathEnv = `${path21.dirname(tailscaledBinPath())}:${pathEnv}`;
8644
9600
  } catch {
8645
9601
  }
8646
- fs18.mkdirSync(path19.dirname(plistPath), { recursive: true });
8647
- fs18.writeFileSync(
9602
+ fs20.mkdirSync(path21.dirname(plistPath), { recursive: true });
9603
+ fs20.writeFileSync(
8648
9604
  plistPath,
8649
9605
  renderLaunchAgentPlist({
8650
9606
  tenant,
8651
9607
  meshBin: realMeshBin,
8652
9608
  nodeBin: process.execPath,
8653
- logPath: path19.join(tenantStateDir(tenant), "runner.log"),
9609
+ logPath: path21.join(tenantStateDir(tenant), "runner.log"),
8654
9610
  pathEnv
8655
9611
  })
8656
9612
  );
8657
9613
  try {
8658
- execFileSync13("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
9614
+ execFileSync14("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
8659
9615
  } catch {
8660
9616
  }
8661
- execFileSync13("launchctl", ["bootstrap", guiDomain(), plistPath], { stdio: "ignore" });
9617
+ execFileSync14("launchctl", ["bootstrap", guiDomain(), plistPath], { stdio: "ignore" });
8662
9618
  return true;
8663
9619
  } catch {
8664
9620
  return false;
@@ -8667,11 +9623,11 @@ function installLaunchAgent(tenant, meshBin) {
8667
9623
  function uninstallLaunchAgent(tenant) {
8668
9624
  const plistPath = launchAgentPath(tenant);
8669
9625
  try {
8670
- execFileSync13("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
9626
+ execFileSync14("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
8671
9627
  } catch {
8672
9628
  }
8673
9629
  try {
8674
- fs18.unlinkSync(plistPath);
9630
+ fs20.unlinkSync(plistPath);
8675
9631
  } catch {
8676
9632
  }
8677
9633
  }
@@ -8900,7 +9856,7 @@ function headscaleExec(namespace, args, opts) {
8900
9856
  stdio: ["pipe", "pipe", "pipe"]
8901
9857
  };
8902
9858
  try {
8903
- const result = execFileSync14(
9859
+ const result = execFileSync15(
8904
9860
  "kubectl",
8905
9861
  [
8906
9862
  "exec",
@@ -8929,7 +9885,7 @@ function clusterName(tenant, env) {
8929
9885
  }
8930
9886
  function assertPodReady(namespace, options) {
8931
9887
  try {
8932
- const output = execFileSync14(
9888
+ const output = execFileSync15(
8933
9889
  "kubectl",
8934
9890
  [
8935
9891
  "get",
@@ -8964,7 +9920,7 @@ async function vpnStatus(options) {
8964
9920
  const ns = resolveNamespace(options);
8965
9921
  logInfo(`Checking Headscale in namespace ${ns}...`);
8966
9922
  assertPodReady(ns, options);
8967
- const podJson = execFileSync14(
9923
+ const podJson = execFileSync15(
8968
9924
  "kubectl",
8969
9925
  [
8970
9926
  "get",
@@ -8986,7 +9942,7 @@ async function vpnStatus(options) {
8986
9942
  const restarts = container?.restartCount ?? 0;
8987
9943
  let endpoint = "unknown";
8988
9944
  try {
8989
- endpoint = execFileSync14(
9945
+ endpoint = execFileSync15(
8990
9946
  "kubectl",
8991
9947
  [
8992
9948
  "get",
@@ -9141,7 +10097,7 @@ function findFreePort3() {
9141
10097
  }
9142
10098
  function findTailscale() {
9143
10099
  try {
9144
- return execFileSync14("which", ["tailscale"], {
10100
+ return execFileSync15("which", ["tailscale"], {
9145
10101
  encoding: "utf-8",
9146
10102
  stdio: ["pipe", "pipe", "pipe"]
9147
10103
  }).trim() || null;
@@ -9151,7 +10107,7 @@ function findTailscale() {
9151
10107
  }
9152
10108
  function tailscaleStatus() {
9153
10109
  try {
9154
- const json = execFileSync14("tailscale", ["status", "--json"], {
10110
+ const json = execFileSync15("tailscale", ["status", "--json"], {
9155
10111
  encoding: "utf-8",
9156
10112
  stdio: ["pipe", "pipe", "pipe"]
9157
10113
  });
@@ -9232,7 +10188,7 @@ async function vpnConnectSystem(endpoint) {
9232
10188
  "--accept-routes"
9233
10189
  ];
9234
10190
  try {
9235
- execFileSync14("tailscale", upArgs, { stdio: "inherit" });
10191
+ execFileSync15("tailscale", upArgs, { stdio: "inherit" });
9236
10192
  console.log("");
9237
10193
  logSuccess("VPN connected.");
9238
10194
  } catch (error) {
@@ -9259,7 +10215,7 @@ async function vpnDisconnect(opts) {
9259
10215
  }
9260
10216
  logInfo("Disconnecting system VPN\u2026");
9261
10217
  try {
9262
- execFileSync14("tailscale", ["down"], { stdio: "inherit" });
10218
+ execFileSync15("tailscale", ["down"], { stdio: "inherit" });
9263
10219
  logSuccess("VPN disconnected.");
9264
10220
  } catch (error) {
9265
10221
  const code = error.status;
@@ -9311,12 +10267,12 @@ function registerVpnCommands(program) {
9311
10267
  }
9312
10268
 
9313
10269
  // packages/mesh-cli/src/commands/login.ts
9314
- var CONFIG_DIR = path20.join(
9315
- process.env.XDG_CONFIG_HOME ?? path20.join(process.env.HOME ?? "~", ".config"),
10270
+ var CONFIG_DIR = path22.join(
10271
+ process.env.XDG_CONFIG_HOME ?? path22.join(process.env.HOME ?? "~", ".config"),
9316
10272
  "mesh"
9317
10273
  );
9318
- var CONFIG_FILE = path20.join(CONFIG_DIR, "config.json");
9319
- var CREDENTIALS_FILE = path20.join(CONFIG_DIR, "credentials.json");
10274
+ var CONFIG_FILE = path22.join(CONFIG_DIR, "config.json");
10275
+ var CREDENTIALS_FILE = path22.join(CONFIG_DIR, "credentials.json");
9320
10276
  var REDIRECT_PORT = 9876;
9321
10277
  var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
9322
10278
  var BASE_SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
@@ -9332,9 +10288,9 @@ function withDiscoveredFields(config, data) {
9332
10288
  return out;
9333
10289
  }
9334
10290
  function readConfig() {
9335
- if (!fs19.existsSync(CONFIG_FILE)) return {};
10291
+ if (!fs21.existsSync(CONFIG_FILE)) return {};
9336
10292
  try {
9337
- return JSON.parse(fs19.readFileSync(CONFIG_FILE, "utf-8"));
10293
+ return JSON.parse(fs21.readFileSync(CONFIG_FILE, "utf-8"));
9338
10294
  } catch {
9339
10295
  return {};
9340
10296
  }
@@ -9342,15 +10298,15 @@ function readConfig() {
9342
10298
  function writeContextConfig(context, config) {
9343
10299
  const existing = readConfig();
9344
10300
  existing[context] = config;
9345
- fs19.mkdirSync(CONFIG_DIR, { recursive: true });
9346
- fs19.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
10301
+ fs21.mkdirSync(CONFIG_DIR, { recursive: true });
10302
+ fs21.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
9347
10303
  }
9348
10304
  function clearContextConfig(context) {
9349
10305
  const existing = readConfig();
9350
10306
  if (!(context in existing)) return;
9351
10307
  delete existing[context];
9352
- fs19.mkdirSync(CONFIG_DIR, { recursive: true });
9353
- fs19.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
10308
+ fs21.mkdirSync(CONFIG_DIR, { recursive: true });
10309
+ fs21.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
9354
10310
  }
9355
10311
  function getContextConfig(context) {
9356
10312
  const config = readConfig();
@@ -9457,9 +10413,9 @@ function discoverConfigFromWellKnown(domain, contextKey = domain, opts = {}) {
9457
10413
  return discoverConfig(domain, contextKey, opts);
9458
10414
  }
9459
10415
  function readAllCredentials() {
9460
- if (!fs19.existsSync(CREDENTIALS_FILE)) return {};
10416
+ if (!fs21.existsSync(CREDENTIALS_FILE)) return {};
9461
10417
  try {
9462
- return JSON.parse(fs19.readFileSync(CREDENTIALS_FILE, "utf-8"));
10418
+ return JSON.parse(fs21.readFileSync(CREDENTIALS_FILE, "utf-8"));
9463
10419
  } catch {
9464
10420
  return {};
9465
10421
  }
@@ -9468,21 +10424,21 @@ function readCredentials(context) {
9468
10424
  return readAllCredentials()[context] ?? null;
9469
10425
  }
9470
10426
  var atomicWriteCounter = 0;
9471
- function atomicWriteFileSync(path21, data, mode) {
9472
- const tmpPath = `${path21}.${process.pid}.${atomicWriteCounter++}.tmp`;
10427
+ function atomicWriteFileSync(path23, data, mode) {
10428
+ const tmpPath = `${path23}.${process.pid}.${atomicWriteCounter++}.tmp`;
9473
10429
  try {
9474
- fs19.writeFileSync(tmpPath, data, { mode });
9475
- fs19.renameSync(tmpPath, path21);
10430
+ fs21.writeFileSync(tmpPath, data, { mode });
10431
+ fs21.renameSync(tmpPath, path23);
9476
10432
  } catch (err) {
9477
10433
  try {
9478
- fs19.unlinkSync(tmpPath);
10434
+ fs21.unlinkSync(tmpPath);
9479
10435
  } catch {
9480
10436
  }
9481
10437
  throw err;
9482
10438
  }
9483
10439
  }
9484
10440
  function writeCredentials(context, creds) {
9485
- fs19.mkdirSync(CONFIG_DIR, { recursive: true });
10441
+ fs21.mkdirSync(CONFIG_DIR, { recursive: true });
9486
10442
  const all = readAllCredentials();
9487
10443
  all[context] = creds;
9488
10444
  atomicWriteFileSync(CREDENTIALS_FILE, JSON.stringify(all, null, 2), 384);
@@ -9490,8 +10446,8 @@ function writeCredentials(context, creds) {
9490
10446
  function clearCredentials(context) {
9491
10447
  const all = readAllCredentials();
9492
10448
  delete all[context];
9493
- fs19.mkdirSync(CONFIG_DIR, { recursive: true });
9494
- fs19.writeFileSync(CREDENTIALS_FILE, JSON.stringify(all, null, 2), {
10449
+ fs21.mkdirSync(CONFIG_DIR, { recursive: true });
10450
+ fs21.writeFileSync(CREDENTIALS_FILE, JSON.stringify(all, null, 2), {
9495
10451
  mode: 384
9496
10452
  });
9497
10453
  }
@@ -9640,9 +10596,9 @@ function login(context, config) {
9640
10596
  const url = authUrl.toString();
9641
10597
  try {
9642
10598
  if (process.platform === "darwin") {
9643
- execFileSync15("open", [url], { stdio: "ignore" });
10599
+ execFileSync16("open", [url], { stdio: "ignore" });
9644
10600
  } else if (process.platform === "linux") {
9645
- execFileSync15("xdg-open", [url], { stdio: "ignore" });
10601
+ execFileSync16("xdg-open", [url], { stdio: "ignore" });
9646
10602
  } else {
9647
10603
  logInfo(`Open this URL in your browser:
9648
10604
  ${url}`);
@@ -9696,9 +10652,9 @@ async function attemptDeviceCode(context, config) {
9696
10652
  logInfo("Waiting for authorization...");
9697
10653
  try {
9698
10654
  if (process.platform === "darwin") {
9699
- execFileSync15("open", [openUrl], { stdio: "ignore" });
10655
+ execFileSync16("open", [openUrl], { stdio: "ignore" });
9700
10656
  } else if (process.platform === "linux") {
9701
- execFileSync15("xdg-open", [openUrl], { stdio: "ignore" });
10657
+ execFileSync16("xdg-open", [openUrl], { stdio: "ignore" });
9702
10658
  }
9703
10659
  } catch {
9704
10660
  }
@@ -9761,7 +10717,7 @@ function loginTimeoutMessage(context) {
9761
10717
  }
9762
10718
  function isRemoteEnvironment() {
9763
10719
  if (process.env.REMOTE_CONTAINERS || process.env.CODESPACES) return true;
9764
- if (fs19.existsSync("/.dockerenv")) return true;
10720
+ if (fs21.existsSync("/.dockerenv")) return true;
9765
10721
  if (process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
9766
10722
  if (!process.stdout.isTTY || !process.stdin.isTTY) return true;
9767
10723
  return false;
@@ -10015,9 +10971,9 @@ function refuseReservedRegistryContext(context, verb) {
10015
10971
  }
10016
10972
  async function hintRegistryIfMissing() {
10017
10973
  try {
10018
- const { readRegistrySession } = await import("./mesh.registry-identity-ONGUJQAY.js");
10974
+ const { readRegistrySession } = await import("./mesh.registry-identity-3AR27UGO.js");
10019
10975
  if (readRegistrySession()) return;
10020
- const { probeRegistryToken: probeRegistryToken2 } = await import("./mesh.auth-preflight-JO5G7SOF.js");
10976
+ const { probeRegistryToken: probeRegistryToken2 } = await import("./mesh.auth-preflight-AUFQ2FGD.js");
10021
10977
  const probe = await probeRegistryToken2();
10022
10978
  if (probe.state === "missing" || probe.state === "expired") {
10023
10979
  logInfo(REGISTRY_SEPARATE_HINT);
@@ -10149,12 +11105,12 @@ export AWS_REGION=${shellSingleQuote(args.region)}
10149
11105
  `;
10150
11106
  }
10151
11107
  function resolveUserAwsConfigPath(env = process.env) {
10152
- return env.AWS_CONFIG_FILE ?? path20.join(env.HOME ?? "~", ".aws", "config");
11108
+ return env.AWS_CONFIG_FILE ?? path22.join(env.HOME ?? "~", ".aws", "config");
10153
11109
  }
10154
11110
  function canWriteDir(dir) {
10155
11111
  try {
10156
- fs19.mkdirSync(dir, { recursive: true });
10157
- fs19.accessSync(dir, fs19.constants.W_OK);
11112
+ fs21.mkdirSync(dir, { recursive: true });
11113
+ fs21.accessSync(dir, fs21.constants.W_OK);
10158
11114
  return true;
10159
11115
  } catch {
10160
11116
  return false;
@@ -10162,9 +11118,9 @@ function canWriteDir(dir) {
10162
11118
  }
10163
11119
  function resolveAwsConfigTarget(env = process.env) {
10164
11120
  if (env.AWS_CONFIG_FILE) return { configPath: env.AWS_CONFIG_FILE, redirected: false };
10165
- const real = path20.join(env.HOME ?? "~", ".aws", "config");
10166
- if (canWriteDir(path20.dirname(real))) return { configPath: real, redirected: false };
10167
- return { configPath: path20.join(CONFIG_DIR, "aws-config"), redirected: true };
11121
+ const real = path22.join(env.HOME ?? "~", ".aws", "config");
11122
+ if (canWriteDir(path22.dirname(real))) return { configPath: real, redirected: false };
11123
+ return { configPath: path22.join(CONFIG_DIR, "aws-config"), redirected: true };
10168
11124
  }
10169
11125
  function resolveRoleOrExplain(opts, config) {
10170
11126
  const role = opts.role ?? process.env.MESH_AWS_ROLE ?? config.defaultRole;
@@ -10296,10 +11252,10 @@ async function exportAwsCredentials(context, config, opts) {
10296
11252
  const sanitized = context.replace(/[^A-Za-z0-9_-]/g, "-");
10297
11253
  const profileName = `mesh-${sanitized}`;
10298
11254
  const { configPath, redirected } = resolveAwsConfigTarget();
10299
- fs19.mkdirSync(path20.dirname(configPath), { recursive: true });
10300
- const existedBefore = fs19.existsSync(configPath);
10301
- const existing = existedBefore ? fs19.readFileSync(configPath, "utf-8") : "";
10302
- const mode = existedBefore ? fs19.statSync(configPath).mode & 511 : 384;
11255
+ fs21.mkdirSync(path22.dirname(configPath), { recursive: true });
11256
+ const existedBefore = fs21.existsSync(configPath);
11257
+ const existing = existedBefore ? fs21.readFileSync(configPath, "utf-8") : "";
11258
+ const mode = existedBefore ? fs21.statSync(configPath).mode & 511 : 384;
10303
11259
  atomicWriteFileSync(
10304
11260
  configPath,
10305
11261
  upsertManagedAwsConfigSection(
@@ -10415,18 +11371,21 @@ export {
10415
11371
  adoptPublishedHubProject,
10416
11372
  readAllContextConfigs,
10417
11373
  discoverConfigForContext,
11374
+ derivePlatformRef,
10418
11375
  readStackPlatform,
10419
11376
  derivePlatformContext,
10420
11377
  projectRoleKeys,
10421
11378
  resolveAwsCredentials,
10422
11379
  credProbeToPreflightError,
10423
11380
  resolvePulumiEnv,
11381
+ devSessionsRoot,
10424
11382
  resolveHubPlatformName,
10425
11383
  ensureKubeconfig,
10426
11384
  resolveTemporalAuth,
10427
11385
  acquireTemporalBearer,
10428
11386
  findRepoRoot2 as findRepoRoot,
10429
11387
  writeEnvFile,
11388
+ pickLaunchHostEnv,
10430
11389
  buildLaunchCommand,
10431
11390
  waitForPort,
10432
11391
  MOCK_SAML_PORT,