@mintlify/cli 4.0.1701 → 4.0.1703

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/bin/welcome.js CHANGED
@@ -7,6 +7,10 @@ const GROUPS = [
7
7
  commands: [
8
8
  { name: 'dev', description: 'Start a local preview of your site' },
9
9
  { name: 'new', description: 'Scaffold a new documentation site' },
10
+ {
11
+ name: 'deploy',
12
+ description: 'Provision a docs site without login; get Git push instructions',
13
+ },
10
14
  ],
11
15
  },
12
16
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1701",
3
+ "version": "4.0.1703",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -45,12 +45,12 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@inquirer/prompts": "7.9.0",
48
- "@mintlify/common": "1.0.1290",
49
- "@mintlify/link-rot": "3.0.1514",
48
+ "@mintlify/common": "1.0.1291",
49
+ "@mintlify/link-rot": "3.0.1515",
50
50
  "@mintlify/models": "0.0.377",
51
- "@mintlify/og": "0.0.88",
52
- "@mintlify/prebuild": "1.0.1455",
53
- "@mintlify/previewing": "4.0.1533",
51
+ "@mintlify/og": "0.0.89",
52
+ "@mintlify/prebuild": "1.0.1456",
53
+ "@mintlify/previewing": "4.0.1534",
54
54
  "@mintlify/validation": "0.1.939",
55
55
  "adm-zip": "0.6.0",
56
56
  "chalk": "5.2.0",
@@ -82,7 +82,7 @@
82
82
  "keytar": "7.9.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@mintlify/editor": "0.0.553",
85
+ "@mintlify/editor": "0.0.554",
86
86
  "@mintlify/ts-config": "2.0.2",
87
87
  "@tsconfig/recommended": "1.0.2",
88
88
  "@types/detect-port": "1.3.2",
@@ -101,5 +101,5 @@
101
101
  "vitest": "2.1.9",
102
102
  "vitest-mock-process": "1.0.4"
103
103
  },
