@mintlify/cli 4.0.1802 → 4.0.1804

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.
@@ -33,6 +33,21 @@ describe('agent deployment API', () => {
33
33
  docsPath: '',
34
34
  });
35
35
  });
36
+ it('requests a base when the project type is workspace', async () => {
37
+ const request = vi.fn().mockResolvedValue(response({ ...ready, orgSlug: 'acme' }, 201));
38
+ vi.stubGlobal('fetch', request);
39
+ expect(
40
+ await provisionAgentSite('https://api.example.com', 'access', {
41
+ subdomain: 'requested',
42
+ projectType: 'workspace',
43
+ })
44
+ ).toEqual({ ...ready, orgSlug: 'acme', created: true });
45
+ expect(JSON.parse(request.mock.calls[0]?.[1].body)).toEqual({
46
+ subdomain: 'requested',
47
+ projectType: 'workspace',
48
+ docsPath: '',
49
+ });
50
+ });
36
51
  it('returns an existing site as an update, not a new creation', async () => {
37
52
  vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response(existing, 409)));
38
53
  expect(await provision()).toEqual({ ...existing, created: false });
@@ -66,6 +66,7 @@ beforeEach(async () => {
66
66
  });
67
67
  vi.mocked(prepareAgentDeployment).mockResolvedValue({
68
68
  status: 'ready',
69
+ projectType: 'site',
69
70
  site,
70
71
  write: { remote: site.remote, branch: 'main', token: 'write', tokenExpiresAt: site.expiresAt },
71
72
  created: false,
@@ -74,6 +74,7 @@ describe('deploy help and claim handoff', () => {
74
74
  const text = deploymentInstructions(
75
75
  {
76
76
  status: 'ready',
77
+ projectType: 'site',
77
78
  created: true,
78
79
  site,
79
80
  credentials,
@@ -95,6 +96,52 @@ describe('deploy help and claim handoff', () => {
95
96
  expect(text).not.toContain('claim-secret');
96
97
  expect(text).not.toContain('write-secret');
97
98
  });
99
+ it('gives base claim, clone, and push steps without site build or GitHub hints', () => {
100
+ const write = {
101
+ remote: site.remote,
102
+ branch: site.branch,
103
+ token: 'write-secret',
104
+ tokenExpiresAt: site.expiresAt,
105
+ };
106
+ const text = deploymentInstructions({
107
+ status: 'ready',
108
+ projectType: 'workspace',
109
+ created: true,
110
+ site,
111
+ credentials,
112
+ write,
113
+ });
114
+ expect(text).toContain('mint deploy --base --email <their-email>');
115
+ expect(text).toContain('mint claim --code <code>');
116
+ expect(text).toContain(
117
+ `git -c http.${site.remote}.extraHeader="Authorization: Bearer write-secret" clone ${site.remote} ${site.subdomain}`
118
+ );
119
+ expect(text).toContain(`mint deploy --base --push ${site.subdomain}`);
120
+ expect(text).not.toContain('Git examples');
121
+ expect(text.indexOf('--push')).toBeLessThan(text.indexOf('mint claim --code'));
122
+ expect(text).not.toContain('build');
123
+ expect(text).not.toContain('--github');
124
+ });
125
+ it('gives the base claim link when one was created', () => {
126
+ const text = deploymentInstructions(
127
+ {
128
+ status: 'ready',
129
+ projectType: 'workspace',
130
+ created: true,
131
+ site,
132
+ credentials,
133
+ write: {
134
+ remote: site.remote,
135
+ branch: site.branch,
136
+ token: 't',
137
+ tokenExpiresAt: site.expiresAt,
138
+ },
139
+ },
140
+ 'https://claim.example.com/x'
141
+ );
142
+ expect(text).toContain('Send the user this link: https://claim.example.com/x');
143
+ expect(text).not.toContain('--email <their-email>');
144
+ });
98
145
  it('documents the command in root help and explains that it does not push files', () => {
99
146
  expect(renderWelcome({ packageName: 'mint', signedIn: false })).toContain('deploy');
100
147
  expect(DEPLOY_HELP).toContain('Commit and push the documentation yourself');
@@ -102,5 +149,7 @@ describe('deploy help and claim handoff', () => {
102
149
  expect(DEPLOY_HELP).toContain('mint claim --code <code>');
103
150
  expect(renderWelcome({ packageName: 'mint', signedIn: false })).toContain('claim --email');
104
151
  expect(DEPLOY_HELP).toContain('ask the user for their email');
152
+ expect(DEPLOY_HELP).toContain('--base cannot be combined with --github');
153
+ expect(DEPLOY_HELP).toContain('mint deploy --base --push <clone-directory>');
105
154
  });
106
155
  });
@@ -5,6 +5,7 @@ import * as auth from '../src/agent-auth.js';
5
5
  import * as storage from '../src/agent-credentials.js';
6
6
  import * as deployment from '../src/agent-deployment.js';
7
7
  import * as config from '../src/config.js';
8
+ import { DASHBOARD_URL } from '../src/constants.js';
8
9
  import { prepareAgentDeployment, formatDeployment } from '../src/deploy.js';
9
10
  import { trackEvent } from '../src/telemetry/track.js';
10
11
 
@@ -100,6 +101,20 @@ describe('mint deploy', () => {
100
101
  const result = await prepareAgentDeployment({ ...options, apiUrl }, warn);
101
102
  expect(formatDeployment(result, apiUrl)).toContain(`Site: ${siteUrl}`);
102
103
  });
104
+ it('provisions a base and prints its editor link instead of a site URL', async () => {
105
+ vi.mocked(deployment.provisionAgentSite).mockResolvedValue({ ...site, orgSlug: 'acme' });
106
+ const result = await prepareAgentDeployment({ ...options, base: true }, warn);
107
+ expect(deployment.provisionAgentSite).toHaveBeenCalledWith(options.apiUrl, 'access', {
108
+ subdomain: 'requested',
109
+ orgName: undefined,
110
+ projectType: 'workspace',
111
+ });
112
+ const text = formatDeployment(result, options.apiUrl);
113
+ expect(text).toContain(`Editor: ${new URL('/acme/allocated/editor', DASHBOARD_URL)}`);
114
+ expect(text).not.toContain('Site:');
115
+ expect(text).not.toContain('build');
116
+ expect(text).not.toContain('Write token');
117
+ });
103
118
  it('fails before provisioning when credentials cannot be saved', async () => {
104
119
  vi.mocked(storage.saveAgentCredentials).mockRejectedValue(new Error('disk full'));
105
120
  await expect(prepareAgentDeployment(options, warn)).rejects.toThrow('disk full');
@@ -17,6 +17,7 @@ const readySchema = z.object({
17
17
  remote: z.url(),
18
18
  branch: z.literal('main'),
19
19
  expiresAt: z.iso.datetime(),
20
+ orgSlug: z.string().min(1).optional(),
20
21
  });
21
22
  const existingSchema = z.discriminatedUnion('status', [
22
23
  readySchema.extend({ code: z.literal('deployment_already_exists') }),
package/bin/cli.js CHANGED
@@ -23,10 +23,11 @@ import { comingSoon } from './comingSoon.js';
23
23
  import { setTelemetryEnabled } from './config.js';
24
24
  import { getConfigValue, setConfigValue, clearConfigValue } from './config.js';
25
25
  import { API_URL } from './constants.js';
26
+ import { pushBaseCheckout } from './deploy-base.js';
26
27
  import { deployFromGithubActions, isGithubActions } from './deploy-github-actions.js';
27
28
  import { formatGithubSetup, setupGithubDeployment } from './deploy-github-setup.js';
28
29
  import { DEPLOY_HELP, deploymentInstructions } from './deploy-instructions.js';
29
- import { formatDeployment, prepareAgentDeployment } from './deploy.js';
30
+ import { agentEditorUrl, formatDeployment, prepareAgentDeployment } from './deploy.js';
30
31
  import { FEEDBACK_CATEGORIES, feedback } from './feedback.js';
31
32
  import { formatHandler } from './format.js';
32
33
  import { checkPort, checkNodeVersion, autoUpgradeIfNeeded, getVersions, isAI, suppressConsoleWarnings, terminate, } from './helpers.js';
@@ -187,10 +188,22 @@ export const cli = ({ packageName = 'mint' }) => {
187
188
  default: false,
188
189
  description: 'With --github, overwrite an existing .github/workflows/mintlify.yml',
189
190
  })
191
+ .option('base', {
192
+ type: 'boolean',
193
+ description: 'Provision a base, edited in the Mintlify editor, instead of a docs site',
194
+ })
195
+ .option('push', {
196
+ type: 'string',
197
+ description: 'With --base, rebase this clone onto main and push it to the base',
198
+ })
199
+ .implies('push', 'base')
200
+ .conflicts('push', 'email')
190
201
  .conflicts('github', 'email')
202
+ .conflicts('base', 'github')
191
203
  .example('$0 deploy --subdomain my-docs', 'Provision a site without signing in')
192
204
  .example('$0 deploy --email you@example.com', 'Get repository access and a claim link for the current site')
193
205
  .example('$0 deploy --new --subdomain another-site', 'Create another anonymous site')
206
+ .example('$0 deploy --base --subdomain my-base', 'Provision a base and get its editor link')
194
207
  .example('$0 deploy --github --subdomain my-docs', 'Deploy the site from GitHub Actions on every push to the default branch')
195
208
  .example('$0 deploy --subdomain my-docs', 'In GitHub Actions with id-token: write, push the current commit to a trusted site')
196
209
  .epilog(DEPLOY_HELP), (argv) => __awaiter(void 0, void 0, void 0, function* () {
@@ -220,9 +233,16 @@ export const cli = ({ packageName = 'mint' }) => {
220
233
  subdomain: argv.subdomain,
221
234
  orgName: argv.orgName,
222
235
  newSite: argv.new,
236
+ base: argv.base,
223
237
  }, (message) => {
224
238
  process.stderr.write(`Warning: ${message}\n`);
225
239
  }, (message) => process.stderr.write(`${message}\n`));
240
+ if (argv.push && result.status === 'ready') {
241
+ yield pushBaseCheckout(argv.push, result.write);
242
+ process.stdout.write(`Pushed ${argv.push} to the base.\nEditor: ${agentEditorUrl(result.site)}\n`);
243
+ yield terminate(0);
244
+ return;
245
+ }
226
246
  process.stdout.write(`${formatDeployment(result, API_URL)}\n`);
227
247
  let claimUrl;
228
248
  if (argv.email && result.status === 'ready') {
@@ -0,0 +1,20 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { git } from './deploy-github-actions.js';
11
+ export function pushBaseCheckout(checkout, write) {
12
+ return __awaiter(this, void 0, void 0, function* () {
13
+ const env = Object.assign(Object.assign({}, process.env), { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: `http.${write.remote}.extraHeader`, GIT_CONFIG_VALUE_0: `Authorization: Bearer ${write.token}` });
14
+ yield git(['fetch', write.remote, write.branch], checkout, env);
15
+ yield git(['rebase', 'FETCH_HEAD'], checkout, env).catch(() => {
16
+ throw new Error(`Could not rebase ${checkout} onto the latest ${write.branch}. Resolve the conflicts, run git rebase --continue, then rerun mint deploy --base --push ${checkout}.`);
17
+ });
18
+ yield git(['push', write.remote, `HEAD:refs/heads/${write.branch}`], checkout, env);
19
+ });
20
+ }
@@ -41,7 +41,7 @@ function getGithubOidcToken(env, audience) {
41
41
  return parsed.data.value;
42
42
  });
43
43
  }
44
- function git(args, cwd, env) {
44
+ export function git(args, cwd, env) {
45
45
  return new Promise((resolve, reject) => {
46
46
  const child = spawn('git', args, { cwd, env, stdio: ['ignore', 'pipe', 'inherit'] });
47
47
  let out = '';
@@ -7,15 +7,44 @@ export const DEPLOY_HELP = [
7
7
  'To claim an existing site without provisioning, run mint claim --email <email>. Show the link to the user and ask them to return the displayed code. Then run mint claim --code <code>. Use the original documentation directory for both commands, or pass --directory.',
8
8
  'The output contains a short-lived write token. Do not paste it into public logs or save it in a Git remote.',
9
9
  'Pass --github from the root of a GitHub repository to deploy on every push: it trusts the repository (via the gh CLI and the origin remote) to deploy the site and writes .github/workflows/mintlify.yml. Commit and push that workflow. Pass --directory to deploy a docs subdirectory of the repository as the site root. Use --force to overwrite a different existing workflow.',
10
+ 'Pass --base to provision a base instead of a docs site. The output has the claim steps, a clone command, and the editor link; there is no site URL. Commit in the clone, then run mint deploy --base --push <clone-directory> to publish. --base cannot be combined with --github.',
10
11
  'In GitHub Actions (GITHUB_ACTIONS=true), no agent credentials are used: the workflow needs `permissions: id-token: write`, --subdomain is required, and the current commit is force-pushed to the site from a trusted repository and branch.',
11
12
  ].join('\n\n');
12
13
  export const CLAIM_INSTRUCTIONS = [
13
14
  'Ask the user for the email address they want to use to claim this site. Never invent an email address.',
14
15
  '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.',
15
16
  ];
17
+ const baseClaimStep = (claimUrl) => claimUrl
18
+ ? [
19
+ `3. Claim the base last. Send the user this link: ${claimUrl}`,
20
+ ' They sign in and get a code. When they give it to you, run: mint claim --code <code>',
21
+ ]
22
+ : [
23
+ '3. Claim the base last. Ask the user for their email (never invent one), then run:',
24
+ ' mint deploy --base --email <their-email>',
25
+ ' Send the user the claim link it prints. They sign in and get a code. When they give it to you, run: mint claim --code <code>',
26
+ ];
27
+ function baseInstructions(result, claimUrl) {
28
+ const { site, write } = result;
29
+ const checkout = site.subdomain;
30
+ return [
31
+ '\nNext steps:',
32
+ `1. Clone the base into ./${checkout}:`,
33
+ ` git -c http.${write.remote}.extraHeader="Authorization: Bearer ${write.token}" clone ${write.remote} ${checkout}`,
34
+ ` The clone command works until ${write.tokenExpiresAt}. Run mint deploy --base again for a fresh one.`,
35
+ `2. Commit your changes in ./${checkout}, then publish them:`,
36
+ ` mint deploy --base --push ${checkout}`,
37
+ ' This rebases onto the latest main and pushes. Do not run git push yourself.',
38
+ ...baseClaimStep(claimUrl),
39
+ ' After the claim, the user opens the base at the editor link above and edits it there. The CLI can no longer push to it.',
40
+ 'Run every mint command from this directory (or repeat --directory).',
41
+ ].join('\n');
42
+ }
16
43
  export function deploymentInstructions(result, claimUrl) {
17
44
  if (result.status !== 'ready')
18
45
  return '';
46
+ if (result.projectType === 'workspace')
47
+ return baseInstructions(result, claimUrl);
19
48
  return [
20
49
  '\nNext steps:',
21
50
  '1. Clone the hosted repository into a separate directory. Do not replace the user’s existing Git remote.',
package/bin/deploy.js CHANGED
@@ -12,6 +12,7 @@ import { discoverAgentAuth, exchangeAgentToken, getAgentIdentity, registerAgent,
12
12
  import { loadAgentCredentials, saveAgentCredentials, } from './agent-credentials.js';
13
13
  import { getAgentWriteToken, provisionAgentSite, } from './agent-deployment.js';
14
14
  import { getAgentSiteMetadata, setAgentSiteMetadata } from './config.js';
15
+ import { DASHBOARD_URL } from './constants.js';
15
16
  import { trackEvent } from './telemetry/track.js';
16
17
  export function prepareAgentDeployment(options_1, warn_1) {
17
18
  return __awaiter(this, arguments, void 0, function* (options, warn, progress = () => { }) {
@@ -33,6 +34,7 @@ export function prepareAgentDeployment(options_1, warn_1) {
33
34
  if (!subdomain)
34
35
  throw new Error('Choose a site name with --subdomain <name>.');
35
36
  const discovery = yield discoverAgentAuth(apiUrl);
37
+ const projectType = options.base ? 'workspace' : 'site';
36
38
  const save = (credentials) => saveAgentCredentials(apiUrl, directory, credentials);
37
39
  for (let attempt = 0; attempt < 2; attempt++) {
38
40
  const credentials = saved !== null && saved !== void 0 ? saved : (yield registerAgent(discovery));
@@ -47,11 +49,8 @@ export function prepareAgentDeployment(options_1, warn_1) {
47
49
  throw new Error('Agent identity does not match the saved registration.');
48
50
  if (identity.identityType === 'user_bound')
49
51
  return { status: 'claimed' };
50
- progress('Provisioning the site and repository...');
51
- const site = yield provisionAgentSite(apiUrl, authenticated.accessToken, {
52
- subdomain,
53
- orgName: options.orgName,
54
- });
52
+ progress(`Provisioning the ${options.base ? 'base' : 'site'} and repository...`);
53
+ const site = yield provisionAgentSite(apiUrl, authenticated.accessToken, Object.assign({ subdomain, orgName: options.orgName }, (options.base && { projectType })));
55
54
  if (site.status === 'expired') {
56
55
  warn('The previous anonymous site expired. Creating a new registration and site.');
57
56
  saved = null;
@@ -71,7 +70,14 @@ export function prepareAgentDeployment(options_1, warn_1) {
71
70
  const write = yield getAgentWriteToken(apiUrl, authenticated.accessToken);
72
71
  if (write.remote !== site.remote)
73
72
  throw new Error('Repository credentials do not match the provisioned site.');
74
- return { status: 'ready', site, write, created: site.created, credentials: authenticated };
73
+ return {
74
+ status: 'ready',
75
+ projectType,
76
+ site,
77
+ write,
78
+ created: site.created,
79
+ credentials: authenticated,
80
+ };
75
81
  }
76
82
  throw new Error('The new anonymous site is already expired. Please retry later.');
77
83
  });
@@ -82,10 +88,27 @@ export function agentSiteUrl(apiUrl, subdomain) {
82
88
  : 'mintlify.site';
83
89
  return new URL(`https://${subdomain}.${domain}`).toString();
84
90
  }
91
+ export function agentEditorUrl(site) {
92
+ return site.orgSlug
93
+ ? new URL(`/${site.orgSlug}/${site.subdomain}/editor`, DASHBOARD_URL).toString()
94
+ : new URL(DASHBOARD_URL).toString();
95
+ }
85
96
  export function formatDeployment(result, apiUrl) {
86
97
  if (result.status === 'claimed')
87
98
  return 'This site has been claimed. Manage it in the Mintlify dashboard. Use mint deploy --new --subdomain <name> to create another site.';
88
99
  const { site, write } = result;
100
+ const access = [
101
+ `Repository: ${site.remote}`,
102
+ `Branch: ${site.branch}`,
103
+ `Write token: ${write.token}`,
104
+ `Write token expires: ${write.tokenExpiresAt}`,
105
+ ];
106
+ if (result.projectType === 'workspace')
107
+ return [
108
+ result.created ? 'Base provisioned.' : 'Using the existing anonymous base.',
109
+ `Editor: ${agentEditorUrl(site)}`,
110
+ `Expires: ${site.expiresAt}. Claim it before then or it is deleted.`,
111
+ ].join('\n');
89
112
  const siteUrl = agentSiteUrl(apiUrl, site.subdomain);
90
113
  return [
91
114
  result.created
@@ -93,10 +116,7 @@ export function formatDeployment(result, apiUrl) {
93
116
  : 'Using the existing anonymous repository; push any changes to build the site.',
94
117
  `Site: ${siteUrl}`,
95
118
  `Expires: ${site.expiresAt}`,
96
- `Repository: ${site.remote}`,
97
- `Branch: ${site.branch}`,
98
- `Write token: ${write.token}`,
99
- `Write token expires: ${write.tokenExpiresAt}`,
119
+ ...access,
100
120
  'Commit and push your documentation to this repository to start the build.',
101
121
  'The site link becomes available after the push and build complete.',
102
122
  ].join('\n');