@mesh-tech/mesh-cli 0.34.3 → 0.35.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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "e4eb14cb8",
3
- "builtAt": "2026-09-25T02:38:02.510Z"
2
+ "commit": "cf1f2fb06",
3
+ "builtAt": "2026-09-25T19:28:25.368Z"
4
4
  }
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { logError, logInfo } from "../utils/log.js";
3
3
  import { findAppRoot, findStackConfigs, getCurrentStack, pulumiStackOutput, readStackConfig, } from "../utils/pulumi.js";
4
- import { resolveAwsCredentials, derivePlatformContext } from "../utils/aws-auth.js";
4
+ import { resolveAwsCredentials, derivePlatformContext, readStackPlatform } from "../utils/aws-auth.js";
5
5
  import { ensureKubeconfig, resolveHubPlatformName, } from "../utils/kubeconfig.js";
6
6
  import { probeCredentials } from "./login.js";
7
7
  import { credProbeToPreflightError } from "../utils/pulumi-run.js";
@@ -26,7 +26,7 @@ async function resolveClusterAccess(opts) {
26
26
  : null;
27
27
  if (deployerRole && !resolved && !process.env.AWS_ACCESS_KEY_ID) {
28
28
  const pf = ctx
29
- ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx)
29
+ ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx, readStackPlatform(appRoot, stack))
30
30
  : null;
31
31
  throw new Error(pf?.message ??
32
32
  `Couldn't resolve AWS credentials. Run: mesh login ${ctx ?? "mesh.dev"} --device`);
@@ -6,7 +6,7 @@ import * as path from 'path';
6
6
  import { Option } from 'commander';
7
7
  import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
8
8
  import { logInfo, logSuccess, logError, logWarn, isVpnConnected, getTailscaleInfo, headscaleDnsConfig, registerDnsRecords, unregisterDnsRecords, readDnsRecords, getPlatformBastionInfo, } from '../utils/index.js';
9
- import { derivePlatformContext, renderCredentialProcessProfile, resolveAwsCredentials, resolveStableMeshBin, selectRoleForCaller, stripBareProfile, upsertManagedAwsConfigSection, } from '../utils/aws-auth.js';
9
+ import { derivePlatformContext, readStackPlatform, renderCredentialProcessProfile, resolveAwsCredentials, resolveStableMeshBin, selectRoleForCaller, stripBareProfile, upsertManagedAwsConfigSection, } from '../utils/aws-auth.js';
10
10
  import { atomicWriteFileSync, readCredentials, getContextConfig, probeCredentials, getValidToken } from './login.js';
11
11
  import { resolveVpnJoinBroker, mintPreAuthKey } from '../utils/vpn-join.js';
12
12
  import { credProbeToPreflightError } from '../utils/pulumi-run.js';
@@ -2218,7 +2218,9 @@ export function registerDevCommand(program) {
2218
2218
  }
2219
2219
  const resolved = deployerRole ? await resolveAwsCredentials(deployerRole, appRoot, stack) : null;
2220
2220
  if (deployerRole && !resolved && !process.env.AWS_ACCESS_KEY_ID) {
2221
- const pf = ctx ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx) : null;
2221
+ const pf = ctx
2222
+ ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx, readStackPlatform(appRoot, stack))
2223
+ : null;
2222
2224
  logError(pf?.message ??
2223
2225
  `Couldn't resolve AWS credentials to restart '${service}'. Run: mesh login ${ctx ?? 'mesh.dev'} --device`);
2224
2226
  process.exit(1);
@@ -49,6 +49,7 @@ export declare function renderAwsFallbackStatus(context: string, reason: string,
49
49
  lines: StatusLine[];
50
50
  exitCode: 0 | 1;
51
51
  };
52
+ export declare function deployableTenants(idToken: string, accessToken: string | undefined): string[];
52
53
  export declare function tokenAudience(payload: Record<string, unknown>): string[];
53
54
  export declare function registerLoginCommand(program: Command): void;
54
55
  export declare const RESERVED_REGISTRY_CONTEXT = "registry";
@@ -6,7 +6,7 @@ import { execFileSync } from "child_process";
6
6
  import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js";
7
7
  import { MeshCliError } from "../utils/errors.js";
8
8
  import { assumeRoleCredentials, assumeRoleWithWebIdentity, describeAssumeFailure, probeAwsIdentity, renderCredentialProcessProfile, tokenIssuer, resolveStableMeshBin, selectRoleForCaller, toCredentialProcessJson, upsertManagedAwsConfigSection, } from "../utils/aws-auth.js";
