@mesh-tech/mesh-cli 0.16.0 → 0.17.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 (78) hide show
  1. package/dist/bin/mesh.js +1381 -897
  2. package/dist/bin/mesh.js.map +4 -4
  3. package/dist/build-info.json +2 -2
  4. package/dist/src/commands/agent-api-client.d.ts +17 -0
  5. package/dist/src/commands/agent-api-client.d.ts.map +1 -1
  6. package/dist/src/commands/agent-api-client.js +22 -0
  7. package/dist/src/commands/agent-api-client.js.map +1 -1
  8. package/dist/src/commands/dev-doctor.d.ts.map +1 -1
  9. package/dist/src/commands/dev-doctor.js +7 -2
  10. package/dist/src/commands/dev-doctor.js.map +1 -1
  11. package/dist/src/commands/dev.d.ts +22 -0
  12. package/dist/src/commands/dev.d.ts.map +1 -1
  13. package/dist/src/commands/dev.js +124 -9
  14. package/dist/src/commands/dev.js.map +1 -1
  15. package/dist/src/commands/local/docker-runner.d.ts.map +1 -1
  16. package/dist/src/commands/local/docker-runner.js +2 -4
  17. package/dist/src/commands/local/docker-runner.js.map +1 -1
  18. package/dist/src/commands/local/hub-local.d.ts.map +1 -1
  19. package/dist/src/commands/local/hub-local.js +2 -3
  20. package/dist/src/commands/local/hub-local.js.map +1 -1
  21. package/dist/src/commands/local/seed-zitadel.d.ts.map +1 -1
  22. package/dist/src/commands/local/seed-zitadel.js +108 -109
  23. package/dist/src/commands/local/seed-zitadel.js.map +1 -1
  24. package/dist/src/commands/local/stack.d.ts.map +1 -1
  25. package/dist/src/commands/local/stack.js +4 -9
  26. package/dist/src/commands/local/stack.js.map +1 -1
  27. package/dist/src/commands/site.d.ts +3 -0
  28. package/dist/src/commands/site.d.ts.map +1 -0
  29. package/dist/src/commands/site.js +277 -0
  30. package/dist/src/commands/site.js.map +1 -0
  31. package/dist/src/commands/temporal.d.ts +25 -0
  32. package/dist/src/commands/temporal.d.ts.map +1 -1
  33. package/dist/src/commands/temporal.js +2459 -732
  34. package/dist/src/commands/temporal.js.map +7 -1
  35. package/dist/src/commands/vcs/clone.d.ts.map +1 -1
  36. package/dist/src/commands/vcs/clone.js +4 -4
  37. package/dist/src/commands/vcs/clone.js.map +1 -1
  38. package/dist/src/commands/vcs/common.d.ts +34 -0
  39. package/dist/src/commands/vcs/common.d.ts.map +1 -1
  40. package/dist/src/commands/vcs/common.js +52 -0
  41. package/dist/src/commands/vcs/common.js.map +1 -1
  42. package/dist/src/commands/vcs/index.d.ts.map +1 -1
  43. package/dist/src/commands/vcs/index.js +7 -1
  44. package/dist/src/commands/vcs/index.js.map +1 -1
  45. package/dist/src/commands/vcs/propose.d.ts +22 -0
  46. package/dist/src/commands/vcs/propose.d.ts.map +1 -1
  47. package/dist/src/commands/vcs/propose.js +25 -5
  48. package/dist/src/commands/vcs/propose.js.map +1 -1
  49. package/dist/src/commands/vcs/rm.d.ts +18 -0
  50. package/dist/src/commands/vcs/rm.d.ts.map +1 -0
  51. package/dist/src/commands/vcs/rm.js +88 -0
  52. package/dist/src/commands/vcs/rm.js.map +1 -0
  53. package/dist/src/program.d.ts.map +1 -1
  54. package/dist/src/program.js +2 -0
  55. package/dist/src/program.js.map +1 -1
  56. package/dist/src/utils/aws-auth.d.ts +10 -0
  57. package/dist/src/utils/aws-auth.d.ts.map +1 -1
  58. package/dist/src/utils/aws-auth.js +35 -0
  59. package/dist/src/utils/aws-auth.js.map +1 -1
  60. package/dist/src/utils/cache-home.d.ts +25 -0
  61. package/dist/src/utils/cache-home.d.ts.map +1 -0
  62. package/dist/src/utils/cache-home.js +36 -0
  63. package/dist/src/utils/cache-home.js.map +1 -0
  64. package/dist/src/utils/temporal-codec.js +207 -294
  65. package/dist/src/utils/temporal-codec.js.map +7 -1
  66. package/fragments/base/index.ts.hbs +1 -1
  67. package/package.json +12 -3
  68. package/skills/core/SKILL.md +13 -3
  69. package/templates/api-auth/README.md.hbs +6 -0
  70. package/templates/api-auth/index.ts.hbs +1 -1
  71. package/templates/api-auth/package.json.hbs +1 -0
  72. package/templates/api-role-gating/index.ts.hbs +1 -1
  73. package/templates/api-role-gating/package.json.hbs +1 -0
  74. package/templates/external-service/api/package.json.hbs +1 -0
  75. package/templates/external-service/index.ts.hbs +1 -1
  76. package/templates/external-service/package.json.hbs +1 -0
  77. package/templates/workflow/index.ts.hbs +1 -1
  78. package/templates/workflow/package.json.hbs +1 -0
@@ -1,781 +1,2508 @@
1
- /**
2
- * mesh temporal — inspect Temporal workflows for the current app/stack.
3
- *
4
- * Commands:
5
- * mesh temporal describe <workflowId> [runId] Show workflow status and metadata
6
- * mesh temporal history <workflowId> [runId] Show workflow event history
7
- *
8
- * Connection info is resolved from the app's Pulumi stack output (same as mesh dev).
9
- * Uses the raw gRPC API so we can render events without a codec proxy; when
10
- * `--show-payloads` is set we resolve the app's `TEMPORAL_ENCODING_KEY` from
11
- * its Kubernetes secret and decrypt inline via the shared codec helper.
12
- */
13
- import { spawnSync } from "node:child_process";
14
- import { writeFileSync, mkdirSync } from "node:fs";
15
- import { homedir } from "node:os";
16
- import { dirname, join, resolve } from "node:path";
17
- import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js";
18
- import { classifyHistoryEvents, extractSnapshotMessages, reconstructTranscript, renderTranscriptMarkdown, } from "../utils/recover-conversation.js";
19
- import { findAppRoot, findStackConfigs, getCurrentStack, pulumiStackOutput, readStackConfig, } from "../utils/pulumi.js";
20
- import { resolveAwsCredentials } from "../utils/aws-auth.js";
21
- import { resolveTemporalAuth, acquireTemporalBearer } from "../utils/temporal-auth.js";
22
- import { buildDecrypterForNamespace, buildRawDecrypterForNamespace, } from "../utils/temporal-codec.js";
23
- /**
24
- * Resolve Temporal address and namespace from the current app's stack output.
25
- * Falls back to env vars if stack output isn't available.
26
- */
27
- async function resolveConnection(options) {
28
- // Explicit overrides take precedence
29
- if (options.address && options.namespace) {
30
- return { address: options.address, namespace: options.namespace };
31
- }
32
- // Try from environment (set by mesh dev)
33
- if (!options.address && process.env.TEMPORAL_ADDRESS) {
34
- const address = process.env.TEMPORAL_ADDRESS;
35
- const namespace = options.namespace ?? process.env.TEMPORAL_NAMESPACE;
36
- if (namespace)
37
- return { address, namespace };
38
- }
39
- // Resolve from Pulumi stack output
40
- const appRoot = findAppRoot(process.cwd());
41
- if (!appRoot) {
42
- throw new Error("Not in a Mesh app directory (no Pulumi.yaml found).\n" +
43
- "Run from an app directory, or provide --address and --namespace.");
44
- }
45
- const stack = options.stack ?? getCurrentStack(appRoot);
46
- if (!stack) {
47
- const stacks = findStackConfigs(appRoot);
48
- throw new Error(`No Pulumi stack selected.\n` +
49
- (stacks.length > 0
50
- ? `Available: ${stacks.join(", ")}\nUse: mesh temporal --stack <name> ...`
51
- : "No stack configs found in this directory."));
52
- }
53
- const sa = stack ? ["--stack", stack] : [];
54
- // Resolve AWS credentials the same way mesh dev does
55
- const deployerRole = readStackConfig(appRoot, stack, "mesh:deployerRole");
56
- const awsCreds = deployerRole
57
- ? (await resolveAwsCredentials(deployerRole, appRoot, stack))?.env
58
- : undefined;
59
- // Pulumi's `app` output from env.register() is a struct containing a
60
- // secret-tainted `dev` field (secret because service env vars include values
61
- // like TEMPORAL_ENCODING_KEY). pulumiStackOutput transparently retries with
62
- // --show-secrets when it sees a `"[secret]"` placeholder, so we can reach
63
- // the non-sensitive tunnel/namespace fields nested inside.
64
- let devOutput;
65
- let appOutput;
66
- try {
67
- const result = pulumiStackOutput(appRoot, "app", sa, awsCreds);
68
- appOutput = JSON.parse(result);
69
- devOutput = appOutput.dev ?? appOutput;
70
- }
71
- catch {
72
- // Legacy stacks that exported `dev` directly (pre-env.register() format).
73
- try {
74
- const result = pulumiStackOutput(appRoot, "dev", sa, awsCreds);
75
- devOutput = JSON.parse(result);
76
- appOutput = devOutput;
77
- }
78
- catch {
79
- throw new Error(`Could not read stack output for '${stack}'.\n` +
80
- "Ensure you've run: mesh deploy up\n" +
81
- "Or provide --address and --namespace explicitly.");
82
- }
83
- }
84
- const tunnel = devOutput?.tunnels?.temporal;
85
- // namespace lives at the app output level, not inside dev
86
- const namespace = options.namespace ?? appOutput?.namespace ?? devOutput?.namespace;
87
- if (!tunnel) {
88
- throw new Error("No temporal tunnel found in stack output.");
89
- }
90
- if (!namespace) {
91
- throw new Error("No temporal namespace found in stack output.");
92
- }
93
- // Auth context for minting a Bearer, mirroring resolveTemporalAuthVars in dev.ts:
94
- // tenant/env/name come from the PLATFORM block (platformName is the platform's
95
- // name, not the app tenant — it keys the Zitadel issuer lookup).
96
- const platform = (devOutput?.platform ?? appOutput?.platform);
97
- const auth = platform
98
- ? {
99
- tenant: platform.tenant ?? "mesh",
100
- env: platform.env ?? "dev",
101
- platformName: platform.name ?? platform.tenant ?? "mesh",
102
- }
103
- : undefined;
104
- return {
105
- address: options.address ?? `${tunnel.host}:${tunnel.port}`,
106
- namespace,
107
- auth,
108
- };
109
- }
110
- /**
111
- * Mint a Zitadel Bearer for the connection, or return undefined to connect
112
- * unauthenticated (correct for local/non-enforcing Temporal). Prefers ambient
113
- * env creds (e.g. exported by `mesh dev`); otherwise resolves the app's M2M
114
- * credentials from SSM via the auth context.
115
- */
116
- async function resolveBearerToken(auth) {
117
- try {
118
- const haveEnvCreds = !!process.env.TEMPORAL_AUTH_CLIENT_ID && !!process.env.ZITADEL_ISSUER;
119
- const vars = !haveEnvCreds && auth
120
- ? await resolveTemporalAuth(auth.tenant, auth.env, auth.platformName)
121
- : {};
122
- return await acquireTemporalBearer(vars);
123
- }
124
- catch (err) {
125
- logWarn(`Temporal auth unavailable (${err instanceof Error ? err.message : String(err)}); ` +
126
- `connecting unauthenticated`);
127
- return undefined;
128
- }
129
- }
130
- // ── Event type names ─────────────────────────────────────────────────────────
131
- // Temporal event type enum → human-readable name
132
- const EVENT_TYPE_NAMES = {
133
- 1: "WorkflowExecutionStarted",
134
- 2: "WorkflowExecutionCompleted",
135
- 3: "WorkflowExecutionFailed",
136
- 4: "WorkflowExecutionTimedOut",
137
- 5: "WorkflowTaskScheduled",
138
- 6: "WorkflowTaskStarted",
139
- 7: "WorkflowTaskCompleted",
140
- 8: "WorkflowTaskTimedOut",
141
- 9: "WorkflowTaskFailed",
142
- 10: "ActivityTaskScheduled",
143
- 11: "ActivityTaskStarted",
144
- 12: "ActivityTaskCompleted",
145
- 13: "ActivityTaskFailed",
146
- 14: "ActivityTaskTimedOut",
147
- 15: "ActivityTaskCancelRequested",
148
- 16: "ActivityTaskCanceled",
149
- 17: "TimerStarted",
150
- 18: "TimerFired",
151
- 19: "TimerCanceled",
152
- 20: "WorkflowExecutionCancelRequested",
153
- 21: "WorkflowExecutionCanceled",
154
- 24: "WorkflowExecutionSignaled", // not 22/23 which were removed
155
- 25: "WorkflowExecutionTerminated",
156
- 26: "WorkflowExecutionSignaled",
157
- 29: "WorkflowPropertiesModified",
158
- 40: "ChildWorkflowExecutionStarted",
159
- 41: "ChildWorkflowExecutionCompleted",
160
- 42: "ChildWorkflowExecutionFailed",
161
- 43: "ChildWorkflowExecutionCanceled",
162
- 44: "ChildWorkflowExecutionTimedOut",
163
- 45: "ChildWorkflowExecutionTerminated",
164
- 46: "WorkflowPropertiesModified",
165
- 48: "NexusOperationScheduled",
166
- 49: "NexusOperationStarted",
167
- 50: "NexusOperationCompleted",
168
- 51: "NexusOperationFailed",
169
- 52: "NexusOperationCanceled",
170
- 53: "NexusOperationTimedOut",
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
171
5
  };
172
- // Workflow status enum name
173
- const STATUS_NAMES = {
174
- 0: "UNSPECIFIED",
175
- 1: "RUNNING",
176
- 2: "COMPLETED",
177
- 3: "FAILED",
178
- 4: "CANCELED",
179
- 5: "TERMINATED",
180
- 6: "CONTINUED_AS_NEW",
181
- 7: "TIMED_OUT",
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
182
9
  };
