@mesh-tech/mesh-cli 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/bin/{mesh.auth-preflight-JO5G7SOF.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
  2. package/dist/bin/{mesh.chunk-CNLSVKYY.js → mesh.chunk-2ASXJ5DO.js} +233 -581
  3. package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
  4. package/dist/bin/{mesh.chunk-WODQO6TP.js → mesh.chunk-6AIG5OJM.js} +3 -3
  5. package/dist/bin/{mesh.chunk-OSV3EZQ3.js → mesh.chunk-6SSOVZOI.js} +1 -1
  6. package/dist/bin/{mesh.chunk-2ZJR5LGN.js → mesh.chunk-7HTKSUWR.js} +1507 -548
  7. package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
  8. package/dist/bin/{mesh.chunk-UI6EFAMD.js → mesh.chunk-F44A3ACW.js} +2 -2
  9. package/dist/bin/{mesh.chunk-C2TKCYHP.js → mesh.chunk-HJGMOEUG.js} +1 -1
  10. package/dist/bin/{mesh.chunk-O6WRZU4X.js → mesh.chunk-NSQJEX36.js} +1 -1
  11. package/dist/bin/{mesh.chunk-C4ZM7DM2.js → mesh.chunk-PZVI7V3J.js} +1 -1
  12. package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
  13. package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
  14. package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
  15. package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
  16. package/dist/bin/{mesh.create-app-SOHBL75U.js → mesh.create-app-Z3RKSTBO.js} +3 -3
  17. package/dist/bin/{mesh.dev-token-server-SS7QEQOQ.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
  18. package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
  19. package/dist/bin/mesh.js +9 -8
  20. package/dist/bin/mesh.js.map +1 -1
  21. package/dist/bin/{mesh.login-6VPMWHHU.js → mesh.login-3AWVGPCM.js} +2 -2
  22. package/dist/bin/{mesh.platform-setup-DJJB2PWL.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
  23. package/dist/bin/{mesh.program-Y4HUNBFP.js → mesh.program-K5JQXEHX.js} +9 -8
  24. package/dist/bin/{mesh.registry-5P4ZLFLJ.js → mesh.registry-JLQYFUNN.js} +4 -4
  25. package/dist/bin/{mesh.registry-identity-ONGUJQAY.js → mesh.registry-identity-3AR27UGO.js} +3 -3
  26. package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
  27. package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
  28. package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
  29. package/dist/bin/{mesh.start-5CPWOHDJ.js → mesh.start-KZ5L6SOJ.js} +2 -2
  30. package/dist/bin/{mesh.wizard-FNRTSGVN.js → mesh.wizard-FCTT55KI.js} +7 -7
  31. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
  32. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
  33. package/dist/build-info.json +2 -2
  34. package/dist/src/commands/dev-doctor.d.ts +22 -0
  35. package/dist/src/commands/dev-doctor.js +149 -0
  36. package/dist/src/commands/dev-env-leaks.d.ts +24 -0
  37. package/dist/src/commands/dev-env-leaks.js +178 -0
  38. package/dist/src/commands/dev-launch.d.ts +17 -1
  39. package/dist/src/commands/dev-launch.js +130 -5
  40. package/dist/src/commands/dev-readiness.d.ts +49 -0
  41. package/dist/src/commands/dev-readiness.js +78 -0
  42. package/dist/src/commands/dev.d.ts +60 -0
  43. package/dist/src/commands/dev.js +539 -140
  44. package/dist/src/commands/hub/index.js +10 -7
  45. package/dist/src/commands/mcp/generate.d.ts +11 -0
  46. package/dist/src/commands/mcp/generate.js +78 -0
  47. package/dist/src/commands/mcp/index.d.ts +2 -0
  48. package/dist/src/commands/mcp/index.js +70 -0
  49. package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
  50. package/dist/src/commands/mcp/manifest-file.js +24 -0
  51. package/dist/src/commands/mcp/review-command.d.ts +24 -0
  52. package/dist/src/commands/mcp/review-command.js +66 -0
  53. package/dist/src/commands/mcp/review.d.ts +41 -0
  54. package/dist/src/commands/mcp/review.js +140 -0
  55. package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
  56. package/dist/src/commands/mcp/workspace-membership.js +77 -0
  57. package/dist/src/commands/secrets/reindex.js +2 -7
  58. package/dist/src/commands/secrets/set.d.ts +22 -1
  59. package/dist/src/commands/secrets/set.js +31 -8
  60. package/dist/src/commands/temporal.js +35 -4
  61. package/dist/src/commands/temporal.js.map +2 -2
  62. package/dist/src/program.js +2 -0
  63. package/dist/src/utils/aws-auth.d.ts +4 -0
  64. package/dist/src/utils/aws-auth.js +7 -3
  65. package/dist/src/utils/dev-sessions-root.d.ts +3 -0
  66. package/dist/src/utils/dev-sessions-root.js +45 -0
  67. package/dist/src/utils/kubeconfig.js +2 -2
  68. package/dist/src/utils/tailscale.js +7 -1
  69. package/dist/src/utils/temporal-auth.js +2 -1
  70. package/dist/src/utils/workflow-fingerprint.js +23 -12
  71. package/fragments/base/index.ts.hbs +3 -0
  72. package/package.json +7 -5
  73. package/skills/mesh/SKILL.md +10 -6
  74. package/templates/api-auth/index.ts.hbs +1 -0
  75. package/templates/api-role-gating/index.ts.hbs +1 -0
  76. package/templates/external-service/index.ts.hbs +1 -0
  77. package/templates/workflow/index.ts.hbs +2 -0
  78. package/dist/bin/mesh.chunk-2ZJR5LGN.js.map +0 -6
  79. package/dist/bin/mesh.chunk-CNLSVKYY.js.map +0 -6
  80. /package/dist/bin/{mesh.auth-preflight-JO5G7SOF.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
  81. /package/dist/bin/{mesh.chunk-WODQO6TP.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
  82. /package/dist/bin/{mesh.chunk-OSV3EZQ3.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
  83. /package/dist/bin/{mesh.chunk-UI6EFAMD.js.map → mesh.chunk-F44A3ACW.js.map} +0 -0
  84. /package/dist/bin/{mesh.chunk-C2TKCYHP.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
  85. /package/dist/bin/{mesh.chunk-O6WRZU4X.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
  86. /package/dist/bin/{mesh.chunk-C4ZM7DM2.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
  87. /package/dist/bin/{mesh.create-app-SOHBL75U.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
  88. /package/dist/bin/{mesh.dev-token-server-SS7QEQOQ.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
  89. /package/dist/bin/{mesh.login-6VPMWHHU.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
  90. /package/dist/bin/{mesh.platform-setup-DJJB2PWL.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
  91. /package/dist/bin/{mesh.program-Y4HUNBFP.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
  92. /package/dist/bin/{mesh.registry-5P4ZLFLJ.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
  93. /package/dist/bin/{mesh.registry-identity-ONGUJQAY.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
  94. /package/dist/bin/{mesh.start-5CPWOHDJ.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
  95. /package/dist/bin/{mesh.wizard-FNRTSGVN.js.map → mesh.wizard-FCTT55KI.js.map} +0 -0
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ let migrated = false;
5
+ export function devSessionsRoot(env = process.env) {
6
+ const configured = env.MESH_DEV_SESSIONS_DIR?.trim();
7
+ const root = configured ||
8
+ path.join(env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'), 'mesh', 'dev-sessions');
9
+ fs.mkdirSync(root, { recursive: true });
10
+ if (!configured && !migrated) {
11
+ migrated = true;
12
+ migrateLegacySessionsDir(path.join(env.TMPDIR?.trim() || os.tmpdir(), 'mesh-dev-sessions'), root);
13
+ }
14
+ return root;
15
+ }
16
+ export function migrateLegacySessionsDir(legacy, root) {
17
+ try {
18
+ if (path.resolve(legacy) === path.resolve(root))
19
+ return;
20
+ let stat;
21
+ try {
22
+ stat = fs.lstatSync(legacy);
23
+ }
24
+ catch {
25
+ stat = undefined;
26
+ }
27
+ if (stat?.isSymbolicLink())
28
+ return;
29
+ if (stat?.isDirectory()) {
30
+ for (const entry of fs.readdirSync(legacy)) {
31
+ const to = path.join(root, entry);
32
+ if (!fs.existsSync(to))
33
+ fs.cpSync(path.join(legacy, entry), to, { recursive: true, preserveTimestamps: true });
34
+ }
35
+ fs.renameSync(legacy, `${legacy}.migrated-${Date.now()}`);
36
+ }
37
+ fs.mkdirSync(path.dirname(legacy), { recursive: true });
38
+ fs.symlinkSync(root, legacy, 'dir');
39
+ }
40
+ catch {
41
+ }
42
+ }
43
+ export function resetDevSessionsRootForTests() {
44
+ migrated = false;
45
+ }
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
- import * as os from 'os';
3
2
  import * as path from 'path';
3
+ import { devSessionsRoot } from './dev-sessions-root.js';
4
4
  export function buildKubeconfig(cluster, region) {
5
5
  return `apiVersion: v1
6
6
  kind: Config
@@ -31,7 +31,7 @@ users:
31
31
  `;
32
32
  }
33
33
  export function sessionKubeconfigPath(sessionName) {
34
- return path.join(os.tmpdir(), 'mesh-dev-sessions', `${sessionName}.kubeconfig`);
34
+ return path.join(devSessionsRoot(), `${sessionName}.kubeconfig`);
35
35
  }
36
36
  export function resolveHubPlatformName(platform) {
37
37
  return platform?.name ?? 'mesh';
@@ -434,8 +434,14 @@ export async function ensureForwarders(state) {
434
434
  }
435
435
  export function rewriteTunnelsForTargets(devOutput, targets) {
436
436
  const tunnels = { ...devOutput.tunnels };
437
+ for (const [name, tunnel] of Object.entries(devOutput.tunnels)) {
438
+ const target = targets.find((t) => t.name === name) ??
439
+ targets.find((t) => t.targetHost === tunnel.host && t.remotePort === tunnel.port);
440
+ if (target)
441
+ tunnels[name] = { host: "localhost", port: target.localPort };
442
+ }
437
443
  for (const t of targets) {
438
- tunnels[t.name] = { host: "localhost", port: t.localPort };
444
+ tunnels[t.name] ??= { host: "localhost", port: t.localPort };
439
445
  }
440
446
  return { ...devOutput, tunnels };
441
447
  }
@@ -74,7 +74,8 @@ export async function resolveTemporalAuth(tenant, env, platformName = tenant) {
74
74
  if (/\.kube[/\\]config|error loading config file/i.test(`${detail} ${stderr}`)) {
75
75
  logWarn("kubectl could not read a kubeconfig and fell back to ~/.kube/config " +
76
76
  "(macOS blocks it for shells without Full Disk Access). Run inside the " +
77
- "mesh dev tmux session, or export KUBECONFIG=$TMPDIR/mesh-dev-sessions/<session>.kubeconfig.");
77
+ "mesh dev tmux session, or export KUBECONFIG=~/.config/mesh/dev-sessions/<session>.kubeconfig " +
78
+ "($MESH_DEV_SESSIONS_DIR/<session>.kubeconfig when that is set).");
78
79
  }
79
80
  }
80
81
  }
@@ -7,6 +7,7 @@ export const WORKFLOW_PACKAGE_DIRS = [
7
7
  "packages/agent-contracts/src",
8
8
  ];
9
9
  const EXCLUDE = /(^|\/)(node_modules|dist|__tests__)(\/|$)|\.(test|spec)\.[cm]?tsx?$/;
10
+ const EXCLUDED_DIR_NAMES = new Set(["node_modules", "dist", "__tests__"]);
10
11
  export function findRepoRoot(startDir) {
11
12
  let dir = startDir;
12
13
  for (;;) {
@@ -33,22 +34,32 @@ export function resolveWorkerSourceDirs(repoRoot, services = {}) {
33
34
  }
34
35
  return dirs;
35
36
  }
36
- export function computeWorkflowFingerprint(dirs, relativeTo) {
37
- const files = [];
38
- for (const dir of dirs) {
39
- let rels;
40
- try {
41
- rels = readdirSync(dir, { recursive: true });
42
- }
43
- catch {
37
+ function listDir(dir) {
38
+ try {
39
+ return readdirSync(dir, { withFileTypes: true });
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ function collectSourceFiles(dir, files) {
46
+ for (const entry of listDir(dir)) {
47
+ if (entry.isSymbolicLink())
44
48
  continue;
49
+ if (entry.isDirectory()) {
50
+ if (!EXCLUDED_DIR_NAMES.has(entry.name))
51
+ collectSourceFiles(join(dir, entry.name), files);
45
52
  }
46
- for (const rel of rels) {
47
- const p = String(rel);
48
- if (/\.(ts|tsx)$/.test(p) && !EXCLUDE.test(p))
49
- files.push(join(dir, p));
53
+ else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !EXCLUDE.test(entry.name)) {
54
+ files.push(join(dir, entry.name));
50
55
  }
51
56
  }
57
+ }
58
+ export function computeWorkflowFingerprint(dirs, relativeTo) {
59
+ const files = [];
60
+ for (const dir of dirs) {
61
+ collectSourceFiles(dir, files);
62
+ }
52
63
  files.sort();
53
64
  if (files.length === 0)
54
65
  return "";
@@ -76,6 +76,8 @@ const env = new mesh.apps.AppEnvironment("env", {
76
76
  codecServer: {
77
77
  ingressSubdomain: "codec-{{name}}",
78
78
  },
79
+ // Opt in to hold payloads over 20 KiB in the namespace's own bucket, by hash, outside Event History.
80
+ // claimCheck: true,
79
81
  },
80
82
  {{/if}}
81
83
  {{/if}}
@@ -249,6 +251,7 @@ new mesh.auth.AppAuthzPointer("authz", {
249
251
  });
250
252
 
251
253
  {{/if}}
254
+ // `register()` comes after every component; one created after the entry point awaits throws at preview.
252
255
  export const app = env.register({
253
256
  // The line under this app's name on the Hub's Apps list, read by the
254
257
  // client's own operators more often than by anyone on the team. One
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mesh-tech/mesh-cli",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "CLI for Mesh platform development utilities",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -41,13 +41,14 @@
41
41
  "./package.json": "./package.json"
42
42
  },
43
43
  "devDependencies": {
44
- "@mesh-tech/agent-contracts": "0.2.0",
45
- "@mesh-tech/tsconfig": "2.41.0",
44
+ "@mesh-tech/agent-contracts": "0.3.0",
45
+ "@mesh-tech/tsconfig": "2.43.0",
46
46
  "@mesh-tech/workflow-viz": "0.3.1",
47
47
  "@tanstack/intent": "^0.3.6",
48
48
  "@types/node": "^20.0.0",
49
49
  "@types/react": "^19.1.8",
50
50
  "@types/react-dom": "^19.1.6",
51
+ "eventsource": "^3.0.7",
51
52
  "tailwindcss": "^4.2.2"
52
53
  },
53
54
  "dependencies": {
@@ -60,6 +61,7 @@
60
61
  "@inquirer/input": "^4.2.1",
61
62
  "@inquirer/password": "^4.0.17",
62
63
  "@inquirer/select": "^4.3.1",
64
+ "@modelcontextprotocol/client": "^2.0.0",
63
65
  "chalk": "^5.3.0",
64
66
  "commander": "^12.1.0",
65
67
  "handlebars": "^4.7.8",
@@ -82,8 +84,8 @@
82
84
  },
83
85
  "optionalDependencies": {
84
86
  "@aws-sdk/client-s3": "^3.864.0",
85
- "@temporalio/client": "^1.17.0",
86
- "@temporalio/common": "^1.17.0"
87
+ "@temporalio/client": "^1.24.0",
88
+ "@temporalio/common": "^1.24.0"
87
89
  },
88
90
  "main": "./dist/src/index.js",
89
91
  "types": "./dist/src/index.d.ts"
@@ -186,7 +186,8 @@ Top-level commands (each `register*` is wired in `src/program.ts`, which `bin/me
186
186
  | `mesh stack` | `init`, `status`, `rm <name>` | `init` in an **app** creates a personal app stack `dev-{github-user}`, copying base-stack config, setting `mesh:deploy: false`, inheriting KMS secrets provider; `--worktree` appends the git-worktree token (`dev-{user}-{token}`) so concurrent worktrees deploy to **distinct** stacks (SSM paths, Temporal namespace, Nexus endpoint all derive from the stack name → one discriminator isolates all three; no-op on the primary checkout). `init --platform` in a platform repo's **`platform/`** creates a **personal platform** `{tenant}-{github-user}-dev` — its own Zitadel, Hub, Temporal and tenant envs on the base stack's core (`mesh:coreEnv`, `mesh:clusterOwner false`, registry/VPN singletons off, deploy on); `--parent-zone` gives it its own DNS zone. `status` is the read-only drift check: for each platform this directory deploys (or an app deploys to), pinned vs deployed (the `core` and `platform` SSM registrations) vs latest `@mesh-tech/infra-components` / `@mesh-tech/hub`, plus this CLI vs the latest `@mesh-tech/mesh-cli`, with the upgrade steps when anything is behind. `rm <name>` removes a personal stack (backend state + local config), credentialed. `init`/`rm` assume the deployer role — no raw `pulumi` / manual creds needed. | `init`: `--from <stack>`, `--name <stack>`, `--worktree`, `--adopt`, `--platform`, `--parent-zone <zone>`; `status`: `--stack`, `--profile`, `--region`, `--json`, `--check` (exit 1 when behind); `rm`: `--yes` |
187
187
  | `mesh tenant` | `add <name>`, `list` | Register/inspect app tenants on a platform stack. Run from the **platform layer** of a tenant platform repo (e.g. `mesh-sandbox/platform`). `add` inserts the tenant under `mesh:tenants` in `Pulumi.<stack>.yaml` via a comment-preserving YAML round-trip (untouched lines don't reformat), refuses a tenant that is already declared (an existing entry may carry fields a re-add would drop), derives the `{name}-{env}` subdomain from the stack name (echoed in the success line), and guards against running on the core layer or an app dir; it then prints the deploy + verify sequence — `mesh deploy up` is what makes the registration live. `list` prints what the stack config declares, so "is it registered?" is answerable without opening YAML (warns when the project doesn't look like a platform layer, so an empty result isn't mistaken for "no tenants"). | `add`: `--display-name <name>`, `--subdomain <sub>`, `--zitadel-org-id <id>`, `--stack <stack>`, `--json`; `list`: `--stack <stack>`, `--json` |
188
188
  | `mesh tunnel [group]` | `external <name>` | SSM port-forward to platform services. Groups: `dev` (temporal+db), `temporal`. **`external <name>`** tunnels to a *registered* `ExternalService`: host/port are read from its credential secret (`mesh/{app-tenant}/{app-stage}/external/{name}`) — the same values deployed apps resolve at runtime — so anything the bastion can reach is tunnelable with no per-service CLI entry and no bastion services-map change. `-t/-e` are **optional inside an app directory** — they fall back to the same Pulumi/SST context detection `mesh dev` uses, which fails closed elsewhere, so pass `-t` outside an app dir (`-t` alone is enough; `-e` alone is not). `-e` names a platform env, not a stack, so it no longer doubles as the stage hint: the credential axis `external` keys on stays the app's real stack name. And `AWS_REGION`/`AWS_PROFILE` default from the nearest Pulumi config + the `{tenant}-{env}` profile convention (region falls back to the effective profile's own `region` — the `[default]` profile when none was resolved — before the platform default us-east-2). | root: `-t/--tenant`, `-e/--env`, `-l/--list`, `-s/--services <csv\|group>`, `--db-port`, `--temporal-ui-port`, `--temporal-frontend-port`; `external`: `-p/--port` (default: the remote port), `-k/--key <key>` (multi-instance `credentials.keyedBy` externals), `--app-tenant`/`--app-stage` (credential axis; default to `-t`/`-e`) |
189
- | `mesh secrets` | `exec <command...>`, `set [service]`, `reindex [service]`, `migrate-config` | Manage external-service creds in Secrets Manager. `exec` injects secrets as env; `set` reads field schema from SSM and prompts (or `--json`) — with `--key` it also records the instance in the service's instance index (`{prefix}/.index`, what `listInstances()` reads). `reindex` rebuilds that index from the per-key secrets actually in Secrets Manager (operator-credential `ListSecrets`) — the repair for an index that drifted (out-of-band secret create/delete, concurrent writers, malformed value) and the populate step when rolling the index out to an estate: **deploy the declaring stack first** (creates the managed, empty index), then `reindex`. All three resolve tenant + platform env from the cwd stack file and the app's `config.ts`; `-t/--tenant` overrides the tenant for one invocation, and with several `Pulumi.<stack>.yaml` files present pass `--stack` rather than relying on the base-stack pick. | `set`: `--key <id>` (multi-instance), `--all`, `--json <str>`, `-t/--tenant`, `--stack`, `--region`; `reindex`: `--dry-run`, `-t/--tenant`, `--stack`, `--region`; `migrate-config`: `--force`, `--dry-run`, `-t/--tenant`, `--stack`, `--region` |
189
+ | `mesh secrets` | `exec <command...>`, `set [service]`, `reindex [service]`, `migrate-config` | Manage external-service creds in Secrets Manager. `exec` injects secrets as env; `set` reads field schema from SSM and prompts (or `--json`) — with `--key` it also records the instance in the service's instance index (`{prefix}/.index`, what `listInstances()` reads). `reindex` rebuilds that index from the per-key secrets actually in Secrets Manager (operator-credential `ListSecrets`) — the repair for an index that drifted (out-of-band secret create/delete, concurrent writers, malformed value) and the populate step when rolling the index out to an estate: **deploy the declaring stack first** (creates the managed, empty index), then `reindex`. All three resolve tenant + platform env from the cwd stack file and the app's `config.ts`; `-t/--tenant` overrides the tenant for one invocation, and with several `Pulumi.<stack>.yaml` files present pass `--stack` rather than relying on the base-stack pick. `set`, `reindex` and `mesh mcp review` share one lookup (`discoverStackExternalServices`): the stack's `mesh:platform` env, and only that stack's registration, never a sibling stack's on the same env. | `set`: `--key <id>` (multi-instance), `--all`, `--json <str>`, `-t/--tenant`, `--stack`, `--region`; `reindex`: `--dry-run`, `-t/--tenant`, `--stack`, `--region`; `migrate-config`: `--force`, `--dry-run`, `-t/--tenant`, `--stack`, `--region` |
190
+ | `mesh mcp` | `review <manifest>`, `generate <manifest>` | Manifest-driven MCP Integrations (`mesh.apps.McpIntegration`). `review` fetches the server's `tools/list` with the Integration's own credential (stack resolved like `mesh secrets set`), diffs it against the reviewed snapshot with each tool's access class (a vendor `readOnlyHint` is shown as a suggestion, never a `read`), writes on confirmation and regenerates the worker package; it never enables a new upstream tool. `generate` writes the worker package the component deploys (`<manifest dir>/.mesh-mcp/<name>`: `package.json`, `main.ts`, `openapi.json`), keeping committed versions, and appends `<manifest dir>/.mesh-mcp/*` to `pnpm-workspace.yaml` when no glob covers the package (`pnpm install` records only workspace members; the apps-repo template's `apps/*`, `apps/*/*` do not cover it) or prints the glob when it cannot edit the file; run `pnpm install` and commit it with the lockfile. | `review`: `--yes`, `--dry-run`, `--stack`, `-t/--tenant`, `--secret <id>`, `--region`; `generate`: `--out <dir>`, `--refresh-versions` |
190
191
  | `mesh artifacts` | `get <ref>` | Download workflow artifact files from an AI-agent conversation. Ref `<conversationId>:<artifactId>` (artifactId defaults to `workflow-artifact`). | `-o/--output <dir>`, `--target <name>`, `--api-url <url>`, `--context <ctx>` (default `mesh.dev`) |
191
192
  | `mesh conversations` (alias `conv`) | `list`, `show <id>`, `artifacts <id>` | Read a deployed Mesh agent's conversations: list the caller's conversations, render a transcript, or list a conversation's artifacts. Resolves against the same `agent-targets` registry the MCP `agent_*` tools use. | all three: `--target <name>`, `--api-url <url>` (overrides `--target`), `--context <ctx>` (used with `--api-url`, default `mesh.dev`), `--json` (default when stdout isn't a TTY) |
192
193
  | `mesh vpn` | `connect <context>`, `disconnect`, `status`, `tunnel <up\|down\|status>`, `api-key`, `pre-auth-key`, `users` | Headscale VPN mgmt via Tailscale + kubectl. **`connect` now defaults to the userspace `tailscaled`** (brew formula, NOT the GUI Tailscale.app) — the same daemon `mesh dev --transport tailscale` / `mesh vpn tunnel` use, so it **runs headless / under the sandbox** and exposes a local SOCKS5 proxy (route a tool via `ALL_PROXY=socks5://127.0.0.1:<port>`). It relays a one-time browser-register URL for Zitadel auth. `--system` opts into the whole-machine GUI Tailscale.app (system TUN) — needs the app installed and **cannot run headless/sandboxed** (Sparkle.framework abort). For auto-forwarded VPC services (Temporal/RDS) prefer **`mesh vpn tunnel up`** or `mesh dev`. `tunnel status` reports runner **ownership** (`Runner: owned (verified)` vs `NOT OWNED (<reason>)` — a port answering a dial is not proof it's mesh's); `--json` adds `owned` (bool) + `ownership` (reason string). Admin subcommands `exec` into the `headscale-0` pod. | root: `-t/--tenant` (default `mesh`), `-e/--env` (default `dev`), `-n/--namespace`; `connect`/`disconnect`: `--system`; `tunnel up`: `--context <ctx>`; `tunnel down`: `--stop` (stop daemon, keep login), `--logout`; `tunnel status`: `--json`; `pre-auth-key`: `-u/--user` (**required**), `--expiration <24h>`, `--reusable`, `--ephemeral` |
@@ -436,13 +437,16 @@ code. If the **workflow** source (`agent-sdk` / `workflow-interpreter` /
436
437
  against the new code — a replay-incompatible change **strands** them. `mesh dev` now
437
438
  fingerprints that source at launch and **warns + refuses (use `--force`)** when it
438
439
  changed on a relaunch/worker-restart. The fix is not `--force`: keep the change
439
- replay-compatible — gate it with `wf.patched()` and regenerate the replay goldens
440
- (see the `temporal-workflow-safety` skill + the CI replay gate). Restarting unchanged
440
+ replay-compatible — gate it with `wf.patched()` and commit a frozen taken/not-taken fixture
441
+ pair for the gate (the `temporal-replay` skill's patch rule); never regenerate an existing
442
+ golden (see the CI replay gate). Restarting unchanged
441
443
  code, or a non-worker service (ui/api), never warns.
442
444
 
443
445
  For **config/deploy** changes (not code): `mesh dev restart <svc> --refresh-env`
444
- re-reads stack/SSM output; if the *topology* changed, run `mesh deploy up` first
445
- (`mesh dev doctor` flags config staleness).
446
+ re-reads stack/SSM output; if the *topology* changed, run `mesh deploy up` first.
447
+ `mesh dev` itself warns, at launch and at `--refresh-env`, when the stack's stored
448
+ outputs are older than the program or stack config ("Stack outputs are older than
449
+ the program (…); run `mesh deploy up --stack <s>` to refresh the dev env.").
446
450
 
447
451
  ## Config / environment requirements
448
452
 
@@ -469,7 +473,7 @@ re-reads stack/SSM output; if the *topology* changed, run `mesh deploy up` first
469
473
  | `AssumeRoleWithWebIdentity … web identity token could not be validated` (`mesh deploy`, `mesh dev --stack`) | the cached session is from a different platform's Zitadel than the one the stack's deployer role trusts — e.g. a `mesh.dev` session (the package-registry front door) against a stack on the `mesh`/`dev` platform, which signs in as `mesh-platform.dev` | run the `mesh login <context> --device` the preflight prints (it is derived from the stack: `mesh:loginContext`, else `mesh:platform`); for a personal platform, set `mesh:loginContext` to its domain. Never fall back to exported AWS keys |
470
474
  | `login` config not found | context not discoverable | a known first-party short context (`mesh.dev`) resolves anonymously — no AWS needed. Otherwise use the platform's full domain (`mesh login dev.<tenant>.meshtech.io`). Only if neither works: ensure AWS SSO is active + the platform is deployed (SSM `cliClientId`), or add config to `~/.config/mesh/config.json` |
471
475
  | `vpn`/`temporal --show-payloads` fail | kubeconfig not set / expired creds | refresh the tenant cluster kubeconfig (`mesh vpn connect <ctx>` wires it; kubeconfig absorption into `mesh deploy` is planned) then retry | <!-- skill-lint-disable-next-line — next row's backticked `kubectl …` is a quoted ERROR SYMPTOM, not an operator command; the row's fix column already routes through mesh dev -->|
472
- | `kubectl could not read a kubeconfig … macOS blocks it` (from `mesh kubectl`/`logs`/`exec`/`temporal` in a bare shell) | macOS Full-Disk-Access blocks reading `~/.kube` from a shell outside the `mesh dev` session | run the command **inside** the `mesh dev` tmux window, or `export KUBECONFIG=$TMPDIR/mesh-dev-sessions/<session>.kubeconfig` first |
476
+ | `kubectl could not read a kubeconfig … macOS blocks it` (from `mesh kubectl`/`logs`/`exec`/`temporal` in a bare shell) | macOS Full-Disk-Access blocks reading `~/.kube` from a shell outside the `mesh dev` session | run the command **inside** the `mesh dev` tmux window, or `export KUBECONFIG=~/.config/mesh/dev-sessions/<session>.kubeconfig` first (the sessions root is `$MESH_DEV_SESSIONS_DIR` when set) |
473
477
  | `mesh temporal …` won't authenticate against a **deployed** Temporal | passing `--address` (or `TEMPORAL_ADDRESS`) skips the app/stack resolution that mints a Zitadel **Bearer** — auth falls back to ambient env | omit `--address` and run from the app dir so `mesh temporal` resolves the authenticated frontend over the tunnel; use `--address` only for a local/unauthenticated Temporal |
474
478
  | `mesh dev` chose "VPN direct" then api/worker spin on `ECONNREFUSED …:27233` | tailnet-blind VPN *presence* check picked direct routing the mesh VPC doesn't actually serve | **no manual fallback needed** — current `mesh dev` TCP-probes the frontend and auto-falls back to SSM tunnels; if still stuck, force `--transport ssm` (or `--no-vpn`) |
475
479
  | `mesh dev` / `mesh vpn tunnel up` → `Tunnel port(s) for tenant '<t>' are bound by a process mesh does not own` | a foreign or orphaned listener holds a forwarder port; mesh refuses to adopt it rather than route traffic to the wrong upstream | `mesh vpn -t <t> tunnel down --stop` to reap mesh-owned runners; if the port is still held, `lsof -nP -iTCP:<port> -sTCP:LISTEN` and stop that process (if it names another mesh runner, it belongs to a different tenant — tear that one down). `mesh vpn -t <t> tunnel status` shows `Runner: owned (verified)` when healthy |
@@ -124,6 +124,7 @@ new mesh.auth.AppAuthzPointer("authz", { env, mode: "policy-engine", opsHubMetad
124
124
 
125
125
  // --- Outputs ---
126
126
 
127
+ // `register()` comes after every component; one created after the entry point awaits throws at preview.
127
128
  export const app = env.register({
128
129
  displayName: "{{titleCase name}}",
129
130
  // What a client operator reads on the Apps list. One sentence, their words.
@@ -109,6 +109,7 @@ new mesh.auth.AppAuthzPointer("authz", {
109
109
  opsHubMetadata: compileOpsHubMetadata(schemaDef),
110
110
  });
111
111
 
112
+ // `register()` comes after every component; one created after the entry point awaits throws at preview.
112
113
  export const app = env.register({
113
114
  displayName: "{{titleCase name}}",
114
115
  // What a client operator reads on the Apps list. One sentence, their words.
@@ -85,6 +85,7 @@ const api = new mesh.apps.Service("api", {
85
85
 
86
86
  // --- Outputs ---
87
87
 
88
+ // `register()` comes after every component; one created after the entry point awaits throws at preview.
88
89
  export const app = env.register({
89
90
  description: "{{titleCase name}} — external-service (vendor credentials) pattern",
90
91
  });
@@ -27,6 +27,7 @@ const env = new mesh.apps.AppEnvironment("env", {
27
27
  appName: "{{name}}",
28
28
  namespace: true,
29
29
  rootDir: __dirname,
30
+ // `temporal: { claimCheck: true }` holds payloads over 20 KiB in the namespace's own bucket, by hash, outside Event History.
30
31
  temporal: true,
31
32
  });
32
33
 
@@ -66,6 +67,7 @@ const worker = new mesh.apps.TemporalWorker("worker", {
66
67
 
67
68
  // --- Outputs ---
68
69
 
70
+ // `register()` comes after every component; one created after the entry point awaits throws at preview.
69
71
  export const app = env.register({
70
72
  displayName: "{{titleCase name}}",
71
73
  // What a client operator reads on the Apps list. One sentence, their words.