9
- import { firstPartyDomainFor } from "../utils/first-party-contexts.js";
9
+ import { firstPartyDomainFor, isPlatformLoginAlias } from "../utils/first-party-contexts.js";
10
10
  import { tailscaleBackendState } from "./vpn/index.js";
11
11
  const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME ?? path.join(process.env.HOME ?? "~", ".config"), "mesh");
12
12
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
@@ -320,14 +320,7 @@ function login(context, config) {
320
320
  idPayload.preferred_username ??
321
321
  "unknown";
322
322
  const sub = idPayload.sub;
323
- let tenants = [];
324
- try {
325
- const accessPayload = decodeJwtPayload(tokens.access_token);
326
- tenants =
327
- accessPayload["urn:mesh:tenants"] ?? [];
328
- }
329
- catch {
330
- }
323
+ const tenants = deployableTenants(tokens.id_token, tokens.access_token);
331
324
  writeCredentials(context, {
332
325
  idToken: tokens.id_token,
333
326
  accessToken: tokens.access_token,
@@ -442,12 +435,7 @@ async function attemptDeviceCode(context, config) {
442
435
  const idPayload = decodeJwtPayload(tokens.id_token);
443
436
  const email = idPayload.email ?? idPayload.preferred_username ?? "unknown";
444
437
  const sub = idPayload.sub;
445
- let tenants = [];
446
- try {
447
- const accessPayload = decodeJwtPayload(tokens.access_token);
448
- tenants = accessPayload["urn:mesh:tenants"] ?? [];
449
- }
450
- catch { }
438
+ const tenants = deployableTenants(tokens.id_token, tokens.access_token);
451
439
  writeCredentials(context, {
452
440
  idToken: tokens.id_token,
453
441
  accessToken: tokens.access_token,
@@ -620,12 +608,10 @@ async function showStatus(context, config) {
620
608
  await showAwsCredentialStatus(context, `Zitadel token expired at ${creds.expiresAt}`, config);
621
609
  return;
622
610
  }
623
- let tenants = [];
611
+ const tenants = deployableTenants(creds.idToken, creds.accessToken);
624
612
  let audience = [];
625
613
  try {
626
- const accessPayload = decodeJwtPayload(creds.accessToken);
627
- tenants = accessPayload["urn:mesh:tenants"] ?? [];
628
- audience = tokenAudience(accessPayload);
614
+ audience = tokenAudience(decodeJwtPayload(creds.accessToken));
629
615
  }
630
616
  catch {
631
617
  }
@@ -648,6 +634,23 @@ async function showStatus(context, config) {
648
634
  `\`mesh login ${context}\` re-checks what the platform publishes.`);
649
635
  }
650
636
  }
637
+ const DEPLOY_TENANTS_CLAIM = "urn:mesh:tenants";
638
+ export function deployableTenants(idToken, accessToken) {
639
+ for (const token of [idToken, accessToken]) {
640
+ if (!token)
641
+ continue;
642
+ let claim;
643
+ try {
644
+ claim = decodeJwtPayload(token)[DEPLOY_TENANTS_CLAIM];
645
+ }
646
+ catch {
647
+ continue;
648
+ }
649
+ if (Array.isArray(claim))
650
+ return claim.filter((t) => typeof t === "string");
651
+ }
652
+ return [];
653
+ }
651
654
  export function tokenAudience(payload) {
652
655
  const aud = payload.aud;
653
656
  if (typeof aud === "string")
@@ -938,7 +941,7 @@ export async function ensureLogin(context, opts = {}) {
938
941
  }
939
942
  async function discoverConfigGuarded(context, opts = {}) {
940
943
  let config = null;
941
- if (parseTenantEnv(context)) {
944
+ if (parseTenantEnv(context) && !isPlatformLoginAlias(context)) {
942
945
  config = await discoverConfigFromSsm(context, opts);
943
946
  }
944
947
  if (!config) {
@@ -1,13 +1,18 @@
1
1
  import { Command } from "commander";
2
2
  export declare const INTENT_RANGE = "^0.3.6";
3
3
  export declare function resolveTargetRoot(startDir?: string): string;
4
- export declare function listPackageSkills(root: string): Array<{
4
+ export declare function installedSkillName(segments: string[]): string;
5
+ export declare function renderManagedSkill(source: string, name: string): string;
6
+ interface PackageSkill {
5
7
  name: string;
8
+ legacyName: string;
6
9
  dir: string;
7
10
  source: string;
8
- }>;
11
+ }
12
+ export declare function listPackageSkills(root: string): Array<PackageSkill>;
9
13
  export declare function seedAppSkill(root: string, appName: string, appRelPath: string): string | null;
10
14
  export declare function syncSkills(root: string, opts?: {
11
15
  check?: boolean;
12
16
  }): boolean;
13
17
  export declare function registerSkillsCommands(program: Command): void;
18
+ export {};
@@ -30,14 +30,32 @@ function listBaseSkills() {
30
30
  return fs
31
31
  .readdirSync(dir, { withFileTypes: true })
32
32
  .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(dir, entry.name, "SKILL.md")))
33
- .map((entry) => ({ name: entry.name, source: path.join(dir, entry.name, "SKILL.md") }));
33
+ .map((entry) => ({
34
+ name: installedSkillName([entry.name]),
35
+ legacyName: entry.name === "mesh" ? "mesh-core" : `mesh-${entry.name}`,
36
+ source: path.join(dir, entry.name, "SKILL.md"),
37
+ }));
34
38
  }
35
- function renderManagedSkill(source) {
39
+ export function installedSkillName(segments) {
40
+ const parts = ["mesh"];
41
+ for (const segment of segments) {
42
+ if (segment === "core")
43
+ continue;
44
+ if (segment === parts[parts.length - 1])
45
+ continue;
46
+ parts.push(segment);
47
+ }
48
+ return parts.join("-");
49
+ }
50
+ export function renderManagedSkill(source, name) {
36
51
  const raw = fs.readFileSync(source, "utf-8");
37
52
  const frontmatter = raw.match(/^(---\n[\s\S]*?\n---\n)/)?.[1];
38
53
  if (!frontmatter)
39
- return `${MANAGED_MARKER}\n\n${raw}`;
40
- return `${frontmatter}\n${MANAGED_MARKER}\n${raw.slice(frontmatter.length)}`;
54
+ return `---\nname: ${name}\n---\n\n${MANAGED_MARKER}\n\n${raw}`;
55
+ const named = /^name:.*$/m.test(frontmatter)
56
+ ? frontmatter.replace(/^name:.*$/m, `name: ${name}`)
57
+ : frontmatter.replace(/^---\n/, `---\nname: ${name}\n`);
58
+ return `${named}\n${MANAGED_MARKER}\n${raw.slice(frontmatter.length)}`;
41
59
  }
42
60
  const BASE_SKILL_PACKAGE = "mesh-cli";
43
61
  function meshTechScopeDirs(root, maxDepth = 6) {
@@ -84,14 +102,35 @@ export function listPackageSkills(root) {
84
102
  const source = path.join(skillsDir, domain.name, "SKILL.md");
85
103
  if (!fs.existsSync(source))
86
104
  continue;
87
- const name = `mesh-${pkg.name}-${domain.name}`;
88
- if (!byName.has(name))
89
- byName.set(name, { name, dir: path.join(skillsDir, domain.name), source });
105
+ const name = installedSkillName([pkg.name, domain.name]);
106
+ if (!byName.has(name)) {
107
+ byName.set(name, {
108
+ name,
109
+ legacyName: `mesh-${pkg.name}-${domain.name}`,
110
+ dir: path.join(skillsDir, domain.name),
111
+ source,
112
+ });
113
+ }
90
114
  }
91
115
  }
92
116
  }
93
117
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
94
118
  }
119
+ function extrasInSync(sourceDir, targetDir) {
120
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true, recursive: true })) {
121
+ if (!entry.isFile())
122
+ continue;
123
+ const rel = path.relative(sourceDir, path.join(entry.parentPath, entry.name));
124
+ if (rel === "SKILL.md")
125
+ continue;
126
+ const installed = path.join(targetDir, rel);
127
+ if (!fs.existsSync(installed))
128
+ return false;
129
+ if (!fs.readFileSync(installed).equals(fs.readFileSync(path.join(sourceDir, rel))))
130
+ return false;
131
+ }
132
+ return true;
133
+ }
95
134
  function copySkillExtras(sourceDir, targetDir) {
96
135
  for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
97
136
  if (entry.name === "SKILL.md")
@@ -111,27 +150,38 @@ export function seedAppSkill(root, appName, appRelPath) {
111
150
  }
112
151
  function planSync(root) {
113
152
  const items = [];
114
- for (const skill of listBaseSkills()) {
115
- const target = path.join(root, ".claude", "skills", `mesh-${skill.name}`, "SKILL.md");
116
- const desired = renderManagedSkill(skill.source);
117
- const label = `.claude/skills/mesh-${skill.name}/SKILL.md (base skill)`;
153
+ const baseSkills = listBaseSkills();
154
+ for (const skill of baseSkills) {
155
+ const targetDir = path.join(root, ".claude", "skills", skill.name);
156
+ const target = path.join(targetDir, "SKILL.md");
157
+ const sourceDir = path.dirname(skill.source);
158
+ const desired = renderManagedSkill(skill.source, skill.name);
159
+ const label = `.claude/skills/${skill.name}/SKILL.md (base skill)`;
118
160
  if (!fs.existsSync(target)) {
119
161
  items.push({
120
162
  label,
121
163
  state: "write",
122
164
  apply: () => {
123
- fs.mkdirSync(path.dirname(target), { recursive: true });
165
+ fs.mkdirSync(targetDir, { recursive: true });
124
166
  fs.writeFileSync(target, desired);
167
+ copySkillExtras(sourceDir, targetDir);
125
168
  },
126
169
  });
127
170
  }
128
171
  else {
129
172
  const current = fs.readFileSync(target, "utf-8");
130
- if (current === desired) {
173
+ if (current === desired && extrasInSync(sourceDir, targetDir)) {
131
174
  items.push({ label, state: "ok" });
132
175
  }
133
176
  else if (current.includes(MANAGED_MARKER)) {
134
- items.push({ label, state: "write", apply: () => fs.writeFileSync(target, desired) });
177
+ items.push({
178
+ label,
179
+ state: "write",
180
+ apply: () => {
181
+ fs.writeFileSync(target, desired);
182
+ copySkillExtras(sourceDir, targetDir);
183
+ },
184
+ });
135
185
  }
136
186
  else {
137
187
  items.push({ label: `${label} — exists without the managed marker, leaving as-is`, state: "skip" });
@@ -142,7 +192,7 @@ function planSync(root) {
142
192
  for (const skill of packageSkills) {
143
193
  const targetDir = path.join(root, ".claude", "skills", skill.name);
144
194
  const target = path.join(targetDir, "SKILL.md");
145
- const desired = renderManagedSkill(skill.source);
195
+ const desired = renderManagedSkill(skill.source, skill.name);
146
196
  const label = `.claude/skills/${skill.name}/SKILL.md (platform skill)`;
147
197
  if (!fs.existsSync(target)) {
148
198
  items.push({
@@ -178,6 +228,25 @@ function planSync(root) {
178
228
  if (packageSkills.length === 0 && meshTechScopeDirs(root).length === 0) {
179
229
  logInfo("No @mesh-tech packages installed yet — run `pnpm install`, then `mesh skills sync` again for the platform skills.");
180
230
  }
231
+ for (const skill of [...baseSkills, ...packageSkills]) {
232
+ if (skill.legacyName === skill.name)
233
+ continue;
234
+ const legacyDir = path.join(root, ".claude", "skills", skill.legacyName);
235
+ const legacyFile = path.join(legacyDir, "SKILL.md");
236
+ if (!fs.existsSync(legacyFile))
237
+ continue;
238
+ const label = `.claude/skills/${skill.legacyName}/ (renamed to ${skill.name})`;
239
+ if (fs.readFileSync(legacyFile, "utf-8").includes(MANAGED_MARKER)) {
240
+ items.push({
241
+ label: `${label} — removing the old copy`,
242
+ state: "write",
243
+ apply: () => fs.rmSync(legacyDir, { recursive: true, force: true }),
244
+ });
245
+ }
246
+ else {
247
+ items.push({ label: `${label} — exists without the managed marker, leaving as-is`, state: "skip" });
248
+ }
249
+ }
181
250
  const hookSource = cliAsset("assets", "intent", "intent-claude-gate.mjs");
182
251
  const hookTarget = path.join(root, HOOK_RELATIVE);
183
252
  const hookDesired = fs.readFileSync(hookSource, "utf-8");
@@ -0,0 +1,65 @@
1
+ export declare const INFRA_PACKAGE = "@mesh-tech/infra-components";
2
+ export declare const HUB_PACKAGE = "@mesh-tech/hub";
3
+ export declare const CLI_PACKAGE = "@mesh-tech/mesh-cli";
4
+ export interface StatusTarget {
5
+ stack: string;
6
+ kind: "layer" | "app";
7
+ tenant: string;
8
+ env: string;
9
+ coreTenant: string;
10
+ coreEnv: string;
11
+ platformDir?: string;
12
+ coreDir?: string;
13
+ }
14
+ export declare function splitLayerStack(stack: string, tenant: string | null): {
15
+ tenant: string;
16
+ env: string;
17
+ };
18
+ export declare function targetForStack(projectDir: string, stack: string): StatusTarget | null;
19
+ export declare function findStatusTargets(cwd: string, stack?: string): StatusTarget[];
20
+ export declare function pinnedVersion(dir: string | undefined, pkg: string): string | null;
21
+ export declare function compareVersions(a: string, b: string): number;
22
+ export type LayerState = "current" | "behind" | "not-deployed" | "unknown";
23
+ export interface LayerRow {
24
+ layer: "core" | "platform" | "hub";
25
+ registration: string;
26
+ pinned: string | null;
27
+ deployed: string | null;
28
+ latest: string | null;
29
+ state: LayerState;
30
+ undeployedPin: boolean;
31
+ note?: string;
32
+ }
33
+ export declare function layerState(deployed: string | null, latest: string | null): LayerState;
34
+ export interface StatusReport {
35
+ target: StatusTarget;
36
+ rows: LayerRow[];
37
+ }
38
+ export interface CliStatus {
39
+ running: string;
40
+ latest: string | null;
41
+ behind: boolean;
42
+ }
43
+ export interface Registration {
44
+ infraVersion?: string;
45
+ hubVersion?: string;
46
+ }
47
+ export declare function parseRegistration(raw: string): Registration;
48
+ export declare function buildRows(target: StatusTarget, deployed: {
49
+ core: Registration | Error | null;
50
+ platform: Registration | Error | null;
51
+ }, latest: {
52
+ infra: string | null;
53
+ hub: string | null;
54
+ }): LayerRow[];
55
+ export declare function renderStatus(reports: StatusReport[], cli: CliStatus): string;
56
+ export declare function upgradeSteps(target: StatusTarget, rows: LayerRow[]): string[];
57
+ export declare function latestPublished(pkg: string, cwd: string): string | null;
58
+ export interface StatusOptions {
59
+ stack?: string;
60
+ profile?: string;
61
+ region?: string;
62
+ json?: boolean;
63
+ check?: boolean;
64
+ }
65
+ export declare function runStackStatus(cwd: string, opts: StatusOptions): Promise<boolean>;
@@ -0,0 +1,285 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execFileSync } from "child_process";
4
+ import { logInfo, logWarn } from "../utils/log.js";
5
+ import { findAppRoot, findStackConfigs, readStackConfig } from "../utils/pulumi.js";
6
+ import { derivePlatformContext, readStackPlatform, resolveAwsCredentials } from "../utils/aws-auth.js";
7
+ import { resolveCliRuntime } from "../utils/build-info.js";
8
+ export const INFRA_PACKAGE = "@mesh-tech/infra-components";
9
+ export const HUB_PACKAGE = "@mesh-tech/hub";
10
+ export const CLI_PACKAGE = "@mesh-tech/mesh-cli";
11
+ export function splitLayerStack(stack, tenant) {
12
+ if (tenant && stack.startsWith(`${tenant}-`))
13
+ return { tenant, env: stack.slice(tenant.length + 1) };
14
+ const cut = stack.indexOf("-");
15
+ if (cut <= 0)
16
+ return { tenant: tenant ?? stack, env: stack };
17
+ return { tenant: tenant ?? stack.slice(0, cut), env: stack.slice(cut + 1) };
18
+ }
19
+ export function targetForStack(projectDir, stack) {
20
+ const platform = readStackPlatform(projectDir, stack);
21
+ if (platform) {
22
+ const [tenant, env] = platform.split(".");
23
+ return { stack, kind: "app", tenant, env, coreTenant: tenant, coreEnv: env };
24
+ }
25
+ const declared = readStackConfig(projectDir, stack, "mesh:tenant");
26
+ const layerDir = ["core", "platform"].includes(path.basename(projectDir));
27
+ if (!declared && !layerDir)
28
+ return null;
29
+ const { tenant, env } = splitLayerStack(stack, declared);
30
+ const isCore = path.basename(projectDir) === "core";
31
+ return {
32
+ stack,
33
+ kind: "layer",
34
+ tenant,
35
+ env,
36
+ coreTenant: readStackConfig(projectDir, stack, "mesh:coreTenant") ?? tenant,
37
+ coreEnv: readStackConfig(projectDir, stack, "mesh:coreEnv") ?? env,
38
+ ...(isCore ? { coreDir: projectDir } : { platformDir: projectDir }),
39
+ };
40
+ }
41
+ export function findStatusTargets(cwd, stack) {
42
+ const project = findAppRoot(cwd);
43
+ let projectDir = project;
44
+ let repoRoot = null;
45
+ if (!projectDir) {
46
+ const platformDir = path.join(cwd, "platform");
47
+ if (fs.existsSync(path.join(platformDir, "Pulumi.yaml"))) {
48
+ projectDir = platformDir;
49
+ repoRoot = cwd;
50
+ }
51
+ }
52
+ else if (["core", "platform"].includes(path.basename(projectDir))) {
53
+ repoRoot = path.dirname(projectDir);
54
+ }
55
+ if (!projectDir)
56
+ return [];
57
+ const stacks = stack ? [stack] : findStackConfigs(projectDir);
58
+ const targets = [];
59
+ for (const name of stacks) {
60
+ const target = targetForStack(projectDir, name);
61
+ if (!target)
62
+ continue;
63
+ if (repoRoot && target.kind === "layer") {
64
+ const core = path.join(repoRoot, "core");
65
+ const platform = path.join(repoRoot, "platform");
66
+ if (fs.existsSync(path.join(core, "package.json")))
67
+ target.coreDir = core;
68
+ if (fs.existsSync(path.join(platform, "package.json")))
69
+ target.platformDir = platform;
70
+ }
71
+ targets.push(target);
72
+ }
73
+ return targets;
74
+ }
75
+ export function pinnedVersion(dir, pkg) {
76
+ if (!dir)
77
+ return null;
78
+ try {
79
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
80
+ const spec = manifest.dependencies?.[pkg] ?? manifest.devDependencies?.[pkg];
81
+ return typeof spec === "string" ? spec.replace(/^[\^~=v]+/, "") : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ export function compareVersions(a, b) {
88
+ const parse = (v) => v.split(/[.+-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
89
+ const [x, y] = [parse(a), parse(b)];
90
+ for (let i = 0; i < 3; i++) {
91
+ const diff = (x[i] ?? 0) - (y[i] ?? 0);
92
+ if (diff)
93
+ return diff;
94
+ }
95
+ return 0;
96
+ }
97
+ export function layerState(deployed, latest) {
98
+ if (!deployed)
99
+ return "not-deployed";
100
+ if (!latest)
101
+ return "unknown";
102
+ return compareVersions(deployed, latest) < 0 ? "behind" : "current";
103
+ }
104
+ export function parseRegistration(raw) {
105
+ try {
106
+ const data = JSON.parse(raw);
107
+ const out = {};
108
+ if (typeof data.build?.infraVersion === "string")
109
+ out.infraVersion = data.build.infraVersion;
110
+ if (typeof data.hub?.hubVersion === "string")
111
+ out.hubVersion = data.hub.hubVersion;
112
+ return out;
113
+ }
114
+ catch {
115
+ return {};
116
+ }
117
+ }
118
+ export function buildRows(target, deployed, latest) {
119
+ const corePath = `/mesh-platform/${target.coreTenant}/${target.coreEnv}/core`;
120
+ const platformPath = `/mesh-platform/${target.tenant}/${target.env}/platform`;
121
+ const row = (layer, registration, source, field, pinned, latestVersion) => {
122
+ const value = source && !(source instanceof Error) ? (source[field] ?? null) : null;
123
+ const predatesExport = !!source && !(source instanceof Error) && value === null;
124
+ const note = source instanceof Error
125
+ ? source.message
126
+ : source === null
127
+ ? "no registration — never deployed, or not readable with these credentials"
128
+ : predatesExport
129
+ ? "registration predates version export — last deployed with an infra-components older than any current release"
130
+ : undefined;
131
+ return {
132
+ layer,
133
+ registration,
134
+ pinned,
135
+ deployed: value,
136
+ latest: latestVersion,
137
+ state: source instanceof Error ? "unknown" : predatesExport ? "behind" : layerState(value, latestVersion),
138
+ undeployedPin: !!pinned && !!value && pinned !== value,
139
+ ...(note ? { note } : {}),
140
+ };
141
+ };
142
+ return [
143
+ row("core", corePath, deployed.core, "infraVersion", pinnedVersion(target.coreDir, INFRA_PACKAGE), latest.infra),
144
+ row("platform", platformPath, deployed.platform, "infraVersion", pinnedVersion(target.platformDir, INFRA_PACKAGE), latest.infra),
145
+ row("hub", platformPath, deployed.platform, "hubVersion", pinnedVersion(target.platformDir, HUB_PACKAGE), latest.hub),
146
+ ];
147
+ }
148
+ export function renderStatus(reports, cli) {
149
+ const lines = [];
150
+ const show = (v) => v ?? "—";
151
+ for (const { target, rows } of reports) {
152
+ const what = target.kind === "app"
153
+ ? `App stack ${target.stack} → platform ${target.tenant}/${target.env}`
154
+ : `Platform ${target.tenant}/${target.env} (stack ${target.stack}) on core ${target.coreTenant}/${target.coreEnv}`;
155
+ lines.push(what);
156
+ lines.push(` ${"layer".padEnd(9)} ${"pinned".padEnd(10)} ${"deployed".padEnd(10)} ${"latest".padEnd(10)} state`);
157
+ for (const r of rows) {
158
+ const flag = r.undeployedPin ? " (pin not deployed)" : "";
159
+ lines.push(` ${r.layer.padEnd(9)} ${show(r.pinned).padEnd(10)} ${show(r.deployed).padEnd(10)} ${show(r.latest).padEnd(10)} ${r.state}${flag}`);
160
+ if (r.note)
161
+ lines.push(` ${r.registration}: ${r.note}`);
162
+ }
163
+ if (rows.some((r) => r.state === "behind" || r.undeployedPin)) {
164
+ lines.push(...upgradeSteps(target, rows));
165
+ }
166
+ lines.push("");
167
+ }
168
+ lines.push(`CLI ${CLI_PACKAGE} ${cli.running}` +
169
+ (cli.latest ? ` (latest ${cli.latest})${cli.behind ? " — behind: pnpm add -D @mesh-tech/mesh-cli@latest" : ""}` : " (latest unknown)"));
170
+ return lines.join("\n");
171
+ }
172
+ export function upgradeSteps(target, rows) {
173
+ const latest = rows.find((r) => r.latest)?.latest;
174
+ if (target.kind === "app") {
175
+ return [
176
+ " This platform is behind. Its operator upgrades it from the org's platform repo:",
177
+ " run `mesh stack status` there, then upgrade core and platform together.",
178
+ ];
179
+ }
180
+ const to = latest ? `@${latest}` : "@latest";
181
+ return [
182
+ " To upgrade (both layers — deploying platform/ alone leaves core on the old version):",
183
+ ` 1. core/ and platform/: set ${INFRA_PACKAGE}${to}` +
184
+ ` (and ${HUB_PACKAGE}${to} in platform/), then pnpm install`,
185
+ ` 2. cd core && mesh deploy preview --stack ${target.coreTenant}-${target.coreEnv}` +
186
+ " # review: this layer is the VPC/EKS/RDS",
187
+ ` 3. mesh deploy up --stack ${target.coreTenant}-${target.coreEnv}` +
188
+ ` && cd ../platform && mesh deploy up --stack ${target.stack}`,
189
+ " 4. mesh stack status # both layers now report the new version",
190
+ ];
191
+ }
192
+ export function latestPublished(pkg, cwd) {
193
+ try {
194
+ const out = execFileSync("npm", ["view", pkg, "version"], {
195
+ cwd,
196
+ encoding: "utf-8",
197
+ timeout: 20_000,
198
+ stdio: ["ignore", "pipe", "ignore"],
199
+ }).trim();
200
+ return /^\d+\.\d+\.\d+$/.test(out) ? out : null;
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ async function readRegistration(name, region, auth) {
207
+ const { credentials, profile, signInHint } = auth;
208
+ try {
209
+ const { SSMClient, GetParameterCommand } = await import("@aws-sdk/client-ssm");
210
+ const client = new SSMClient({
211
+ region,
212
+ ...(profile && !credentials?.AWS_ACCESS_KEY_ID ? { profile } : {}),
213
+ ...(credentials?.AWS_ACCESS_KEY_ID
214
+ ? {
215
+ credentials: {
216
+ accessKeyId: credentials.AWS_ACCESS_KEY_ID,
217
+ secretAccessKey: credentials.AWS_SECRET_ACCESS_KEY ?? "",
218
+ sessionToken: credentials.AWS_SESSION_TOKEN,
219
+ },
220
+ }
221
+ : {}),
222
+ });
223
+ const resp = await client.send(new GetParameterCommand({ Name: name }));
224
+ return resp.Parameter?.Value ? parseRegistration(resp.Parameter.Value) : null;
225
+ }
226
+ catch (err) {
227
+ const e = err;
228
+ const kind = e.name || e.__type || e.message || String(err);
229
+ if (/ParameterNotFound/.test(kind))
230
+ return null;
231
+ if (/credential|ExpiredToken|AccessDenied|UnrecognizedClient|security token/i.test(`${kind} ${e.message ?? ""}`)) {
232
+ return new Error(`could not read — no AWS credentials for this account (${signInHint})`);
233
+ }
234
+ return new Error(`could not read — ${e.message ?? kind}`);
235
+ }
236
+ }
237
+ export async function runStackStatus(cwd, opts) {
238
+ const targets = findStatusTargets(cwd, opts.stack);
239
+ if (targets.length === 0) {
240
+ logWarn("No Mesh stack found here. Run from an app directory (a Pulumi.<stack>.yaml naming mesh:platform),\n" +
241
+ " or from your org's Mesh platform repo (its root, core/ or platform/).");
242
+ return true;
243
+ }
244
+ const infraLatest = latestPublished(INFRA_PACKAGE, cwd);
245
+ const hubLatest = latestPublished(HUB_PACKAGE, cwd);
246
+ const running = resolveCliRuntime({ vcs: false }).version;
247
+ const cliLatest = latestPublished(CLI_PACKAGE, cwd);
248
+ const cli = { running, latest: cliLatest, behind: !!cliLatest && compareVersions(running, cliLatest) < 0 };
249
+ if (!infraLatest)
250
+ logInfo("Registry lookup failed — `latest` is unknown. Run `mesh registry login` and retry.");
251
+ const reports = [];
252
+ for (const target of targets) {
253
+ const projectDir = findAppRoot(cwd) ?? target.platformDir ?? cwd;
254
+ const region = opts.region ?? readStackConfig(projectDir, target.stack, "aws:region") ?? "us-east-2";
255
+ const ambient = !!process.env.AWS_ACCESS_KEY_ID || !!process.env.AWS_PROFILE;
256
+ let profile = opts.profile;
257
+ let credentials;
258
+ if (!profile && !ambient) {
259
+ profile = readStackConfig(projectDir, target.stack, "aws:profile") ?? undefined;
260
+ if (!profile && target.kind === "app") {
261
+ const role = readStackConfig(projectDir, target.stack, "mesh:deployerRole");
262
+ if (role)
263
+ credentials = (await resolveAwsCredentials(role, projectDir, target.stack))?.env;
264
+ }
265
+ }
266
+ const loginContext = target.kind === "app" ? derivePlatformContext(projectDir, target.stack) : null;
267
+ const signInHint = loginContext
268
+ ? `sign in with \`mesh login ${loginContext} --device\`, or pass --profile`
269
+ : "set AWS_PROFILE or pass --profile with access to the core's account";
270
+ const auth = { credentials, profile, signInHint };
271
+ const [core, platform] = await Promise.all([
272
+ readRegistration(`/mesh-platform/${target.coreTenant}/${target.coreEnv}/core`, region, auth),
273
+ readRegistration(`/mesh-platform/${target.tenant}/${target.env}/platform`, region, auth),
274
+ ]);
275
+ reports.push({ target, rows: buildRows(target, { core, platform }, { infra: infraLatest, hub: hubLatest }) });
276
+ }
277
+ if (opts.json) {
278
+ process.stdout.write(JSON.stringify({ platforms: reports, cli }, null, 2) + "\n");
279
+ }
280
+ else {
281
+ process.stdout.write(renderStatus(reports, cli) + "\n");
282
+ }
283
+ const behind = cli.behind || reports.some((r) => r.rows.some((row) => row.state === "behind" || row.undeployedPin));
284
+ return !behind;
285
+ }