@mintmcp/hosted-cli 0.0.16 → 0.0.18
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/api.d.ts +87 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +65 -4
- package/dist/api.js.map +1 -1
- package/dist/auth.d.ts +95 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +659 -0
- package/dist/auth.js.map +1 -0
- package/dist/containerBuilder.d.ts +48 -0
- package/dist/containerBuilder.d.ts.map +1 -0
- package/dist/containerBuilder.js +364 -0
- package/dist/containerBuilder.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +506 -135
- package/dist/index.js.map +1 -1
- package/dist/updateBuilder.d.ts +28 -3
- package/dist/updateBuilder.d.ts.map +1 -1
- package/dist/updateBuilder.js +8 -3
- package/dist/updateBuilder.js.map +1 -1
- package/package.json +4 -3
- package/src/api.ts +87 -4
- package/src/auth.ts +945 -0
- package/src/containerBuilder.ts +602 -0
- package/src/index.ts +789 -174
- package/src/updateBuilder.ts +10 -3
package/src/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|
|
6
6
|
import archiver from "archiver";
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import { readFileSync } from "fs";
|
|
9
|
-
import keytar from "keytar";
|
|
10
9
|
import * as path from "path";
|
|
11
10
|
import { dirname, join } from "path";
|
|
12
11
|
import superjson from "superjson";
|
|
@@ -14,10 +13,40 @@ import { fileURLToPath } from "url";
|
|
|
14
13
|
import z, { ZodSchema } from "zod";
|
|
15
14
|
import {
|
|
16
15
|
type AppRouter,
|
|
16
|
+
type GatewayId,
|
|
17
17
|
HostedIdSchema,
|
|
18
18
|
type HostedTransport,
|
|
19
19
|
UserConfigUpdateSchema,
|
|
20
20
|
} from "./api.js";
|
|
21
|
+
import {
|
|
22
|
+
type Auth,
|
|
23
|
+
clearAllAuth,
|
|
24
|
+
clearSelectedAuth,
|
|
25
|
+
type Env,
|
|
26
|
+
EnvSchema,
|
|
27
|
+
formatAuthSummary,
|
|
28
|
+
getAuth,
|
|
29
|
+
getCurrentAuthAccount,
|
|
30
|
+
getSelectedAuth,
|
|
31
|
+
getTokenClaims,
|
|
32
|
+
listAuthRecords,
|
|
33
|
+
requestAndStoreAuth,
|
|
34
|
+
type StoredAuthAccount,
|
|
35
|
+
sameStoredAuthAccount,
|
|
36
|
+
useAuth,
|
|
37
|
+
} from "./auth.js";
|
|
38
|
+
import {
|
|
39
|
+
assertImageSupportsPlatform,
|
|
40
|
+
buildImage,
|
|
41
|
+
defaultBuildPlatform,
|
|
42
|
+
defaultLocalTestImageRef,
|
|
43
|
+
ensureDockerAvailable,
|
|
44
|
+
ensureDockerBuildxAvailable,
|
|
45
|
+
ensureImageAvailableLocally,
|
|
46
|
+
loginToRegistry,
|
|
47
|
+
pushImage,
|
|
48
|
+
smokeTestImage,
|
|
49
|
+
} from "./containerBuilder.js";
|
|
21
50
|
import {
|
|
22
51
|
buildPartialUpdate,
|
|
23
52
|
type DeployOptions,
|
|
@@ -35,10 +64,8 @@ const version = packageJson.version;
|
|
|
35
64
|
export type CliClientAppRouterInputs = inferRouterInputs<AppRouter>;
|
|
36
65
|
export type CliClientAppRouterOutputs = inferRouterOutputs<AppRouter>;
|
|
37
66
|
|
|
38
|
-
const EnvSchema = z.enum(["staging", "production"]);
|
|
39
|
-
type Env = z.infer<typeof EnvSchema>;
|
|
40
|
-
|
|
41
67
|
const TransportSchema = z.enum(["http", "stdio"]);
|
|
68
|
+
const parseTransport = argParser(TransportSchema);
|
|
42
69
|
|
|
43
70
|
const TRPC_API_URL: Record<Env, string> = {
|
|
44
71
|
staging: "http://localhost:3000/api.trpc",
|
|
@@ -50,147 +77,68 @@ const WEB_CLIENT_URL: Record<Env, string> = {
|
|
|
50
77
|
production: "https://app.mintmcp.com",
|
|
51
78
|
};
|
|
52
79
|
|
|
53
|
-
const CLIENT_IDS: Record<Env, string> = {
|
|
54
|
-
staging: "client_01K0WB8ABW72JSBZ62V98AZMPS",
|
|
55
|
-
production: "client_01K0WB8AHKS2J39SFKAPTHTR90",
|
|
56
|
-
};
|
|
57
|
-
|
|
58
80
|
// Where to look for the hosted config, relative to "--base".
|
|
59
81
|
const HOSTED_CONFIG_PATHS: Record<Env, string> = {
|
|
60
82
|
staging: ".mintmcp/hosted-staging.json",
|
|
61
83
|
production: ".mintmcp/hosted.json",
|
|
62
84
|
};
|
|
63
85
|
|
|
64
|
-
const
|
|
86
|
+
const HostedConfigBaseSchema = z.object({
|
|
65
87
|
hostedId: HostedIdSchema,
|
|
66
88
|
|
|
67
|
-
//
|
|
68
|
-
|
|
89
|
+
// Organization that owns this hosted server. Older config files may omit it.
|
|
90
|
+
organizationId: z.string().optional(),
|
|
69
91
|
});
|
|
70
|
-
type HostedConfig = z.infer<typeof HostedConfigSchema>;
|
|
71
|
-
|
|
72
|
-
const AuthSchema = z.object({
|
|
73
|
-
accessToken: z.string(),
|
|
74
|
-
email: z.string(),
|
|
75
|
-
organizationId: z.string(),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
type Auth = z.infer<typeof AuthSchema>;
|
|
79
|
-
|
|
80
|
-
async function requestAuth(env: Env): Promise<Auth> {
|
|
81
|
-
const clientId = CLIENT_IDS[env];
|
|
82
|
-
|
|
83
|
-
const deviceCodeResponse = await fetch(
|
|
84
|
-
"https://api.workos.com/user_management/authorize/device",
|
|
85
|
-
{
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: {
|
|
88
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
89
|
-
},
|
|
90
|
-
body: new URLSearchParams({
|
|
91
|
-
client_id: clientId,
|
|
92
|
-
}),
|
|
93
|
-
},
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (!deviceCodeResponse.ok) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Failed to get device code: ${await deviceCodeResponse.text()}`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
92
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} = deviceData;
|
|
93
|
+
const HostedConfigSchema = z.union([
|
|
94
|
+
HostedConfigBaseSchema.extend({
|
|
95
|
+
mode: z.literal("container"),
|
|
96
|
+
image: z.string().optional(),
|
|
97
|
+
transport: TransportSchema.optional(),
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
headers: {
|
|
123
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
124
|
-
},
|
|
125
|
-
body: new URLSearchParams({
|
|
126
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
127
|
-
device_code: deviceCode,
|
|
128
|
-
client_id: clientId,
|
|
129
|
-
}),
|
|
130
|
-
},
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
if (!tokenResponse.ok) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const tokenData = (await tokenResponse.json()) as any;
|
|
99
|
+
// Directory containing data to send to hosted server, relative to "--base".
|
|
100
|
+
mntDataDir: z.string().optional(),
|
|
101
|
+
}),
|
|
102
|
+
HostedConfigBaseSchema.extend({
|
|
103
|
+
// Directory containing data to send to hosted server, relative to "--base".
|
|
104
|
+
mntDataDir: z.string(),
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
type HostedConfig = z.infer<typeof HostedConfigSchema>;
|
|
108
|
+
type DeployResult = {
|
|
109
|
+
config: HostedConfig;
|
|
110
|
+
gatewayId?: GatewayId;
|
|
111
|
+
};
|
|
138
112
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
continue;
|
|
142
|
-
} else if (tokenData.error === "slow_down") {
|
|
143
|
-
// Increase interval as requested
|
|
144
|
-
await new Promise<void>((resolve) => {
|
|
145
|
-
setTimeout(resolve, 5000);
|
|
146
|
-
});
|
|
147
|
-
continue;
|
|
148
|
-
} else {
|
|
149
|
-
throw new Error(`Authentication failed: ${tokenData.error}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
113
|
+
type RegistryPushSession =
|
|
114
|
+
CliClientAppRouterOutputs["mcpHost"]["createRegistryPushSession"];
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
if (!tokenData.organization_id) {
|
|
158
|
-
throw new Error("Authentication response missing organization ID");
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
accessToken: tokenData.access_token,
|
|
162
|
-
email: tokenData.user.email,
|
|
163
|
-
organizationId: tokenData.organization_id,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
116
|
+
function storedTransport(config: HostedConfig): Transport | undefined {
|
|
117
|
+
return "mode" in config ? config.transport : undefined;
|
|
118
|
+
}
|
|
167
119
|
|
|
168
|
-
|
|
120
|
+
function canUseDirectImageStartup(params: {
|
|
121
|
+
transport: Transport | undefined;
|
|
122
|
+
mntDataDir: string | undefined;
|
|
123
|
+
}): boolean {
|
|
124
|
+
return params.transport === "http" && params.mntDataDir == undefined;
|
|
169
125
|
}
|
|
170
126
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return validatedAuth;
|
|
179
|
-
} catch (error) {
|
|
180
|
-
console.warn("Invalid stored credentials, re-authenticating:", error);
|
|
181
|
-
// If validation fails, delete the invalid stored credentials and re-authenticate
|
|
182
|
-
await keytar.deletePassword(keychainServiceName, env);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const auth = await requestAuth(env);
|
|
186
|
-
await keytar.setPassword(keychainServiceName, env, JSON.stringify(auth));
|
|
187
|
-
return auth;
|
|
127
|
+
function isTRPCUnauthorized(error: unknown): boolean {
|
|
128
|
+
return (
|
|
129
|
+
error instanceof Error &&
|
|
130
|
+
"data" in error &&
|
|
131
|
+
typeof (error as any).data === "object" &&
|
|
132
|
+
(error as any).data?.code === "UNAUTHORIZED"
|
|
133
|
+
);
|
|
188
134
|
}
|
|
189
135
|
|
|
190
|
-
async function makeAuthenticatedClient(
|
|
191
|
-
|
|
136
|
+
async function makeAuthenticatedClient(
|
|
137
|
+
env: Env,
|
|
138
|
+
options?: { forceRefresh?: boolean },
|
|
139
|
+
) {
|
|
140
|
+
const auth = await getAuth(env, options);
|
|
192
141
|
|
|
193
|
-
// TODO: Get the organization name because that is more human readable.
|
|
194
142
|
console.log(`Authenticated as ${auth.email} in ${auth.organizationId}.`);
|
|
195
143
|
|
|
196
144
|
return createTRPCClient<AppRouter>({
|
|
@@ -206,6 +154,29 @@ async function makeAuthenticatedClient(env: Env) {
|
|
|
206
154
|
});
|
|
207
155
|
}
|
|
208
156
|
|
|
157
|
+
type AuthenticatedClient = Awaited<ReturnType<typeof makeAuthenticatedClient>>;
|
|
158
|
+
|
|
159
|
+
async function withAuthRetry<T>(
|
|
160
|
+
env: Env,
|
|
161
|
+
fn: (client: AuthenticatedClient) => Promise<T>,
|
|
162
|
+
): Promise<T> {
|
|
163
|
+
const client = await makeAuthenticatedClient(env);
|
|
164
|
+
try {
|
|
165
|
+
return await fn(client);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (isTRPCUnauthorized(error)) {
|
|
168
|
+
console.warn(
|
|
169
|
+
"Session expired or invalid. Refreshing credentials and retrying...",
|
|
170
|
+
);
|
|
171
|
+
const newClient = await makeAuthenticatedClient(env, {
|
|
172
|
+
forceRefresh: true,
|
|
173
|
+
});
|
|
174
|
+
return await fn(newClient);
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
209
180
|
function argParser<T>(schema: ZodSchema<T>) {
|
|
210
181
|
return (value: string): T => {
|
|
211
182
|
const result = schema.safeParse(value);
|
|
@@ -281,7 +252,7 @@ async function uploadData(
|
|
|
281
252
|
return { secretDataZipGcsPath: upload.secretDataZipGcsPath };
|
|
282
253
|
}
|
|
283
254
|
|
|
284
|
-
const
|
|
255
|
+
const DEPLOY_OPTIONS_LEGACY_DEFAULTS = {
|
|
285
256
|
transport: "stdio",
|
|
286
257
|
// Skip install/build if already done (e.g., by startup probe or previous session).
|
|
287
258
|
startupCommand:
|
|
@@ -289,32 +260,53 @@ const DEPLOY_OPTIONS_NEW_SERVER_DEFAULTS = {
|
|
|
289
260
|
mntDataDir: ".",
|
|
290
261
|
} as const;
|
|
291
262
|
|
|
263
|
+
const DEPLOY_OPTIONS_CONTAINER_DEFAULTS = {
|
|
264
|
+
transport: "http",
|
|
265
|
+
} as const;
|
|
266
|
+
|
|
292
267
|
async function createNew(
|
|
293
268
|
client: Awaited<ReturnType<typeof makeAuthenticatedClient>>,
|
|
294
269
|
base: string,
|
|
270
|
+
organizationId: string,
|
|
295
271
|
options: DeployOptions,
|
|
296
|
-
): Promise<
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} = {
|
|
303
|
-
...DEPLOY_OPTIONS_NEW_SERVER_DEFAULTS,
|
|
272
|
+
): Promise<DeployResult> {
|
|
273
|
+
const defaults = options.image
|
|
274
|
+
? DEPLOY_OPTIONS_CONTAINER_DEFAULTS
|
|
275
|
+
: DEPLOY_OPTIONS_LEGACY_DEFAULTS;
|
|
276
|
+
const { name, image, transport, startupCommand, mntDataDir } = {
|
|
277
|
+
...defaults,
|
|
304
278
|
...options,
|
|
305
279
|
};
|
|
306
280
|
if (name == undefined) {
|
|
307
281
|
throw new Error("--name is a required option when creating a new server");
|
|
308
282
|
}
|
|
283
|
+
if (
|
|
284
|
+
image != undefined &&
|
|
285
|
+
startupCommand == undefined &&
|
|
286
|
+
!canUseDirectImageStartup({ transport, mntDataDir })
|
|
287
|
+
) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
"--startup-command is required unless using --transport http without --mnt-data-dir",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (image == undefined && startupCommand == undefined) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
"Expected --startup-command to be defined after applying defaults",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
309
297
|
|
|
310
|
-
const upload =
|
|
298
|
+
const upload =
|
|
299
|
+
mntDataDir != undefined || image == undefined
|
|
300
|
+
? await uploadData(client, path.join(base, mntDataDir ?? "."))
|
|
301
|
+
: undefined;
|
|
311
302
|
|
|
312
303
|
const config = UserConfigUpdateSchema.parse({
|
|
313
304
|
userGivenName: name,
|
|
314
|
-
|
|
305
|
+
...(image != undefined ? { image } : {}),
|
|
306
|
+
...(startupCommand != undefined ? { command: startupCommand } : {}),
|
|
315
307
|
transport: toHostedTransport(transport),
|
|
316
308
|
replaceEnv: [],
|
|
317
|
-
secretDataZipGcsPath: upload.secretDataZipGcsPath,
|
|
309
|
+
...(upload ? { secretDataZipGcsPath: upload.secretDataZipGcsPath } : {}),
|
|
318
310
|
} satisfies z.input<typeof UserConfigUpdateSchema>);
|
|
319
311
|
|
|
320
312
|
const createdServer = await client.mcpHost.createServer.mutate({
|
|
@@ -322,8 +314,15 @@ async function createNew(
|
|
|
322
314
|
});
|
|
323
315
|
|
|
324
316
|
return {
|
|
325
|
-
|
|
326
|
-
|
|
317
|
+
config: HostedConfigSchema.parse({
|
|
318
|
+
hostedId: createdServer.hostedId,
|
|
319
|
+
...(image != undefined
|
|
320
|
+
? { mode: "container" as const, image, transport }
|
|
321
|
+
: {}),
|
|
322
|
+
...(mntDataDir != undefined ? { mntDataDir } : {}),
|
|
323
|
+
organizationId,
|
|
324
|
+
}),
|
|
325
|
+
gatewayId: createdServer.gatewayId,
|
|
327
326
|
};
|
|
328
327
|
}
|
|
329
328
|
|
|
@@ -331,21 +330,284 @@ async function updateExisting(
|
|
|
331
330
|
client: Awaited<ReturnType<typeof makeAuthenticatedClient>>,
|
|
332
331
|
base: string,
|
|
333
332
|
config: HostedConfig,
|
|
333
|
+
organizationId: string,
|
|
334
334
|
options: DeployOptions,
|
|
335
|
-
): Promise<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
): Promise<DeployResult> {
|
|
336
|
+
if (
|
|
337
|
+
config.organizationId != undefined &&
|
|
338
|
+
config.organizationId !== organizationId
|
|
339
|
+
) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Hosted config belongs to organization ${config.organizationId}, but the selected credentials are for ${organizationId}. Switch organizations with "hosted auth use --organization-id ${config.organizationId}" before deploying.`,
|
|
342
|
+
);
|
|
339
343
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
const nextTransport = options.transport ?? storedTransport(config);
|
|
345
|
+
const clearCommand =
|
|
346
|
+
options.image != undefined &&
|
|
347
|
+
options.startupCommand == undefined &&
|
|
348
|
+
options.mntDataDir == undefined &&
|
|
349
|
+
nextTransport === "http";
|
|
350
|
+
if (
|
|
351
|
+
options.image != undefined &&
|
|
352
|
+
options.startupCommand == undefined &&
|
|
353
|
+
!clearCommand
|
|
354
|
+
) {
|
|
355
|
+
if (options.mntDataDir != undefined) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"--startup-command is required when using --mnt-data-dir with --image",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (nextTransport == undefined) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
"--startup-command is required unless the resulting transport is known to be http. Pass --transport http to switch to direct image startup.",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
throw new Error(
|
|
366
|
+
"--startup-command is required unless using --transport http without --mnt-data-dir",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const newConfig = HostedConfigSchema.parse({
|
|
370
|
+
...config,
|
|
371
|
+
...(options.mntDataDir != undefined
|
|
372
|
+
? { mntDataDir: options.mntDataDir }
|
|
373
|
+
: {}),
|
|
374
|
+
...(options.image != undefined
|
|
375
|
+
? {
|
|
376
|
+
mode: "container" as const,
|
|
377
|
+
image: options.image,
|
|
378
|
+
...(nextTransport != undefined ? { transport: nextTransport } : {}),
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
381
|
+
...("mode" in config && nextTransport != undefined
|
|
382
|
+
? { transport: nextTransport }
|
|
383
|
+
: {}),
|
|
384
|
+
organizationId,
|
|
385
|
+
});
|
|
386
|
+
const upload =
|
|
387
|
+
(options.image == undefined || options.mntDataDir != undefined) &&
|
|
388
|
+
newConfig.mntDataDir != undefined
|
|
389
|
+
? await uploadData(client, path.join(base, newConfig.mntDataDir))
|
|
390
|
+
: undefined;
|
|
391
|
+
const updatedServer = await client.mcpHost.partialUpdateServer.mutate({
|
|
345
392
|
hostedId: config.hostedId,
|
|
346
|
-
update: buildPartialUpdate(options, upload
|
|
393
|
+
update: buildPartialUpdate(options, upload?.secretDataZipGcsPath, {
|
|
394
|
+
clearCommand,
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
config: newConfig,
|
|
399
|
+
...(updatedServer.gatewayId != undefined
|
|
400
|
+
? { gatewayId: updatedServer.gatewayId }
|
|
401
|
+
: {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function connectorSettingsUrl(env: Env, gatewayId: GatewayId): string {
|
|
406
|
+
const url = new URL("/vmcps", WEB_CLIENT_URL[env]);
|
|
407
|
+
url.searchParams.set("id", gatewayId);
|
|
408
|
+
url.searchParams.set("serverTab", "connector-settings");
|
|
409
|
+
return url.toString();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function logDeployResult(
|
|
413
|
+
env: Env,
|
|
414
|
+
action: "Created" | "Updated",
|
|
415
|
+
result: DeployResult,
|
|
416
|
+
): void {
|
|
417
|
+
if (result.gatewayId == undefined) {
|
|
418
|
+
console.log(`${action}.`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
console.log(`${action} ${connectorSettingsUrl(env, result.gatewayId)}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function updateConfigAfterManagedPush(
|
|
425
|
+
config: HostedConfig,
|
|
426
|
+
organizationId: string,
|
|
427
|
+
managedImageRef: string,
|
|
428
|
+
): HostedConfig {
|
|
429
|
+
return HostedConfigSchema.parse({
|
|
430
|
+
...config,
|
|
431
|
+
mode: "container" as const,
|
|
432
|
+
image: managedImageRef,
|
|
433
|
+
organizationId,
|
|
434
|
+
...("mode" in config && config.transport != undefined
|
|
435
|
+
? { transport: config.transport }
|
|
436
|
+
: {}),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function managedRegistryImageRef(session: RegistryPushSession): string {
|
|
441
|
+
return `${session.registryHost}/${session.repository}:${session.imageTag}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function buildAndPushManagedImage(params: {
|
|
445
|
+
client: AuthenticatedClient;
|
|
446
|
+
organizationId: string;
|
|
447
|
+
config: HostedConfig;
|
|
448
|
+
dockerfile: string;
|
|
449
|
+
context: string;
|
|
450
|
+
buildArg: string[];
|
|
451
|
+
target?: string;
|
|
452
|
+
platform: string;
|
|
453
|
+
}): Promise<DeployResult> {
|
|
454
|
+
if (
|
|
455
|
+
params.config.organizationId != undefined &&
|
|
456
|
+
params.config.organizationId !== params.organizationId
|
|
457
|
+
) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`Hosted config belongs to organization ${params.config.organizationId}, but the selected credentials are for ${params.organizationId}. Switch organizations with "hosted auth use --organization-id ${params.config.organizationId}" before building and pushing.`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const session = await params.client.mcpHost.createRegistryPushSession.mutate({
|
|
464
|
+
hostedId: params.config.hostedId,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const deployImageRef = managedRegistryImageRef(session);
|
|
468
|
+
const buildResult = buildImage({
|
|
469
|
+
dockerfile: params.dockerfile,
|
|
470
|
+
context: params.context,
|
|
471
|
+
buildArgs: params.buildArg,
|
|
472
|
+
...(params.target != undefined ? { target: params.target } : {}),
|
|
473
|
+
platform: params.platform,
|
|
474
|
+
imageRef: deployImageRef,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
loginToRegistry(session.registryHost, session.username, session.password);
|
|
478
|
+
pushImage(buildResult.imageRef);
|
|
479
|
+
|
|
480
|
+
const finalized = await params.client.mcpHost.finalizeRegistryPush.mutate({
|
|
481
|
+
hostedId: params.config.hostedId,
|
|
482
|
+
finalizeToken: session.finalizeToken,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
config: updateConfigAfterManagedPush(
|
|
487
|
+
params.config,
|
|
488
|
+
params.organizationId,
|
|
489
|
+
deployImageRef,
|
|
490
|
+
),
|
|
491
|
+
...(finalized.gatewayId != undefined
|
|
492
|
+
? { gatewayId: finalized.gatewayId }
|
|
493
|
+
: {}),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function buildManagedImageCreateConfig(params: {
|
|
498
|
+
name?: string;
|
|
499
|
+
transport?: Transport;
|
|
500
|
+
startupCommand?: string;
|
|
501
|
+
}) {
|
|
502
|
+
const transport = params.transport ?? "http";
|
|
503
|
+
if (params.name == undefined) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
"--name is a required option when building and pushing a new managed image connector",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (
|
|
509
|
+
params.startupCommand == undefined &&
|
|
510
|
+
!canUseDirectImageStartup({ transport, mntDataDir: undefined })
|
|
511
|
+
) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
"--startup-command is required unless using --transport http",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return UserConfigUpdateSchema.omit({ image: true }).parse({
|
|
517
|
+
userGivenName: params.name,
|
|
518
|
+
...(params.startupCommand != undefined
|
|
519
|
+
? { command: params.startupCommand }
|
|
520
|
+
: {}),
|
|
521
|
+
transport: toHostedTransport(transport),
|
|
522
|
+
replaceEnv: [],
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function buildAndPushManagedImageForCreate(params: {
|
|
527
|
+
client: AuthenticatedClient;
|
|
528
|
+
organizationId: string;
|
|
529
|
+
name?: string;
|
|
530
|
+
transport?: Transport;
|
|
531
|
+
startupCommand?: string;
|
|
532
|
+
dockerfile: string;
|
|
533
|
+
context: string;
|
|
534
|
+
buildArg: string[];
|
|
535
|
+
target?: string;
|
|
536
|
+
platform: string;
|
|
537
|
+
}): Promise<DeployResult> {
|
|
538
|
+
const config = buildManagedImageCreateConfig({
|
|
539
|
+
...(params.name != undefined ? { name: params.name } : {}),
|
|
540
|
+
...(params.transport != undefined ? { transport: params.transport } : {}),
|
|
541
|
+
...(params.startupCommand != undefined
|
|
542
|
+
? { startupCommand: params.startupCommand }
|
|
543
|
+
: {}),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const session =
|
|
547
|
+
await params.client.mcpHost.createRegistryPushSessionForCreate.mutate({});
|
|
548
|
+
const deployImageRef = managedRegistryImageRef(session);
|
|
549
|
+
const buildResult = buildImage({
|
|
550
|
+
dockerfile: params.dockerfile,
|
|
551
|
+
context: params.context,
|
|
552
|
+
buildArgs: params.buildArg,
|
|
553
|
+
...(params.target != undefined ? { target: params.target } : {}),
|
|
554
|
+
platform: params.platform,
|
|
555
|
+
imageRef: deployImageRef,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
loginToRegistry(session.registryHost, session.username, session.password);
|
|
559
|
+
pushImage(buildResult.imageRef);
|
|
560
|
+
|
|
561
|
+
const finalized =
|
|
562
|
+
await params.client.mcpHost.finalizeRegistryPushCreate.mutate({
|
|
563
|
+
finalizeToken: session.finalizeToken,
|
|
564
|
+
config,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
config: HostedConfigSchema.parse({
|
|
569
|
+
hostedId: finalized.hostedId,
|
|
570
|
+
organizationId: params.organizationId,
|
|
571
|
+
mode: "container" as const,
|
|
572
|
+
image: deployImageRef,
|
|
573
|
+
...(params.transport != undefined
|
|
574
|
+
? { transport: params.transport }
|
|
575
|
+
: { transport: "http" as const }),
|
|
576
|
+
}),
|
|
577
|
+
gatewayId: finalized.gatewayId,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function deployWithOptions(
|
|
582
|
+
env: Env,
|
|
583
|
+
base: string,
|
|
584
|
+
options: DeployOptions,
|
|
585
|
+
): Promise<void> {
|
|
586
|
+
const auth = await getAuth(env);
|
|
587
|
+
|
|
588
|
+
await withAuthRetry(env, async (client) => {
|
|
589
|
+
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
590
|
+
|
|
591
|
+
let result: DeployResult;
|
|
592
|
+
if (fs.existsSync(configPath)) {
|
|
593
|
+
console.log("Updating existing server...");
|
|
594
|
+
const configData = fs.readFileSync(configPath, "utf8");
|
|
595
|
+
result = await updateExisting(
|
|
596
|
+
client,
|
|
597
|
+
base,
|
|
598
|
+
HostedConfigSchema.parse(JSON.parse(configData)),
|
|
599
|
+
auth.organizationId,
|
|
600
|
+
options,
|
|
601
|
+
);
|
|
602
|
+
logDeployResult(env, "Updated", result);
|
|
603
|
+
} else {
|
|
604
|
+
console.log("Creating new server...");
|
|
605
|
+
result = await createNew(client, base, auth.organizationId, options);
|
|
606
|
+
logDeployResult(env, "Created", result);
|
|
607
|
+
}
|
|
608
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
609
|
+
fs.writeFileSync(configPath, JSON.stringify(result.config, null, 2));
|
|
347
610
|
});
|
|
348
|
-
return newConfig;
|
|
349
611
|
}
|
|
350
612
|
|
|
351
613
|
const program = new Command()
|
|
@@ -361,11 +623,109 @@ const program = new Command()
|
|
|
361
623
|
.option("-b, --base <base>", "base directory for config and data.", ".");
|
|
362
624
|
|
|
363
625
|
function makeDefaultMessage(
|
|
364
|
-
optionName: keyof typeof
|
|
626
|
+
optionName: keyof typeof DEPLOY_OPTIONS_LEGACY_DEFAULTS,
|
|
365
627
|
): string {
|
|
366
|
-
return `
|
|
628
|
+
return `For new zip-based servers, defaults to '${DEPLOY_OPTIONS_LEGACY_DEFAULTS[optionName]}'.`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function collect(value: string, previous: string[]): string[] {
|
|
632
|
+
return previous.concat([value]);
|
|
367
633
|
}
|
|
368
634
|
|
|
635
|
+
function parsePositiveInt(value: string): number {
|
|
636
|
+
const parsed = Number.parseInt(value, 10);
|
|
637
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
638
|
+
throw new InvalidArgumentError("Expected a positive integer.");
|
|
639
|
+
}
|
|
640
|
+
return parsed;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function shouldBuildTestImage(options: {
|
|
644
|
+
build?: boolean;
|
|
645
|
+
image?: string;
|
|
646
|
+
dockerfile?: string;
|
|
647
|
+
context?: string;
|
|
648
|
+
buildArg?: string[];
|
|
649
|
+
target?: string;
|
|
650
|
+
}): boolean {
|
|
651
|
+
if (options.build) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
if (options.image == undefined) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
if (options.dockerfile !== undefined && options.dockerfile !== "Dockerfile") {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
if (options.context !== undefined && options.context !== ".") {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
if ((options.buildArg?.length ?? 0) > 0) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return options.target != undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const authCommand = program
|
|
670
|
+
.command("auth")
|
|
671
|
+
.description("Manage cached hosted-cli authentication credentials.");
|
|
672
|
+
|
|
673
|
+
authCommand
|
|
674
|
+
.command("login")
|
|
675
|
+
.description(
|
|
676
|
+
"Authenticate and cache credentials for the current environment.",
|
|
677
|
+
)
|
|
678
|
+
.action(async () => {
|
|
679
|
+
const { env } = program.opts();
|
|
680
|
+
const auth = await requestAndStoreAuth(env);
|
|
681
|
+
console.log(`Cached ${formatAuthSummary(auth)}.`);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
authCommand
|
|
685
|
+
.command("list")
|
|
686
|
+
.description("List cached credentials for the current environment.")
|
|
687
|
+
.action(async () => {
|
|
688
|
+
const { env } = program.opts();
|
|
689
|
+
const currentAccount = await getCurrentAuthAccount(env);
|
|
690
|
+
const records = await listAuthRecords(env);
|
|
691
|
+
if (records.length === 0) {
|
|
692
|
+
console.log(`No cached credentials found for ${env}.`);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
for (const record of records) {
|
|
696
|
+
const marker =
|
|
697
|
+
currentAccount && sameStoredAuthAccount(record.account, currentAccount)
|
|
698
|
+
? "*"
|
|
699
|
+
: " ";
|
|
700
|
+
console.log(`${marker} ${formatAuthSummary(record.auth)}`);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
authCommand
|
|
705
|
+
.command("use")
|
|
706
|
+
.description("Select which cached credentials the CLI should use.")
|
|
707
|
+
.option("--user-id <id>", "WorkOS user ID to select.")
|
|
708
|
+
.option("--email <email>", "Email address to select.")
|
|
709
|
+
.option("--organization-id <id>", "Organization ID to select.")
|
|
710
|
+
.action(
|
|
711
|
+
async (options: {
|
|
712
|
+
userId?: string;
|
|
713
|
+
email?: string;
|
|
714
|
+
organizationId?: string;
|
|
715
|
+
}) => {
|
|
716
|
+
const { env } = program.opts();
|
|
717
|
+
if (!options.userId && !options.email && !options.organizationId) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
'Specify at least one filter, for example "hosted auth use --organization-id <id>".',
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const result = await useAuth(env, options);
|
|
723
|
+
const message =
|
|
724
|
+
result.mode === "switched" ? "Cached and selected" : "Selected";
|
|
725
|
+
console.log(`${message} ${formatAuthSummary(result.auth)}.`);
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
|
|
369
729
|
program
|
|
370
730
|
.command("deploy")
|
|
371
731
|
.description("Create or update a hosted server.")
|
|
@@ -373,14 +733,18 @@ program
|
|
|
373
733
|
"-n, --name <name>",
|
|
374
734
|
"Name of the hosted server. Required for new servers.",
|
|
375
735
|
)
|
|
736
|
+
.option(
|
|
737
|
+
"--image <ref>",
|
|
738
|
+
"Container image reference (e.g., ghcr.io/org/repo:tag). Skips zip upload when --mnt-data-dir is not also supplied.",
|
|
739
|
+
)
|
|
376
740
|
.option(
|
|
377
741
|
"-t, --transport <transport>",
|
|
378
|
-
`Transport (stdio or http). ${makeDefaultMessage("transport")}`,
|
|
742
|
+
`Transport (stdio or http). Defaults to 'http' for image-based creates. ${makeDefaultMessage("transport")}`,
|
|
379
743
|
argParser(TransportSchema),
|
|
380
744
|
)
|
|
381
745
|
.option(
|
|
382
746
|
"--startup-command <command>",
|
|
383
|
-
`Command that runs on hosted container to start the server (runs in bash). ${makeDefaultMessage("startupCommand")}`,
|
|
747
|
+
`Command that runs on hosted container to start the server (runs in bash). Required for stdio and zip-backed image deploys. ${makeDefaultMessage("startupCommand")}`,
|
|
384
748
|
)
|
|
385
749
|
.option(
|
|
386
750
|
"--mnt-data-dir <directory>",
|
|
@@ -388,32 +752,283 @@ program
|
|
|
388
752
|
)
|
|
389
753
|
.action(async (options) => {
|
|
390
754
|
const { env, base } = program.opts();
|
|
391
|
-
|
|
755
|
+
await deployWithOptions(env, base, options);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
program
|
|
759
|
+
.command("test-image")
|
|
760
|
+
.description(
|
|
761
|
+
"Run a local MCP smoke test against a container image. Builds the current Dockerfile by default; with --image, tests that existing image directly.",
|
|
762
|
+
)
|
|
763
|
+
.option(
|
|
764
|
+
"--image <ref>",
|
|
765
|
+
"Existing image reference to test directly, or the output tag to build when --build or other build options are supplied.",
|
|
766
|
+
)
|
|
767
|
+
.option(
|
|
768
|
+
"--build",
|
|
769
|
+
"Force a local build before testing, even when --image is supplied.",
|
|
770
|
+
)
|
|
771
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
772
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
773
|
+
.option(
|
|
774
|
+
"--build-arg <arg>",
|
|
775
|
+
"Build arguments (KEY=VALUE), repeatable",
|
|
776
|
+
collect,
|
|
777
|
+
[],
|
|
778
|
+
)
|
|
779
|
+
.option("--target <target>", "Multi-stage build target")
|
|
780
|
+
.option(
|
|
781
|
+
"--probe-path <path>",
|
|
782
|
+
"HTTP path to probe with MCP initialize",
|
|
783
|
+
"/mcp",
|
|
784
|
+
)
|
|
785
|
+
.option(
|
|
786
|
+
"--probe-timeout-ms <ms>",
|
|
787
|
+
"How long to wait for the local MCP probe to succeed",
|
|
788
|
+
parsePositiveInt,
|
|
789
|
+
30_000,
|
|
790
|
+
)
|
|
791
|
+
.option(
|
|
792
|
+
"--env <entry>",
|
|
793
|
+
"Container runtime env vars for the local smoke test (KEY=VALUE), repeatable",
|
|
794
|
+
collect,
|
|
795
|
+
[],
|
|
796
|
+
)
|
|
797
|
+
.option(
|
|
798
|
+
"--env-file <path>",
|
|
799
|
+
"Path to a Docker env file for the local smoke test",
|
|
800
|
+
)
|
|
801
|
+
.action(async (options) => {
|
|
802
|
+
ensureDockerAvailable();
|
|
803
|
+
ensureDockerBuildxAvailable();
|
|
804
|
+
|
|
805
|
+
const shouldBuild = shouldBuildTestImage(options);
|
|
806
|
+
const imageRef = options.image ?? defaultLocalTestImageRef();
|
|
807
|
+
|
|
808
|
+
if (shouldBuild) {
|
|
809
|
+
buildImage({
|
|
810
|
+
dockerfile: options.dockerfile,
|
|
811
|
+
context: options.context,
|
|
812
|
+
buildArgs: options.buildArg,
|
|
813
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
814
|
+
imageRef,
|
|
815
|
+
});
|
|
816
|
+
} else {
|
|
817
|
+
ensureImageAvailableLocally(imageRef);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
await smokeTestImage({
|
|
821
|
+
imageRef,
|
|
822
|
+
probePath: options.probePath,
|
|
823
|
+
probeTimeoutMs: options.probeTimeoutMs,
|
|
824
|
+
env: options.env,
|
|
825
|
+
...(options.envFile != undefined ? { envFile: options.envFile } : {}),
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
program
|
|
830
|
+
.command("build-and-push")
|
|
831
|
+
.description(
|
|
832
|
+
"Build a Docker image, push it through the managed registry, and finalize the hosted connector image update or creation.",
|
|
833
|
+
)
|
|
834
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
835
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
836
|
+
.option(
|
|
837
|
+
"--build-arg <arg>",
|
|
838
|
+
"Build arguments (KEY=VALUE), repeatable",
|
|
839
|
+
collect,
|
|
840
|
+
[],
|
|
841
|
+
)
|
|
842
|
+
.option("--target <target>", "Multi-stage build target")
|
|
843
|
+
.option(
|
|
844
|
+
"--platform <platform>",
|
|
845
|
+
`Target platform to build for before push. Defaults to '${defaultBuildPlatform()}'.`,
|
|
846
|
+
defaultBuildPlatform(),
|
|
847
|
+
)
|
|
848
|
+
.option(
|
|
849
|
+
"--name <name>",
|
|
850
|
+
"Connector name when creating a new hosted connector",
|
|
851
|
+
)
|
|
852
|
+
.option(
|
|
853
|
+
"--transport <transport>",
|
|
854
|
+
"Transport to configure when creating a new managed connector",
|
|
855
|
+
parseTransport,
|
|
856
|
+
)
|
|
857
|
+
.option(
|
|
858
|
+
"--startup-command <command>",
|
|
859
|
+
"Startup command to configure when creating a new managed connector",
|
|
860
|
+
)
|
|
861
|
+
.action(async (options) => {
|
|
862
|
+
const { env, base } = program.opts();
|
|
863
|
+
|
|
864
|
+
ensureDockerAvailable();
|
|
865
|
+
ensureDockerBuildxAvailable();
|
|
866
|
+
|
|
867
|
+
if (options.platform !== defaultBuildPlatform()) {
|
|
868
|
+
console.warn(
|
|
869
|
+
`Building for ${options.platform}. Hosted deployments typically target ${defaultBuildPlatform()}; choose a different platform only when you know the runtime is compatible.`,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
392
872
|
|
|
393
873
|
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
874
|
+
const auth = await getAuth(env);
|
|
875
|
+
const hasExistingConfig = fs.existsSync(configPath);
|
|
394
876
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
877
|
+
const result = await withAuthRetry(env, async (client) => {
|
|
878
|
+
if (hasExistingConfig) {
|
|
879
|
+
const config = HostedConfigSchema.parse(
|
|
880
|
+
JSON.parse(fs.readFileSync(configPath, "utf8")),
|
|
881
|
+
);
|
|
882
|
+
return await buildAndPushManagedImage({
|
|
883
|
+
client,
|
|
884
|
+
organizationId: auth.organizationId,
|
|
885
|
+
config,
|
|
886
|
+
dockerfile: options.dockerfile,
|
|
887
|
+
context: options.context,
|
|
888
|
+
buildArg: options.buildArg,
|
|
889
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
890
|
+
platform: options.platform,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return await buildAndPushManagedImageForCreate({
|
|
400
895
|
client,
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
options
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
896
|
+
organizationId: auth.organizationId,
|
|
897
|
+
...(options.name != undefined ? { name: options.name } : {}),
|
|
898
|
+
...(options.transport != undefined
|
|
899
|
+
? { transport: options.transport }
|
|
900
|
+
: {}),
|
|
901
|
+
...(options.startupCommand != undefined
|
|
902
|
+
? { startupCommand: options.startupCommand }
|
|
903
|
+
: {}),
|
|
904
|
+
dockerfile: options.dockerfile,
|
|
905
|
+
context: options.context,
|
|
906
|
+
buildArg: options.buildArg,
|
|
907
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
908
|
+
platform: options.platform,
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
913
|
+
fs.writeFileSync(configPath, JSON.stringify(result.config, null, 2));
|
|
914
|
+
logDeployResult(env, hasExistingConfig ? "Updated" : "Created", result);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
program
|
|
918
|
+
.command("build-and-deploy")
|
|
919
|
+
.description(
|
|
920
|
+
"Build a Docker image for hosted deployment, push it, verify the target platform, and deploy it.",
|
|
921
|
+
)
|
|
922
|
+
.requiredOption(
|
|
923
|
+
"--image <ref>",
|
|
924
|
+
"Image reference to build, push, and deploy (e.g., ghcr.io/org/repo:tag).",
|
|
925
|
+
)
|
|
926
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
927
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
928
|
+
.option(
|
|
929
|
+
"--build-arg <arg>",
|
|
930
|
+
"Build arguments (KEY=VALUE), repeatable",
|
|
931
|
+
collect,
|
|
932
|
+
[],
|
|
933
|
+
)
|
|
934
|
+
.option("--target <target>", "Multi-stage build target")
|
|
935
|
+
.option(
|
|
936
|
+
"--platform <platform>",
|
|
937
|
+
`Target platform to build for before deploy. Defaults to '${defaultBuildPlatform()}'.`,
|
|
938
|
+
defaultBuildPlatform(),
|
|
939
|
+
)
|
|
940
|
+
.option(
|
|
941
|
+
"-n, --name <name>",
|
|
942
|
+
"Name of the hosted server. Required for new servers.",
|
|
943
|
+
)
|
|
944
|
+
.option(
|
|
945
|
+
"-t, --transport <transport>",
|
|
946
|
+
`Transport (stdio or http). Defaults to 'http' for image-based creates. ${makeDefaultMessage("transport")}`,
|
|
947
|
+
argParser(TransportSchema),
|
|
948
|
+
)
|
|
949
|
+
.option(
|
|
950
|
+
"--startup-command <command>",
|
|
951
|
+
`Command that runs on hosted container to start the server (runs in bash). Required for stdio and zip-backed image deploys. ${makeDefaultMessage("startupCommand")}`,
|
|
952
|
+
)
|
|
953
|
+
.option(
|
|
954
|
+
"--mnt-data-dir <directory>",
|
|
955
|
+
`Directory to copy to /mnt on the server. ${makeDefaultMessage("mntDataDir")}`,
|
|
956
|
+
)
|
|
957
|
+
.action(async (options) => {
|
|
958
|
+
const { env, base } = program.opts();
|
|
959
|
+
|
|
960
|
+
ensureDockerAvailable();
|
|
961
|
+
ensureDockerBuildxAvailable();
|
|
962
|
+
|
|
963
|
+
if (options.platform !== defaultBuildPlatform()) {
|
|
964
|
+
console.warn(
|
|
965
|
+
`Building for ${options.platform}. Hosted deployments typically target ${defaultBuildPlatform()}; choose a different platform only when you know the runtime is compatible.`,
|
|
407
966
|
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const buildResult = buildImage({
|
|
970
|
+
dockerfile: options.dockerfile,
|
|
971
|
+
context: options.context,
|
|
972
|
+
buildArgs: options.buildArg,
|
|
973
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
974
|
+
platform: options.platform,
|
|
975
|
+
imageRef: options.image,
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
pushImage(buildResult.imageRef);
|
|
979
|
+
assertImageSupportsPlatform(buildResult.imageRef, options.platform);
|
|
980
|
+
|
|
981
|
+
await deployWithOptions(env, base, {
|
|
982
|
+
image: buildResult.imageRef,
|
|
983
|
+
...(options.name != undefined ? { name: options.name } : {}),
|
|
984
|
+
...(options.transport != undefined
|
|
985
|
+
? { transport: options.transport }
|
|
986
|
+
: {}),
|
|
987
|
+
...(options.startupCommand != undefined
|
|
988
|
+
? { startupCommand: options.startupCommand }
|
|
989
|
+
: {}),
|
|
990
|
+
...(options.mntDataDir != undefined
|
|
991
|
+
? { mntDataDir: options.mntDataDir }
|
|
992
|
+
: {}),
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
program
|
|
997
|
+
.command("logout")
|
|
998
|
+
.description(
|
|
999
|
+
"Clear stored credentials and sign out of the WorkOS browser session.",
|
|
1000
|
+
)
|
|
1001
|
+
.option("--all", "Clear all cached credentials for the current environment.")
|
|
1002
|
+
.action(async (options: { all?: boolean }) => {
|
|
1003
|
+
const { env } = program.opts();
|
|
1004
|
+
|
|
1005
|
+
let logoutUrl: string | undefined;
|
|
1006
|
+
|
|
1007
|
+
const selectedAuth = await getSelectedAuth(env);
|
|
1008
|
+
if (selectedAuth) {
|
|
1009
|
+
const { sid } = getTokenClaims(selectedAuth.accessToken);
|
|
1010
|
+
if (sid) {
|
|
1011
|
+
logoutUrl = `https://api.workos.com/user_management/sessions/logout?session_id=${sid}`;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (options.all) {
|
|
1016
|
+
await clearAllAuth(env);
|
|
1017
|
+
console.log(`All cached credentials cleared for ${env}.`);
|
|
408
1018
|
} else {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
1019
|
+
const clearedAuth = await clearSelectedAuth(env);
|
|
1020
|
+
if (clearedAuth) {
|
|
1021
|
+
console.log(
|
|
1022
|
+
`Cleared cached credentials for ${formatAuthSummary(clearedAuth)}.`,
|
|
1023
|
+
);
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(`No cached credentials found for ${env}.`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (logoutUrl) {
|
|
1029
|
+
console.log("To fully sign out of the WorkOS browser session, visit:");
|
|
1030
|
+
console.log(logoutUrl);
|
|
414
1031
|
}
|
|
415
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
416
|
-
fs.writeFileSync(configPath, JSON.stringify(newOrUpdatedConfig, null, 2));
|
|
417
1032
|
});
|
|
418
1033
|
|
|
419
1034
|
program.parse();
|