@mesh-tech/mesh-cli 0.38.1 → 0.40.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/dist/bin/{mesh.auth-preflight-AQJ5TQES.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
- package/dist/bin/{mesh.chunk-I6NXLXGC.js → mesh.chunk-2ASXJ5DO.js} +235 -581
- package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
- package/dist/bin/{mesh.chunk-IU2IWH6O.js → mesh.chunk-6AIG5OJM.js} +3 -3
- package/dist/bin/{mesh.chunk-KN4BZOYH.js → mesh.chunk-6SSOVZOI.js} +1 -1
- package/dist/bin/{mesh.chunk-LMYPTJ2F.js → mesh.chunk-7HTKSUWR.js} +1900 -577
- package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
- package/dist/bin/{mesh.chunk-2YRWJH5X.js → mesh.chunk-F44A3ACW.js} +4 -4
- package/dist/bin/{mesh.chunk-2YRWJH5X.js.map → mesh.chunk-F44A3ACW.js.map} +1 -1
- package/dist/bin/{mesh.chunk-BF2SH6XT.js → mesh.chunk-HJGMOEUG.js} +1 -1
- package/dist/bin/{mesh.chunk-TZAGJTKU.js → mesh.chunk-NSQJEX36.js} +1 -1
- package/dist/bin/{mesh.chunk-HPYZWPRM.js → mesh.chunk-PZVI7V3J.js} +1 -1
- package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
- package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
- package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
- package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
- package/dist/bin/{mesh.create-app-NGXACFAU.js → mesh.create-app-Z3RKSTBO.js} +3 -3
- package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
- package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
- package/dist/bin/mesh.js +9 -8
- package/dist/bin/mesh.js.map +1 -1
- package/dist/bin/{mesh.login-7SIAKFXJ.js → mesh.login-3AWVGPCM.js} +2 -2
- package/dist/bin/{mesh.platform-setup-CJXE3BCP.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
- package/dist/bin/{mesh.program-BIDQBSSI.js → mesh.program-K5JQXEHX.js} +9 -8
- package/dist/bin/{mesh.registry-7P4MGXEK.js → mesh.registry-JLQYFUNN.js} +4 -4
- package/dist/bin/{mesh.registry-identity-JW4KO7N7.js → mesh.registry-identity-3AR27UGO.js} +3 -3
- package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
- package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
- package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
- package/dist/bin/{mesh.start-MPQFIEMH.js → mesh.start-KZ5L6SOJ.js} +2 -2
- package/dist/bin/{mesh.wizard-ZJE4S6PC.js → mesh.wizard-FCTT55KI.js} +7 -7
- package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
- package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.js +2 -2
- package/dist/src/commands/dev-doctor.d.ts +22 -0
- package/dist/src/commands/dev-doctor.js +149 -0
- package/dist/src/commands/dev-env-leaks.d.ts +24 -0
- package/dist/src/commands/dev-env-leaks.js +178 -0
- package/dist/src/commands/dev-launch.d.ts +17 -1
- package/dist/src/commands/dev-launch.js +130 -5
- package/dist/src/commands/dev-readiness.d.ts +49 -0
- package/dist/src/commands/dev-readiness.js +78 -0
- package/dist/src/commands/dev.d.ts +60 -0
- package/dist/src/commands/dev.js +574 -140
- package/dist/src/commands/hub/index.js +10 -7
- package/dist/src/commands/local/app-authz.d.ts +118 -0
- package/dist/src/commands/local/app-authz.js +295 -0
- package/dist/src/commands/local/auth-provision.d.ts +1 -0
- package/dist/src/commands/local/auth-provision.js +12 -1
- package/dist/src/commands/local/index.js +2 -0
- package/dist/src/commands/mcp/generate.d.ts +11 -0
- package/dist/src/commands/mcp/generate.js +78 -0
- package/dist/src/commands/mcp/index.d.ts +2 -0
- package/dist/src/commands/mcp/index.js +70 -0
- package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
- package/dist/src/commands/mcp/manifest-file.js +24 -0
- package/dist/src/commands/mcp/review-command.d.ts +24 -0
- package/dist/src/commands/mcp/review-command.js +66 -0
- package/dist/src/commands/mcp/review.d.ts +41 -0
- package/dist/src/commands/mcp/review.js +140 -0
- package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
- package/dist/src/commands/mcp/workspace-membership.js +77 -0
- package/dist/src/commands/secrets/reindex.js +2 -7
- package/dist/src/commands/secrets/set.d.ts +22 -1
- package/dist/src/commands/secrets/set.js +31 -8
- package/dist/src/commands/temporal.js +51 -5
- package/dist/src/commands/temporal.js.map +2 -2
- package/dist/src/program.js +2 -0
- package/dist/src/utils/aws-auth.d.ts +4 -0
- package/dist/src/utils/aws-auth.js +7 -3
- package/dist/src/utils/dev-sessions-root.d.ts +3 -0
- package/dist/src/utils/dev-sessions-root.js +45 -0
- package/dist/src/utils/kubeconfig.js +2 -2
- package/dist/src/utils/tailscale.js +7 -1
- package/dist/src/utils/temporal-auth.js +2 -1
- package/dist/src/utils/workflow-fingerprint.js +23 -12
- package/fragments/base/index.ts.hbs +4 -1
- package/package.json +7 -5
- package/skills/mesh/SKILL.md +10 -6
- package/templates/api-auth/api/src/schema.ts.hbs +17 -0
- package/templates/api-auth/index.ts.hbs +1 -0
- package/templates/api-auth/package.json.hbs +3 -0
- package/templates/api-role-gating/api/src/schema.ts.hbs +10 -0
- package/templates/api-role-gating/index.ts.hbs +1 -0
- package/templates/external-service/index.ts.hbs +1 -0
- package/templates/workflow/index.ts.hbs +2 -0
- package/dist/bin/mesh.chunk-I6NXLXGC.js.map +0 -6
- package/dist/bin/mesh.chunk-LMYPTJ2F.js.map +0 -6
- /package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-IU2IWH6O.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-KN4BZOYH.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-BF2SH6XT.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-TZAGJTKU.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-HPYZWPRM.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
- /package/dist/bin/{mesh.create-app-NGXACFAU.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
- /package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
- /package/dist/bin/{mesh.login-7SIAKFXJ.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
- /package/dist/bin/{mesh.platform-setup-CJXE3BCP.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
- /package/dist/bin/{mesh.program-BIDQBSSI.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
- /package/dist/bin/{mesh.registry-7P4MGXEK.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
- /package/dist/bin/{mesh.registry-identity-JW4KO7N7.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
- /package/dist/bin/{mesh.start-MPQFIEMH.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
- /package/dist/bin/{mesh.wizard-ZJE4S6PC.js.map → mesh.wizard-FCTT55KI.js.map} +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as net from "node:net";
|
|
4
|
-
import * as os from "node:os";
|
|
5
4
|
import * as path from "node:path";
|
|
6
5
|
import chalk from "chalk";
|
|
7
|
-
import { buildLaunchCommand, waitForPort, writeEnvFile } from "../dev-launch.js";
|
|
6
|
+
import { buildLaunchCommand, pickLaunchHostEnv, waitForPort, writeEnvFile } from "../dev-launch.js";
|
|
8
7
|
import { tunnelClientAddress } from "../dev.js";
|
|
9
8
|
import { MeshCliError } from "../../utils/errors.js";
|
|
10
9
|
import { DEFAULT_HUB_PORT } from "../local/stack.js";
|
|
11
10
|
import { logInfo, logSuccess, logWarn } from "../../utils/log.js";
|
|
12
11
|
import { findAppRoot } from "../../utils/pulumi.js";
|
|
12
|
+
import { devSessionsRoot } from "../../utils/dev-sessions-root.js";
|
|
13
13
|
export const HUB_SESSION_PREFIX = "mesh-hub-dev";
|
|
14
14
|
export function hubSessionName(apiPort) {
|
|
15
15
|
return `${HUB_SESSION_PREFIX}-${apiPort}`;
|
|
@@ -44,7 +44,7 @@ export function killInstructions(targets) {
|
|
|
44
44
|
export const DEFAULT_HUB_UI_PORT = Number(DEFAULT_HUB_PORT);
|
|
45
45
|
export const DEFAULT_HUB_API_PORT = 3002;
|
|
46
46
|
export function devSessionsDir() {
|
|
47
|
-
return
|
|
47
|
+
return devSessionsRoot();
|
|
48
48
|
}
|
|
49
49
|
export function listDevSessions(dir = devSessionsDir()) {
|
|
50
50
|
if (!fs.existsSync(dir))
|
|
@@ -279,7 +279,7 @@ function isPortListening(port) {
|
|
|
279
279
|
});
|
|
280
280
|
}
|
|
281
281
|
function hubEnvDir(sessionName) {
|
|
282
|
-
return path.join(
|
|
282
|
+
return path.join(devSessionsRoot(), sessionName);
|
|
283
283
|
}
|
|
284
284
|
export function parsePort(raw, fallback, source) {
|
|
285
285
|
if (raw === undefined)
|
|
@@ -375,6 +375,7 @@ async function hubDevAction(opts) {
|
|
|
375
375
|
const apiDir = path.join(platformDir, "apps", "hub", "api");
|
|
376
376
|
const uiDir = path.join(platformDir, "apps", "hub", "ui");
|
|
377
377
|
execFileSync("tmux", ["new-session", "-d", "-s", hubSession, "-n", "api", "-c", apiDir]);
|
|
378
|
+
const hostEnv = pickLaunchHostEnv(process.env);
|
|
378
379
|
if (!assembled.apiEnv.DEV_USER_TOKEN_URL && assembled.apiEnv.DEV_USER_ID_TOKEN) {
|
|
379
380
|
const platform = session.state.devOutput.platform;
|
|
380
381
|
const credContext = `mesh.${platform.env}`;
|
|
@@ -384,11 +385,13 @@ async function hubDevAction(opts) {
|
|
|
384
385
|
execFileSync("tmux", ["set-option", "-t", `${hubSession}:token-server`, "remain-on-exit", "on"], {
|
|
385
386
|
stdio: "ignore",
|
|
386
387
|
});
|
|
388
|
+
const tokenServerEnvFile = path.join(hubEnvDir(hubSession), "token-server.env.sh");
|
|
389
|
+
writeEnvFile(tokenServerEnvFile, hostEnv);
|
|
387
390
|
execFileSync("tmux", [
|
|
388
391
|
"send-keys",
|
|
389
392
|
"-t",
|
|
390
393
|
`${hubSession}:token-server`,
|
|
391
|
-
`npx mesh dev __token-server ${tokenPort} ${credContext}`,
|
|
394
|
+
buildLaunchCommand(tokenServerEnvFile, platformDir, `npx mesh dev __token-server ${tokenPort} ${credContext}`, undefined, hostEnv),
|
|
392
395
|
"Enter",
|
|
393
396
|
]);
|
|
394
397
|
for (let i = 0; i < 15 && !(await isPortListening(tokenPort)); i++) {
|
|
@@ -400,14 +403,14 @@ async function hubDevAction(opts) {
|
|
|
400
403
|
}
|
|
401
404
|
const launch = (window, dir, env, cmd, createWindow) => {
|
|
402
405
|
const envFile = path.join(hubEnvDir(hubSession), `${window}.env.sh`);
|
|
403
|
-
writeEnvFile(envFile, env);
|
|
406
|
+
writeEnvFile(envFile, { ...hostEnv, ...env });
|
|
404
407
|
if (createWindow) {
|
|
405
408
|
execFileSync("tmux", ["new-window", "-t", hubSession, "-n", window, "-c", dir]);
|
|
406
409
|
}
|
|
407
410
|
execFileSync("tmux", ["set-option", "-t", `${hubSession}:${window}`, "remain-on-exit", "on"], {
|
|
408
411
|
stdio: "ignore",
|
|
409
412
|
});
|
|
410
|
-
execFileSync("tmux", ["send-keys", "-t", `${hubSession}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
|
|
413
|
+
execFileSync("tmux", ["send-keys", "-t", `${hubSession}:${window}`, buildLaunchCommand(envFile, dir, cmd, undefined, hostEnv), "Enter"]);
|
|
411
414
|
};
|
|
412
415
|
launch("api", apiDir, assembled.apiEnv, "pnpm dev", false);
|
|
413
416
|
launch("ui", uiDir, assembled.uiEnv, `pnpm dev --port ${uiPort} --strictPort`, true);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export declare const LOCAL_APP_SPICEDB_IMAGE = "authzed/spicedb:v1.42.1";
|
|
2
|
+
export declare const LOCAL_APP_SPICEDB_KEY = "local-dev-key";
|
|
3
|
+
export declare const LOCAL_APP_SPICEDB_LABEL = "mesh.local.app-spicedb";
|
|
4
|
+
export declare const LOCAL_STACK_NETWORK = "mesh-local_mesh_network";
|
|
5
|
+
export type AuthzMode = "policy-engine" | "role-gating";
|
|
6
|
+
export declare function readLocalAuthzConfig(appRoot: string): {
|
|
7
|
+
schema: string;
|
|
8
|
+
prebuilt: boolean;
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function appSpiceDBSlot(app: string): number;
|
|
11
|
+
export declare function appSpiceDBPorts(slot: number): {
|
|
12
|
+
grpc: number;
|
|
13
|
+
http: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const appSpiceDBContainerName: (app: string) => string;
|
|
16
|
+
export declare const appSpiceDBDatabase: (app: string) => string;
|
|
17
|
+
export declare const appSpiceDBSecretName: (tenant: string, env: string, app: string) => string;
|
|
18
|
+
export declare const spicedbInstanceParam: (tenant: string, env: string, instanceId: string) => string;
|
|
19
|
+
export declare const appAuthzParam: (tenant: string, env: string, app: string) => string;
|
|
20
|
+
export declare function instanceRegistryValue(args: {
|
|
21
|
+
app: string;
|
|
22
|
+
tenant: string;
|
|
23
|
+
env: string;
|
|
24
|
+
grpcPort: number;
|
|
25
|
+
httpPort: number;
|
|
26
|
+
}): Record<string, unknown>;
|
|
27
|
+
export declare function buildAppAuthzPointer(args: {
|
|
28
|
+
existing: Record<string, unknown>;
|
|
29
|
+
mode: AuthzMode;
|
|
30
|
+
instanceId?: string;
|
|
31
|
+
zitadel?: {
|
|
32
|
+
projectId: string;
|
|
33
|
+
orgId: string;
|
|
34
|
+
};
|
|
35
|
+
metadataRef: string;
|
|
36
|
+
}): Record<string, unknown>;
|
|
37
|
+
export declare function appSpiceDBEnv(args: {
|
|
38
|
+
grpcPort: number;
|
|
39
|
+
httpPort: number;
|
|
40
|
+
metadataPath: string;
|
|
41
|
+
}): Record<string, string>;
|
|
42
|
+
export declare function spicedbMigrateArgs(app: string): string[];
|
|
43
|
+
export declare function spicedbServeArgs(app: string, ports: {
|
|
44
|
+
grpc: number;
|
|
45
|
+
http: number;
|
|
46
|
+
}): string[];
|
|
47
|
+
export declare function parseDockerPort(output: string): number | null;
|
|
48
|
+
export type DockerResult = {
|
|
49
|
+
status: number | null;
|
|
50
|
+
stdout: string;
|
|
51
|
+
stderr: string;
|
|
52
|
+
};
|
|
53
|
+
export type DockerRunner = (args: string[]) => DockerResult;
|
|
54
|
+
export declare const defaultDocker: DockerRunner;
|
|
55
|
+
export declare function ensureLocalAppSpiceDB(app: string, deps?: {
|
|
56
|
+
docker?: DockerRunner;
|
|
57
|
+
portFree?: (port: number) => Promise<boolean>;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
grpcPort: number;
|
|
60
|
+
httpPort: number;
|
|
61
|
+
created: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function writeSpiceDBSchema(httpUrl: string, zed: string, opts?: {
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
attempts?: number;
|
|
66
|
+
delayMs?: number;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
export declare function removeLocalAppSpiceDBs(docker?: DockerRunner): string[];
|
|
69
|
+
export type ModuleImporter = (specifier: string, parentFile: string) => Promise<Record<string, any>>;
|
|
70
|
+
export declare const tsxImporter: ModuleImporter;
|
|
71
|
+
export type AppSchema = {
|
|
72
|
+
namespace: string;
|
|
73
|
+
mode?: AuthzMode;
|
|
74
|
+
} & Record<string, unknown>;
|
|
75
|
+
type Compilers = {
|
|
76
|
+
compileOpsHubMetadata: (schema: AppSchema) => unknown;
|
|
77
|
+
compileSpiceDBSchema: (schema: AppSchema) => {
|
|
78
|
+
zed: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export declare function loadAppAuthz(appRoot: string, schemaRel: string, importer?: ModuleImporter): Promise<{
|
|
82
|
+
schema: AppSchema;
|
|
83
|
+
compilers: Compilers;
|
|
84
|
+
chunk?: (body: string) => {
|
|
85
|
+
value: string;
|
|
86
|
+
parts: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
}>;
|
|
89
|
+
export type ParamStore = {
|
|
90
|
+
get(name: string): Promise<string | undefined>;
|
|
91
|
+
put(name: string, value: string): Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
export declare const localParamStore: () => Promise<ParamStore>;
|
|
94
|
+
export type PublishLocalAppAuthzDeps = {
|
|
95
|
+
importer?: ModuleImporter;
|
|
96
|
+
params?: ParamStore;
|
|
97
|
+
putSecret?: (name: string, value: Record<string, string>) => Promise<void>;
|
|
98
|
+
ensureSpiceDB?: typeof ensureLocalAppSpiceDB;
|
|
99
|
+
writeSchema?: typeof writeSpiceDBSchema;
|
|
100
|
+
};
|
|
101
|
+
export declare function publishLocalAppAuthz(args: {
|
|
102
|
+
appRoot: string;
|
|
103
|
+
schema: string;
|
|
104
|
+
tenant: string;
|
|
105
|
+
app: string;
|
|
106
|
+
zitadel?: {
|
|
107
|
+
projectId: string;
|
|
108
|
+
orgId: string;
|
|
109
|
+
};
|
|
110
|
+
}, deps?: PublishLocalAppAuthzDeps): Promise<{
|
|
111
|
+
mode: AuthzMode;
|
|
112
|
+
env: Record<string, string>;
|
|
113
|
+
instance?: {
|
|
114
|
+
grpcPort: number;
|
|
115
|
+
httpPort: number;
|
|
116
|
+
};
|
|
117
|
+
}>;
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { MeshCliError } from "../../utils/errors.js";
|
|
8
|
+
import { upsertLocalSecret, probeTcp } from "./helpers.js";
|
|
9
|
+
import { LOCAL_AWS_CONFIG, LOCAL_ENV } from "./seed.js";
|
|
10
|
+
import { COMPOSE_PROJECT } from "./stack.js";
|
|
11
|
+
export const LOCAL_APP_SPICEDB_IMAGE = "authzed/spicedb:v1.42.1";
|
|
12
|
+
export const LOCAL_APP_SPICEDB_KEY = "local-dev-key";
|
|
13
|
+
export const LOCAL_APP_SPICEDB_LABEL = "mesh.local.app-spicedb";
|
|
14
|
+
export const LOCAL_STACK_NETWORK = `${COMPOSE_PROJECT}_mesh_network`;
|
|
15
|
+
const STACK = "local";
|
|
16
|
+
const GRPC_PORT_BASE = 50100;
|
|
17
|
+
const HTTP_PORT_BASE = 18100;
|
|
18
|
+
const PORT_SLOTS = 100;
|
|
19
|
+
export function readLocalAuthzConfig(appRoot) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf-8"));
|
|
22
|
+
const schema = pkg?.mesh?.authz?.schema;
|
|
23
|
+
if (typeof schema !== "string" || !schema.trim())
|
|
24
|
+
return null;
|
|
25
|
+
return { schema: schema.trim(), prebuilt: pkg?.mesh?.authz?.metadata !== undefined };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const slug = (name, sep) => name.toLowerCase().replace(/[^a-z0-9]+/g, sep).replace(new RegExp(`^\\${sep}+|\\${sep}+$`, "g"), "");
|
|
32
|
+
export function appSpiceDBSlot(app) {
|
|
33
|
+
return createHash("sha256").update(app).digest().readUInt32BE(0) % PORT_SLOTS;
|
|
34
|
+
}
|
|
35
|
+
export function appSpiceDBPorts(slot) {
|
|
36
|
+
const s = ((slot % PORT_SLOTS) + PORT_SLOTS) % PORT_SLOTS;
|
|
37
|
+
return { grpc: GRPC_PORT_BASE + s, http: HTTP_PORT_BASE + s };
|
|
38
|
+
}
|
|
39
|
+
export const appSpiceDBContainerName = (app) => `mesh-local-spicedb-${slug(app, "-")}`;
|
|
40
|
+
export const appSpiceDBDatabase = (app) => `spicedb_${slug(app, "_")}`.slice(0, 63);
|
|
41
|
+
export const appSpiceDBSecretName = (tenant, env, app) => `mesh/${tenant}/${env}/${app}/spicedb/preshared-key`;
|
|
42
|
+
export const spicedbInstanceParam = (tenant, env, instanceId) => `/mesh-platform/${tenant}/${env}/authz/spicedb-instances/${instanceId}`;
|
|
43
|
+
export const appAuthzParam = (tenant, env, app) => `/mesh-platform/${tenant}/${env}/apps/${app}/stacks/${STACK}/authz`;
|
|
44
|
+
export function instanceRegistryValue(args) {
|
|
45
|
+
const secret = appSpiceDBSecretName(args.tenant, args.env, args.app);
|
|
46
|
+
return {
|
|
47
|
+
grpcEndpoint: `localhost:${args.grpcPort}`,
|
|
48
|
+
httpEndpoint: `localhost:${args.httpPort}`,
|
|
49
|
+
namespace: args.app,
|
|
50
|
+
presharedKeySecretName: secret,
|
|
51
|
+
configSecretName: secret,
|
|
52
|
+
owner: { kind: "app", name: args.app },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function buildAppAuthzPointer(args) {
|
|
56
|
+
const existingZitadel = args.existing.zitadel && typeof args.existing.zitadel === "object"
|
|
57
|
+
? args.existing.zitadel
|
|
58
|
+
: {};
|
|
59
|
+
return {
|
|
60
|
+
...args.existing,
|
|
61
|
+
spicedb: {
|
|
62
|
+
instanceRefs: args.mode === "policy-engine" && args.instanceId ? [{ instanceId: args.instanceId }] : [],
|
|
63
|
+
},
|
|
64
|
+
...(args.zitadel || Object.keys(existingZitadel).length > 0
|
|
65
|
+
? { zitadel: { ...existingZitadel, ...(args.zitadel ?? {}) } }
|
|
66
|
+
: {}),
|
|
67
|
+
opsHubMetadataRef: args.metadataRef,
|
|
68
|
+
mode: args.mode,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function appSpiceDBEnv(args) {
|
|
72
|
+
return {
|
|
73
|
+
SPICEDB_ENDPOINT: `localhost:${args.grpcPort}`,
|
|
74
|
+
SPICEDB_HTTP_ENDPOINT: `http://localhost:${args.httpPort}`,
|
|
75
|
+
SPICEDB_PRESHARED_KEY: LOCAL_APP_SPICEDB_KEY,
|
|
76
|
+
SPICEDB_TOKEN: LOCAL_APP_SPICEDB_KEY,
|
|
77
|
+
AUTHZ_METADATA_SSM_PATH: args.metadataPath,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const datastoreUri = (app) => `postgres://postgres:postgres@database:5432/${appSpiceDBDatabase(app)}?sslmode=disable`;
|
|
81
|
+
export function spicedbMigrateArgs(app) {
|
|
82
|
+
return [
|
|
83
|
+
"run", "--rm", "--network", LOCAL_STACK_NETWORK,
|
|
84
|
+
"-e", "SPICEDB_DATASTORE_ENGINE=postgres",
|
|
85
|
+
"-e", `SPICEDB_DATASTORE_CONN_URI=${datastoreUri(app)}`,
|
|
86
|
+
LOCAL_APP_SPICEDB_IMAGE, "datastore", "migrate", "head",
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
export function spicedbServeArgs(app, ports) {
|
|
90
|
+
return [
|
|
91
|
+
"run", "-d",
|
|
92
|
+
"--name", appSpiceDBContainerName(app),
|
|
93
|
+
"--label", `${LOCAL_APP_SPICEDB_LABEL}=${app}`,
|
|
94
|
+
"--restart", "unless-stopped",
|
|
95
|
+
"--network", LOCAL_STACK_NETWORK,
|
|
96
|
+
"-p", `127.0.0.1:${ports.grpc}:50051`,
|
|
97
|
+
"-p", `127.0.0.1:${ports.http}:8443`,
|
|
98
|
+
"-e", `SPICEDB_GRPC_PRESHARED_KEY=${LOCAL_APP_SPICEDB_KEY}`,
|
|
99
|
+
"-e", "SPICEDB_DATASTORE_ENGINE=postgres",
|
|
100
|
+
"-e", `SPICEDB_DATASTORE_CONN_URI=${datastoreUri(app)}`,
|
|
101
|
+
"-e", "SPICEDB_HTTP_ENABLED=true",
|
|
102
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_READ_MAX_OPEN=4",
|
|
103
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_READ_MIN_OPEN=1",
|
|
104
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_WRITE_MAX_OPEN=2",
|
|
105
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_WRITE_MIN_OPEN=1",
|
|
106
|
+
LOCAL_APP_SPICEDB_IMAGE, "serve",
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
export function parseDockerPort(output) {
|
|
110
|
+
const match = output.trim().split("\n")[0]?.match(/:(\d+)\s*$/);
|
|
111
|
+
return match ? Number(match[1]) : null;
|
|
112
|
+
}
|
|
113
|
+
export const defaultDocker = (args) => {
|
|
114
|
+
const r = spawnSync("docker", args, { encoding: "utf-8" });
|
|
115
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
116
|
+
};
|
|
117
|
+
const mustDocker = (docker, args, what) => {
|
|
118
|
+
const r = docker(args);
|
|
119
|
+
if (r.status !== 0) {
|
|
120
|
+
throw new MeshCliError(`${what} failed: ${(r.stderr || r.stdout).trim() || `docker exited ${r.status}`}`);
|
|
121
|
+
}
|
|
122
|
+
return r.stdout;
|
|
123
|
+
};
|
|
124
|
+
function ensureDatabase(docker, app) {
|
|
125
|
+
const container = mustDocker(docker, [
|
|
126
|
+
"ps", "-q",
|
|
127
|
+
"--filter", `label=com.docker.compose.project=${COMPOSE_PROJECT}`,
|
|
128
|
+
"--filter", "label=com.docker.compose.service=database",
|
|
129
|
+
], "Finding the local Postgres").trim().split("\n")[0];
|
|
130
|
+
if (!container) {
|
|
131
|
+
throw new MeshCliError("The local platform's Postgres is not running.", {
|
|
132
|
+
remediation: { command: "mesh start" },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const db = appSpiceDBDatabase(app);
|
|
136
|
+
const exists = mustDocker(docker, ["exec", container, "psql", "-U", "postgres", "-tAc", `SELECT 1 FROM pg_database WHERE datname = '${db}'`], `Checking database ${db}`).trim();
|
|
137
|
+
if (exists !== "1") {
|
|
138
|
+
mustDocker(docker, ["exec", container, "psql", "-U", "postgres", "-c", `CREATE DATABASE "${db}"`], `Creating database ${db}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function ensureLocalAppSpiceDB(app, deps = {}) {
|
|
142
|
+
const docker = deps.docker ?? defaultDocker;
|
|
143
|
+
const portFree = deps.portFree ?? (async (port) => !(await probeTcp(port, { timeoutMs: 300 })));
|
|
144
|
+
const name = appSpiceDBContainerName(app);
|
|
145
|
+
const state = docker(["inspect", "-f", "{{.State.Running}}", name]);
|
|
146
|
+
if (state.status === 0) {
|
|
147
|
+
if (state.stdout.trim() !== "true")
|
|
148
|
+
mustDocker(docker, ["start", name], `Starting ${name}`);
|
|
149
|
+
const grpcPort = parseDockerPort(mustDocker(docker, ["port", name, "50051/tcp"], `Reading ${name}'s ports`));
|
|
150
|
+
const httpPort = parseDockerPort(mustDocker(docker, ["port", name, "8443/tcp"], `Reading ${name}'s ports`));
|
|
151
|
+
if (grpcPort && httpPort)
|
|
152
|
+
return { grpcPort, httpPort, created: false };
|
|
153
|
+
mustDocker(docker, ["rm", "-f", name], `Removing ${name}`);
|
|
154
|
+
}
|
|
155
|
+
ensureDatabase(docker, app);
|
|
156
|
+
mustDocker(docker, spicedbMigrateArgs(app), `Migrating ${name}'s datastore`);
|
|
157
|
+
const first = appSpiceDBSlot(app);
|
|
158
|
+
for (let i = 0; i < PORT_SLOTS; i++) {
|
|
159
|
+
const ports = appSpiceDBPorts(first + i);
|
|
160
|
+
if (!(await portFree(ports.grpc)) || !(await portFree(ports.http)))
|
|
161
|
+
continue;
|
|
162
|
+
mustDocker(docker, spicedbServeArgs(app, ports), `Starting ${name}`);
|
|
163
|
+
return { grpcPort: ports.grpc, httpPort: ports.http, created: true };
|
|
164
|
+
}
|
|
165
|
+
throw new MeshCliError(`No free port pair for ${name} in ${GRPC_PORT_BASE}-${GRPC_PORT_BASE + PORT_SLOTS - 1}.`);
|
|
166
|
+
}
|
|
167
|
+
export async function writeSpiceDBSchema(httpUrl, zed, opts = {}) {
|
|
168
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
169
|
+
const attempts = opts.attempts ?? 30;
|
|
170
|
+
let last = "";
|
|
171
|
+
for (let i = 0; i < attempts; i++) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetchImpl(`${httpUrl}/v1/schema/write`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { Authorization: `Bearer ${LOCAL_APP_SPICEDB_KEY}`, "content-type": "application/json" },
|
|
176
|
+
body: JSON.stringify({ schema: zed }),
|
|
177
|
+
});
|
|
178
|
+
if (res.ok)
|
|
179
|
+
return;
|
|
180
|
+
last = `${res.status} ${await res.text()}`;
|
|
181
|
+
if (res.status >= 400 && res.status < 500)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
last = err instanceof Error ? err.message : String(err);
|
|
186
|
+
}
|
|
187
|
+
await new Promise((r) => setTimeout(r, opts.delayMs ?? 500));
|
|
188
|
+
}
|
|
189
|
+
throw new MeshCliError(`SpiceDB at ${httpUrl} did not accept the app's schema: ${last}`);
|
|
190
|
+
}
|
|
191
|
+
export function removeLocalAppSpiceDBs(docker = defaultDocker) {
|
|
192
|
+
const ids = docker(["ps", "-aq", "--filter", `label=${LOCAL_APP_SPICEDB_LABEL}`]).stdout.trim().split("\n").filter(Boolean);
|
|
193
|
+
if (ids.length > 0)
|
|
194
|
+
docker(["rm", "-f", ...ids]);
|
|
195
|
+
return ids;
|
|
196
|
+
}
|
|
197
|
+
export const tsxImporter = async (specifier, parentFile) => {
|
|
198
|
+
let api;
|
|
199
|
+
try {
|
|
200
|
+
api = createRequire(parentFile).resolve("tsx/esm/api");
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw new MeshCliError(`Cannot load the app's authz schema: \`tsx\` is not installed in ${path.dirname(parentFile)}. ` +
|
|
204
|
+
"Add it to the app's devDependencies (`pnpm add -D tsx`).");
|
|
205
|
+
}
|
|
206
|
+
const { tsImport } = (await import(pathToFileURL(api).href));
|
|
207
|
+
return tsImport(specifier, pathToFileURL(parentFile).href);
|
|
208
|
+
};
|
|
209
|
+
const unwrapDefault = (mod) => {
|
|
210
|
+
const d = mod.default ?? mod.schema;
|
|
211
|
+
return d && typeof d === "object" && "default" in d && !("namespace" in d) ? d.default : d;
|
|
212
|
+
};
|
|
213
|
+
export async function loadAppAuthz(appRoot, schemaRel, importer = tsxImporter) {
|
|
214
|
+
const file = path.resolve(appRoot, schemaRel);
|
|
215
|
+
if (!fs.existsSync(file)) {
|
|
216
|
+
throw new MeshCliError(`mesh.authz.schema names ${schemaRel}, which does not exist under ${appRoot}.`);
|
|
217
|
+
}
|
|
218
|
+
const schema = unwrapDefault(await importer(pathToFileURL(file).href, file));
|
|
219
|
+
if (!schema || typeof schema.namespace !== "string") {
|
|
220
|
+
throw new MeshCliError(`${schemaRel} must export the app's defineSchema(...) result as its default export.`);
|
|
221
|
+
}
|
|
222
|
+
const compilers = (await importer("@mesh-tech/authz/compilers", file));
|
|
223
|
+
let chunk;
|
|
224
|
+
try {
|
|
225
|
+
const ssm = await importer("@mesh-tech/authz/metadata/ssm", file);
|
|
226
|
+
if (typeof ssm.chunkSsmValue === "function")
|
|
227
|
+
chunk = ssm.chunkSsmValue;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
}
|
|
231
|
+
return { schema, compilers, chunk };
|
|
232
|
+
}
|
|
233
|
+
export const localParamStore = async () => {
|
|
234
|
+
const { SSMClient, GetParameterCommand, PutParameterCommand } = await import("@aws-sdk/client-ssm");
|
|
235
|
+
const ssm = new SSMClient(LOCAL_AWS_CONFIG);
|
|
236
|
+
return {
|
|
237
|
+
async get(name) {
|
|
238
|
+
try {
|
|
239
|
+
return (await ssm.send(new GetParameterCommand({ Name: name }))).Parameter?.Value;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
async put(name, value) {
|
|
246
|
+
await ssm.send(new PutParameterCommand({ Name: name, Value: value, Type: "String", Tier: "Advanced", Overwrite: true }));
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
export async function publishLocalAppAuthz(args, deps = {}) {
|
|
251
|
+
const env = LOCAL_ENV;
|
|
252
|
+
const { schema, compilers, chunk } = await loadAppAuthz(args.appRoot, args.schema, deps.importer);
|
|
253
|
+
const mode = schema.mode === "role-gating" ? "role-gating" : "policy-engine";
|
|
254
|
+
const params = deps.params ?? (await localParamStore());
|
|
255
|
+
const base = appAuthzParam(args.tenant, env, args.app);
|
|
256
|
+
const metadataRef = `${base}/ops-hub-metadata`;
|
|
257
|
+
let instance;
|
|
258
|
+
if (mode === "policy-engine") {
|
|
259
|
+
const running = await (deps.ensureSpiceDB ?? ensureLocalAppSpiceDB)(args.app);
|
|
260
|
+
instance = { grpcPort: running.grpcPort, httpPort: running.httpPort };
|
|
261
|
+
await (deps.writeSchema ?? writeSpiceDBSchema)(`http://localhost:${instance.httpPort}`, compilers.compileSpiceDBSchema(schema).zed);
|
|
262
|
+
await (deps.putSecret ?? upsertLocalSecret)(appSpiceDBSecretName(args.tenant, env, args.app), {
|
|
263
|
+
PRESHARED_KEY: LOCAL_APP_SPICEDB_KEY,
|
|
264
|
+
});
|
|
265
|
+
await params.put(spicedbInstanceParam(args.tenant, env, args.app), JSON.stringify(instanceRegistryValue({ app: args.app, tenant: args.tenant, env, ...instance })));
|
|
266
|
+
}
|
|
267
|
+
const body = JSON.stringify(compilers.compileOpsHubMetadata(schema));
|
|
268
|
+
const split = chunk ? chunk(body) : { value: body, parts: [] };
|
|
269
|
+
for (const [i, part] of split.parts.entries())
|
|
270
|
+
await params.put(`${metadataRef}/part-${i}`, part);
|
|
271
|
+
await params.put(metadataRef, split.value);
|
|
272
|
+
let existing = {};
|
|
273
|
+
const raw = await params.get(base);
|
|
274
|
+
if (raw) {
|
|
275
|
+
try {
|
|
276
|
+
const parsed = JSON.parse(raw);
|
|
277
|
+
if (parsed && typeof parsed === "object")
|
|
278
|
+
existing = parsed;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
await params.put(base, JSON.stringify(buildAppAuthzPointer({
|
|
284
|
+
existing,
|
|
285
|
+
mode,
|
|
286
|
+
instanceId: instance ? args.app : undefined,
|
|
287
|
+
zitadel: args.zitadel,
|
|
288
|
+
metadataRef,
|
|
289
|
+
})));
|
|
290
|
+
return {
|
|
291
|
+
mode,
|
|
292
|
+
env: instance ? appSpiceDBEnv({ ...instance, metadataPath: `${base}/metadata` }) : {},
|
|
293
|
+
...(instance ? { instance } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -21,6 +21,7 @@ export declare function reconcileRegistryFromZitadel(): Promise<{
|
|
|
21
21
|
tenants: string[];
|
|
22
22
|
apps: number;
|
|
23
23
|
}>;
|
|
24
|
+
export declare function isZitadelNoChanges(err: unknown): boolean;
|
|
24
25
|
export declare function ensureSignInApp(args: {
|
|
25
26
|
tenant: string;
|
|
26
27
|
app: string;
|
|
@@ -197,6 +197,11 @@ export async function reconcileRegistryFromZitadel() {
|
|
|
197
197
|
}
|
|
198
198
|
return { tenants, apps };
|
|
199
199
|
}
|
|
200
|
+
export function isZitadelNoChanges(err) {
|
|
201
|
+
if (!(err instanceof Error))
|
|
202
|
+
return false;
|
|
203
|
+
return err.status === 400 && /No changes|COMMAND-1m88i/.test(err.message);
|
|
204
|
+
}
|
|
200
205
|
export async function ensureSignInApp(args) {
|
|
201
206
|
const pat = readSeederPat();
|
|
202
207
|
const orgId = await ensureOrg(pat, args.tenant);
|
|
@@ -220,7 +225,13 @@ export async function ensureSignInApp(args) {
|
|
|
220
225
|
};
|
|
221
226
|
const existing = await searchZitadelApp(pat, projectId, name, orgId);
|
|
222
227
|
if (existing) {
|
|
223
|
-
|
|
228
|
+
try {
|
|
229
|
+
await api(pat, "PUT", `/management/v1/projects/${projectId}/apps/${existing.id}/oidc_config`, config, orgId);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (!isZitadelNoChanges(err))
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
224
235
|
return;
|
|
225
236
|
}
|
|
226
237
|
const created = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/oidc`, { name, ...config }, orgId);
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { logError, logInfo, logSuccess, logWarn } from "../../utils/log.js";
|
|
5
5
|
import { MeshCliError } from "../../utils/errors.js";
|
|
6
|
+
import { removeLocalAppSpiceDBs } from "./app-authz.js";
|
|
6
7
|
import { compose, COMPOSE_PROJECT, composeStreamed, crashedServices, DEFAULT_HUB_PORT, ensureDockerAvailable, findPackageRoot, hubApiRunning, hubEndpoints, hubOverlayServices, hubPort, idpsOverlayServices, ONE_SHOT_SERVICES, overlayRunning, probeEndpoint, STACK_ENDPOINTS, stackOwnedElsewhere, stackServices, } from "./stack.js";
|
|
7
8
|
import { probeRegistryToken, REGISTRY_LOGIN_FIX } from "../../utils/auth-preflight.js";
|
|
8
9
|
import { ensureHubAuthImage, ensureHubImages, buildHubImagesFromSource, readHubCompiledAuthz, hasRegistryAuth, localHubVersion, planWithHubRefresh, } from "./hub-local.js";
|
|
@@ -314,6 +315,7 @@ export function registerLocalCommands(program) {
|
|
|
314
315
|
if (foreign && !opts.force) {
|
|
315
316
|
throw new MeshCliError(`The running local platform was started from a different checkout (${foreign}) — another project may be using it.`, { remediation: { command: "mesh stop --force # stop it anyway" } });
|
|
316
317
|
}
|
|
318
|
+
removeLocalAppSpiceDBs();
|
|
317
319
|
const aliases = removeAllAliasContainers();
|
|
318
320
|
if (aliases.length > 0)
|
|
319
321
|
logInfo(`Removed ${aliases.length} local docs alias container(s).`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function resolveServeDependencies(from: string, pinned?: Readonly<Record<string, string>>, declared?: Readonly<Record<string, string>>): Record<string, string>;
|
|
2
|
+
export declare function defaultServeDir(manifestPath: string, name: string): string;
|
|
3
|
+
export interface GeneratedMcpServeDir {
|
|
4
|
+
serveDir: string;
|
|
5
|
+
written: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function generateMcpServeDir(manifestPath: string, opts?: {
|
|
8
|
+
out?: string;
|
|
9
|
+
refreshVersions?: boolean;
|
|
10
|
+
dependencies?: Record<string, string>;
|
|
11
|
+
}): GeneratedMcpServeDir;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { MCP_SERVE_DEPENDENCIES, MCP_SERVE_ENTRY, MCP_SERVE_SPEC, mcpServeEntrySource, mcpServePackageJson, mcpServeSpecSource, } from "@mesh-tech/api-registry/mcp-manifest";
|
|
5
|
+
import { readMcpManifestFile } from "./manifest-file.js";
|
|
6
|
+
const SERVE_HOST_PACKAGE = "@mesh-tech/app-kit";
|
|
7
|
+
function installedManifest(require_, from, name) {
|
|
8
|
+
try {
|
|
9
|
+
return require_(`${name}/package.json`);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
}
|
|
13
|
+
let dir = path.resolve(from);
|
|
14
|
+
for (;;) {
|
|
15
|
+
const file = path.join(dir, "node_modules", ...name.split("/"), "package.json");
|
|
16
|
+
if (fs.existsSync(file)) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const parent = path.dirname(dir);
|
|
25
|
+
if (parent === dir)
|
|
26
|
+
return undefined;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function resolveServeDependencies(from, pinned = {}, declared = MCP_SERVE_DEPENDENCIES) {
|
|
31
|
+
const require_ = createRequire(path.join(path.resolve(from), "noop.js"));
|
|
32
|
+
const host = installedManifest(require_, from, SERVE_HOST_PACKAGE);
|
|
33
|
+
const peers = host?.peerDependencies ?? {};
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [name, fallback] of Object.entries(declared)) {
|
|
36
|
+
const peer = name === SERVE_HOST_PACKAGE ? undefined : peers[name];
|
|
37
|
+
const usablePeer = peer && !peer.startsWith("workspace:") ? peer : undefined;
|
|
38
|
+
out[name] =
|
|
39
|
+
pinned[name] ?? usablePeer ?? installedManifest(require_, from, name)?.version ?? fallback;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
export function defaultServeDir(manifestPath, name) {
|
|
44
|
+
return path.join(path.dirname(path.resolve(manifestPath)), ".mesh-mcp", name);
|
|
45
|
+
}
|
|
46
|
+
function readPinned(packageJsonPath) {
|
|
47
|
+
if (!fs.existsSync(packageJsonPath))
|
|
48
|
+
return {};
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
51
|
+
return Object.fromEntries(Object.entries(pkg.dependencies ?? {}).filter((entry) => typeof entry[1] === "string"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function generateMcpServeDir(manifestPath, opts = {}) {
|
|
58
|
+
const loaded = readMcpManifestFile(manifestPath);
|
|
59
|
+
const serveDir = path.resolve(opts.out ?? defaultServeDir(manifestPath, loaded.manifest.name));
|
|
60
|
+
fs.mkdirSync(serveDir, { recursive: true });
|
|
61
|
+
const packageJsonPath = path.join(serveDir, "package.json");
|
|
62
|
+
const dependencies = opts.dependencies ??
|
|
63
|
+
resolveServeDependencies(serveDir, opts.refreshVersions ? {} : readPinned(packageJsonPath));
|
|
64
|
+
const files = {
|
|
65
|
+
"package.json": `${JSON.stringify(mcpServePackageJson(loaded.manifest.name, dependencies), null, 2)}\n`,
|
|
66
|
+
[MCP_SERVE_ENTRY]: mcpServeEntrySource(loaded.manifest, loaded.tools),
|
|
67
|
+
[MCP_SERVE_SPEC]: mcpServeSpecSource(loaded),
|
|
68
|
+
};
|
|
69
|
+
const written = [];
|
|
70
|
+
for (const [name, content] of Object.entries(files)) {
|
|
71
|
+
const file = path.join(serveDir, name);
|
|
72
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === content)
|
|
73
|
+
continue;
|
|
74
|
+
fs.writeFileSync(file, content);
|
|
75
|
+
written.push(name);
|
|
76
|
+
}
|
|
77
|
+
return { serveDir, written };
|
|
78
|
+
}
|