@mesh-tech/mesh-cli 0.38.1 → 0.40.0

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 (104) hide show
  1. package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
  2. package/dist/bin/{mesh.chunk-I6NXLXGC.js → mesh.chunk-2ASXJ5DO.js} +235 -581
  3. package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
  4. package/dist/bin/{mesh.chunk-IU2IWH6O.js → mesh.chunk-6AIG5OJM.js} +3 -3
  5. package/dist/bin/{mesh.chunk-KN4BZOYH.js → mesh.chunk-6SSOVZOI.js} +1 -1
  6. package/dist/bin/{mesh.chunk-LMYPTJ2F.js → mesh.chunk-7HTKSUWR.js} +1900 -577
  7. package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
  8. package/dist/bin/{mesh.chunk-2YRWJH5X.js → mesh.chunk-F44A3ACW.js} +4 -4
  9. package/dist/bin/{mesh.chunk-2YRWJH5X.js.map → mesh.chunk-F44A3ACW.js.map} +1 -1
  10. package/dist/bin/{mesh.chunk-BF2SH6XT.js → mesh.chunk-HJGMOEUG.js} +1 -1
  11. package/dist/bin/{mesh.chunk-TZAGJTKU.js → mesh.chunk-NSQJEX36.js} +1 -1
  12. package/dist/bin/{mesh.chunk-HPYZWPRM.js → mesh.chunk-PZVI7V3J.js} +1 -1
  13. package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
  14. package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
  15. package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
  16. package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
  17. package/dist/bin/{mesh.create-app-NGXACFAU.js → mesh.create-app-Z3RKSTBO.js} +3 -3
  18. package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
  19. package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
  20. package/dist/bin/mesh.js +9 -8
  21. package/dist/bin/mesh.js.map +1 -1
  22. package/dist/bin/{mesh.login-7SIAKFXJ.js → mesh.login-3AWVGPCM.js} +2 -2
  23. package/dist/bin/{mesh.platform-setup-CJXE3BCP.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
  24. package/dist/bin/{mesh.program-BIDQBSSI.js → mesh.program-K5JQXEHX.js} +9 -8
  25. package/dist/bin/{mesh.registry-7P4MGXEK.js → mesh.registry-JLQYFUNN.js} +4 -4
  26. package/dist/bin/{mesh.registry-identity-JW4KO7N7.js → mesh.registry-identity-3AR27UGO.js} +3 -3
  27. package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
  28. package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
  29. package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
  30. package/dist/bin/{mesh.start-MPQFIEMH.js → mesh.start-KZ5L6SOJ.js} +2 -2
  31. package/dist/bin/{mesh.wizard-ZJE4S6PC.js → mesh.wizard-FCTT55KI.js} +7 -7
  32. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
  33. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
  34. package/dist/build-info.json +2 -2
  35. package/dist/src/commands/app-check.js +2 -2
  36. package/dist/src/commands/dev-doctor.d.ts +22 -0
  37. package/dist/src/commands/dev-doctor.js +149 -0
  38. package/dist/src/commands/dev-env-leaks.d.ts +24 -0
  39. package/dist/src/commands/dev-env-leaks.js +178 -0
  40. package/dist/src/commands/dev-launch.d.ts +17 -1
  41. package/dist/src/commands/dev-launch.js +130 -5
  42. package/dist/src/commands/dev-readiness.d.ts +49 -0
  43. package/dist/src/commands/dev-readiness.js +78 -0
  44. package/dist/src/commands/dev.d.ts +60 -0
  45. package/dist/src/commands/dev.js +574 -140
  46. package/dist/src/commands/hub/index.js +10 -7
  47. package/dist/src/commands/local/app-authz.d.ts +118 -0
  48. package/dist/src/commands/local/app-authz.js +295 -0
  49. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  50. package/dist/src/commands/local/auth-provision.js +12 -1
  51. package/dist/src/commands/local/index.js +2 -0
  52. package/dist/src/commands/mcp/generate.d.ts +11 -0
  53. package/dist/src/commands/mcp/generate.js +78 -0
  54. package/dist/src/commands/mcp/index.d.ts +2 -0
  55. package/dist/src/commands/mcp/index.js +70 -0
  56. package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
  57. package/dist/src/commands/mcp/manifest-file.js +24 -0
  58. package/dist/src/commands/mcp/review-command.d.ts +24 -0
  59. package/dist/src/commands/mcp/review-command.js +66 -0
  60. package/dist/src/commands/mcp/review.d.ts +41 -0
  61. package/dist/src/commands/mcp/review.js +140 -0
  62. package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
  63. package/dist/src/commands/mcp/workspace-membership.js +77 -0
  64. package/dist/src/commands/secrets/reindex.js +2 -7
  65. package/dist/src/commands/secrets/set.d.ts +22 -1
  66. package/dist/src/commands/secrets/set.js +31 -8
  67. package/dist/src/commands/temporal.js +51 -5
  68. package/dist/src/commands/temporal.js.map +2 -2
  69. package/dist/src/program.js +2 -0
  70. package/dist/src/utils/aws-auth.d.ts +4 -0
  71. package/dist/src/utils/aws-auth.js +7 -3
  72. package/dist/src/utils/dev-sessions-root.d.ts +3 -0
  73. package/dist/src/utils/dev-sessions-root.js +45 -0
  74. package/dist/src/utils/kubeconfig.js +2 -2
  75. package/dist/src/utils/tailscale.js +7 -1
  76. package/dist/src/utils/temporal-auth.js +2 -1
  77. package/dist/src/utils/workflow-fingerprint.js +23 -12
  78. package/fragments/base/index.ts.hbs +4 -1
  79. package/package.json +7 -5
  80. package/skills/mesh/SKILL.md +10 -6
  81. package/templates/api-auth/api/src/schema.ts.hbs +17 -0
  82. package/templates/api-auth/index.ts.hbs +1 -0
  83. package/templates/api-auth/package.json.hbs +3 -0
  84. package/templates/api-role-gating/api/src/schema.ts.hbs +10 -0
  85. package/templates/api-role-gating/index.ts.hbs +1 -0
  86. package/templates/external-service/index.ts.hbs +1 -0
  87. package/templates/workflow/index.ts.hbs +2 -0
  88. package/dist/bin/mesh.chunk-I6NXLXGC.js.map +0 -6
  89. package/dist/bin/mesh.chunk-LMYPTJ2F.js.map +0 -6
  90. /package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
  91. /package/dist/bin/{mesh.chunk-IU2IWH6O.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
  92. /package/dist/bin/{mesh.chunk-KN4BZOYH.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
  93. /package/dist/bin/{mesh.chunk-BF2SH6XT.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
  94. /package/dist/bin/{mesh.chunk-TZAGJTKU.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
  95. /package/dist/bin/{mesh.chunk-HPYZWPRM.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
  96. /package/dist/bin/{mesh.create-app-NGXACFAU.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
  97. /package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
  98. /package/dist/bin/{mesh.login-7SIAKFXJ.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
  99. /package/dist/bin/{mesh.platform-setup-CJXE3BCP.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
  100. /package/dist/bin/{mesh.program-BIDQBSSI.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
  101. /package/dist/bin/{mesh.registry-7P4MGXEK.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
  102. /package/dist/bin/{mesh.registry-identity-JW4KO7N7.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
  103. /package/dist/bin/{mesh.start-MPQFIEMH.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
  104. /package/dist/bin/{mesh.wizard-ZJE4S6PC.js.map → mesh.wizard-FCTT55KI.js.map} +0 -0
@@ -14,6 +14,7 @@ import {
14
14
  } from "./mesh.chunk-2QIVBQX5.js";
15
15
  import {
16
16
  ARTIFACTS_BUCKET,
17
+ COMPOSE_PROJECT,
17
18
  DEFAULT_HUB_PORT,
18
19
  LOCAL_AWS_CONFIG,
19
20
  LOCAL_ENV,
@@ -48,14 +49,14 @@ import {
48
49
  isExpiredAwsTokenMessage,
49
50
  probeRegistryToken,
50
51
  registryLoginFix
51
- } from "./mesh.chunk-KN4BZOYH.js";
52
+ } from "./mesh.chunk-6SSOVZOI.js";
52
53
 
53
54
  // packages/mesh-cli/src/commands/login.ts
54
55
  import * as http from "http";
55
56
  import * as crypto3 from "crypto";
56
- import * as fs18 from "fs";
57
- import * as path19 from "path";
58
- 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";
59
60
 
60
61
  // packages/mesh-cli/src/utils/aws-auth.ts
61
62
  import * as fs2 from "fs";
@@ -166,7 +167,7 @@ function pulumiStackOutput(appRoot, key, extraArgs, env) {
166
167
 
167
168
  // packages/mesh-cli/src/utils/aws-auth.ts
168
169
  var DEFAULT_REGION = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
169
- function readStackPlatform(appRoot, stack) {
170
+ function derivePlatformRef(appRoot, stack) {
170
171
  const configFile = path2.join(appRoot, `Pulumi.${stack}.yaml`);
171
172
  if (!fs2.existsSync(configFile)) return null;
172
173
  const content = fs2.readFileSync(configFile, "utf-8");
@@ -174,17 +175,21 @@ function readStackPlatform(appRoot, stack) {
174
175
  if (jsonMatch) {
175
176
  try {
176
177
  const parsed = JSON.parse(jsonMatch[1]);
177
- if (parsed.name && parsed.env) return `${parsed.name}.${parsed.env}`;
178
+ if (parsed.name && parsed.env) return { name: parsed.name, env: parsed.env };
178
179
  } catch {
179
180
  }
180
181
  }
181
182
  const nameMatch = content.match(/mesh:platform:[\s\S]*?name:\s*(\S+)/);
182
183
  const envMatch = content.match(/mesh:platform:[\s\S]*?env:\s*(\S+)/);
183
184
  if (nameMatch && envMatch) {
184
- return `${nameMatch[1]}.${envMatch[1]}`;
185
+ return { name: nameMatch[1], env: envMatch[1] };
185
186
  }
186
187
  return null;
187
188
  }
189
+ function readStackPlatform(appRoot, stack) {
190
+ const ref = derivePlatformRef(appRoot, stack);
191
+ return ref ? `${ref.name}.${ref.env}` : null;
192
+ }
188
193
  function derivePlatformContext(appRoot, stack) {
189
194
  const explicit = readStackConfig(appRoot, stack, "mesh:loginContext");
190
195
  if (explicit) return explicit;
@@ -561,7 +566,7 @@ async function resolveAwsCredentials(roleArn, appRoot, stack) {
561
566
  }
562
567
 
563
568
  // packages/mesh-cli/src/commands/vpn/index.ts
564
- import { execFileSync as execFileSync14 } from "child_process";
569
+ import { execFileSync as execFileSync15 } from "child_process";
565
570
  import * as net8 from "node:net";
566
571
 
567
572
  // packages/mesh-cli/src/utils/context.ts
@@ -1193,11 +1198,11 @@ function unregisterDnsRecords(cfg, suffix) {
1193
1198
  import * as net7 from "node:net";
1194
1199
 
1195
1200
  // packages/mesh-cli/src/utils/tailscale.ts
1196
- import * as fs17 from "node:fs";
1201
+ import * as fs20 from "node:fs";
1197
1202
  import * as net6 from "node:net";
1198
- import * as os4 from "node:os";
1199
- import * as path18 from "node:path";
1200
- 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";
1201
1206
 
1202
1207
  // packages/mesh-cli/src/utils/pid.ts
1203
1208
  var MIN_SIGNALABLE_PID = 2;
@@ -1235,16 +1240,16 @@ function assertSignalablePid(pid, source) {
1235
1240
  }
1236
1241
 
1237
1242
  // packages/mesh-cli/src/utils/tailscale-targets.ts
1238
- import { execFileSync as execFileSync11 } from "node:child_process";
1239
- import * as fs15 from "node:fs";
1240
- import * as path16 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";
1241
1246
 
1242
1247
  // packages/mesh-cli/src/commands/dev.ts
1243
- import { execFileSync as execFileSync10, spawn, spawnSync } from "child_process";
1244
- import * as fs14 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";
1245
1251
  import * as net3 from "net";
1246
- import * as os3 from "os";
1247
- import * as path15 from "path";
1252
+ import * as path18 from "path";
1248
1253
  import { Option } from "commander";
1249
1254
  import { SecretsManagerClient as SecretsManagerClient2, GetSecretValueCommand as GetSecretValueCommand2 } from "@aws-sdk/client-secrets-manager";
1250
1255
 
@@ -1359,9 +1364,48 @@ async function resolvePulumiEnv(opts) {
1359
1364
  }
1360
1365
 
1361
1366
  // packages/mesh-cli/src/utils/kubeconfig.ts
1362
- import * as fs5 from "fs";
1363
- import * as os from "os";
1364
- 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
1365
1409
  function buildKubeconfig(cluster, region) {
1366
1410
  return `apiVersion: v1
1367
1411
  kind: Config
@@ -1392,7 +1436,7 @@ users:
1392
1436
  `;
1393
1437
  }
1394
1438
  function sessionKubeconfigPath(sessionName) {
1395
- return path5.join(os.tmpdir(), "mesh-dev-sessions", `${sessionName}.kubeconfig`);
1439
+ return path6.join(devSessionsRoot(), `${sessionName}.kubeconfig`);
1396
1440
  }
1397
1441
  function resolveHubPlatformName(platform) {
1398
1442
  return platform?.name ?? "mesh";
@@ -1425,10 +1469,10 @@ async function ensureKubeconfig(platformName, env, sessionName, deps = {}) {
1425
1469
  }
1426
1470
  const region = process.env.AWS_REGION || "us-east-2";
1427
1471
  const kubeconfigPath = sessionKubeconfigPath(sessionName);
1428
- if (!fs5.existsSync(path5.dirname(kubeconfigPath))) {
1429
- fs5.mkdirSync(path5.dirname(kubeconfigPath), { recursive: true });
1472
+ if (!fs6.existsSync(path6.dirname(kubeconfigPath))) {
1473
+ fs6.mkdirSync(path6.dirname(kubeconfigPath), { recursive: true });
1430
1474
  }
1431
- fs5.writeFileSync(kubeconfigPath, buildKubeconfig(cluster, region), { mode: 384 });
1475
+ fs6.writeFileSync(kubeconfigPath, buildKubeconfig(cluster, region), { mode: 384 });
1432
1476
  return kubeconfigPath;
1433
1477
  }
1434
1478
 
@@ -1507,7 +1551,7 @@ async function resolveTemporalAuth(tenant, env, platformName = tenant) {
1507
1551
  const stderr = String(err?.stderr ?? "");
1508
1552
  if (/\.kube[/\\]config|error loading config file/i.test(`${detail} ${stderr}`)) {
1509
1553
  logWarn(
1510
- "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)."
1511
1555
  );
1512
1556
  }
1513
1557
  }
@@ -1609,6 +1653,7 @@ var WORKFLOW_PACKAGE_DIRS = [
1609
1653
  "packages/agent-contracts/src"
1610
1654
  ];
1611
1655
  var EXCLUDE = /(^|\/)(node_modules|dist|__tests__)(\/|$)|\.(test|spec)\.[cm]?tsx?$/;
1656
+ var EXCLUDED_DIR_NAMES = /* @__PURE__ */ new Set(["node_modules", "dist", "__tests__"]);
1612
1657
  function findRepoRoot2(startDir) {
1613
1658
  let dir = startDir;
1614
1659
  for (; ; ) {
@@ -1632,19 +1677,27 @@ function resolveWorkerSourceDirs(repoRoot, services = {}) {
1632
1677
  }
1633
1678
  return dirs;
1634
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
+ }
1635
1697
  function computeWorkflowFingerprint(dirs, relativeTo) {
1636
1698
  const files = [];
1637
1699
  for (const dir of dirs) {
1638
- let rels;
1639
- try {
1640
- rels = readdirSync3(dir, { recursive: true });
1641
- } catch {
1642
- continue;
1643
- }
1644
- for (const rel of rels) {
1645
- const p = String(rel);
1646
- if (/\.(ts|tsx)$/.test(p) && !EXCLUDE.test(p)) files.push(join6(dir, p));
1647
- }
1700
+ collectSourceFiles(dir, files);
1648
1701
  }
1649
1702
  files.sort();
1650
1703
  if (files.length === 0) return "";
@@ -1668,9 +1721,9 @@ function fingerprintWorkflowSource(appRoot, services = {}) {
1668
1721
  }
1669
1722
 
1670
1723
  // packages/mesh-cli/src/commands/dev-launch.ts
1671
- import * as fs6 from "fs";
1724
+ import * as fs7 from "fs";
1672
1725
  import * as net from "net";
1673
- import * as path6 from "path";
1726
+ import * as path7 from "path";
1674
1727
  var ENV_FILE_MODE = 384;
1675
1728
  var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1676
1729
  function quoteShellValue(value) {
@@ -1693,15 +1746,158 @@ function renderEnvFile(env) {
1693
1746
  function envFileName(serviceName) {
1694
1747
  return `${serviceName.replace(/[^A-Za-z0-9._-]/g, "-")}.env.sh`;
1695
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
+ }
1696
1766
  function writeEnvFile(filePath, env) {
1697
- fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
1698
- fs6.writeFileSync(filePath, renderEnvFile(env), { mode: ENV_FILE_MODE });
1699
- 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;
1700
1877
  }
1701
- function buildLaunchCommand(envFilePath, dir, command, logShipper) {
1702
- const base = `cd ${quoteShellValue(dir)} && source ${quoteShellValue(envFilePath)} && `;
1703
- if (!logShipper) return `${base}${command}`;
1704
- 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;
1705
1901
  }
1706
1902
  async function waitForPort(host, port, timeoutMs, intervalMs = 500) {
1707
1903
  const deadline = Date.now() + timeoutMs;
@@ -1729,13 +1925,13 @@ function tryConnect(host, port, timeoutMs) {
1729
1925
  }
1730
1926
 
1731
1927
  // packages/mesh-cli/src/commands/local/dev-local.ts
1732
- import * as fs10 from "fs";
1733
- import * as path10 from "path";
1928
+ import * as fs11 from "fs";
1929
+ import * as path11 from "path";
1734
1930
 
1735
1931
  // packages/mesh-cli/src/commands/local/mocks.ts
1736
1932
  import { execFileSync as execFileSync4 } from "child_process";
1737
- import * as fs7 from "fs";
1738
- import * as path7 from "path";
1933
+ import * as fs8 from "fs";
1934
+ import * as path8 from "path";
1739
1935
  var EXTERNAL_MODES = ["mock", "local", "remote"];
1740
1936
  var DECL_DOCS = { docs: 'package.json \u2192 "mesh": { "externals": { \u2026 } }' };
1741
1937
  function externalMode(name, decl) {
@@ -1867,8 +2063,8 @@ var dockerProbes = {
1867
2063
  portPublisher: composeFilePortPublisher
1868
2064
  };
1869
2065
  async function composeExternalUp(appRoot, sessionName, name, decl, probes = dockerProbes) {
1870
- const composeFile = path7.resolve(appRoot, decl.compose);
1871
- if (!fs7.existsSync(composeFile)) {
2066
+ const composeFile = path8.resolve(appRoot, decl.compose);
2067
+ if (!fs8.existsSync(composeFile)) {
1872
2068
  throw new MeshCliError(`External '${name}': compose file not found at ${composeFile}.`, {
1873
2069
  remediation: { docs: 'package.json \u2192 "mesh": { "externals": { \u2026 } }' }
1874
2070
  });
@@ -2006,10 +2202,10 @@ function composeExternalsDown(refs) {
2006
2202
  }
2007
2203
  }
2008
2204
  }
2009
- var SECRET_KEY_RE = /key|secret|password|token/i;
2205
+ var SECRET_KEY_RE2 = /key|secret|password|token/i;
2010
2206
  function readLocalMocks(appRoot) {
2011
2207
  try {
2012
- 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"));
2013
2209
  const mocks = pkg?.mesh?.mocks;
2014
2210
  const externals = pkg?.mesh?.externals;
2015
2211
  return {
@@ -2104,12 +2300,12 @@ function externalDocs(decl) {
2104
2300
  return typeof decl.docs === "string" ? { url: decl.docs } : decl.docs;
2105
2301
  }
2106
2302
  function localProbeFile(tenant, app, external) {
2107
- return path7.join(localProbesDir(), `${tenant}-${app}-${external}.json`);
2303
+ return path8.join(localProbesDir(), `${tenant}-${app}-${external}.json`);
2108
2304
  }
2109
2305
  function localProbesRemove(files) {
2110
2306
  for (const file of files ?? []) {
2111
2307
  try {
2112
- fs7.rmSync(file, { force: true });
2308
+ fs8.rmSync(file, { force: true });
2113
2309
  } catch {
2114
2310
  }
2115
2311
  }
@@ -2145,7 +2341,7 @@ async function seedLocalMock(args) {
2145
2341
  await upsertLocalSecret(secretName, value);
2146
2342
  await upsertLocalSecret(
2147
2343
  `${secretName}/.config`,
2148
- 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)))
2149
2345
  );
2150
2346
  const { SSMClient: SSMClient2, PutParameterCommand } = await import("@aws-sdk/client-ssm");
2151
2347
  const ssm = new SSMClient2(LOCAL_AWS_CONFIG);
@@ -2178,7 +2374,7 @@ async function seedLocalMock(args) {
2178
2374
  fields: Object.fromEntries(
2179
2375
  Object.keys(value).map((key) => [
2180
2376
  key,
2181
- { 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 }
2182
2378
  ])
2183
2379
  ),
2184
2380
  keyedBy: null
@@ -2198,7 +2394,7 @@ async function seedLocalMock(args) {
2198
2394
  );
2199
2395
  const probeFile = localProbeFile(tenant, app, decl.external);
2200
2396
  if (probe) {
2201
- fs7.writeFileSync(
2397
+ fs8.writeFileSync(
2202
2398
  probeFile,
2203
2399
  JSON.stringify(
2204
2400
  [
@@ -2232,7 +2428,7 @@ async function seedLocalMock(args) {
2232
2428
  }
2233
2429
 
2234
2430
  // packages/mesh-cli/src/commands/local/seed-zitadel.ts
2235
- import * as fs9 from "fs";
2431
+ import * as fs10 from "fs";
2236
2432
 
2237
2433
  // packages/mesh-cli/src/commands/local/act-as-local.ts
2238
2434
  import { createPublicKey } from "node:crypto";
@@ -2275,12 +2471,12 @@ function actAsEnv(publication, tenant, env, surface) {
2275
2471
 
2276
2472
  // packages/mesh-cli/src/commands/local/seed-zitadel.ts
2277
2473
  import * as os2 from "os";
2278
- import * as path9 from "path";
2474
+ import * as path10 from "path";
2279
2475
 
2280
2476
  // packages/mesh-cli/src/commands/local/mock-idps.ts
2281
2477
  import { execFileSync as execFileSync5 } from "child_process";
2282
- import * as fs8 from "fs";
2283
- import * as path8 from "path";
2478
+ import * as fs9 from "fs";
2479
+ import * as path9 from "path";
2284
2480
  var MOCK_SAML_PORT = 4569;
2285
2481
  var MOCK_OIDC_PORT = 5556;
2286
2482
  var MOCK_OIDC_SERVICE = "mock-oidc";
@@ -2313,13 +2509,13 @@ var mockOidcEndpoints = () => ({
2313
2509
  token: `http://${MOCK_OIDC_SERVICE}:${MOCK_OIDC_PORT}/dex/token`,
2314
2510
  user: `http://${MOCK_OIDC_SERVICE}:${MOCK_OIDC_PORT}/dex/userinfo`
2315
2511
  });
2316
- var keyDir = () => path8.join(meshCacheDir("mesh-local"), "mock-idp");
2512
+ var keyDir = () => path9.join(meshCacheDir("mesh-local"), "mock-idp");
2317
2513
  function ensureMockSamlSigningKey() {
2318
2514
  const dir = keyDir();
2319
- const certPath = path8.join(dir, "cert.pem");
2320
- const keyPath = path8.join(dir, "key.pem");
2321
- if (!fs8.existsSync(certPath) || !fs8.existsSync(keyPath)) {
2322
- 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 });
2323
2519
  try {
2324
2520
  execFileSync5(
2325
2521
  "openssl",
@@ -2350,8 +2546,8 @@ function ensureMockSamlSigningKey() {
2350
2546
  }
2351
2547
  }
2352
2548
  return {
2353
- publicKeyB64: fs8.readFileSync(certPath).toString("base64"),
2354
- privateKeyB64: fs8.readFileSync(keyPath).toString("base64")
2549
+ publicKeyB64: fs9.readFileSync(certPath).toString("base64"),
2550
+ privateKeyB64: fs9.readFileSync(keyPath).toString("base64")
2355
2551
  };
2356
2552
  }
2357
2553
  var mockIdpComposeEnv = () => {
@@ -2448,10 +2644,10 @@ function buildCliAppPayload() {
2448
2644
  };
2449
2645
  }
2450
2646
  function readSeederPat() {
2451
- 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");
2452
2648
  try {
2453
2649
  compose(["cp", "zitadel:/machinekey/pat.txt", tmp]);
2454
- const pat = fs9.readFileSync(tmp, "utf-8").trim();
2650
+ const pat = fs10.readFileSync(tmp, "utf-8").trim();
2455
2651
  if (!pat) throw new Error("empty PAT file");
2456
2652
  return pat;
2457
2653
  } catch {
@@ -2460,7 +2656,7 @@ function readSeederPat() {
2460
2656
  { remediation: { command: "mesh stop --destroy && mesh start" } }
2461
2657
  );
2462
2658
  } finally {
2463
- fs9.rmSync(path9.dirname(tmp), { recursive: true, force: true });
2659
+ fs10.rmSync(path10.dirname(tmp), { recursive: true, force: true });
2464
2660
  }
2465
2661
  }
2466
2662
  async function api(pat, method, apiPath, body, orgId) {
@@ -2564,11 +2760,11 @@ async function ensureTestUsers(pat) {
2564
2760
  }
2565
2761
  }
2566
2762
  function hubAuthPath() {
2567
- return path9.join(meshCacheDir("mesh-local"), "hub-auth.json");
2763
+ return path10.join(meshCacheDir("mesh-local"), "hub-auth.json");
2568
2764
  }
2569
2765
  function readHubAuth() {
2570
2766
  try {
2571
- const parsed = JSON.parse(fs9.readFileSync(hubAuthPath(), "utf-8"));
2767
+ const parsed = JSON.parse(fs10.readFileSync(hubAuthPath(), "utf-8"));
2572
2768
  if (parsed?.clientId && parsed?.clientSecret && parsed?.cookieSecret) return parsed;
2573
2769
  return null;
2574
2770
  } catch {
@@ -2708,7 +2904,7 @@ async function seedHubAuth(pat) {
2708
2904
  clientSecret: app.clientSecret ?? persisted?.clientSecret ?? "",
2709
2905
  cookieSecret: persistedCookie ?? (await import("crypto")).randomBytes(16).toString("hex")
2710
2906
  };
2711
- fs9.writeFileSync(hubAuthPath(), JSON.stringify(config, null, 2), { mode: 384 });
2907
+ fs10.writeFileSync(hubAuthPath(), JSON.stringify(config, null, 2), { mode: 384 });
2712
2908
  return config;
2713
2909
  }
2714
2910
  async function publishHubAuthzPointer(projectId, orgId, awsConfig) {
@@ -3218,6 +3414,10 @@ async function reconcileRegistryFromZitadel() {
3218
3414
  }
3219
3415
  return { tenants, apps };
3220
3416
  }
3417
+ function isZitadelNoChanges(err) {
3418
+ if (!(err instanceof Error)) return false;
3419
+ return err.status === 400 && /No changes|COMMAND-1m88i/.test(err.message);
3420
+ }
3221
3421
  async function ensureSignInApp(args) {
3222
3422
  const pat = readSeederPat();
3223
3423
  const orgId = await ensureOrg(pat, args.tenant);
@@ -3244,13 +3444,17 @@ async function ensureSignInApp(args) {
3244
3444
  };
3245
3445
  const existing = await searchZitadelApp(pat, projectId, name, orgId);
3246
3446
  if (existing) {
3247
- await api(
3248
- pat,
3249
- "PUT",
3250
- `/management/v1/projects/${projectId}/apps/${existing.id}/oidc_config`,
3251
- config,
3252
- orgId
3253
- );
3447
+ try {
3448
+ await api(
3449
+ pat,
3450
+ "PUT",
3451
+ `/management/v1/projects/${projectId}/apps/${existing.id}/oidc_config`,
3452
+ config,
3453
+ orgId
3454
+ );
3455
+ } catch (err) {
3456
+ if (!isZitadelNoChanges(err)) throw err;
3457
+ }
3254
3458
  return;
3255
3459
  }
3256
3460
  const created = await api(
@@ -3322,11 +3526,11 @@ function localAppNamespace(tenant, appName) {
3322
3526
  return `${tenant}-${LOCAL_ENV}-${appName}`;
3323
3527
  }
3324
3528
  function logShipperPath() {
3325
- return path10.join(findPackageRoot(), "assets", "log-shipper.mjs");
3529
+ return path11.join(findPackageRoot(), "assets", "log-shipper.mjs");
3326
3530
  }
3327
3531
  function autoInstrumentationEnv(appRoot, src) {
3328
- const pkgDir = path10.join("node_modules", "@opentelemetry", "auto-instrumentations-node");
3329
- 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));
3330
3534
  if (!present) return {};
3331
3535
  return {
3332
3536
  NODE_OPTIONS: { value: "--import @opentelemetry/auto-instrumentations-node/register" },
@@ -3382,26 +3586,26 @@ function localPlatformEnv(tenant, appName, serviceName) {
3382
3586
  return env;
3383
3587
  }
3384
3588
  function isDagsterWorkspaceRoot(appRoot) {
3385
- if (fs10.existsSync(path10.join(appRoot, "dg.toml"))) return true;
3386
- const pyproject = path10.join(appRoot, "pyproject.toml");
3387
- 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;
3388
3592
  try {
3389
- return /^\s*\[tool\.dg[\].]/m.test(fs10.readFileSync(pyproject, "utf-8"));
3593
+ return /^\s*\[tool\.dg[\].]/m.test(fs11.readFileSync(pyproject, "utf-8"));
3390
3594
  } catch {
3391
3595
  return false;
3392
3596
  }
3393
3597
  }
3394
3598
  function resolveDgBinary(appRoot) {
3395
3599
  for (const candidate of ["deployments/local/.venv/bin/dg", ".venv/bin/dg"]) {
3396
- if (fs10.existsSync(path10.join(appRoot, candidate))) return candidate;
3600
+ if (fs11.existsSync(path11.join(appRoot, candidate))) return candidate;
3397
3601
  }
3398
3602
  return "dg";
3399
3603
  }
3400
3604
  function hasDevScript(dir) {
3401
- const pkgPath = path10.join(dir, "package.json");
3402
- if (!fs10.existsSync(pkgPath)) return false;
3605
+ const pkgPath = path11.join(dir, "package.json");
3606
+ if (!fs11.existsSync(pkgPath)) return false;
3403
3607
  try {
3404
- const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
3608
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
3405
3609
  return typeof pkg?.scripts?.dev === "string";
3406
3610
  } catch {
3407
3611
  return false;
@@ -3409,14 +3613,14 @@ function hasDevScript(dir) {
3409
3613
  }
3410
3614
  function detectLocalServices(appRoot) {
3411
3615
  const services = {};
3412
- for (const entry of fs10.readdirSync(appRoot, { withFileTypes: true })) {
3616
+ for (const entry of fs11.readdirSync(appRoot, { withFileTypes: true })) {
3413
3617
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
3414
- if (hasDevScript(path10.join(appRoot, entry.name))) {
3618
+ if (hasDevScript(path11.join(appRoot, entry.name))) {
3415
3619
  services[entry.name] = { src: entry.name };
3416
3620
  }
3417
3621
  }
3418
3622
  if (Object.keys(services).length === 0 && hasDevScript(appRoot)) {
3419
- services[path10.basename(appRoot)] = { src: "." };
3623
+ services[path11.basename(appRoot)] = { src: "." };
3420
3624
  }
3421
3625
  return services;
3422
3626
  }
@@ -3430,15 +3634,15 @@ function buildLocalDevOutput(appRoot, tenant, opts = {}) {
3430
3634
  { remediation: { command: "mesh create-app" } }
3431
3635
  );
3432
3636
  }
3433
- let appName = path10.basename(appRoot);
3637
+ let appName = path11.basename(appRoot);
3434
3638
  try {
3435
- 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"));
3436
3640
  if (typeof pkg?.name === "string" && pkg.name) appName = pkg.name.replace(/^@[^/]+\//, "");
3437
3641
  } catch {
3438
3642
  }
3439
- const pulumiYaml = path10.join(appRoot, "Pulumi.yaml");
3440
- if (fs10.existsSync(pulumiYaml)) {
3441
- 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);
3442
3646
  if (match?.[1]?.trim()) appName = match[1].trim();
3443
3647
  }
3444
3648
  const services = {};
@@ -3488,7 +3692,7 @@ function buildLocalDevOutput(appRoot, tenant, opts = {}) {
3488
3692
  command: [resolveDgBinary(appRoot), "dev", "-h", "127.0.0.1", "-p", '"$PORT"'],
3489
3693
  env: {
3490
3694
  PORT: { value: String(port) },
3491
- DAGSTER_HOME: { value: path10.join(appRoot, ".dagster") },
3695
+ DAGSTER_HOME: { value: path11.join(appRoot, ".dagster") },
3492
3696
  ...localPlatformEnv(tenant, appName, "dagster")
3493
3697
  }
3494
3698
  };
@@ -3520,23 +3724,23 @@ function buildLocalDevOutput(appRoot, tenant, opts = {}) {
3520
3724
  };
3521
3725
  }
3522
3726
  function hasStackBacking(appRoot) {
3523
- 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));
3524
3728
  if (hasOwn) return true;
3525
- let dir = path10.dirname(appRoot);
3729
+ let dir = path11.dirname(appRoot);
3526
3730
  while (true) {
3527
- if (fs10.existsSync(path10.join(dir, "Pulumi.yaml"))) return true;
3528
- if (fs10.existsSync(path10.join(dir, ".git")) || fs10.existsSync(path10.join(dir, "pnpm-workspace.yaml"))) {
3529
- 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"));
3530
3734
  }
3531
- const parent = path10.dirname(dir);
3735
+ const parent = path11.dirname(dir);
3532
3736
  if (parent === dir) return false;
3533
3737
  dir = parent;
3534
3738
  }
3535
3739
  }
3536
3740
  function detectLocalTenant(appRoot) {
3537
- for (const file of fs10.readdirSync(appRoot)) {
3741
+ for (const file of fs11.readdirSync(appRoot)) {
3538
3742
  if (!/^Pulumi\..*\.yaml$/.test(file)) continue;
3539
- 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);
3540
3744
  if (match) return match[1];
3541
3745
  }
3542
3746
  const recorded = findMeshJson(appRoot);
@@ -3553,12 +3757,335 @@ async function ensureLocalPlatformRunning() {
3553
3757
  }
3554
3758
  }
3555
3759
 
3556
- // packages/mesh-cli/src/commands/local/publish-docs.ts
3760
+ // packages/mesh-cli/src/commands/local/app-authz.ts
3761
+ import { spawnSync } from "node:child_process";
3557
3762
  import { createHash as createHash2 } from "node:crypto";
3558
- import * as fs11 from "node:fs";
3559
- import * as path11 from "node:path";
3560
- import { execFileSync as execFileSync6 } from "node:child_process";
3763
+ import { createRequire } from "node:module";
3764
+ import fs12 from "node:fs";
3765
+ import path12 from "node:path";
3561
3766
  import { pathToFileURL } from "node:url";
3767
+ var LOCAL_APP_SPICEDB_IMAGE = "authzed/spicedb:v1.42.1";
3768
+ var LOCAL_APP_SPICEDB_KEY = "local-dev-key";
3769
+ var LOCAL_APP_SPICEDB_LABEL = "mesh.local.app-spicedb";
3770
+ var LOCAL_STACK_NETWORK = `${COMPOSE_PROJECT}_mesh_network`;
3771
+ var STACK = "local";
3772
+ var GRPC_PORT_BASE = 50100;
3773
+ var HTTP_PORT_BASE = 18100;
3774
+ var PORT_SLOTS = 100;
3775
+ function readLocalAuthzConfig(appRoot) {
3776
+ try {
3777
+ const pkg = JSON.parse(fs12.readFileSync(path12.join(appRoot, "package.json"), "utf-8"));
3778
+ const schema = pkg?.mesh?.authz?.schema;
3779
+ if (typeof schema !== "string" || !schema.trim()) return null;
3780
+ return { schema: schema.trim(), prebuilt: pkg?.mesh?.authz?.metadata !== void 0 };
3781
+ } catch {
3782
+ return null;
3783
+ }
3784
+ }
3785
+ var slug = (name, sep3) => name.toLowerCase().replace(/[^a-z0-9]+/g, sep3).replace(new RegExp(`^\\${sep3}+|\\${sep3}+$`, "g"), "");
3786
+ function appSpiceDBSlot(app) {
3787
+ return createHash2("sha256").update(app).digest().readUInt32BE(0) % PORT_SLOTS;
3788
+ }
3789
+ function appSpiceDBPorts(slot) {
3790
+ const s = (slot % PORT_SLOTS + PORT_SLOTS) % PORT_SLOTS;
3791
+ return { grpc: GRPC_PORT_BASE + s, http: HTTP_PORT_BASE + s };
3792
+ }
3793
+ var appSpiceDBContainerName = (app) => `mesh-local-spicedb-${slug(app, "-")}`;
3794
+ var appSpiceDBDatabase = (app) => `spicedb_${slug(app, "_")}`.slice(0, 63);
3795
+ var appSpiceDBSecretName = (tenant, env, app) => `mesh/${tenant}/${env}/${app}/spicedb/preshared-key`;
3796
+ var spicedbInstanceParam = (tenant, env, instanceId) => `/mesh-platform/${tenant}/${env}/authz/spicedb-instances/${instanceId}`;
3797
+ var appAuthzParam = (tenant, env, app) => `/mesh-platform/${tenant}/${env}/apps/${app}/stacks/${STACK}/authz`;
3798
+ function instanceRegistryValue(args) {
3799
+ const secret = appSpiceDBSecretName(args.tenant, args.env, args.app);
3800
+ return {
3801
+ grpcEndpoint: `localhost:${args.grpcPort}`,
3802
+ httpEndpoint: `localhost:${args.httpPort}`,
3803
+ namespace: args.app,
3804
+ presharedKeySecretName: secret,
3805
+ configSecretName: secret,
3806
+ owner: { kind: "app", name: args.app }
3807
+ };
3808
+ }
3809
+ function buildAppAuthzPointer(args) {
3810
+ const existingZitadel = args.existing.zitadel && typeof args.existing.zitadel === "object" ? args.existing.zitadel : {};
3811
+ return {
3812
+ ...args.existing,
3813
+ spicedb: {
3814
+ instanceRefs: args.mode === "policy-engine" && args.instanceId ? [{ instanceId: args.instanceId }] : []
3815
+ },
3816
+ ...args.zitadel || Object.keys(existingZitadel).length > 0 ? { zitadel: { ...existingZitadel, ...args.zitadel ?? {} } } : {},
3817
+ opsHubMetadataRef: args.metadataRef,
3818
+ mode: args.mode
3819
+ };
3820
+ }
3821
+ function appSpiceDBEnv(args) {
3822
+ return {
3823
+ SPICEDB_ENDPOINT: `localhost:${args.grpcPort}`,
3824
+ SPICEDB_HTTP_ENDPOINT: `http://localhost:${args.httpPort}`,
3825
+ SPICEDB_PRESHARED_KEY: LOCAL_APP_SPICEDB_KEY,
3826
+ SPICEDB_TOKEN: LOCAL_APP_SPICEDB_KEY,
3827
+ AUTHZ_METADATA_SSM_PATH: args.metadataPath
3828
+ };
3829
+ }
3830
+ var datastoreUri = (app) => `postgres://postgres:postgres@database:5432/${appSpiceDBDatabase(app)}?sslmode=disable`;
3831
+ function spicedbMigrateArgs(app) {
3832
+ return [
3833
+ "run",
3834
+ "--rm",
3835
+ "--network",
3836
+ LOCAL_STACK_NETWORK,
3837
+ "-e",
3838
+ "SPICEDB_DATASTORE_ENGINE=postgres",
3839
+ "-e",
3840
+ `SPICEDB_DATASTORE_CONN_URI=${datastoreUri(app)}`,
3841
+ LOCAL_APP_SPICEDB_IMAGE,
3842
+ "datastore",
3843
+ "migrate",
3844
+ "head"
3845
+ ];
3846
+ }
3847
+ function spicedbServeArgs(app, ports) {
3848
+ return [
3849
+ "run",
3850
+ "-d",
3851
+ "--name",
3852
+ appSpiceDBContainerName(app),
3853
+ "--label",
3854
+ `${LOCAL_APP_SPICEDB_LABEL}=${app}`,
3855
+ "--restart",
3856
+ "unless-stopped",
3857
+ "--network",
3858
+ LOCAL_STACK_NETWORK,
3859
+ "-p",
3860
+ `127.0.0.1:${ports.grpc}:50051`,
3861
+ "-p",
3862
+ `127.0.0.1:${ports.http}:8443`,
3863
+ "-e",
3864
+ `SPICEDB_GRPC_PRESHARED_KEY=${LOCAL_APP_SPICEDB_KEY}`,
3865
+ "-e",
3866
+ "SPICEDB_DATASTORE_ENGINE=postgres",
3867
+ "-e",
3868
+ `SPICEDB_DATASTORE_CONN_URI=${datastoreUri(app)}`,
3869
+ "-e",
3870
+ "SPICEDB_HTTP_ENABLED=true",
3871
+ // SpiceDB opens 30 datastore connections by default, and every app's
3872
+ // instance shares the stack's Postgres (100 connections in all): a local
3873
+ // app needs a handful.
3874
+ "-e",
3875
+ "SPICEDB_DATASTORE_CONN_POOL_READ_MAX_OPEN=4",
3876
+ "-e",
3877
+ "SPICEDB_DATASTORE_CONN_POOL_READ_MIN_OPEN=1",
3878
+ "-e",
3879
+ "SPICEDB_DATASTORE_CONN_POOL_WRITE_MAX_OPEN=2",
3880
+ "-e",
3881
+ "SPICEDB_DATASTORE_CONN_POOL_WRITE_MIN_OPEN=1",
3882
+ LOCAL_APP_SPICEDB_IMAGE,
3883
+ "serve"
3884
+ ];
3885
+ }
3886
+ function parseDockerPort(output) {
3887
+ const match = output.trim().split("\n")[0]?.match(/:(\d+)\s*$/);
3888
+ return match ? Number(match[1]) : null;
3889
+ }
3890
+ var defaultDocker = (args) => {
3891
+ const r = spawnSync("docker", args, { encoding: "utf-8" });
3892
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
3893
+ };
3894
+ var mustDocker = (docker2, args, what) => {
3895
+ const r = docker2(args);
3896
+ if (r.status !== 0) {
3897
+ throw new MeshCliError(`${what} failed: ${(r.stderr || r.stdout).trim() || `docker exited ${r.status}`}`);
3898
+ }
3899
+ return r.stdout;
3900
+ };
3901
+ function ensureDatabase(docker2, app) {
3902
+ const container = mustDocker(
3903
+ docker2,
3904
+ [
3905
+ "ps",
3906
+ "-q",
3907
+ "--filter",
3908
+ `label=com.docker.compose.project=${COMPOSE_PROJECT}`,
3909
+ "--filter",
3910
+ "label=com.docker.compose.service=database"
3911
+ ],
3912
+ "Finding the local Postgres"
3913
+ ).trim().split("\n")[0];
3914
+ if (!container) {
3915
+ throw new MeshCliError("The local platform's Postgres is not running.", {
3916
+ remediation: { command: "mesh start" }
3917
+ });
3918
+ }
3919
+ const db = appSpiceDBDatabase(app);
3920
+ const exists = mustDocker(
3921
+ docker2,
3922
+ ["exec", container, "psql", "-U", "postgres", "-tAc", `SELECT 1 FROM pg_database WHERE datname = '${db}'`],
3923
+ `Checking database ${db}`
3924
+ ).trim();
3925
+ if (exists !== "1") {
3926
+ mustDocker(docker2, ["exec", container, "psql", "-U", "postgres", "-c", `CREATE DATABASE "${db}"`], `Creating database ${db}`);
3927
+ }
3928
+ }
3929
+ async function ensureLocalAppSpiceDB(app, deps = {}) {
3930
+ const docker2 = deps.docker ?? defaultDocker;
3931
+ const portFree = deps.portFree ?? (async (port) => !await probeTcp(port, { timeoutMs: 300 }));
3932
+ const name = appSpiceDBContainerName(app);
3933
+ const state = docker2(["inspect", "-f", "{{.State.Running}}", name]);
3934
+ if (state.status === 0) {
3935
+ if (state.stdout.trim() !== "true") mustDocker(docker2, ["start", name], `Starting ${name}`);
3936
+ const grpcPort = parseDockerPort(mustDocker(docker2, ["port", name, "50051/tcp"], `Reading ${name}'s ports`));
3937
+ const httpPort = parseDockerPort(mustDocker(docker2, ["port", name, "8443/tcp"], `Reading ${name}'s ports`));
3938
+ if (grpcPort && httpPort) return { grpcPort, httpPort, created: false };
3939
+ mustDocker(docker2, ["rm", "-f", name], `Removing ${name}`);
3940
+ }
3941
+ ensureDatabase(docker2, app);
3942
+ mustDocker(docker2, spicedbMigrateArgs(app), `Migrating ${name}'s datastore`);
3943
+ const first = appSpiceDBSlot(app);
3944
+ for (let i = 0; i < PORT_SLOTS; i++) {
3945
+ const ports = appSpiceDBPorts(first + i);
3946
+ if (!await portFree(ports.grpc) || !await portFree(ports.http)) continue;
3947
+ mustDocker(docker2, spicedbServeArgs(app, ports), `Starting ${name}`);
3948
+ return { grpcPort: ports.grpc, httpPort: ports.http, created: true };
3949
+ }
3950
+ throw new MeshCliError(`No free port pair for ${name} in ${GRPC_PORT_BASE}-${GRPC_PORT_BASE + PORT_SLOTS - 1}.`);
3951
+ }
3952
+ async function writeSpiceDBSchema(httpUrl, zed, opts = {}) {
3953
+ const fetchImpl = opts.fetchImpl ?? fetch;
3954
+ const attempts = opts.attempts ?? 30;
3955
+ let last = "";
3956
+ for (let i = 0; i < attempts; i++) {
3957
+ try {
3958
+ const res = await fetchImpl(`${httpUrl}/v1/schema/write`, {
3959
+ method: "POST",
3960
+ headers: { Authorization: `Bearer ${LOCAL_APP_SPICEDB_KEY}`, "content-type": "application/json" },
3961
+ body: JSON.stringify({ schema: zed })
3962
+ });
3963
+ if (res.ok) return;
3964
+ last = `${res.status} ${await res.text()}`;
3965
+ if (res.status >= 400 && res.status < 500) break;
3966
+ } catch (err) {
3967
+ last = err instanceof Error ? err.message : String(err);
3968
+ }
3969
+ await new Promise((r) => setTimeout(r, opts.delayMs ?? 500));
3970
+ }
3971
+ throw new MeshCliError(`SpiceDB at ${httpUrl} did not accept the app's schema: ${last}`);
3972
+ }
3973
+ function removeLocalAppSpiceDBs(docker2 = defaultDocker) {
3974
+ const ids = docker2(["ps", "-aq", "--filter", `label=${LOCAL_APP_SPICEDB_LABEL}`]).stdout.trim().split("\n").filter(Boolean);
3975
+ if (ids.length > 0) docker2(["rm", "-f", ...ids]);
3976
+ return ids;
3977
+ }
3978
+ var tsxImporter = async (specifier, parentFile) => {
3979
+ let api2;
3980
+ try {
3981
+ api2 = createRequire(parentFile).resolve("tsx/esm/api");
3982
+ } catch {
3983
+ throw new MeshCliError(
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\`).`
3985
+ );
3986
+ }
3987
+ const { tsImport } = await import(pathToFileURL(api2).href);
3988
+ return tsImport(specifier, pathToFileURL(parentFile).href);
3989
+ };
3990
+ var unwrapDefault = (mod) => {
3991
+ const d = mod.default ?? mod.schema;
3992
+ return d && typeof d === "object" && "default" in d && !("namespace" in d) ? d.default : d;
3993
+ };
3994
+ async function loadAppAuthz(appRoot, schemaRel, importer = tsxImporter) {
3995
+ const file = path12.resolve(appRoot, schemaRel);
3996
+ if (!fs12.existsSync(file)) {
3997
+ throw new MeshCliError(`mesh.authz.schema names ${schemaRel}, which does not exist under ${appRoot}.`);
3998
+ }
3999
+ const schema = unwrapDefault(await importer(pathToFileURL(file).href, file));
4000
+ if (!schema || typeof schema.namespace !== "string") {
4001
+ throw new MeshCliError(`${schemaRel} must export the app's defineSchema(...) result as its default export.`);
4002
+ }
4003
+ const compilers = await importer("@mesh-tech/authz/compilers", file);
4004
+ let chunk;
4005
+ try {
4006
+ const ssm = await importer("@mesh-tech/authz/metadata/ssm", file);
4007
+ if (typeof ssm.chunkSsmValue === "function") chunk = ssm.chunkSsmValue;
4008
+ } catch {
4009
+ }
4010
+ return { schema, compilers, chunk };
4011
+ }
4012
+ var localParamStore = async () => {
4013
+ const { SSMClient: SSMClient2, GetParameterCommand: GetParameterCommand2, PutParameterCommand } = await import("@aws-sdk/client-ssm");
4014
+ const ssm = new SSMClient2(LOCAL_AWS_CONFIG);
4015
+ return {
4016
+ async get(name) {
4017
+ try {
4018
+ return (await ssm.send(new GetParameterCommand2({ Name: name }))).Parameter?.Value;
4019
+ } catch {
4020
+ return void 0;
4021
+ }
4022
+ },
4023
+ async put(name, value) {
4024
+ await ssm.send(new PutParameterCommand({ Name: name, Value: value, Type: "String", Tier: "Advanced", Overwrite: true }));
4025
+ }
4026
+ };
4027
+ };
4028
+ async function publishLocalAppAuthz(args, deps = {}) {
4029
+ const env = LOCAL_ENV;
4030
+ const { schema, compilers, chunk } = await loadAppAuthz(args.appRoot, args.schema, deps.importer);
4031
+ const mode = schema.mode === "role-gating" ? "role-gating" : "policy-engine";
4032
+ const params = deps.params ?? await localParamStore();
4033
+ const base = appAuthzParam(args.tenant, env, args.app);
4034
+ const metadataRef = `${base}/ops-hub-metadata`;
4035
+ let instance;
4036
+ if (mode === "policy-engine") {
4037
+ const running = await (deps.ensureSpiceDB ?? ensureLocalAppSpiceDB)(args.app);
4038
+ instance = { grpcPort: running.grpcPort, httpPort: running.httpPort };
4039
+ await (deps.writeSchema ?? writeSpiceDBSchema)(
4040
+ `http://localhost:${instance.httpPort}`,
4041
+ compilers.compileSpiceDBSchema(schema).zed
4042
+ );
4043
+ await (deps.putSecret ?? upsertLocalSecret)(appSpiceDBSecretName(args.tenant, env, args.app), {
4044
+ PRESHARED_KEY: LOCAL_APP_SPICEDB_KEY
4045
+ });
4046
+ await params.put(
4047
+ spicedbInstanceParam(args.tenant, env, args.app),
4048
+ JSON.stringify(instanceRegistryValue({ app: args.app, tenant: args.tenant, env, ...instance }))
4049
+ );
4050
+ }
4051
+ const body = JSON.stringify(compilers.compileOpsHubMetadata(schema));
4052
+ const split = chunk ? chunk(body) : { value: body, parts: [] };
4053
+ for (const [i, part] of split.parts.entries()) await params.put(`${metadataRef}/part-${i}`, part);
4054
+ await params.put(metadataRef, split.value);
4055
+ let existing = {};
4056
+ const raw = await params.get(base);
4057
+ if (raw) {
4058
+ try {
4059
+ const parsed = JSON.parse(raw);
4060
+ if (parsed && typeof parsed === "object") existing = parsed;
4061
+ } catch {
4062
+ }
4063
+ }
4064
+ await params.put(
4065
+ base,
4066
+ JSON.stringify(
4067
+ buildAppAuthzPointer({
4068
+ existing,
4069
+ mode,
4070
+ instanceId: instance ? args.app : void 0,
4071
+ zitadel: args.zitadel,
4072
+ metadataRef
4073
+ })
4074
+ )
4075
+ );
4076
+ return {
4077
+ mode,
4078
+ env: instance ? appSpiceDBEnv({ ...instance, metadataPath: `${base}/metadata` }) : {},
4079
+ ...instance ? { instance } : {}
4080
+ };
4081
+ }
4082
+
4083
+ // packages/mesh-cli/src/commands/local/publish-docs.ts
4084
+ import { createHash as createHash3 } from "node:crypto";
4085
+ import * as fs13 from "node:fs";
4086
+ import * as path13 from "node:path";
4087
+ import { execFileSync as execFileSync6 } from "node:child_process";
4088
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
3562
4089
 
3563
4090
  // packages/mesh-cli/src/commands/local/local-spicedb.ts
3564
4091
  var LOCAL_SPICEDB_INSTANCE_ID = "mesh-local";
@@ -3709,14 +4236,14 @@ function zedOf(value) {
3709
4236
  if (typeof zed !== "string" || !zed.trim()) throw new Error("the spicedbSchema export returned no zed schema");
3710
4237
  return zed;
3711
4238
  }
3712
- async function spicedbHttp(path20, body) {
3713
- const res = await fetch(`${HOST_HTTP}${path20}`, {
4239
+ async function spicedbHttp(path23, body) {
4240
+ const res = await fetch(`${HOST_HTTP}${path23}`, {
3714
4241
  method: "POST",
3715
4242
  headers: { Authorization: `Bearer ${LOCAL_SPICEDB_KEY}`, "Content-Type": "application/json" },
3716
4243
  body: JSON.stringify(body)
3717
4244
  });
3718
4245
  const parsed = await res.json().catch(() => ({}));
3719
- if (!res.ok) throw Object.assign(new Error(`SpiceDB ${path20}: ${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 });
3720
4247
  return parsed;
3721
4248
  }
3722
4249
  async function readLocalSpicedbSchema() {
@@ -3853,7 +4380,7 @@ function readLocalPublishDeclaration(pkg, appRoot) {
3853
4380
  }
3854
4381
  if (typeof docs?.config === "string" && docs.config) {
3855
4382
  out.docs = {
3856
- config: path11.resolve(appRoot, docs.config),
4383
+ config: path13.resolve(appRoot, docs.config),
3857
4384
  actAs: docs.actAs === true,
3858
4385
  ...sandbox?.success ? { sandbox: sandbox.data } : {},
3859
4386
  ...typeof docs.local === "string" && docs.local.includes("#") ? { local: docs.local } : {},
@@ -3883,10 +4410,10 @@ function readLocalPublishDeclaration(pkg, appRoot) {
3883
4410
  }
3884
4411
  async function callExport(appRoot, spec, arg) {
3885
4412
  const [modulePath, exportName] = spec.split("#");
3886
- const file = path11.resolve(appRoot, modulePath);
4413
+ const file = path13.resolve(appRoot, modulePath);
3887
4414
  let mod;
3888
4415
  if (/\.m?js$/.test(file)) {
3889
- mod = await import(pathToFileURL(file).href);
4416
+ mod = await import(pathToFileURL2(file).href);
3890
4417
  } else {
3891
4418
  let tsImport;
3892
4419
  try {
@@ -4062,12 +4589,12 @@ async function publishLocalDocs(input) {
4062
4589
  ...local.events ? { events: local.events } : {}
4063
4590
  });
4064
4591
  result.problems.push(...built.problems);
4065
- const config = parseDocsConfig(fs11.readFileSync(docs.config, "utf8"), docs.config);
4592
+ const config = parseDocsConfig(fs13.readFileSync(docs.config, "utf8"), docs.config);
4066
4593
  const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
4067
4594
  const s3 = new S3Client({ ...awsConfig, forcePathStyle: true });
4068
4595
  const specKey = `api-docs/${app}/openapi.json`;
4069
4596
  const sourceKey = docsSourceKey(app);
4070
- const specText = fs11.readFileSync(built.specPath, "utf8");
4597
+ const specText = fs13.readFileSync(built.specPath, "utf8");
4071
4598
  await s3.send(new PutObjectCommand({ Bucket: ARTIFACTS_BUCKET, Key: specKey, Body: specText, ContentType: "application/json" }));
4072
4599
  await s3.send(new PutObjectCommand({ Bucket: ARTIFACTS_BUCKET, Key: sourceKey, Body: built.json, ContentType: "application/json" }));
4073
4600
  if (docs.actAs) {
@@ -4084,7 +4611,7 @@ async function publishLocalDocs(input) {
4084
4611
  ...config.description ? { description: config.description } : {},
4085
4612
  docs: {
4086
4613
  url: `s3://${ARTIFACTS_BUCKET}/${specKey}`,
4087
- contentHash: createHash2("sha256").update(specText).digest("hex"),
4614
+ contentHash: createHash3("sha256").update(specText).digest("hex"),
4088
4615
  sourceUrl: `s3://${ARTIFACTS_BUCKET}/${sourceKey}`,
4089
4616
  sourceContentHash: built.contentHash
4090
4617
  },
@@ -4233,8 +4760,8 @@ async function ensureLocalApiKey(input) {
4233
4760
 
4234
4761
  // packages/mesh-cli/src/commands/local/docker-runner.ts
4235
4762
  import { execFileSync as execFileSync7 } from "child_process";
4236
- import * as fs12 from "fs";
4237
- import * as path12 from "path";
4763
+ import * as fs14 from "fs";
4764
+ import * as path14 from "path";
4238
4765
  var DEV_RUNNER_IMAGE = "node:22-bookworm@sha256:5647be709086c696ff32edaaf1c70cd26d1da6ab2b39c32f3c7b4c4a31957e37";
4239
4766
  function sessionDir(sessionName) {
4240
4767
  return meshCacheDir("mesh-local", "dev", sessionName);
@@ -4257,8 +4784,8 @@ function writeDevCompose(sessionName, appRoot, services) {
4257
4784
  ];
4258
4785
  for (const [name, service] of Object.entries(services)) {
4259
4786
  const env = service.env ?? {};
4260
- fs12.writeFileSync(path12.join(dir, `${name}.env`), renderDockerEnvFile(env), { mode: 384 });
4261
- const workdir = path12.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);
4262
4789
  const ship = env.OTEL_RESOURCE_ATTRIBUTES ? ` 2>&1 | NODE_OPTIONS= node /mesh-assets/log-shipper.mjs` : "";
4263
4790
  blocks.push(
4264
4791
  ` ${name}:`,
@@ -4267,7 +4794,7 @@ function writeDevCompose(sessionName, appRoot, services) {
4267
4794
  ` working_dir: ${workdir}`,
4268
4795
  ` volumes:`,
4269
4796
  ` - '${appRoot}:/workspace'`,
4270
- ` - '${path12.dirname(shipper)}:/mesh-assets:ro'`,
4797
+ ` - '${path14.dirname(shipper)}:/mesh-assets:ro'`,
4271
4798
  ` environment:`,
4272
4799
  // node_modules is the HOST's bind-mounted install — pnpm inside the
4273
4800
  // container must never "verify" it and try a purge/reinstall (pnpm
@@ -4283,12 +4810,12 @@ function writeDevCompose(sessionName, appRoot, services) {
4283
4810
  ` restart: unless-stopped`
4284
4811
  );
4285
4812
  }
4286
- const composePath = path12.join(dir, "compose.yml");
4287
- fs12.writeFileSync(composePath, blocks.join("\n") + "\n");
4813
+ const composePath = path14.join(dir, "compose.yml");
4814
+ fs14.writeFileSync(composePath, blocks.join("\n") + "\n");
4288
4815
  return composePath;
4289
4816
  }
4290
4817
  function composeArgs(sessionName, args) {
4291
- return ["compose", "-p", sessionName, "-f", path12.join(sessionDir(sessionName), "compose.yml"), ...args];
4818
+ return ["compose", "-p", sessionName, "-f", path14.join(sessionDir(sessionName), "compose.yml"), ...args];
4292
4819
  }
4293
4820
  function dockerDevUp(sessionName) {
4294
4821
  execFileSync7("docker", composeArgs(sessionName, ["up", "-d"]), {
@@ -4361,10 +4888,173 @@ function rewriteClusterHostsToLocal(devOutput) {
4361
4888
  }
4362
4889
 
4363
4890
  // packages/mesh-cli/src/commands/dev-doctor.ts
4364
- import { execFileSync as execFileSync8 } from "node:child_process";
4365
- import * as fs13 from "node:fs";
4891
+ import { execFileSync as execFileSync9 } from "node:child_process";
4892
+ import * as fs16 from "node:fs";
4366
4893
  import * as net2 from "node:net";
4367
- import * as path13 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
4368
5058
  function aggregateStatus(results) {
4369
5059
  if (results.some((r) => r.status === "error")) return "error";
4370
5060
  if (results.some((r) => r.status === "warn")) return "warn";
@@ -4532,9 +5222,9 @@ function parsePortSquatter(lsofOut, psOut) {
4532
5222
  return { pid, command };
4533
5223
  }
4534
5224
  function sharesWorktreeRoot(serviceSrc, repoRoot) {
4535
- const src = path13.resolve(serviceSrc);
4536
- const root = path13.resolve(repoRoot);
4537
- return src === root || src.startsWith(root + path13.sep);
5225
+ const src = path16.resolve(serviceSrc);
5226
+ const root = path16.resolve(repoRoot);
5227
+ return src === root || src.startsWith(root + path16.sep);
4538
5228
  }
4539
5229
  function classifyPortListener(listenerPid, sessionPids) {
4540
5230
  if (listenerPid === null) return "dead";
@@ -4563,7 +5253,7 @@ function collectSessionPids(panePids, psOut) {
4563
5253
  }
4564
5254
  function whoHasPort(port) {
4565
5255
  try {
4566
- const lsof = execFileSync8("lsof", ["-ti", `:${port}`], {
5256
+ const lsof = execFileSync9("lsof", ["-ti", `:${port}`], {
4567
5257
  encoding: "utf8",
4568
5258
  stdio: ["ignore", "pipe", "ignore"]
4569
5259
  });
@@ -4571,7 +5261,7 @@ function whoHasPort(port) {
4571
5261
  if (!pid) return null;
4572
5262
  let ps = "";
4573
5263
  try {
4574
- ps = execFileSync8("ps", ["-o", "comm=", "-p", pid], {
5264
+ ps = execFileSync9("ps", ["-o", "comm=", "-p", pid], {
4575
5265
  encoding: "utf8",
4576
5266
  stdio: ["ignore", "pipe", "ignore"]
4577
5267
  });
@@ -4585,7 +5275,7 @@ function whoHasPort(port) {
4585
5275
  function sessionProcessTree(sessionName) {
4586
5276
  let panePids;
4587
5277
  try {
4588
- const out = execFileSync8(
5278
+ const out = execFileSync9(
4589
5279
  "tmux",
4590
5280
  ["list-panes", "-s", "-t", sessionName, "-F", "#{pane_pid}"],
4591
5281
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -4597,7 +5287,7 @@ function sessionProcessTree(sessionName) {
4597
5287
  if (panePids.length === 0) return /* @__PURE__ */ new Set();
4598
5288
  let psOut = "";
4599
5289
  try {
4600
- psOut = execFileSync8("ps", ["-eo", "pid=,ppid="], {
5290
+ psOut = execFileSync9("ps", ["-eo", "pid=,ppid="], {
4601
5291
  encoding: "utf8",
4602
5292
  stdio: ["ignore", "pipe", "ignore"]
4603
5293
  });
@@ -4606,28 +5296,159 @@ function sessionProcessTree(sessionName) {
4606
5296
  }
4607
5297
  return collectSessionPids(panePids, psOut);
4608
5298
  }
4609
- var tmuxCheck = {
4610
- id: "tmux",
4611
- title: "tmux",
4612
- phases: ["preflight", "ondemand"],
4613
- async run() {
4614
- try {
4615
- execFileSync8("which", ["tmux"], { stdio: "ignore" });
4616
- return { status: "ok", summary: "installed" };
4617
- } catch {
4618
- return {
4619
- status: "error",
4620
- summary: "tmux is not installed",
4621
- remediation: "brew install tmux"
4622
- };
4623
- }
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;
4624
5313
  }
4625
- };
4626
- var portsCheck = {
4627
- id: "ports",
4628
- title: "Ports",
4629
- phases: ["preflight", "ondemand"],
4630
- async run(ctx) {
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
+ };
5430
+ var tmuxCheck = {
5431
+ id: "tmux",
5432
+ title: "tmux",
5433
+ phases: ["preflight", "ondemand"],
5434
+ async run() {
5435
+ try {
5436
+ execFileSync9("which", ["tmux"], { stdio: "ignore" });
5437
+ return { status: "ok", summary: "installed" };
5438
+ } catch {
5439
+ return {
5440
+ status: "error",
5441
+ summary: "tmux is not installed",
5442
+ remediation: "brew install tmux"
5443
+ };
5444
+ }
5445
+ }
5446
+ };
5447
+ var portsCheck = {
5448
+ id: "ports",
5449
+ title: "Ports",
5450
+ phases: ["preflight", "ondemand"],
5451
+ async run(ctx) {
4631
5452
  const services = ctx.sessionState?.devOutput.services;
4632
5453
  if (!services || Object.keys(services).length === 0) {
4633
5454
  return {
@@ -4690,14 +5511,14 @@ var configStalenessCheck = {
4690
5511
  if (!state) {
4691
5512
  return { status: "ok", summary: "no session \u2014 config read fresh at launch" };
4692
5513
  }
4693
- const configPath = path13.join(ctx.appRoot, `Pulumi.${ctx.stack}.yaml`);
4694
- if (!fs13.existsSync(configPath)) {
5514
+ const configPath = path16.join(ctx.appRoot, `Pulumi.${ctx.stack}.yaml`);
5515
+ if (!fs16.existsSync(configPath)) {
4695
5516
  return {
4696
5517
  status: "ok",
4697
5518
  summary: `no Pulumi.${ctx.stack}.yaml to compare`
4698
5519
  };
4699
5520
  }
4700
- const mtimeMs = fs13.statSync(configPath).mtimeMs;
5521
+ const mtimeMs = fs16.statSync(configPath).mtimeMs;
4701
5522
  if (isConfigStale(mtimeMs, state.startedAt)) {
4702
5523
  return {
4703
5524
  status: "warn",
@@ -4720,7 +5541,7 @@ var worktreeCheck = {
4720
5541
  }
4721
5542
  let repoRoot;
4722
5543
  try {
4723
- repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
5544
+ repoRoot = execFileSync9("git", ["rev-parse", "--show-toplevel"], {
4724
5545
  cwd: ctx.appRoot,
4725
5546
  encoding: "utf8",
4726
5547
  stdio: ["ignore", "pipe", "ignore"]
@@ -4868,9 +5689,11 @@ var ALL_CHECKS = [
4868
5689
  registryCheck,
4869
5690
  configStalenessCheck,
4870
5691
  portsCheck,
5692
+ servicesCheck,
4871
5693
  worktreeCheck,
4872
5694
  tunnelsCheck,
4873
- temporalCheck
5695
+ temporalCheck,
5696
+ envLeaksCheck
4874
5697
  ];
4875
5698
  async function runDoctor(ctx, opts) {
4876
5699
  const results = await runChecks(ctx, "ondemand", ALL_CHECKS);
@@ -4885,10 +5708,75 @@ ${ICONS[status]} overall: ${status}`);
4885
5708
  return status;
4886
5709
  }
4887
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
+
4888
5776
  // packages/mesh-cli/src/utils/worktree-identity.ts
4889
- import { execFileSync as execFileSync9 } from "node:child_process";
5777
+ import { execFileSync as execFileSync10 } from "node:child_process";
4890
5778
  import * as crypto from "node:crypto";
4891
- import * as path14 from "node:path";
5779
+ import * as path17 from "node:path";
4892
5780
  var NUM_PORT_BLOCKS = 64;
4893
5781
  var PORT_BLOCK_BASE = 4e4;
4894
5782
  var PORT_BLOCK_SIZE = 40;
@@ -4910,7 +5798,7 @@ function blockBasePort(block) {
4910
5798
  }
4911
5799
  function withAppScopedPortBlock(wt, appRoot) {
4912
5800
  if (wt.isPrimary) return wt;
4913
- const appHash = worktreeHash(`${wt.worktreeRoot}#${path14.resolve(appRoot)}`);
5801
+ const appHash = worktreeHash(`${wt.worktreeRoot}#${path17.resolve(appRoot)}`);
4914
5802
  return { ...wt, portBlock: portBlockFor(appHash, false) };
4915
5803
  }
4916
5804
  var STACK_SLUG_MAX = 16;
@@ -4922,33 +5810,33 @@ function worktreeStackName(baseName, wt) {
4922
5810
  function stackNeedsWorktreeIsolation(stackName, wt) {
4923
5811
  return !!wt.token && !stackName.endsWith(`-${wt.hash}`);
4924
5812
  }
4925
- 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();
4926
5814
  function resolveWorktreeIdentity(cwd = process.cwd(), gitRunner = defaultGitRunner) {
4927
5815
  let worktreeRoot;
4928
5816
  let isPrimary = true;
4929
5817
  try {
4930
- worktreeRoot = path14.resolve(gitRunner(["rev-parse", "--show-toplevel"], cwd));
5818
+ worktreeRoot = path17.resolve(gitRunner(["rev-parse", "--show-toplevel"], cwd));
4931
5819
  const commonDir = gitRunner(["rev-parse", "--git-common-dir"], cwd);
4932
- const primaryRoot = path14.resolve(path14.dirname(path14.resolve(cwd, commonDir)));
5820
+ const primaryRoot = path17.resolve(path17.dirname(path17.resolve(cwd, commonDir)));
4933
5821
  isPrimary = worktreeRoot === primaryRoot;
4934
5822
  } catch {
4935
5823
  return {
4936
- worktreeRoot: path14.resolve(cwd),
5824
+ worktreeRoot: path17.resolve(cwd),
4937
5825
  isPrimary: true,
4938
- slug: sanitizeSlug(path14.basename(path14.resolve(cwd))),
4939
- hash: worktreeHash(path14.resolve(cwd)),
5826
+ slug: sanitizeSlug(path17.basename(path17.resolve(cwd))),
5827
+ hash: worktreeHash(path17.resolve(cwd)),
4940
5828
  token: "",
4941
5829
  portBlock: 0,
4942
5830
  taskQueueSuffix: ""
4943
5831
  };
4944
5832
  }
4945
- const slug = sanitizeSlug(path14.basename(worktreeRoot));
5833
+ const slug2 = sanitizeSlug(path17.basename(worktreeRoot));
4946
5834
  const hash = worktreeHash(worktreeRoot);
4947
- const token = isPrimary ? "" : `${slug}-${hash}`;
5835
+ const token = isPrimary ? "" : `${slug2}-${hash}`;
4948
5836
  return {
4949
5837
  worktreeRoot,
4950
5838
  isPrimary,
4951
- slug,
5839
+ slug: slug2,
4952
5840
  hash,
4953
5841
  token,
4954
5842
  portBlock: portBlockFor(hash, isPrimary),
@@ -4980,6 +5868,9 @@ function registerServiceProbes(devOutput, tenant, probeFiles) {
4980
5868
  function deriveSessionName(projectName, wt) {
4981
5869
  return wt.isPrimary ? `${projectName}-dev` : `${projectName}-${wt.slug}`;
4982
5870
  }
5871
+ function sessionNameFor(appRoot, explicit) {
5872
+ return explicit ?? deriveSessionName(getProjectName(appRoot), resolveWorktreeIdentity(appRoot));
5873
+ }
4983
5874
  function printDevPlan(sessionName, appRoot, wt, devOutput) {
4984
5875
  const kind = wt.isPrimary ? "primary checkout" : `linked worktree (block ${wt.portBlock})`;
4985
5876
  console.log(`
@@ -5002,9 +5893,9 @@ mesh dev plan \u2014 ${kind}
5002
5893
  console.log(` (none \u2014 all services are deployed/K8s)`);
5003
5894
  } else {
5004
5895
  const monorepoRoot = findMonorepoRoot();
5005
- const wtWithSep = wt.worktreeRoot.endsWith(path15.sep) ? wt.worktreeRoot : wt.worktreeRoot + path15.sep;
5896
+ const wtWithSep = wt.worktreeRoot.endsWith(path18.sep) ? wt.worktreeRoot : wt.worktreeRoot + path18.sep;
5006
5897
  for (const [name, s] of locals) {
5007
- const dir = path15.resolve(appRoot, rebaseServiceSrc(s.src, monorepoRoot));
5898
+ const dir = path18.resolve(appRoot, rebaseServiceSrc(s.src, monorepoRoot));
5008
5899
  const outside = dir !== wt.worktreeRoot && !dir.startsWith(wtWithSep);
5009
5900
  const port = s.port > 0 ? `:${s.port}` : "(no port)";
5010
5901
  console.log(` ${name.padEnd(18)} ${port.padEnd(7)} ${dir}${outside ? " \u26A0 OUTSIDE worktree" : ""}`);
@@ -5013,18 +5904,17 @@ mesh dev plan \u2014 ${kind}
5013
5904
  console.log("");
5014
5905
  }
5015
5906
  function getSessionStatePath(sessionName) {
5016
- const dir = path15.join(os3.tmpdir(), "mesh-dev-sessions");
5017
- if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
5018
- return path15.join(dir, `${sessionName}.json`);
5907
+ const dir = devSessionsRoot();
5908
+ return path18.join(dir, `${sessionName}.json`);
5019
5909
  }
5020
5910
  function saveSessionState(sessionName, state) {
5021
- fs14.writeFileSync(getSessionStatePath(sessionName), JSON.stringify(state, null, 2));
5911
+ fs17.writeFileSync(getSessionStatePath(sessionName), JSON.stringify(state, null, 2));
5022
5912
  }
5023
5913
  function loadSessionState(sessionName) {
5024
5914
  const filePath = getSessionStatePath(sessionName);
5025
- if (!fs14.existsSync(filePath)) return null;
5915
+ if (!fs17.existsSync(filePath)) return null;
5026
5916
  try {
5027
- return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
5917
+ return JSON.parse(fs17.readFileSync(filePath, "utf-8"));
5028
5918
  } catch {
5029
5919
  return null;
5030
5920
  }
@@ -5038,7 +5928,7 @@ function workflowChangeWouldStrand(priorState, appRoot, force) {
5038
5928
  const current = fingerprintWorkflowSource(appRoot, priorState?.devOutput?.services);
5039
5929
  if (!workflowChanged(prior, current)) return false;
5040
5930
  logWarn(
5041
- "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."
5042
5932
  );
5043
5933
  if (force) {
5044
5934
  logWarn(" Proceeding anyway (--force).");
@@ -5047,19 +5937,56 @@ function workflowChangeWouldStrand(priorState, appRoot, force) {
5047
5937
  logError(" Refusing to restart the worker. Re-run with --force once the change is replay-safe.");
5048
5938
  return true;
5049
5939
  }
5050
- function getSessionEnvDir(sessionName) {
5051
- return path15.join(os3.tmpdir(), "mesh-dev-sessions", sessionName);
5052
- }
5053
5940
  function getServiceEnvFilePath(sessionName, serviceName) {
5054
- return path15.join(getSessionEnvDir(sessionName), envFileName(serviceName));
5941
+ return path18.join(getSessionEnvDir(sessionName), envFileName(serviceName));
5055
5942
  }
5056
5943
  function removeSessionState(sessionName) {
5057
5944
  try {
5058
- fs14.unlinkSync(getSessionStatePath(sessionName));
5945
+ fs17.unlinkSync(getSessionStatePath(sessionName));
5059
5946
  } catch {
5060
5947
  }
5061
5948
  try {
5062
- fs14.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));
5063
5990
  } catch {
5064
5991
  }
5065
5992
  }
@@ -5108,7 +6035,7 @@ function rewriteOwnServicePort(service, oldPort, newPort) {
5108
6035
  }
5109
6036
  }
5110
6037
  }
5111
- async function allocatePorts(devOutput, worktree) {
6038
+ async function allocatePorts(devOutput, worktree, remembered = {}, probes = { isPortFree, findFreePort }, ownPorts = /* @__PURE__ */ new Set()) {
5112
6039
  const allocated = JSON.parse(JSON.stringify(devOutput));
5113
6040
  const usedPorts = /* @__PURE__ */ new Set();
5114
6041
  const portRemaps = /* @__PURE__ */ new Map();
@@ -5118,25 +6045,35 @@ async function allocatePorts(devOutput, worktree) {
5118
6045
  if (service.port <= 0) continue;
5119
6046
  if (!service.command || service.command.length === 0) continue;
5120
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));
5121
6050
  let target = -1;
5122
6051
  if (worktree.isPrimary) {
5123
- if (!usedPorts.has(preferred) && await isPortFree(preferred)) {
6052
+ if (!usedPorts.has(preferred) && await probes.isPortFree(preferred)) {
5124
6053
  target = preferred;
6054
+ } else if (await lastIsUsable()) {
6055
+ target = last;
6056
+ logInfo(`Port ${preferred} busy for service '${name}', reusing last launch's ${target}`);
5125
6057
  } else {
5126
- target = await findFreePort();
6058
+ target = await probes.findFreePort();
5127
6059
  logWarn(`Port ${preferred} busy for service '${name}', using ${target}`);
5128
6060
  }
6061
+ } else if (await lastIsUsable()) {
6062
+ target = last;
5129
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
+ }
5130
6067
  for (let i = blockCursor; i < PORT_BLOCK_SERVICE_SUBRANGE; i++) {
5131
6068
  const candidate = blockBase + i;
5132
- if (!usedPorts.has(candidate) && await isPortFree(candidate)) {
6069
+ if (!usedPorts.has(candidate) && await probes.isPortFree(candidate)) {
5133
6070
  target = candidate;
5134
6071
  blockCursor = i + 1;
5135
6072
  break;
5136
6073
  }
5137
6074
  }
5138
6075
  if (target < 0) {
5139
- target = await findFreePort();
6076
+ target = await probes.findFreePort();
5140
6077
  logWarn(`Worktree port block exhausted for '${name}', using ${target}`);
5141
6078
  }
5142
6079
  }
@@ -5174,12 +6111,15 @@ function mergePinnedPorts(fresh, pinned) {
5174
6111
  if (!pinnedSvc || typeof pinnedSvc.port !== "number") continue;
5175
6112
  const freshPort = mergedSvc.port;
5176
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
+ }
5177
6120
  if (mergedSvc.env?.PORT) {
5178
6121
  mergedSvc.env.PORT.value = String(pinnedSvc.port);
5179
6122
  }
5180
- if (typeof freshPort === "number" && freshPort > 0 && freshPort !== pinnedSvc.port) {
5181
- portRemaps.set(freshPort, pinnedSvc.port);
5182
- }
5183
6123
  }
5184
6124
  rewriteCrossServicePorts(merged, portRemaps);
5185
6125
  rewriteClusterHostsToLocal(merged);
@@ -5187,16 +6127,16 @@ function mergePinnedPorts(fresh, pinned) {
5187
6127
  }
5188
6128
  function findMonorepoRoot() {
5189
6129
  let dir = process.cwd();
5190
- const root = path15.parse(dir).root;
6130
+ const root = path18.parse(dir).root;
5191
6131
  while (dir !== root) {
5192
- if (fs14.existsSync(path15.join(dir, "pnpm-workspace.yaml")) || fs14.existsSync(path15.join(dir, "turbo.json"))) {
6132
+ if (fs17.existsSync(path18.join(dir, "pnpm-workspace.yaml")) || fs17.existsSync(path18.join(dir, "turbo.json"))) {
5193
6133
  return dir;
5194
6134
  }
5195
- const gitPath = path15.join(dir, ".git");
5196
- if (fs14.existsSync(gitPath)) {
6135
+ const gitPath = path18.join(dir, ".git");
6136
+ if (fs17.existsSync(gitPath)) {
5197
6137
  return dir;
5198
6138
  }
5199
- dir = path15.dirname(dir);
6139
+ dir = path18.dirname(dir);
5200
6140
  }
5201
6141
  return null;
5202
6142
  }
@@ -5204,40 +6144,40 @@ function discoverApps(monorepoRoot) {
5204
6144
  const apps = [];
5205
6145
  function addApp(appDir, tenant) {
5206
6146
  const projectName = getProjectName(appDir);
5207
- const stacks = fs14.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$/, ""));
5208
6148
  apps.push({
5209
6149
  name: projectName,
5210
6150
  tenant,
5211
6151
  appPath: appDir,
5212
- relativePath: path15.relative(monorepoRoot, appDir),
6152
+ relativePath: path18.relative(monorepoRoot, appDir),
5213
6153
  stacks
5214
6154
  });
5215
6155
  }
5216
6156
  const searchDirs = ["tenants", "tests/tenants"];
5217
6157
  for (const searchDir of searchDirs) {
5218
- const base = path15.join(monorepoRoot, searchDir);
5219
- if (!fs14.existsSync(base)) continue;
5220
- for (const tenant of fs14.readdirSync(base)) {
5221
- const appsDir = path15.join(base, tenant, "apps");
5222
- if (!fs14.existsSync(appsDir) || !fs14.statSync(appsDir).isDirectory()) continue;
5223
- for (const app of fs14.readdirSync(appsDir)) {
5224
- const appDir = path15.join(appsDir, app);
5225
- if (!fs14.existsSync(path15.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;
5226
6166
  addApp(appDir, tenant);
5227
6167
  }
5228
6168
  }
5229
6169
  }
5230
- const flatAppsDir = path15.join(monorepoRoot, "apps");
5231
- if (fs14.existsSync(flatAppsDir) && fs14.statSync(flatAppsDir).isDirectory()) {
5232
- for (const app of fs14.readdirSync(flatAppsDir)) {
5233
- const appDir = path15.join(flatAppsDir, app);
5234
- if (!fs14.statSync(appDir).isDirectory()) continue;
5235
- if (!fs14.existsSync(path15.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;
5236
6176
  let tenant = "unknown";
5237
- const stackConfigs = fs14.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");
5238
6178
  if (stackConfigs.length > 0) {
5239
6179
  try {
5240
- const content = fs14.readFileSync(path15.join(appDir, stackConfigs[0]), "utf-8");
6180
+ const content = fs17.readFileSync(path18.join(appDir, stackConfigs[0]), "utf-8");
5241
6181
  const tenantMatch = content.match(/mesh:tenant:\s*(\S+)/);
5242
6182
  if (tenantMatch) tenant = tenantMatch[1];
5243
6183
  } catch {
@@ -5250,38 +6190,38 @@ function discoverApps(monorepoRoot) {
5250
6190
  }
5251
6191
  function findAppRoot2(appPath) {
5252
6192
  if (appPath) {
5253
- const resolved = path15.resolve(appPath);
5254
- if (fs14.existsSync(path15.join(resolved, "Pulumi.yaml"))) return resolved;
6193
+ const resolved = path18.resolve(appPath);
6194
+ if (fs17.existsSync(path18.join(resolved, "Pulumi.yaml"))) return resolved;
5255
6195
  const mono = findMonorepoRoot();
5256
6196
  if (mono) {
5257
- const fromMono = path15.resolve(mono, appPath);
5258
- if (fs14.existsSync(path15.join(fromMono, "Pulumi.yaml"))) return fromMono;
6197
+ const fromMono = path18.resolve(mono, appPath);
6198
+ if (fs17.existsSync(path18.join(fromMono, "Pulumi.yaml"))) return fromMono;
5259
6199
  }
5260
6200
  logError(`No Pulumi.yaml found at: ${appPath}`);
5261
6201
  process.exit(1);
5262
6202
  }
5263
6203
  let dir = process.cwd();
5264
- const root = path15.parse(dir).root;
6204
+ const root = path18.parse(dir).root;
5265
6205
  while (dir !== root) {
5266
- if (fs14.existsSync(path15.join(dir, "Pulumi.yaml"))) {
6206
+ if (fs17.existsSync(path18.join(dir, "Pulumi.yaml"))) {
5267
6207
  return dir;
5268
6208
  }
5269
- dir = path15.dirname(dir);
6209
+ dir = path18.dirname(dir);
5270
6210
  }
5271
6211
  return process.cwd();
5272
6212
  }
5273
6213
  function getProjectName(appRoot) {
5274
- const yamlPath = path15.join(appRoot, "Pulumi.yaml");
5275
- if (!fs14.existsSync(yamlPath)) return path15.basename(appRoot);
5276
- const content = fs14.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");
5277
6217
  const match = content.match(/^name:\s*(.+)$/m);
5278
- return match?.[1]?.trim() ?? path15.basename(appRoot);
6218
+ return match?.[1]?.trim() ?? path18.basename(appRoot);
5279
6219
  }
5280
6220
  function detectStack(appRoot, stageArg) {
5281
6221
  if (stageArg) return stageArg;
5282
6222
  if (process.env.MESH_STAGE) return process.env.MESH_STAGE;
5283
6223
  try {
5284
- const result = execFileSync10("pulumi", ["stack", "--show-name"], {
6224
+ const result = execFileSync11("pulumi", ["stack", "--show-name"], {
5285
6225
  cwd: appRoot,
5286
6226
  encoding: "utf-8",
5287
6227
  stdio: ["ignore", "pipe", "ignore"]
@@ -5290,7 +6230,7 @@ function detectStack(appRoot, stageArg) {
5290
6230
  } catch {
5291
6231
  }
5292
6232
  try {
5293
- const files = fs14.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");
5294
6234
  if (files.length === 1) {
5295
6235
  const match = files[0].match(/^Pulumi\.(.+)\.yaml$/);
5296
6236
  if (match?.[1]) {
@@ -5304,7 +6244,7 @@ function detectStack(appRoot, stageArg) {
5304
6244
  }
5305
6245
  function stackArgs(appRoot, stack) {
5306
6246
  try {
5307
- const selected = execFileSync10("pulumi", ["stack", "--show-name"], {
6247
+ const selected = execFileSync11("pulumi", ["stack", "--show-name"], {
5308
6248
  cwd: appRoot,
5309
6249
  encoding: "utf-8",
5310
6250
  stdio: ["ignore", "pipe", "ignore"]
@@ -5313,7 +6253,7 @@ function stackArgs(appRoot, stack) {
5313
6253
  } catch {
5314
6254
  }
5315
6255
  try {
5316
- execFileSync10("pulumi", ["stack", "select", stack], {
6256
+ execFileSync11("pulumi", ["stack", "select", stack], {
5317
6257
  cwd: appRoot,
5318
6258
  encoding: "utf-8",
5319
6259
  stdio: ["ignore", "pipe", "ignore"]
@@ -5347,9 +6287,85 @@ function getDevOutput(appRoot, stack, awsEnv) {
5347
6287
  throw new MissingStackOutputError(stack, err);
5348
6288
  }
5349
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
+ }
5350
6366
  function hasTmux() {
5351
6367
  try {
5352
- execFileSync10("which", ["tmux"], { stdio: "ignore" });
6368
+ execFileSync11("which", ["tmux"], { stdio: "ignore" });
5353
6369
  return true;
5354
6370
  } catch {
5355
6371
  return false;
@@ -5357,7 +6373,7 @@ function hasTmux() {
5357
6373
  }
5358
6374
  function sessionExists(name) {
5359
6375
  try {
5360
- execFileSync10("tmux", ["has-session", "-t", name], { stdio: "ignore" });
6376
+ execFileSync11("tmux", ["has-session", "-t", name], { stdio: "ignore" });
5361
6377
  return true;
5362
6378
  } catch {
5363
6379
  return false;
@@ -5365,7 +6381,7 @@ function sessionExists(name) {
5365
6381
  }
5366
6382
  function killSession(name) {
5367
6383
  try {
5368
- execFileSync10("tmux", ["kill-session", "-t", name], { stdio: "ignore" });
6384
+ execFileSync11("tmux", ["kill-session", "-t", name], { stdio: "ignore" });
5369
6385
  return true;
5370
6386
  } catch {
5371
6387
  return false;
@@ -5411,9 +6427,9 @@ function buildChildAwsEnv(awsEnv, method, profile, opts) {
5411
6427
  if (method === "zitadel" && opts) {
5412
6428
  const sanitizedContext = opts.context.replace(/[^A-Za-z0-9_-]/g, "-");
5413
6429
  const profileName = `mesh-dev-${sanitizedContext}`;
5414
- const configPath = path15.join(opts.sessionScratchDir, "aws-config");
6430
+ const configPath = path18.join(opts.sessionScratchDir, "aws-config");
5415
6431
  const region = awsEnv.AWS_REGION ?? awsEnv.AWS_DEFAULT_REGION ?? "";
5416
- fs14.mkdirSync(opts.sessionScratchDir, { recursive: true });
6432
+ fs17.mkdirSync(opts.sessionScratchDir, { recursive: true });
5417
6433
  const profileBlock = renderCredentialProcessProfile({
5418
6434
  profileName,
5419
6435
  context: opts.context,
@@ -5421,7 +6437,7 @@ function buildChildAwsEnv(awsEnv, method, profile, opts) {
5421
6437
  region,
5422
6438
  meshBin: opts.meshBin
5423
6439
  });
5424
- const existing = fs14.existsSync(configPath) ? fs14.readFileSync(configPath, "utf8") : "";
6440
+ const existing = fs17.existsSync(configPath) ? fs17.readFileSync(configPath, "utf8") : "";
5425
6441
  const next = upsertManagedAwsConfigSection(
5426
6442
  stripBareProfile(existing, profileName),
5427
6443
  `${opts.context} (mesh dev)`,
@@ -5446,7 +6462,7 @@ function buildChildAwsEnv(awsEnv, method, profile, opts) {
5446
6462
  function readDotEnvFile(file) {
5447
6463
  let text;
5448
6464
  try {
5449
- text = fs14.readFileSync(file, "utf-8");
6465
+ text = fs17.readFileSync(file, "utf-8");
5450
6466
  } catch {
5451
6467
  return {};
5452
6468
  }
@@ -5479,37 +6495,42 @@ function rebaseServiceSrc(src, monorepoRoot) {
5479
6495
  if (src === "." || src === "./") return src;
5480
6496
  const rel = monorepoRelativeSrc(src);
5481
6497
  if (rel === null) return src;
5482
- const rebased = path15.join(monorepoRoot, rel);
5483
- return fs14.existsSync(rebased) ? rebased : src;
6498
+ const rebased = path18.join(monorepoRoot, rel);
6499
+ return fs17.existsSync(rebased) ? rebased : src;
5484
6500
  }
5485
6501
  function isLinkedDependencyDir(serviceDir, appRoot) {
5486
- let pkgRoot = null;
5487
- let name = "";
5488
- for (let dir = serviceDir; ; dir = path15.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)) {
5489
6507
  try {
5490
- const manifest = JSON.parse(fs14.readFileSync(path15.join(dir, "package.json"), "utf8"));
5491
- if (typeof manifest?.name === "string" && manifest.name) {
5492
- pkgRoot = dir;
5493
- name = manifest.name;
5494
- break;
5495
- }
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 };
5496
6510
  } catch {
5497
6511
  }
5498
- const parent = path15.dirname(dir);
5499
- if (parent === dir) break;
6512
+ if (path18.dirname(at) === at) return null;
5500
6513
  }
5501
- if (!pkgRoot) return false;
5502
- for (let dir = appRoot; ; dir = path15.dirname(dir)) {
6514
+ }
6515
+ function appResolvedPackageDir(name, appRoot) {
6516
+ for (let dir = appRoot; ; dir = path18.dirname(dir)) {
5503
6517
  try {
5504
- if (fs14.realpathSync(path15.join(dir, "node_modules", name)) === pkgRoot) return true;
6518
+ return fs17.realpathSync(path18.join(dir, "node_modules", name));
5505
6519
  } catch {
5506
6520
  }
5507
- const parent = path15.dirname(dir);
5508
- if (parent === dir) return false;
6521
+ if (path18.dirname(dir) === dir) return null;
5509
6522
  }
5510
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
+ }
5511
6532
  function monorepoRelativeSrc(src) {
5512
- if (!path15.isAbsolute(src)) return src.replace(/^\.\//, "") || ".";
6533
+ if (!path18.isAbsolute(src)) return src.replace(/^\.\//, "") || ".";
5513
6534
  const m = src.match(/^.*\/mesh-platform(?:\/(.*))?$/);
5514
6535
  if (!m) return null;
5515
6536
  const rest = (m[1] ?? "").replace(/^\.worktrees\/[^/]+(?:\/|$)/, "");
@@ -5523,30 +6544,51 @@ function getServiceEnvVars(service, tunnels) {
5523
6544
  if (spec.tunnel) {
5524
6545
  const tunnel = tunnels[spec.tunnel];
5525
6546
  if (tunnel) {
5526
- try {
5527
- const url = new URL(value);
5528
- if (url.hostname) {
5529
- url.hostname = tunnel.host;
5530
- url.port = String(tunnel.port);
5531
- value = url.toString();
5532
- } else {
5533
- value = `${tunnel.host}:${tunnel.port}`;
5534
- }
5535
- } catch {
5536
- value = `${tunnel.host}:${tunnel.port}`;
5537
- }
6547
+ value = pointAtTunnel(key, value, tunnel);
6548
+ const portKey = portSiblingOf(key);
6549
+ if (portKey && service.env[portKey]) result[portKey] = String(tunnel.port);
5538
6550
  }
5539
6551
  }
5540
- result[key] = value;
6552
+ if (!(key in result)) result[key] = value;
5541
6553
  }
5542
6554
  }
5543
6555
  return result;
5544
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
+ }
5545
6573
  function setTmuxEnv(sessionName, vars) {
5546
6574
  for (const [key, value] of Object.entries(vars)) {
5547
- execFileSync10("tmux", ["set-environment", "-t", sessionName, key, value], { stdio: "ignore" });
6575
+ execFileSync11("tmux", ["set-environment", "-t", sessionName, key, value], { stdio: "ignore" });
5548
6576
  }
5549
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
+ }
5550
6592
  function envPrefix(vars) {
5551
6593
  const entries = Object.entries(vars);
5552
6594
  if (entries.length === 0) return "";
@@ -5557,7 +6599,32 @@ function shellEscape(s) {
5557
6599
  if (/^[a-zA-Z0-9_./:@=+,-]+$/.test(s)) return s;
5558
6600
  return `'${s.replace(/'/g, "'\\''")}'`;
5559
6601
  }
5560
- 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) {
5561
6628
  const client = new SecretsManagerClient2({});
5562
6629
  const resolved = {};
5563
6630
  for (const [name, secret] of Object.entries(secrets)) {
@@ -5567,11 +6634,13 @@ async function resolveSecrets(secrets) {
5567
6634
  );
5568
6635
  if (!response.SecretString) continue;
5569
6636
  const values = JSON.parse(response.SecretString);
6637
+ const mapped = {};
5570
6638
  for (const [secretKey, envVar] of Object.entries(secret.envMapping)) {
5571
6639
  const value = values[secretKey];
5572
6640
  if (value === void 0) continue;
5573
- resolved[envVar] = value;
6641
+ mapped[envVar] = value;
5574
6642
  }
6643
+ Object.assign(resolved, pointSecretUrlsAtTunnel(mapped, tunnelForSecret(name, tunnels)));
5575
6644
  logSuccess(`Secret resolved: ${name} (${secret.secretName})`);
5576
6645
  } catch (err) {
5577
6646
  logWarn(
@@ -5581,11 +6650,15 @@ async function resolveSecrets(secrets) {
5581
6650
  }
5582
6651
  return resolved;
5583
6652
  }
5584
- function resolveTemporalEncodingKey(tenant, env, appName) {
5585
- 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) {
5586
6659
  const secretName = `${namespace}-temporal-encoding-key`;
5587
6660
  try {
5588
- const b64 = execFileSync10(
6661
+ const b64 = execFileSync11(
5589
6662
  "kubectl",
5590
6663
  [
5591
6664
  "get",
@@ -5621,18 +6694,20 @@ async function resolveTemporalAuthVars(sessionName, devOutput) {
5621
6694
  setTmuxEnv(sessionName, { KUBECONFIG: kubeconfigPath });
5622
6695
  } else if (!process.env.KUBECONFIG) {
5623
6696
  const existing = sessionKubeconfigPath(sessionName);
5624
- if (fs14.existsSync(existing)) {
6697
+ if (fs17.existsSync(existing)) {
5625
6698
  process.env.KUBECONFIG = existing;
5626
6699
  }
5627
6700
  }
5628
6701
  let temporalAuthVars = {};
5629
6702
  if (devOutput.tunnels["temporal"]) {
5630
6703
  temporalAuthVars = await resolveTemporalAuth(tenant, platformEnv, devOutput.platform?.name ?? tenant);
5631
- const appName = devOutput.app ?? "";
5632
- if (appName) {
5633
- const encodingKey = resolveTemporalEncodingKey(tenant, platformEnv, appName);
6704
+ const namespace = stackNamespace(devOutput);
6705
+ if (namespace) {
6706
+ const encodingKey = resolveTemporalEncodingKey(namespace);
5634
6707
  if (encodingKey) {
5635
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.`);
5636
6711
  }
5637
6712
  }
5638
6713
  const temporalTunnel = devOutput.tunnels["temporal"];
@@ -5792,13 +6867,29 @@ async function allocateSsmLocalPort(remotePort, reservedPorts) {
5792
6867
  }
5793
6868
  function hasSessionManagerPlugin() {
5794
6869
  try {
5795
- execFileSync10("which", ["session-manager-plugin"], { stdio: "ignore" });
6870
+ execFileSync11("which", ["session-manager-plugin"], { stdio: "ignore" });
5796
6871
  return true;
5797
6872
  } catch {
5798
6873
  return false;
5799
6874
  }
5800
6875
  }
5801
- 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) {
5802
6893
  const platformEnv = devOutput.platform?.env ?? "dev";
5803
6894
  const bastionTenant = devOutput.platform?.name ?? "mesh";
5804
6895
  const bastion = await getPlatformBastionInfo(bastionTenant, platformEnv).catch(() => null);
@@ -5815,13 +6906,6 @@ async function startSsmTunnels(sessionName, devOutput) {
5815
6906
  return rewritten;
5816
6907
  }
5817
6908
  logInfo(`Starting ${tunnelCount} SSM tunnel(s) via bastion ${bastion.instanceId}...`);
5818
- const tunnelToBastionKey = {
5819
- temporal: "temporal-frontend",
5820
- "temporal-ui": "temporal-ui",
5821
- rds: "rds",
5822
- db: "rds",
5823
- database: "rds"
5824
- };
5825
6909
  const OPTIONAL_TUNNEL_NAMES = /* @__PURE__ */ new Set([
5826
6910
  "loki",
5827
6911
  "prometheus",
@@ -5833,9 +6917,10 @@ async function startSsmTunnels(sessionName, devOutput) {
5833
6917
  const skippedOptional = [];
5834
6918
  const reservedPorts = /* @__PURE__ */ new Set();
5835
6919
  const plannedTunnels = [];
5836
- for (const [tunnelName] of Object.entries(devOutput.tunnels)) {
5837
- const bastionKey = tunnelToBastionKey[tunnelName] ?? tunnelName;
5838
- 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;
5839
6924
  if (!endpoint) {
5840
6925
  if (OPTIONAL_TUNNEL_NAMES.has(tunnelName)) {
5841
6926
  skippedOptional.push(`${tunnelName} (bastion key: ${bastionKey})`);
@@ -5874,6 +6959,7 @@ async function startSsmTunnels(sessionName, devOutput) {
5874
6959
  );
5875
6960
  throw new Error("SSM tunnel fallback unavailable: missing bastion endpoints");
5876
6961
  }
6962
+ const hostEnv = pickLaunchHostEnv(process.env);
5877
6963
  for (const { tunnelName, endpoint, localPort } of plannedTunnels) {
5878
6964
  const windowName = `tunnel-${tunnelName}`;
5879
6965
  const ssmCmd = [
@@ -5891,13 +6977,21 @@ async function startSsmTunnels(sessionName, devOutput) {
5891
6977
  localPortNumber: [String(localPort)]
5892
6978
  })}'`
5893
6979
  ].join(" ");
5894
- execFileSync10("tmux", ["new-window", "-t", sessionName, "-n", windowName]);
5895
- execFileSync10(
6980
+ execFileSync11("tmux", ["new-window", "-t", sessionName, "-n", windowName]);
6981
+ execFileSync11(
5896
6982
  "tmux",
5897
6983
  ["set-option", "-t", `${sessionName}:${windowName}`, "remain-on-exit", "on"],
5898
6984
  { stdio: "ignore" }
5899
6985
  );
5900
- 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
+ ]);
5901
6995
  rewritten.tunnels[tunnelName] = {
5902
6996
  host: "localhost",
5903
6997
  port: localPort
@@ -5909,7 +7003,7 @@ async function startSsmTunnels(sessionName, devOutput) {
5909
7003
  return rewritten;
5910
7004
  }
5911
7005
  async function updateDevboxProxy(devOutput, appRoot) {
5912
- const appName = devOutput.app ?? path15.basename(appRoot);
7006
+ const appName = devOutput.app ?? path18.basename(appRoot);
5913
7007
  const tenant = devOutput.platform?.tenant ?? "mesh";
5914
7008
  const env = devOutput.platform?.env ?? "dev";
5915
7009
  const stack = devOutput.stack ?? "dev";
@@ -5941,7 +7035,7 @@ async function updateDevboxProxy(devOutput, appRoot) {
5941
7035
  lines.push("");
5942
7036
  }
5943
7037
  const caddyfile = lines.join("\n");
5944
- execFileSync10(
7038
+ execFileSync11(
5945
7039
  "docker",
5946
7040
  [
5947
7041
  "run",
@@ -5957,7 +7051,7 @@ CADDYEOF`
5957
7051
  ],
5958
7052
  { stdio: "pipe" }
5959
7053
  );
5960
- execFileSync10(
7054
+ execFileSync11(
5961
7055
  "docker",
5962
7056
  [
5963
7057
  "run",
@@ -5987,7 +7081,7 @@ CADDYEOF`
5987
7081
  async function setupSubdomainRouting(devOutput, appRoot) {
5988
7082
  const tsInfo = await getTailscaleInfo();
5989
7083
  if (!tsInfo) return null;
5990
- const appName = devOutput.app ?? path15.basename(appRoot);
7084
+ const appName = devOutput.app ?? path18.basename(appRoot);
5991
7085
  const stack = devOutput.stack ?? "dev";
5992
7086
  const baseDomain = `${appName}.${stack}.${tsInfo.hostname}.vpn.internal`;
5993
7087
  const platform = devOutput.platform ?? { tenant: "mesh", env: "dev" };
@@ -6006,8 +7100,8 @@ async function setupSubdomainRouting(devOutput, appRoot) {
6006
7100
  } catch {
6007
7101
  }
6008
7102
  const caddyDir = "/tmp/mesh-dev-caddy";
6009
- const caddyfile = path15.join(caddyDir, "Caddyfile");
6010
- if (!fs14.existsSync(caddyDir)) fs14.mkdirSync(caddyDir, { recursive: true });
7103
+ const caddyfile = path18.join(caddyDir, "Caddyfile");
7104
+ if (!fs17.existsSync(caddyDir)) fs17.mkdirSync(caddyDir, { recursive: true });
6011
7105
  const caddyLines = [`# mesh-dev: ${appName}/${stack} (${tsInfo.hostname})`, ""];
6012
7106
  for (const [name, service] of Object.entries(devOutput.services)) {
6013
7107
  if (service.port) {
@@ -6025,16 +7119,16 @@ async function setupSubdomainRouting(devOutput, appRoot) {
6025
7119
  caddyLines.push("}");
6026
7120
  caddyLines.push("");
6027
7121
  }
6028
- fs14.writeFileSync(caddyfile, caddyLines.join("\n"));
7122
+ fs17.writeFileSync(caddyfile, caddyLines.join("\n"));
6029
7123
  let caddyRunning = false;
6030
7124
  try {
6031
- execFileSync10("pgrep", ["-f", "caddy run.*mesh-dev-caddy"], { stdio: "pipe" });
7125
+ execFileSync11("pgrep", ["-f", "caddy run.*mesh-dev-caddy"], { stdio: "pipe" });
6032
7126
  caddyRunning = true;
6033
7127
  } catch {
6034
7128
  }
6035
7129
  if (caddyRunning) {
6036
7130
  try {
6037
- execFileSync10("caddy", ["reload", "--config", caddyfile, "--adapter", "caddyfile"], {
7131
+ execFileSync11("caddy", ["reload", "--config", caddyfile, "--adapter", "caddyfile"], {
6038
7132
  stdio: "pipe"
6039
7133
  });
6040
7134
  } catch {
@@ -6043,7 +7137,7 @@ async function setupSubdomainRouting(devOutput, appRoot) {
6043
7137
  }
6044
7138
  } else {
6045
7139
  try {
6046
- execFileSync10("which", ["caddy"], { stdio: "pipe" });
7140
+ execFileSync11("which", ["caddy"], { stdio: "pipe" });
6047
7141
  } catch {
6048
7142
  logWarn(
6049
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"
@@ -6085,8 +7179,14 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6085
7179
  return devOutput;
6086
7180
  }
6087
7181
  logInfo(`Creating tmux session: ${sessionName}`);
6088
- execFileSync10("tmux", ["new-session", "-d", "-s", sessionName, "-n", "status", "-c", appRoot]);
6089
- 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);
6090
7190
  const hasTunnels = Object.keys(devOutput.tunnels).length > 0;
6091
7191
  let effectiveTransport = tunnelPlan.transport;
6092
7192
  if (hasTunnels && effectiveTransport === "tailscale" && tunnelPlan.tailscale) {
@@ -6116,7 +7216,7 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6116
7216
  }
6117
7217
  }
6118
7218
  if (hasTunnels && effectiveTransport === "ssm") {
6119
- devOutput = await startSsmTunnels(sessionName, devOutput);
7219
+ devOutput = await startSsmTunnels(sessionName, devOutput, appRoot, awsEnv);
6120
7220
  }
6121
7221
  if (hasTunnels && effectiveTransport !== "vpn-direct") {
6122
7222
  for (const [tName, tunnel] of Object.entries(devOutput.tunnels)) {
@@ -6153,7 +7253,7 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6153
7253
  }
6154
7254
  let secretEnvVars = {};
6155
7255
  if (devOutput.secrets && Object.keys(devOutput.secrets).length > 0) {
6156
- secretEnvVars = await resolveSecrets(devOutput.secrets);
7256
+ secretEnvVars = await resolveSecrets(devOutput.secrets, devOutput.tunnels);
6157
7257
  }
6158
7258
  const tenant = devOutput.platform?.tenant ?? "mesh";
6159
7259
  const platformEnv = devOutput.platform?.env ?? "dev";
@@ -6167,7 +7267,7 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6167
7267
  });
6168
7268
  if (kubeconfigPath) {
6169
7269
  process.env.KUBECONFIG = kubeconfigPath;
6170
- setTmuxEnv(sessionName, { KUBECONFIG: kubeconfigPath });
7270
+ shareOnSession({ KUBECONFIG: kubeconfigPath });
6171
7271
  logSuccess(`Kubeconfig resolved from SSM (hub ${platformName}/${platformEnv}) \u2192 ${kubeconfigPath}`);
6172
7272
  } else {
6173
7273
  logWarn("Could not resolve EKS kubeconfig from SSM; kubectl calls will use ambient config (if any).");
@@ -6176,37 +7276,47 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6176
7276
  let temporalAuthVars = {};
6177
7277
  if (devOutput.tunnels["temporal"]) {
6178
7278
  temporalAuthVars = await resolveTemporalAuth(tenant, platformEnv, devOutput.platform?.name ?? tenant);
6179
- const appName = devOutput.app ?? "";
6180
- if (appName) {
6181
- const encodingKey = resolveTemporalEncodingKey(tenant, platformEnv, appName);
7279
+ const namespace = stackNamespace(devOutput);
7280
+ if (namespace) {
7281
+ const encodingKey = resolveTemporalEncodingKey(namespace);
6182
7282
  if (encodingKey) {
6183
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.`);
6184
7286
  }
6185
7287
  }
6186
7288
  const temporalTunnel = devOutput.tunnels["temporal"];
6187
7289
  temporalAuthVars.TEMPORAL_ADDRESS = tunnelClientAddress(temporalTunnel);
6188
7290
  }
6189
- setTmuxEnv(sessionName, secretEnvVars);
6190
- setTmuxEnv(sessionName, temporalAuthVars);
7291
+ shareOnSession(secretEnvVars);
7292
+ shareOnSession(temporalAuthVars);
6191
7293
  const credContext = devOutput.platform ? isLocalPlatform ? "local" : loginContextFor(devOutput.platform) : null;
6192
7294
  const devUserVars = resolveDevUserVars(devOutput);
6193
7295
  if (Object.keys(devUserVars).length > 0) {
6194
- setTmuxEnv(sessionName, devUserVars);
7296
+ shareOnSession(devUserVars);
6195
7297
  logSuccess(`Dev user injected from mesh login: ${credContext}`);
6196
7298
  const tokenPort = await findFreePort();
6197
7299
  const tokenUrl = `http://127.0.0.1:${tokenPort}`;
6198
- setTmuxEnv(sessionName, { DEV_USER_TOKEN_URL: tokenUrl });
6199
- execFileSync10("tmux", ["new-window", "-t", sessionName, "-n", "token-server"]);
6200
- execFileSync10(
7300
+ shareOnSession({ DEV_USER_TOKEN_URL: tokenUrl });
7301
+ execFileSync11("tmux", ["new-window", "-t", sessionName, "-n", "token-server"]);
7302
+ execFileSync11(
6201
7303
  "tmux",
6202
7304
  ["set-option", "-t", `${sessionName}:token-server`, "remain-on-exit", "on"],
6203
7305
  { stdio: "ignore" }
6204
7306
  );
6205
- execFileSync10("tmux", [
7307
+ const tokenServerEnvFile = getServiceEnvFilePath(sessionName, "token-server");
7308
+ writeEnvFile(tokenServerEnvFile, hostEnv);
7309
+ execFileSync11("tmux", [
6206
7310
  "send-keys",
6207
7311
  "-t",
6208
7312
  `${sessionName}:token-server`,
6209
- `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
+ ),
6210
7320
  "Enter"
6211
7321
  ]);
6212
7322
  for (let i = 0; i < 15 && !await isPortListening(tokenPort); i++) {
@@ -6219,33 +7329,55 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6219
7329
  logInfo(" Hub UI will show unauthenticated state without valid credentials.");
6220
7330
  }
6221
7331
  const monorepoRoot = findMonorepoRoot();
7332
+ const notLaunched = [];
6222
7333
  for (const [name, service] of Object.entries(devOutput.services)) {
6223
7334
  const cmd = service.command.join(" ");
6224
7335
  if (!cmd) {
6225
7336
  logInfo(`Skipped: ${name} (no dev command, deployed to K8s)`);
6226
7337
  continue;
6227
7338
  }
6228
- const serviceDir = path15.resolve(appRoot, rebaseServiceSrc(service.src, monorepoRoot));
6229
- if (worktreeRoot && fs14.existsSync(serviceDir)) {
6230
- const wtRootWithSep = worktreeRoot.endsWith(path15.sep) ? worktreeRoot : worktreeRoot + path15.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;
6231
7342
  const outside = serviceDir !== worktreeRoot && !serviceDir.startsWith(wtRootWithSep);
6232
7343
  if (outside && isLinkedDependencyDir(serviceDir, appRoot)) {
6233
7344
  logInfo(`${name}: source is a dev-linked package (${serviceDir})`);
6234
7345
  } else if (outside) {
6235
- logError(
6236
- `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.`
6237
- );
6238
- 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
+ }
6239
7359
  }
6240
7360
  }
6241
- if (!fs14.existsSync(serviceDir)) {
7361
+ if (!fs17.existsSync(serviceDir)) {
6242
7362
  logWarn(
6243
7363
  `Skipped: ${name} (source directory not found: ${serviceDir}${serviceDir !== service.src ? ` \u2014 rebased from ${service.src}` : ""})`
6244
7364
  );
6245
7365
  continue;
6246
7366
  }
6247
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,
6248
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,
6249
7381
  ...temporalAuthVars,
6250
7382
  // Overlay the resolved dev-user identity for the same reason as the
6251
7383
  // Temporal vars: the dev output may carry a placeholder (e.g. hub ui's
@@ -6268,21 +7400,31 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6268
7400
  // no AWS_* here it silently has none to give. `buildChildAwsEnv` has already
6269
7401
  // chosen the SELF-REFRESHING shape (a profile with `credential_process`, not a
6270
7402
  // frozen STS triple), which is exactly what a long-lived child needs.
6271
- ...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
6272
7410
  };
6273
7411
  const envFilePath = getServiceEnvFilePath(sessionName, name);
6274
- 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 });
6275
7416
  const launchCmd = buildLaunchCommand(
6276
7417
  envFilePath,
6277
7418
  serviceDir,
6278
7419
  cmd,
6279
- service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0
7420
+ service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0,
7421
+ hostEnv
6280
7422
  );
6281
- execFileSync10("tmux", ["new-window", "-t", sessionName, "-n", name, "-c", serviceDir]);
6282
- 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"], {
6283
7425
  stdio: "ignore"
6284
7426
  });
6285
- execFileSync10("tmux", ["send-keys", "-t", `${sessionName}:${name}`, launchCmd, "Enter"]);
7427
+ execFileSync11("tmux", ["send-keys", "-t", `${sessionName}:${name}`, launchCmd, "Enter"]);
6286
7428
  logSuccess(`Started: ${name} (${serviceDir}, port ${service.port})`);
6287
7429
  }
6288
7430
  if (process.env.DEVCONTAINER === "1") {
@@ -6293,9 +7435,13 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6293
7435
  }
6294
7436
  }
6295
7437
  const statusCmd = `watch -n2 -t npx mesh dev --status --session '${sessionName}'`;
6296
- execFileSync10("tmux", ["send-keys", "-t", `${sessionName}:status`, statusCmd, "Enter"]);
7438
+ execFileSync11("tmux", ["send-keys", "-t", `${sessionName}:status`, statusCmd, "Enter"]);
6297
7439
  console.log("");
6298
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
+ );
6299
7445
  console.log("");
6300
7446
  if (tunnelNames.length > 0) {
6301
7447
  console.log("Connections (VPN direct):");
@@ -6328,9 +7474,9 @@ async function startServices(sessionName, appRoot, devOutput, headless, awsEnv,
6328
7474
  if (!headless) {
6329
7475
  logInfo("Attaching to tmux session...");
6330
7476
  if (process.env.TMUX) {
6331
- spawnSync("tmux", ["switch-client", "-t", sessionName], { stdio: "inherit" });
7477
+ spawnSync2("tmux", ["switch-client", "-t", sessionName], { stdio: "inherit" });
6332
7478
  } else {
6333
- spawnSync("tmux", ["attach", "-t", sessionName], { stdio: "inherit" });
7479
+ spawnSync2("tmux", ["attach", "-t", sessionName], { stdio: "inherit" });
6334
7480
  }
6335
7481
  } else {
6336
7482
  console.log(`Attach with: tmux attach -t ${sessionName}`);
@@ -6348,31 +7494,20 @@ async function showStatus(sessionName, devOutput, asJson) {
6348
7494
  }
6349
7495
  return;
6350
7496
  }
6351
- let windows = [];
6352
- try {
6353
- const raw = execFileSync10(
6354
- "tmux",
6355
- ["list-windows", "-t", sessionName, "-F", "#{window_name} #{pane_dead}"],
6356
- { encoding: "utf-8" }
6357
- );
6358
- windows = raw.trim().split("\n");
6359
- } catch {
6360
- }
6361
- const windowStatus = {};
6362
- for (const line of windows) {
6363
- const [name, dead] = line.split(" ");
6364
- if (name) windowStatus[name] = dead === "1" ? "exited" : "running";
6365
- }
7497
+ const liveness = classifySessionServices(sessionName, devOutput.services, paneProcessQuery);
6366
7498
  const tunnelNames = Object.keys(devOutput.tunnels);
6367
7499
  const tunnelHealth = await probeTunnelHealth(devOutput.tunnels);
6368
7500
  const healthByName = new Map(tunnelHealth.map((h) => [h.name, h]));
6369
7501
  if (asJson) {
6370
7502
  const services = {};
6371
7503
  for (const [name, svc] of Object.entries(devOutput.services)) {
7504
+ const l = liveness.get(name);
6372
7505
  services[name] = {
6373
- status: windowStatus[name] ?? "unknown",
7506
+ status: l?.state ?? "unknown",
6374
7507
  port: svc.port,
6375
- 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() } : {}
6376
7511
  };
6377
7512
  }
6378
7513
  const tunnels = {};
@@ -6402,10 +7537,16 @@ async function showStatus(sessionName, devOutput, asJson) {
6402
7537
  console.log(` \u2601 ${name.padEnd(16)} deployed ${svc.src}`);
6403
7538
  continue;
6404
7539
  }
6405
- const status = windowStatus[name] ?? "unknown";
7540
+ const l = liveness.get(name);
7541
+ const status = l?.state ?? "unknown";
6406
7542
  const icon = status === "running" ? "\u25CF" : status === "exited" ? "\u2717" : "?";
7543
+ const statusCol = l?.state === "exited" && l.exitCode !== null ? `exited(${l.exitCode})` : status;
6407
7544
  const addr = svc.port ? `:${svc.port}` : "";
6408
- 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);
6409
7550
  }
6410
7551
  console.log("");
6411
7552
  console.log("Commands:");
@@ -6427,10 +7568,15 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6427
7568
  logError(`No active session: ${sessionName}. Run 'mesh dev' first.`);
6428
7569
  process.exit(1);
6429
7570
  }
6430
- 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) {
6431
7577
  if (!opts.stack) {
6432
7578
  logError("--refresh-env: no stack resolved; keeping existing env and NOT restarting.");
6433
- return;
7579
+ return null;
6434
7580
  }
6435
7581
  const pinned = opts.sessionState?.devOutput ?? devOutput;
6436
7582
  let fresh;
@@ -6441,20 +7587,26 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6441
7587
  `--refresh-env: failed to read stack output for '${opts.stack}'. Keeping the existing env file for '${serviceName}' and NOT restarting.`
6442
7588
  );
6443
7589
  logInfo(String(err?.message ?? err));
6444
- return;
7590
+ return null;
6445
7591
  }
7592
+ warnIfDevOutputStale(fresh, appRoot, opts.stack);
6446
7593
  const merged = mergePinnedPorts(fresh, pinned);
6447
7594
  const mergedService = merged.services[serviceName];
6448
7595
  if (!mergedService) {
6449
7596
  logError(
6450
7597
  `--refresh-env: service '${serviceName}' is absent from the fresh stack output. Keeping the existing env file and NOT restarting.`
6451
7598
  );
6452
- return;
7599
+ return null;
6453
7600
  }
6454
7601
  const temporalAuthVars = await resolveTemporalAuthVars(sessionName, merged);
7602
+ const secretEnvVars = merged.secrets && Object.keys(merged.secrets).length > 0 ? await resolveSecrets(merged.secrets, merged.tunnels) : {};
6455
7603
  const { taskQueueSuffix } = resolveWorktreeIdentity(appRoot);
6456
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),
6457
7608
  ...getServiceEnvVars(mergedService, merged.tunnels),
7609
+ ...secretEnvVars,
6458
7610
  ...temporalAuthVars,
6459
7611
  // Same overlay as launch: without it, --refresh-env rewrites the env
6460
7612
  // file from dev output and its DEV_USER_* placeholders clobber the real
@@ -6464,30 +7616,75 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6464
7616
  // Same overlay as launch — a refresh that dropped these would take a working
6465
7617
  // service's credentials away on restart, which is the hardest kind of change to
6466
7618
  // attribute afterwards.
6467
- ...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"])
6468
7625
  };
6469
- 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 });
6470
7645
  service = mergedService;
6471
7646
  logSuccess(`Regenerated env for '${serviceName}' from current stack/SSM outputs.`);
6472
7647
  }
6473
7648
  const target = `${sessionName}:${serviceName}`;
6474
7649
  try {
6475
- execFileSync10("tmux", ["respawn-pane", "-k", "-t", target], { stdio: "ignore" });
7650
+ execFileSync11("tmux", ["respawn-pane", "-k", "-t", target], { stdio: "ignore" });
7651
+ } catch {
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" });
6476
7662
  } catch {
6477
- logError(`Window '${serviceName}' not found in session.`);
6478
- process.exit(1);
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`);
6479
7674
  }
6480
7675
  setTmuxEnv(sessionName, awsEnv);
6481
7676
  const cmd = service.command.join(" ");
6482
7677
  const envFilePath = getServiceEnvFilePath(sessionName, serviceName);
6483
7678
  let restartCmd;
6484
- if (fs14.existsSync(envFilePath)) {
6485
- const serviceDir = path15.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;
6486
7682
  restartCmd = buildLaunchCommand(
6487
7683
  envFilePath,
6488
7684
  serviceDir,
6489
7685
  cmd,
6490
- service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0
7686
+ service.env?.OTEL_RESOURCE_ATTRIBUTES ? logShipperPath() : void 0,
7687
+ pickLaunchHostEnv(process.env)
6491
7688
  );
6492
7689
  } else {
6493
7690
  logWarn(
@@ -6497,8 +7694,80 @@ async function restartService(sessionName, serviceName, appRoot, devOutput, awsE
6497
7694
  const serviceVars = getServiceEnvVars(service, devOutput.tunnels);
6498
7695
  restartCmd = `${envPrefix(serviceVars)}${cmd}`;
6499
7696
  }
6500
- execFileSync10("tmux", ["send-keys", "-t", target, restartCmd, "Enter"]);
6501
- 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
+ }
6502
7771
  }
6503
7772
  function showLogs(sessionName, serviceName, tail) {
6504
7773
  if (!sessionExists(sessionName)) {
@@ -6507,7 +7776,7 @@ function showLogs(sessionName, serviceName, tail) {
6507
7776
  }
6508
7777
  const target = `${sessionName}:${serviceName}`;
6509
7778
  try {
6510
- const result = execFileSync10("tmux", ["capture-pane", "-t", target, "-p", "-S", `-${tail}`], {
7779
+ const result = execFileSync11("tmux", ["capture-pane", "-t", target, "-p", "-S", `-${tail}`], {
6511
7780
  encoding: "utf-8"
6512
7781
  });
6513
7782
  process.stdout.write(result);
@@ -6542,7 +7811,9 @@ function buildDoctorContext(appRoot, stack, sessionName) {
6542
7811
  };
6543
7812
  }
6544
7813
  function registerDevCommand(program) {
6545
- 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(
6546
7817
  "--session <name>",
6547
7818
  "Override the tmux session name (auto-derived per git worktree by default \u2014 omit to keep concurrent worktrees isolated)"
6548
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(
@@ -6562,7 +7833,7 @@ function registerDevCommand(program) {
6562
7833
  "tmux"
6563
7834
  );
6564
7835
  dev.command("__token-server <port> <context>", { hidden: true }).action(async (port, context) => {
6565
- const { startTokenServer } = await import("./mesh.dev-token-server-YP2VBP2S.js");
7836
+ const { startTokenServer } = await import("./mesh.dev-token-server-XG26C6IQ.js");
6566
7837
  await startTokenServer(Number(port), context);
6567
7838
  });
6568
7839
  dev.action(async (options) => {
@@ -6715,7 +7986,13 @@ function registerDevCommand(program) {
6715
7986
  mocks: selectedMocks
6716
7987
  });
6717
7988
  if (options.dryRun) {
6718
- 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
+ );
6719
7996
  for (const [name, decl] of Object.entries(selectedMocks)) {
6720
7997
  const mode = externalMode(name, decl);
6721
7998
  if (mode === "local") {
@@ -6740,7 +8017,7 @@ function registerDevCommand(program) {
6740
8017
  signInServices = [];
6741
8018
  try {
6742
8019
  const appPkg = JSON.parse(
6743
- fs14.readFileSync(path15.join(appRoot, "package.json"), "utf-8")
8020
+ fs17.readFileSync(path18.join(appRoot, "package.json"), "utf-8")
6744
8021
  );
6745
8022
  if (Array.isArray(appPkg?.mesh?.auth?.roles)) {
6746
8023
  authRoles = appPkg.mesh.auth.roles.filter((r) => typeof r === "string");
@@ -6761,7 +8038,39 @@ function registerDevCommand(program) {
6761
8038
  `Auth auto-provisioning skipped: ${err instanceof Error ? err.message : err} \u2014 if the local Zitadel predates seeding, run: mesh stop --destroy && mesh start`
6762
8039
  );
6763
8040
  }
6764
- const devOutput2 = await allocatePorts(rawDevOutput2, worktree);
8041
+ const authzConfig = readLocalAuthzConfig(appRoot);
8042
+ if (authzConfig?.prebuilt) {
8043
+ logWarn(
8044
+ `package.json declares both mesh.authz.schema and mesh.authz.metadata \u2014 both publish this app's authorization. Using mesh.authz.metadata; remove one of them.`
8045
+ );
8046
+ } else if (authzConfig) {
8047
+ const app = rawDevOutput2.app ?? projectName;
8048
+ try {
8049
+ const published = await publishLocalAppAuthz({
8050
+ appRoot,
8051
+ schema: authzConfig.schema,
8052
+ tenant: localTenant,
8053
+ app,
8054
+ ...provisioned ? { zitadel: { projectId: provisioned.projectId, orgId: provisioned.orgId } } : {}
8055
+ });
8056
+ for (const [name, svc] of Object.entries(rawDevOutput2.services)) {
8057
+ if (name.startsWith("mock-")) continue;
8058
+ svc.env = {
8059
+ ...svc.env ?? {},
8060
+ ...Object.fromEntries(Object.entries(published.env).map(([k, value]) => [k, { value }]))
8061
+ };
8062
+ }
8063
+ logSuccess(
8064
+ published.instance ? `Authz (${published.mode}) published for '${app}' \u2014 its SpiceDB on localhost:${published.instance.grpcPort} (HTTP ${published.instance.httpPort})` : `Authz (${published.mode}) published for '${app}'`
8065
+ );
8066
+ } catch (err) {
8067
+ logWarn(
8068
+ `Could not publish '${app}'s authorization (${err instanceof Error ? err.message : err}) \u2014 the Hub cannot author its roles, and a policy-engine app denies every check.`
8069
+ );
8070
+ }
8071
+ }
8072
+ const plan2 = sessionPortPlan(sessionName);
8073
+ const devOutput2 = await allocatePorts(rawDevOutput2, worktree, plan2.remembered, void 0, plan2.ownPorts);
6765
8074
  for (const service of signInServices) {
6766
8075
  const port = devOutput2.services[service]?.port;
6767
8076
  if (!port) {
@@ -6805,7 +8114,7 @@ function registerDevCommand(program) {
6805
8114
  }
6806
8115
  let localDocs;
6807
8116
  try {
6808
- const appPkg = JSON.parse(fs14.readFileSync(path15.join(appRoot, "package.json"), "utf-8"));
8117
+ const appPkg = JSON.parse(fs17.readFileSync(path18.join(appRoot, "package.json"), "utf-8"));
6809
8118
  const declaration = readLocalPublishDeclaration(appPkg, appRoot);
6810
8119
  if (declaration.docs || declaration.authzMetadata || declaration.problems.length > 0) {
6811
8120
  const app = devOutput2.app ?? projectName;
@@ -6815,10 +8124,10 @@ function registerDevCommand(program) {
6815
8124
  if (!httpService || !service?.port) {
6816
8125
  logWarn(`mesh.docs names service '${httpService ?? ""}', which this app does not run \u2014 docs not published.`);
6817
8126
  } else {
6818
- const serviceDir = path15.resolve(appRoot, service.src);
8127
+ const serviceDir = path18.resolve(appRoot, service.src);
6819
8128
  const env = service.env ?? (service.env = {});
6820
8129
  const serviceEnv = {
6821
- ...readDotEnvFile(path15.join(serviceDir, ".env")),
8130
+ ...readDotEnvFile(path18.join(serviceDir, ".env")),
6822
8131
  ...Object.fromEntries(Object.entries(process.env).filter((e) => typeof e[1] === "string")),
6823
8132
  ...Object.fromEntries(Object.entries(env).map(([k, v]) => [k, v.value]))
6824
8133
  };
@@ -6963,6 +8272,7 @@ function registerDevCommand(program) {
6963
8272
  externalProbeFiles,
6964
8273
  ...localDocs ? { localDocs } : {}
6965
8274
  });
8275
+ saveSessionPortMemory(sessionName, devOutput2);
6966
8276
  console.log("");
6967
8277
  logSuccess(`Dev session started (docker): ${sessionName}`);
6968
8278
  console.log("");
@@ -6992,10 +8302,12 @@ function registerDevCommand(program) {
6992
8302
  devOutput: finalDevOutput2,
6993
8303
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6994
8304
  runner: "tmux",
8305
+ cleanEnv: true,
6995
8306
  composeExternals,
6996
8307
  externalProbeFiles,
6997
8308
  ...localDocs ? { localDocs } : {}
6998
8309
  });
8310
+ saveSessionPortMemory(sessionName, finalDevOutput2);
6999
8311
  return;
7000
8312
  }
7001
8313
  if (options.mock || options.externals) {
@@ -7007,9 +8319,9 @@ function registerDevCommand(program) {
7007
8319
  logInfo(`Project: ${projectName}, Stack: ${stack}`);
7008
8320
  const monorepoRoot = findMonorepoRoot();
7009
8321
  const looksLikeStandaloneApp = ["api", "worker", "web"].some(
7010
- (dir) => fs14.existsSync(path15.join(appRoot, dir, "package.json"))
8322
+ (dir) => fs17.existsSync(path18.join(appRoot, dir, "package.json"))
7011
8323
  );
7012
- if (monorepoRoot && path15.resolve(appRoot) === path15.resolve(monorepoRoot) && !looksLikeStandaloneApp) {
8324
+ if (monorepoRoot && path18.resolve(appRoot) === path18.resolve(monorepoRoot) && !looksLikeStandaloneApp) {
7013
8325
  logError(`mesh dev should be run from an app directory, not the monorepo root.
7014
8326
  `);
7015
8327
  logInfo("Try:");
@@ -7140,7 +8452,9 @@ function registerDevCommand(program) {
7140
8452
  await showStatus(sessionName, state?.devOutput ?? rawDevOutput, !!options.json);
7141
8453
  return;
7142
8454
  }
7143
- 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);
7144
8458
  transport = await gateVpnDirectReachability(transport, devOutput);
7145
8459
  if (options.dryRun) {
7146
8460
  printDevPlan(sessionName, appRoot, worktree, devOutput);
@@ -7208,112 +8522,113 @@ function registerDevCommand(program) {
7208
8522
  stack,
7209
8523
  devOutput: finalDevOutput,
7210
8524
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8525
+ cleanEnv: true,
7211
8526
  workflowFingerprint: fingerprintWorkflowSource(appRoot, finalDevOutput.services)
7212
8527
  });
8528
+ saveSessionPortMemory(sessionName, finalDevOutput);
7213
8529
  });
7214
8530
  dev.command("logs <service>").description("Show logs for a service").option("--tail <lines>", "Number of lines", "100").action((service, opts) => {
7215
- const sessionName = dev.opts().session;
7216
- const resolvedSession = sessionName ?? `${getProjectName(findAppRoot2())}-dev`;
7217
- if (loadSessionState(resolvedSession)?.runner === "docker") {
7218
- 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)));
7219
8534
  return;
7220
8535
  }
7221
- if (sessionName) {
7222
- showLogs(sessionName, service, parseInt(opts.tail, 10));
7223
- } else {
7224
- const appRoot = findAppRoot2();
7225
- const projectName = getProjectName(appRoot);
7226
- showLogs(`${projectName}-dev`, service, parseInt(opts.tail, 10));
7227
- }
8536
+ showLogs(sessionName, service, parseInt(opts.tail, 10));
7228
8537
  });
7229
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(
7230
8539
  "--refresh-env",
7231
8540
  "Regenerate this service's env from current stack/SSM outputs before restarting (for config/deploy changes)"
7232
- ).option("--force", "Restart the worker even if workflow code changed since launch (may strand in-flight conversations)").action(async (service, opts) => {
7233
- let ctx = null;
7234
- try {
7235
- const parentSession = dev.opts().session;
7236
- const parentProfile = dev.opts().profile;
7237
- if (parentProfile) {
7238
- applyAwsProfileOverride(parentProfile);
7239
- }
7240
- const appRoot = findAppRoot2();
7241
- const projectName = getProjectName(appRoot);
7242
- const sessionName = parentSession ?? `${projectName}-dev`;
7243
- const state = loadSessionState(sessionName);
7244
- if (state?.runner === "docker") {
7245
- dockerDevRestart(sessionName, service);
7246
- logSuccess(`Restarted: ${service}`);
7247
- return;
7248
- }
7249
- if (/worker/i.test(service) && workflowChangeWouldStrand(state, appRoot, !!opts.force)) {
7250
- process.exit(1);
7251
- }
7252
- const stack = state?.stack ?? detectStack(appRoot, resolveStackOption(opts));
7253
- ctx = derivePlatformContext(appRoot, stack);
7254
- const defaultDeployerRole = readStackConfig(appRoot, stack, "mesh:deployerRole");
7255
- const adminDeployerRole = readStackConfig(appRoot, stack, "mesh:adminDeployerRole");
7256
- let deployerRole = defaultDeployerRole;
7257
- if (defaultDeployerRole) {
7258
- const meshCreds = ctx ? readCredentials(ctx) : null;
7259
- const idToken = meshCreds && new Date(meshCreds.expiresAt) > /* @__PURE__ */ new Date() ? meshCreds.idToken : null;
7260
- deployerRole = selectRoleForCaller(idToken, {
7261
- defaultRole: defaultDeployerRole,
7262
- 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
7263
8606
  });
7264
- }
7265
- const resolved = deployerRole ? await resolveAwsCredentials(deployerRole, appRoot, stack) : null;
7266
- if (deployerRole && !resolved && !process.env.AWS_ACCESS_KEY_ID) {
7267
- const pf = ctx ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx, readStackPlatform(appRoot, stack)) : null;
7268
- logError(
7269
- 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.`
7270
8618
  );
7271
8619
  process.exit(1);
7272
8620
  }
7273
- const baseAwsEnv = resolved?.env ?? getAwsEnvVars();
7274
- const awsRegion = readStackConfig(appRoot, stack, "aws:region");
7275
- if (awsRegion && !baseAwsEnv.AWS_REGION) {
7276
- baseAwsEnv.AWS_REGION = awsRegion;
7277
- }
7278
- Object.assign(process.env, baseAwsEnv);
7279
- const profileHint = parentProfile ?? process.env.AWS_PROFILE ?? "mesh-dev";
7280
- const zitadelOpts = resolved?.method === "zitadel" && deployerRole && ctx ? {
7281
- context: ctx,
7282
- roleArn: deployerRole,
7283
- sessionScratchDir: getSessionEnvDir(sessionName),
7284
- meshBin: resolveStableMeshBin(process.argv[1])
7285
- } : void 0;
7286
- const awsEnv = buildChildAwsEnv(baseAwsEnv, resolved?.method ?? "ambient", profileHint, zitadelOpts);
7287
- const devOutput = state?.devOutput ?? getDevOutput(appRoot, stack, baseAwsEnv);
7288
- await restartService(sessionName, service, appRoot, devOutput, awsEnv, {
7289
- refreshEnv: !!opts.refreshEnv,
7290
- stack,
7291
- sessionState: state
7292
- });
7293
- if (/worker/i.test(service) && state) {
7294
- saveSessionState(sessionName, {
7295
- ...state,
7296
- workflowFingerprint: fingerprintWorkflowSource(appRoot, state.devOutput?.services)
7297
- });
7298
- }
7299
- } catch (err) {
7300
- logError(`Failed to restart '${service}': ${err?.message ?? String(err)}`);
7301
- logInfo(
7302
- `If this is a credential/kubeconfig issue, run \`mesh login ${ctx ?? "<your platform context>"} --device\`, or \`mesh dev\` to relaunch the session cleanly.`
7303
- );
7304
- process.exit(1);
7305
8621
  }
7306
- });
8622
+ );
7307
8623
  dev.command("doctor").description(
7308
- "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"
7309
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) => {
7310
8626
  const parentProfile = dev.opts().profile;
7311
8627
  if (parentProfile) {
7312
8628
  applyAwsProfileOverride(parentProfile);
7313
8629
  }
7314
8630
  const appRoot = findAppRoot2();
7315
- const projectName = getProjectName(appRoot);
7316
- const sessionName = dev.opts().session ?? `${projectName}-dev`;
8631
+ const sessionName = sessionNameFor(appRoot, dev.opts().session);
7317
8632
  const state = loadSessionState(sessionName);
7318
8633
  const stack = state?.stack ?? detectStack(appRoot, resolveStackOption(opts));
7319
8634
  const asJson = !!opts.json || !!dev.opts().json;
@@ -7441,17 +8756,17 @@ function registerDevCommand(program) {
7441
8756
  // packages/mesh-cli/src/utils/tailscale-targets.ts
7442
8757
  var PORT_BLOCK = 3e3;
7443
8758
  function tenantPortIndex(tenant, registryPath) {
7444
- const p = registryPath ?? path16.join(CONFIG_DIR, "tailscale", "port-registry.json");
8759
+ const p = registryPath ?? path19.join(CONFIG_DIR, "tailscale", "port-registry.json");
7445
8760
  let reg;
7446
8761
  try {
7447
- reg = JSON.parse(fs15.readFileSync(p, "utf8"));
8762
+ reg = JSON.parse(fs18.readFileSync(p, "utf8"));
7448
8763
  } catch {
7449
8764
  reg = { mesh: 0 };
7450
8765
  }
7451
8766
  if (tenant in reg) return reg[tenant];
7452
8767
  const idx = Math.max(-1, ...Object.values(reg)) + 1;
7453
8768
  reg[tenant] = idx;
7454
- fs15.mkdirSync(path16.dirname(p), { recursive: true });
8769
+ fs18.mkdirSync(path19.dirname(p), { recursive: true });
7455
8770
  atomicWriteFileSync(p, JSON.stringify(reg, null, 2), 384);
7456
8771
  return idx;
7457
8772
  }
@@ -7479,7 +8794,7 @@ function resolveTunnelTargets(services, resolveIp, portOffset = 0) {
7479
8794
  }
7480
8795
  function resolveElbIp(host) {
7481
8796
  try {
7482
- const out = execFileSync11("dig", ["+short", host, "@1.1.1.1"], {
8797
+ const out = execFileSync12("dig", ["+short", host, "@1.1.1.1"], {
7483
8798
  encoding: "utf8",
7484
8799
  stdio: ["ignore", "pipe", "ignore"]
7485
8800
  });
@@ -7563,35 +8878,35 @@ function createForwarder(opts) {
7563
8878
 
7564
8879
  // packages/mesh-cli/src/utils/tunnel-ownership.ts
7565
8880
  import * as crypto2 from "node:crypto";
7566
- import * as fs16 from "node:fs";
8881
+ import * as fs19 from "node:fs";
7567
8882
  import * as net5 from "node:net";
7568
- import * as path17 from "node:path";
7569
- import { execFileSync as execFileSync12 } from "node:child_process";
8883
+ import * as path20 from "node:path";
8884
+ import { execFileSync as execFileSync13 } from "node:child_process";
7570
8885
  function mintRunnerToken() {
7571
8886
  return crypto2.randomBytes(16).toString("hex");
7572
8887
  }
7573
8888
  function readRunnerManifest(file) {
7574
- if (!fs16.existsSync(file)) return null;
8889
+ if (!fs19.existsSync(file)) return null;
7575
8890
  try {
7576
- return JSON.parse(fs16.readFileSync(file, "utf8"));
8891
+ return JSON.parse(fs19.readFileSync(file, "utf8"));
7577
8892
  } catch {
7578
8893
  return null;
7579
8894
  }
7580
8895
  }
7581
8896
  function writeRunnerManifest(file, manifest) {
7582
- fs16.mkdirSync(path17.dirname(file), { recursive: true });
7583
- fs16.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 });
7584
8899
  }
7585
8900
  function clearRunnerManifest(file, token) {
7586
8901
  if (token !== void 0 && readRunnerManifest(file)?.token !== token) return;
7587
8902
  try {
7588
- fs16.unlinkSync(file);
8903
+ fs19.unlinkSync(file);
7589
8904
  } catch {
7590
8905
  }
7591
8906
  }
7592
8907
  function getProcessGroupId(pid) {
7593
8908
  try {
7594
- const out = execFileSync12("ps", ["-o", "pgid=", "-p", String(pid)], {
8909
+ const out = execFileSync13("ps", ["-o", "pgid=", "-p", String(pid)], {
7595
8910
  encoding: "utf8",
7596
8911
  stdio: ["ignore", "pipe", "ignore"]
7597
8912
  }).trim();
@@ -7617,7 +8932,7 @@ function parseRunnerPidsFromPs(psOutput, tenant, exclude) {
7617
8932
  }
7618
8933
  function discoverRunnerPids(tenant, exclude) {
7619
8934
  try {
7620
- const out = execFileSync12("ps", ["ax", "-o", "pid=,command="], {
8935
+ const out = execFileSync13("ps", ["ax", "-o", "pid=,command="], {
7621
8936
  encoding: "utf8",
7622
8937
  stdio: ["ignore", "pipe", "ignore"]
7623
8938
  });
@@ -7709,7 +9024,7 @@ async function pollOwnership(check, deadlineMs) {
7709
9024
  }
7710
9025
  function describePortOwner(port) {
7711
9026
  try {
7712
- const out = execFileSync12("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fcp"], {
9027
+ const out = execFileSync13("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fcp"], {
7713
9028
  encoding: "utf8",
7714
9029
  stdio: ["ignore", "pipe", "ignore"]
7715
9030
  });
@@ -7751,58 +9066,58 @@ function buildSquatterError(tenant, squatted, detail) {
7751
9066
  // packages/mesh-cli/src/utils/tailscale.ts
7752
9067
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7753
9068
  function tenantStateDir(tenant) {
7754
- return path18.join(CONFIG_DIR, "tailscale", tenant);
9069
+ return path21.join(CONFIG_DIR, "tailscale", tenant);
7755
9070
  }
7756
9071
  function socketPath(tenant) {
7757
- return path18.join(tenantStateDir(tenant), "tailscaled.sock");
9072
+ return path21.join(tenantStateDir(tenant), "tailscaled.sock");
7758
9073
  }
7759
9074
  function logPath(tenant) {
7760
- return path18.join(tenantStateDir(tenant), "tailscaled.log");
9075
+ return path21.join(tenantStateDir(tenant), "tailscaled.log");
7761
9076
  }
7762
9077
  function statePath(tenant) {
7763
- return path18.join(tenantStateDir(tenant), "tunnel-state.json");
9078
+ return path21.join(tenantStateDir(tenant), "tunnel-state.json");
7764
9079
  }
7765
9080
  function daemonMetaPath(tenant) {
7766
- return path18.join(tenantStateDir(tenant), "daemon.json");
9081
+ return path21.join(tenantStateDir(tenant), "daemon.json");
7767
9082
  }
7768
9083
  function runnerManifestPath(tenant) {
7769
- return path18.join(tenantStateDir(tenant), "runner.json");
9084
+ return path21.join(tenantStateDir(tenant), "runner.json");
7770
9085
  }
7771
9086
  function readTunnelState(tenant) {
7772
9087
  const p = statePath(tenant);
7773
- if (!fs17.existsSync(p)) return null;
9088
+ if (!fs20.existsSync(p)) return null;
7774
9089
  try {
7775
- return JSON.parse(fs17.readFileSync(p, "utf8"));
9090
+ return JSON.parse(fs20.readFileSync(p, "utf8"));
7776
9091
  } catch {
7777
9092
  return null;
7778
9093
  }
7779
9094
  }
7780
9095
  function writeTunnelState(tenant, state) {
7781
- fs17.mkdirSync(tenantStateDir(tenant), { recursive: true });
9096
+ fs20.mkdirSync(tenantStateDir(tenant), { recursive: true });
7782
9097
  atomicWriteFileSync(statePath(tenant), JSON.stringify(state, null, 2), 384);
7783
9098
  }
7784
9099
  function clearTunnelState(tenant) {
7785
9100
  try {
7786
- fs17.unlinkSync(statePath(tenant));
9101
+ fs20.unlinkSync(statePath(tenant));
7787
9102
  } catch {
7788
9103
  }
7789
9104
  }
7790
9105
  function readDaemonMeta(tenant) {
7791
9106
  const p = daemonMetaPath(tenant);
7792
- if (!fs17.existsSync(p)) return null;
9107
+ if (!fs20.existsSync(p)) return null;
7793
9108
  try {
7794
- return JSON.parse(fs17.readFileSync(p, "utf8"));
9109
+ return JSON.parse(fs20.readFileSync(p, "utf8"));
7795
9110
  } catch {
7796
9111
  return null;
7797
9112
  }
7798
9113
  }
7799
9114
  function writeDaemonMeta(tenant, meta) {
7800
- fs17.mkdirSync(tenantStateDir(tenant), { recursive: true });
9115
+ fs20.mkdirSync(tenantStateDir(tenant), { recursive: true });
7801
9116
  atomicWriteFileSync(daemonMetaPath(tenant), JSON.stringify(meta, null, 2), 384);
7802
9117
  }
7803
9118
  function clearDaemonMeta(tenant) {
7804
9119
  try {
7805
- fs17.unlinkSync(daemonMetaPath(tenant));
9120
+ fs20.unlinkSync(daemonMetaPath(tenant));
7806
9121
  } catch {
7807
9122
  }
7808
9123
  }
@@ -7822,18 +9137,18 @@ function tailscaleBinPath() {
7822
9137
  }
7823
9138
  function resolveBrewBin(name) {
7824
9139
  const brew = `/opt/homebrew/bin/${name}`;
7825
- if (fs17.existsSync(brew)) return brew;
9140
+ if (fs20.existsSync(brew)) return brew;
7826
9141
  try {
7827
- 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();
7828
9143
  if (p) return p;
7829
9144
  } catch {
7830
9145
  }
7831
9146
  throw new Error(`${name} not found \u2014 install the standalone binary: brew install tailscale`);
7832
9147
  }
7833
9148
  function daemonState(tenant) {
7834
- if (!fs17.existsSync(socketPath(tenant))) return { backendState: "Down" };
9149
+ if (!fs20.existsSync(socketPath(tenant))) return { backendState: "Down" };
7835
9150
  try {
7836
- const out = execFileSync13(
9151
+ const out = execFileSync14(
7837
9152
  tailscaleBinPath(),
7838
9153
  ["--socket", socketPath(tenant), "status", "--json"],
7839
9154
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -7853,7 +9168,7 @@ function findRunningDaemon(tenant) {
7853
9168
  const sock = socketPath(tenant);
7854
9169
  let out;
7855
9170
  try {
7856
- out = execFileSync13("ps", ["ax", "-o", "pid=,command="], {
9171
+ out = execFileSync14("ps", ["ax", "-o", "pid=,command="], {
7857
9172
  encoding: "utf8",
7858
9173
  stdio: ["ignore", "pipe", "ignore"]
7859
9174
  });
@@ -7905,8 +9220,8 @@ async function ensureDaemon(tenant, opts) {
7905
9220
  }
7906
9221
  }
7907
9222
  const dir = tenantStateDir(tenant);
7908
- fs17.mkdirSync(dir, { recursive: true });
7909
- const log = fs17.openSync(logPath(tenant), "a");
9223
+ fs20.mkdirSync(dir, { recursive: true });
9224
+ const log = fs20.openSync(logPath(tenant), "a");
7910
9225
  try {
7911
9226
  const child = spawn2(
7912
9227
  tailscaledBinPath(),
@@ -7926,7 +9241,7 @@ async function ensureDaemon(tenant, opts) {
7926
9241
  child.unref();
7927
9242
  if (child.pid) writeDaemonMeta(tenant, { socksPort: opts.socksPort, pid: child.pid });
7928
9243
  } finally {
7929
- fs17.closeSync(log);
9244
+ fs20.closeSync(log);
7930
9245
  }
7931
9246
  for (let i = 0; i < 40; i++) {
7932
9247
  await sleep(250);
@@ -7952,7 +9267,7 @@ function stopDaemon(tenant) {
7952
9267
  }
7953
9268
  for (const p of [socketPath(tenant)]) {
7954
9269
  try {
7955
- fs17.unlinkSync(p);
9270
+ fs20.unlinkSync(p);
7956
9271
  } catch {
7957
9272
  }
7958
9273
  }
@@ -7974,7 +9289,7 @@ function buildTailscaleUpArgs(opts) {
7974
9289
  }
7975
9290
  async function joinHeadscale(tenant, loginServer, opts = {}) {
7976
9291
  if (daemonState(tenant).backendState === "Running") return;
7977
- const hostname2 = `mesh-${tenant}-${os4.hostname().split(".")[0]}`;
9292
+ const hostname2 = `mesh-${tenant}-${os3.hostname().split(".")[0]}`;
7978
9293
  const child = spawn2(
7979
9294
  tailscaleBinPath(),
7980
9295
  buildTailscaleUpArgs({ socket: socketPath(tenant), loginServer, hostname: hostname2, preAuthKey: opts.preAuthKey }),
@@ -8017,7 +9332,7 @@ async function joinHeadscale(tenant, loginServer, opts = {}) {
8017
9332
  }
8018
9333
  function logout(tenant) {
8019
9334
  try {
8020
- execFileSync13(tailscaleBinPath(), ["--socket", socketPath(tenant), "logout"], { stdio: "ignore" });
9335
+ execFileSync14(tailscaleBinPath(), ["--socket", socketPath(tenant), "logout"], { stdio: "ignore" });
8021
9336
  } catch {
8022
9337
  }
8023
9338
  }
@@ -8097,7 +9412,7 @@ async function assertTunnelPortsFree(tenant, ports) {
8097
9412
  async function ensureForwarders(state) {
8098
9413
  const prior = readTunnelState(state.tenant);
8099
9414
  const expected = { socksPort: state.socksPort, ports: state.tunnels.map((t) => t.localPort) };
8100
- const runnerAlive = prior?.managed === "launchd" ? fs17.existsSync(launchAgentPath(state.tenant)) : pidAlive(prior?.supervisorPid);
9415
+ const runnerAlive = prior?.managed === "launchd" ? fs20.existsSync(launchAgentPath(state.tenant)) : pidAlive(prior?.supervisorPid);
8101
9416
  if (prior && runnerAlive && tunnelTargetsEqual(prior.tunnels, state.tunnels) && // A prior supervisor spawned for a DIFFERENT SOCKS port still binds the same
8102
9417
  // local ports but dials a dead upstream — reusing it silently strands every
8103
9418
  // tunnel. The port must match, and the runner must PROVE it is
@@ -8122,7 +9437,7 @@ async function ensureForwarders(state) {
8122
9437
  if (launchdAvailable()) {
8123
9438
  state.managed = "launchd";
8124
9439
  writeTunnelState(state.tenant, state);
8125
- if (path18.isAbsolute(meshBin) && installLaunchAgent(state.tenant, meshBin)) {
9440
+ if (path21.isAbsolute(meshBin) && installLaunchAgent(state.tenant, meshBin)) {
8126
9441
  if ((await awaitOwnership(state.tenant, expected, 8e3)).ok) return 0;
8127
9442
  uninstallLaunchAgent(state.tenant);
8128
9443
  await assertTunnelPortsFree(state.tenant, expected.ports);
@@ -8157,8 +9472,12 @@ async function ensureForwarders(state) {
8157
9472
  }
8158
9473
  function rewriteTunnelsForTargets(devOutput, targets) {
8159
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
+ }
8160
9479
  for (const t of targets) {
8161
- tunnels[t.name] = { host: "localhost", port: t.localPort };
9480
+ tunnels[t.name] ??= { host: "localhost", port: t.localPort };
8162
9481
  }
8163
9482
  return { ...devOutput, tunnels };
8164
9483
  }
@@ -8250,7 +9569,7 @@ function forwardersNeedingRebind(targets, listening) {
8250
9569
  return targets.filter((t) => !listening.has(t.localPort));
8251
9570
  }
8252
9571
  function launchAgentPath(tenant) {
8253
- return path18.join(os4.homedir(), "Library", "LaunchAgents", `${launchAgentLabel(tenant)}.plist`);
9572
+ return path21.join(os3.homedir(), "Library", "LaunchAgents", `${launchAgentLabel(tenant)}.plist`);
8254
9573
  }
8255
9574
  function guiDomain() {
8256
9575
  return `gui/${process.getuid?.() ?? 0}`;
@@ -8259,8 +9578,8 @@ var _launchdAvailable;
8259
9578
  function launchdAvailable() {
8260
9579
  if (_launchdAvailable !== void 0) return _launchdAvailable;
8261
9580
  try {
8262
- execFileSync13("which", ["launchctl"], { stdio: "ignore" });
8263
- execFileSync13("launchctl", ["print", guiDomain()], { stdio: "ignore" });
9581
+ execFileSync14("which", ["launchctl"], { stdio: "ignore" });
9582
+ execFileSync14("launchctl", ["print", guiDomain()], { stdio: "ignore" });
8264
9583
  _launchdAvailable = true;
8265
9584
  } catch {
8266
9585
  _launchdAvailable = false;
@@ -8272,30 +9591,30 @@ function installLaunchAgent(tenant, meshBin) {
8272
9591
  try {
8273
9592
  let realMeshBin = meshBin;
8274
9593
  try {
8275
- realMeshBin = fs17.realpathSync(meshBin);
9594
+ realMeshBin = fs20.realpathSync(meshBin);
8276
9595
  } catch {
8277
9596
  }
8278
9597
  let pathEnv = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
8279
9598
  try {
8280
- pathEnv = `${path18.dirname(tailscaledBinPath())}:${pathEnv}`;
9599
+ pathEnv = `${path21.dirname(tailscaledBinPath())}:${pathEnv}`;
8281
9600
  } catch {
8282
9601
  }
8283
- fs17.mkdirSync(path18.dirname(plistPath), { recursive: true });
8284
- fs17.writeFileSync(
9602
+ fs20.mkdirSync(path21.dirname(plistPath), { recursive: true });
9603
+ fs20.writeFileSync(
8285
9604
  plistPath,
8286
9605
  renderLaunchAgentPlist({
8287
9606
  tenant,
8288
9607
  meshBin: realMeshBin,
8289
9608
  nodeBin: process.execPath,
8290
- logPath: path18.join(tenantStateDir(tenant), "runner.log"),
9609
+ logPath: path21.join(tenantStateDir(tenant), "runner.log"),
8291
9610
  pathEnv
8292
9611
  })
8293
9612
  );
8294
9613
  try {
8295
- execFileSync13("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
9614
+ execFileSync14("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
8296
9615
  } catch {
8297
9616
  }
8298
- execFileSync13("launchctl", ["bootstrap", guiDomain(), plistPath], { stdio: "ignore" });
9617
+ execFileSync14("launchctl", ["bootstrap", guiDomain(), plistPath], { stdio: "ignore" });
8299
9618
  return true;
8300
9619
  } catch {
8301
9620
  return false;
@@ -8304,11 +9623,11 @@ function installLaunchAgent(tenant, meshBin) {
8304
9623
  function uninstallLaunchAgent(tenant) {
8305
9624
  const plistPath = launchAgentPath(tenant);
8306
9625
  try {
8307
- execFileSync13("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
9626
+ execFileSync14("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
8308
9627
  } catch {
8309
9628
  }
8310
9629
  try {
8311
- fs17.unlinkSync(plistPath);
9630
+ fs20.unlinkSync(plistPath);
8312
9631
  } catch {
8313
9632
  }
8314
9633
  }
@@ -8537,7 +9856,7 @@ function headscaleExec(namespace, args, opts) {
8537
9856
  stdio: ["pipe", "pipe", "pipe"]
8538
9857
  };
8539
9858
  try {
8540
- const result = execFileSync14(
9859
+ const result = execFileSync15(
8541
9860
  "kubectl",
8542
9861
  [
8543
9862
  "exec",
@@ -8566,7 +9885,7 @@ function clusterName(tenant, env) {
8566
9885
  }
8567
9886
  function assertPodReady(namespace, options) {
8568
9887
  try {
8569
- const output = execFileSync14(
9888
+ const output = execFileSync15(
8570
9889
  "kubectl",
8571
9890
  [
8572
9891
  "get",
@@ -8601,7 +9920,7 @@ async function vpnStatus(options) {
8601
9920
  const ns = resolveNamespace(options);
8602
9921
  logInfo(`Checking Headscale in namespace ${ns}...`);
8603
9922
  assertPodReady(ns, options);
8604
- const podJson = execFileSync14(
9923
+ const podJson = execFileSync15(
8605
9924
  "kubectl",
8606
9925
  [
8607
9926
  "get",
@@ -8623,7 +9942,7 @@ async function vpnStatus(options) {
8623
9942
  const restarts = container?.restartCount ?? 0;
8624
9943
  let endpoint = "unknown";
8625
9944
  try {
8626
- endpoint = execFileSync14(
9945
+ endpoint = execFileSync15(
8627
9946
  "kubectl",
8628
9947
  [
8629
9948
  "get",
@@ -8778,7 +10097,7 @@ function findFreePort3() {
8778
10097
  }
8779
10098
  function findTailscale() {
8780
10099
  try {
8781
- return execFileSync14("which", ["tailscale"], {
10100
+ return execFileSync15("which", ["tailscale"], {
8782
10101
  encoding: "utf-8",
8783
10102
  stdio: ["pipe", "pipe", "pipe"]
8784
10103
  }).trim() || null;
@@ -8788,7 +10107,7 @@ function findTailscale() {
8788
10107
  }
8789
10108
  function tailscaleStatus() {
8790
10109
  try {
8791
- const json = execFileSync14("tailscale", ["status", "--json"], {
10110
+ const json = execFileSync15("tailscale", ["status", "--json"], {
8792
10111
  encoding: "utf-8",
8793
10112
  stdio: ["pipe", "pipe", "pipe"]
8794
10113
  });
@@ -8869,7 +10188,7 @@ async function vpnConnectSystem(endpoint) {
8869
10188
  "--accept-routes"
8870
10189
  ];
8871
10190
  try {
8872
- execFileSync14("tailscale", upArgs, { stdio: "inherit" });
10191
+ execFileSync15("tailscale", upArgs, { stdio: "inherit" });
8873
10192
  console.log("");
8874
10193
  logSuccess("VPN connected.");
8875
10194
  } catch (error) {
@@ -8896,7 +10215,7 @@ async function vpnDisconnect(opts) {
8896
10215
  }
8897
10216
  logInfo("Disconnecting system VPN\u2026");
8898
10217
  try {
8899
- execFileSync14("tailscale", ["down"], { stdio: "inherit" });
10218
+ execFileSync15("tailscale", ["down"], { stdio: "inherit" });
8900
10219
  logSuccess("VPN disconnected.");
8901
10220
  } catch (error) {
8902
10221
  const code = error.status;
@@ -8948,12 +10267,12 @@ function registerVpnCommands(program) {
8948
10267
  }
8949
10268
 
8950
10269
  // packages/mesh-cli/src/commands/login.ts
8951
- var CONFIG_DIR = path19.join(
8952
- process.env.XDG_CONFIG_HOME ?? path19.join(process.env.HOME ?? "~", ".config"),
10270
+ var CONFIG_DIR = path22.join(
10271
+ process.env.XDG_CONFIG_HOME ?? path22.join(process.env.HOME ?? "~", ".config"),
8953
10272
  "mesh"
8954
10273
  );
8955
- var CONFIG_FILE = path19.join(CONFIG_DIR, "config.json");
8956
- var CREDENTIALS_FILE = path19.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");
8957
10276
  var REDIRECT_PORT = 9876;
8958
10277
  var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
8959
10278
  var BASE_SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
@@ -8969,9 +10288,9 @@ function withDiscoveredFields(config, data) {
8969
10288
  return out;
8970
10289
  }
8971
10290
  function readConfig() {
8972
- if (!fs18.existsSync(CONFIG_FILE)) return {};
10291
+ if (!fs21.existsSync(CONFIG_FILE)) return {};
8973
10292
  try {
8974
- return JSON.parse(fs18.readFileSync(CONFIG_FILE, "utf-8"));
10293
+ return JSON.parse(fs21.readFileSync(CONFIG_FILE, "utf-8"));
8975
10294
  } catch {
8976
10295
  return {};
8977
10296
  }
@@ -8979,15 +10298,15 @@ function readConfig() {
8979
10298
  function writeContextConfig(context, config) {
8980
10299
  const existing = readConfig();
8981
10300
  existing[context] = config;
8982
- fs18.mkdirSync(CONFIG_DIR, { recursive: true });
8983
- fs18.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));
8984
10303
  }
8985
10304
  function clearContextConfig(context) {
8986
10305
  const existing = readConfig();
8987
10306
  if (!(context in existing)) return;
8988
10307
  delete existing[context];
8989
- fs18.mkdirSync(CONFIG_DIR, { recursive: true });
8990
- fs18.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));
8991
10310
  }
8992
10311
  function getContextConfig(context) {
8993
10312
  const config = readConfig();
@@ -9094,9 +10413,9 @@ function discoverConfigFromWellKnown(domain, contextKey = domain, opts = {}) {
9094
10413
  return discoverConfig(domain, contextKey, opts);
9095
10414
  }
9096
10415
  function readAllCredentials() {
9097
- if (!fs18.existsSync(CREDENTIALS_FILE)) return {};
10416
+ if (!fs21.existsSync(CREDENTIALS_FILE)) return {};
9098
10417
  try {
9099
- return JSON.parse(fs18.readFileSync(CREDENTIALS_FILE, "utf-8"));
10418
+ return JSON.parse(fs21.readFileSync(CREDENTIALS_FILE, "utf-8"));
9100
10419
  } catch {
9101
10420
  return {};
9102
10421
  }
@@ -9105,21 +10424,21 @@ function readCredentials(context) {
9105
10424
  return readAllCredentials()[context] ?? null;
9106
10425
  }
9107
10426
  var atomicWriteCounter = 0;
9108
- function atomicWriteFileSync(path20, data, mode) {
9109
- const tmpPath = `${path20}.${process.pid}.${atomicWriteCounter++}.tmp`;
10427
+ function atomicWriteFileSync(path23, data, mode) {
10428
+ const tmpPath = `${path23}.${process.pid}.${atomicWriteCounter++}.tmp`;
9110
10429
  try {
9111
- fs18.writeFileSync(tmpPath, data, { mode });
9112
- fs18.renameSync(tmpPath, path20);
10430
+ fs21.writeFileSync(tmpPath, data, { mode });
10431
+ fs21.renameSync(tmpPath, path23);
9113
10432
  } catch (err) {
9114
10433
  try {
9115
- fs18.unlinkSync(tmpPath);
10434
+ fs21.unlinkSync(tmpPath);
9116
10435
  } catch {
9117
10436
  }
9118
10437
  throw err;
9119
10438
  }
9120
10439
  }
9121
10440
  function writeCredentials(context, creds) {
9122
- fs18.mkdirSync(CONFIG_DIR, { recursive: true });
10441
+ fs21.mkdirSync(CONFIG_DIR, { recursive: true });
9123
10442
  const all = readAllCredentials();
9124
10443
  all[context] = creds;
9125
10444
  atomicWriteFileSync(CREDENTIALS_FILE, JSON.stringify(all, null, 2), 384);
@@ -9127,8 +10446,8 @@ function writeCredentials(context, creds) {
9127
10446
  function clearCredentials(context) {
9128
10447
  const all = readAllCredentials();
9129
10448
  delete all[context];
9130
- fs18.mkdirSync(CONFIG_DIR, { recursive: true });
9131
- fs18.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), {
9132
10451
  mode: 384
9133
10452
  });
9134
10453
  }
@@ -9277,9 +10596,9 @@ function login(context, config) {
9277
10596
  const url = authUrl.toString();
9278
10597
  try {
9279
10598
  if (process.platform === "darwin") {
9280
- execFileSync15("open", [url], { stdio: "ignore" });
10599
+ execFileSync16("open", [url], { stdio: "ignore" });
9281
10600
  } else if (process.platform === "linux") {
9282
- execFileSync15("xdg-open", [url], { stdio: "ignore" });
10601
+ execFileSync16("xdg-open", [url], { stdio: "ignore" });
9283
10602
  } else {
9284
10603
  logInfo(`Open this URL in your browser:
9285
10604
  ${url}`);
@@ -9333,9 +10652,9 @@ async function attemptDeviceCode(context, config) {
9333
10652
  logInfo("Waiting for authorization...");
9334
10653
  try {
9335
10654
  if (process.platform === "darwin") {
9336
- execFileSync15("open", [openUrl], { stdio: "ignore" });
10655
+ execFileSync16("open", [openUrl], { stdio: "ignore" });
9337
10656
  } else if (process.platform === "linux") {
9338
- execFileSync15("xdg-open", [openUrl], { stdio: "ignore" });
10657
+ execFileSync16("xdg-open", [openUrl], { stdio: "ignore" });
9339
10658
  }
9340
10659
  } catch {
9341
10660
  }
@@ -9398,7 +10717,7 @@ function loginTimeoutMessage(context) {
9398
10717
  }
9399
10718
  function isRemoteEnvironment() {
9400
10719
  if (process.env.REMOTE_CONTAINERS || process.env.CODESPACES) return true;
9401
- if (fs18.existsSync("/.dockerenv")) return true;
10720
+ if (fs21.existsSync("/.dockerenv")) return true;
9402
10721
  if (process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
9403
10722
  if (!process.stdout.isTTY || !process.stdin.isTTY) return true;
9404
10723
  return false;
@@ -9652,9 +10971,9 @@ function refuseReservedRegistryContext(context, verb) {
9652
10971
  }
9653
10972
  async function hintRegistryIfMissing() {
9654
10973
  try {
9655
- const { readRegistrySession } = await import("./mesh.registry-identity-JW4KO7N7.js");
10974
+ const { readRegistrySession } = await import("./mesh.registry-identity-3AR27UGO.js");
9656
10975
  if (readRegistrySession()) return;
9657
- const { probeRegistryToken: probeRegistryToken2 } = await import("./mesh.auth-preflight-AQJ5TQES.js");
10976
+ const { probeRegistryToken: probeRegistryToken2 } = await import("./mesh.auth-preflight-AUFQ2FGD.js");
9658
10977
  const probe = await probeRegistryToken2();
9659
10978
  if (probe.state === "missing" || probe.state === "expired") {
9660
10979
  logInfo(REGISTRY_SEPARATE_HINT);
@@ -9786,12 +11105,12 @@ export AWS_REGION=${shellSingleQuote(args.region)}
9786
11105
  `;
9787
11106
  }
9788
11107
  function resolveUserAwsConfigPath(env = process.env) {
9789
- return env.AWS_CONFIG_FILE ?? path19.join(env.HOME ?? "~", ".aws", "config");
11108
+ return env.AWS_CONFIG_FILE ?? path22.join(env.HOME ?? "~", ".aws", "config");
9790
11109
  }
9791
11110
  function canWriteDir(dir) {
9792
11111
  try {
9793
- fs18.mkdirSync(dir, { recursive: true });
9794
- fs18.accessSync(dir, fs18.constants.W_OK);
11112
+ fs21.mkdirSync(dir, { recursive: true });
11113
+ fs21.accessSync(dir, fs21.constants.W_OK);
9795
11114
  return true;
9796
11115
  } catch {
9797
11116
  return false;
@@ -9799,9 +11118,9 @@ function canWriteDir(dir) {
9799
11118
  }
9800
11119
  function resolveAwsConfigTarget(env = process.env) {
9801
11120
  if (env.AWS_CONFIG_FILE) return { configPath: env.AWS_CONFIG_FILE, redirected: false };
9802
- const real = path19.join(env.HOME ?? "~", ".aws", "config");
9803
- if (canWriteDir(path19.dirname(real))) return { configPath: real, redirected: false };
9804
- return { configPath: path19.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 };
9805
11124
  }
9806
11125
  function resolveRoleOrExplain(opts, config) {
9807
11126
  const role = opts.role ?? process.env.MESH_AWS_ROLE ?? config.defaultRole;
@@ -9933,10 +11252,10 @@ async function exportAwsCredentials(context, config, opts) {
9933
11252
  const sanitized = context.replace(/[^A-Za-z0-9_-]/g, "-");
9934
11253
  const profileName = `mesh-${sanitized}`;
9935
11254
  const { configPath, redirected } = resolveAwsConfigTarget();
9936
- fs18.mkdirSync(path19.dirname(configPath), { recursive: true });
9937
- const existedBefore = fs18.existsSync(configPath);
9938
- const existing = existedBefore ? fs18.readFileSync(configPath, "utf-8") : "";
9939
- const mode = existedBefore ? fs18.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;
9940
11259
  atomicWriteFileSync(
9941
11260
  configPath,
9942
11261
  upsertManagedAwsConfigSection(
@@ -10052,18 +11371,21 @@ export {
10052
11371
  adoptPublishedHubProject,
10053
11372
  readAllContextConfigs,
10054
11373
  discoverConfigForContext,
11374
+ derivePlatformRef,
10055
11375
  readStackPlatform,
10056
11376
  derivePlatformContext,
10057
11377
  projectRoleKeys,
10058
11378
  resolveAwsCredentials,
10059
11379
  credProbeToPreflightError,
10060
11380
  resolvePulumiEnv,
11381
+ devSessionsRoot,
10061
11382
  resolveHubPlatformName,
10062
11383
  ensureKubeconfig,
10063
11384
  resolveTemporalAuth,
10064
11385
  acquireTemporalBearer,
10065
11386
  findRepoRoot2 as findRepoRoot,
10066
11387
  writeEnvFile,
11388
+ pickLaunchHostEnv,
10067
11389
  buildLaunchCommand,
10068
11390
  waitForPort,
10069
11391
  MOCK_SAML_PORT,
@@ -10077,6 +11399,7 @@ export {
10077
11399
  seedMockIdps,
10078
11400
  reconcileRegistryFromZitadel,
10079
11401
  detectLocalTenant,
11402
+ removeLocalAppSpiceDBs,
10080
11403
  removeAllAliasContainers,
10081
11404
  resolveStackOption,
10082
11405
  worktreeStackName,