@mesh-tech/mesh-cli 0.34.4 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "bfebd7328",
3
- "builtAt": "2026-09-25T04:08:06.365Z"
2
+ "commit": "7e553d060",
3
+ "builtAt": "2026-09-29T02:18:34.716Z"
4
4
  }
@@ -75,7 +75,7 @@ export const APP_CHECK_RULES = {
75
75
  gate: "H",
76
76
  title: "No role grants in IaC",
77
77
  severity: "block",
78
- remediation: "remove the per-user grant map; grant roles from the Hub (App → Access → People)",
78
+ remediation: "remove the per-user grant map; grant roles from the Hub (App → Access → Users)",
79
79
  scope: "app",
80
80
  },
81
81
  POINTER_PROJECT_OVERRIDE: {
@@ -762,7 +762,7 @@ export function scanApp(root, appRel, resolution = probeResolution(root, appRel)
762
762
  for (const f of files)
763
763
  hit(code, rel(root, f), why);
764
764
  };
765
- anti("IAC_GRANTS", RE_IAC_GRANT, "per-user role grant baked into IaC — grants belong in the Hub (Access → People), not in a config redeploy");
765
+ anti("IAC_GRANTS", RE_IAC_GRANT, "per-user role grant baked into IaC — grants belong in the Hub (Access → Users), not in a config redeploy");
766
766
  anti("POINTER_PROJECT_OVERRIDE", RE_POINTER_OVERRIDE, "AppAuthzPointer is given a `zitadel:` override — an app binds its project with AppEnvironment's zitadelAppProjectId so every consumer of env.zitadel agrees; the override is reserved for the Hub");
767
767
  anti("PASSWORD_STORE", RE_PASSWORD, "password storage or hashing — Zitadel owns credentials; the Hub creates sign-in users with a one-time temporary password");
768
768
  anti("EMAIL_ALLOWLIST", RE_ALLOWLIST, "email allowlist standing in for roles — declare a role and let the Hub grant it");
@@ -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);
@@ -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");
@@ -941,7 +941,7 @@ export async function ensureLogin(context, opts = {}) {
941
941
  }
942
942
  async function discoverConfigGuarded(context, opts = {}) {
943
943
  let config = null;
944
- if (parseTenantEnv(context)) {
944
+ if (parseTenantEnv(context) && !isPlatformLoginAlias(context)) {
945
945
  config = await discoverConfigFromSsm(context, opts);
946
946
  }
947
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
+ }
@@ -6,6 +6,7 @@ import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js";
6
6
  import { findAppRoot, findStackConfigs, getCurrentStack, readStackConfig } from "../utils/pulumi.js";
7
7
  import { resolveWorktreeIdentity, worktreeStackName } from "../utils/worktree-identity.js";
8
8
  import { resolvePulumiEnv } from "../utils/pulumi-run.js";
9
+ import { runStackStatus } from "./stack-status.js";
9
10
  function readTopLevelYamlKey(appRoot, stack, key) {
10
11
  const configFile = path.join(appRoot, `Pulumi.${stack}.yaml`);
11
12
  if (!fs.existsSync(configFile))
@@ -130,7 +131,7 @@ function getGitHubUsername() {
130
131
  export function registerStackCommand(program) {
131
132
  const stack = program
132
133
  .command("stack")
133
- .description("Manage personal Pulumi stacks");
134
+ .description("Manage personal Pulumi stacks and check a Mesh instance against the latest release");
134
135
  stack
135
136
  .command("init")
136
137
  .description("Create a personal dev stack (dev-{github-username})")
@@ -383,4 +384,17 @@ export function registerStackCommand(program) {
383
384
  process.exit(err.status ?? 1);
384
385
  }
385
386
  });
387
+ stack
388
+ .command("status")
389
+ .description("Read-only drift check: for each platform this directory deploys (a platform repo's core/platform stacks) or deploys apps to (an app's mesh:platform), compare the pinned, deployed (the core and platform SSM registrations written by the last successful deploy) and latest published @mesh-tech/infra-components and @mesh-tech/hub, plus this CLI against the latest @mesh-tech/mesh-cli, and print the upgrade steps when anything is behind. Changes nothing.")
390
+ .option("--stack <name>", "check one stack (default: every Pulumi.<stack>.yaml here)")
391
+ .option("--profile <profile>", "AWS profile to read the SSM registrations with (default: AWS_PROFILE, else the stack's aws:profile, else an app stack's deployer role)")
392
+ .option("--region <region>", "AWS region of the registrations (default: the stack's aws:region, else us-east-2)")
393
+ .option("--json", "print the report as JSON", false)
394
+ .option("--check", "exit 1 when any layer or the CLI is behind, or a pin is not deployed", false)
395
+ .action(async (opts) => {
396
+ const ok = await runStackStatus(process.cwd(), opts);
397
+ if (opts.check && !ok)
398
+ process.exit(1);
399
+ });
386
400
  }