@mesh-tech/mesh-cli 0.25.0 → 0.26.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/README.md +74 -246
- package/dist/bin/mesh.js +1415 -842
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/db/axes.d.ts +19 -0
- package/dist/src/commands/db/axes.js +35 -0
- package/dist/src/commands/db/connect.d.ts +2 -4
- package/dist/src/commands/db/connect.js +4 -19
- package/dist/src/commands/db/credentials.d.ts +2 -5
- package/dist/src/commands/db/credentials.js +7 -23
- package/dist/src/commands/db/env.d.ts +2 -5
- package/dist/src/commands/db/env.js +7 -23
- package/dist/src/commands/db/exec.js +7 -24
- package/dist/src/commands/db/psql.d.ts +2 -5
- package/dist/src/commands/db/psql.js +8 -23
- package/dist/src/commands/init/platform-setup.d.ts +68 -0
- package/dist/src/commands/init/platform-setup.js +203 -0
- package/dist/src/commands/init/wizard.d.ts +20 -0
- package/dist/src/commands/init/wizard.js +89 -7
- package/dist/src/commands/init.js +42 -36
- package/dist/src/commands/login.d.ts +4 -0
- package/dist/src/commands/login.js +16 -2
- package/dist/src/commands/registry.d.ts +2 -2
- package/dist/src/commands/registry.js +11 -6
- package/dist/src/commands/secrets/context.d.ts +5 -0
- package/dist/src/commands/secrets/context.js +5 -0
- package/dist/src/commands/secrets/index.js +3 -0
- package/dist/src/commands/secrets/migrate-config.d.ts +1 -0
- package/dist/src/commands/secrets/migrate-config.js +2 -3
- package/dist/src/commands/secrets/reindex.d.ts +1 -0
- package/dist/src/commands/secrets/reindex.js +2 -2
- package/dist/src/commands/secrets/set.d.ts +1 -0
- package/dist/src/commands/secrets/set.js +2 -2
- package/dist/src/commands/temporal.js +25 -8
- package/dist/src/commands/temporal.js.map +2 -2
- package/dist/src/commands/tunnel/index.d.ts +8 -0
- package/dist/src/commands/tunnel/index.js +3 -3
- package/dist/src/index.js +324 -62
- package/dist/src/index.js.map +3 -3
- package/dist/src/utils/context.d.ts +6 -1
- package/dist/src/utils/context.js +302 -39
- package/dist/src/utils/registry-identity.d.ts +5 -0
- package/dist/src/utils/registry-identity.js +16 -0
- package/fragments/agents/AGENTS.md.hbs +10 -2
- package/fragments/base/index.ts.hbs +15 -5
- package/package.json +2 -2
- package/skills/core/SKILL.md +73 -6
- package/templates/api-auth/index.ts.hbs +5 -4
- package/templates/api-role-gating/index.ts.hbs +5 -4
- package/templates/external-service/index.ts.hbs +5 -4
- package/templates/platform-repo/README.md.hbs +17 -3
- package/templates/platform-repo/core/Pulumi.{{tenant}}-{{env}}.yaml.hbs +7 -0
- package/templates/workflow/index.ts.hbs +5 -4
|
@@ -1,89 +1,352 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { MeshCliError } from "./errors.js";
|
|
4
|
+
import { logInfo, logWarn } from "./log.js";
|
|
5
|
+
import { findMeshJson } from "./mesh-json.js";
|
|
6
|
+
function findFileUpwardInRepo(filename, startDir = process.cwd()) {
|
|
5
7
|
let currentDir = startDir;
|
|
6
8
|
const root = path.parse(currentDir).root;
|
|
7
|
-
|
|
9
|
+
for (;;) {
|
|
8
10
|
const filePath = path.join(currentDir, filename);
|
|
9
11
|
if (fs.existsSync(filePath)) {
|
|
10
12
|
return filePath;
|
|
11
13
|
}
|
|
14
|
+
if (fs.existsSync(path.join(currentDir, ".git")) ||
|
|
15
|
+
fs.existsSync(path.join(currentDir, "pnpm-workspace.yaml")) ||
|
|
16
|
+
currentDir === root) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
currentDir = path.dirname(currentDir);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function findRepoRoot(startDir) {
|
|
23
|
+
let currentDir = startDir;
|
|
24
|
+
const root = path.parse(currentDir).root;
|
|
25
|
+
for (;;) {
|
|
26
|
+
if (fs.existsSync(path.join(currentDir, ".git")) ||
|
|
27
|
+
fs.existsSync(path.join(currentDir, "pnpm-workspace.yaml"))) {
|
|
28
|
+
return currentDir;
|
|
29
|
+
}
|
|
30
|
+
if (currentDir === root) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
12
33
|
currentDir = path.dirname(currentDir);
|
|
13
34
|
}
|
|
35
|
+
}
|
|
36
|
+
function stripComments(source) {
|
|
37
|
+
let out = "";
|
|
38
|
+
for (let i = 0; i < source.length; i++) {
|
|
39
|
+
const ch = source[i];
|
|
40
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
41
|
+
const start = i;
|
|
42
|
+
i++;
|
|
43
|
+
while (i < source.length && source[i] !== ch) {
|
|
44
|
+
if (source[i] === "\\")
|
|
45
|
+
i++;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
out += source.slice(start, i + 1);
|
|
49
|
+
}
|
|
50
|
+
else if (ch === "/" && source[i + 1] === "/") {
|
|
51
|
+
while (i < source.length && source[i] !== "\n")
|
|
52
|
+
i++;
|
|
53
|
+
i--;
|
|
54
|
+
}
|
|
55
|
+
else if (ch === "/" && source[i + 1] === "*") {
|
|
56
|
+
i += 2;
|
|
57
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/"))
|
|
58
|
+
i++;
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
out += ch;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function findExportedConfigStart(source) {
|
|
68
|
+
const declMatch = /export\s+(const\s+config\b|default\b)/.exec(source);
|
|
69
|
+
if (!declMatch)
|
|
70
|
+
return undefined;
|
|
71
|
+
const start = source.indexOf("{", declMatch.index);
|
|
72
|
+
if (start === -1)
|
|
73
|
+
return undefined;
|
|
74
|
+
const gap = source.slice(declMatch.index + declMatch[0].length, start);
|
|
75
|
+
if (!/^[\s=]*(:[^;{]*)?$/.test(gap))
|
|
76
|
+
return undefined;
|
|
77
|
+
return start;
|
|
78
|
+
}
|
|
79
|
+
function scanDepth1(source, start, visit) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
for (let i = start; i < source.length; i++) {
|
|
82
|
+
const ch = source[i];
|
|
83
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
84
|
+
i++;
|
|
85
|
+
while (i < source.length && source[i] !== ch) {
|
|
86
|
+
if (source[i] === "\\")
|
|
87
|
+
i++;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (ch === "{") {
|
|
92
|
+
depth++;
|
|
93
|
+
}
|
|
94
|
+
else if (ch === "}") {
|
|
95
|
+
depth--;
|
|
96
|
+
if (depth === 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
else if (depth === 1 && !/[\w$.]/.test(source[i - 1] ?? "")) {
|
|
100
|
+
const found = visit(i);
|
|
101
|
+
if (found !== undefined)
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function extractConfigTenant(rawSource) {
|
|
108
|
+
const source = stripComments(rawSource);
|
|
109
|
+
const start = findExportedConfigStart(source);
|
|
110
|
+
if (start === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
const tenantRe = /tenant\s*:\s*(["'])([^"'\n]+)\1/y;
|
|
113
|
+
return scanDepth1(source, start, (i) => {
|
|
114
|
+
tenantRe.lastIndex = i;
|
|
115
|
+
return tenantRe.exec(source)?.[2];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function matchBlock(source, open) {
|
|
119
|
+
let depth = 0;
|
|
120
|
+
for (let i = open; i < source.length; i++) {
|
|
121
|
+
const ch = source[i];
|
|
122
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
123
|
+
i++;
|
|
124
|
+
while (i < source.length && source[i] !== ch) {
|
|
125
|
+
if (source[i] === "\\")
|
|
126
|
+
i++;
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (ch === "{") {
|
|
131
|
+
depth++;
|
|
132
|
+
}
|
|
133
|
+
else if (ch === "}") {
|
|
134
|
+
depth--;
|
|
135
|
+
if (depth === 0)
|
|
136
|
+
return source.slice(open, i + 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
14
139
|
return undefined;
|
|
15
140
|
}
|
|
16
|
-
|
|
141
|
+
function extractExportedConfigSource(cleanedSource) {
|
|
142
|
+
const start = findExportedConfigStart(cleanedSource);
|
|
143
|
+
return start === undefined ? undefined : matchBlock(cleanedSource, start);
|
|
144
|
+
}
|
|
145
|
+
function extractPlatformBlock(cleanedSource) {
|
|
146
|
+
const start = findExportedConfigStart(cleanedSource);
|
|
147
|
+
if (start === undefined)
|
|
148
|
+
return undefined;
|
|
149
|
+
const platformRe = /platform\s*:\s*\{/y;
|
|
150
|
+
let open;
|
|
151
|
+
scanDepth1(cleanedSource, start, (i) => {
|
|
152
|
+
platformRe.lastIndex = i;
|
|
153
|
+
if (platformRe.exec(cleanedSource)) {
|
|
154
|
+
open = platformRe.lastIndex - 1;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
});
|
|
159
|
+
if (open === undefined)
|
|
160
|
+
return undefined;
|
|
161
|
+
return matchBlock(cleanedSource, open);
|
|
162
|
+
}
|
|
163
|
+
function pickStackFile(files, hint, explicitStack) {
|
|
164
|
+
if (explicitStack !== undefined) {
|
|
165
|
+
const wanted = `Pulumi.${explicitStack}.yaml`;
|
|
166
|
+
return { file: files.includes(wanted) ? wanted : undefined, qualified: true };
|
|
167
|
+
}
|
|
168
|
+
if (hint) {
|
|
169
|
+
const wanted = `Pulumi.${hint}.yaml`;
|
|
170
|
+
if (files.includes(wanted))
|
|
171
|
+
return { file: wanted, qualified: true };
|
|
172
|
+
}
|
|
173
|
+
const stageOf = (f) => f.slice("Pulumi.".length, -".yaml".length);
|
|
174
|
+
const byBaseFirst = [...files].sort((a, b) => stageOf(a).length - stageOf(b).length || a.localeCompare(b));
|
|
175
|
+
return { file: byBaseFirst[0], qualified: false };
|
|
176
|
+
}
|
|
177
|
+
function tenantNotDeclared(cwd, found) {
|
|
178
|
+
const looked = found.stackFile
|
|
179
|
+
? `${found.stackFile} in ${cwd} does not declare mesh:tenant`
|
|
180
|
+
: found.hasProjectFile
|
|
181
|
+
? `${cwd} has a Pulumi.yaml but no Pulumi.<stack>.yaml to read mesh:tenant from`
|
|
182
|
+
: `${cwd} is not a Pulumi project directory (no Pulumi.yaml)`;
|
|
183
|
+
return new MeshCliError(`No tenant declared: ${looked}, and MESH_TENANT is not set. ` +
|
|
184
|
+
`There is no default tenant — "mesh" is the live hub and is never assumed. ` +
|
|
185
|
+
`Run from a directory whose Pulumi.<stack>.yaml declares mesh:tenant, set MESH_TENANT, ` +
|
|
186
|
+
`or pass --tenant where the command accepts one.`, {
|
|
187
|
+
remediation: {
|
|
188
|
+
command: "MESH_TENANT=<tenant> mesh … # or --tenant <tenant> where accepted, or run from a directory whose Pulumi.<stack>.yaml declares mesh:tenant",
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export function detectContext(stageArg, options = {}) {
|
|
17
193
|
let tenant;
|
|
18
194
|
let stage;
|
|
19
195
|
const cwd = process.cwd();
|
|
196
|
+
const stageHint = stageArg || process.env.MESH_STAGE || process.env.SST_STAGE;
|
|
197
|
+
let hasProjectFile = false;
|
|
198
|
+
let stackFile;
|
|
199
|
+
let stackFilesHere = [];
|
|
200
|
+
let namedStackMissing = false;
|
|
20
201
|
if (fs.existsSync("Pulumi.yaml")) {
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
202
|
+
hasProjectFile = true;
|
|
203
|
+
const files = fs
|
|
204
|
+
.readdirSync(".")
|
|
205
|
+
.filter((f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml")
|
|
206
|
+
.sort();
|
|
207
|
+
const picked = pickStackFile(files, stageHint, options.stack);
|
|
208
|
+
stackFile = picked.file;
|
|
209
|
+
namedStackMissing = options.stack !== undefined && picked.file === undefined;
|
|
210
|
+
stackFilesHere = files;
|
|
211
|
+
if (!picked.qualified && files.length > 1) {
|
|
212
|
+
const lengths = files.map((f) => f.length - "Pulumi..yaml".length).sort((a, b) => a - b);
|
|
213
|
+
if (lengths[0] === lengths[1]) {
|
|
214
|
+
logWarn(`Several Pulumi stack configs here (${files.join(", ")}); using ${stackFile}. ` +
|
|
215
|
+
`Pass --stack to choose one.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (stackFile) {
|
|
219
|
+
const content = fs.readFileSync(stackFile, "utf-8");
|
|
220
|
+
const tenantMatch = content.match(/^\s*mesh:tenant:\s*["']?([^"'#\n]+)["']?/m);
|
|
26
221
|
if (tenantMatch?.[1]) {
|
|
27
222
|
tenant = tenantMatch[1].trim();
|
|
28
223
|
}
|
|
29
|
-
const stageMatch =
|
|
224
|
+
const stageMatch = stackFile.match(/Pulumi\.(.+)\.yaml/);
|
|
30
225
|
if (stageMatch?.[1]) {
|
|
31
226
|
stage = stageMatch[1];
|
|
32
227
|
}
|
|
33
228
|
}
|
|
34
229
|
}
|
|
35
|
-
const sstConfigPath =
|
|
230
|
+
const sstConfigPath = findFileUpwardInRepo("sst.config.ts");
|
|
36
231
|
let sstDir;
|
|
37
232
|
if (sstConfigPath) {
|
|
38
233
|
sstDir = path.dirname(sstConfigPath);
|
|
39
|
-
const parentConfigPath = path.join(sstDir, "..", "config.ts");
|
|
40
|
-
if (fs.existsSync(parentConfigPath)) {
|
|
41
|
-
const configContent = fs.readFileSync(parentConfigPath, "utf-8");
|
|
42
|
-
const tenantMatch = configContent.match(/tenant:\s*["']([^"']+)["']/);
|
|
43
|
-
if (tenantMatch) {
|
|
44
|
-
tenant = tenantMatch[1];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
234
|
const sstStagePath = path.join(sstDir, ".sst", "stage");
|
|
48
235
|
if (fs.existsSync(sstStagePath)) {
|
|
49
236
|
stage = fs.readFileSync(sstStagePath, "utf-8").trim();
|
|
50
237
|
}
|
|
51
238
|
}
|
|
52
|
-
|
|
53
|
-
|
|
239
|
+
const repoRoot = findRepoRoot(cwd);
|
|
240
|
+
const parentConfigPath = (sstDir
|
|
241
|
+
? [path.join(sstDir, "config.ts"), path.join(sstDir, "..", "config.ts")]
|
|
242
|
+
: hasProjectFile
|
|
243
|
+
? [path.resolve(cwd, "..", "config.ts")]
|
|
244
|
+
: [])
|
|
245
|
+
.filter((p) => repoRoot !== undefined && !path.relative(repoRoot, p).startsWith(".."))
|
|
246
|
+
.find((p) => fs.existsSync(p));
|
|
247
|
+
const parentConfig = parentConfigPath ? fs.readFileSync(parentConfigPath, "utf-8") : undefined;
|
|
248
|
+
if (parentConfig && !tenant) {
|
|
249
|
+
tenant = extractConfigTenant(parentConfig);
|
|
250
|
+
}
|
|
251
|
+
if (!tenant && repoRoot) {
|
|
252
|
+
const pathMatch = path.relative(repoRoot, cwd).match(/(?:^|\/)tenants\/([^/]+)/);
|
|
54
253
|
if (pathMatch) {
|
|
55
254
|
tenant = pathMatch[1];
|
|
56
255
|
}
|
|
57
256
|
}
|
|
58
|
-
|
|
59
|
-
|
|
257
|
+
if (!tenant) {
|
|
258
|
+
tenant = findMeshJson(cwd)?.data.tenant;
|
|
259
|
+
}
|
|
260
|
+
stage = stageHint || stage || "dev";
|
|
261
|
+
const detectedTenant = tenant;
|
|
262
|
+
tenant = options.tenant || process.env.MESH_TENANT || tenant;
|
|
263
|
+
if (!tenant) {
|
|
264
|
+
throw tenantNotDeclared(cwd, { stackFile, hasProjectFile });
|
|
265
|
+
}
|
|
266
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(tenant)) {
|
|
267
|
+
throw new MeshCliError(`Invalid tenant "${tenant}": expected lowercase alphanumerics and hyphens. ` +
|
|
268
|
+
`This usually means a config file was misparsed.`, { remediation: { command: "mesh <command> --tenant <tenant>" } });
|
|
269
|
+
}
|
|
270
|
+
const overrideSource = options.tenant ? "--tenant" : "MESH_TENANT";
|
|
271
|
+
const tenantOverridden = detectedTenant !== undefined && detectedTenant !== tenant;
|
|
272
|
+
if (tenantOverridden) {
|
|
273
|
+
logWarn(`${overrideSource}=${tenant} overrides the tenant detected here (${detectedTenant}); ` +
|
|
274
|
+
`paths will use ${tenant} while implicit AWS credentials still come from ` +
|
|
275
|
+
`the ${detectedTenant} app's deployer role`);
|
|
276
|
+
}
|
|
60
277
|
let platformEnv = stage;
|
|
61
|
-
|
|
278
|
+
const platformEnvMap = Object.create(null);
|
|
62
279
|
let defaultPlatformEnv;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
280
|
+
if (parentConfig) {
|
|
281
|
+
const cleaned = stripComments(parentConfig);
|
|
282
|
+
const platformScope = extractPlatformBlock(cleaned);
|
|
283
|
+
const legacyScope = extractExportedConfigSource(cleaned);
|
|
284
|
+
const mapSource = (scope, key) => {
|
|
285
|
+
if (!scope)
|
|
286
|
+
return undefined;
|
|
287
|
+
const at = scope.search(new RegExp(`${key}\\s*:\\s*\\{`));
|
|
288
|
+
if (at === -1)
|
|
289
|
+
return undefined;
|
|
290
|
+
return matchBlock(scope, scope.indexOf("{", at));
|
|
291
|
+
};
|
|
292
|
+
const mapBody = mapSource(platformScope, "envMap") ?? mapSource(legacyScope, "platformEnvMap");
|
|
293
|
+
if (mapBody) {
|
|
294
|
+
const entries = mapBody.matchAll(/(?:"([^"]+)"|'([^']+)'|([\w-]+))\s*:\s*["']([^"']+)["']/g);
|
|
69
295
|
for (const entry of entries) {
|
|
70
|
-
const key = entry[1];
|
|
71
|
-
const value = entry[
|
|
296
|
+
const key = entry[1] ?? entry[2] ?? entry[3];
|
|
297
|
+
const value = entry[4];
|
|
72
298
|
if (key && value) {
|
|
73
299
|
platformEnvMap[key] = value;
|
|
74
300
|
}
|
|
75
301
|
}
|
|
76
302
|
}
|
|
77
|
-
const defaultMatch =
|
|
303
|
+
const defaultMatch = platformScope?.match(/defaultEnv:\s*["']([^"']+)["']/) ??
|
|
304
|
+
legacyScope?.match(/defaultPlatformEnv:\s*["']([^"']+)["']/);
|
|
78
305
|
if (defaultMatch?.[1]) {
|
|
79
306
|
defaultPlatformEnv = defaultMatch[1];
|
|
80
307
|
}
|
|
81
308
|
}
|
|
82
|
-
const tenantPrefix =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
309
|
+
const tenantPrefix = [detectedTenant, tenant]
|
|
310
|
+
.filter((t) => Boolean(t))
|
|
311
|
+
.map((t) => `${t}-`)
|
|
312
|
+
.sort((a, b) => b.length - a.length)
|
|
313
|
+
.find((p) => stage.startsWith(p));
|
|
314
|
+
const parsedEnv = tenantPrefix ? stage.slice(tenantPrefix.length) : stage;
|
|
315
|
+
const mapApplies = !tenantOverridden;
|
|
316
|
+
if (tenantOverridden && (Object.keys(platformEnvMap).length > 0 || defaultPlatformEnv)) {
|
|
317
|
+
logWarn(`${overrideSource}=${tenant} also leaves the ${detectedTenant} config's envMap ` +
|
|
318
|
+
`unapplied — it maps ${detectedTenant}'s stages, not ${tenant}'s — so the ` +
|
|
319
|
+
`platform env is the stage itself ("${parsedEnv}"). Pass -e/--env where the ` +
|
|
320
|
+
`command takes one, or run from the ${tenant} app's own directory.`);
|
|
321
|
+
}
|
|
322
|
+
const mapped = mapApplies ? (platformEnvMap[stage] ?? platformEnvMap[parsedEnv]) : undefined;
|
|
323
|
+
if (mapped) {
|
|
324
|
+
platformEnv = mapped;
|
|
325
|
+
}
|
|
326
|
+
else if (mapApplies && defaultPlatformEnv) {
|
|
327
|
+
if (namedStackMissing) {
|
|
328
|
+
const known = [
|
|
329
|
+
...stackFilesHere.map((f) => f.slice("Pulumi.".length, -".yaml".length)),
|
|
330
|
+
...Object.keys(platformEnvMap),
|
|
331
|
+
].filter((v, i, a) => a.indexOf(v) === i);
|
|
332
|
+
throw new MeshCliError(`No such stack "${options.stack}": ${cwd} has no Pulumi.${options.stack}.yaml, ` +
|
|
333
|
+
`and the app's config.ts does not map it to a platform env — it would ` +
|
|
334
|
+
`address defaultEnv "${defaultPlatformEnv}", not the stack you asked for.`, {
|
|
335
|
+
remediation: {
|
|
336
|
+
command: `mesh <command> --stack <${known.join("|") || "stack"}>`,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
platformEnv = defaultPlatformEnv;
|
|
341
|
+
if (Object.keys(platformEnvMap).length > 0) {
|
|
342
|
+
logWarn(`Stage "${stage}" is not in the config's envMap ` +
|
|
343
|
+
`(${Object.keys(platformEnvMap).join(", ")}); using defaultEnv ` +
|
|
344
|
+
`"${defaultPlatformEnv}".`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
platformEnv = parsedEnv;
|
|
349
|
+
}
|
|
87
350
|
logInfo(`Stage: ${stage}, Platform: ${platformEnv}, Tenant: ${tenant}`);
|
|
88
|
-
return { tenant, stage, platformEnv };
|
|
351
|
+
return { tenant, stage, platformEnv, detectedTenant };
|
|
89
352
|
}
|
|
@@ -45,6 +45,11 @@ export declare function ensureRegistrySession(opts: {
|
|
|
45
45
|
interactive: boolean;
|
|
46
46
|
}): Promise<RegistrySessionResult>;
|
|
47
47
|
export declare function registryTokenRoles(idToken: string | null | undefined): string[];
|
|
48
|
+
export interface RegistryAccountState {
|
|
49
|
+
email?: string;
|
|
50
|
+
emailVerified?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function registryAccountState(idToken: string | null | undefined): RegistryAccountState;
|
|
48
53
|
export interface RegistryRemint {
|
|
49
54
|
reminted: boolean;
|
|
50
55
|
rolesChanged: boolean;
|
|
@@ -138,6 +138,22 @@ export function registryTokenRoles(idToken) {
|
|
|
138
138
|
return [];
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
export function registryAccountState(idToken) {
|
|
142
|
+
if (!idToken)
|
|
143
|
+
return {};
|
|
144
|
+
let claims = null;
|
|
145
|
+
try {
|
|
146
|
+
claims = decodeJwtPayload(idToken);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
if (!claims)
|
|
152
|
+
return {};
|
|
153
|
+
const email = typeof claims.email === "string" ? claims.email : undefined;
|
|
154
|
+
const verified = claims.email_verified;
|
|
155
|
+
return { email, emailVerified: typeof verified === "boolean" ? verified : undefined };
|
|
156
|
+
}
|
|
141
157
|
export async function refreshRegistrySession(context) {
|
|
142
158
|
const before = registryTokenRoles(readCredentials(context)?.idToken);
|
|
143
159
|
const idToken = await forceRefreshToken(context);
|
|
@@ -27,8 +27,16 @@ readers the app's `docs` role admits. A site declared on a
|
|
|
27
27
|
`mesh.apps.VendorApiSurface` (a third-party core's raw reference) is read
|
|
28
28
|
through the same door by holders of the Hub's `vendor-docs` add-on only —
|
|
29
29
|
never by a partner — and declaring `audience: { roles: [] }` on one fails
|
|
30
|
-
the deploy. `
|
|
31
|
-
|
|
30
|
+
the deploy. Add `actAs: true` on an app surface to let Hub operators execute
|
|
31
|
+
requests against the deployed API as one of the app's API keys from the Docs
|
|
32
|
+
tab (the Hub's act-as playground) — the surface then verifies the Hub's
|
|
33
|
+
signed assertion and runs the request as the key. `mesh app check` flags a
|
|
34
|
+
docs site put on an ingress (`DOCS_SITE_PUBLIC`). A vendor surface's site is
|
|
35
|
+
built from the enabled-ops cut of the core's spec, not the whole document, and
|
|
36
|
+
a spec over 1 MiB is split into per-entity variants at build time
|
|
37
|
+
(`docs.split: { above }` moves the threshold, `false` turns it off) — an app's
|
|
38
|
+
own spec is already the shape it serves and gets neither. Keep the spec in
|
|
39
|
+
step with the routes in
|
|
32
40
|
`api/src/index.ts` — the site renders what the file says. Keep pages `.md`,
|
|
33
41
|
not `.mdx`: a site an API surface declares is served by the Hub under its own
|
|
34
42
|
origin, which carries data and never code, so an `.mdx` page fails that
|
|
@@ -75,10 +75,10 @@ const api = new mesh.apps.Service("api", {
|
|
|
75
75
|
env.temporal!.link({ environmentPrefix: "TEMPORAL", taskQueue: TASK_QUEUE }),
|
|
76
76
|
{{/if}}
|
|
77
77
|
],
|
|
78
|
-
ingress: env.buildIngress({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
ingress: env.buildIngress({ subdomain: "{{name}}" }),
|
|
79
|
+
// Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
|
|
80
|
+
// `ingress.healthCheckPath` sets the ALB target group's path only, and a
|
|
81
|
+
// gateway (HTTPRoute) tenant has no target group — so the path belongs here.
|
|
82
82
|
healthCheck: "/health",
|
|
83
83
|
replicas: deploy ? 2 : 1,
|
|
84
84
|
resources: { cpu: "100m", memory: "128Mi" },
|
|
@@ -113,7 +113,17 @@ const worker = new mesh.apps.TemporalWorker("worker", {
|
|
|
113
113
|
// it from Access → People; that same role is what the Hub reads when a
|
|
114
114
|
// partner holding the Hub's `docs:<tenant>` role opens this app's Docs tab,
|
|
115
115
|
// and the app then appears in their Apps list. `mesh app check` flags a docs
|
|
116
|
-
// site put on an ingress instead (`DOCS_SITE_PUBLIC`).
|
|
116
|
+
// site put on an ingress instead (`DOCS_SITE_PUBLIC`). Declaring `actAs: true`
|
|
117
|
+
// on that same surface opens the Hub's act-as playground: an operator picks
|
|
118
|
+
// one of this app's API keys on the Docs tab and the Hub executes the request
|
|
119
|
+
// against the deployed API as that key, over the cluster network, with a
|
|
120
|
+
// signed assertion the surface verifies — the Hub never holds the key's secret.
|
|
121
|
+
// Whichever of those the surface declares, the platform bounds them at the
|
|
122
|
+
// workload: each workload it publishes an in-cluster address for allows the
|
|
123
|
+
// Hub on the ports it published and refuses it on every other one, so a
|
|
124
|
+
// registry entry cannot walk the Hub onto a port the surface never opened.
|
|
125
|
+
// Nothing here declares that — it rides the surface (`hubDialPolicy`, on by
|
|
126
|
+
// default wherever the namespace is in the service mesh).
|
|
117
127
|
// =============================================================================
|
|
118
128
|
|
|
119
129
|
new mesh.apps.ApiDocs("docs", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mesh-tech/mesh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "CLI for Mesh platform development utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@mesh-tech/agent-contracts": "0.2.0",
|
|
45
|
-
"@mesh-tech/tsconfig": "2.
|
|
45
|
+
"@mesh-tech/tsconfig": "2.16.0",
|
|
46
46
|
"@mesh-tech/workflow-viz": "0.3.0",
|
|
47
47
|
"@tanstack/intent": "^0.3.6",
|
|
48
48
|
"@types/node": "^20.0.0",
|