@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.
- package/assets/app-skill/SKILL.md +1 -1
- package/dist/bin/mesh.js +549 -180
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.js +2 -2
- package/dist/src/commands/cluster.js +2 -2
- package/dist/src/commands/dev.js +4 -2
- package/dist/src/commands/login.js +2 -2
- package/dist/src/commands/skills.d.ts +7 -2
- package/dist/src/commands/skills.js +84 -15
- package/dist/src/commands/stack-status.d.ts +65 -0
- package/dist/src/commands/stack-status.js +285 -0
- package/dist/src/commands/stack.js +15 -1
- package/dist/src/commands/temporal.js +28 -4
- package/dist/src/commands/temporal.js.map +1 -1
- package/dist/src/program.d.ts +1 -1
- package/dist/src/program.js +1 -1
- package/dist/src/utils/aws-auth.d.ts +1 -0
- package/dist/src/utils/aws-auth.js +10 -1
- package/dist/src/utils/first-party-contexts.d.ts +3 -0
- package/dist/src/utils/first-party-contexts.js +12 -0
- package/dist/src/utils/pulumi-run.d.ts +1 -1
- package/dist/src/utils/pulumi-run.js +16 -6
- package/fragments/base/index.ts.hbs +4 -3
- package/package.json +2 -2
- package/skills/{core → mesh}/SKILL.md +106 -100
- package/skills/mesh/references/programmatic-api.md +35 -0
- package/skills/mesh/references/worktrees.md +53 -0
- package/skills/pull-remote-agent-context/SKILL.md +1 -1
- package/templates/api-auth/api/src/schema.ts.hbs +3 -0
package/dist/build-info.json
CHANGED
|
@@ -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 →
|
|
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 →
|
|
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`);
|
package/dist/src/commands/dev.js
CHANGED
|
@@ -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
|
|
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
|
|
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) => ({
|
|
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
|
|
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
|
|
40
|
-
|
|
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 =
|
|
88
|
-
if (!byName.has(name))
|
|
89
|
-
byName.set(name, {
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
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(
|
|
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({
|
|
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
|
}
|