@mesh-tech/mesh-cli 0.28.0 → 0.30.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 +11 -2
- package/dist/bin/mesh.js +1199 -888
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check-resolve.d.ts +28 -0
- package/dist/src/commands/app-check-resolve.js +276 -0
- package/dist/src/commands/app-check.d.ts +6 -3
- package/dist/src/commands/app-check.js +29 -6
- package/dist/src/commands/login.d.ts +8 -0
- package/dist/src/commands/login.js +84 -32
- package/dist/src/commands/temporal.js +49 -27
- package/dist/src/commands/temporal.js.map +1 -1
- package/fragments/base/index.ts.hbs +3 -0
- package/package.json +2 -2
- package/skills/core/SKILL.md +10 -4
- package/templates/platform-repo/platform/index.ts +10 -0
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ProbeResult {
|
|
2
|
+
findings: UnresolvedImport[];
|
|
3
|
+
unchecked: string | null;
|
|
4
|
+
}
|
|
5
|
+
export interface UnresolvedImport {
|
|
6
|
+
pkg: string;
|
|
7
|
+
specifier: string;
|
|
8
|
+
file: string;
|
|
9
|
+
importer: string;
|
|
10
|
+
via: string[];
|
|
11
|
+
fix: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function stripComments(source: string): string;
|
|
14
|
+
export declare function moduleSpecifiers(source: string): string[];
|
|
15
|
+
export declare function splitSpecifier(specifier: string): {
|
|
16
|
+
pkg: string;
|
|
17
|
+
sub: string;
|
|
18
|
+
} | null;
|
|
19
|
+
export declare function findPackageDir(fromDir: string, pkg: string): string | null;
|
|
20
|
+
export declare function resolveSubpath(pkgDir: string, sub: string): string | null;
|
|
21
|
+
export declare function programEntry(appDir: string): string | null;
|
|
22
|
+
export declare function dependenciesInstalled(dir: string, stopAt?: string): boolean;
|
|
23
|
+
export interface ProbeOptions {
|
|
24
|
+
entry?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
root?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveProgramImports(appDir: string, options?: ProbeOptions): ProbeResult;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
const CONDITIONS = new Set(["source", "node", "import", "require", "module", "module-sync", "default"]);
|
|
6
|
+
const EXTENSIONS = ["", ".ts", ".tsx", ".js", ".mjs", ".cjs", "/index.ts", "/index.js", "/index.mjs"];
|
|
7
|
+
const SCOPE = "@mesh-tech/";
|
|
8
|
+
const FILE_BUDGET = 1500;
|
|
9
|
+
const BUILTINS = new Set(builtinModules);
|
|
10
|
+
export function stripComments(source) {
|
|
11
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`\\])\/\/[^\n]*/g, "$1");
|
|
12
|
+
}
|
|
13
|
+
export function moduleSpecifiers(source) {
|
|
14
|
+
const text = stripComments(source);
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const m of text.matchAll(/(?<![.\w$])(?:import|export)\b([^;]*?)\bfrom\s*["']([^"']+)["']/g)) {
|
|
17
|
+
if (/^\s*type\s/.test(m[1] ?? ""))
|
|
18
|
+
continue;
|
|
19
|
+
out.push(m[2] ?? "");
|
|
20
|
+
}
|
|
21
|
+
for (const m of text.matchAll(/(?<![.\w$])import\s*["']([^"']+)["']/g))
|
|
22
|
+
out.push(m[1] ?? "");
|
|
23
|
+
for (const m of text.matchAll(/(?<![.\w$])import\s*\(\s*["']([^"']+)["']\s*\)/g))
|
|
24
|
+
out.push(m[1] ?? "");
|
|
25
|
+
for (const m of text.matchAll(/(?<![.\w$])require\s*\(\s*["']([^"']+)["']\s*\)/g))
|
|
26
|
+
out.push(m[1] ?? "");
|
|
27
|
+
return [...new Set(out)].filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
export function splitSpecifier(specifier) {
|
|
30
|
+
if (!specifier || specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#"))
|
|
31
|
+
return null;
|
|
32
|
+
if (specifier.includes(":"))
|
|
33
|
+
return null;
|
|
34
|
+
if (BUILTINS.has(specifier))
|
|
35
|
+
return null;
|
|
36
|
+
const parts = specifier.split("/");
|
|
37
|
+
const depth = specifier.startsWith("@") ? 2 : 1;
|
|
38
|
+
if (parts.length < depth || parts.some((p) => p === ""))
|
|
39
|
+
return null;
|
|
40
|
+
const pkg = parts.slice(0, depth).join("/");
|
|
41
|
+
const rest = parts.slice(depth);
|
|
42
|
+
return { pkg, sub: rest.length > 0 ? `./${rest.join("/")}` : "." };
|
|
43
|
+
}
|
|
44
|
+
export function findPackageDir(fromDir, pkg) {
|
|
45
|
+
let dir = path.resolve(fromDir);
|
|
46
|
+
for (;;) {
|
|
47
|
+
const candidate = path.join(dir, "node_modules", ...pkg.split("/"));
|
|
48
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) {
|
|
49
|
+
try {
|
|
50
|
+
return fs.realpathSync(candidate);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const parent = path.dirname(dir);
|
|
57
|
+
if (parent === dir)
|
|
58
|
+
return null;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function conditionTargets(node, acc = []) {
|
|
63
|
+
if (typeof node === "string") {
|
|
64
|
+
acc.push(node);
|
|
65
|
+
return acc;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(node)) {
|
|
68
|
+
for (const entry of node)
|
|
69
|
+
conditionTargets(entry, acc);
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
if (!node || typeof node !== "object")
|
|
73
|
+
return acc;
|
|
74
|
+
for (const [key, value] of Object.entries(node)) {
|
|
75
|
+
if (!CONDITIONS.has(key))
|
|
76
|
+
continue;
|
|
77
|
+
conditionTargets(value, acc);
|
|
78
|
+
}
|
|
79
|
+
return acc;
|
|
80
|
+
}
|
|
81
|
+
function exportTargets(exportsField, sub) {
|
|
82
|
+
if (typeof exportsField === "string")
|
|
83
|
+
return sub === "." ? [exportsField] : [];
|
|
84
|
+
if (Array.isArray(exportsField))
|
|
85
|
+
return exportsField.flatMap((entry) => exportTargets(entry, sub));
|
|
86
|
+
if (!exportsField || typeof exportsField !== "object")
|
|
87
|
+
return [];
|
|
88
|
+
const map = exportsField;
|
|
89
|
+
const keys = Object.keys(map);
|
|
90
|
+
if (keys.length > 0 && !keys.some((k) => k.startsWith(".")))
|
|
91
|
+
return sub === "." ? conditionTargets(map) : [];
|
|
92
|
+
if (sub in map)
|
|
93
|
+
return conditionTargets(map[sub]);
|
|
94
|
+
let best = [];
|
|
95
|
+
let bestPrefix = -1;
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const star = key.indexOf("*");
|
|
98
|
+
if (star === -1)
|
|
99
|
+
continue;
|
|
100
|
+
const prefix = key.slice(0, star);
|
|
101
|
+
const suffix = key.slice(star + 1);
|
|
102
|
+
if (sub.length < prefix.length + suffix.length)
|
|
103
|
+
continue;
|
|
104
|
+
if (!sub.startsWith(prefix) || !sub.endsWith(suffix))
|
|
105
|
+
continue;
|
|
106
|
+
if (prefix.length <= bestPrefix)
|
|
107
|
+
continue;
|
|
108
|
+
const segment = sub.slice(prefix.length, sub.length - suffix.length);
|
|
109
|
+
bestPrefix = prefix.length;
|
|
110
|
+
best = conditionTargets(map[key]).map((target) => target.replaceAll("*", segment));
|
|
111
|
+
}
|
|
112
|
+
return best;
|
|
113
|
+
}
|
|
114
|
+
function isDirectory(p) {
|
|
115
|
+
try {
|
|
116
|
+
return fs.statSync(p).isDirectory();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function firstFile(base) {
|
|
123
|
+
for (const ext of EXTENSIONS) {
|
|
124
|
+
const candidate = base + ext;
|
|
125
|
+
try {
|
|
126
|
+
if (fs.statSync(candidate).isFile())
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
export function resolveSubpath(pkgDir, sub) {
|
|
135
|
+
let manifest = {};
|
|
136
|
+
try {
|
|
137
|
+
manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (manifest.exports !== undefined) {
|
|
143
|
+
for (const target of exportTargets(manifest.exports, sub)) {
|
|
144
|
+
const file = firstFile(path.resolve(pkgDir, target));
|
|
145
|
+
if (file)
|
|
146
|
+
return file;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (sub === ".")
|
|
151
|
+
return firstFile(path.resolve(pkgDir, manifest.main ?? "index.js"));
|
|
152
|
+
return firstFile(path.resolve(pkgDir, sub));
|
|
153
|
+
}
|
|
154
|
+
export function programEntry(appDir) {
|
|
155
|
+
const app = path.resolve(appDir);
|
|
156
|
+
const index = path.join(app, "index.ts");
|
|
157
|
+
if (fs.existsSync(index))
|
|
158
|
+
return index;
|
|
159
|
+
let main;
|
|
160
|
+
try {
|
|
161
|
+
main = parseYaml(fs.readFileSync(path.join(app, "Pulumi.yaml"), "utf8"))?.main;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
if (typeof main !== "string" || !main)
|
|
167
|
+
return null;
|
|
168
|
+
const target = path.resolve(app, main);
|
|
169
|
+
return isDirectory(target) ? firstFile(path.join(target, "index")) : firstFile(target);
|
|
170
|
+
}
|
|
171
|
+
export function dependenciesInstalled(dir, stopAt) {
|
|
172
|
+
const root = stopAt === undefined ? null : path.resolve(stopAt);
|
|
173
|
+
let at = path.resolve(dir);
|
|
174
|
+
for (;;) {
|
|
175
|
+
if (fs.existsSync(path.join(at, "node_modules")))
|
|
176
|
+
return true;
|
|
177
|
+
if (at === root)
|
|
178
|
+
return false;
|
|
179
|
+
const parent = path.dirname(at);
|
|
180
|
+
if (parent === at)
|
|
181
|
+
return false;
|
|
182
|
+
at = parent;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function suggestedRange(appDir, parentDir) {
|
|
186
|
+
try {
|
|
187
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(appDir, "package.json"), "utf8"));
|
|
188
|
+
for (const section of [manifest.dependencies, manifest.devDependencies]) {
|
|
189
|
+
for (const [name, range] of Object.entries(section ?? {})) {
|
|
190
|
+
if (name.startsWith(SCOPE) && range)
|
|
191
|
+
return range;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
}
|
|
197
|
+
if (!parentDir)
|
|
198
|
+
return null;
|
|
199
|
+
try {
|
|
200
|
+
const version = JSON.parse(fs.readFileSync(path.join(parentDir, "package.json"), "utf8")).version;
|
|
201
|
+
return version ? `^${version}` : null;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function displayPath(app, visit) {
|
|
208
|
+
const within = path.relative(visit.pkg === null ? app : visit.pkgDir, visit.file).split(path.sep).join("/");
|
|
209
|
+
return visit.pkg === null ? within : `${visit.pkg}/${within}`;
|
|
210
|
+
}
|
|
211
|
+
export function resolveProgramImports(appDir, options = {}) {
|
|
212
|
+
const app = path.resolve(appDir);
|
|
213
|
+
const label = options.label ?? path.basename(app);
|
|
214
|
+
const entryFile = options.entry ? firstFile(path.resolve(app, options.entry)) : programEntry(app);
|
|
215
|
+
if (!dependenciesInstalled(app, options.root)) {
|
|
216
|
+
return { findings: [], unchecked: "dependencies are not installed (run the install, then re-run)" };
|
|
217
|
+
}
|
|
218
|
+
if (!entryFile) {
|
|
219
|
+
return { findings: [], unchecked: "no Pulumi program found (index.ts, or the file Pulumi.yaml's `main` names)" };
|
|
220
|
+
}
|
|
221
|
+
const findings = [];
|
|
222
|
+
const reported = new Set();
|
|
223
|
+
const seen = new Set([entryFile]);
|
|
224
|
+
const queue = [{ file: entryFile, pkgDir: app, pkg: null, via: [] }];
|
|
225
|
+
let budget = FILE_BUDGET;
|
|
226
|
+
while (queue.length > 0 && budget-- > 0) {
|
|
227
|
+
const here = queue.shift();
|
|
228
|
+
let source;
|
|
229
|
+
try {
|
|
230
|
+
source = fs.readFileSync(here.file, "utf8");
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const fromDir = path.dirname(here.file);
|
|
236
|
+
for (const specifier of moduleSpecifiers(source)) {
|
|
237
|
+
if (specifier.startsWith(".")) {
|
|
238
|
+
if (here.pkg === null)
|
|
239
|
+
continue;
|
|
240
|
+
const next = firstFile(path.resolve(fromDir, specifier));
|
|
241
|
+
if (next && !seen.has(next) && next.startsWith(here.pkgDir)) {
|
|
242
|
+
seen.add(next);
|
|
243
|
+
queue.push({ ...here, file: next });
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const split = splitSpecifier(specifier);
|
|
248
|
+
if (!split)
|
|
249
|
+
continue;
|
|
250
|
+
const inScope = split.pkg.startsWith(SCOPE);
|
|
251
|
+
if (here.pkg !== null && !inScope)
|
|
252
|
+
continue;
|
|
253
|
+
const pkgDir = findPackageDir(fromDir, split.pkg);
|
|
254
|
+
const target = pkgDir ? resolveSubpath(pkgDir, split.sub) : null;
|
|
255
|
+
if (!pkgDir || !target) {
|
|
256
|
+
if (reported.has(specifier))
|
|
257
|
+
continue;
|
|
258
|
+
reported.add(specifier);
|
|
259
|
+
const range = inScope ? suggestedRange(app, here.pkg === null ? null : here.pkgDir) : null;
|
|
260
|
+
const line = `"${split.pkg}": "${range ?? (inScope ? "<the version your other @mesh-tech packages use>" : "<the version you need>")}"`;
|
|
261
|
+
const fix = pkgDir
|
|
262
|
+
? `${split.pkg} is installed but ships no ${split.sub} — the installed version predates the subpath; raise ${line} in ${label}/package.json`
|
|
263
|
+
: `add ${line} to ${label}/package.json dependencies`;
|
|
264
|
+
findings.push({ pkg: split.pkg, specifier, file: here.file, importer: displayPath(app, here), via: here.via, fix });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (!inScope || seen.has(target))
|
|
268
|
+
continue;
|
|
269
|
+
seen.add(target);
|
|
270
|
+
queue.push({ file: target, pkgDir, pkg: split.pkg, via: [...here.via, specifier] });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
findings.sort((a, b) => a.pkg.localeCompare(b.pkg) || a.specifier.localeCompare(b.specifier));
|
|
274
|
+
const unchecked = queue.length > 0 ? `the import graph exceeded ${FILE_BUDGET} files` : null;
|
|
275
|
+
return { findings, unchecked };
|
|
276
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { type ProbeResult } from "./app-check-resolve.js";
|
|
2
3
|
import { jsonReport, type Check, type CheckResult, type CheckStatus } from "./dev-doctor.js";
|
|
3
|
-
export type AppCheckCode = "UI_IN_API" | "NO_UI_SERVICE" | "NO_REGISTER" | "CUSTOM_SESSION_AUTH" | "NON_PLATFORM_LOGGER" | "LOCAL_AUTH_LIB" | "NO_AUTHZ_POINTER" | "METADATA_UNPUBLISHED" | "IAC_GRANTS" | "POINTER_PROJECT_OVERRIDE" | "PASSWORD_STORE" | "EMAIL_ALLOWLIST" | "USERS_TABLE" | "DOCS_SITE_PUBLIC" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE" | "DOCS_VENDOR_AUDIENCE_PARTNER";
|
|
4
|
+
export type AppCheckCode = "UNRESOLVABLE_IMPORT" | "UI_IN_API" | "NO_UI_SERVICE" | "NO_REGISTER" | "CUSTOM_SESSION_AUTH" | "NON_PLATFORM_LOGGER" | "LOCAL_AUTH_LIB" | "NO_AUTHZ_POINTER" | "METADATA_UNPUBLISHED" | "IAC_GRANTS" | "POINTER_PROJECT_OVERRIDE" | "PASSWORD_STORE" | "EMAIL_ALLOWLIST" | "USERS_TABLE" | "DOCS_SITE_PUBLIC" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE" | "DOCS_VENDOR_AUDIENCE_PARTNER";
|
|
4
5
|
export interface AppCheckRule {
|
|
5
6
|
gate: string;
|
|
6
7
|
title: string;
|
|
@@ -35,14 +36,16 @@ export declare function docsSiteFindings(text: string): Array<{
|
|
|
35
36
|
}>;
|
|
36
37
|
export declare function statedKind(decl: Pick<SurfaceDeclaration, "ctor">, args: Map<string, string>): string | undefined;
|
|
37
38
|
export declare function listApps(root: string): string[];
|
|
38
|
-
export declare function
|
|
39
|
+
export declare function probeResolution(root: string, appRel: string): ProbeResult;
|
|
40
|
+
export declare function scanApp(root: string, appRel: string, resolution?: ProbeResult): Finding[];
|
|
39
41
|
export declare function scanRepo(root: string): Finding[];
|
|
40
42
|
export interface AppCheckContext {
|
|
41
43
|
root: string;
|
|
42
44
|
app: string | null;
|
|
43
45
|
findings: Finding[];
|
|
46
|
+
unchecked: string | null;
|
|
44
47
|
}
|
|
45
|
-
export declare function resultFor(code: AppCheckCode, findings: Finding[]): CheckResult;
|
|
48
|
+
export declare function resultFor(code: AppCheckCode, findings: Finding[], unchecked?: string | null): CheckResult;
|
|
46
49
|
export declare function appContractChecks(scope: "app" | "repo"): Check<AppCheckContext>[];
|
|
47
50
|
export interface AppCheckReport {
|
|
48
51
|
root: string;
|
|
@@ -3,8 +3,17 @@ import * as path from "node:path";
|
|
|
3
3
|
import { logError, logInfo } from "../utils/index.js";
|
|
4
4
|
import { MeshCliError, emitJsonPayload } from "../utils/errors.js";
|
|
5
5
|
import { resolveTargetRoot } from "./skills.js";
|
|
6
|
+
import { programEntry, resolveProgramImports } from "./app-check-resolve.js";
|
|
6
7
|
import { ICONS, aggregateStatus, jsonReport, renderHuman, runChecks, } from "./dev-doctor.js";
|
|
7
8
|
export const APP_CHECK_RULES = {
|
|
9
|
+
UNRESOLVABLE_IMPORT: {
|
|
10
|
+
gate: "0.6",
|
|
11
|
+
title: "The Pulumi program's imports resolve",
|
|
12
|
+
severity: "block",
|
|
13
|
+
remediation: "declare the package in apps/<app>/package.json dependencies, at the range the app's other @mesh-tech/* packages use, and re-install — " +
|
|
14
|
+
"an optional peer that nothing names installs silently and fails at `mesh deploy`",
|
|
15
|
+
scope: "app",
|
|
16
|
+
},
|
|
8
17
|
UI_IN_API: {
|
|
9
18
|
gate: "0.1",
|
|
10
19
|
title: "UI is its own service",
|
|
@@ -610,7 +619,10 @@ export function listApps(root) {
|
|
|
610
619
|
apps.push(".");
|
|
611
620
|
return apps;
|
|
612
621
|
}
|
|
613
|
-
export function
|
|
622
|
+
export function probeResolution(root, appRel) {
|
|
623
|
+
return resolveProgramImports(path.resolve(root, appRel), { root, label: appRel === "." ? "this app" : appRel });
|
|
624
|
+
}
|
|
625
|
+
export function scanApp(root, appRel, resolution = probeResolution(root, appRel)) {
|
|
614
626
|
const app = path.resolve(root, appRel);
|
|
615
627
|
const index = path.join(app, "index.ts");
|
|
616
628
|
const findings = [];
|
|
@@ -620,6 +632,12 @@ export function scanApp(root, appRel) {
|
|
|
620
632
|
const hasProgram = fs.existsSync(index) || fs.existsSync(path.join(app, "Pulumi.yaml"));
|
|
621
633
|
if (!hasProgram)
|
|
622
634
|
return findings;
|
|
635
|
+
const entry = programEntry(app);
|
|
636
|
+
for (const bad of resolution.findings) {
|
|
637
|
+
const chain = [...bad.via, bad.specifier].join(" → ");
|
|
638
|
+
const at = rel(root, entry ?? index);
|
|
639
|
+
hit("UNRESOLVABLE_IMPORT", at, `${chain} does not resolve from this app (imported by ${bad.importer}) — ${bad.fix}`);
|
|
640
|
+
}
|
|
623
641
|
if (fs.existsSync(index)) {
|
|
624
642
|
const indexRel = rel(root, index);
|
|
625
643
|
let servesHtml = false;
|
|
@@ -728,9 +746,12 @@ export function scanRepo(root) {
|
|
|
728
746
|
}
|
|
729
747
|
return findings;
|
|
730
748
|
}
|
|
731
|
-
export function resultFor(code, findings) {
|
|
749
|
+
export function resultFor(code, findings, unchecked = null) {
|
|
732
750
|
const rule = APP_CHECK_RULES[code];
|
|
733
751
|
const mine = findings.filter((f) => f.code === code);
|
|
752
|
+
if (code === "UNRESOLVABLE_IMPORT" && unchecked) {
|
|
753
|
+
return { status: "ok", summary: `gate ${rule.gate}: not checked — ${unchecked}` };
|
|
754
|
+
}
|
|
734
755
|
if (mine.length === 0)
|
|
735
756
|
return { status: "ok", summary: `gate ${rule.gate}: no ${code} finding` };
|
|
736
757
|
return {
|
|
@@ -745,7 +766,7 @@ export function appContractChecks(scope) {
|
|
|
745
766
|
id: code,
|
|
746
767
|
title: `${APP_CHECK_RULES[code].title} (${APP_CHECK_RULES[code].gate})`,
|
|
747
768
|
phases: ["ondemand", "preflight"],
|
|
748
|
-
run: async (ctx) => resultFor(code, ctx.findings),
|
|
769
|
+
run: async (ctx) => resultFor(code, ctx.findings, ctx.unchecked),
|
|
749
770
|
}));
|
|
750
771
|
}
|
|
751
772
|
export async function runAppCheck(root, apps) {
|
|
@@ -758,10 +779,12 @@ export async function runAppCheck(root, apps) {
|
|
|
758
779
|
}
|
|
759
780
|
const scopes = [];
|
|
760
781
|
for (const app of targets) {
|
|
761
|
-
const
|
|
782
|
+
const resolution = probeResolution(root, app);
|
|
783
|
+
const ctx = { root, app, findings: scanApp(root, app, resolution), unchecked: resolution.unchecked };
|
|
784
|
+
const results = await runChecks(ctx, "ondemand", appContractChecks("app"));
|
|
762
785
|
scopes.push({ scope: app, ...jsonReport(results) });
|
|
763
786
|
}
|
|
764
|
-
const repoResults = await runChecks({ root, app: null, findings: scanRepo(root) }, "ondemand", appContractChecks("repo"));
|
|
787
|
+
const repoResults = await runChecks({ root, app: null, findings: scanRepo(root), unchecked: null }, "ondemand", appContractChecks("repo"));
|
|
765
788
|
scopes.push({ scope: "(repo)", ...jsonReport(repoResults) });
|
|
766
789
|
return { root, status: aggregateStatus(scopes.map((s) => ({ status: s.status, summary: "" }))), scopes };
|
|
767
790
|
}
|
|
@@ -787,7 +810,7 @@ export function registerAppCommands(program) {
|
|
|
787
810
|
const app = program.command("app").description("Inspect a tenant app against the Mesh app contract");
|
|
788
811
|
app
|
|
789
812
|
.command("check")
|
|
790
|
-
.description("Check tenant apps against the Mesh app contract (@mesh-tech/app-kit skills/apps/references/app-contract.md): a UI is its own service, the app registers with the Hub, sign-in goes through the platform proxy, logs go through @mesh-tech/logger,
|
|
813
|
+
.description("Check tenant apps against the Mesh app contract (@mesh-tech/app-kit skills/apps/references/app-contract.md): a UI is its own service, the app registers with the Hub, sign-in goes through the platform proxy, logs go through @mesh-tech/logger, people/roles/keys are surfaced through the Hub, and the Pulumi program's imports resolve from the app directory. Exit 1 on any blocking finding — the same gate mesh create-app, tenant CI and the platform reviewer run. The import gate reads the installed dependency tree, so run it after the install; with no node_modules it reports itself not checked rather than met.")
|
|
791
814
|
.argument("[apps...]", "repo-relative app dirs to check (default: every apps/* with a Pulumi program)")
|
|
792
815
|
.option("--root <dir>", "checkout to scan (default: the enclosing git root)")
|
|
793
816
|
.option("--json", "machine-readable output (one report per app plus the repo-scoped checks)", false)
|
|
@@ -3,6 +3,7 @@ import { type AwsIdentity } from "../utils/aws-auth.js";
|
|
|
3
3
|
declare const CONFIG_DIR: string;
|
|
4
4
|
declare const CONFIG_FILE: string;
|
|
5
5
|
declare const CREDENTIALS_FILE: string;
|
|
6
|
+
export declare function loginScopes(config: Pick<ContextConfig, "hubProjectId">): string;
|
|
6
7
|
interface ContextConfig {
|
|
7
8
|
issuer: string;
|
|
8
9
|
clientId: string;
|
|
@@ -12,7 +13,9 @@ interface ContextConfig {
|
|
|
12
13
|
vpn?: string;
|
|
13
14
|
vpnJoinBroker?: string;
|
|
14
15
|
registryBroker?: string;
|
|
16
|
+
hubProjectId?: string;
|
|
15
17
|
}
|
|
18
|
+
export declare function withDiscoveredFields(config: ContextConfig, data: Record<string, unknown>): ContextConfig;
|
|
16
19
|
interface CachedCredentials {
|
|
17
20
|
idToken: string;
|
|
18
21
|
accessToken: string;
|
|
@@ -46,6 +49,7 @@ export declare function renderAwsFallbackStatus(context: string, reason: string,
|
|
|
46
49
|
lines: StatusLine[];
|
|
47
50
|
exitCode: 0 | 1;
|
|
48
51
|
};
|
|
52
|
+
export declare function tokenAudience(payload: Record<string, unknown>): string[];
|
|
49
53
|
export declare function registerLoginCommand(program: Command): void;
|
|
50
54
|
export declare const RESERVED_REGISTRY_CONTEXT = "registry";
|
|
51
55
|
export declare const NO_REGISTRY_DEPRECATED = "--no-registry is deprecated and ignored: mesh login no longer touches the registry.";
|
|
@@ -107,6 +111,10 @@ export declare function ensureLogin(context: string, opts?: {
|
|
|
107
111
|
device?: boolean;
|
|
108
112
|
interactive?: boolean;
|
|
109
113
|
}): Promise<CachedCredentials | null>;
|
|
114
|
+
export declare function adoptPublishedHubProject(context: string, config: ContextConfig, discover?: (context: string, opts: {
|
|
115
|
+
quiet: true;
|
|
116
|
+
silent: true;
|
|
117
|
+
}) => Promise<ContextConfig | null>, write?: (context: string, config: ContextConfig) => void): Promise<ContextConfig>;
|
|
110
118
|
export { CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE };
|
|
111
119
|
export type { ContextConfig, CachedCredentials, CredentialsFile };
|
|
112
120
|
export declare function readAllContextConfigs(): ConfigFile;
|