183
- // ── Commands ─────────────────────────────────────────────────────────────────
184
- async function describeWorkflow(workflowId, runId, options) {
185
- const conn = await connect(options);
186
- try {
187
- const desc = await conn.connection.workflowService.describeWorkflowExecution({
188
- namespace: conn.namespace,
189
- execution: { workflowId, runId },
190
- });
191
- const info = desc.workflowExecutionInfo;
192
- if (!info) {
193
- logError("No workflow execution info returned.");
194
- return;
195
- }
196
- console.log(`Workflow: ${workflowId}`);
197
- console.log(`Run ID: ${info.execution?.runId ?? runId ?? "?"}`);
198
- console.log(`Type: ${info.type?.name ?? "?"}`);
199
- console.log(`Status: ${STATUS_NAMES[info.status ?? 0] ?? info.status}`);
200
- console.log(`Task Queue: ${info.taskQueue ?? "?"}`);
201
- console.log(`Namespace: ${conn.namespace}`);
202
- // Pending activities
203
- const pending = desc.pendingActivities ?? [];
204
- if (pending.length > 0) {
205
- console.log(`\nPending Activities (${pending.length}):`);
206
- for (const pa of pending) {
207
- console.log(` - ${pa.activityType?.name ?? "?"} (attempt ${pa.attempt}, state: ${pa.state})`);
208
- }
209
- }
210
- // Pending nexus operations
211
- const pendingNexus = desc.pendingNexusOperations ?? [];
212
- if (pendingNexus.length > 0) {
213
- console.log(`\nPending Nexus Operations (${pendingNexus.length}):`);
214
- for (const pn of pendingNexus) {
215
- console.log(` - ${pn.operation ?? "?"} → ${pn.endpoint ?? "?"} (state: ${pn.state})`);
216
- }
217
- }
10
+
11
+ // libs/mesh-cli/src/utils/log.ts
12
+ import chalk from "chalk";
13
+ function logPrefix(opts) {
14
+ const enabled = opts.envFlag === "1" || opts.envFlag !== "0" && !opts.isTTY;
15
+ return enabled ? `[${opts.now.toISOString().slice(11, 19)}] ` : "";
16
+ }
17
+ function prefix() {
18
+ return chalk.dim(
19
+ logPrefix({
20
+ isTTY: !!process.stderr.isTTY,
21
+ envFlag: process.env.MESH_LOG_TIMESTAMPS,
22
+ now: /* @__PURE__ */ new Date()
23
+ })
24
+ );
25
+ }
26
+ function logInfo(message) {
27
+ console.error(prefix() + chalk.blue("\u2139"), message);
28
+ }
29
+ function logSuccess(message) {
30
+ console.error(prefix() + chalk.green("\u2713"), message);
31
+ }
32
+ function logWarn(message) {
33
+ console.error(prefix() + chalk.yellow("\u26A0"), message);
34
+ }
35
+ function logError(message) {
36
+ console.error(prefix() + chalk.red("\u2717"), message);
37
+ }
38
+ var init_log = __esm({
39
+ "libs/mesh-cli/src/utils/log.ts"() {
40
+ "use strict";
41
+ }
42
+ });
43
+
44
+ // libs/mesh-cli/src/utils/pulumi.ts
45
+ import { execFileSync } from "child_process";
46
+ import * as path from "path";
47
+ import * as fs from "fs";
48
+ function findAppRoot(startDir) {
49
+ let dir = startDir;
50
+ while (true) {
51
+ if (fs.existsSync(path.join(dir, "Pulumi.yaml"))) return dir;
52
+ const parent = path.dirname(dir);
53
+ if (parent === dir) return null;
54
+ dir = parent;
55
+ }
56
+ }
57
+ function findStackConfigs(appRoot) {
58
+ return fs.readdirSync(appRoot).filter((f) => /^Pulumi\..+\.yaml$/.test(f) && f !== "Pulumi.yaml").map((f) => f.replace(/^Pulumi\./, "").replace(/\.yaml$/, ""));
59
+ }
60
+ function getCurrentStack(appRoot) {
61
+ try {
62
+ const result = execFileSync("pulumi", ["stack", "--show-name"], {
63
+ encoding: "utf-8",
64
+ cwd: appRoot,
65
+ stdio: ["pipe", "pipe", "pipe"]
66
+ });
67
+ return result.trim() || null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ function readStackConfig(appRoot, stack, key) {
73
+ const configFile = path.join(appRoot, `Pulumi.${stack}.yaml`);
74
+ if (!fs.existsSync(configFile)) return null;
75
+ const content = fs.readFileSync(configFile, "utf-8");
76
+ const pattern = new RegExp(`^\\s+${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*(.+)$`, "m");
77
+ const match = content.match(pattern);
78
+ if (!match) return null;
79
+ return match[1].trim().replace(/^["']|["']$/g, "");
80
+ }
81
+ function pulumiStackOutput(appRoot, key, extraArgs, env) {
82
+ const execEnv = env ? { ...process.env, ...env } : void 0;
83
+ try {
84
+ const result = execFileSync(
85
+ "pulumi",
86
+ ["stack", "output", key, "--json", ...extraArgs],
87
+ { cwd: appRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: execEnv }
88
+ );
89
+ if (result.includes('"[secret]"')) {
90
+ return execFileSync(
91
+ "pulumi",
92
+ ["stack", "output", key, "--json", "--show-secrets", ...extraArgs],
93
+ { cwd: appRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: execEnv }
94
+ );
218
95
  }
219
- finally {
220
- conn.connection.close();
96
+ return result;
97
+ } catch (err) {
98
+ const errMsg = err?.stderr ?? "";
99
+ if (errMsg.includes("kms:") || errMsg.includes("KMS") || errMsg.includes("secrets manager")) {
100
+ throw err;
221
101
  }
102
+ return execFileSync(
103
+ "pulumi",
104
+ ["stack", "output", key, "--json", "--show-secrets", ...extraArgs],
105
+ { cwd: appRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: execEnv }
106
+ );
107
+ }
222
108
  }
223
- /**
224
- * Page through a workflow's execution history, yielding each event in order.
225
- * Shared by `history` (existing-history phase) and `recover-conversation` so the
226
- * pagination logic lives in one place. Follow-mode long-polling is separate
227
- * (it uses `waitNewEvent` + terminal detection) and stays in `showHistory`.
228
- */
229
- async function* iterHistoryEvents(workflowService, namespace, workflowId, runId, maximumPageSize = 100) {
230
- let nextPageToken;
231
- do {
232
- const resp = await workflowService.getWorkflowExecutionHistory({
233
- namespace,
234
- execution: { workflowId, runId },
235
- maximumPageSize,
236
- nextPageToken,
237
- });
238
- for (const event of resp.history?.events ?? []) {
239
- yield event;
240
- }
241
- nextPageToken = resp.nextPageToken?.length ? resp.nextPageToken : undefined;
242
- } while (nextPageToken);
109
+ var init_pulumi = __esm({
110
+ "libs/mesh-cli/src/utils/pulumi.ts"() {
111
+ "use strict";
112
+ }
113
+ });
114
+
115
+ // libs/mesh-cli/src/utils/auth-preflight.ts
116
+ function isExpiredAwsTokenMessage(message) {
117
+ return /expired|invalid.*(security )?token|token.*(is )?invalid|no credential|could not load credentials|could not be found|sso session/i.test(
118
+ message
119
+ );
243
120
  }
244
- async function showHistory(workflowId, runId, options) {
245
- const conn = await connect(options);
246
- const maxEvents = options.follow ? Infinity : parseInt(options.limit ?? "200", 10);
247
- // Compact mode skips WorkflowTask* events (scheduling noise)
248
- const skipTypes = new Set([5, 6, 7, 8, 9]); // WorkflowTask*
249
- // Resolve the per-app encoding key lazily — only when the caller asks for
250
- // payload bodies. Failure to resolve is non-fatal: we print the history
251
- // without payloads and the warning from buildDecrypterForNamespace tells
252
- // the user why.
253
- const decrypter = options.showPayloads
254
- ? await buildDecrypterForNamespace(conn.namespace)
255
- : undefined;
121
+ var REGISTRY_CONTEXT, REGISTRY_LOGIN_FIX, SSO_LOGIN_FIX;
122
+ var init_auth_preflight = __esm({
123
+ "libs/mesh-cli/src/utils/auth-preflight.ts"() {
124
+ "use strict";
125
+ REGISTRY_CONTEXT = "mesh.dev";
126
+ REGISTRY_LOGIN_FIX = `mesh registry login ${REGISTRY_CONTEXT}`;
127
+ SSO_LOGIN_FIX = "aws sso login --sso-session=mesh # or: pnpm sso";
128
+ }
129
+ });
130
+
131
+ // libs/mesh-cli/src/utils/first-party-contexts.ts
132
+ function firstPartyDomainFor(context) {
133
+ if (!Object.hasOwn(FIRST_PARTY_CONTEXTS, context)) return null;
134
+ const domain = FIRST_PARTY_CONTEXTS[context];
135
+ return typeof domain === "string" && domain ? domain : null;
136
+ }
137
+ var FIRST_PARTY_CONTEXTS;
138
+ var init_first_party_contexts = __esm({
139
+ "libs/mesh-cli/src/utils/first-party-contexts.ts"() {
140
+ "use strict";
141
+ FIRST_PARTY_CONTEXTS = Object.freeze({
142
+ // Verified 2026-08-14: https://cli.dev.platform.meshtech.io/.well-known/mesh.json
143
+ // answers with the platform's issuer + CLI client id + registryBroker,
144
+ // anonymously. (Repointed from dev.trabian.meshtech.io — the trabian tenant
145
+ // platform lives in a different AWS account with no registry; the
146
+ // platform-dev stack in the registry-owning account is the developer front
147
+ // door. mesh-sandbox#32 / #3587.)
148
+ "mesh.dev": "dev.platform.meshtech.io"
149
+ });
150
+ }
151
+ });
152
+
153
+ // libs/mesh-cli/src/utils/errors.ts
154
+ import chalk2 from "chalk";
155
+ var init_errors = __esm({
156
+ "libs/mesh-cli/src/utils/errors.ts"() {
157
+ "use strict";
158
+ }
159
+ });
160
+
161
+ // libs/mesh-cli/src/utils/context.ts
162
+ var init_context = __esm({
163
+ "libs/mesh-cli/src/utils/context.ts"() {
164
+ "use strict";
165
+ init_log();
166
+ }
167
+ });
168
+
169
+ // libs/mesh-cli/src/utils/bastion.ts
170
+ import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
171
+ var init_bastion = __esm({
172
+ "libs/mesh-cli/src/utils/bastion.ts"() {
173
+ "use strict";
174
+ init_log();
175
+ }
176
+ });
177
+
178
+ // libs/mesh-cli/src/utils/credentials.ts
179
+ import {
180
+ SecretsManagerClient,
181
+ GetSecretValueCommand
182
+ } from "@aws-sdk/client-secrets-manager";
183
+ var init_credentials = __esm({
184
+ "libs/mesh-cli/src/utils/credentials.ts"() {
185
+ "use strict";
186
+ init_log();
187
+ }
188
+ });
189
+
190
+ // libs/mesh-cli/src/utils/vpn.ts
191
+ var init_vpn = __esm({
192
+ "libs/mesh-cli/src/utils/vpn.ts"() {
193
+ "use strict";
194
+ }
195
+ });
196
+
197
+ // libs/mesh-cli/src/utils/index.ts
198
+ var init_utils = __esm({
199
+ "libs/mesh-cli/src/utils/index.ts"() {
200
+ "use strict";
201
+ init_errors();
202
+ init_log();
203
+ init_context();
204
+ init_bastion();
205
+ init_credentials();
206
+ init_pulumi();
207
+ init_vpn();
208
+ }
209
+ });
210
+
211
+ // libs/mesh-cli/src/utils/pid.ts
212
+ var init_pid = __esm({
213
+ "libs/mesh-cli/src/utils/pid.ts"() {
214
+ "use strict";
215
+ init_errors();
216
+ }
217
+ });
218
+
219
+ // libs/mesh-cli/src/utils/vpn-join.ts
220
+ var init_vpn_join = __esm({
221
+ "libs/mesh-cli/src/utils/vpn-join.ts"() {
222
+ "use strict";
223
+ init_login();
224
+ }
225
+ });
226
+
227
+ // libs/mesh-cli/src/utils/pulumi-run.ts
228
+ var init_pulumi_run = __esm({
229
+ "libs/mesh-cli/src/utils/pulumi-run.ts"() {
230
+ "use strict";
231
+ init_log();
232
+ init_pulumi();
233
+ init_aws_auth();
234
+ init_login();
235
+ }
236
+ });
237
+
238
+ // libs/mesh-cli/src/utils/kubeconfig.ts
239
+ var init_kubeconfig = __esm({
240
+ "libs/mesh-cli/src/utils/kubeconfig.ts"() {
241
+ "use strict";
242
+ }
243
+ });
244
+
245
+ // libs/mesh-cli/src/utils/temporal-auth.ts
246
+ import { execFileSync as execFileSync2 } from "node:child_process";
247
+ async function resolveTemporalAuth(tenant, env, platformName = tenant) {
248
+ const { SSMClient: SSMClient2, GetParameterCommand: GetParameterCommand2 } = await import("@aws-sdk/client-ssm");
249
+ const ssm = new SSMClient2({ region: process.env.AWS_REGION || "us-east-2" });
250
+ const results = {};
251
+ async function trySSM(name) {
256
252
  try {
257
- let totalShown = 0;
258
- let lastEventId = 0;
259
- console.log(`History for ${workflowId} (namespace: ${conn.namespace})${options.follow ? " [following]" : ""}\n`);
260
- // Fetch existing history first
261
- for await (const event of iterHistoryEvents(conn.connection.workflowService, conn.namespace, workflowId, runId)) {
262
- lastEventId = Number(event.eventId);
263
- const eventType = event.eventType ?? 0;
264
- if (options.compact !== false && skipTypes.has(eventType))
265
- continue;
266
- await printEvent(event, decrypter);
267
- totalShown++;
268
- if (totalShown >= maxEvents)
269
- break;
270
- }
271
- if (!options.follow) {
272
- console.log(`\n${totalShown} events shown.`);
273
- return;
274
- }
275
- // Follow mode: long-poll for new events until workflow completes
276
- const terminalTypes = new Set([2, 3, 4, 21, 27, 28]); // Completed, Failed, TimedOut, Canceled, Terminated, ContinuedAsNew
277
- let done = false;
278
- while (!done) {
279
- try {
280
- const resp = await conn.connection.workflowService.getWorkflowExecutionHistory({
281
- namespace: conn.namespace,
282
- execution: { workflowId, runId },
283
- maximumPageSize: 100,
284
- waitNewEvent: true,
285
- // Start after the last event we've seen
286
- ...(lastEventId > 0 ? { skipArchival: true } : {}),
287
- });
288
- for (const event of resp.history?.events ?? []) {
289
- const eid = Number(event.eventId);
290
- if (eid <= lastEventId)
291
- continue; // already shown
292
- lastEventId = eid;
293
- const eventType = event.eventType ?? 0;
294
- if (options.compact !== false && skipTypes.has(eventType))
295
- continue;
296
- await printEvent(event, decrypter);
297
- totalShown++;
298
- // Check if workflow reached a terminal state
299
- if (terminalTypes.has(eventType)) {
300
- done = true;
301
- }
302
- }
253
+ const resp = await ssm.send(new GetParameterCommand2({ Name: name, WithDecryption: true }));
254
+ return resp.Parameter?.Value ?? void 0;
255
+ } catch {
256
+ return void 0;
257
+ }
258
+ }
259
+ try {
260
+ const workerPath = `/mesh-platform/${tenant}/${env}/temporal/worker-service-account-key`;
261
+ const exchangePath = `/mesh-platform/${tenant}/${env}/temporal/token-exchange-app`;
262
+ const workerJson = await trySSM(workerPath);
263
+ if (workerJson) {
264
+ const worker = JSON.parse(workerJson);
265
+ results.TEMPORAL_AUTH_CLIENT_ID = worker.clientId;
266
+ results.TEMPORAL_AUTH_CLIENT_SECRET = worker.clientSecret;
267
+ }
268
+ const exchangeJson = await trySSM(exchangePath);
269
+ if (exchangeJson) {
270
+ const exchange = JSON.parse(exchangeJson);
271
+ results.ZITADEL_TOKEN_EXCHANGE_CLIENT_ID = exchange.clientId;
272
+ results.ZITADEL_TOKEN_EXCHANGE_CLIENT_SECRET = exchange.clientSecret;
273
+ }
274
+ const platformJson = await trySSM(`/mesh-platform/${platformName}/${env}/platform/zitadel`);
275
+ if (platformJson) {
276
+ const platform = JSON.parse(platformJson);
277
+ if (platform.endpoint) results.ZITADEL_ISSUER = platform.endpoint;
278
+ if (platform.projectId) results.ZITADEL_PROJECT_ID = platform.projectId;
279
+ }
280
+ if (!results.TEMPORAL_AUTH_CLIENT_ID) {
281
+ const legacyClientId = await trySSM("/platform/temporal/auth/CLIENT_ID");
282
+ const legacyClientSecret = await trySSM("/platform/temporal/auth/CLIENT_SECRET");
283
+ if (legacyClientId && legacyClientSecret) {
284
+ results.TEMPORAL_AUTH_CLIENT_ID = legacyClientId;
285
+ results.TEMPORAL_AUTH_CLIENT_SECRET = legacyClientSecret;
286
+ }
287
+ }
288
+ if (!results.ZITADEL_ISSUER) {
289
+ try {
290
+ const podEnv = execFileSync2(
291
+ "kubectl",
292
+ [
293
+ "get",
294
+ "pods",
295
+ "-n",
296
+ `temporal-${env}`,
297
+ "-l",
298
+ "app.kubernetes.io/component=frontend",
299
+ "-o",
300
+ "jsonpath={.items[0].spec.containers[0].env}"
301
+ ],
302
+ { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
303
+ ).trim();
304
+ if (podEnv) {
305
+ const envVars = JSON.parse(podEnv);
306
+ for (const v of envVars) {
307
+ if (v.name === "ZITADEL_ISSUER_URL" && v.value && !results.ZITADEL_ISSUER) {
308
+ results.ZITADEL_ISSUER = v.value;
303
309
  }
304
- catch (err) {
305
- // Connection reset or workflow completed between polls
306
- if (err?.code === 5 /* NOT_FOUND */ || err?.message?.includes("not found")) {
307
- console.log("\nWorkflow completed or not found.");
308
- done = true;
309
- }
310
- else {
311
- throw err;
312
- }
310
+ if (v.name === "ZITADEL_PROJECT_ID" && v.value && !results.ZITADEL_PROJECT_ID) {
311
+ results.ZITADEL_PROJECT_ID = v.value;
313
312
  }
313
+ }
314
314
  }
315
- console.log(`\n${totalShown} events shown (workflow ended).`);
315
+ } catch (err) {
316
+ const detail = err instanceof Error ? err.message : String(err);
317
+ const stderr = String(err?.stderr ?? "");
318
+ if (/\.kube[/\\]config|error loading config file/i.test(`${detail} ${stderr}`)) {
319
+ logWarn(
320
+ "kubectl could not read a kubeconfig and fell back to ~/.kube/config (macOS blocks it for shells without Full Disk Access). Run inside the mesh dev tmux session, or export KUBECONFIG=$TMPDIR/mesh-dev-sessions/<session>.kubeconfig."
321
+ );
322
+ }
323
+ }
316
324
  }
317
- finally {
318
- conn.connection.close();
325
+ if (results.TEMPORAL_AUTH_CLIENT_ID && results.ZITADEL_ISSUER) {
326
+ logSuccess(`Temporal auth resolved (client: ${results.TEMPORAL_AUTH_CLIENT_ID})`);
327
+ } else if (Object.keys(results).length > 0) {
328
+ logWarn("Partial Temporal auth resolved (some credentials missing)");
319
329
  }
330
+ } catch (err) {
331
+ logWarn(
332
+ `Could not resolve Temporal auth from SSM: ${err instanceof Error ? err.message : String(err)}`
333
+ );
334
+ }
335
+ return results;
320
336
  }
321
- async function printEvent(event, decrypter) {
322
- const eventType = event.eventType ?? 0;
323
- const typeName = EVENT_TYPE_NAMES[eventType] ?? `Unknown(${eventType})`;
324
- const id = String(event.eventId).padStart(4);
325
- const detail = extractEventDetail(event);
326
- const detailStr = detail ? ` — ${detail}` : "";
327
- console.log(`${id} ${typeName}${detailStr}`);
328
- if (!decrypter)
329
- return;
330
- // Print any named payload groups attached to this event — activity input/
331
- // output, signal input, workflow start args, etc. — decrypted and pretty-
332
- // printed under the header line with a 6-space indent so they align under
333
- // the event ID column.
334
- for (const group of extractEventPayloads(event)) {
335
- const rendered = await renderPayloads(group.payloads, decrypter);
336
- if (rendered === null)
337
- continue;
338
- console.log(` ${group.label}:`);
339
- for (const line of rendered.split("\n")) {
340
- console.log(` ${line}`);
341
- }
337
+ async function acquireTemporalBearer(vars = {}) {
338
+ const issuerUrl = vars.ZITADEL_ISSUER ?? process.env.ZITADEL_ISSUER;
339
+ const projectId = vars.ZITADEL_PROJECT_ID ?? process.env.ZITADEL_PROJECT_ID;
340
+ const clientId = vars.TEMPORAL_AUTH_CLIENT_ID ?? process.env.TEMPORAL_AUTH_CLIENT_ID;
341
+ const clientSecret = vars.TEMPORAL_AUTH_CLIENT_SECRET ?? process.env.TEMPORAL_AUTH_CLIENT_SECRET;
342
+ if (!issuerUrl || !clientId || !clientSecret) {
343
+ return void 0;
344
+ }
345
+ const scopes = ["openid"];
346
+ if (projectId) {
347
+ scopes.push(`urn:zitadel:iam:org:project:id:${projectId}:aud`);
348
+ if (process.env.TEMPORAL_M2M_SKIP_ROLES_SCOPE !== "true") {
349
+ scopes.push("urn:zitadel:iam:org:projects:roles");
342
350
  }
351
+ }
352
+ const response = await fetch(`${issuerUrl}/oauth/v2/token`, {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
355
+ body: new URLSearchParams({
356
+ grant_type: "client_credentials",
357
+ client_id: clientId,
358
+ client_secret: clientSecret,
359
+ scope: scopes.join(" ")
360
+ })
361
+ });
362
+ if (!response.ok) {
363
+ const text = await response.text();
364
+ throw new Error(
365
+ `Temporal token acquisition failed: ${response.status} ${response.statusText} \u2014 ${text}`
366
+ );
367
+ }
368
+ const data = await response.json();
369
+ return data.access_token;
343
370
  }
344
- /**
345
- * Pick out the payload-bearing fields on a history event. Returns an empty
346
- * array for events that carry no interesting data (workflow task lifecycle,
347
- * timers, property modifications, etc.).
348
- */
349
- function extractEventPayloads(event) {
350
- const groups = [];
351
- const pushIfAny = (label, payloads) => {
352
- if (payloads && payloads.length > 0)
353
- groups.push({ label, payloads });
354
- };
355
- if (event.workflowExecutionStartedEventAttributes) {
356
- pushIfAny("input", event.workflowExecutionStartedEventAttributes.input?.payloads);
357
- }
358
- if (event.workflowExecutionCompletedEventAttributes) {
359
- pushIfAny("result", event.workflowExecutionCompletedEventAttributes.result?.payloads);
360
- }
361
- if (event.workflowExecutionFailedEventAttributes) {
362
- const failure = event.workflowExecutionFailedEventAttributes.failure;
363
- if (failure?.message)
364
- groups.push({ label: "failure", payloads: [{ __failure: failure }] });
365
- }
366
- if (event.workflowExecutionSignaledEventAttributes) {
367
- pushIfAny("signal input", event.workflowExecutionSignaledEventAttributes.input?.payloads);
368
- }
369
- if (event.activityTaskScheduledEventAttributes) {
370
- pushIfAny("input", event.activityTaskScheduledEventAttributes.input?.payloads);
371
- }
372
- if (event.activityTaskCompletedEventAttributes) {
373
- pushIfAny("result", event.activityTaskCompletedEventAttributes.result?.payloads);
374
- }
375
- if (event.activityTaskFailedEventAttributes) {
376
- const failure = event.activityTaskFailedEventAttributes.failure;
377
- if (failure?.message)
378
- groups.push({ label: "failure", payloads: [{ __failure: failure }] });
379
- }
380
- if (event.nexusOperationScheduledEventAttributes) {
381
- const input = event.nexusOperationScheduledEventAttributes.input;
382
- if (input)
383
- pushIfAny("input", [input]);
384
- }
385
- if (event.nexusOperationCompletedEventAttributes) {
386
- const result = event.nexusOperationCompletedEventAttributes.result;
387
- if (result)
388
- pushIfAny("result", [result]);
389
- }
390
- if (event.nexusOperationFailedEventAttributes) {
391
- const failure = event.nexusOperationFailedEventAttributes.failure;
392
- if (failure?.message)
393
- groups.push({ label: "failure", payloads: [{ __failure: failure }] });
394
- }
395
- return groups;
396
- }
397
- /**
398
- * Render one or more payloads into a multi-line string, or return `null`
399
- * when nothing rendered (e.g. all payloads were non-encrypted and empty).
400
- *
401
- * Failures (`{ __failure: ... }`) are rendered specially since they're a
402
- * structured error, not an encrypted payload.
403
- */
404
- async function renderPayloads(payloads, decrypter) {
405
- const parts = [];
406
- for (const payload of payloads) {
407
- if (payload && typeof payload === "object" && "__failure" in payload) {
408
- const failure = payload.__failure;
409
- parts.push(renderFailure(failure));
410
- continue;
411
- }
412
- const decoded = await decrypter(payload);
413
- if (decoded === null) {
414
- parts.push("[non-encrypted payload]");
415
- }
416
- else {
417
- parts.push(decoded);
418
- }
371
+ var init_temporal_auth = __esm({
372
+ "libs/mesh-cli/src/utils/temporal-auth.ts"() {
373
+ "use strict";
374
+ init_log();
375
+ }
376
+ });
377
+
378
+ // libs/mesh-cli/src/utils/reachability.ts
379
+ var init_reachability = __esm({
380
+ "libs/mesh-cli/src/utils/reachability.ts"() {
381
+ "use strict";
382
+ }
383
+ });
384
+
385
+ // libs/mesh-cli/src/utils/workflow-fingerprint.ts
386
+ var init_workflow_fingerprint = __esm({
387
+ "libs/mesh-cli/src/utils/workflow-fingerprint.ts"() {
388
+ "use strict";
389
+ }
390
+ });
391
+
392
+ // libs/mesh-cli/src/commands/dev-launch.ts
393
+ var init_dev_launch = __esm({
394
+ "libs/mesh-cli/src/commands/dev-launch.ts"() {
395
+ "use strict";
396
+ }
397
+ });
398
+
399
+ // libs/mesh-cli/src/commands/local/seed.ts
400
+ var LOCAL_TENANT, LOCAL_ENV, APP_TENANTS_PARAM, FABRIC_CHECK_PATH, TEMPORAL_NAMESPACE;
401
+ var init_seed = __esm({
402
+ "libs/mesh-cli/src/commands/local/seed.ts"() {
403
+ "use strict";
404
+ init_log();
405
+ init_errors();
406
+ LOCAL_TENANT = "local";
407
+ LOCAL_ENV = "dev";
408
+ APP_TENANTS_PARAM = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/app-tenants`;
409
+ FABRIC_CHECK_PATH = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/.fabric-check`;
410
+ TEMPORAL_NAMESPACE = `${LOCAL_TENANT}-${LOCAL_ENV}`;
411
+ }
412
+ });
413
+
414
+ // libs/mesh-cli/src/commands/local/helpers.ts
415
+ var init_helpers = __esm({
416
+ "libs/mesh-cli/src/commands/local/helpers.ts"() {
417
+ "use strict";
418
+ init_seed();
419
+ }
420
+ });
421
+
422
+ // libs/mesh-cli/src/utils/cache-home.ts
423
+ var init_cache_home = __esm({
424
+ "libs/mesh-cli/src/utils/cache-home.ts"() {
425
+ "use strict";
426
+ }
427
+ });
428
+
429
+ // libs/mesh-cli/src/commands/local/stack.ts
430
+ import { parse as parseYaml } from "yaml";
431
+ var DEFAULT_HUB_PORT;
432
+ var init_stack = __esm({
433
+ "libs/mesh-cli/src/commands/local/stack.ts"() {
434
+ "use strict";
435
+ init_helpers();
436
+ init_cache_home();
437
+ init_errors();
438
+ init_log();
439
+ DEFAULT_HUB_PORT = "9000";
440
+ }
441
+ });
442
+
443
+ // libs/mesh-cli/src/commands/local/mocks.ts
444
+ var init_mocks = __esm({
445
+ "libs/mesh-cli/src/commands/local/mocks.ts"() {
446
+ "use strict";
447
+ init_log();
448
+ init_errors();
449
+ init_seed();
450
+ init_helpers();
451
+ init_stack();
452
+ }
453
+ });
454
+
455
+ // libs/api-registry/src/hub-roles.ts
456
+ function hubOperatorRoleKeys(tenants) {
457
+ const cleaned = [...new Set(tenants.filter((t) => t !== ""))].sort();
458
+ for (const tenant of cleaned) {
459
+ for (const sep of [HUB_ROLE_KEY_SEPARATOR, HUB_ROLE_APP_SEPARATOR]) {
460
+ if (tenant.includes(sep)) {
461
+ throw new Error(
462
+ `hubOperatorRoleKeys: tenant name "${tenant}" contains the role-key separator "${sep}" \u2014 it would parse as a different tenant's or app's grant`
463
+ );
464
+ }
419
465
  }
420
- return parts.length > 0 ? parts.join("\n---\n") : null;
466
+ }
467
+ return [
468
+ ...HUB_BASE_ROLES,
469
+ ...cleaned.flatMap(
470
+ (tenant) => HUB_RESTRICTED_ROLES.map((role) => `${role}${HUB_ROLE_KEY_SEPARATOR}${tenant}`)
471
+ )
472
+ ];
421
473
  }
422
- function renderFailure(failure) {
423
- const lines = [failure.message ?? "(no message)"];
424
- if (failure.stackTrace)
425
- lines.push(failure.stackTrace);
426
- return lines.join("\n");
474
+ var HUB_STAFF_ROLES, HUB_RESTRICTED_ROLES, HUB_BASE_ROLES, HUB_ROLE_KEY_SEPARATOR, HUB_ROLE_APP_SEPARATOR, RESTRICTED;
475
+ var init_hub_roles = __esm({
476
+ "libs/api-registry/src/hub-roles.ts"() {
477
+ "use strict";
478
+ HUB_STAFF_ROLES = ["ops", "admin"];
479
+ HUB_RESTRICTED_ROLES = ["developer", "auditor", "vendor"];
480
+ HUB_BASE_ROLES = [
481
+ ...HUB_STAFF_ROLES,
482
+ ...HUB_RESTRICTED_ROLES
483
+ ];
484
+ HUB_ROLE_KEY_SEPARATOR = ":";
485
+ HUB_ROLE_APP_SEPARATOR = "/";
486
+ RESTRICTED = new Set(HUB_RESTRICTED_ROLES);
487
+ }
488
+ });
489
+
490
+ // libs/api-registry/src/index.ts
491
+ import { z } from "zod";
492
+ var rateLimitSpecSchema, rateLimitDefaultsSchema, integrationHealthSchema, apiSurfaceSchema, apiRegistryEntrySchema, integrationStatusSchema;
493
+ var init_src = __esm({
494
+ "libs/api-registry/src/index.ts"() {
495
+ "use strict";
496
+ init_hub_roles();
497
+ rateLimitSpecSchema = z.object({
498
+ rps: z.number().int().min(1),
499
+ burst: z.number().int().min(1)
500
+ });
501
+ rateLimitDefaultsSchema = z.record(z.string().min(1), rateLimitSpecSchema);
502
+ integrationHealthSchema = z.union([
503
+ z.object({ op: z.string().trim().min(1) }),
504
+ z.object({ unavailable: z.string().trim().min(1) })
505
+ ]);
506
+ apiSurfaceSchema = z.object({
507
+ http: z.object({ url: z.string().min(1) }).optional(),
508
+ nexus: z.object({ endpoint: z.string().min(1), taskQueue: z.string().min(1) }).optional()
509
+ });
510
+ apiRegistryEntrySchema = z.object({
511
+ schemaVersion: z.number().int().positive().default(1),
512
+ name: z.string().min(1),
513
+ provider: z.string().optional(),
514
+ version: z.string().optional(),
515
+ title: z.string().optional(),
516
+ description: z.string().optional(),
517
+ surfaces: apiSurfaceSchema,
518
+ credentials: z.object({ keyedBy: z.string().nullish() }).optional(),
519
+ docs: z.object({ url: z.string().min(1), contentHash: z.string().min(1) }),
520
+ enabledOps: z.array(z.string()),
521
+ // The consumer schema strips unknown keys, so a producer-side field that is
522
+ // not mirrored here never reaches a consumer — mirror every addition.
523
+ status: z.object({ url: z.string().min(1) }).optional(),
524
+ appVersion: z.string().min(1).optional(),
525
+ rateLimits: rateLimitDefaultsSchema.optional(),
526
+ health: integrationHealthSchema.optional(),
527
+ producedRateClasses: z.array(z.string().min(1)).optional()
528
+ });
529
+ integrationStatusSchema = z.object({
530
+ contract: z.literal("v1"),
531
+ name: z.string().min(1),
532
+ provider: z.string().optional(),
533
+ type: z.string().optional(),
534
+ title: z.string().optional(),
535
+ definitionVersion: z.string().optional(),
536
+ packageVersion: z.string().min(1).optional(),
537
+ tenant: z.string().min(1),
538
+ instanceKey: z.string().optional(),
539
+ surfaces: z.array(z.enum(["http", "nexus"])),
540
+ coreMode: z.enum(["mock", "live"]),
541
+ coreModeSource: z.string().optional(),
542
+ enabledOps: z.array(z.string()),
543
+ credentialsWired: z.boolean(),
544
+ rateLimits: rateLimitDefaultsSchema.optional(),
545
+ health: integrationHealthSchema.optional(),
546
+ startedAt: z.string().min(1)
547
+ });
548
+ }
549
+ });
550
+
551
+ // libs/mesh-cli/src/commands/local/seed-zitadel.ts
552
+ var HUB_DEFAULT_REDIRECT_URI, HUB_ROLES, ZITADEL_SSM_PARAM, TEST_USERS_SSM_PREFIX;
553
+ var init_seed_zitadel = __esm({
554
+ "libs/mesh-cli/src/commands/local/seed-zitadel.ts"() {
555
+ "use strict";
556
+ init_src();
557
+ init_log();
558
+ init_errors();
559
+ init_login();
560
+ init_stack();
561
+ init_seed();
562
+ init_cache_home();
563
+ HUB_DEFAULT_REDIRECT_URI = `http://localhost:${DEFAULT_HUB_PORT}/oauth2/callback`;
564
+ HUB_ROLES = hubOperatorRoleKeys([LOCAL_TENANT]);
565
+ ZITADEL_SSM_PARAM = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/platform/zitadel`;
566
+ TEST_USERS_SSM_PREFIX = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/temporal/test-users`;
567
+ }
568
+ });
569
+
570
+ // libs/mesh-cli/src/commands/local/auth-provision.ts
571
+ var init_auth_provision = __esm({
572
+ "libs/mesh-cli/src/commands/local/auth-provision.ts"() {
573
+ "use strict";
574
+ init_log();
575
+ init_seed();
576
+ init_seed_zitadel();
577
+ init_helpers();
578
+ }
579
+ });
580
+
581
+ // libs/mesh-cli/src/commands/local/dev-local.ts
582
+ var init_dev_local = __esm({
583
+ "libs/mesh-cli/src/commands/local/dev-local.ts"() {
584
+ "use strict";
585
+ init_mocks();
586
+ init_helpers();
587
+ init_errors();
588
+ init_seed();
589
+ init_seed();
590
+ init_seed_zitadel();
591
+ init_auth_provision();
592
+ init_stack();
593
+ init_login();
594
+ }
595
+ });
596
+
597
+ // libs/mesh-cli/src/commands/local/docker-runner.ts
598
+ var init_docker_runner = __esm({
599
+ "libs/mesh-cli/src/commands/local/docker-runner.ts"() {
600
+ "use strict";
601
+ init_dev_local();
602
+ init_cache_home();
603
+ }
604
+ });
605
+
606
+ // libs/mesh-cli/src/utils/stack-flag.ts
607
+ var init_stack_flag = __esm({
608
+ "libs/mesh-cli/src/utils/stack-flag.ts"() {
609
+ "use strict";
610
+ init_log();
611
+ }
612
+ });
613
+
614
+ // libs/mesh-cli/src/commands/peer-addressing.ts
615
+ var init_peer_addressing = __esm({
616
+ "libs/mesh-cli/src/commands/peer-addressing.ts"() {
617
+ "use strict";
618
+ }
619
+ });
620
+
621
+ // libs/mesh-cli/src/commands/dev-doctor.ts
622
+ var init_dev_doctor = __esm({
623
+ "libs/mesh-cli/src/commands/dev-doctor.ts"() {
624
+ "use strict";
625
+ init_dev();
626
+ init_auth_preflight();
627
+ init_login();
628
+ }
629
+ });
630
+
631
+ // libs/mesh-cli/src/utils/worktree-identity.ts
632
+ var init_worktree_identity = __esm({
633
+ "libs/mesh-cli/src/utils/worktree-identity.ts"() {
634
+ "use strict";
635
+ }
636
+ });
637
+
638
+ // libs/mesh-cli/src/commands/dev.ts
639
+ import { Option } from "commander";
640
+ import { SecretsManagerClient as SecretsManagerClient2, GetSecretValueCommand as GetSecretValueCommand2 } from "@aws-sdk/client-secrets-manager";
641
+ var init_dev = __esm({
642
+ "libs/mesh-cli/src/commands/dev.ts"() {
643
+ "use strict";
644
+ init_utils();
645
+ init_aws_auth();
646
+ init_login();
647
+ init_vpn_join();
648
+ init_pulumi_run();
649
+ init_tailscale();
650
+ init_pulumi();
651
+ init_kubeconfig();
652
+ init_temporal_auth();
653
+ init_reachability();
654
+ init_workflow_fingerprint();
655
+ init_dev_launch();
656
+ init_dev_local();
657
+ init_auth_provision();
658
+ init_seed();
659
+ init_mocks();
660
+ init_dev_local();
661
+ init_stack();
662
+ init_docker_runner();
663
+ init_errors();
664
+ init_stack_flag();
665
+ init_peer_addressing();
666
+ init_dev_doctor();
667
+ init_worktree_identity();
668
+ }
669
+ });
670
+
671
+ // libs/mesh-cli/src/utils/tailscale-targets.ts
672
+ var init_tailscale_targets = __esm({
673
+ "libs/mesh-cli/src/utils/tailscale-targets.ts"() {
674
+ "use strict";
675
+ init_dev();
676
+ init_login();
677
+ }
678
+ });
679
+
680
+ // libs/mesh-cli/src/utils/socks-forward.ts
681
+ var init_socks_forward = __esm({
682
+ "libs/mesh-cli/src/utils/socks-forward.ts"() {
683
+ "use strict";
684
+ }
685
+ });
686
+
687
+ // libs/mesh-cli/src/utils/tunnel-ownership.ts
688
+ var init_tunnel_ownership = __esm({
689
+ "libs/mesh-cli/src/utils/tunnel-ownership.ts"() {
690
+ "use strict";
691
+ }
692
+ });
693
+
694
+ // libs/mesh-cli/src/utils/tailscale.ts
695
+ var init_tailscale = __esm({
696
+ "libs/mesh-cli/src/utils/tailscale.ts"() {
697
+ "use strict";
698
+ init_login();
699
+ init_aws_auth();
700
+ init_log();
701
+ init_errors();
702
+ init_pid();
703
+ init_bastion();
704
+ init_tailscale_targets();
705
+ init_socks_forward();
706
+ init_tunnel_ownership();
707
+ }
708
+ });
709
+
710
+ // libs/mesh-cli/src/commands/vpn/tunnel.ts
711
+ var init_tunnel = __esm({
712
+ "libs/mesh-cli/src/commands/vpn/tunnel.ts"() {
713
+ "use strict";
714
+ init_log();
715
+ init_login();
716
+ init_tailscale();
717
+ }
718
+ });
719
+
720
+ // libs/mesh-cli/src/commands/vpn/index.ts
721
+ import { execFileSync as execFileSync3 } from "child_process";
722
+ function tailscaleStatus() {
723
+ try {
724
+ const json = execFileSync3("tailscale", ["status", "--json"], {
725
+ encoding: "utf-8",
726
+ stdio: ["pipe", "pipe", "pipe"]
727
+ });
728
+ return JSON.parse(json);
729
+ } catch {
730
+ return null;
731
+ }
427
732
  }
428
- /** Extract a human-readable detail string from event attributes. */
429
- function extractEventDetail(event) {
430
- // Activity scheduled
431
- if (event.activityTaskScheduledEventAttributes) {
432
- const a = event.activityTaskScheduledEventAttributes;
433
- return `activity: ${a.activityType?.name ?? "?"}, queue: ${a.taskQueue?.name ?? "?"}`;
434
- }
435
- // Activity completed
436
- if (event.activityTaskCompletedEventAttributes) {
437
- return `scheduledId: ${event.activityTaskCompletedEventAttributes.scheduledEventId}`;
438
- }
439
- // Activity failed
440
- if (event.activityTaskFailedEventAttributes) {
441
- const a = event.activityTaskFailedEventAttributes;
442
- const msg = a.failure?.message ?? "";
443
- return `scheduledId: ${a.scheduledEventId}${msg ? `, error: ${msg.slice(0, 120)}` : ""}`;
444
- }
445
- // Activity timed out
446
- if (event.activityTaskTimedOutEventAttributes) {
447
- return `scheduledId: ${event.activityTaskTimedOutEventAttributes.scheduledEventId}`;
448
- }
449
- // Nexus scheduled
450
- if (event.nexusOperationScheduledEventAttributes) {
451
- const a = event.nexusOperationScheduledEventAttributes;
452
- return `endpoint: ${a.endpoint ?? "?"}, operation: ${a.operation ?? "?"}, service: ${a.service ?? "?"}`;
453
- }
454
- // Nexus completed/failed/timed out
455
- if (event.nexusOperationCompletedEventAttributes) {
456
- return `scheduledId: ${event.nexusOperationCompletedEventAttributes.scheduledEventId}`;
457
- }
458
- if (event.nexusOperationFailedEventAttributes) {
459
- const a = event.nexusOperationFailedEventAttributes;
460
- return `scheduledId: ${a.scheduledEventId}, error: ${a.failure?.message?.slice(0, 120) ?? "?"}`;
461
- }
462
- if (event.nexusOperationTimedOutEventAttributes) {
463
- return `scheduledId: ${event.nexusOperationTimedOutEventAttributes.scheduledEventId}`;
464
- }
465
- // Signal
466
- if (event.workflowExecutionSignaledEventAttributes) {
467
- return `signal: ${event.workflowExecutionSignaledEventAttributes.signalName ?? "?"}`;
468
- }
469
- // Workflow started
470
- if (event.workflowExecutionStartedEventAttributes) {
471
- const a = event.workflowExecutionStartedEventAttributes;
472
- return `type: ${a.workflowType?.name ?? "?"}, queue: ${a.taskQueue?.name ?? "?"}`;
473
- }
474
- // Workflow completed/failed
475
- if (event.workflowExecutionCompletedEventAttributes)
476
- return null;
477
- if (event.workflowExecutionFailedEventAttributes) {
478
- return `error: ${event.workflowExecutionFailedEventAttributes.failure?.message?.slice(0, 120) ?? "?"}`;
733
+ function tailscaleBackendState() {
734
+ return tailscaleStatus()?.BackendState ?? null;
735
+ }
736
+ var init_vpn2 = __esm({
737
+ "libs/mesh-cli/src/commands/vpn/index.ts"() {
738
+ "use strict";
739
+ init_utils();
740
+ init_login();
741
+ init_tunnel();
742
+ init_tailscale();
743
+ }
744
+ });
745
+
746
+ // libs/mesh-cli/src/commands/login.ts
747
+ import * as http from "http";
748
+ import * as crypto from "crypto";
749
+ import * as fs2 from "fs";
750
+ import * as path2 from "path";
751
+ import { execFileSync as execFileSync4 } from "child_process";
752
+ function readConfig() {
753
+ if (!fs2.existsSync(CONFIG_FILE)) return {};
754
+ try {
755
+ return JSON.parse(fs2.readFileSync(CONFIG_FILE, "utf-8"));
756
+ } catch {
757
+ return {};
758
+ }
759
+ }
760
+ function writeContextConfig(context, config) {
761
+ const existing = readConfig();
762
+ existing[context] = config;
763
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
764
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
765
+ }
766
+ function getContextConfig(context) {
767
+ const config = readConfig();
768
+ return config[context] ?? null;
769
+ }
770
+ function parseTenantEnv(context) {
771
+ const parts = context.split(".");
772
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
773
+ return { tenant: parts[0], env: parts[1] };
774
+ }
775
+ async function discoverConfigFromSsm(context) {
776
+ const parsed = parseTenantEnv(context);
777
+ if (!parsed) return null;
778
+ const { tenant, env } = parsed;
779
+ const ssmPath = `/mesh-platform/${tenant}/${env}/platform/zitadel`;
780
+ logInfo(`Attempting SSM discovery from ${ssmPath}...`);
781
+ try {
782
+ const { SSMClient: SSMClient2, GetParameterCommand: GetParameterCommand2 } = await import("@aws-sdk/client-ssm");
783
+ const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
784
+ const ssm = new SSMClient2({ region });
785
+ const resp = await ssm.send(new GetParameterCommand2({ Name: ssmPath }));
786
+ const raw = resp.Parameter?.Value;
787
+ if (!raw) {
788
+ logWarn(`SSM parameter ${ssmPath} has no value`);
789
+ return null;
479
790
  }
480
- // Timer
481
- if (event.timerStartedEventAttributes) {
482
- const dur = event.timerStartedEventAttributes.startToFireTimeout;
483
- return `timerId: ${event.timerStartedEventAttributes.timerId}, duration: ${dur?.seconds ?? "?"}s`;
791
+ const data = JSON.parse(raw);
792
+ const issuer = data.endpoint;
793
+ const clientId = data.cliClientId;
794
+ if (typeof issuer !== "string" || !issuer) {
795
+ logWarn("SSM zitadel entry missing 'endpoint' field");
796
+ return null;
484
797
  }
485
- if (event.timerFiredEventAttributes) {
486
- return `timerId: ${event.timerFiredEventAttributes.timerId}`;
798
+ if (typeof clientId !== "string" || !clientId) {
799
+ logWarn(
800
+ "SSM zitadel entry missing 'cliClientId' field.\n The platform needs to be deployed with an updated ZitadelPlatformIdentity\n that exports cliClientId. Until then, contact your platform admin."
801
+ );
802
+ return null;
803
+ }
804
+ const config = { issuer, clientId };
805
+ if (typeof data.vpn === "string") config.vpn = data.vpn;
806
+ if (typeof data.vpnJoinBroker === "string") config.vpnJoinBroker = data.vpnJoinBroker;
807
+ if (typeof data.registryBroker === "string") config.registryBroker = data.registryBroker;
808
+ writeContextConfig(context, config);
809
+ logSuccess(`Discovered platform configuration for ${context} via SSM`);
810
+ return config;
811
+ } catch (err) {
812
+ const message = err instanceof Error ? err.message : String(err);
813
+ if (message.includes("ExpiredToken") || message.includes("credentials")) {
814
+ logWarn(`SSM discovery failed: AWS credentials expired or unavailable.`);
815
+ if (firstPartyDomainFor(context) || context.split(".").length - 1 >= 2) {
816
+ logInfo(` Falling back to anonymous HTTPS discovery \u2014 no AWS needed.`);
817
+ } else {
818
+ logInfo(` If you have an AWS account: aws sso login --profile <profile>`);
819
+ logInfo(` Otherwise use the platform's full domain, e.g. mesh login dev.<tenant>.meshtech.io`);
820
+ }
821
+ } else if (message.includes("ParameterNotFound")) {
822
+ logWarn(`SSM parameter not found: ${ssmPath}`);
823
+ logInfo(" This platform context may not be deployed.");
824
+ } else {
825
+ logWarn(`SSM discovery failed: ${message}`);
487
826
  }
488
827
  return null;
828
+ }
489
829
  }
490
- /**
491
- * Reconstruct an agent conversation's transcript from its durable Temporal
492
- * history — for when the worker can no longer replay it (code drift) but the
493
- * history is intact. Fetches the full history (same client/decrypt path as
494
- * `history`), classifies the `submit_user_message` signals (human turns) and
495
- * `callLLM` activity completions (assistant turns), decrypts their payloads,
496
- * and renders an ordered transcript.
497
- */
498
- async function recoverConversation(workflowId, runId, options) {
499
- const conn = await connect(options);
500
- // The history is encrypted — without the key there's nothing to reconstruct,
501
- // so (unlike `history`) a missing key is fatal here.
502
- const decrypter = await buildDecrypterForNamespace(conn.namespace);
503
- if (!decrypter) {
504
- conn.connection.close();
505
- throw new Error("Cannot decrypt conversation payloads: no encoding key available.\n" +
506
- "Set TEMPORAL_ENCODING_KEY (e.g. from a `mesh dev` worker env) or configure " +
507
- "kubectl namespace read access, then retry.");
830
+ async function discoverConfig(domain, contextKey = domain) {
831
+ const url = `https://cli.${domain}/.well-known/mesh.json`;
832
+ logInfo(`Attempting discovery from ${url}...`);
833
+ try {
834
+ const controller = new AbortController();
835
+ const timeout = setTimeout(() => controller.abort(), 5e3);
836
+ const resp = await fetch(url, { signal: controller.signal });
837
+ clearTimeout(timeout);
838
+ if (!resp.ok) {
839
+ logWarn(`Discovery endpoint returned ${resp.status}`);
840
+ return null;
841
+ }
842
+ const data = await resp.json();
843
+ const issuer = data.issuer;
844
+ const clientId = data.clientId;
845
+ if (typeof issuer !== "string" || !issuer || typeof clientId !== "string" || !clientId) {
846
+ logWarn("Discovery endpoint returned invalid config (missing issuer or clientId)");
847
+ return null;
848
+ }
849
+ const config = { issuer, clientId };
850
+ if (typeof data.vpn === "string") config.vpn = data.vpn;
851
+ if (typeof data.vpnJoinBroker === "string") config.vpnJoinBroker = data.vpnJoinBroker;
852
+ if (typeof data.registryBroker === "string") config.registryBroker = data.registryBroker;
853
+ writeContextConfig(contextKey, config);
854
+ logSuccess(`Discovered platform configuration for ${contextKey}`);
855
+ return config;
856
+ } catch (err) {
857
+ const message = err instanceof Error ? err.message : String(err);
858
+ if (message.includes("abort")) {
859
+ logWarn("Discovery timed out");
860
+ } else {
861
+ logWarn(`Discovery failed: ${message}`);
508
862
  }
863
+ return null;
864
+ }
865
+ }
866
+ function readAllCredentials() {
867
+ if (!fs2.existsSync(CREDENTIALS_FILE)) return {};
868
+ try {
869
+ return JSON.parse(fs2.readFileSync(CREDENTIALS_FILE, "utf-8"));
870
+ } catch {
871
+ return {};
872
+ }
873
+ }
874
+ function readCredentials(context) {
875
+ return readAllCredentials()[context] ?? null;
876
+ }
877
+ function atomicWriteFileSync(path4, data, mode) {
878
+ const tmpPath = `${path4}.${process.pid}.${atomicWriteCounter++}.tmp`;
879
+ try {
880
+ fs2.writeFileSync(tmpPath, data, { mode });
881
+ fs2.renameSync(tmpPath, path4);
882
+ } catch (err) {
509
883
  try {
510
- // Decode one payload group (signal input / activity result) into a plain
511
- // object. The decrypter yields pretty-printed JSON; parse it back. Any
512
- // failure returns undefined so the pure reconstructor emits a placeholder.
513
- const decodePayload = async (payloads) => {
514
- const first = payloads?.[0];
515
- if (first === undefined)
516
- return undefined;
517
- const decoded = await decrypter(first);
518
- if (decoded === null)
519
- return undefined;
520
- try {
521
- return JSON.parse(decoded);
522
- }
523
- catch {
524
- return undefined;
525
- }
526
- };
527
- // Collect the full history in order, then classify it (which activity
528
- // completions were `callLLM`, which signals were `submit_user_message`)
529
- // via the shared pure helper.
530
- const rawEvents = [];
531
- for await (const event of iterHistoryEvents(conn.connection.workflowService, conn.namespace, workflowId, runId)) {
532
- rawEvents.push(event);
884
+ fs2.unlinkSync(tmpPath);
885
+ } catch {
886
+ }
887
+ throw err;
888
+ }
889
+ }
890
+ function writeCredentials(context, creds) {
891
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
892
+ const all = readAllCredentials();
893
+ all[context] = creds;
894
+ atomicWriteFileSync(CREDENTIALS_FILE, JSON.stringify(all, null, 2), 384);
895
+ }
896
+ function base64url(buffer) {
897
+ return buffer.toString("base64url");
898
+ }
899
+ function generateCodeVerifier() {
900
+ return base64url(crypto.randomBytes(32));
901
+ }
902
+ function generateCodeChallenge(verifier) {
903
+ return base64url(crypto.createHash("sha256").update(verifier).digest());
904
+ }
905
+ function decodeJwtPayload(token) {
906
+ const parts = token.split(".");
907
+ if (parts.length !== 3) throw new Error("Invalid JWT");
908
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString());
909
+ }
910
+ async function exchangeCode(issuer, clientId, code, codeVerifier) {
911
+ const body = new URLSearchParams({
912
+ grant_type: "authorization_code",
913
+ code,
914
+ redirect_uri: REDIRECT_URI,
915
+ client_id: clientId,
916
+ code_verifier: codeVerifier
917
+ });
918
+ const resp = await fetch(`${issuer}/oauth/v2/token`, {
919
+ method: "POST",
920
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
921
+ body: body.toString()
922
+ });
923
+ if (!resp.ok) {
924
+ const text = await resp.text();
925
+ throw new Error(`Token exchange failed (${resp.status}): ${text}`);
926
+ }
927
+ return resp.json();
928
+ }
929
+ async function refreshTokens(issuer, clientId, refreshToken) {
930
+ const body = new URLSearchParams({
931
+ grant_type: "refresh_token",
932
+ refresh_token: refreshToken,
933
+ client_id: clientId
934
+ });
935
+ const resp = await fetch(`${issuer}/oauth/v2/token`, {
936
+ method: "POST",
937
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
938
+ body: body.toString()
939
+ });
940
+ if (!resp.ok) {
941
+ throw new Error(`Token refresh failed (${resp.status})`);
942
+ }
943
+ return resp.json();
944
+ }
945
+ function login(context, config) {
946
+ return new Promise((resolve3, reject) => {
947
+ const codeVerifier = generateCodeVerifier();
948
+ const codeChallenge = generateCodeChallenge(codeVerifier);
949
+ const state = base64url(crypto.randomBytes(16));
950
+ const authUrl = new URL(`${config.issuer}/oauth/v2/authorize`);
951
+ authUrl.searchParams.set("client_id", config.clientId);
952
+ authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
953
+ authUrl.searchParams.set("response_type", "code");
954
+ authUrl.searchParams.set("scope", SCOPES);
955
+ authUrl.searchParams.set("code_challenge", codeChallenge);
956
+ authUrl.searchParams.set("code_challenge_method", "S256");
957
+ authUrl.searchParams.set("state", state);
958
+ let timeoutId;
959
+ const server = http.createServer(async (req, res) => {
960
+ try {
961
+ const url = new URL(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
962
+ if (url.pathname !== "/callback") {
963
+ res.writeHead(404);
964
+ res.end("Not found");
965
+ return;
533
966
  }
534
- // --snapshot: emit the store-ready conversation blob with STRUCTURED
535
- // ModelMessage[] (tool-call parts intact) for a full-fidelity backfill,
536
- // rather than the flattened text turns. artifacts/focus are left empty —
537
- // the artifact index isn't reconstructable from history here, and the store
538
- // defaults them.
539
- if (options.snapshot) {
540
- const messages = await extractSnapshotMessages(rawEvents, decodePayload);
541
- const blob = {
542
- conversationId: workflowId,
543
- messages,
544
- artifacts: [],
545
- focus: null,
546
- };
547
- const output = JSON.stringify(blob, null, 2) + "\n";
548
- if (options.out) {
549
- writeFileSync(options.out, output);
550
- logSuccess(`Wrote snapshot (${messages.length} messages) to ${options.out}`);
551
- }
552
- else {
553
- process.stdout.write(output);
554
- }
555
- return;
967
+ const error = url.searchParams.get("error");
968
+ if (error) {
969
+ const desc = url.searchParams.get("error_description") ?? error;
970
+ const safeDesc = desc.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
971
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
972
+ res.end(
973
+ `<html><body><h2>Login failed</h2><p>${safeDesc}</p></body></html>`
974
+ );
975
+ teardown();
976
+ reject(new Error(desc));
977
+ return;
556
978
  }
557
- const events = await classifyHistoryEvents(rawEvents, decodePayload);
558
- const turns = reconstructTranscript(events);
559
- const output = options.json
560
- ? JSON.stringify({
561
- turns: turns.map((t) => ({
562
- role: t.role,
563
- text: t.text,
564
- ...(t.toolCalls ? { toolCalls: t.toolCalls } : {}),
565
- })),
566
- }, null, 2) + "\n"
567
- : renderTranscriptMarkdown(turns);
568
- if (options.out) {
569
- writeFileSync(options.out, output);
570
- logSuccess(`Wrote ${turns.length} turns to ${options.out}`);
979
+ const returnedState = url.searchParams.get("state");
980
+ if (returnedState !== state) {
981
+ res.writeHead(400);
982
+ res.end("State mismatch");
983
+ teardown();
984
+ reject(new Error("State mismatch"));
985
+ return;
571
986
  }
572
- else {
573
- process.stdout.write(output);
987
+ const code = url.searchParams.get("code");
988
+ if (!code) {
989
+ res.writeHead(400);
990
+ res.end("No code");
991
+ teardown();
992
+ reject(new Error("No authorization code received"));
993
+ return;
574
994
  }
995
+ const tokens = await exchangeCode(
996
+ config.issuer,
997
+ config.clientId,
998
+ code,
999
+ codeVerifier
1000
+ );
1001
+ const idPayload = decodeJwtPayload(tokens.id_token);
1002
+ const email = idPayload.email ?? idPayload.preferred_username ?? "unknown";
1003
+ const sub = idPayload.sub;
1004
+ let tenants = [];
1005
+ try {
1006
+ const accessPayload = decodeJwtPayload(tokens.access_token);
1007
+ tenants = accessPayload["urn:mesh:tenants"] ?? [];
1008
+ } catch {
1009
+ }
1010
+ writeCredentials(context, {
1011
+ idToken: tokens.id_token,
1012
+ accessToken: tokens.access_token,
1013
+ refreshToken: tokens.refresh_token,
1014
+ expiresAt: new Date(
1015
+ Date.now() + tokens.expires_in * 1e3
1016
+ ).toISOString(),
1017
+ email,
1018
+ sub
1019
+ });
1020
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1021
+ res.end(
1022
+ `<html><body><h2>&#x2705; Logged in to ${context}!</h2><p>You can close this tab.</p></body></html>`
1023
+ );
1024
+ logSuccess(`Logged in as ${email} (${context})`);
1025
+ if (tenants.length > 0) {
1026
+ logInfo(`Deployable tenants: ${tenants.join(", ")}`);
1027
+ }
1028
+ hintVpnIfDisconnected(context);
1029
+ teardown();
1030
+ resolve3();
1031
+ } catch (err) {
1032
+ teardown();
1033
+ reject(err);
1034
+ }
1035
+ });
1036
+ const teardown = () => {
1037
+ if (timeoutId) clearTimeout(timeoutId);
1038
+ server.closeAllConnections?.();
1039
+ server.close();
1040
+ };
1041
+ server.listen(REDIRECT_PORT, () => {
1042
+ logInfo(`Opening browser for authentication (${context})...`);
1043
+ const url = authUrl.toString();
1044
+ try {
1045
+ if (process.platform === "darwin") {
1046
+ execFileSync4("open", [url], { stdio: "ignore" });
1047
+ } else if (process.platform === "linux") {
1048
+ execFileSync4("xdg-open", [url], { stdio: "ignore" });
1049
+ } else {
1050
+ logInfo(`Open this URL in your browser:
1051
+ ${url}`);
1052
+ }
1053
+ } catch {
1054
+ logInfo(`Open this URL in your browser:
1055
+ ${url}`);
1056
+ }
1057
+ });
1058
+ timeoutId = setTimeout(() => {
1059
+ teardown();
1060
+ reject(new Error("Login timed out (2 minutes)"));
1061
+ }, 12e4);
1062
+ });
1063
+ }
1064
+ async function deviceLoginWithReissue(attempt, maxCodes = MAX_DEVICE_CODES) {
1065
+ for (let n = 1; n <= maxCodes; n++) {
1066
+ if (n > 1) logWarn(`That code expired \u2014 issuing a fresh one (${n}/${maxCodes})\u2026`);
1067
+ if (await attempt(n) === "success") return;
1068
+ }
1069
+ throw new Error(
1070
+ `Device login not completed after ${maxCodes} codes. Re-run the command when you're ready to authorize.`
1071
+ );
1072
+ }
1073
+ async function deviceCodeLogin(context, config) {
1074
+ await deviceLoginWithReissue(() => attemptDeviceCode(context, config));
1075
+ }
1076
+ async function attemptDeviceCode(context, config) {
1077
+ const resp = await fetch(`${config.issuer}/oauth/v2/device_authorization`, {
1078
+ method: "POST",
1079
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1080
+ body: new URLSearchParams({
1081
+ client_id: config.clientId,
1082
+ scope: SCOPES
1083
+ }).toString()
1084
+ });
1085
+ if (!resp.ok) {
1086
+ const text = await resp.text();
1087
+ throw new Error(`Device authorization request failed (${resp.status}): ${text}`);
1088
+ }
1089
+ const deviceAuth = await resp.json();
1090
+ const openUrl = deviceAuth.verification_uri_complete ?? deviceAuth.verification_uri;
1091
+ console.log();
1092
+ logInfo(`Open this URL in your browser:
1093
+ `);
1094
+ logInfo(` ${openUrl}
1095
+ `);
1096
+ logInfo(`Code: ${deviceAuth.user_code}`);
1097
+ console.log();
1098
+ logInfo("Waiting for authorization...");
1099
+ try {
1100
+ if (process.platform === "darwin") {
1101
+ execFileSync4("open", [openUrl], { stdio: "ignore" });
1102
+ } else if (process.platform === "linux") {
1103
+ execFileSync4("xdg-open", [openUrl], { stdio: "ignore" });
575
1104
  }
576
- finally {
577
- conn.connection.close();
578
- }
579
- }
580
- /**
581
- * Capture a workflow's FULL durable Temporal history to a replay fixture JSON
582
- * file for LOCAL replay debugging of a specific production break. This is the
583
- * real-history counterpart of the CI gate's synthetic golden fixtures: the
584
- * output is the same proto3-JSON `{ events }` shape `@mesh-tech/agent-sdk`'s
585
- * `historyToFixture` produces, so it runs through `replayFixture` locally. It
586
- * is never committed to the replay fixtures dir — those are synthetic goldens;
587
- * this file holds real decrypted production data (see warning below).
588
- *
589
- * Payloads are DECRYPTED (via the same codec `history --show-payloads` uses) so
590
- * the fixture replays under agent-sdk's plaintext converter. Because that means
591
- * the file contains real customer/conversation data, the default output path is
592
- * `~/.mesh/replay-histories/` outside any git repo — and a loud warning is
593
- * printed. An explicit `--out` inside a git repo is checked with
594
- * `git check-ignore`; a non-ignored path gets a loud warning (but is not
595
- * blocked). Like `recover-conversation`, a missing encoding key is fatal — an
596
- * encrypted fixture can't be replayed.
597
- */
598
- async function captureHistory(workflowId, runId, options) {
599
- const conn = await connect(options);
600
- // A fixture whose payloads stay encrypted can't be replayed (the replayer
601
- // uses a codec-free converter), so — like recover-conversation — treat a
602
- // missing key as fatal rather than emitting a useless file.
603
- const decrypter = await buildRawDecrypterForNamespace(conn.namespace);
604
- if (!decrypter) {
605
- conn.connection.close();
606
- throw new Error("Cannot decrypt history payloads: no encoding key available.\n" +
607
- "A replay fixture needs decrypted payloads. Set TEMPORAL_ENCODING_KEY " +
608
- "(e.g. from a `mesh dev` worker env) or configure kubectl namespace read " +
609
- "access, then retry.");
1105
+ } catch {
1106
+ }
1107
+ const deadline = Date.now() + deviceAuth.expires_in * 1e3;
1108
+ let interval = deviceAuth.interval * 1e3;
1109
+ while (Date.now() < deadline) {
1110
+ await new Promise((r) => setTimeout(r, interval));
1111
+ const tokenResp = await fetch(`${config.issuer}/oauth/v2/token`, {
1112
+ method: "POST",
1113
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1114
+ body: new URLSearchParams({
1115
+ client_id: config.clientId,
1116
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1117
+ device_code: deviceAuth.device_code
1118
+ }).toString()
1119
+ });
1120
+ if (tokenResp.ok) {
1121
+ const tokens = await tokenResp.json();
1122
+ const idPayload = decodeJwtPayload(tokens.id_token);
1123
+ const email = idPayload.email ?? idPayload.preferred_username ?? "unknown";
1124
+ const sub = idPayload.sub;
1125
+ let tenants = [];
1126
+ try {
1127
+ const accessPayload = decodeJwtPayload(tokens.access_token);
1128
+ tenants = accessPayload["urn:mesh:tenants"] ?? [];
1129
+ } catch {
1130
+ }
1131
+ writeCredentials(context, {
1132
+ idToken: tokens.id_token,
1133
+ accessToken: tokens.access_token,
1134
+ refreshToken: tokens.refresh_token,
1135
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
1136
+ email,
1137
+ sub
1138
+ });
1139
+ logSuccess(`Logged in as ${email} (${context})`);
1140
+ if (tenants.length > 0) {
1141
+ logInfo(`Deployable tenants: ${tenants.join(", ")}`);
1142
+ }
1143
+ hintVpnIfDisconnected(context);
1144
+ return "success";
1145
+ }
1146
+ const error = await tokenResp.json();
1147
+ switch (error.error) {
1148
+ case "authorization_pending":
1149
+ continue;
1150
+ case "slow_down":
1151
+ interval += 1e3;
1152
+ continue;
1153
+ case "expired_token":
1154
+ return "expired";
1155
+ // caller re-issues a fresh code
1156
+ case "access_denied":
1157
+ throw new Error("Authorization denied by user.");
1158
+ default:
1159
+ throw new Error(`Token exchange failed: ${error.error} \u2014 ${error.error_description ?? ""}`);
1160
+ }
1161
+ }
1162
+ return "expired";
1163
+ }
1164
+ function isRemoteEnvironment() {
1165
+ if (process.env.REMOTE_CONTAINERS || process.env.CODESPACES) return true;
1166
+ if (fs2.existsSync("/.dockerenv")) return true;
1167
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
1168
+ if (!process.stdout.isTTY || !process.stdin.isTTY) return true;
1169
+ return false;
1170
+ }
1171
+ function hintVpnIfDisconnected(context) {
1172
+ try {
1173
+ const state = tailscaleBackendState();
1174
+ if (state !== "Running") {
1175
+ console.log("");
1176
+ logInfo(`VPN not connected. To access dev services, run:`);
1177
+ logInfo(` mesh vpn connect ${context}`);
1178
+ }
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ function tokenStillValid(expiresAt, marginMs = 0, now = Date.now()) {
1183
+ return new Date(expiresAt).getTime() - marginMs > now;
1184
+ }
1185
+ async function getValidToken(context, opts = {}) {
1186
+ const config = getContextConfig(context);
1187
+ if (!config) return null;
1188
+ const creds = readCredentials(context);
1189
+ if (!creds) return null;
1190
+ if (tokenStillValid(creds.expiresAt, opts.marginMs ?? 0)) {
1191
+ return creds.idToken;
1192
+ }
1193
+ if (!creds.refreshToken) return null;
1194
+ try {
1195
+ const tokens = await refreshTokens(config.issuer, config.clientId, creds.refreshToken);
1196
+ const idPayload = decodeJwtPayload(tokens.id_token);
1197
+ writeCredentials(context, {
1198
+ ...creds,
1199
+ idToken: tokens.id_token,
1200
+ accessToken: tokens.access_token,
1201
+ refreshToken: tokens.refresh_token ?? creds.refreshToken,
1202
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
1203
+ email: idPayload.email ?? creds.email
1204
+ });
1205
+ return tokens.id_token;
1206
+ } catch {
1207
+ return null;
1208
+ }
1209
+ }
1210
+ async function ensureValidToken(context, config, opts) {
1211
+ let token = await getValidToken(context);
1212
+ if (token) return token;
1213
+ logInfo(`No valid Zitadel session for ${context} \u2014 running login flow`);
1214
+ try {
1215
+ const useDevice = opts.device || isRemoteEnvironment();
1216
+ if (useDevice) {
1217
+ logInfo("Using device code flow");
1218
+ await deviceCodeLogin(context, config);
1219
+ } else {
1220
+ await login(context, config);
1221
+ }
1222
+ } catch (err) {
1223
+ logError(`Login failed: ${err.message}`);
1224
+ return null;
1225
+ }
1226
+ return await getValidToken(context);
1227
+ }
1228
+ async function ensureLogin(context, opts = {}) {
1229
+ const existing = readCredentials(context);
1230
+ if (existing && new Date(existing.expiresAt) > /* @__PURE__ */ new Date()) return existing;
1231
+ if (opts.interactive === false) return null;
1232
+ const config = getContextConfig(context) ?? await discoverConfigGuarded(context);
1233
+ if (!config) {
1234
+ logWarn(`No login config for "${context}". Run: mesh login ${context}`);
1235
+ return null;
1236
+ }
1237
+ const token = await ensureValidToken(context, config, opts);
1238
+ if (!token) return null;
1239
+ return readCredentials(context);
1240
+ }
1241
+ async function discoverConfigGuarded(context) {
1242
+ let config = null;
1243
+ if (parseTenantEnv(context)) {
1244
+ config = await discoverConfigFromSsm(context);
1245
+ }
1246
+ if (!config) {
1247
+ const pinned = firstPartyDomainFor(context);
1248
+ if (pinned) {
1249
+ logInfo(`"${context}" is a known Mesh platform \u2014 resolving via ${pinned} (no AWS needed)`);
1250
+ config = await discoverConfig(pinned, context);
610
1251
  }
1252
+ }
1253
+ if (!config && context.split(".").length - 1 >= 2) {
1254
+ config = await discoverConfig(context);
1255
+ }
1256
+ return config;
1257
+ }
1258
+ var CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE, REDIRECT_PORT, REDIRECT_URI, SCOPES, atomicWriteCounter, MAX_DEVICE_CODES;
1259
+ var init_login = __esm({
1260
+ "libs/mesh-cli/src/commands/login.ts"() {
1261
+ "use strict";
1262
+ init_log();
1263
+ init_aws_auth();
1264
+ init_first_party_contexts();
1265
+ init_vpn2();
1266
+ CONFIG_DIR = path2.join(
1267
+ process.env.XDG_CONFIG_HOME ?? path2.join(process.env.HOME ?? "~", ".config"),
1268
+ "mesh"
1269
+ );
1270
+ CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
1271
+ CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials.json");
1272
+ REDIRECT_PORT = 9876;
1273
+ REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
1274
+ SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
1275
+ atomicWriteCounter = 0;
1276
+ MAX_DEVICE_CODES = 3;
1277
+ }
1278
+ });
1279
+
1280
+ // libs/mesh-cli/src/utils/aws-auth.ts
1281
+ import * as fs3 from "fs";
1282
+ import * as path3 from "path";
1283
+ import {
1284
+ AssumeRoleCommand,
1285
+ AssumeRoleWithWebIdentityCommand,
1286
+ GetCallerIdentityCommand,
1287
+ STSClient
1288
+ } from "@aws-sdk/client-sts";
1289
+ function derivePlatformContext(appRoot, stack) {
1290
+ const configFile = path3.join(appRoot, `Pulumi.${stack}.yaml`);
1291
+ if (!fs3.existsSync(configFile)) return null;
1292
+ const content = fs3.readFileSync(configFile, "utf-8");
1293
+ const jsonMatch = content.match(/mesh:platform:\s*'(\{[^']+\})'/);
1294
+ if (jsonMatch) {
611
1295
  try {
612
- const events = [];
613
- for await (const event of iterHistoryEvents(conn.connection.workflowService, conn.namespace, workflowId, runId)) {
614
- events.push(event);
615
- }
616
- if (events.length === 0) {
617
- throw new Error(`No history events found for workflow '${workflowId}'` +
618
- (runId ? ` (run ${runId})` : "") +
619
- `. Check the workflow id and namespace (${conn.namespace}).`);
1296
+ const parsed = JSON.parse(jsonMatch[1]);
1297
+ if (parsed.name && parsed.env) return `${parsed.name}.${parsed.env}`;
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ const nameMatch = content.match(/mesh:platform:[\s\S]*?name:\s*(\S+)/);
1302
+ const envMatch = content.match(/mesh:platform:[\s\S]*?env:\s*(\S+)/);
1303
+ if (nameMatch && envMatch) {
1304
+ return `${nameMatch[1]}.${envMatch[1]}`;
1305
+ }
1306
+ return null;
1307
+ }
1308
+ function newStsClient() {
1309
+ return new STSClient({ region: DEFAULT_REGION });
1310
+ }
1311
+ function toEnv(credentials) {
1312
+ const { AccessKeyId, SecretAccessKey, SessionToken } = credentials;
1313
+ if (!AccessKeyId || !SecretAccessKey || !SessionToken) return null;
1314
+ return {
1315
+ AWS_ACCESS_KEY_ID: AccessKeyId,
1316
+ AWS_SECRET_ACCESS_KEY: SecretAccessKey,
1317
+ AWS_SESSION_TOKEN: SessionToken
1318
+ };
1319
+ }
1320
+ function isMaxSessionDurationError(err) {
1321
+ if (!(err instanceof Error)) return false;
1322
+ const message = err.message ?? "";
1323
+ return message.includes("MaxSessionDuration") || message.includes("DurationSeconds");
1324
+ }
1325
+ async function assumeRole(roleArn, sessionName = "mesh-cli", durationSeconds = 43200) {
1326
+ const sts = newStsClient();
1327
+ for (const duration of [durationSeconds, 3600]) {
1328
+ try {
1329
+ const resp = await sts.send(
1330
+ new AssumeRoleCommand({
1331
+ RoleArn: roleArn,
1332
+ RoleSessionName: sessionName,
1333
+ DurationSeconds: duration
1334
+ })
1335
+ );
1336
+ if (!resp.Credentials) return null;
1337
+ return toEnv(resp.Credentials);
1338
+ } catch (err) {
1339
+ if (!isMaxSessionDurationError(err) || duration === 3600) {
1340
+ const message = err instanceof Error ? err.message : String(err);
1341
+ logWarn(`AssumeRole failed: ${message}`);
1342
+ if (isExpiredAwsTokenMessage(message)) {
1343
+ logWarn(` Your AWS SSO session looks expired or missing \u2014 refresh with: ${SSO_LOGIN_FIX}`);
620
1344
  }
621
- // Lazy import — pulls in @temporalio/common only when actually capturing,
622
- // so the CLI still loads without the Temporal SDK installed.
623
- const { serializeHistoryToFixture } = await import("../utils/capture-history.js");
624
- const { fixture, eventCount, decryptedPayloads } = await serializeHistoryToFixture(events, decrypter);
625
- const outPath = options.out ??
626
- join(homedir(), ".mesh", "replay-histories", `${sanitizeFileId(workflowId)}.json`);
627
- mkdirSync(dirname(outPath), { recursive: true });
628
- if (options.out)
629
- warnIfNotGitIgnored(options.out);
630
- writeFileSync(outPath, JSON.stringify(fixture, null, 2) + "\n");
631
- logSuccess(`Wrote replay history (${eventCount} events, ${decryptedPayloads} payloads decrypted) to ${outPath}`);
632
- logWarn("⚠ This file contains DECRYPTED payloads — real customer/conversation " +
633
- "data.\n" +
634
- " For LOCAL replay debugging ONLY. Do NOT commit it. (CI golden " +
635
- "fixtures are synthetic; this is the debugging counterpart.)");
636
- }
637
- finally {
638
- conn.connection.close();
639
- }
640
- }
641
- /** Make a workflow id safe to use as a filename (slashes, spaces, etc.). */
642
- function sanitizeFileId(id) {
643
- return id.replace(/[^A-Za-z0-9._-]/g, "_");
644
- }
645
- /**
646
- * A replay fixture holds real DECRYPTED production data, so when the user
647
- * points `--out` at a path inside a git repo we check whether that path is
648
- * git-ignored (`git check-ignore`) and print a LOUD warning when it is not —
649
- * we warn, we don't block. Best-effort: silent when git is unavailable or the
650
- * path is outside any repo (exit code 128), since there is nothing to commit
651
- * it into.
652
- */
653
- function warnIfNotGitIgnored(outPath) {
654
- const abs = resolve(outPath);
1345
+ return null;
1346
+ }
1347
+ }
1348
+ }
1349
+ return null;
1350
+ }
1351
+ async function assumeRoleCredentials(roleArn, idToken, sessionName, durationSeconds = 43200) {
1352
+ const sts = newStsClient();
1353
+ for (const duration of [durationSeconds, 3600]) {
655
1354
  try {
656
- // Exit 0 = ignored, 1 = in a repo but NOT ignored, 128 = not a repo.
657
- const res = spawnSync("git", ["-C", dirname(abs), "check-ignore", "-q", abs], {
658
- stdio: "ignore",
659
- });
660
- if (res.status !== 1)
661
- return;
1355
+ const resp = await sts.send(
1356
+ new AssumeRoleWithWebIdentityCommand({
1357
+ RoleArn: roleArn,
1358
+ RoleSessionName: sessionName,
1359
+ WebIdentityToken: idToken,
1360
+ DurationSeconds: duration
1361
+ })
1362
+ );
1363
+ if (!resp.Credentials) return null;
1364
+ const env = toEnv(resp.Credentials);
1365
+ if (!env) return null;
1366
+ return {
1367
+ // `env` is a `Record<string, string>` (index-signature typed), so
1368
+ // `noUncheckedIndexedAccess` widens these to `string | undefined`
1369
+ // even though `toEnv` guarantees they're set — assert accordingly.
1370
+ AccessKeyId: env.AWS_ACCESS_KEY_ID,
1371
+ SecretAccessKey: env.AWS_SECRET_ACCESS_KEY,
1372
+ SessionToken: env.AWS_SESSION_TOKEN,
1373
+ Expiration: resp.Credentials.Expiration.toISOString()
1374
+ };
1375
+ } catch (err) {
1376
+ if (!isMaxSessionDurationError(err) || duration === 3600) {
1377
+ logWarn(
1378
+ `AssumeRoleWithWebIdentity failed: ${err instanceof Error ? err.message : String(err)}`
1379
+ );
1380
+ return null;
1381
+ }
662
1382
  }
663
- catch {
664
- return; // git missing/unusable — can't check, stay quiet
1383
+ }
1384
+ return null;
1385
+ }
1386
+ async function assumeRoleWithWebIdentity(roleArn, idToken, sessionName, durationSeconds = 43200) {
1387
+ const creds = await assumeRoleCredentials(roleArn, idToken, sessionName, durationSeconds);
1388
+ if (!creds) return null;
1389
+ return toEnv(creds);
1390
+ }
1391
+ async function resolveAwsCredentials(roleArn, appRoot, stack) {
1392
+ const roleName = roleArn.split("/").pop() ?? roleArn;
1393
+ if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SESSION_TOKEN) {
1394
+ logInfo("Using existing AWS credentials from environment");
1395
+ return {
1396
+ env: {
1397
+ AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
1398
+ AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? "",
1399
+ AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN
1400
+ },
1401
+ method: "environment"
1402
+ };
1403
+ }
1404
+ const platformContext = derivePlatformContext(appRoot, stack);
1405
+ if (platformContext) {
1406
+ const meshCreds = await ensureLogin(platformContext, { interactive: false });
1407
+ if (meshCreds) {
1408
+ const sessionName = (meshCreds.email ?? "mesh-cli").replace(/[^a-zA-Z0-9=,.@-]/g, "_").slice(0, 64);
1409
+ logInfo(`Assuming ${roleName} via Zitadel (${meshCreds.email ?? platformContext})`);
1410
+ const creds2 = await assumeRoleWithWebIdentity(roleArn, meshCreds.idToken, sessionName);
1411
+ if (creds2) {
1412
+ logSuccess(`Assumed ${roleName} via Zitadel`);
1413
+ return { env: creds2, method: "zitadel" };
1414
+ }
1415
+ logWarn(
1416
+ `Zitadel JWT auth failed \u2014 falling back to AWS SSO.
1417
+ (Try: mesh login ${platformContext})`
1418
+ );
665
1419
  }
666
- logWarn("⚠⚠⚠ OUTPUT PATH IS INSIDE A GIT REPO AND IS *NOT* GITIGNORED ⚠⚠⚠\n" +
667
- ` ${abs}\n` +
668
- " This fixture contains DECRYPTED production payloads. Add it to " +
669
- ".gitignore or move it outside the repo — do NOT commit it.");
1420
+ }
1421
+ logInfo(`Assuming ${roleName} via AWS SSO`);
1422
+ const creds = await assumeRole(roleArn, "mesh-cli");
1423
+ if (creds) {
1424
+ logSuccess(`Assumed ${roleName}`);
1425
+ return { env: creds, method: "sso" };
1426
+ }
1427
+ if (platformContext && process.stdout.isTTY) {
1428
+ const meshCreds = await ensureLogin(platformContext);
1429
+ if (meshCreds) {
1430
+ const sessionName = (meshCreds.email ?? "mesh-cli").replace(/[^a-zA-Z0-9=,.@-]/g, "_").slice(0, 64);
1431
+ logInfo(`Assuming ${roleName} via Zitadel (${meshCreds.email ?? platformContext})`);
1432
+ const zCreds = await assumeRoleWithWebIdentity(roleArn, meshCreds.idToken, sessionName);
1433
+ if (zCreds) {
1434
+ logSuccess(`Assumed ${roleName} via Zitadel`);
1435
+ return { env: zCreds, method: "zitadel" };
1436
+ }
1437
+ }
1438
+ }
1439
+ return null;
670
1440
  }
671
- async function terminateWorkflow(workflowId, runId, options) {
672
- const conn = await connect(options);
673
- try {
674
- await conn.connection.workflowService.terminateWorkflowExecution({
675
- namespace: conn.namespace,
676
- workflowExecution: { workflowId, runId },
677
- reason: options.reason ?? "terminated via mesh temporal",
678
- identity: "mesh-cli",
1441
+ var DEFAULT_REGION;
1442
+ var init_aws_auth = __esm({
1443
+ "libs/mesh-cli/src/utils/aws-auth.ts"() {
1444
+ "use strict";
1445
+ init_log();
1446
+ init_auth_preflight();
1447
+ init_login();
1448
+ DEFAULT_REGION = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
1449
+ }
1450
+ });
1451
+
1452
+ // libs/mesh-cli/src/utils/capture-history.ts
1453
+ var capture_history_exports = {};
1454
+ __export(capture_history_exports, {
1455
+ decryptHistoryPayloads: () => decryptHistoryPayloads,
1456
+ serializeHistoryToFixture: () => serializeHistoryToFixture
1457
+ });
1458
+ import { historyToJSON } from "@temporalio/common/lib/proto-utils.js";
1459
+ function isPayload(value) {
1460
+ return value != null && typeof value === "object" && "metadata" in value && "data" in value;
1461
+ }
1462
+ async function decryptHistoryPayloads(node, decrypt) {
1463
+ if (node == null || typeof node !== "object") return 0;
1464
+ if (isPayload(node)) {
1465
+ const inner = await decrypt(node);
1466
+ if (inner === null) return 0;
1467
+ node.metadata = inner.metadata ?? {};
1468
+ node.data = inner.data ?? new Uint8Array(0);
1469
+ return 1;
1470
+ }
1471
+ let count = 0;
1472
+ if (Array.isArray(node)) {
1473
+ for (const item of node) count += await decryptHistoryPayloads(item, decrypt);
1474
+ } else {
1475
+ for (const value of Object.values(node)) {
1476
+ count += await decryptHistoryPayloads(value, decrypt);
1477
+ }
1478
+ }
1479
+ return count;
1480
+ }
1481
+ async function serializeHistoryToFixture(events, decrypter) {
1482
+ let decryptedPayloads = 0;
1483
+ if (decrypter) {
1484
+ for (const event of events) {
1485
+ decryptedPayloads += await decryptHistoryPayloads(event, decrypter);
1486
+ }
1487
+ }
1488
+ const fixture = JSON.parse(historyToJSON({ events }));
1489
+ return { fixture, eventCount: events.length, decryptedPayloads };
1490
+ }
1491
+ var init_capture_history = __esm({
1492
+ "libs/mesh-cli/src/utils/capture-history.ts"() {
1493
+ "use strict";
1494
+ }
1495
+ });
1496
+
1497
+ // libs/mesh-cli/src/commands/temporal.ts
1498
+ init_log();
1499
+ import { spawnSync } from "node:child_process";
1500
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
1501
+ import { homedir } from "node:os";
1502
+ import { dirname as dirname4, join as join4, resolve as resolve2 } from "node:path";
1503
+
1504
+ // libs/mesh-cli/src/utils/recover-conversation.ts
1505
+ async function classifyHistoryEvents(rawEvents, decodePayload) {
1506
+ const activityTypeByScheduledId = /* @__PURE__ */ new Map();
1507
+ const events = [];
1508
+ for (const event of rawEvents) {
1509
+ const eventId = String(event.eventId);
1510
+ const scheduled = event.activityTaskScheduledEventAttributes;
1511
+ if (scheduled) {
1512
+ const name = scheduled.activityType?.name;
1513
+ if (name) activityTypeByScheduledId.set(eventId, name);
1514
+ continue;
1515
+ }
1516
+ const signal = event.workflowExecutionSignaledEventAttributes;
1517
+ if (signal?.signalName === "submit_user_message") {
1518
+ events.push({
1519
+ eventId,
1520
+ kind: "userSignal",
1521
+ payload: await decodePayload(signal.input?.payloads)
1522
+ });
1523
+ continue;
1524
+ }
1525
+ const completed = event.activityTaskCompletedEventAttributes;
1526
+ if (completed) {
1527
+ const scheduledId = String(completed.scheduledEventId);
1528
+ if (activityTypeByScheduledId.get(scheduledId) === "callLLM") {
1529
+ events.push({
1530
+ eventId,
1531
+ kind: "callLLMResult",
1532
+ payload: await decodePayload(completed.result?.payloads)
679
1533
  });
680
- logSuccess(`Terminated ${workflowId}${runId ? ` (run ${runId})` : ""} in namespace ${conn.namespace}`);
1534
+ }
681
1535
  }
682
- finally {
683
- conn.connection.close();
1536
+ }
1537
+ return events;
1538
+ }
1539
+ async function extractSnapshotMessages(rawEvents, decodePayload) {
1540
+ let lastCallLLMScheduledId;
1541
+ let lastCallLLMInputPayloads;
1542
+ const activityTypeByScheduledId = /* @__PURE__ */ new Map();
1543
+ let lastCallLLMResultPayloads;
1544
+ for (const event of rawEvents) {
1545
+ const eventId = String(event.eventId);
1546
+ const scheduled = event.activityTaskScheduledEventAttributes;
1547
+ if (scheduled) {
1548
+ const name = scheduled.activityType?.name;
1549
+ if (name) activityTypeByScheduledId.set(eventId, name);
1550
+ if (name === "callLLM") {
1551
+ lastCallLLMScheduledId = eventId;
1552
+ lastCallLLMInputPayloads = scheduled.input?.payloads;
1553
+ lastCallLLMResultPayloads = void 0;
1554
+ }
1555
+ continue;
1556
+ }
1557
+ const completed = event.activityTaskCompletedEventAttributes;
1558
+ if (completed) {
1559
+ const scheduledId = String(completed.scheduledEventId);
1560
+ if (activityTypeByScheduledId.get(scheduledId) === "callLLM") {
1561
+ lastCallLLMResultPayloads = completed.result?.payloads;
1562
+ }
684
1563
  }
1564
+ }
1565
+ if (lastCallLLMScheduledId === void 0) return [];
1566
+ const inputArgs = await decodePayload(lastCallLLMInputPayloads);
1567
+ const inputMessages = messagesFrom(inputArgs);
1568
+ const result = await decodePayload(lastCallLLMResultPayloads);
1569
+ const resultMessages = messagesFrom(result);
1570
+ return [...inputMessages, ...resultMessages];
685
1571
  }
686
- async function connect(options) {
687
- const info = await resolveConnection(options);
688
- logInfo(`Connecting to ${info.address} (namespace: ${info.namespace})`);
689
- // Dynamic import — @temporalio/client is only available in workspaces
690
- // that have it installed. The mesh-cli uses it from the monorepo.
691
- const { Connection } = await import("@temporalio/client");
692
- // Attach a Zitadel Bearer when the server enforces auth. Without it the
693
- // MeshAuthorizer denies calls with "Request unauthorized" (e.g. terminate).
694
- const token = await resolveBearerToken(info.auth);
695
- const metadata = token ? { authorization: `Bearer ${token}` } : undefined;
696
- const connection = await Connection.connect({ address: info.address, metadata });
697
- return { connection, namespace: info.namespace };
698
- }
699
- // ── Registration ─────────────────────────────────────────────────────────────
700
- export function registerTemporalCommands(program) {
701
- const temporal = program
702
- .command("temporal")
703
- .description("Inspect Temporal workflows for the current app/stack");
704
- const sharedOpts = (cmd) => cmd
705
- .option("--stack <stack>", "Pulumi stack name (auto-detected from current directory)")
706
- .option("--address <addr>", "Temporal server address (default: from stack output)")
707
- .option("--namespace <ns>", "Temporal namespace (default: from stack output)");
708
- sharedOpts(temporal
709
- .command("describe <workflowId> [runId]")
710
- .description("Show workflow status and metadata")).action(async (workflowId, runId, opts) => {
711
- try {
712
- await describeWorkflow(workflowId, runId, opts);
713
- }
714
- catch (err) {
715
- logError(err instanceof Error ? err.message : String(err));
716
- process.exitCode = 1;
717
- }
718
- });
719
- sharedOpts(temporal
720
- .command("terminate <workflowId> [runId]")
721
- .description("Terminate a running workflow (e.g. a wedged/looping conversation)"))
722
- .option("--reason <reason>", "Termination reason (recorded in workflow history)")
723
- .action(async (workflowId, runId, opts) => {
724
- try {
725
- await terminateWorkflow(workflowId, runId, opts);
726
- }
727
- catch (err) {
728
- logError(err instanceof Error ? err.message : String(err));
729
- process.exitCode = 1;
1572
+ function messagesFrom(payload) {
1573
+ if (isRecord(payload) && Array.isArray(payload.messages)) {
1574
+ return payload.messages;
1575
+ }
1576
+ return [];
1577
+ }
1578
+ function extractText(content) {
1579
+ if (typeof content === "string") return content;
1580
+ if (Array.isArray(content)) {
1581
+ return content.filter(
1582
+ (p) => !!p && typeof p === "object" && p.type === "text"
1583
+ ).map((p) => typeof p.text === "string" ? p.text : "").join("");
1584
+ }
1585
+ return "";
1586
+ }
1587
+ function summarizeArgs(args) {
1588
+ try {
1589
+ const str = JSON.stringify(args ?? {});
1590
+ return str.length > 100 ? str.substring(0, 97) + "..." : str;
1591
+ } catch {
1592
+ return "...";
1593
+ }
1594
+ }
1595
+ function formatToolCall(part) {
1596
+ const name = typeof part.toolName === "string" ? part.toolName : "?";
1597
+ const args = "input" in part ? part.input : part.args;
1598
+ return `${name}(${summarizeArgs(args)})`;
1599
+ }
1600
+ function isRecord(v) {
1601
+ return !!v && typeof v === "object";
1602
+ }
1603
+ function extractUserText(payload) {
1604
+ if (!isRecord(payload)) return null;
1605
+ const messages = payload.messages;
1606
+ if (Array.isArray(messages)) {
1607
+ const parts = [];
1608
+ for (const m of messages) {
1609
+ if (isRecord(m) && m.role === "user") {
1610
+ const t = extractText(m.content);
1611
+ if (t) parts.push(t);
1612
+ }
1613
+ }
1614
+ if (parts.length > 0) return parts.join("\n");
1615
+ }
1616
+ for (const field of ["content", "text", "message"]) {
1617
+ const v = payload[field];
1618
+ if (typeof v === "string" && v) return v;
1619
+ const t = extractText(v);
1620
+ if (t) return t;
1621
+ }
1622
+ return null;
1623
+ }
1624
+ function extractAssistantTurn(payload) {
1625
+ if (!isRecord(payload)) return null;
1626
+ const messages = payload.messages;
1627
+ if (!Array.isArray(messages)) {
1628
+ const t = extractText(payload.content) || extractText(payload.text);
1629
+ return t ? { text: t, toolCalls: [] } : null;
1630
+ }
1631
+ let text = "";
1632
+ const toolCalls = [];
1633
+ for (const m of messages) {
1634
+ if (!isRecord(m) || m.role !== "assistant") continue;
1635
+ text += extractText(m.content);
1636
+ if (Array.isArray(m.content)) {
1637
+ for (const p of m.content) {
1638
+ if (isRecord(p) && p.type === "tool-call") {
1639
+ toolCalls.push(formatToolCall(p));
730
1640
  }
1641
+ }
1642
+ }
1643
+ }
1644
+ if (!text && toolCalls.length === 0) return null;
1645
+ return { text, toolCalls };
1646
+ }
1647
+ function reconstructTranscript(events) {
1648
+ const turns = [];
1649
+ for (const event of events) {
1650
+ if (event.kind === "userSignal") {
1651
+ const text = extractUserText(event.payload);
1652
+ turns.push(
1653
+ text !== null ? { role: "user", text, eventId: event.eventId } : { role: "user", text: placeholder(event.eventId), eventId: event.eventId }
1654
+ );
1655
+ } else if (event.kind === "callLLMResult") {
1656
+ const turn = extractAssistantTurn(event.payload);
1657
+ if (turn) {
1658
+ turns.push({
1659
+ role: "assistant",
1660
+ text: turn.text,
1661
+ eventId: event.eventId,
1662
+ ...turn.toolCalls.length > 0 ? { toolCalls: turn.toolCalls } : {}
1663
+ });
1664
+ } else {
1665
+ turns.push({
1666
+ role: "assistant",
1667
+ text: placeholder(event.eventId),
1668
+ eventId: event.eventId
1669
+ });
1670
+ }
1671
+ }
1672
+ }
1673
+ return turns;
1674
+ }
1675
+ function placeholder(eventId) {
1676
+ return `_[unparseable turn: ${eventId}]_`;
1677
+ }
1678
+ function renderTranscriptMarkdown(turns) {
1679
+ const blocks = [];
1680
+ for (const turn of turns) {
1681
+ const heading = turn.role === "user" ? "## User" : "## Assistant";
1682
+ const lines = [heading, ""];
1683
+ if (turn.text) lines.push(turn.text);
1684
+ if (turn.toolCalls && turn.toolCalls.length > 0) {
1685
+ if (turn.text) lines.push("");
1686
+ for (const tc of turn.toolCalls) lines.push(`\u2192 tool: ${tc}`);
1687
+ }
1688
+ blocks.push(lines.join("\n"));
1689
+ }
1690
+ return blocks.join("\n\n") + "\n";
1691
+ }
1692
+
1693
+ // libs/mesh-cli/src/commands/temporal.ts
1694
+ init_pulumi();
1695
+ init_aws_auth();
1696
+ init_temporal_auth();
1697
+
1698
+ // libs/mesh-cli/src/utils/temporal-codec.ts
1699
+ init_log();
1700
+ import { execFileSync as execFileSync5 } from "child_process";
1701
+ import { webcrypto as crypto2 } from "node:crypto";
1702
+ function resolveTemporalEncodingKeyFromK8s(namespace) {
1703
+ const secretName = `${namespace}-temporal-encoding-key`;
1704
+ try {
1705
+ const b64 = execFileSync5(
1706
+ "kubectl",
1707
+ [
1708
+ "get",
1709
+ "secret",
1710
+ secretName,
1711
+ "-n",
1712
+ namespace,
1713
+ "-o",
1714
+ "jsonpath={.data.TEMPORAL_ENCODING_KEY}"
1715
+ ],
1716
+ { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
1717
+ ).trim();
1718
+ if (!b64) return void 0;
1719
+ return Buffer.from(b64, "base64").toString("utf-8");
1720
+ } catch {
1721
+ return void 0;
1722
+ }
1723
+ }
1724
+ var ENCRYPTED_ENCODING = "binary/encrypted";
1725
+ var IV_LENGTH = 12;
1726
+ async function deriveKey(encodingKey) {
1727
+ const keyData = await crypto2.subtle.digest(
1728
+ "SHA-256",
1729
+ new TextEncoder().encode(encodingKey)
1730
+ );
1731
+ return crypto2.subtle.importKey(
1732
+ "raw",
1733
+ keyData,
1734
+ { name: "AES-GCM" },
1735
+ false,
1736
+ ["decrypt"]
1737
+ );
1738
+ }
1739
+ async function aesGcmDecrypt(data, key) {
1740
+ const iv = data.slice(0, IV_LENGTH);
1741
+ const ciphertextWithTag = data.slice(IV_LENGTH);
1742
+ const decrypted = await crypto2.subtle.decrypt(
1743
+ { name: "AES-GCM", iv },
1744
+ key,
1745
+ ciphertextWithTag
1746
+ );
1747
+ return new Uint8Array(decrypted);
1748
+ }
1749
+ async function createPayloadDecrypter(encodingKey) {
1750
+ const key = await deriveKey(encodingKey);
1751
+ return async (payload) => {
1752
+ if (!payload?.metadata) return null;
1753
+ const encodingBytes = payload.metadata["encoding"];
1754
+ if (!encodingBytes) return null;
1755
+ const encoding = new TextDecoder().decode(encodingBytes);
1756
+ if (encoding !== ENCRYPTED_ENCODING) return null;
1757
+ if (!payload.data) return "[encrypted payload missing data]";
1758
+ let inner;
1759
+ try {
1760
+ const decrypted = await aesGcmDecrypt(payload.data, key);
1761
+ inner = decodePayloadProtobuf(decrypted);
1762
+ } catch (err) {
1763
+ const msg = err instanceof Error ? err.message : String(err);
1764
+ return `[undecryptable: ${msg}]`;
1765
+ }
1766
+ return decodeInnerPayload(inner);
1767
+ };
1768
+ }
1769
+ async function createRawPayloadDecrypter(encodingKey) {
1770
+ const key = await deriveKey(encodingKey);
1771
+ return async (payload) => {
1772
+ if (!payload?.metadata) return null;
1773
+ const encodingBytes = payload.metadata["encoding"];
1774
+ if (!encodingBytes) return null;
1775
+ const encoding = new TextDecoder().decode(encodingBytes);
1776
+ if (encoding !== ENCRYPTED_ENCODING) return null;
1777
+ if (!payload.data) {
1778
+ throw new Error("encrypted payload has no data to decrypt");
1779
+ }
1780
+ const decrypted = await aesGcmDecrypt(payload.data, key);
1781
+ return decodePayloadProtobuf(decrypted);
1782
+ };
1783
+ }
1784
+ function readVarint(buf, pos) {
1785
+ let value = 0;
1786
+ let shift = 0;
1787
+ let next = pos;
1788
+ while (next < buf.length) {
1789
+ const byte = buf[next];
1790
+ next += 1;
1791
+ value |= (byte & 127) << shift;
1792
+ if ((byte & 128) === 0) return { value, next };
1793
+ shift += 7;
1794
+ if (shift >= 32) throw new Error("varint too large");
1795
+ }
1796
+ throw new Error("truncated varint");
1797
+ }
1798
+ function readLengthDelimited(buf, pos) {
1799
+ const { value: length, next: afterLen } = readVarint(buf, pos);
1800
+ const end = afterLen + length;
1801
+ if (end > buf.length) throw new Error("truncated length-delimited field");
1802
+ return { bytes: buf.slice(afterLen, end), next: end };
1803
+ }
1804
+ function decodePayloadProtobuf(buf) {
1805
+ const metadata = {};
1806
+ let data;
1807
+ let pos = 0;
1808
+ while (pos < buf.length) {
1809
+ const { value: tag, next: afterTag } = readVarint(buf, pos);
1810
+ const fieldNumber = tag >>> 3;
1811
+ const wireType = tag & 7;
1812
+ pos = afterTag;
1813
+ if (wireType === 2) {
1814
+ const { bytes, next } = readLengthDelimited(buf, pos);
1815
+ pos = next;
1816
+ if (fieldNumber === 1) {
1817
+ const entry = decodeMetadataEntry(bytes);
1818
+ if (entry) metadata[entry.key] = entry.value;
1819
+ } else if (fieldNumber === 2) {
1820
+ data = bytes;
1821
+ }
1822
+ } else if (wireType === 0) {
1823
+ pos = readVarint(buf, pos).next;
1824
+ } else if (wireType === 1) {
1825
+ pos += 8;
1826
+ } else if (wireType === 5) {
1827
+ pos += 4;
1828
+ } else {
1829
+ throw new Error(`unsupported wire type ${wireType} at pos ${pos}`);
1830
+ }
1831
+ }
1832
+ return { metadata, data: data ?? new Uint8Array(0) };
1833
+ }
1834
+ function decodeMetadataEntry(buf) {
1835
+ let key;
1836
+ let value;
1837
+ let pos = 0;
1838
+ while (pos < buf.length) {
1839
+ const { value: tag, next: afterTag } = readVarint(buf, pos);
1840
+ const fieldNumber = tag >>> 3;
1841
+ const wireType = tag & 7;
1842
+ pos = afterTag;
1843
+ if (wireType !== 2) {
1844
+ if (wireType === 0) pos = readVarint(buf, pos).next;
1845
+ else if (wireType === 1) pos += 8;
1846
+ else if (wireType === 5) pos += 4;
1847
+ else throw new Error(`unsupported metadata wire type ${wireType}`);
1848
+ continue;
1849
+ }
1850
+ const { bytes, next } = readLengthDelimited(buf, pos);
1851
+ pos = next;
1852
+ if (fieldNumber === 1) key = new TextDecoder().decode(bytes);
1853
+ else if (fieldNumber === 2) value = bytes;
1854
+ }
1855
+ if (key === void 0 || value === void 0) return null;
1856
+ return { key, value };
1857
+ }
1858
+ function decodeInnerPayload(payload) {
1859
+ const meta = payload.metadata ?? {};
1860
+ const encodingBytes = meta["encoding"];
1861
+ const encoding = encodingBytes ? new TextDecoder().decode(encodingBytes) : "unknown";
1862
+ const data = payload.data ?? new Uint8Array(0);
1863
+ if (encoding === "binary/null") return "null";
1864
+ if (encoding === "json/plain") {
1865
+ const text = new TextDecoder().decode(data);
1866
+ try {
1867
+ return JSON.stringify(JSON.parse(text), null, 2);
1868
+ } catch {
1869
+ return text;
1870
+ }
1871
+ }
1872
+ if (encoding === "binary/plain") return `[binary, ${data.length} bytes]`;
1873
+ return `[encoding=${encoding}, ${data.length} bytes]`;
1874
+ }
1875
+ function resolveEncodingKey(namespace) {
1876
+ const fromK8s = resolveTemporalEncodingKeyFromK8s(namespace);
1877
+ const key = fromK8s ?? process.env.TEMPORAL_ENCODING_KEY;
1878
+ if (!key) {
1879
+ logWarn(
1880
+ `Could not read TEMPORAL_ENCODING_KEY from secret ${namespace}/${namespace}-temporal-encoding-key, and TEMPORAL_ENCODING_KEY is not set. Configure kubectl (namespace read access), or export TEMPORAL_ENCODING_KEY (e.g. from a \`mesh dev\` worker env).`
1881
+ );
1882
+ return void 0;
1883
+ }
1884
+ logSuccess(
1885
+ fromK8s ? `Resolved encoding key from ${namespace}-temporal-encoding-key` : "Resolved encoding key from TEMPORAL_ENCODING_KEY env"
1886
+ );
1887
+ return key;
1888
+ }
1889
+ async function buildDecrypterForNamespace(namespace) {
1890
+ const key = resolveEncodingKey(namespace);
1891
+ if (!key) return void 0;
1892
+ return createPayloadDecrypter(key);
1893
+ }
1894
+ async function buildRawDecrypterForNamespace(namespace) {
1895
+ const key = resolveEncodingKey(namespace);
1896
+ if (!key) return void 0;
1897
+ return createRawPayloadDecrypter(key);
1898
+ }
1899
+
1900
+ // libs/mesh-cli/src/commands/temporal.ts
1901
+ async function resolveConnection(options) {
1902
+ if (options.address && options.namespace) {
1903
+ return { address: options.address, namespace: options.namespace };
1904
+ }
1905
+ if (!options.address && process.env.TEMPORAL_ADDRESS) {
1906
+ const address = process.env.TEMPORAL_ADDRESS;
1907
+ const namespace2 = options.namespace ?? process.env.TEMPORAL_NAMESPACE;
1908
+ if (namespace2) return { address, namespace: namespace2 };
1909
+ }
1910
+ const appRoot = findAppRoot(process.cwd());
1911
+ if (!appRoot) {
1912
+ throw new Error(
1913
+ "Not in a Mesh app directory (no Pulumi.yaml found).\nRun from an app directory, or provide --address and --namespace."
1914
+ );
1915
+ }
1916
+ const stack = options.stack ?? getCurrentStack(appRoot);
1917
+ if (!stack) {
1918
+ const stacks = findStackConfigs(appRoot);
1919
+ throw new Error(
1920
+ `No Pulumi stack selected.
1921
+ ` + (stacks.length > 0 ? `Available: ${stacks.join(", ")}
1922
+ Use: mesh temporal --stack <name> ...` : "No stack configs found in this directory.")
1923
+ );
1924
+ }
1925
+ const sa = stack ? ["--stack", stack] : [];
1926
+ const deployerRole = readStackConfig(appRoot, stack, "mesh:deployerRole");
1927
+ const awsCreds = deployerRole ? (await resolveAwsCredentials(deployerRole, appRoot, stack))?.env : void 0;
1928
+ let devOutput;
1929
+ let appOutput;
1930
+ try {
1931
+ const result = pulumiStackOutput(appRoot, "app", sa, awsCreds);
1932
+ appOutput = JSON.parse(result);
1933
+ devOutput = appOutput.dev ?? appOutput;
1934
+ } catch {
1935
+ try {
1936
+ const result = pulumiStackOutput(appRoot, "dev", sa, awsCreds);
1937
+ devOutput = JSON.parse(result);
1938
+ appOutput = devOutput;
1939
+ } catch {
1940
+ throw new Error(
1941
+ `Could not read stack output for '${stack}'.
1942
+ Ensure you've run: mesh deploy up
1943
+ Or provide --address and --namespace explicitly.`
1944
+ );
1945
+ }
1946
+ }
1947
+ const tunnel = devOutput?.tunnels?.temporal;
1948
+ const namespace = options.namespace ?? appOutput?.namespace ?? devOutput?.namespace;
1949
+ if (!tunnel) {
1950
+ throw new Error("No temporal tunnel found in stack output.");
1951
+ }
1952
+ if (!namespace) {
1953
+ throw new Error("No temporal namespace found in stack output.");
1954
+ }
1955
+ const platform = devOutput?.platform ?? appOutput?.platform;
1956
+ const auth = platform ? {
1957
+ tenant: platform.tenant ?? "mesh",
1958
+ env: platform.env ?? "dev",
1959
+ platformName: platform.name ?? platform.tenant ?? "mesh"
1960
+ } : void 0;
1961
+ return {
1962
+ address: options.address ?? `${tunnel.host}:${tunnel.port}`,
1963
+ namespace,
1964
+ auth
1965
+ };
1966
+ }
1967
+ async function resolveBearerToken(auth) {
1968
+ try {
1969
+ const haveEnvCreds = !!process.env.TEMPORAL_AUTH_CLIENT_ID && !!process.env.ZITADEL_ISSUER;
1970
+ const vars = !haveEnvCreds && auth ? await resolveTemporalAuth(auth.tenant, auth.env, auth.platformName) : {};
1971
+ return await acquireTemporalBearer(vars);
1972
+ } catch (err) {
1973
+ logWarn(
1974
+ `Temporal auth unavailable (${err instanceof Error ? err.message : String(err)}); connecting unauthenticated`
1975
+ );
1976
+ return void 0;
1977
+ }
1978
+ }
1979
+ var EVENT_TYPE_NAMES = {
1980
+ 1: "WorkflowExecutionStarted",
1981
+ 2: "WorkflowExecutionCompleted",
1982
+ 3: "WorkflowExecutionFailed",
1983
+ 4: "WorkflowExecutionTimedOut",
1984
+ 5: "WorkflowTaskScheduled",
1985
+ 6: "WorkflowTaskStarted",
1986
+ 7: "WorkflowTaskCompleted",
1987
+ 8: "WorkflowTaskTimedOut",
1988
+ 9: "WorkflowTaskFailed",
1989
+ 10: "ActivityTaskScheduled",
1990
+ 11: "ActivityTaskStarted",
1991
+ 12: "ActivityTaskCompleted",
1992
+ 13: "ActivityTaskFailed",
1993
+ 14: "ActivityTaskTimedOut",
1994
+ 15: "ActivityTaskCancelRequested",
1995
+ 16: "ActivityTaskCanceled",
1996
+ 17: "TimerStarted",
1997
+ 18: "TimerFired",
1998
+ 19: "TimerCanceled",
1999
+ 20: "WorkflowExecutionCancelRequested",
2000
+ 21: "WorkflowExecutionCanceled",
2001
+ 24: "WorkflowExecutionSignaled",
2002
+ // not 22/23 which were removed
2003
+ 25: "WorkflowExecutionTerminated",
2004
+ 26: "WorkflowExecutionSignaled",
2005
+ 29: "WorkflowPropertiesModified",
2006
+ 40: "ChildWorkflowExecutionStarted",
2007
+ 41: "ChildWorkflowExecutionCompleted",
2008
+ 42: "ChildWorkflowExecutionFailed",
2009
+ 43: "ChildWorkflowExecutionCanceled",
2010
+ 44: "ChildWorkflowExecutionTimedOut",
2011
+ 45: "ChildWorkflowExecutionTerminated",
2012
+ 46: "WorkflowPropertiesModified",
2013
+ 48: "NexusOperationScheduled",
2014
+ 49: "NexusOperationStarted",
2015
+ 50: "NexusOperationCompleted",
2016
+ 51: "NexusOperationFailed",
2017
+ 52: "NexusOperationCanceled",
2018
+ 53: "NexusOperationTimedOut"
2019
+ };
2020
+ var STATUS_NAMES = {
2021
+ 0: "UNSPECIFIED",
2022
+ 1: "RUNNING",
2023
+ 2: "COMPLETED",
2024
+ 3: "FAILED",
2025
+ 4: "CANCELED",
2026
+ 5: "TERMINATED",
2027
+ 6: "CONTINUED_AS_NEW",
2028
+ 7: "TIMED_OUT"
2029
+ };
2030
+ async function describeWorkflow(workflowId, runId, options) {
2031
+ const conn = await connect(options);
2032
+ try {
2033
+ const desc = await conn.connection.workflowService.describeWorkflowExecution({
2034
+ namespace: conn.namespace,
2035
+ execution: { workflowId, runId }
731
2036
  });
732
- sharedOpts(temporal
733
- .command("history <workflowId> [runId]")
734
- .description("Show workflow event history")
735
- .option("-n, --limit <n>", "Maximum events to show (default: 200)")
736
- .option("-f, --follow", "Tail the history, waiting for new events until the workflow completes")
737
- .option("--no-compact", "Show all events including WorkflowTask scheduling noise")
738
- .option("-p, --show-payloads", "Decrypt and print activity inputs/outputs (reads TEMPORAL_ENCODING_KEY from the app's K8s secret via kubectl)")).action(async (workflowId, runId, opts) => {
739
- try {
740
- await showHistory(workflowId, runId, opts);
741
- }
742
- catch (err) {
743
- logError(err instanceof Error ? err.message : String(err));
744
- process.exitCode = 1;
745
- }
2037
+ const info = desc.workflowExecutionInfo;
2038
+ if (!info) {
2039
+ logError("No workflow execution info returned.");
2040
+ return;
2041
+ }
2042
+ console.log(`Workflow: ${workflowId}`);
2043
+ console.log(`Run ID: ${info.execution?.runId ?? runId ?? "?"}`);
2044
+ console.log(`Type: ${info.type?.name ?? "?"}`);
2045
+ console.log(`Status: ${STATUS_NAMES[info.status ?? 0] ?? info.status}`);
2046
+ console.log(`Task Queue: ${info.taskQueue ?? "?"}`);
2047
+ console.log(`Namespace: ${conn.namespace}`);
2048
+ const pending = desc.pendingActivities ?? [];
2049
+ if (pending.length > 0) {
2050
+ console.log(`
2051
+ Pending Activities (${pending.length}):`);
2052
+ for (const pa of pending) {
2053
+ console.log(` - ${pa.activityType?.name ?? "?"} (attempt ${pa.attempt}, state: ${pa.state})`);
2054
+ }
2055
+ }
2056
+ const pendingNexus = desc.pendingNexusOperations ?? [];
2057
+ if (pendingNexus.length > 0) {
2058
+ console.log(`
2059
+ Pending Nexus Operations (${pendingNexus.length}):`);
2060
+ for (const pn of pendingNexus) {
2061
+ console.log(` - ${pn.operation ?? "?"} \u2192 ${pn.endpoint ?? "?"} (state: ${pn.state})`);
2062
+ }
2063
+ }
2064
+ } finally {
2065
+ conn.connection.close();
2066
+ }
2067
+ }
2068
+ async function* iterHistoryEvents(workflowService, namespace, workflowId, runId, maximumPageSize = 100) {
2069
+ let nextPageToken;
2070
+ do {
2071
+ const resp = await workflowService.getWorkflowExecutionHistory({
2072
+ namespace,
2073
+ execution: { workflowId, runId },
2074
+ maximumPageSize,
2075
+ nextPageToken
746
2076
  });
747
- sharedOpts(temporal
748
- .command("recover-conversation <workflowId> [runId]")
749
- .description("Reconstruct an agent conversation transcript from durable Temporal history " +
750
- "(for when the worker can no longer replay it). Requires the encoding key — " +
751
- "reads TEMPORAL_ENCODING_KEY (or the app's K8s secret).")
752
- .option("--out <path>", "Write the transcript to a file instead of stdout")
753
- .option("--json", "Emit a structured { turns: [...] } JSON array instead of markdown")
754
- .option("--snapshot", "Emit a store-ready conversation blob { conversationId, messages, artifacts, focus } " +
755
- "with STRUCTURED ModelMessage[] (tool-call parts intact) for a full-fidelity backfill")).action(async (workflowId, runId, opts) => {
756
- try {
757
- await recoverConversation(workflowId, runId, opts);
2077
+ for (const event of resp.history?.events ?? []) {
2078
+ yield event;
2079
+ }
2080
+ nextPageToken = resp.nextPageToken?.length ? resp.nextPageToken : void 0;
2081
+ } while (nextPageToken);
2082
+ }
2083
+ async function showHistory(workflowId, runId, options) {
2084
+ const conn = await connect(options);
2085
+ const maxEvents = options.follow ? Infinity : parseInt(options.limit ?? "200", 10);
2086
+ const skipTypes = /* @__PURE__ */ new Set([5, 6, 7, 8, 9]);
2087
+ const decrypter = options.showPayloads ? await buildDecrypterForNamespace(conn.namespace) : void 0;
2088
+ try {
2089
+ let totalShown = 0;
2090
+ let lastEventId = 0;
2091
+ console.log(`History for ${workflowId} (namespace: ${conn.namespace})${options.follow ? " [following]" : ""}
2092
+ `);
2093
+ for await (const event of iterHistoryEvents(
2094
+ conn.connection.workflowService,
2095
+ conn.namespace,
2096
+ workflowId,
2097
+ runId
2098
+ )) {
2099
+ lastEventId = Number(event.eventId);
2100
+ const eventType = event.eventType ?? 0;
2101
+ if (options.compact !== false && skipTypes.has(eventType)) continue;
2102
+ await printEvent(event, decrypter);
2103
+ totalShown++;
2104
+ if (totalShown >= maxEvents) break;
2105
+ }
2106
+ if (!options.follow) {
2107
+ console.log(`
2108
+ ${totalShown} events shown.`);
2109
+ return;
2110
+ }
2111
+ const terminalTypes = /* @__PURE__ */ new Set([2, 3, 4, 21, 27, 28]);
2112
+ let done = false;
2113
+ while (!done) {
2114
+ try {
2115
+ const resp = await conn.connection.workflowService.getWorkflowExecutionHistory({
2116
+ namespace: conn.namespace,
2117
+ execution: { workflowId, runId },
2118
+ maximumPageSize: 100,
2119
+ waitNewEvent: true,
2120
+ // Start after the last event we've seen
2121
+ ...lastEventId > 0 ? { skipArchival: true } : {}
2122
+ });
2123
+ for (const event of resp.history?.events ?? []) {
2124
+ const eid = Number(event.eventId);
2125
+ if (eid <= lastEventId) continue;
2126
+ lastEventId = eid;
2127
+ const eventType = event.eventType ?? 0;
2128
+ if (options.compact !== false && skipTypes.has(eventType)) continue;
2129
+ await printEvent(event, decrypter);
2130
+ totalShown++;
2131
+ if (terminalTypes.has(eventType)) {
2132
+ done = true;
2133
+ }
758
2134
  }
759
- catch (err) {
760
- logError(err instanceof Error ? err.message : String(err));
761
- process.exitCode = 1;
2135
+ } catch (err) {
2136
+ if (err?.code === 5 || err?.message?.includes("not found")) {
2137
+ console.log("\nWorkflow completed or not found.");
2138
+ done = true;
2139
+ } else {
2140
+ throw err;
762
2141
  }
2142
+ }
2143
+ }
2144
+ console.log(`
2145
+ ${totalShown} events shown (workflow ended).`);
2146
+ } finally {
2147
+ conn.connection.close();
2148
+ }
2149
+ }
2150
+ async function printEvent(event, decrypter) {
2151
+ const eventType = event.eventType ?? 0;
2152
+ const typeName = EVENT_TYPE_NAMES[eventType] ?? `Unknown(${eventType})`;
2153
+ const id = String(event.eventId).padStart(4);
2154
+ const detail = extractEventDetail(event);
2155
+ const detailStr = detail ? ` \u2014 ${detail}` : "";
2156
+ console.log(`${id} ${typeName}${detailStr}`);
2157
+ if (!decrypter) return;
2158
+ for (const group of extractEventPayloads(event)) {
2159
+ const rendered = await renderPayloads(group.payloads, decrypter);
2160
+ if (rendered === null) continue;
2161
+ console.log(` ${group.label}:`);
2162
+ for (const line of rendered.split("\n")) {
2163
+ console.log(` ${line}`);
2164
+ }
2165
+ }
2166
+ }
2167
+ function extractEventPayloads(event) {
2168
+ const groups = [];
2169
+ const pushIfAny = (label, payloads) => {
2170
+ if (payloads && payloads.length > 0) groups.push({ label, payloads });
2171
+ };
2172
+ if (event.workflowExecutionStartedEventAttributes) {
2173
+ pushIfAny("input", event.workflowExecutionStartedEventAttributes.input?.payloads);
2174
+ }
2175
+ if (event.workflowExecutionCompletedEventAttributes) {
2176
+ pushIfAny("result", event.workflowExecutionCompletedEventAttributes.result?.payloads);
2177
+ }
2178
+ if (event.workflowExecutionFailedEventAttributes) {
2179
+ const failure = event.workflowExecutionFailedEventAttributes.failure;
2180
+ if (failure?.message) groups.push({ label: "failure", payloads: [{ __failure: failure }] });
2181
+ }
2182
+ if (event.workflowExecutionSignaledEventAttributes) {
2183
+ pushIfAny("signal input", event.workflowExecutionSignaledEventAttributes.input?.payloads);
2184
+ }
2185
+ if (event.activityTaskScheduledEventAttributes) {
2186
+ pushIfAny("input", event.activityTaskScheduledEventAttributes.input?.payloads);
2187
+ }
2188
+ if (event.activityTaskCompletedEventAttributes) {
2189
+ pushIfAny("result", event.activityTaskCompletedEventAttributes.result?.payloads);
2190
+ }
2191
+ if (event.activityTaskFailedEventAttributes) {
2192
+ const failure = event.activityTaskFailedEventAttributes.failure;
2193
+ if (failure?.message) groups.push({ label: "failure", payloads: [{ __failure: failure }] });
2194
+ }
2195
+ if (event.nexusOperationScheduledEventAttributes) {
2196
+ const input = event.nexusOperationScheduledEventAttributes.input;
2197
+ if (input) pushIfAny("input", [input]);
2198
+ }
2199
+ if (event.nexusOperationCompletedEventAttributes) {
2200
+ const result = event.nexusOperationCompletedEventAttributes.result;
2201
+ if (result) pushIfAny("result", [result]);
2202
+ }
2203
+ if (event.nexusOperationFailedEventAttributes) {
2204
+ const failure = event.nexusOperationFailedEventAttributes.failure;
2205
+ if (failure?.message) groups.push({ label: "failure", payloads: [{ __failure: failure }] });
2206
+ }
2207
+ return groups;
2208
+ }
2209
+ async function renderPayloads(payloads, decrypter) {
2210
+ const parts = [];
2211
+ for (const payload of payloads) {
2212
+ if (payload && typeof payload === "object" && "__failure" in payload) {
2213
+ const failure = payload.__failure;
2214
+ parts.push(renderFailure(failure));
2215
+ continue;
2216
+ }
2217
+ const decoded = await decrypter(payload);
2218
+ if (decoded === null) {
2219
+ parts.push("[non-encrypted payload]");
2220
+ } else {
2221
+ parts.push(decoded);
2222
+ }
2223
+ }
2224
+ return parts.length > 0 ? parts.join("\n---\n") : null;
2225
+ }
2226
+ function renderFailure(failure) {
2227
+ const lines = [failure.message ?? "(no message)"];
2228
+ if (failure.stackTrace) lines.push(failure.stackTrace);
2229
+ return lines.join("\n");
2230
+ }
2231
+ function extractEventDetail(event) {
2232
+ if (event.activityTaskScheduledEventAttributes) {
2233
+ const a = event.activityTaskScheduledEventAttributes;
2234
+ return `activity: ${a.activityType?.name ?? "?"}, queue: ${a.taskQueue?.name ?? "?"}`;
2235
+ }
2236
+ if (event.activityTaskCompletedEventAttributes) {
2237
+ return `scheduledId: ${event.activityTaskCompletedEventAttributes.scheduledEventId}`;
2238
+ }
2239
+ if (event.activityTaskFailedEventAttributes) {
2240
+ const a = event.activityTaskFailedEventAttributes;
2241
+ const msg = a.failure?.message ?? "";
2242
+ return `scheduledId: ${a.scheduledEventId}${msg ? `, error: ${msg.slice(0, 120)}` : ""}`;
2243
+ }
2244
+ if (event.activityTaskTimedOutEventAttributes) {
2245
+ return `scheduledId: ${event.activityTaskTimedOutEventAttributes.scheduledEventId}`;
2246
+ }
2247
+ if (event.nexusOperationScheduledEventAttributes) {
2248
+ const a = event.nexusOperationScheduledEventAttributes;
2249
+ return `endpoint: ${a.endpoint ?? "?"}, operation: ${a.operation ?? "?"}, service: ${a.service ?? "?"}`;
2250
+ }
2251
+ if (event.nexusOperationCompletedEventAttributes) {
2252
+ return `scheduledId: ${event.nexusOperationCompletedEventAttributes.scheduledEventId}`;
2253
+ }
2254
+ if (event.nexusOperationFailedEventAttributes) {
2255
+ const a = event.nexusOperationFailedEventAttributes;
2256
+ return `scheduledId: ${a.scheduledEventId}, error: ${a.failure?.message?.slice(0, 120) ?? "?"}`;
2257
+ }
2258
+ if (event.nexusOperationTimedOutEventAttributes) {
2259
+ return `scheduledId: ${event.nexusOperationTimedOutEventAttributes.scheduledEventId}`;
2260
+ }
2261
+ if (event.workflowExecutionSignaledEventAttributes) {
2262
+ return `signal: ${event.workflowExecutionSignaledEventAttributes.signalName ?? "?"}`;
2263
+ }
2264
+ if (event.workflowExecutionStartedEventAttributes) {
2265
+ const a = event.workflowExecutionStartedEventAttributes;
2266
+ return `type: ${a.workflowType?.name ?? "?"}, queue: ${a.taskQueue?.name ?? "?"}`;
2267
+ }
2268
+ if (event.workflowExecutionCompletedEventAttributes) return null;
2269
+ if (event.workflowExecutionFailedEventAttributes) {
2270
+ return `error: ${event.workflowExecutionFailedEventAttributes.failure?.message?.slice(0, 120) ?? "?"}`;
2271
+ }
2272
+ if (event.timerStartedEventAttributes) {
2273
+ const dur = event.timerStartedEventAttributes.startToFireTimeout;
2274
+ return `timerId: ${event.timerStartedEventAttributes.timerId}, duration: ${dur?.seconds ?? "?"}s`;
2275
+ }
2276
+ if (event.timerFiredEventAttributes) {
2277
+ return `timerId: ${event.timerFiredEventAttributes.timerId}`;
2278
+ }
2279
+ return null;
2280
+ }
2281
+ async function recoverConversation(workflowId, runId, options) {
2282
+ const conn = await connect(options);
2283
+ const decrypter = await buildDecrypterForNamespace(conn.namespace);
2284
+ if (!decrypter) {
2285
+ conn.connection.close();
2286
+ throw new Error(
2287
+ "Cannot decrypt conversation payloads: no encoding key available.\nSet TEMPORAL_ENCODING_KEY (e.g. from a `mesh dev` worker env) or configure kubectl namespace read access, then retry."
2288
+ );
2289
+ }
2290
+ try {
2291
+ const decodePayload = async (payloads) => {
2292
+ const first = payloads?.[0];
2293
+ if (first === void 0) return void 0;
2294
+ const decoded = await decrypter(first);
2295
+ if (decoded === null) return void 0;
2296
+ try {
2297
+ return JSON.parse(decoded);
2298
+ } catch {
2299
+ return void 0;
2300
+ }
2301
+ };
2302
+ const rawEvents = [];
2303
+ for await (const event of iterHistoryEvents(
2304
+ conn.connection.workflowService,
2305
+ conn.namespace,
2306
+ workflowId,
2307
+ runId
2308
+ )) {
2309
+ rawEvents.push(event);
2310
+ }
2311
+ if (options.snapshot) {
2312
+ const messages = await extractSnapshotMessages(rawEvents, decodePayload);
2313
+ const blob = {
2314
+ conversationId: workflowId,
2315
+ messages,
2316
+ artifacts: [],
2317
+ focus: null
2318
+ };
2319
+ const output2 = JSON.stringify(blob, null, 2) + "\n";
2320
+ if (options.out) {
2321
+ writeFileSync2(options.out, output2);
2322
+ logSuccess(
2323
+ `Wrote snapshot (${messages.length} messages) to ${options.out}`
2324
+ );
2325
+ } else {
2326
+ process.stdout.write(output2);
2327
+ }
2328
+ return;
2329
+ }
2330
+ const events = await classifyHistoryEvents(rawEvents, decodePayload);
2331
+ const turns = reconstructTranscript(events);
2332
+ const output = options.json ? JSON.stringify(
2333
+ {
2334
+ turns: turns.map((t) => ({
2335
+ role: t.role,
2336
+ text: t.text,
2337
+ ...t.toolCalls ? { toolCalls: t.toolCalls } : {}
2338
+ }))
2339
+ },
2340
+ null,
2341
+ 2
2342
+ ) + "\n" : renderTranscriptMarkdown(turns);
2343
+ if (options.out) {
2344
+ writeFileSync2(options.out, output);
2345
+ logSuccess(`Wrote ${turns.length} turns to ${options.out}`);
2346
+ } else {
2347
+ process.stdout.write(output);
2348
+ }
2349
+ } finally {
2350
+ conn.connection.close();
2351
+ }
2352
+ }
2353
+ async function captureHistory(workflowId, runId, options) {
2354
+ const conn = await connect(options);
2355
+ const decrypter = await buildRawDecrypterForNamespace(conn.namespace);
2356
+ if (!decrypter) {
2357
+ conn.connection.close();
2358
+ throw new Error(
2359
+ "Cannot decrypt history payloads: no encoding key available.\nA replay fixture needs decrypted payloads. Set TEMPORAL_ENCODING_KEY (e.g. from a `mesh dev` worker env) or configure kubectl namespace read access, then retry."
2360
+ );
2361
+ }
2362
+ try {
2363
+ const events = [];
2364
+ for await (const event of iterHistoryEvents(
2365
+ conn.connection.workflowService,
2366
+ conn.namespace,
2367
+ workflowId,
2368
+ runId
2369
+ )) {
2370
+ events.push(event);
2371
+ }
2372
+ if (events.length === 0) {
2373
+ throw new Error(
2374
+ `No history events found for workflow '${workflowId}'` + (runId ? ` (run ${runId})` : "") + `. Check the workflow id and namespace (${conn.namespace}).`
2375
+ );
2376
+ }
2377
+ const { serializeHistoryToFixture: serializeHistoryToFixture2 } = await Promise.resolve().then(() => (init_capture_history(), capture_history_exports));
2378
+ const { fixture, eventCount, decryptedPayloads } = await serializeHistoryToFixture2(events, decrypter);
2379
+ const outPath = options.out ?? join4(homedir(), ".mesh", "replay-histories", `${sanitizeFileId(workflowId)}.json`);
2380
+ mkdirSync2(dirname4(outPath), { recursive: true });
2381
+ if (options.out) warnIfNotGitIgnored(options.out);
2382
+ writeFileSync2(outPath, JSON.stringify(fixture, null, 2) + "\n");
2383
+ logSuccess(
2384
+ `Wrote replay history (${eventCount} events, ${decryptedPayloads} payloads decrypted) to ${outPath}`
2385
+ );
2386
+ logWarn(
2387
+ "\u26A0 This file contains DECRYPTED payloads \u2014 real customer/conversation data.\n For LOCAL replay debugging ONLY. Do NOT commit it. (CI golden fixtures are synthetic; this is the debugging counterpart.)"
2388
+ );
2389
+ } finally {
2390
+ conn.connection.close();
2391
+ }
2392
+ }
2393
+ function sanitizeFileId(id) {
2394
+ return id.replace(/[^A-Za-z0-9._-]/g, "_");
2395
+ }
2396
+ function warnIfNotGitIgnored(outPath) {
2397
+ const abs = resolve2(outPath);
2398
+ try {
2399
+ const res = spawnSync("git", ["-C", dirname4(abs), "check-ignore", "-q", abs], {
2400
+ stdio: "ignore"
763
2401
  });
764
- sharedOpts(temporal
765
- .command("capture-history <workflowId> [runId]")
766
- .description("Capture a workflow's full history to a replay fixture JSON (proto3-JSON, " +
767
- "DECRYPTED payloads) for LOCAL replay debugging. Requires the encoding key. " +
768
- "Defaults to ~/.mesh/replay-histories/<workflowId>.json (outside any repo) — " +
769
- "do NOT commit the output.")
770
- .option("--out <path>", "Output path (default: ~/.mesh/replay-histories/<workflowId>.json; a non-gitignored " +
771
- "path inside a git repo triggers a loud warning)")).action(async (workflowId, runId, opts) => {
772
- try {
773
- await captureHistory(workflowId, runId, opts);
774
- }
775
- catch (err) {
776
- logError(err instanceof Error ? err.message : String(err));
777
- process.exitCode = 1;
778
- }
2402
+ if (res.status !== 1) return;
2403
+ } catch {
2404
+ return;
2405
+ }
2406
+ logWarn(
2407
+ `\u26A0\u26A0\u26A0 OUTPUT PATH IS INSIDE A GIT REPO AND IS *NOT* GITIGNORED \u26A0\u26A0\u26A0
2408
+ ${abs}
2409
+ This fixture contains DECRYPTED production payloads. Add it to .gitignore or move it outside the repo \u2014 do NOT commit it.`
2410
+ );
2411
+ }
2412
+ async function terminateWorkflow(workflowId, runId, options) {
2413
+ const conn = await connect(options);
2414
+ try {
2415
+ await conn.connection.workflowService.terminateWorkflowExecution({
2416
+ namespace: conn.namespace,
2417
+ workflowExecution: { workflowId, runId },
2418
+ reason: options.reason ?? "terminated via mesh temporal",
2419
+ identity: "mesh-cli"
779
2420
  });
2421
+ logSuccess(
2422
+ `Terminated ${workflowId}${runId ? ` (run ${runId})` : ""} in namespace ${conn.namespace}`
2423
+ );
2424
+ } finally {
2425
+ conn.connection.close();
2426
+ }
2427
+ }
2428
+ async function connect(options) {
2429
+ const info = await resolveConnection(options);
2430
+ logInfo(`Connecting to ${info.address} (namespace: ${info.namespace})`);
2431
+ const { Connection } = await import("@temporalio/client");
2432
+ const token = await resolveBearerToken(info.auth);
2433
+ const metadata = token ? { authorization: `Bearer ${token}` } : void 0;
2434
+ const connection = await Connection.connect({
2435
+ address: info.address,
2436
+ metadata,
2437
+ channelArgs: { "grpc.max_receive_message_length": 256 * 1024 * 1024 }
2438
+ });
2439
+ return { connection, namespace: info.namespace };
780
2440
  }
781
- //# sourceMappingURL=temporal.js.map
2441
+ function registerTemporalCommands(program) {
2442
+ const temporal = program.command("temporal").description("Inspect Temporal workflows for the current app/stack");
2443
+ const sharedOpts = (cmd) => cmd.option("--stack <stack>", "Pulumi stack name (auto-detected from current directory)").option("--address <addr>", "Temporal server address (default: from stack output)").option("--namespace <ns>", "Temporal namespace (default: from stack output)");
2444
+ sharedOpts(
2445
+ temporal.command("describe <workflowId> [runId]").description("Show workflow status and metadata")
2446
+ ).action(async (workflowId, runId, opts) => {
2447
+ try {
2448
+ await describeWorkflow(workflowId, runId, opts);
2449
+ } catch (err) {
2450
+ logError(err instanceof Error ? err.message : String(err));
2451
+ process.exitCode = 1;
2452
+ }
2453
+ });
2454
+ sharedOpts(
2455
+ temporal.command("terminate <workflowId> [runId]").description("Terminate a running workflow (e.g. a wedged/looping conversation)")
2456
+ ).option("--reason <reason>", "Termination reason (recorded in workflow history)").action(async (workflowId, runId, opts) => {
2457
+ try {
2458
+ await terminateWorkflow(workflowId, runId, opts);
2459
+ } catch (err) {
2460
+ logError(err instanceof Error ? err.message : String(err));
2461
+ process.exitCode = 1;
2462
+ }
2463
+ });
2464
+ sharedOpts(
2465
+ temporal.command("history <workflowId> [runId]").description("Show workflow event history").option("-n, --limit <n>", "Maximum events to show (default: 200)").option("-f, --follow", "Tail the history, waiting for new events until the workflow completes").option("--no-compact", "Show all events including WorkflowTask scheduling noise").option("-p, --show-payloads", "Decrypt and print activity inputs/outputs (reads TEMPORAL_ENCODING_KEY from the app's K8s secret via kubectl)")
2466
+ ).action(async (workflowId, runId, opts) => {
2467
+ try {
2468
+ await showHistory(workflowId, runId, opts);
2469
+ } catch (err) {
2470
+ logError(err instanceof Error ? err.message : String(err));
2471
+ process.exitCode = 1;
2472
+ }
2473
+ });
2474
+ sharedOpts(
2475
+ temporal.command("recover-conversation <workflowId> [runId]").description(
2476
+ "Reconstruct an agent conversation transcript from durable Temporal history (for when the worker can no longer replay it). Requires the encoding key \u2014 reads TEMPORAL_ENCODING_KEY (or the app's K8s secret)."
2477
+ ).option("--out <path>", "Write the transcript to a file instead of stdout").option("--json", "Emit a structured { turns: [...] } JSON array instead of markdown").option(
2478
+ "--snapshot",
2479
+ "Emit a store-ready conversation blob { conversationId, messages, artifacts, focus } with STRUCTURED ModelMessage[] (tool-call parts intact) for a full-fidelity backfill"
2480
+ )
2481
+ ).action(async (workflowId, runId, opts) => {
2482
+ try {
2483
+ await recoverConversation(workflowId, runId, opts);
2484
+ } catch (err) {
2485
+ logError(err instanceof Error ? err.message : String(err));
2486
+ process.exitCode = 1;
2487
+ }
2488
+ });
2489
+ sharedOpts(
2490
+ temporal.command("capture-history <workflowId> [runId]").description(
2491
+ "Capture a workflow's full history to a replay fixture JSON (proto3-JSON, DECRYPTED payloads) for LOCAL replay debugging. Requires the encoding key. Defaults to ~/.mesh/replay-histories/<workflowId>.json (outside any repo) \u2014 do NOT commit the output."
2492
+ ).option(
2493
+ "--out <path>",
2494
+ "Output path (default: ~/.mesh/replay-histories/<workflowId>.json; a non-gitignored path inside a git repo triggers a loud warning)"
2495
+ )
2496
+ ).action(async (workflowId, runId, opts) => {
2497
+ try {
2498
+ await captureHistory(workflowId, runId, opts);
2499
+ } catch (err) {
2500
+ logError(err instanceof Error ? err.message : String(err));
2501
+ process.exitCode = 1;
2502
+ }
2503
+ });
2504
+ }
2505
+ export {
2506
+ connect,
2507
+ registerTemporalCommands
2508
+ };