@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.
- package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
- package/dist/bin/{mesh.chunk-I6NXLXGC.js → mesh.chunk-2ASXJ5DO.js} +235 -581
- package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
- package/dist/bin/{mesh.chunk-IU2IWH6O.js → mesh.chunk-6AIG5OJM.js} +3 -3
- package/dist/bin/{mesh.chunk-KN4BZOYH.js → mesh.chunk-6SSOVZOI.js} +1 -1
- package/dist/bin/{mesh.chunk-LMYPTJ2F.js → mesh.chunk-7HTKSUWR.js} +1900 -577
- package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
- package/dist/bin/{mesh.chunk-2YRWJH5X.js → mesh.chunk-F44A3ACW.js} +4 -4
- package/dist/bin/{mesh.chunk-2YRWJH5X.js.map → mesh.chunk-F44A3ACW.js.map} +1 -1
- package/dist/bin/{mesh.chunk-BF2SH6XT.js → mesh.chunk-HJGMOEUG.js} +1 -1
- package/dist/bin/{mesh.chunk-TZAGJTKU.js → mesh.chunk-NSQJEX36.js} +1 -1
- package/dist/bin/{mesh.chunk-HPYZWPRM.js → mesh.chunk-PZVI7V3J.js} +1 -1
- package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
- package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
- package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
- package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
- package/dist/bin/{mesh.create-app-NGXACFAU.js → mesh.create-app-Z3RKSTBO.js} +3 -3
- package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
- package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
- package/dist/bin/mesh.js +9 -8
- package/dist/bin/mesh.js.map +1 -1
- package/dist/bin/{mesh.login-7SIAKFXJ.js → mesh.login-3AWVGPCM.js} +2 -2
- package/dist/bin/{mesh.platform-setup-CJXE3BCP.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
- package/dist/bin/{mesh.program-BIDQBSSI.js → mesh.program-K5JQXEHX.js} +9 -8
- package/dist/bin/{mesh.registry-7P4MGXEK.js → mesh.registry-JLQYFUNN.js} +4 -4
- package/dist/bin/{mesh.registry-identity-JW4KO7N7.js → mesh.registry-identity-3AR27UGO.js} +3 -3
- package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
- package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
- package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
- package/dist/bin/{mesh.start-MPQFIEMH.js → mesh.start-KZ5L6SOJ.js} +2 -2
- package/dist/bin/{mesh.wizard-ZJE4S6PC.js → mesh.wizard-FCTT55KI.js} +7 -7
- package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
- package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.js +2 -2
- package/dist/src/commands/dev-doctor.d.ts +22 -0
- package/dist/src/commands/dev-doctor.js +149 -0
- package/dist/src/commands/dev-env-leaks.d.ts +24 -0
- package/dist/src/commands/dev-env-leaks.js +178 -0
- package/dist/src/commands/dev-launch.d.ts +17 -1
- package/dist/src/commands/dev-launch.js +130 -5
- package/dist/src/commands/dev-readiness.d.ts +49 -0
- package/dist/src/commands/dev-readiness.js +78 -0
- package/dist/src/commands/dev.d.ts +60 -0
- package/dist/src/commands/dev.js +574 -140
- package/dist/src/commands/hub/index.js +10 -7
- package/dist/src/commands/local/app-authz.d.ts +118 -0
- package/dist/src/commands/local/app-authz.js +295 -0
- package/dist/src/commands/local/auth-provision.d.ts +1 -0
- package/dist/src/commands/local/auth-provision.js +12 -1
- package/dist/src/commands/local/index.js +2 -0
- package/dist/src/commands/mcp/generate.d.ts +11 -0
- package/dist/src/commands/mcp/generate.js +78 -0
- package/dist/src/commands/mcp/index.d.ts +2 -0
- package/dist/src/commands/mcp/index.js +70 -0
- package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
- package/dist/src/commands/mcp/manifest-file.js +24 -0
- package/dist/src/commands/mcp/review-command.d.ts +24 -0
- package/dist/src/commands/mcp/review-command.js +66 -0
- package/dist/src/commands/mcp/review.d.ts +41 -0
- package/dist/src/commands/mcp/review.js +140 -0
- package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
- package/dist/src/commands/mcp/workspace-membership.js +77 -0
- package/dist/src/commands/secrets/reindex.js +2 -7
- package/dist/src/commands/secrets/set.d.ts +22 -1
- package/dist/src/commands/secrets/set.js +31 -8
- package/dist/src/commands/temporal.js +51 -5
- package/dist/src/commands/temporal.js.map +2 -2
- package/dist/src/program.js +2 -0
- package/dist/src/utils/aws-auth.d.ts +4 -0
- package/dist/src/utils/aws-auth.js +7 -3
- package/dist/src/utils/dev-sessions-root.d.ts +3 -0
- package/dist/src/utils/dev-sessions-root.js +45 -0
- package/dist/src/utils/kubeconfig.js +2 -2
- package/dist/src/utils/tailscale.js +7 -1
- package/dist/src/utils/temporal-auth.js +2 -1
- package/dist/src/utils/workflow-fingerprint.js +23 -12
- package/fragments/base/index.ts.hbs +4 -1
- package/package.json +7 -5
- package/skills/mesh/SKILL.md +10 -6
- package/templates/api-auth/api/src/schema.ts.hbs +17 -0
- package/templates/api-auth/index.ts.hbs +1 -0
- package/templates/api-auth/package.json.hbs +3 -0
- package/templates/api-role-gating/api/src/schema.ts.hbs +10 -0
- package/templates/api-role-gating/index.ts.hbs +1 -0
- package/templates/external-service/index.ts.hbs +1 -0
- package/templates/workflow/index.ts.hbs +2 -0
- package/dist/bin/mesh.chunk-I6NXLXGC.js.map +0 -6
- package/dist/bin/mesh.chunk-LMYPTJ2F.js.map +0 -6
- /package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-IU2IWH6O.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-KN4BZOYH.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-BF2SH6XT.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-TZAGJTKU.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-HPYZWPRM.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
- /package/dist/bin/{mesh.create-app-NGXACFAU.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
- /package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
- /package/dist/bin/{mesh.login-7SIAKFXJ.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
- /package/dist/bin/{mesh.platform-setup-CJXE3BCP.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
- /package/dist/bin/{mesh.program-BIDQBSSI.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
- /package/dist/bin/{mesh.registry-7P4MGXEK.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
- /package/dist/bin/{mesh.registry-identity-JW4KO7N7.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
- /package/dist/bin/{mesh.start-MPQFIEMH.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
- /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-
|
|
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
|
|
57
|
-
import * as
|
|
58
|
-
import { execFileSync as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1201
|
+
import * as fs20 from "node:fs";
|
|
1197
1202
|
import * as net6 from "node:net";
|
|
1198
|
-
import * as
|
|
1199
|
-
import * as
|
|
1200
|
-
import { spawn as spawn2, execFileSync as
|
|
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
|
|
1239
|
-
import * as
|
|
1240
|
-
import * as
|
|
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
|
|
1244
|
-
import
|
|
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
|
|
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
|
|
1363
|
-
import * as
|
|
1364
|
-
|
|
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
|
|
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 (!
|
|
1429
|
-
|
|
1472
|
+
if (!fs6.existsSync(path6.dirname(kubeconfigPath))) {
|
|
1473
|
+
fs6.mkdirSync(path6.dirname(kubeconfigPath), { recursive: true });
|
|
1430
1474
|
}
|
|
1431
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1724
|
+
import * as fs7 from "fs";
|
|
1672
1725
|
import * as net from "net";
|
|
1673
|
-
import * as
|
|
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
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
|
1733
|
-
import * as
|
|
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
|
|
1738
|
-
import * as
|
|
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 =
|
|
1871
|
-
if (!
|
|
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
|
|
2205
|
+
var SECRET_KEY_RE2 = /key|secret|password|token/i;
|
|
2010
2206
|
function readLocalMocks(appRoot) {
|
|
2011
2207
|
try {
|
|
2012
|
-
const pkg = JSON.parse(
|
|
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
|
|
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
|
-
|
|
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]) => !
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2283
|
-
import * as
|
|
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 = () =>
|
|
2512
|
+
var keyDir = () => path9.join(meshCacheDir("mesh-local"), "mock-idp");
|
|
2317
2513
|
function ensureMockSamlSigningKey() {
|
|
2318
2514
|
const dir = keyDir();
|
|
2319
|
-
const certPath =
|
|
2320
|
-
const keyPath =
|
|
2321
|
-
if (!
|
|
2322
|
-
|
|
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:
|
|
2354
|
-
privateKeyB64:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2763
|
+
return path10.join(meshCacheDir("mesh-local"), "hub-auth.json");
|
|
2568
2764
|
}
|
|
2569
2765
|
function readHubAuth() {
|
|
2570
2766
|
try {
|
|
2571
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
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
|
|
3529
|
+
return path11.join(findPackageRoot(), "assets", "log-shipper.mjs");
|
|
3326
3530
|
}
|
|
3327
3531
|
function autoInstrumentationEnv(appRoot, src) {
|
|
3328
|
-
const pkgDir =
|
|
3329
|
-
const present =
|
|
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 (
|
|
3386
|
-
const pyproject =
|
|
3387
|
-
if (!
|
|
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(
|
|
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 (
|
|
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 =
|
|
3402
|
-
if (!
|
|
3605
|
+
const pkgPath = path11.join(dir, "package.json");
|
|
3606
|
+
if (!fs11.existsSync(pkgPath)) return false;
|
|
3403
3607
|
try {
|
|
3404
|
-
const pkg = JSON.parse(
|
|
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
|
|
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(
|
|
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[
|
|
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 =
|
|
3637
|
+
let appName = path11.basename(appRoot);
|
|
3434
3638
|
try {
|
|
3435
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
3440
|
-
if (
|
|
3441
|
-
const match =
|
|
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:
|
|
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 =
|
|
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 =
|
|
3729
|
+
let dir = path11.dirname(appRoot);
|
|
3526
3730
|
while (true) {
|
|
3527
|
-
if (
|
|
3528
|
-
if (
|
|
3529
|
-
return
|
|
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 =
|
|
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
|
|
3741
|
+
for (const file of fs11.readdirSync(appRoot)) {
|
|
3538
3742
|
if (!/^Pulumi\..*\.yaml$/.test(file)) continue;
|
|
3539
|
-
const match =
|
|
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/
|
|
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
|
|
3559
|
-
import
|
|
3560
|
-
import
|
|
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(
|
|
3713
|
-
const res = await fetch(`${HOST_HTTP}${
|
|
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 ${
|
|
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:
|
|
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 =
|
|
4413
|
+
const file = path13.resolve(appRoot, modulePath);
|
|
3887
4414
|
let mod;
|
|
3888
4415
|
if (/\.m?js$/.test(file)) {
|
|
3889
|
-
mod = await import(
|
|
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(
|
|
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 =
|
|
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:
|
|
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
|
|
4237
|
-
import * as
|
|
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
|
-
|
|
4261
|
-
const workdir =
|
|
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
|
-
` - '${
|
|
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 =
|
|
4287
|
-
|
|
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",
|
|
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
|
|
4365
|
-
import * as
|
|
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
|
|
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 =
|
|
4536
|
-
const root =
|
|
4537
|
-
return src === root || src.startsWith(root +
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
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
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
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 =
|
|
4694
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
|
5777
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4890
5778
|
import * as crypto from "node:crypto";
|
|
4891
|
-
import * as
|
|
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}#${
|
|
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) =>
|
|
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 =
|
|
5818
|
+
worktreeRoot = path17.resolve(gitRunner(["rev-parse", "--show-toplevel"], cwd));
|
|
4931
5819
|
const commonDir = gitRunner(["rev-parse", "--git-common-dir"], cwd);
|
|
4932
|
-
const primaryRoot =
|
|
5820
|
+
const primaryRoot = path17.resolve(path17.dirname(path17.resolve(cwd, commonDir)));
|
|
4933
5821
|
isPrimary = worktreeRoot === primaryRoot;
|
|
4934
5822
|
} catch {
|
|
4935
5823
|
return {
|
|
4936
|
-
worktreeRoot:
|
|
5824
|
+
worktreeRoot: path17.resolve(cwd),
|
|
4937
5825
|
isPrimary: true,
|
|
4938
|
-
slug: sanitizeSlug(
|
|
4939
|
-
hash: worktreeHash(
|
|
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
|
|
5833
|
+
const slug2 = sanitizeSlug(path17.basename(worktreeRoot));
|
|
4946
5834
|
const hash = worktreeHash(worktreeRoot);
|
|
4947
|
-
const token = isPrimary ? "" : `${
|
|
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(
|
|
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 =
|
|
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 =
|
|
5017
|
-
|
|
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
|
-
|
|
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 (!
|
|
5915
|
+
if (!fs17.existsSync(filePath)) return null;
|
|
5026
5916
|
try {
|
|
5027
|
-
return JSON.parse(
|
|
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
|
|
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
|
|
5941
|
+
return path18.join(getSessionEnvDir(sessionName), envFileName(serviceName));
|
|
5055
5942
|
}
|
|
5056
5943
|
function removeSessionState(sessionName) {
|
|
5057
5944
|
try {
|
|
5058
|
-
|
|
5945
|
+
fs17.unlinkSync(getSessionStatePath(sessionName));
|
|
5059
5946
|
} catch {
|
|
5060
5947
|
}
|
|
5061
5948
|
try {
|
|
5062
|
-
|
|
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 =
|
|
6130
|
+
const root = path18.parse(dir).root;
|
|
5191
6131
|
while (dir !== root) {
|
|
5192
|
-
if (
|
|
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 =
|
|
5196
|
-
if (
|
|
6135
|
+
const gitPath = path18.join(dir, ".git");
|
|
6136
|
+
if (fs17.existsSync(gitPath)) {
|
|
5197
6137
|
return dir;
|
|
5198
6138
|
}
|
|
5199
|
-
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 =
|
|
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:
|
|
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 =
|
|
5219
|
-
if (!
|
|
5220
|
-
for (const tenant of
|
|
5221
|
-
const appsDir =
|
|
5222
|
-
if (!
|
|
5223
|
-
for (const app of
|
|
5224
|
-
const appDir =
|
|
5225
|
-
if (!
|
|
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 =
|
|
5231
|
-
if (
|
|
5232
|
-
for (const app of
|
|
5233
|
-
const appDir =
|
|
5234
|
-
if (!
|
|
5235
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5254
|
-
if (
|
|
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 =
|
|
5258
|
-
if (
|
|
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 =
|
|
6204
|
+
const root = path18.parse(dir).root;
|
|
5265
6205
|
while (dir !== root) {
|
|
5266
|
-
if (
|
|
6206
|
+
if (fs17.existsSync(path18.join(dir, "Pulumi.yaml"))) {
|
|
5267
6207
|
return dir;
|
|
5268
6208
|
}
|
|
5269
|
-
dir =
|
|
6209
|
+
dir = path18.dirname(dir);
|
|
5270
6210
|
}
|
|
5271
6211
|
return process.cwd();
|
|
5272
6212
|
}
|
|
5273
6213
|
function getProjectName(appRoot) {
|
|
5274
|
-
const yamlPath =
|
|
5275
|
-
if (!
|
|
5276
|
-
const content =
|
|
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() ??
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
6430
|
+
const configPath = path18.join(opts.sessionScratchDir, "aws-config");
|
|
5415
6431
|
const region = awsEnv.AWS_REGION ?? awsEnv.AWS_DEFAULT_REGION ?? "";
|
|
5416
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5483
|
-
return
|
|
6498
|
+
const rebased = path18.join(monorepoRoot, rel);
|
|
6499
|
+
return fs17.existsSync(rebased) ? rebased : src;
|
|
5484
6500
|
}
|
|
5485
6501
|
function isLinkedDependencyDir(serviceDir, appRoot) {
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
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(
|
|
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
|
-
|
|
5499
|
-
if (parent === dir) break;
|
|
6512
|
+
if (path18.dirname(at) === at) return null;
|
|
5500
6513
|
}
|
|
5501
|
-
|
|
5502
|
-
|
|
6514
|
+
}
|
|
6515
|
+
function appResolvedPackageDir(name, appRoot) {
|
|
6516
|
+
for (let dir = appRoot; ; dir = path18.dirname(dir)) {
|
|
5503
6517
|
try {
|
|
5504
|
-
|
|
6518
|
+
return fs17.realpathSync(path18.join(dir, "node_modules", name));
|
|
5505
6519
|
} catch {
|
|
5506
6520
|
}
|
|
5507
|
-
|
|
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 (!
|
|
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
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5585
|
-
const
|
|
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 =
|
|
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 (
|
|
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
|
|
5632
|
-
if (
|
|
5633
|
-
const encodingKey = resolveTemporalEncodingKey(
|
|
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
|
-
|
|
6870
|
+
execFileSync11("which", ["session-manager-plugin"], { stdio: "ignore" });
|
|
5796
6871
|
return true;
|
|
5797
6872
|
} catch {
|
|
5798
6873
|
return false;
|
|
5799
6874
|
}
|
|
5800
6875
|
}
|
|
5801
|
-
|
|
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
|
|
5838
|
-
const
|
|
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
|
-
|
|
5895
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
7038
|
+
execFileSync11(
|
|
5945
7039
|
"docker",
|
|
5946
7040
|
[
|
|
5947
7041
|
"run",
|
|
@@ -5957,7 +7051,7 @@ CADDYEOF`
|
|
|
5957
7051
|
],
|
|
5958
7052
|
{ stdio: "pipe" }
|
|
5959
7053
|
);
|
|
5960
|
-
|
|
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 ??
|
|
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 =
|
|
6010
|
-
if (!
|
|
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
|
-
|
|
7122
|
+
fs17.writeFileSync(caddyfile, caddyLines.join("\n"));
|
|
6029
7123
|
let caddyRunning = false;
|
|
6030
7124
|
try {
|
|
6031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6089
|
-
|
|
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
|
-
|
|
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
|
|
6180
|
-
if (
|
|
6181
|
-
const encodingKey = resolveTemporalEncodingKey(
|
|
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
|
-
|
|
6190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6229
|
-
if (worktreeRoot &&
|
|
6230
|
-
const wtRootWithSep = worktreeRoot.endsWith(
|
|
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
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
6282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7477
|
+
spawnSync2("tmux", ["switch-client", "-t", sessionName], { stdio: "inherit" });
|
|
6332
7478
|
} else {
|
|
6333
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6478
|
-
|
|
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 (
|
|
6485
|
-
|
|
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
|
-
|
|
6501
|
-
|
|
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 =
|
|
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(
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
8127
|
+
const serviceDir = path18.resolve(appRoot, service.src);
|
|
6819
8128
|
const env = service.env ?? (service.env = {});
|
|
6820
8129
|
const serviceEnv = {
|
|
6821
|
-
...readDotEnvFile(
|
|
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) =>
|
|
8322
|
+
(dir) => fs17.existsSync(path18.join(appRoot, dir, "package.json"))
|
|
7011
8323
|
);
|
|
7012
|
-
if (monorepoRoot &&
|
|
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
|
-
|
|
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
|
-
|
|
7217
|
-
|
|
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
|
-
|
|
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)").
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
const
|
|
7259
|
-
const
|
|
7260
|
-
deployerRole =
|
|
7261
|
-
|
|
7262
|
-
|
|
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
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
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,
|
|
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
|
|
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 ??
|
|
8759
|
+
const p = registryPath ?? path19.join(CONFIG_DIR, "tailscale", "port-registry.json");
|
|
7445
8760
|
let reg;
|
|
7446
8761
|
try {
|
|
7447
|
-
reg = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
8881
|
+
import * as fs19 from "node:fs";
|
|
7567
8882
|
import * as net5 from "node:net";
|
|
7568
|
-
import * as
|
|
7569
|
-
import { execFileSync as
|
|
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 (!
|
|
8889
|
+
if (!fs19.existsSync(file)) return null;
|
|
7575
8890
|
try {
|
|
7576
|
-
return JSON.parse(
|
|
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
|
-
|
|
7583
|
-
|
|
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
|
-
|
|
8903
|
+
fs19.unlinkSync(file);
|
|
7589
8904
|
} catch {
|
|
7590
8905
|
}
|
|
7591
8906
|
}
|
|
7592
8907
|
function getProcessGroupId(pid) {
|
|
7593
8908
|
try {
|
|
7594
|
-
const out =
|
|
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 =
|
|
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 =
|
|
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
|
|
9069
|
+
return path21.join(CONFIG_DIR, "tailscale", tenant);
|
|
7755
9070
|
}
|
|
7756
9071
|
function socketPath(tenant) {
|
|
7757
|
-
return
|
|
9072
|
+
return path21.join(tenantStateDir(tenant), "tailscaled.sock");
|
|
7758
9073
|
}
|
|
7759
9074
|
function logPath(tenant) {
|
|
7760
|
-
return
|
|
9075
|
+
return path21.join(tenantStateDir(tenant), "tailscaled.log");
|
|
7761
9076
|
}
|
|
7762
9077
|
function statePath(tenant) {
|
|
7763
|
-
return
|
|
9078
|
+
return path21.join(tenantStateDir(tenant), "tunnel-state.json");
|
|
7764
9079
|
}
|
|
7765
9080
|
function daemonMetaPath(tenant) {
|
|
7766
|
-
return
|
|
9081
|
+
return path21.join(tenantStateDir(tenant), "daemon.json");
|
|
7767
9082
|
}
|
|
7768
9083
|
function runnerManifestPath(tenant) {
|
|
7769
|
-
return
|
|
9084
|
+
return path21.join(tenantStateDir(tenant), "runner.json");
|
|
7770
9085
|
}
|
|
7771
9086
|
function readTunnelState(tenant) {
|
|
7772
9087
|
const p = statePath(tenant);
|
|
7773
|
-
if (!
|
|
9088
|
+
if (!fs20.existsSync(p)) return null;
|
|
7774
9089
|
try {
|
|
7775
|
-
return JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
9107
|
+
if (!fs20.existsSync(p)) return null;
|
|
7793
9108
|
try {
|
|
7794
|
-
return JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
9140
|
+
if (fs20.existsSync(brew)) return brew;
|
|
7826
9141
|
try {
|
|
7827
|
-
const p =
|
|
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 (!
|
|
9149
|
+
if (!fs20.existsSync(socketPath(tenant))) return { backendState: "Down" };
|
|
7835
9150
|
try {
|
|
7836
|
-
const out =
|
|
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 =
|
|
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
|
-
|
|
7909
|
-
const log =
|
|
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
|
-
|
|
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
|
-
|
|
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}-${
|
|
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
|
-
|
|
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" ?
|
|
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 (
|
|
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]
|
|
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
|
|
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
|
-
|
|
8263
|
-
|
|
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 =
|
|
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 = `${
|
|
9599
|
+
pathEnv = `${path21.dirname(tailscaledBinPath())}:${pathEnv}`;
|
|
8281
9600
|
} catch {
|
|
8282
9601
|
}
|
|
8283
|
-
|
|
8284
|
-
|
|
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:
|
|
9609
|
+
logPath: path21.join(tenantStateDir(tenant), "runner.log"),
|
|
8291
9610
|
pathEnv
|
|
8292
9611
|
})
|
|
8293
9612
|
);
|
|
8294
9613
|
try {
|
|
8295
|
-
|
|
9614
|
+
execFileSync14("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
|
|
8296
9615
|
} catch {
|
|
8297
9616
|
}
|
|
8298
|
-
|
|
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
|
-
|
|
9626
|
+
execFileSync14("launchctl", ["bootout", guiDomain(), plistPath], { stdio: "ignore" });
|
|
8308
9627
|
} catch {
|
|
8309
9628
|
}
|
|
8310
9629
|
try {
|
|
8311
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
8952
|
-
process.env.XDG_CONFIG_HOME ??
|
|
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 =
|
|
8956
|
-
var CREDENTIALS_FILE =
|
|
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 (!
|
|
10291
|
+
if (!fs21.existsSync(CONFIG_FILE)) return {};
|
|
8973
10292
|
try {
|
|
8974
|
-
return JSON.parse(
|
|
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
|
-
|
|
8983
|
-
|
|
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
|
-
|
|
8990
|
-
|
|
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 (!
|
|
10416
|
+
if (!fs21.existsSync(CREDENTIALS_FILE)) return {};
|
|
9098
10417
|
try {
|
|
9099
|
-
return JSON.parse(
|
|
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(
|
|
9109
|
-
const tmpPath = `${
|
|
10427
|
+
function atomicWriteFileSync(path23, data, mode) {
|
|
10428
|
+
const tmpPath = `${path23}.${process.pid}.${atomicWriteCounter++}.tmp`;
|
|
9110
10429
|
try {
|
|
9111
|
-
|
|
9112
|
-
|
|
10430
|
+
fs21.writeFileSync(tmpPath, data, { mode });
|
|
10431
|
+
fs21.renameSync(tmpPath, path23);
|
|
9113
10432
|
} catch (err) {
|
|
9114
10433
|
try {
|
|
9115
|
-
|
|
10434
|
+
fs21.unlinkSync(tmpPath);
|
|
9116
10435
|
} catch {
|
|
9117
10436
|
}
|
|
9118
10437
|
throw err;
|
|
9119
10438
|
}
|
|
9120
10439
|
}
|
|
9121
10440
|
function writeCredentials(context, creds) {
|
|
9122
|
-
|
|
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
|
-
|
|
9131
|
-
|
|
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
|
-
|
|
10599
|
+
execFileSync16("open", [url], { stdio: "ignore" });
|
|
9281
10600
|
} else if (process.platform === "linux") {
|
|
9282
|
-
|
|
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
|
-
|
|
10655
|
+
execFileSync16("open", [openUrl], { stdio: "ignore" });
|
|
9337
10656
|
} else if (process.platform === "linux") {
|
|
9338
|
-
|
|
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 (
|
|
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-
|
|
10974
|
+
const { readRegistrySession } = await import("./mesh.registry-identity-3AR27UGO.js");
|
|
9656
10975
|
if (readRegistrySession()) return;
|
|
9657
|
-
const { probeRegistryToken: probeRegistryToken2 } = await import("./mesh.auth-preflight-
|
|
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 ??
|
|
11108
|
+
return env.AWS_CONFIG_FILE ?? path22.join(env.HOME ?? "~", ".aws", "config");
|
|
9790
11109
|
}
|
|
9791
11110
|
function canWriteDir(dir) {
|
|
9792
11111
|
try {
|
|
9793
|
-
|
|
9794
|
-
|
|
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 =
|
|
9803
|
-
if (canWriteDir(
|
|
9804
|
-
return { configPath:
|
|
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
|
-
|
|
9937
|
-
const existedBefore =
|
|
9938
|
-
const existing = existedBefore ?
|
|
9939
|
-
const mode = existedBefore ?
|
|
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,
|