@mesh-tech/mesh-cli 0.38.1 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
  2. package/dist/bin/{mesh.chunk-I6NXLXGC.js → mesh.chunk-2ASXJ5DO.js} +235 -581
  3. package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
  4. package/dist/bin/{mesh.chunk-IU2IWH6O.js → mesh.chunk-6AIG5OJM.js} +3 -3
  5. package/dist/bin/{mesh.chunk-KN4BZOYH.js → mesh.chunk-6SSOVZOI.js} +1 -1
  6. package/dist/bin/{mesh.chunk-LMYPTJ2F.js → mesh.chunk-7HTKSUWR.js} +1900 -577
  7. package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
  8. package/dist/bin/{mesh.chunk-2YRWJH5X.js → mesh.chunk-F44A3ACW.js} +4 -4
  9. package/dist/bin/{mesh.chunk-2YRWJH5X.js.map → mesh.chunk-F44A3ACW.js.map} +1 -1
  10. package/dist/bin/{mesh.chunk-BF2SH6XT.js → mesh.chunk-HJGMOEUG.js} +1 -1
  11. package/dist/bin/{mesh.chunk-TZAGJTKU.js → mesh.chunk-NSQJEX36.js} +1 -1
  12. package/dist/bin/{mesh.chunk-HPYZWPRM.js → mesh.chunk-PZVI7V3J.js} +1 -1
  13. package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
  14. package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
  15. package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
  16. package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
  17. package/dist/bin/{mesh.create-app-NGXACFAU.js → mesh.create-app-Z3RKSTBO.js} +3 -3
  18. package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
  19. package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
  20. package/dist/bin/mesh.js +9 -8
  21. package/dist/bin/mesh.js.map +1 -1
  22. package/dist/bin/{mesh.login-7SIAKFXJ.js → mesh.login-3AWVGPCM.js} +2 -2
  23. package/dist/bin/{mesh.platform-setup-CJXE3BCP.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
  24. package/dist/bin/{mesh.program-BIDQBSSI.js → mesh.program-K5JQXEHX.js} +9 -8
  25. package/dist/bin/{mesh.registry-7P4MGXEK.js → mesh.registry-JLQYFUNN.js} +4 -4
  26. package/dist/bin/{mesh.registry-identity-JW4KO7N7.js → mesh.registry-identity-3AR27UGO.js} +3 -3
  27. package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
  28. package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
  29. package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
  30. package/dist/bin/{mesh.start-MPQFIEMH.js → mesh.start-KZ5L6SOJ.js} +2 -2
  31. package/dist/bin/{mesh.wizard-ZJE4S6PC.js → mesh.wizard-FCTT55KI.js} +7 -7
  32. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
  33. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
  34. package/dist/build-info.json +2 -2
  35. package/dist/src/commands/app-check.js +2 -2
  36. package/dist/src/commands/dev-doctor.d.ts +22 -0
  37. package/dist/src/commands/dev-doctor.js +149 -0
  38. package/dist/src/commands/dev-env-leaks.d.ts +24 -0
  39. package/dist/src/commands/dev-env-leaks.js +178 -0
  40. package/dist/src/commands/dev-launch.d.ts +17 -1
  41. package/dist/src/commands/dev-launch.js +130 -5
  42. package/dist/src/commands/dev-readiness.d.ts +49 -0
  43. package/dist/src/commands/dev-readiness.js +78 -0
  44. package/dist/src/commands/dev.d.ts +60 -0
  45. package/dist/src/commands/dev.js +574 -140
  46. package/dist/src/commands/hub/index.js +10 -7
  47. package/dist/src/commands/local/app-authz.d.ts +118 -0
  48. package/dist/src/commands/local/app-authz.js +295 -0
  49. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  50. package/dist/src/commands/local/auth-provision.js +12 -1
  51. package/dist/src/commands/local/index.js +2 -0
  52. package/dist/src/commands/mcp/generate.d.ts +11 -0
  53. package/dist/src/commands/mcp/generate.js +78 -0
  54. package/dist/src/commands/mcp/index.d.ts +2 -0
  55. package/dist/src/commands/mcp/index.js +70 -0
  56. package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
  57. package/dist/src/commands/mcp/manifest-file.js +24 -0
  58. package/dist/src/commands/mcp/review-command.d.ts +24 -0
  59. package/dist/src/commands/mcp/review-command.js +66 -0
  60. package/dist/src/commands/mcp/review.d.ts +41 -0
  61. package/dist/src/commands/mcp/review.js +140 -0
  62. package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
  63. package/dist/src/commands/mcp/workspace-membership.js +77 -0
  64. package/dist/src/commands/secrets/reindex.js +2 -7
  65. package/dist/src/commands/secrets/set.d.ts +22 -1
  66. package/dist/src/commands/secrets/set.js +31 -8
  67. package/dist/src/commands/temporal.js +51 -5
  68. package/dist/src/commands/temporal.js.map +2 -2
  69. package/dist/src/program.js +2 -0
  70. package/dist/src/utils/aws-auth.d.ts +4 -0
  71. package/dist/src/utils/aws-auth.js +7 -3
  72. package/dist/src/utils/dev-sessions-root.d.ts +3 -0
  73. package/dist/src/utils/dev-sessions-root.js +45 -0
  74. package/dist/src/utils/kubeconfig.js +2 -2
  75. package/dist/src/utils/tailscale.js +7 -1
  76. package/dist/src/utils/temporal-auth.js +2 -1
  77. package/dist/src/utils/workflow-fingerprint.js +23 -12
  78. package/fragments/base/index.ts.hbs +4 -1
  79. package/package.json +7 -5
  80. package/skills/mesh/SKILL.md +10 -6
  81. package/templates/api-auth/api/src/schema.ts.hbs +17 -0
  82. package/templates/api-auth/index.ts.hbs +1 -0
  83. package/templates/api-auth/package.json.hbs +3 -0
  84. package/templates/api-role-gating/api/src/schema.ts.hbs +10 -0
  85. package/templates/api-role-gating/index.ts.hbs +1 -0
  86. package/templates/external-service/index.ts.hbs +1 -0
  87. package/templates/workflow/index.ts.hbs +2 -0
  88. package/dist/bin/mesh.chunk-I6NXLXGC.js.map +0 -6
  89. package/dist/bin/mesh.chunk-LMYPTJ2F.js.map +0 -6
  90. /package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
  91. /package/dist/bin/{mesh.chunk-IU2IWH6O.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
  92. /package/dist/bin/{mesh.chunk-KN4BZOYH.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
  93. /package/dist/bin/{mesh.chunk-BF2SH6XT.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
  94. /package/dist/bin/{mesh.chunk-TZAGJTKU.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
  95. /package/dist/bin/{mesh.chunk-HPYZWPRM.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
  96. /package/dist/bin/{mesh.create-app-NGXACFAU.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
  97. /package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
  98. /package/dist/bin/{mesh.login-7SIAKFXJ.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
  99. /package/dist/bin/{mesh.platform-setup-CJXE3BCP.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
  100. /package/dist/bin/{mesh.program-BIDQBSSI.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
  101. /package/dist/bin/{mesh.registry-7P4MGXEK.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
  102. /package/dist/bin/{mesh.registry-identity-JW4KO7N7.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
  103. /package/dist/bin/{mesh.start-MPQFIEMH.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
  104. /package/dist/bin/{mesh.wizard-ZJE4S6PC.js.map → mesh.wizard-FCTT55KI.js.map} +0 -0
@@ -0,0 +1,24 @@
1
+ import type { Check } from "./dev-doctor.js";
2
+ export declare const LEAK_NAME_PATTERNS: ReadonlyArray<RegExp>;
3
+ export declare function matchesLeakPattern(name: string): boolean;
4
+ export declare function parseTmuxEnvironment(text: string): Record<string, string>;
5
+ export type LeakSource = "tmux-global" | "shell";
6
+ export type LeakFate = "reaches" | "shadowed" | "dropped" | "partial";
7
+ export interface LeakCandidate {
8
+ name: string;
9
+ sources: LeakSource[];
10
+ fate: LeakFate;
11
+ shadowedIn: string[];
12
+ unshadowedIn: string[];
13
+ }
14
+ export interface ClassifyInput {
15
+ tmuxGlobal: Record<string, string>;
16
+ shell: Record<string, string | undefined>;
17
+ envFileKeysByService: Record<string, string[]>;
18
+ cleanLaunch: boolean | null;
19
+ }
20
+ export declare function classifyLeak(name: string, envFileKeysByService: Record<string, string[]>, cleanLaunch: boolean | null): Pick<LeakCandidate, "fate" | "shadowedIn" | "unshadowedIn">;
21
+ export declare function collectLeakCandidates(input: ClassifyInput): LeakCandidate[];
22
+ export declare function redactValue(value: string): string;
23
+ export declare function renderLeakLines(candidates: LeakCandidate[], input: ClassifyInput): string[];
24
+ export declare const envLeaksCheck: Check;
@@ -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 function buildLaunchCommand(envFilePath: string, dir: string, command: string, logShipper?: string): string;
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 function buildLaunchCommand(envFilePath, dir, command, logShipper) {
32
- const base = `cd ${quoteShellValue(dir)} && source ${quoteShellValue(envFilePath)} && `;
33
- if (!logShipper)
34
- return `${base}${command}`;
35
- return `${base}{ ${command}; } 2>&1 | NODE_OPTIONS= node ${quoteShellValue(logShipper)}`;
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 {};