@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.
- package/assets/app-skill/SKILL.md +1 -1
- package/dist/bin/mesh.js +567 -195
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/cluster.js +2 -2
- package/dist/src/commands/dev.js +4 -2
- package/dist/src/commands/login.d.ts +1 -0
- package/dist/src/commands/login.js +23 -20
- 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 +45 -17
- 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 +3 -2
- 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/README.md.hbs +3 -2
package/dist/build-info.json
CHANGED
|
@@ -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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
+
const tenants = deployableTenants(creds.idToken, creds.accessToken);
|
|
624
612
|
let audience = [];
|
|
625
613
|
try {
|
|
626
|
-
|
|
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
|
|
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
|
+
}
|