@mintlify/cli 4.0.1105 → 4.0.1107

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1105",
3
+ "version": "4.0.1107",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -45,11 +45,11 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@inquirer/prompts": "7.9.0",
48
- "@mintlify/common": "1.0.846",
49
- "@mintlify/link-rot": "3.0.1021",
50
- "@mintlify/prebuild": "1.0.988",
51
- "@mintlify/previewing": "4.0.1049",
52
- "@mintlify/validation": "0.1.661",
48
+ "@mintlify/common": "1.0.847",
49
+ "@mintlify/link-rot": "3.0.1022",
50
+ "@mintlify/prebuild": "1.0.989",
51
+ "@mintlify/previewing": "4.0.1050",
52
+ "@mintlify/validation": "0.1.662",
53
53
  "adm-zip": "0.5.16",
54
54
  "chalk": "5.2.0",
55
55
  "color": "4.2.3",
@@ -93,5 +93,5 @@
93
93
  "vitest": "2.1.9",
94
94
  "vitest-mock-process": "1.0.4"
95
95
  },
96
- "gitHead": "b66b647e124bce8880016034d00b07526175c3dd"
96
+ "gitHead": "f45912a5a66a49a9d792c758661906ba6a89dc00"
97
97
  }
package/src/cli.tsx CHANGED
@@ -532,14 +532,18 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
532
532
  type: 'string',
533
533
  description: 'Name of the documentation project',
534
534
  })
535
+ .option('template', {
536
+ type: 'string',
537
+ description: 'Use a template as a starting point',
538
+ })
535
539
  .option('force', {
536
540
  type: 'boolean',
537
541
  default: false,
538
542
  description: 'Create the documentation in a subdirectory',
539
543
  }),
