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