@mesh-tech/mesh-cli 0.39.0 → 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/agents/AGENTS.md.hbs +3 -1
- 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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { envFileKeys, envFileName, getSessionEnvDir, isAllowedLaunchHostVar } from "./dev-launch.js";
|
|
5
|
+
export const LEAK_NAME_PATTERNS = [
|
|
6
|
+
/^WORKSPACES_/,
|
|
7
|
+
/^AWS_/,
|
|
8
|
+
/^TEMPORAL_/,
|
|
9
|
+
/^VCS_/,
|
|
10
|
+
/_TOKEN$/,
|
|
11
|
+
/_SECRET$/,
|
|
12
|
+
/_KEY$/,
|
|
13
|
+
/_PASSWORD$/,
|
|
14
|
+
];
|
|
15
|
+
export function matchesLeakPattern(name) {
|
|
16
|
+
return LEAK_NAME_PATTERNS.some((re) => re.test(name));
|
|
17
|
+
}
|
|
18
|
+
export function parseTmuxEnvironment(text) {
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const line of text.split("\n")) {
|
|
21
|
+
if (!line || line.startsWith("-"))
|
|
22
|
+
continue;
|
|
23
|
+
const eq = line.indexOf("=");
|
|
24
|
+
if (eq <= 0)
|
|
25
|
+
continue;
|
|
26
|
+
const name = line.slice(0, eq);
|
|
27
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
28
|
+
continue;
|
|
29
|
+
out[name] = line.slice(eq + 1);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function classifyLeak(name, envFileKeysByService, cleanLaunch) {
|
|
34
|
+
const services = Object.keys(envFileKeysByService).sort();
|
|
35
|
+
const shadowedIn = services.filter((svc) => envFileKeysByService[svc].includes(name));
|
|
36
|
+
const unshadowedIn = services.filter((svc) => !shadowedIn.includes(svc));
|
|
37
|
+
if (cleanLaunch === false) {
|
|
38
|
+
if (services.length > 0 && unshadowedIn.length === 0)
|
|
39
|
+
return { fate: "shadowed", shadowedIn, unshadowedIn };
|
|
40
|
+
return { fate: "reaches", shadowedIn, unshadowedIn };
|
|
41
|
+
}
|
|
42
|
+
if (isAllowedLaunchHostVar(name))
|
|
43
|
+
return { fate: "reaches", shadowedIn, unshadowedIn };
|
|
44
|
+
if (services.length === 0)
|
|
45
|
+
return { fate: "dropped", shadowedIn, unshadowedIn };
|
|
46
|
+
if (unshadowedIn.length === 0)
|
|
47
|
+
return { fate: "shadowed", shadowedIn, unshadowedIn };
|
|
48
|
+
if (shadowedIn.length === 0)
|
|
49
|
+
return { fate: "dropped", shadowedIn, unshadowedIn };
|
|
50
|
+
return { fate: "partial", shadowedIn, unshadowedIn };
|
|
51
|
+
}
|
|
52
|
+
export function collectLeakCandidates(input) {
|
|
53
|
+
const names = new Map();
|
|
54
|
+
for (const name of Object.keys(input.tmuxGlobal)) {
|
|
55
|
+
if (matchesLeakPattern(name))
|
|
56
|
+
names.set(name, ["tmux-global"]);
|
|
57
|
+
}
|
|
58
|
+
for (const [name, value] of Object.entries(input.shell)) {
|
|
59
|
+
if (value === undefined || !matchesLeakPattern(name))
|
|
60
|
+
continue;
|
|
61
|
+
const sources = names.get(name) ?? [];
|
|
62
|
+
if (!sources.includes("shell"))
|
|
63
|
+
sources.push("shell");
|
|
64
|
+
names.set(name, sources);
|
|
65
|
+
}
|
|
66
|
+
return [...names.entries()]
|
|
67
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
68
|
+
.map(([name, sources]) => ({
|
|
69
|
+
name,
|
|
70
|
+
sources,
|
|
71
|
+
...classifyLeak(name, input.envFileKeysByService, input.cleanLaunch),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export function redactValue(value) {
|
|
75
|
+
if (value.length === 0)
|
|
76
|
+
return "(empty)";
|
|
77
|
+
if (value.length <= 8)
|
|
78
|
+
return "••••";
|
|
79
|
+
return `${value.slice(0, 4)}…(${value.length} chars)`;
|
|
80
|
+
}
|
|
81
|
+
const FATE_LABEL = {
|
|
82
|
+
reaches: "REACHES SERVICES",
|
|
83
|
+
shadowed: "shadowed by the env file",
|
|
84
|
+
dropped: "dropped by the clean launch",
|
|
85
|
+
partial: "shadowed for some services, dropped for others",
|
|
86
|
+
};
|
|
87
|
+
const SOURCE_LABEL = {
|
|
88
|
+
"tmux-global": "tmux -g",
|
|
89
|
+
shell: "shell",
|
|
90
|
+
};
|
|
91
|
+
export function renderLeakLines(candidates, input) {
|
|
92
|
+
return candidates.map((c) => {
|
|
93
|
+
const values = c.sources.map((src) => {
|
|
94
|
+
const value = src === "tmux-global" ? input.tmuxGlobal[c.name] : input.shell[c.name];
|
|
95
|
+
return `${SOURCE_LABEL[src]}: ${redactValue(value ?? "")}`;
|
|
96
|
+
});
|
|
97
|
+
let fate = FATE_LABEL[c.fate];
|
|
98
|
+
if (c.fate === "partial") {
|
|
99
|
+
fate = `shadowed for ${c.shadowedIn.join(", ")}; dropped for ${c.unshadowedIn.join(", ")}`;
|
|
100
|
+
}
|
|
101
|
+
else if (c.fate === "reaches" && input.cleanLaunch !== false && isAllowedLaunchHostVar(c.name)) {
|
|
102
|
+
fate = `${fate} (allow-listed host variable)`;
|
|
103
|
+
}
|
|
104
|
+
return `${c.name} [${values.join("; ")}] → ${fate}`;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function readTmuxGlobalEnvironment() {
|
|
108
|
+
try {
|
|
109
|
+
const text = execFileSync("tmux", ["show-environment", "-g"], {
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
112
|
+
});
|
|
113
|
+
return parseTmuxEnvironment(text);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function readSessionEnvFileKeys(sessionName, serviceNames) {
|
|
120
|
+
const dir = getSessionEnvDir(sessionName);
|
|
121
|
+
const out = {};
|
|
122
|
+
for (const name of serviceNames) {
|
|
123
|
+
const file = path.join(dir, envFileName(name));
|
|
124
|
+
if (!fs.existsSync(file))
|
|
125
|
+
continue;
|
|
126
|
+
out[name] = envFileKeys(fs.readFileSync(file, "utf8"));
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
export const envLeaksCheck = {
|
|
131
|
+
id: "env-leaks",
|
|
132
|
+
title: "Inherited environment",
|
|
133
|
+
phases: ["ondemand"],
|
|
134
|
+
async run(ctx) {
|
|
135
|
+
const state = ctx.sessionState;
|
|
136
|
+
const services = state?.devOutput.services
|
|
137
|
+
? Object.entries(state.devOutput.services)
|
|
138
|
+
.filter(([, svc]) => svc.command && svc.command.length > 0)
|
|
139
|
+
.map(([name]) => name)
|
|
140
|
+
: [];
|
|
141
|
+
const input = {
|
|
142
|
+
tmuxGlobal: readTmuxGlobalEnvironment(),
|
|
143
|
+
shell: process.env,
|
|
144
|
+
envFileKeysByService: state ? readSessionEnvFileKeys(ctx.sessionName, services) : {},
|
|
145
|
+
cleanLaunch: state ? state.cleanEnv === true : null,
|
|
146
|
+
};
|
|
147
|
+
const candidates = collectLeakCandidates(input);
|
|
148
|
+
if (candidates.length === 0) {
|
|
149
|
+
return {
|
|
150
|
+
status: "ok",
|
|
151
|
+
summary: "no WORKSPACES_*/AWS_*/TEMPORAL_*/VCS_*/*_TOKEN/*_SECRET/*_KEY in the tmux global env or this shell",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const detail = renderLeakLines(candidates, input).join("\n ");
|
|
155
|
+
const reaching = candidates.filter((c) => c.fate === "reaches");
|
|
156
|
+
if (reaching.length > 0 && input.cleanLaunch === false) {
|
|
157
|
+
return {
|
|
158
|
+
status: "warn",
|
|
159
|
+
summary: `${reaching.length} of ${candidates.length} inherited variable(s) reach the services — this session predates the clean launch`,
|
|
160
|
+
detail,
|
|
161
|
+
remediation: "mesh dev # relaunch: services then start under env -i with only their env file",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (reaching.length > 0) {
|
|
165
|
+
return {
|
|
166
|
+
status: "warn",
|
|
167
|
+
summary: `${reaching.length} of ${candidates.length} inherited variable(s) reach the services via the host allow-list`,
|
|
168
|
+
detail,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const where = state ? "services launched clean" : "no session; the next launch is clean";
|
|
172
|
+
return {
|
|
173
|
+
status: "ok",
|
|
174
|
+
summary: `${candidates.length} inherited variable(s) found, none reach the services (${where}) — each is shadowed by the env file or dropped by env -i`,
|
|
175
|
+
detail,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
};
|
|
@@ -2,6 +2,22 @@ export declare const ENV_FILE_MODE = 384;
|
|
|
2
2
|
export declare function quoteShellValue(value: string): string;
|
|
3
3
|
export declare function renderEnvFile(env: Record<string, string>): string;
|
|
4
4
|
export declare function envFileName(serviceName: string): string;
|
|
5
|
+
export declare function getSessionEnvDir(sessionName: string): string;
|
|
6
|
+
export declare function serviceOverridesPaths(sessionName: string, serviceName: string): string[];
|
|
7
|
+
export declare function readServiceOverrides(sessionName: string, serviceName: string): Record<string, string>;
|
|
5
8
|
export declare function writeEnvFile(filePath: string, env: Record<string, string>): void;
|
|
6
|
-
export declare
|
|
9
|
+
export declare const LAUNCH_HOST_ENV_ALLOW_LIST: ReadonlyArray<string>;
|
|
10
|
+
export declare const LAUNCH_PANE_ENV_FORWARDED: ReadonlyArray<string>;
|
|
11
|
+
export declare function isAllowedLaunchHostVar(name: string): boolean;
|
|
12
|
+
export declare function pickLaunchHostEnv(env: Record<string, string | undefined>): Record<string, string>;
|
|
13
|
+
export declare function launchShell(hostEnv: Record<string, string>): string;
|
|
14
|
+
export declare function buildLaunchCommand(envFilePath: string, dir: string, command: string, logShipper: string | undefined, hostEnv: Record<string, string>): string;
|
|
15
|
+
export declare function envFileKeys(content: string): string[];
|
|
16
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
17
|
+
export interface EnvFileChange {
|
|
18
|
+
key: string;
|
|
19
|
+
kind: 'added' | 'removed' | 'changed';
|
|
20
|
+
detail: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function diffEnvFiles(before: Record<string, string>, after: Record<string, string>): EnvFileChange[];
|
|
7
23
|
export declare function waitForPort(host: string, port: number, timeoutMs: number, intervalMs?: number): Promise<boolean>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as net from 'net';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { devSessionsRoot } from '../utils/dev-sessions-root.js';
|
|
4
5
|
export const ENV_FILE_MODE = 0o600;
|
|
5
6
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
7
|
export function quoteShellValue(value) {
|
|
@@ -23,16 +24,140 @@ export function renderEnvFile(env) {
|
|
|
23
24
|
export function envFileName(serviceName) {
|
|
24
25
|
return `${serviceName.replace(/[^A-Za-z0-9._-]/g, '-')}.env.sh`;
|
|
25
26
|
}
|
|
27
|
+
export function getSessionEnvDir(sessionName) {
|
|
28
|
+
return path.join(devSessionsRoot(), sessionName);
|
|
29
|
+
}
|
|
30
|
+
export function serviceOverridesPaths(sessionName, serviceName) {
|
|
31
|
+
const dir = getSessionEnvDir(sessionName);
|
|
32
|
+
return [path.join(dir, '_all.overrides.env.sh'), path.join(dir, `${serviceName}.overrides.env.sh`)];
|
|
33
|
+
}
|
|
34
|
+
export function readServiceOverrides(sessionName, serviceName) {
|
|
35
|
+
const merged = {};
|
|
36
|
+
for (const file of serviceOverridesPaths(sessionName, serviceName)) {
|
|
37
|
+
try {
|
|
38
|
+
Object.assign(merged, parseEnvFile(fs.readFileSync(file, 'utf8')));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
26
45
|
export function writeEnvFile(filePath, env) {
|
|
27
46
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
28
47
|
fs.writeFileSync(filePath, renderEnvFile(env), { mode: ENV_FILE_MODE });
|
|
29
48
|
fs.chmodSync(filePath, ENV_FILE_MODE);
|
|
30
49
|
}
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
export const LAUNCH_HOST_ENV_ALLOW_LIST = [
|
|
51
|
+
'HOME',
|
|
52
|
+
'USER',
|
|
53
|
+
'LOGNAME',
|
|
54
|
+
'PATH',
|
|
55
|
+
'SHELL',
|
|
56
|
+
'TMPDIR',
|
|
57
|
+
'MESH_DEV_SESSIONS_DIR',
|
|
58
|
+
'LANG',
|
|
59
|
+
'LANGUAGE',
|
|
60
|
+
'TZ',
|
|
61
|
+
'XDG_CONFIG_HOME',
|
|
62
|
+
'XDG_CACHE_HOME',
|
|
63
|
+
'XDG_DATA_HOME',
|
|
64
|
+
'XDG_STATE_HOME',
|
|
65
|
+
'XDG_RUNTIME_DIR',
|
|
66
|
+
'MESH_CACHE_HOME',
|
|
67
|
+
'SSH_AUTH_SOCK',
|
|
68
|
+
'DOCKER_HOST',
|
|
69
|
+
'DOCKER_CONTEXT',
|
|
70
|
+
'DOCKER_CONFIG',
|
|
71
|
+
'DOCKER_CERT_PATH',
|
|
72
|
+
'DOCKER_TLS_VERIFY',
|
|
73
|
+
'HTTP_PROXY',
|
|
74
|
+
'HTTPS_PROXY',
|
|
75
|
+
'NO_PROXY',
|
|
76
|
+
'http_proxy',
|
|
77
|
+
'https_proxy',
|
|
78
|
+
'no_proxy',
|
|
79
|
+
'NODE_EXTRA_CA_CERTS',
|
|
80
|
+
'SSL_CERT_FILE',
|
|
81
|
+
'SSL_CERT_DIR',
|
|
82
|
+
];
|
|
83
|
+
const LAUNCH_HOST_ENV_ALLOW_PREFIXES = [
|
|
84
|
+
'LC_',
|
|
85
|
+
];
|
|
86
|
+
export const LAUNCH_PANE_ENV_FORWARDED = ['TERM', 'TMUX', 'TMUX_PANE'];
|
|
87
|
+
export function isAllowedLaunchHostVar(name) {
|
|
88
|
+
return (LAUNCH_HOST_ENV_ALLOW_LIST.includes(name) ||
|
|
89
|
+
LAUNCH_HOST_ENV_ALLOW_PREFIXES.some((prefix) => name.startsWith(prefix)));
|
|
90
|
+
}
|
|
91
|
+
export function pickLaunchHostEnv(env) {
|
|
92
|
+
const picked = {};
|
|
93
|
+
for (const name of Object.keys(env).sort()) {
|
|
94
|
+
const value = env[name];
|
|
95
|
+
if (value && isAllowedLaunchHostVar(name))
|
|
96
|
+
picked[name] = value;
|
|
97
|
+
}
|
|
98
|
+
return picked;
|
|
99
|
+
}
|
|
100
|
+
export function launchShell(hostEnv) {
|
|
101
|
+
const shell = hostEnv.SHELL;
|
|
102
|
+
if (shell && path.isAbsolute(shell) && /^(bash|zsh)$/.test(path.basename(shell)))
|
|
103
|
+
return shell;
|
|
104
|
+
return '/bin/sh';
|
|
105
|
+
}
|
|
106
|
+
export function buildLaunchCommand(envFilePath, dir, command, logShipper, hostEnv) {
|
|
107
|
+
const sourced = `. ${quoteShellValue(envFilePath)} && `;
|
|
108
|
+
const script = logShipper
|
|
109
|
+
? `${sourced}{ ${command}; } 2>&1 | NODE_OPTIONS= node ${quoteShellValue(logShipper)}`
|
|
110
|
+
: `${sourced}${command}`;
|
|
111
|
+
const forwarded = LAUNCH_PANE_ENV_FORWARDED.map((name) => `${name}="$${name}"`).join(' ');
|
|
112
|
+
return (`cd ${quoteShellValue(dir)} && env -i ${forwarded} ` +
|
|
113
|
+
`${quoteShellValue(launchShell(hostEnv))} -c ${quoteShellValue(script)}`);
|
|
114
|
+
}
|
|
115
|
+
export function envFileKeys(content) {
|
|
116
|
+
const keys = [];
|
|
117
|
+
for (const line of content.split('\n')) {
|
|
118
|
+
const match = /^export ([A-Za-z_][A-Za-z0-9_]*)=/.exec(line);
|
|
119
|
+
if (match)
|
|
120
|
+
keys.push(match[1]);
|
|
121
|
+
}
|
|
122
|
+
return keys;
|
|
123
|
+
}
|
|
124
|
+
export function parseEnvFile(content) {
|
|
125
|
+
const env = {};
|
|
126
|
+
const entry = /^export ([A-Za-z_][A-Za-z0-9_]*)='((?:[^']|'\\'')*)'$/gm;
|
|
127
|
+
for (const match of content.matchAll(entry)) {
|
|
128
|
+
env[match[1]] = match[2].replace(/'\\''/g, "'");
|
|
129
|
+
}
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
const SECRET_KEY_RE = /TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|COOKIE|AUTH/i;
|
|
133
|
+
const PRINTABLE_VALUE_MAX = 80;
|
|
134
|
+
export function diffEnvFiles(before, after) {
|
|
135
|
+
const show = (key, value) => {
|
|
136
|
+
if (SECRET_KEY_RE.test(key))
|
|
137
|
+
return '(secret)';
|
|
138
|
+
if (value.length > PRINTABLE_VALUE_MAX)
|
|
139
|
+
return `(${value.length} chars)`;
|
|
140
|
+
return JSON.stringify(value);
|
|
141
|
+
};
|
|
142
|
+
const changes = [];
|
|
143
|
+
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
144
|
+
for (const key of keys) {
|
|
145
|
+
const was = before[key];
|
|
146
|
+
const now = after[key];
|
|
147
|
+
if (was === undefined && now !== undefined) {
|
|
148
|
+
changes.push({ key, kind: 'added', detail: `+ ${key} = ${show(key, now)}` });
|
|
149
|
+
}
|
|
150
|
+
else if (was !== undefined && now === undefined) {
|
|
151
|
+
changes.push({ key, kind: 'removed', detail: `- ${key} (was ${show(key, was)})` });
|
|
152
|
+
}
|
|
153
|
+
else if (was !== undefined && now !== undefined && was !== now) {
|
|
154
|
+
const detail = SECRET_KEY_RE.test(key)
|
|
155
|
+
? `~ ${key} (secret; value changed)`
|
|
156
|
+
: `~ ${key}: ${show(key, was)} → ${show(key, now)}`;
|
|
157
|
+
changes.push({ key, kind: 'changed', detail });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return changes;
|
|
36
161
|
}
|
|
37
162
|
export async function waitForPort(host, port, timeoutMs, intervalMs = 500) {
|
|
38
163
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PaneLiveness } from "./dev-doctor.js";
|
|
2
|
+
export declare const TEMPORAL_WORKER_RUNNING: RegExp;
|
|
3
|
+
export declare const RESTART_READY_TIMEOUT_MS = 60000;
|
|
4
|
+
export declare const RESTART_ALIVE_GRACE_MS = 10000;
|
|
5
|
+
export declare const RESTART_START_WINDOW_MS = 5000;
|
|
6
|
+
export type PaneExit = Extract<PaneLiveness, {
|
|
7
|
+
state: "exited";
|
|
8
|
+
}>;
|
|
9
|
+
export type ServiceReadiness = {
|
|
10
|
+
state: "ready";
|
|
11
|
+
via: "port" | "marker" | "alive";
|
|
12
|
+
afterMs: number;
|
|
13
|
+
} | {
|
|
14
|
+
state: "exited";
|
|
15
|
+
liveness: PaneExit;
|
|
16
|
+
afterMs: number;
|
|
17
|
+
output: string;
|
|
18
|
+
} | {
|
|
19
|
+
state: "pending";
|
|
20
|
+
afterMs: number;
|
|
21
|
+
output: string;
|
|
22
|
+
} | {
|
|
23
|
+
state: "port-held";
|
|
24
|
+
afterMs: number;
|
|
25
|
+
output: string;
|
|
26
|
+
} | {
|
|
27
|
+
state: "unknown";
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
export interface ReadinessProbe {
|
|
31
|
+
liveness(): PaneLiveness;
|
|
32
|
+
portAccepts(port: number): Promise<boolean>;
|
|
33
|
+
paneOutput(): string;
|
|
34
|
+
now(): number;
|
|
35
|
+
sleep(ms: number): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface ReadinessTarget {
|
|
38
|
+
port: number;
|
|
39
|
+
portHeldBeforeStart?: boolean;
|
|
40
|
+
readyMarker?: RegExp;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
intervalMs?: number;
|
|
43
|
+
graceMs?: number;
|
|
44
|
+
startWindowMs?: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function awaitServiceReadiness(target: ReadinessTarget, probe: ReadinessProbe): Promise<ServiceReadiness>;
|
|
47
|
+
export declare function portHeldByAnother(port: number, probe: Pick<ReadinessProbe, "portAccepts" | "now" | "sleep">, settleMs?: number, intervalMs?: number): Promise<boolean>;
|
|
48
|
+
export declare function paneTail(output: string, n?: number): string[];
|
|
49
|
+
export declare function parseWaitSeconds(raw: string | boolean | undefined, defaultSeconds: number): number;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const TEMPORAL_WORKER_RUNNING = /Worker state changed[\s\S]{0,400}?state: 'RUNNING'/;
|
|
2
|
+
export const RESTART_READY_TIMEOUT_MS = 60_000;
|
|
3
|
+
export const RESTART_ALIVE_GRACE_MS = 10_000;
|
|
4
|
+
export const RESTART_START_WINDOW_MS = 5_000;
|
|
5
|
+
export async function awaitServiceReadiness(target, probe) {
|
|
6
|
+
const timeoutMs = target.timeoutMs ?? RESTART_READY_TIMEOUT_MS;
|
|
7
|
+
const intervalMs = target.intervalMs ?? 500;
|
|
8
|
+
const graceMs = Math.min(target.graceMs ?? RESTART_ALIVE_GRACE_MS, timeoutMs);
|
|
9
|
+
const startWindowMs = Math.min(target.startWindowMs ?? RESTART_START_WINDOW_MS, timeoutMs);
|
|
10
|
+
const portIsSignal = target.port > 0 && !target.portHeldBeforeStart;
|
|
11
|
+
const hasSignal = portIsSignal || target.readyMarker !== undefined;
|
|
12
|
+
const start = probe.now();
|
|
13
|
+
for (;;) {
|
|
14
|
+
const afterMs = probe.now() - start;
|
|
15
|
+
const liveness = probe.liveness();
|
|
16
|
+
if (liveness.state === "exited") {
|
|
17
|
+
if (afterMs >= startWindowMs) {
|
|
18
|
+
return { state: "exited", liveness, afterMs, output: probe.paneOutput() };
|
|
19
|
+
}
|
|
20
|
+
await probe.sleep(Math.min(intervalMs, startWindowMs - afterMs));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const blind = liveness.state === "unknown";
|
|
24
|
+
if (portIsSignal && (await probe.portAccepts(target.port))) {
|
|
25
|
+
return { state: "ready", via: "port", afterMs };
|
|
26
|
+
}
|
|
27
|
+
if (target.readyMarker && target.readyMarker.test(probe.paneOutput())) {
|
|
28
|
+
return { state: "ready", via: "marker", afterMs };
|
|
29
|
+
}
|
|
30
|
+
if (!hasSignal) {
|
|
31
|
+
if (blind)
|
|
32
|
+
return { state: "unknown", reason: liveness.reason };
|
|
33
|
+
if (afterMs >= graceMs) {
|
|
34
|
+
return target.portHeldBeforeStart
|
|
35
|
+
? { state: "port-held", afterMs, output: probe.paneOutput() }
|
|
36
|
+
: { state: "ready", via: "alive", afterMs };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (afterMs >= timeoutMs) {
|
|
40
|
+
if (blind && liveness.state === "unknown") {
|
|
41
|
+
return { state: "unknown", reason: `${liveness.reason}; and no readiness signal in ${timeoutMs}ms` };
|
|
42
|
+
}
|
|
43
|
+
return hasSignal
|
|
44
|
+
? { state: "pending", afterMs, output: probe.paneOutput() }
|
|
45
|
+
: { state: "ready", via: "alive", afterMs };
|
|
46
|
+
}
|
|
47
|
+
await probe.sleep(Math.min(intervalMs, timeoutMs - afterMs));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function portHeldByAnother(port, probe, settleMs = 3000, intervalMs = 250) {
|
|
51
|
+
if (port <= 0)
|
|
52
|
+
return false;
|
|
53
|
+
const start = probe.now();
|
|
54
|
+
for (;;) {
|
|
55
|
+
if (!(await probe.portAccepts(port)))
|
|
56
|
+
return false;
|
|
57
|
+
if (probe.now() - start >= settleMs)
|
|
58
|
+
return true;
|
|
59
|
+
await probe.sleep(intervalMs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function paneTail(output, n = 20) {
|
|
63
|
+
return output
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line) => line.trimEnd())
|
|
66
|
+
.filter((line) => line.length > 0)
|
|
67
|
+
.slice(-n);
|
|
68
|
+
}
|
|
69
|
+
export function parseWaitSeconds(raw, defaultSeconds) {
|
|
70
|
+
if (raw === false)
|
|
71
|
+
return 0;
|
|
72
|
+
if (raw === undefined || raw === true)
|
|
73
|
+
return defaultSeconds;
|
|
74
|
+
const n = Number(raw);
|
|
75
|
+
if (!Number.isFinite(n) || n < 0)
|
|
76
|
+
return defaultSeconds;
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type ResolvedAwsCreds } from '../utils/aws-auth.js';
|
|
3
3
|
import { type ComposeExternalRef } from './local/mocks.js';
|
|
4
|
+
import { type WorktreeIdentity } from '../utils/worktree-identity.js';
|
|
4
5
|
interface DevEnvVar {
|
|
5
6
|
value: string;
|
|
6
7
|
tunnel?: string;
|
|
@@ -31,6 +32,12 @@ export interface DevOutput {
|
|
|
31
32
|
services: Record<string, DevService>;
|
|
32
33
|
tunnels: Record<string, DevTunnel>;
|
|
33
34
|
secrets?: Record<string, DevSecret>;
|
|
35
|
+
provenance?: DevOutputProvenance;
|
|
36
|
+
}
|
|
37
|
+
export interface DevOutputProvenance {
|
|
38
|
+
gitCommit?: string;
|
|
39
|
+
configSha256?: string;
|
|
40
|
+
worktreeDigest?: string;
|
|
34
41
|
}
|
|
35
42
|
export declare function registerServiceProbes(devOutput: DevOutput, tenant: string, probeFiles: string[]): void;
|
|
36
43
|
export interface SessionState {
|
|
@@ -46,11 +53,33 @@ export interface SessionState {
|
|
|
46
53
|
app: string;
|
|
47
54
|
};
|
|
48
55
|
workflowFingerprint?: string;
|
|
56
|
+
cleanEnv?: boolean;
|
|
49
57
|
}
|
|
50
58
|
export declare function workflowChanged(prior: string | undefined, current: string): boolean;
|
|
59
|
+
export interface SessionPortMemory {
|
|
60
|
+
services: Record<string, number>;
|
|
61
|
+
savedAt: string;
|
|
62
|
+
}
|
|
63
|
+
export declare function rememberedPortsOf(memory: Partial<SessionPortMemory> | null | undefined): Record<string, number>;
|
|
51
64
|
export declare function isPortFree(port: number): Promise<boolean>;
|
|
52
65
|
export declare function rewriteOwnServicePort(service: DevService, oldPort: number, newPort: number): void;
|
|
66
|
+
export interface PortProbes {
|
|
67
|
+
isPortFree(port: number): Promise<boolean>;
|
|
68
|
+
findFreePort(): Promise<number>;
|
|
69
|
+
}
|
|
70
|
+
export declare function allocatePorts(devOutput: DevOutput, worktree: WorktreeIdentity, remembered?: Record<string, number>, probes?: PortProbes, ownPorts?: ReadonlySet<number>): Promise<DevOutput>;
|
|
53
71
|
export declare function mergePinnedPorts(fresh: DevOutput, pinned: DevOutput): DevOutput;
|
|
72
|
+
export declare function worktreeDigestOfDir(dir: string): string | null;
|
|
73
|
+
export declare function currentProgramProvenance(appRoot: string, stack: string): {
|
|
74
|
+
gitCommit: string | null;
|
|
75
|
+
configSha256: string | null;
|
|
76
|
+
worktreeDigest: string | null;
|
|
77
|
+
};
|
|
78
|
+
export declare function devOutputStaleness(recorded: DevOutputProvenance | undefined, current: {
|
|
79
|
+
gitCommit: string | null;
|
|
80
|
+
configSha256: string | null;
|
|
81
|
+
worktreeDigest?: string | null;
|
|
82
|
+
}): string[];
|
|
54
83
|
export declare function buildChildAwsEnv(awsEnv: Record<string, string>, method: ResolvedAwsCreds['method'] | 'ambient', profile: string | undefined, opts?: {
|
|
55
84
|
context: string;
|
|
56
85
|
roleArn: string;
|
|
@@ -60,7 +89,25 @@ export declare function buildChildAwsEnv(awsEnv: Record<string, string>, method:
|
|
|
60
89
|
export declare function readDotEnvFile(file: string): Record<string, string>;
|
|
61
90
|
export declare function rebaseServiceSrc(src: string, monorepoRoot: string | null): string;
|
|
62
91
|
export declare function isLinkedDependencyDir(serviceDir: string, appRoot: string): boolean;
|
|
92
|
+
export declare function relinkedServiceDir(serviceDir: string, appRoot: string): string | null;
|
|
63
93
|
export declare function monorepoRelativeSrc(src: string): string | null;
|
|
94
|
+
export declare function portSiblingOf(key: string): string | undefined;
|
|
95
|
+
export declare function pointAtTunnel(key: string, value: string, tunnel: {
|
|
96
|
+
host: string;
|
|
97
|
+
port: number;
|
|
98
|
+
}): string;
|
|
99
|
+
export declare function tunnelForSecret(secretName: string, tunnels: Record<string, {
|
|
100
|
+
host: string;
|
|
101
|
+
port: number;
|
|
102
|
+
}>): {
|
|
103
|
+
host: string;
|
|
104
|
+
port: number;
|
|
105
|
+
} | undefined;
|
|
106
|
+
export declare function pointSecretUrlsAtTunnel(values: Record<string, string>, tunnel: {
|
|
107
|
+
host: string;
|
|
108
|
+
port: number;
|
|
109
|
+
} | undefined): Record<string, string>;
|
|
110
|
+
export declare function stackNamespace(devOutput: Pick<DevOutput, 'platform' | 'stack' | 'app'>): string | undefined;
|
|
64
111
|
export declare function tunnelClientAddress(tunnel: {
|
|
65
112
|
host: string;
|
|
66
113
|
port: number;
|
|
@@ -100,5 +147,18 @@ export type ReachabilityDecision = {
|
|
|
100
147
|
export declare function reachabilityFallbackTransport(transport: Transport, temporalReachable: boolean, hasSsmPlugin: boolean): ReachabilityDecision;
|
|
101
148
|
export declare function preferredSsmLocalPort(remotePort: number): number;
|
|
102
149
|
export declare function reserveSsmLocalPortCandidate(remotePort: number, reservedPorts: Set<number>, startAt?: number): number;
|
|
150
|
+
export declare function bastionServiceForTunnel(tunnelName: string, tunnel: {
|
|
151
|
+
host: string;
|
|
152
|
+
port: number;
|
|
153
|
+
}, services: Record<string, {
|
|
154
|
+
host: string;
|
|
155
|
+
port: number;
|
|
156
|
+
}>): {
|
|
157
|
+
key: string;
|
|
158
|
+
endpoint: {
|
|
159
|
+
host: string;
|
|
160
|
+
port: number;
|
|
161
|
+
};
|
|
162
|
+
} | undefined;
|
|
103
163
|
export declare function registerDevCommand(program: Command): void;
|
|
104
164
|
export {};
|