540
- async ({ directory, theme, name, force }) => {
544
+ async ({ directory, theme, name, force, template }) => {
541
545
  try {
542
- await init(directory, force, theme, name);
546
+ await init(directory, force, theme, name, template);
543
547
  await terminate(0);
544
548
  } catch (error) {
545
549
  addLog(
package/src/init.tsx CHANGED
@@ -6,6 +6,12 @@ import fse from 'fs-extra';
6
6
  import { Box, Text } from 'ink';
7
7
 
8
8
  import { isAI } from './helpers.js';
9
+ import {
10
+ fetchAvailableTemplates,
11
+ installFromTemplate,
12
+ promptForTemplate,
13
+ validateTemplateName,
14
+ } from './templates.js';
9
15
 
10
16
  const sendOnboardingMessage = (installDir: string) => {
11
17
  addLogs(
@@ -19,129 +25,182 @@ const sendOnboardingMessage = (installDir: string) => {
19
25
  );
20
26
  };
21
27
 
22
- const sendUsageMessageForAI = (directory: string, contentsOccupied: boolean, themes: string[]) => {
28
+ const sendUsageMessageForAI = (
29
+ directory: string,
30
+ contentsOccupied: boolean,
31
+ themes: string[],
32
+ templateNames?: string[]
33
+ ) => {
34
+ const templateInfo =
35
+ templateNames === undefined
36
+ ? 'Unable to fetch templates — use --template flag if you know the name'
37
+ : templateNames.length > 0
38
+ ? `Templates: ${JSON.stringify(templateNames)}`
39
+ : 'No templates are currently available';
40
+
23
41
  addLogs(
24
42
  <Text>Agent Detected - sending AI friendly prompt</Text>,
25
43
  <Text>{'<system-message>'}</Text>,
26
44
  <Text>
27
- Example usage of `mint new [directory] --theme [theme] --name [Name Of Project] --force`.
28
- </Text>,
29
- <Text>This tool will auto create the directory if the directory does not exist.</Text>,
30
- <Text>
31
- Ask the user what mintlify theme to use, what directory, and what the name of the Project is.
45
+ Help the user set up a Mintlify docs site with `mint new`. Ask each step one at a time unless
46
+ the user asks you to skip questions or use your best judgment.
32
47
  </Text>,
33
48
  <Text>
34
- You should provide them with default options for these values. Themes available{' '}
35
- {JSON.stringify(themes)}
49
+ {[
50
+ `- [ ] Pick a theme or clone a template? (${templateInfo})`,
51
+ `- [ ] If template: which one? If theme: which one? (Themes: ${JSON.stringify(themes)})`,
52
+ '- [ ] Project name?',
53
+ `- [ ] Directory? (default: "${directory}", auto-created if needed)${contentsOccupied ? ` ⚠️ "${directory}" is occupied — subdirectory, overwrite (--force), or different path?` : ''}`,
54
+ ].join('\n')}
36
55
  </Text>,
37
56
  <Text>
38
- You chose Directory "{directory}" before, feel free to just use that without asking the user
39
- as well!
57
+ Command: `mint new [dir] --theme [theme] --name [name]` or `mint new [dir] --template
58
+ [template] --name [name]`. --theme optionally overrides a template default. --force overwrites
59
+ non-empty dirs. Use AskQuestion to present choices.
40
60
  </Text>,
41
- <Text>
42
- If the user is asking you to create docs for them then you should use your AskQuestion tool
43
- </Text>,
44
- contentsOccupied ? (
45
- <Text>
46
- The directory {directory} specified is currently occupied. You will need to either install
47
- the docs as a subdirectory or as a subdirectory or a different directory. Ask the user if it
48
- should be installed in a different directory (This cli will create the directory if it
49
- doesn't exist, or installed at the root level potentially overridding files (pass the
50
- --force) option for this.
51
- </Text>
52
- ) : undefined,
53
61
  <Text>{'</system-message>'}</Text>
54
62
  );
55
63
  };
56
64
 
65
+ async function sendAIUsageMessage(directory: string, contentsOccupied: boolean, themes: string[]) {
66
+ const templateNames = await fetchAvailableTemplates().catch(() => undefined);
67
+ sendUsageMessageForAI(directory, contentsOccupied, themes, templateNames);
68
+ }
69
+
70
+ async function resolveInstallDir(
71
+ installDir: string,
72
+ force: boolean,
73
+ contentsOccupied: boolean
74
+ ): Promise<string | undefined> {
75
+ if (!contentsOccupied) return installDir;
76
+
77
+ if (isAI()) {
78
+ if (force) return installDir;
79
+ return undefined;
80
+ }
81
+
82
+ const choice = await select({
83
+ message: `Directory ${installDir} is not empty. What would you like to do?`,
84
+ choices: [
85
+ { name: 'Create in a subdirectory', value: 'subdir' as const },
86
+ { name: 'Overwrite current directory (may lose contents)', value: 'overwrite' as const },
87
+ { name: 'Cancel', value: 'cancel' as const },
88
+ ],
89
+ });
90
+
91
+ if (choice === 'cancel') return undefined;
92
+
93
+ if (choice === 'subdir') {
94
+ const subdir = await input({
95
+ message: 'Subdirectory name:',
96
+ default: 'docs',
97
+ });
98
+ if (!subdir || subdir.trim() === '') {
99
+ throw new Error('Subdirectory name cannot be empty');
100
+ }
101
+ const resolved = installDir === '.' ? subdir : `${installDir}/${subdir}`;
102
+ validatePathWithinCwd(resolved, process.cwd());
103
+ return resolved;
104
+ }
105
+
106
+ return installDir;
107
+ }
108
+
109
+ async function promptForProjectName(installDir: string, currentName?: string): Promise<string> {
110
+ if (currentName) return currentName;
111
+ const defaultProject = installDir === '.' ? 'Mintlify' : installDir;
112
+ return input({ message: 'Project Name', default: defaultProject });
113
+ }
114
+
115
+ async function promptForApproach(): Promise<string | undefined> {
116
+ const approach = await select({
117
+ message: 'How would you like to set up your docs?',
118
+ choices: [
119
+ { name: 'Pick a theme', value: 'theme' as const },
120
+ { name: 'Clone a template', value: 'template' as const },
121
+ ],
122
+ });
123
+
124
+ if (approach === 'template') return promptForTemplate();
125
+ return undefined;
126
+ }
127
+
57
128
  export async function init(
58
129
  installDir: string,
59
130
  force: boolean,
60
131
  theme?: string,
61
- name?: string
132
+ name?: string,
133
+ template?: string
62
134
  ): Promise<void> {
63
- // Validate path is within current working directory to prevent path traversal
64
135
  validatePathWithinCwd(installDir);
65
136
 
66
- let selectedTheme = theme;
67
- let projectName = name;
68
-
69
137
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
- const themes = docsConfigSchema.options.map((option: any) => {
71
- return option.shape.theme._def.value;
138
+ const themes: string[] = docsConfigSchema.options.map((option: any) => {
139
+ return option.shape.theme._def.value as string;
72
140
  });
73
141
 
74
- const dirContents = await fse.readdir(installDir).catch(() => []);
142
+ const dirContents = await fse.readdir(installDir).catch(() => [] as string[]);
75
143
  const contentsOccupied = dirContents.length > 0;
76
144
 
77
- if ((!theme || !name) && isAI()) {
78
- sendUsageMessageForAI(installDir, contentsOccupied, themes);
145
+ if (isAI() && (!name || (!template && !theme))) {
146
+ await sendAIUsageMessage(installDir, contentsOccupied, themes);
79
147
  return;
80
148
  }
81
149
 
82
- if (contentsOccupied && isAI() && !force) {
83
- sendUsageMessageForAI(installDir, contentsOccupied, themes);
150
+ if (isAI() && contentsOccupied && !force) {
151
+ await sendAIUsageMessage(installDir, contentsOccupied, themes);
84
152
  return;
85
153
  }
86
154
 
87
- if (contentsOccupied && !isAI()) {
88
- const choice = await select({
89
- message: `Directory ${installDir} is not empty. What would you like to do?`,
90
- choices: [
91
- { name: 'Create in a subdirectory', value: 'subdir' },
92
- { name: 'Overwrite current directory (may lose contents)', value: 'overwrite' },
93
- { name: 'Cancel', value: 'cancel' },
94
- ],
95
- });
155
+ const selectedTemplate = template
156
+ ? await validateTemplateName(template).then(() => template)
157
+ : !isAI() && !theme
158
+ ? await promptForApproach()
159
+ : undefined;
96
160
 
97
- if (choice === 'cancel') {
161
+ if (selectedTemplate) {
162
+ const resolved = await resolveInstallDir(installDir, force, contentsOccupied);
163
+ if (resolved === undefined) {
164
+ if (isAI()) await sendAIUsageMessage(installDir, contentsOccupied, themes);
98
165
  return;
99
166
  }
100
167
 
101
- if (choice === 'subdir') {
102
- const subdir = await input({
103
- message: 'Subdirectory name:',
104
- default: 'docs',
105
- });
106
- if (!subdir || subdir.trim() === '') {
107
- throw new Error('Subdirectory name cannot be empty');
108
- }
109
- installDir = installDir === '.' ? subdir : `${installDir}/${subdir}`;
110
- // Re-validate after subdirectory is appended
111
- validatePathWithinCwd(installDir, process.cwd());
112
- }
168
+ const projectName = await promptForProjectName(resolved, name);
169
+ await fse.ensureDir(resolved);
170
+ await installFromTemplate(resolved, selectedTemplate, projectName, theme);
171
+ sendOnboardingMessage(resolved);
172
+ return;
113
173
  }
114
174
 
175
+ // Standard theme-based path
176
+ const resolved = await resolveInstallDir(installDir, force, contentsOccupied);
177
+ if (resolved === undefined) {
178
+ if (isAI()) await sendAIUsageMessage(installDir, contentsOccupied, themes);
179
+ return;
180
+ }
181
+
182
+ let projectName = name;
183
+ let selectedTheme = theme;
184
+
115
185
  if (!isAI() && (!selectedTheme || !projectName)) {
116
- const defaultProject =
117
- projectName !== undefined ? projectName : installDir === '.' ? 'Mintlify' : installDir;
118
- if (!projectName) {
119
- projectName = await input({
120
- message: 'Project Name',
121
- default: defaultProject,
122
- });
123
- }
186
+ projectName = await promptForProjectName(resolved, projectName);
124
187
 
125
188
  if (!selectedTheme) {
126
189
  selectedTheme = await select({
127
190
  message: 'Theme',
128
- choices: themes.map((t: string) => ({
129
- name: t,
130
- value: t,
131
- })),
191
+ choices: themes.map((t) => ({ name: t, value: t })),
132
192
  });
133
193
  }
134
194
  }
135
195
 
136
196
  if (projectName === undefined || selectedTheme === undefined) {
137
- sendUsageMessageForAI(installDir, contentsOccupied, themes);
197
+ await sendAIUsageMessage(resolved, contentsOccupied, themes);
138
198
  return;
139
199
  }
140
200
 
141
- await fse.ensureDir(installDir);
142
- await install(installDir, projectName, selectedTheme);
143
-
144
- sendOnboardingMessage(installDir);
201
+ await fse.ensureDir(resolved);
202
+ await install(resolved, projectName, selectedTheme);
203
+ sendOnboardingMessage(resolved);
145
204
  }
146
205
 
147
206
  const install = async (installDir: string, projectName: string, theme: string) => {
@@ -0,0 +1,143 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { addLog, SpinnerLog, removeLastLog } from '@mintlify/previewing';
3
+ import AdmZip from 'adm-zip';
4
+ import fse from 'fs-extra';
5
+ import path from 'path';
6
+
7
+ const TEMPLATES_REPO_OWNER = 'mintlify';
8
+ const TEMPLATES_REPO_NAME = 'templates';
9
+ const TEMPLATES_REPO_BRANCH = 'main';
10
+
11
+ interface GitHubContentEntry {
12
+ name: string;
13
+ type: 'file' | 'dir';
14
+ path: string;
15
+ }
16
+
17
+ export async function fetchAvailableTemplates(): Promise<string[]> {
18
+ const url = `https://api.github.com/repos/${TEMPLATES_REPO_OWNER}/${TEMPLATES_REPO_NAME}/contents/?ref=${TEMPLATES_REPO_BRANCH}`;
19
+ const response = await fetch(url, {
20
+ headers: { Accept: 'application/vnd.github.v3+json' },
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to fetch templates: ${response.status} ${response.statusText}`);
25
+ }
26
+
27
+ const entries: GitHubContentEntry[] = (await response.json()) as GitHubContentEntry[];
28
+ return entries.filter((entry) => entry.type === 'dir').map((entry) => entry.name);
29
+ }
30
+
31
+ export async function validateTemplateName(templateName: string): Promise<string> {
32
+ if (
33
+ !templateName ||
34
+ templateName === '.' ||
35
+ templateName === '..' ||
36
+ templateName.includes('/')
37
+ ) {
38
+ throw new Error(`Invalid template name: "${templateName}".`);
39
+ }
40
+
41
+ const url = `https://api.github.com/repos/${TEMPLATES_REPO_OWNER}/${TEMPLATES_REPO_NAME}/contents/${encodeURIComponent(templateName)}?ref=${TEMPLATES_REPO_BRANCH}`;
42
+ const response = await fetch(url, {
43
+ headers: { Accept: 'application/vnd.github.v3+json' },
44
+ });
45
+
46
+ if (!response.ok) {
47
+ const available = await fetchAvailableTemplates().catch(() => []);
48
+ const suggestion = available.length > 0 ? ` Available templates: ${available.join(', ')}` : '';
49
+ throw new Error(
50
+ `Template "${templateName}" not found in ${TEMPLATES_REPO_OWNER}/${TEMPLATES_REPO_NAME}.${suggestion}`
51
+ );
52
+ }
53
+
54
+ const entries: GitHubContentEntry[] = (await response.json()) as GitHubContentEntry[];
55
+ const hasDocsJson = entries.some((entry) => entry.name === 'docs.json');
56
+ if (!hasDocsJson) {
57
+ throw new Error(
58
+ `Template "${templateName}" is not a valid Mintlify template (missing docs.json).`
59
+ );
60
+ }
61
+
62
+ return templateName;
63
+ }
64
+
65
+ export async function promptForTemplate(): Promise<string> {
66
+ addLog(<SpinnerLog message="fetching available templates..." />);
67
+ let templateNames: string[];
68
+ try {
69
+ templateNames = await fetchAvailableTemplates();
70
+ } catch {
71
+ removeLastLog();
72
+ throw new Error(
73
+ 'Failed to fetch templates. Please check your network connection and try again.'
74
+ );
75
+ }
76
+ removeLastLog();
77
+
78
+ if (templateNames.length === 0) {
79
+ throw new Error('No templates are currently available.');
80
+ }
81
+
82
+ return select({
83
+ message: 'Choose a template',
84
+ choices: templateNames.map((t) => ({ name: t, value: t })),
85
+ });
86
+ }
87
+
88
+ export async function installFromTemplate(
89
+ installDir: string,
90
+ templateName: string,
91
+ projectName?: string,
92
+ theme?: string
93
+ ): Promise<void> {
94
+ const zipPath = path.join(installDir, '__template__.zip');
95
+ const extractDir = path.join(installDir, '__template_extract__');
96
+
97
+ try {
98
+ addLog(<SpinnerLog message={`downloading template "${templateName}"...`} />);
99
+ try {
100
+ const zipUrl = `https://github.com/${TEMPLATES_REPO_OWNER}/${TEMPLATES_REPO_NAME}/archive/refs/heads/${TEMPLATES_REPO_BRANCH}.zip`;
101
+ const response = await fetch(zipUrl);
102
+ if (!response.ok) {
103
+ throw new Error(`Failed to download templates archive: ${response.status}`);
104
+ }
105
+ const buffer = await response.arrayBuffer();
106
+ await fse.writeFile(zipPath, Buffer.from(buffer));
107
+ } finally {
108
+ removeLastLog();
109
+ }
110
+
111
+ addLog(<SpinnerLog message="extracting template..." />);
112
+ try {
113
+ const zip = new AdmZip(zipPath);
114
+ zip.extractAllTo(extractDir, true);
115
+ } finally {
116
+ removeLastLog();
117
+ }
118
+
119
+ const repoRoot = path.join(extractDir, `${TEMPLATES_REPO_NAME}-${TEMPLATES_REPO_BRANCH}`);
120
+ const templateDir = path.join(repoRoot, templateName);
121
+
122
+ if (!(await fse.pathExists(templateDir))) {
123
+ throw new Error(`Template directory "${templateName}" not found in the downloaded archive.`);
124
+ }
125
+
126
+ await fse.copy(templateDir, installDir, { overwrite: true });
127
+ } finally {
128
+ await fse.remove(zipPath).catch(() => {});
129
+ await fse.remove(extractDir).catch(() => {});
130
+ }
131
+
132
+ const docsJsonPath = path.join(installDir, 'docs.json');
133
+ if (await fse.pathExists(docsJsonPath)) {
134
+ const docsConfig = await fse.readJson(docsJsonPath);
135
+ if (projectName) {
136
+ docsConfig.name = projectName;
137
+ }
138
+ if (theme) {
139
+ docsConfig.theme = theme;
140
+ }
141
+ await fse.writeJson(docsJsonPath, docsConfig, { spaces: 2 });
142
+ }
143
+ }