104
- "gitHead": "0873aa5354dd9084254ffa50e997d84966aa1d65"
104
+ "gitHead": "56ea8d8eb5516d26960a41d5aef1b63eea741717"
105
105
  }
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+
3
+ import type { AgentCredentials } from './agent-credentials.js';
4
+ import { agentRequest, agentResponse } from './agent-http.js';
5
+
6
+ const resourceSchema = z.object({ authorization_servers: z.array(z.url()).min(1) });
7
+ const discoverySchema = z.object({
8
+ issuer: z.url(),
9
+ token_endpoint: z.url(),
10
+ agent_auth: z.object({
11
+ identity_endpoint: z.url(),
12
+ claim_endpoint: z.url(),
13
+ identity_types_supported: z.array(z.string()),
14
+ }),
15
+ });
16
+ const registrationSchema = z.object({
17
+ id: z.string().min(1),
18
+ identity: z.object({ assertion: z.string().min(1) }),
19
+ claim: z.object({ token: z.string().min(1) }),
20
+ });
21
+ const tokenSchema = z.object({
22
+ access_token: z.string().min(1),
23
+ expires_in: z.number().positive(),
24
+ token_type: z.string().regex(/^bearer$/i),
25
+ });
26
+ const identitySchema = z.discriminatedUnion('identityType', [
27
+ z.object({ registrationId: z.string(), identityType: z.literal('anonymous') }),
28
+ z.object({
29
+ registrationId: z.string(),
30
+ identityType: z.literal('user_bound'),
31
+ userId: z.string(),
32
+ orgId: z.string(),
33
+ }),
34
+ ]);
35
+
36
+ export type AgentDiscovery = z.infer<typeof discoverySchema>;
37
+
38
+ export async function discoverAgentAuth(apiUrl: string): Promise<AgentDiscovery> {
39
+ const resource = await agentResponse(
40
+ await agentRequest(`${apiUrl}/api/agent/.well-known/oauth-protected-resource`),
41
+ resourceSchema
42
+ );
43
+ const issuer = resource.authorization_servers[0];
44
+ if (!issuer || new URL(issuer).protocol !== 'https:')
45
+ throw new Error('Agent authentication requires an HTTPS issuer.');
46
+ const discovery = await agentResponse(
47
+ await agentRequest(`${issuer.replace(/\/$/, '')}/.well-known/oauth-authorization-server`),
48
+ discoverySchema
49
+ );
50
+ const origin = new URL(issuer).origin;
51
+ if (
52
+ [
53
+ discovery.issuer,
54
+ discovery.token_endpoint,
55
+ discovery.agent_auth.identity_endpoint,
56
+ discovery.agent_auth.claim_endpoint,
57
+ ].some((url) => new URL(url).origin !== origin)
58
+ ) {
59
+ throw new Error('Agent authentication endpoints do not match the configured issuer.');
60
+ }
61
+ return discovery;
62
+ }
63
+
64
+ export async function registerAgent(discovery: AgentDiscovery): Promise<AgentCredentials> {
65
+ if (!discovery.agent_auth.identity_types_supported.includes('anonymous')) {
66
+ throw new Error('Anonymous registration is not enabled for this environment.');
67
+ }
68
+ const result = await agentResponse(
69
+ await agentRequest(discovery.agent_auth.identity_endpoint, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ type: 'anonymous' }),
73
+ }),
74
+ registrationSchema
75
+ );
76
+ return {
77
+ registrationId: result.id,
78
+ issuer: discovery.issuer,
79
+ assertion: result.identity.assertion,
80
+ claimToken: result.claim.token,
81
+ };
82
+ }
83
+
84
+ export async function exchangeAgentToken(
85
+ discovery: AgentDiscovery,
86
+ credentials: AgentCredentials
87
+ ): Promise<AgentCredentials & { accessToken: string }> {
88
+ if (new URL(credentials.issuer).origin !== new URL(discovery.issuer).origin) {
89
+ throw new Error('Saved agent credentials belong to another issuer.');
90
+ }
91
+ if (credentials.accessToken && (credentials.accessTokenExpiresAt ?? 0) > Date.now() + 30_000) {
92
+ return { ...credentials, accessToken: credentials.accessToken };
93
+ }
94
+ const token = await agentResponse(
95
+ await agentRequest(discovery.token_endpoint, {
96
+ method: 'POST',
97
+ body: new URLSearchParams({
98
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
99
+ assertion: credentials.assertion,
100
+ }),
101
+ }),
102
+ tokenSchema
103
+ );
104
+ return {
105
+ ...credentials,
106
+ accessToken: token.access_token,
107
+ accessTokenExpiresAt: Date.now() + token.expires_in * 1000,
108
+ };
109
+ }
110
+
111
+ export async function getAgentIdentity(apiUrl: string, accessToken: string) {
112
+ return agentResponse(
113
+ await agentRequest(`${apiUrl}/api/agent/identity`, {
114
+ headers: { Authorization: `Bearer ${accessToken}` },
115
+ }),
116
+ identitySchema
117
+ );
118
+ }
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+
3
+ import { discoverAgentAuth } from './agent-auth.js';
4
+ import type { AgentCredentials } from './agent-credentials.js';
5
+ import { agentRequest, agentResponse } from './agent-http.js';
6
+ import { DASHBOARD_URL } from './constants.js';
7
+
8
+ const claimAttemptSchema = z.object({ attempt: z.object({ verification_uri: z.url() }) });
9
+
10
+ export async function createAgentClaimLink(
11
+ apiUrl: string,
12
+ credentials: AgentCredentials,
13
+ email: string
14
+ ): Promise<string> {
15
+ const loginHint = z.email().parse(email);
16
+ const discovery = await discoverAgentAuth(apiUrl);
17
+ if (new URL(discovery.issuer).origin !== new URL(credentials.issuer).origin) {
18
+ throw new Error('Saved agent credentials belong to another issuer.');
19
+ }
20
+ const claim = await agentResponse(
21
+ await agentRequest(discovery.agent_auth.claim_endpoint, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({
25
+ type: 'service_auth',
26
+ claim_token: credentials.claimToken,
27
+ login_hint: loginHint,
28
+ }),
29
+ }),
30
+ claimAttemptSchema
31
+ );
32
+ const token = z
33
+ .string()
34
+ .min(1)
35
+ .parse(new URL(claim.attempt.verification_uri).searchParams.get('token'));
36
+ const claimUrl = new URL('/agent-claim', DASHBOARD_URL);
37
+ claimUrl.searchParams.set('token', token);
38
+ return claimUrl.toString();
39
+ }
@@ -0,0 +1,60 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+
5
+ import { getKeytar } from './keyring.js';
6
+
7
+ const service = 'mintlify-agent';
8
+
9
+ export const agentCredentialsSchema = z.object({
10
+ registrationId: z.string().min(1),
11
+ issuer: z.url(),
12
+ assertion: z.string().min(1),
13
+ claimToken: z.string().min(1),
14
+ accessToken: z.string().min(1).optional(),
15
+ accessTokenExpiresAt: z.number().optional(),
16
+ });
17
+
18
+ export type AgentCredentials = z.infer<typeof agentCredentialsSchema>;
19
+
20
+ function projectAccount(apiUrl: string, directory: string): string {
21
+ return `project:${createHash('sha256')
22
+ .update(JSON.stringify([new URL(apiUrl).origin, path.resolve(directory)]))
23
+ .digest('hex')}`;
24
+ }
25
+
26
+ function registrationAccount(apiUrl: string, registrationId: string): string {
27
+ return `${new URL(apiUrl).origin}:${registrationId}`;
28
+ }
29
+
30
+ export async function loadAgentCredentials(
31
+ apiUrl: string,
32
+ directory: string
33
+ ): Promise<AgentCredentials | null> {
34
+ const keytar = await getKeytar();
35
+ const registrationId = await keytar.getPassword(service, projectAccount(apiUrl, directory));
36
+ if (!registrationId) return null;
37
+ const value = await keytar.getPassword(service, registrationAccount(apiUrl, registrationId));
38
+ if (!value)
39
+ throw new Error('Saved agent credentials are missing. Use --new to create another site.');
40
+ const credentials = agentCredentialsSchema.parse(JSON.parse(value));
41
+ if (credentials.registrationId !== registrationId) {
42
+ throw new Error('Saved agent credentials do not match this project.');
43
+ }
44
+ return credentials;
45
+ }
46
+
47
+ export async function saveAgentCredentials(
48
+ apiUrl: string,
49
+ directory: string,
50
+ credentials: AgentCredentials
51
+ ): Promise<void> {
52
+ const value = agentCredentialsSchema.parse(credentials);
53
+ const keytar = await getKeytar();
54
+ await keytar.setPassword(
55
+ service,
56
+ registrationAccount(apiUrl, value.registrationId),
57
+ JSON.stringify(value)
58
+ );
59
+ await keytar.setPassword(service, projectAccount(apiUrl, directory), value.registrationId);
60
+ }
@@ -0,0 +1,75 @@
1
+ import { setTimeout } from 'node:timers/promises';
2
+ import { z } from 'zod';
3
+
4
+ import { agentRequest, agentResponse } from './agent-http.js';
5
+
6
+ const readySchema = z.object({
7
+ status: z.literal('ready'),
8
+ deploymentId: z.string().min(1),
9
+ subdomain: z.string().min(1),
10
+ remote: z.url(),
11
+ branch: z.literal('main'),
12
+ expiresAt: z.iso.datetime(),
13
+ });
14
+ const existingSchema = z.discriminatedUnion('status', [
15
+ readySchema.extend({ code: z.literal('deployment_already_exists') }),
16
+ z.object({
17
+ status: z.literal('provisioning'),
18
+ code: z.literal('deployment_already_exists'),
19
+ deploymentId: z.string().optional(),
20
+ subdomain: z.string().optional(),
21
+ }),
22
+ z.object({
23
+ status: z.literal('expired'),
24
+ code: z.literal('deployment_already_exists'),
25
+ deploymentId: z.string(),
26
+ subdomain: z.string(),
27
+ expiresAt: z.iso.datetime().optional(),
28
+ }),
29
+ ]);
30
+ const writeTokenSchema = z.object({
31
+ remote: z.url(),
32
+ branch: z.literal('main'),
33
+ token: z.string().min(1),
34
+ tokenExpiresAt: z.iso.datetime(),
35
+ });
36
+
37
+ export type AgentSite = z.infer<typeof readySchema>;
38
+ export type AgentWriteToken = z.infer<typeof writeTokenSchema>;
39
+ export type AgentSiteSettings = { subdomain: string; orgName?: string };
40
+
41
+ export async function provisionAgentSite(
42
+ apiUrl: string,
43
+ accessToken: string,
44
+ settings: AgentSiteSettings
45
+ ) {
46
+ for (let attempt = 0; attempt < 4; attempt++) {
47
+ const response = await agentRequest(`${apiUrl}/api/agent/deployments/provision`, {
48
+ method: 'POST',
49
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ ...settings, docsPath: '' }),
51
+ });
52
+ if (response.status !== 409) {
53
+ return { ...(await agentResponse(response, readySchema)), created: true };
54
+ }
55
+ const body: unknown = await response.json().catch(() => null);
56
+ const parsed = existingSchema.safeParse(body);
57
+ if (!parsed.success) throw new Error('The deployment service returned an unexpected response.');
58
+ if (parsed.data.status !== 'provisioning') return { ...parsed.data, created: false };
59
+ if (attempt < 3) await setTimeout(1000 * (attempt + 1));
60
+ }
61
+ throw new Error('Your site is still provisioning. Run the same command again.');
62
+ }
63
+
64
+ export async function getAgentWriteToken(
65
+ apiUrl: string,
66
+ accessToken: string
67
+ ): Promise<AgentWriteToken> {
68
+ return agentResponse(
69
+ await agentRequest(`${apiUrl}/api/agent/deployments/token`, {
70
+ method: 'POST',
71
+ headers: { Authorization: `Bearer ${accessToken}` },
72
+ }),
73
+ writeTokenSchema
74
+ );
75
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+
3
+ const errorSchema = z.object({ code: z.string().optional(), error: z.string().optional() });
4
+
5
+ export class AgentApiError extends Error {
6
+ constructor(
7
+ public readonly status: number,
8
+ public readonly code?: string
9
+ ) {
10
+ super(`Agent request failed (HTTP ${status}).${code ? ` Code: ${code}.` : ''}`);
11
+ }
12
+ }
13
+
14
+ export async function agentRequest(url: string, init?: RequestInit): Promise<Response> {
15
+ try {
16
+ const timeout = AbortSignal.timeout(30_000);
17
+ const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
18
+ return await fetch(url, { ...init, redirect: 'error', signal });
19
+ } catch {
20
+ throw new Error('Could not reach the agent service. Retry the same command.');
21
+ }
22
+ }
23
+
24
+ export async function agentResponse<T>(response: Response, schema: z.ZodType<T>): Promise<T> {
25
+ const body: unknown = await response.json().catch(() => null);
26
+ if (!response.ok) {
27
+ const error = errorSchema.safeParse(body);
28
+ const code = error.success ? (error.data.code ?? error.data.error) : undefined;
29
+ throw new AgentApiError(response.status, code?.match(/^[a-z_]{1,80}$/)?.[0]);
30
+ }
31
+ const parsed = schema.safeParse(body);
32
+ if (!parsed.success) throw new Error('The agent service returned an unexpected response.');
33
+ return parsed.data;
34
+ }
package/src/cli.tsx CHANGED
@@ -21,11 +21,14 @@ import { hideBin } from 'yargs/helpers';
21
21
 
22
22
  import { accessibilityCheck } from './accessibilityCheck.js';
23
23
  import { addDomain } from './addDomain.js';
24
+ import { createAgentClaimLink } from './agent-claim.js';
24
25
  import { analyticsBuilder } from './analytics/index.js';
25
26
  import { comingSoon } from './comingSoon.js';
26
27
  import { setTelemetryEnabled } from './config.js';
27
28
  import { getConfigValue, setConfigValue, clearConfigValue } from './config.js';
28
29
  import { API_URL } from './constants.js';
30
+ import { DEPLOY_HELP, deploymentInstructions } from './deploy-instructions.js';
31
+ import { formatDeployment, prepareAgentDeployment } from './deploy.js';
29
32
  import { formatHandler } from './format.js';
30
33
  import {
31
34
  CMD_EXEC_PATH,
@@ -117,6 +120,74 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
117
120
  .middleware(checkNodeVersion)
118
121
  .middleware(suppressConsoleWarnings)
119
122
  .middleware(telemetryMiddleware)
123
+ .command(
124
+ 'deploy',
125
+ 'Provision an anonymous docs site and get Git push instructions',
126
+ (yargs) =>
127
+ yargs
128
+ .option('subdomain', {
129
+ type: 'string',
130
+ description: 'Requested site name; required for the first deployment or --new',
131
+ })
132
+ .option('org-name', { type: 'string', description: 'Organization display name' })
133
+ .option('directory', {
134
+ type: 'string',
135
+ default: '.',
136
+ description: 'Documentation directory associated with this site',
137
+ })
138
+ .option('new', {
139
+ type: 'boolean',
140
+ default: false,
141
+ description: 'Create a new registration and site',
142
+ })
143
+ .option('email', {
144
+ type: 'string',
145
+ description: 'User email for an optional claim link',
146
+ })
147
+ .example('$0 deploy --subdomain my-docs', 'Provision a site without signing in')
148
+ .example(
149
+ '$0 deploy --email you@example.com',
150
+ 'Get repository access and a claim link for the current site'
151
+ )
152
+ .example('$0 deploy --new --subdomain another-site', 'Create another anonymous site')
153
+ .epilog(DEPLOY_HELP),
154
+ async (argv) => {
155
+ try {
156
+ const result = await prepareAgentDeployment(
157
+ {
158
+ apiUrl: API_URL,
159
+ directory: argv.directory,
160
+ subdomain: argv.subdomain,
161
+ orgName: argv.orgName,
162
+ newSite: argv.new,
163
+ },
164
+ (message) => {
165
+ process.stderr.write(`Warning: ${message}\n`);
166
+ },
167
+ (message) => process.stderr.write(`${message}\n`)
168
+ );
169
+ process.stdout.write(`${formatDeployment(result, API_URL)}\n`);
170
+ let claimUrl: string | undefined;
171
+ if (argv.email && result.status === 'ready') {
172
+ try {
173
+ process.stderr.write('Creating the claim link...\n');
174
+ claimUrl = await createAgentClaimLink(API_URL, result.credentials, argv.email);
175
+ } catch {
176
+ process.stderr.write(
177
+ 'Warning: Could not create the claim link. Your site is provisioned; retry with --email later.\n'
178
+ );
179
+ }
180
+ }
181
+ process.stdout.write(`${deploymentInstructions(result, claimUrl)}\n`);
182
+ await terminate(0);
183
+ } catch (error) {
184
+ process.stderr.write(
185
+ `${error instanceof Error ? error.message : 'Deployment failed.'}\n`
186
+ );
187
+ await terminate(1);
188
+ }
189
+ }
190
+ )
120
191
  .command(
121
192
  'dev',
122
193
  'Initialize a local preview environment',
package/src/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import { ensureDir } from 'fs-extra';
3
+ import { z } from 'zod';
3
4
 
4
5
  import { CLI_CONFIG_FILE, CONFIG_DIR } from './constants.js';
5
6
 
@@ -9,6 +10,21 @@ interface MintlifyConfig {
9
10
  dateFrom?: string;
10
11
  dateTo?: string;
11
12
  publishPromptShown?: string[];
13
+ agentSites?: Record<string, { registrationId: string; subdomain: string }>;
14
+ }
15
+
16
+ const agentSiteMetadataSchema = z.object({ registrationId: z.string(), subdomain: z.string() });
17
+
18
+ export function getAgentSiteMetadata(key: string) {
19
+ const parsed = agentSiteMetadataSchema.safeParse(readConfig().agentSites?.[key]);
20
+ return parsed.success ? parsed.data : undefined;
21
+ }
22
+
23
+ export async function setAgentSiteMetadata(
24
+ key: string,
25
+ site: z.infer<typeof agentSiteMetadataSchema>
26
+ ) {
27
+ await writeConfig({ agentSites: { ...readConfig().agentSites, [key]: site } });
12
28
  }
13
29
 
14
30
  function readConfig(): MintlifyConfig {
@@ -0,0 +1,41 @@
1
+ import { API_URL } from './constants.js';
2
+ import type { DeployResult } from './deploy.js';
3
+
4
+ export const DEPLOY_HELP = [
5
+ 'No login is required. This command provisions a temporary site and returns repository access. Commit and push the documentation yourself; the CLI does not upload files.',
6
+ 'Run from your documentation directory, or pass --directory. Put docs.json, pages, and assets at the hosted repository root.',
7
+ 'Run again from the same documentation directory for the current anonymous site and a fresh write token. Use --new --subdomain <name> to create another site.',
8
+ 'Claimed sites are managed in the dashboard. Confirmed expired sites start a new anonymous registration.',
9
+ 'Pass --email to obtain a claim link for the user. If it is absent, ask the user for their email and rerun with --email from the same directory. Never invent an email address.',
10
+ 'The output contains a short-lived write token. Do not paste it into public logs or save it in a Git remote.',
11
+ ].join('\n\n');
12
+
13
+ export function deploymentInstructions(result: DeployResult, claimUrl?: string): string {
14
+ if (result.status !== 'ready') return '';
15
+ return [
16
+ '\nNext steps:',
17
+ '1. Clone the hosted repository into a separate directory. Do not replace the user’s existing Git remote.',
18
+ '2. Copy the selected documentation into its root, including docs.json, .md/.mdx pages, images, and other required assets. Exclude credentials, .env files, node_modules, and .git.',
19
+ '3. Commit the documentation. For subsequent updates, fetch the hosted main branch and commit on top of it. Do not force-push.',
20
+ '4. Push to main. Supply the write token as an HTTP Authorization: Bearer header for this Git operation only; never embed it in the remote URL or persist it in Git config.',
21
+ '5. If the write token expires, run mint deploy again from the same documentation directory to obtain a fresh token.',
22
+ 'A successful push starts the normal build. Provisioning and Git success do not confirm build success.',
23
+ '',
24
+ 'Git examples (POSIX shell): set REPOSITORY_URL and WRITE_TOKEN from the output above. Set CHECKOUT_DIR to a new directory outside the source documentation directory. Keep the token private and do not enable shell tracing.',
25
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.${REPOSITORY_URL}.extraHeader" GIT_CONFIG_VALUE_0="Authorization: Bearer ${WRITE_TOKEN}" git clone "$REPOSITORY_URL" "$CHECKOUT_DIR"',
26
+ 'After copying and committing the documentation:',
27
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.${REPOSITORY_URL}.extraHeader" GIT_CONFIG_VALUE_0="Authorization: Bearer ${WRITE_TOKEN}" git -C "$CHECKOUT_DIR" push origin HEAD:main',
28
+ 'These settings apply only to that Git process. Use the same authentication settings for later fetches.',
29
+ '',
30
+ ...(claimUrl
31
+ ? [
32
+ `Claim link for the user: ${claimUrl}`,
33
+ 'Show this link to the user. Ask them to open it, sign in, and return the displayed code. Do not sign in on their behalf.',
34
+ `Follow the claim-completion instructions at ${API_URL}/api/agent/auth.md. This document includes the WorkOS steps and the authenticated Mintlify signup request.`,
35
+ ]
36
+ : [
37
+ 'Ask the user for the email address they want to use to claim this site. Never invent an email address.',
38
+ 'After they provide it, run mint deploy --email <their-email> from the same documentation directory (or repeat --directory). This reuses the site and returns its claim link.',
39
+ ]),
40
+ ].join('\n');
41
+ }
package/src/deploy.ts ADDED
@@ -0,0 +1,130 @@
1
+ import path from 'node:path';
2
+
3
+ import {
4
+ discoverAgentAuth,
5
+ exchangeAgentToken,
6
+ getAgentIdentity,
7
+ registerAgent,
8
+ } from './agent-auth.js';
9
+ import {
10
+ loadAgentCredentials,
11
+ saveAgentCredentials,
12
+ type AgentCredentials,
13
+ } from './agent-credentials.js';
14
+ import {
15
+ getAgentWriteToken,
16
+ provisionAgentSite,
17
+ type AgentSite,
18
+ type AgentWriteToken,
19
+ } from './agent-deployment.js';
20
+ import { getAgentSiteMetadata, setAgentSiteMetadata } from './config.js';
21
+
22
+ export type DeployOptions = {
23
+ apiUrl: string;
24
+ directory: string;
25
+ subdomain?: string;
26
+ orgName?: string;
27
+ newSite?: boolean;
28
+ };
29
+
30
+ export type DeployResult =
31
+ | { status: 'claimed' }
32
+ | {
33
+ status: 'ready';
34
+ site: AgentSite;
35
+ write: AgentWriteToken;
36
+ created: boolean;
37
+ credentials: AgentCredentials;
38
+ };
39
+
40
+ export async function prepareAgentDeployment(
41
+ options: DeployOptions,
42
+ warn: (message: string) => void,
43
+ progress: (message: string) => void = () => {}
44
+ ): Promise<DeployResult> {
45
+ const { apiUrl } = options;
46
+ const directory = path.resolve(options.directory);
47
+ const metadataKey = JSON.stringify([new URL(apiUrl).origin, directory]);
48
+ const previous = getAgentSiteMetadata(metadataKey);
49
+ progress('Authenticating agent...');
50
+ let saved = options.newSite
51
+ ? null
52
+ : await loadAgentCredentials(apiUrl, directory).catch(() => {
53
+ if (previous)
54
+ throw new Error(
55
+ 'Could not read this site’s saved credentials. Unlock your keyring or use --new for another site.'
56
+ );
57
+ warn('Could not read the keyring. Continuing with a new registration for this run.');
58
+ return null;
59
+ });
60
+ const subdomain = options.subdomain ?? (options.newSite ? undefined : previous?.subdomain);
61
+ if (!subdomain) throw new Error('Choose a site name with --subdomain <name>.');
62
+ const discovery = await discoverAgentAuth(apiUrl);
63
+
64
+ const save = async (credentials: AgentCredentials) => {
65
+ try {
66
+ await saveAgentCredentials(apiUrl, directory, credentials);
67
+ } catch {
68
+ warn(
69
+ 'Could not save agent credentials to the keyring. Continuing for this run; later commands may not recover access.'
70
+ );
71
+ }
72
+ };
73
+
74
+ for (let attempt = 0; attempt < 2; attempt++) {
75
+ const credentials = saved ?? (await registerAgent(discovery));
76
+ await save(credentials);
77
+ const authenticated = await exchangeAgentToken(discovery, credentials);
78
+ await save(authenticated);
79
+ const identity = await getAgentIdentity(apiUrl, authenticated.accessToken);
80
+ if (identity.registrationId !== credentials.registrationId)
81
+ throw new Error('Agent identity does not match the saved registration.');
82
+ if (identity.identityType === 'user_bound') return { status: 'claimed' };
83
+ progress('Provisioning the site and repository...');
84
+ const site = await provisionAgentSite(apiUrl, authenticated.accessToken, {
85
+ subdomain,
86
+ orgName: options.orgName,
87
+ });
88
+ if (site.status === 'expired') {
89
+ warn('The previous anonymous site expired. Creating a new registration and site.');
90
+ saved = null;
91
+ continue;
92
+ }
93
+ await setAgentSiteMetadata(metadataKey, {
94
+ registrationId: credentials.registrationId,
95
+ subdomain: site.subdomain,
96
+ }).catch(() =>
97
+ warn('Could not save the site name locally. Provide --subdomain on your next invocation.')
98
+ );
99
+ progress('Getting repository write access...');
100
+ const write = await getAgentWriteToken(apiUrl, authenticated.accessToken);
101
+ if (write.remote !== site.remote)
102
+ throw new Error('Repository credentials do not match the provisioned site.');
103
+ return { status: 'ready', site, write, created: site.created, credentials: authenticated };
104
+ }
105
+ throw new Error('The new anonymous site is already expired. Please retry later.');
106
+ }
107
+
108
+ export function formatDeployment(result: DeployResult, apiUrl: string): string {
109
+ if (result.status === 'claimed')
110
+ return 'This site has been claimed. Manage it in the Mintlify dashboard. Use mint deploy --new --subdomain <name> to create another site.';
111
+ const { site, write } = result;
112
+ const domain =
113
+ new URL(apiUrl).hostname === 'leaves-dev.mintlify.com'
114
+ ? 'mint-dev.mintlify.me'
115
+ : 'mintlify.site';
116
+ const siteUrl = new URL(`https://${site.subdomain}.${domain}`).toString();
117
+ return [
118
+ result.created
119
+ ? 'Repository provisioned; push required to build the site.'
120
+ : 'Using the existing anonymous repository; push any changes to build the site.',
121
+ `Site: ${siteUrl}`,
122
+ `Expires: ${site.expiresAt}`,
123
+ `Repository: ${site.remote}`,
124
+ `Branch: ${site.branch}`,
125
+ `Write token: ${write.token}`,
126
+ `Write token expires: ${write.tokenExpiresAt}`,
127
+ 'Commit and push your documentation to this repository to start the build.',
128
+ 'The site link becomes available after the push and build complete.',
129
+ ].join('\n');
130
+ }