@mintlify/cli 4.0.1763 → 4.0.1765

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.
Files changed (40) hide show
  1. package/__test__/migrate.test.ts +186 -0
  2. package/__test__/validateRules.test.ts +36 -0
  3. package/bin/cli.js +35 -0
  4. package/bin/migrate.js +54 -0
  5. package/bin/tsconfig.build.tsbuildinfo +1 -1
  6. package/bin/validate/files.js +16 -4
  7. package/bin/validate/mdx.js +1 -1
  8. package/migrate/index.md +56 -0
  9. package/migrate/sources/confluence.md +120 -0
  10. package/migrate/sources/docsify.md +128 -0
  11. package/migrate/sources/document360.md +117 -0
  12. package/migrate/sources/docus.md +136 -0
  13. package/migrate/sources/docusaurus.md +143 -0
  14. package/migrate/sources/fern.md +130 -0
  15. package/migrate/sources/fumadocs.md +141 -0
  16. package/migrate/sources/gitbook.md +120 -0
  17. package/migrate/sources/github-wiki.md +90 -0
  18. package/migrate/sources/jekyll.md +104 -0
  19. package/migrate/sources/mdbook.md +98 -0
  20. package/migrate/sources/mkdocs.md +113 -0
  21. package/migrate/sources/nextra.md +144 -0
  22. package/migrate/sources/notion.md +99 -0
  23. package/migrate/sources/nuxt.md +113 -0
  24. package/migrate/sources/pdf.md +99 -0
  25. package/migrate/sources/readme.md +118 -0
  26. package/migrate/sources/redocly.md +114 -0
  27. package/migrate/sources/starlight.md +136 -0
  28. package/migrate/sources/vitepress.md +143 -0
  29. package/migrate/sources/vuepress.md +153 -0
  30. package/migrate/topics/api.md +76 -0
  31. package/migrate/topics/changelog.md +47 -0
  32. package/migrate/topics/conversion.md +194 -0
  33. package/migrate/topics/discovery.md +93 -0
  34. package/migrate/topics/navigation.md +82 -0
  35. package/migrate/topics/verify.md +58 -0
  36. package/package.json +10 -10
  37. package/src/cli.tsx +47 -0
  38. package/src/migrate.ts +76 -0
  39. package/src/validate/files.ts +23 -3
  40. package/src/validate/mdx.ts +1 -1
package/src/cli.tsx CHANGED
@@ -42,6 +42,13 @@ import { login } from './login.js';
42
42
  import { logout } from './logout.js';
43
43
  import { mdxLinter } from './mdxLinter.js';
44
44
  import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
45
+ import {
46
+ agents,
47
+ listMigrationDocs,
48
+ migrateWithAgent,
49
+ migrationInstructions,
50
+ type AgentName,
51
+ } from './migrate.js';
45
52
  import { ogCommand } from './og.js';
46
53
  import { buildOverlayRegistryForCheck, checkOpenApiFile } from './openApiCheck.js';
47
54
  import { scoreHandler } from './score/index.js';
@@ -930,6 +937,46 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
930
937
  .example('mint format', 'format all MDX files in the current directory'),
931
938
  formatHandler
932
939
  )
940
+ .command(
941
+ 'migrate',
942
+ 'Print instructions for migrating a docs site to Mintlify',
943
+ (yargs: Argv) =>
944
+ yargs
945
+ .option('from', {
946
+ type: 'string',
947
+ choices: listMigrationDocs('sources'),
948
+ description: 'Include instructions for a source platform',
949
+ })
950
+ .option('topic', {
951
+ type: 'string',
952
+ choices: listMigrationDocs('topics'),
953
+ description: 'Print a single topic',
954
+ })
955
+ .option('agent', {
956
+ type: 'string',
957
+ choices: Object.keys(agents) as AgentName[],
958
+ description: 'Run the migration with a coding agent',
959
+ })
960
+ .option('claude', {
961
+ type: 'boolean',
962
+ description: 'Alias for --agent claude',
963
+ }),
964
+ async (argv) => {
965
+ const agent = argv.agent ?? (argv.claude ? 'claude' : undefined);
966
+ if (agent) {
967
+ try {
968
+ return await terminate(await migrateWithAgent(agent, argv.from));
969
+ } catch (error) {
970
+ addLog(<ErrorLog message={error instanceof Error ? error.message : String(error)} />);
971
+ return await terminate(1);
972
+ }
973
+ }
974
+ await new Promise<void>((resolve) =>
975
+ process.stdout.write(migrationInstructions(argv), () => resolve())
976
+ );
977
+ await terminate(0);
978
+ }
979
+ )
933
980
  // Coming soon commands — visible in help, tracked via telemetry to gauge interest.
934
981
  .command(
935
982
  'ai',
package/src/migrate.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { windowsCompatibleSpawnOptions } from './helpers.js';
7
+
8
+ type Section = 'topics' | 'sources';
9
+
10
+ const root = fileURLToPath(new URL('../migrate', import.meta.url));
11
+
12
+ const read = (...segments: string[]) => readFileSync(path.join(root, ...segments), 'utf8');
13
+
14
+ export const listMigrationDocs = (section: Section) =>
15
+ readdirSync(path.join(root, section))
16
+ .filter((file) => file.endsWith('.md'))
17
+ .map((file) => file.slice(0, -3))
18
+ .sort();
19
+
20
+ const overview = () =>
21
+ [
22
+ read('index.md').trimEnd(),
23
+ `Topics: ${listMigrationDocs('topics').join(', ')}. Run \`mint migrate --topic <name>\`.`,
24
+ `Sources: ${listMigrationDocs('sources').join(', ')}. Run \`mint migrate --from <name>\`.`,
25
+ ].join('\n\n');
26
+
27
+ export const migrationInstructions = ({ from, topic }: { from?: string; topic?: string }) => {
28
+ if (topic) return read('topics', `${topic}.md`);
29
+ if (from) return `${overview()}\n\n${read('sources', `${from}.md`)}`;
30
+ return `${overview()}\n`;
31
+ };
32
+
33
+ const prompt = (from?: string) =>
34
+ `Migrate a documentation site to Mintlify in the current directory. Run \`mint migrate${from ? ` --from ${from}` : ''}\` and follow the instructions it prints.`;
35
+
36
+ export const agents = {
37
+ claude: {
38
+ bin: 'claude',
39
+ args: (prompt: string) => [prompt],
40
+ install: 'npm install -g @anthropic-ai/claude-code',
41
+ },
42
+ codex: {
43
+ bin: 'codex',
44
+ args: (prompt: string) => [prompt],
45
+ install: 'npm install -g @openai/codex',
46
+ },
47
+ cursor: {
48
+ bin: 'cursor-agent',
49
+ args: (prompt: string) => [prompt],
50
+ install: 'curl https://cursor.com/install -fsS | bash',
51
+ },
52
+ gemini: {
53
+ bin: 'gemini',
54
+ args: (prompt: string) => ['-i', prompt],
55
+ install: 'npm install -g @google/gemini-cli',
56
+ },
57
+ };
58
+
59
+ export type AgentName = keyof typeof agents;
60
+
61
+ export const agentArgs = (name: AgentName, from?: string) => agents[name].args(prompt(from));
62
+
63
+ export const migrateWithAgent = (name: AgentName, from?: string): Promise<number> =>
64
+ new Promise((resolve, reject) => {
65
+ const { bin, install } = agents[name];
66
+ const args = agentArgs(name, from).map((arg) =>
67
+ windowsCompatibleSpawnOptions.shell ? `"${arg}"` : arg
68
+ );
69
+ const child = spawn(bin, args, { stdio: 'inherit', ...windowsCompatibleSpawnOptions });
70
+ child.on('error', (error: NodeJS.ErrnoException) =>
71
+ reject(
72
+ error.code === 'ENOENT' ? new Error(`${bin} not found. Install it with: ${install}`) : error
73
+ )
74
+ );
75
+ child.on('close', (code) => resolve(code ?? 1));
76
+ });
@@ -13,6 +13,7 @@ import {
13
13
  parseContent,
14
14
  relocate,
15
15
  selectEdits,
16
+ toOffset,
16
17
  unmapEdit,
17
18
  } from './mdx.js';
18
19
  import { PARSE_ERROR } from './rules/parseError.js';
@@ -56,16 +57,35 @@ const readFrontmatter = (content: string): Record<string, unknown> => {
56
57
  type ParseError = { line?: number; column?: number; message: string };
57
58
 
58
59
  const REASON_POSITION = /\s*\(\d+:\d+(?:-\d+:\d+)?\)$/;
60
+ const TAG_NAME_RULES = new Set(['unexpected-character', 'unexpected-eof']);
61
+ const ESCAPE_HINT = "; if this < isn't a tag, escape it as \\<";
59
62
 
60
- export const toParseError = (error: unknown): ParseError => {
63
+ const isStrayLessThan = (
64
+ error: unknown,
65
+ content: string,
66
+ position: { line?: number; column?: number } | null
67
+ ) => {
68
+ const { source, ruleId } = (error ?? {}) as { source?: unknown; ruleId?: unknown };
69
+ return (
70
+ source === 'micromark-extension-mdx-jsx' &&
71
+ typeof ruleId === 'string' &&
72
+ TAG_NAME_RULES.has(ruleId) &&
73
+ position?.line != null &&
74
+ position.column != null &&
75
+ content[toOffset(content, position.line, position.column) - 1] === '<'
76
+ );
77
+ };
78
+
79
+ export const toParseError = (error: unknown, content = ''): ParseError => {
61
80
  const reason = (error as { reason?: unknown } | null)?.reason;
62
81
  const fallback = error instanceof Error ? error.message : 'could not parse this file';
63
82
  const text = typeof reason === 'string' ? reason : fallback;
64
83
  const position = error && typeof error === 'object' ? getErrorPosition(error) : null;
84
+ const hint = isStrayLessThan(error, content, position) ? ESCAPE_HINT : '';
65
85
  return {
66
86
  ...(position?.line != null && { line: position.line }),
67
87
  ...(position?.column != null && { column: position.column }),
68
- message: text.replace(REASON_POSITION, ''),
88
+ message: text.replace(REASON_POSITION, '') + hint,
69
89
  };
70
90
  };
71
91
 
@@ -102,7 +122,7 @@ export const parseFile = (
102
122
  try {
103
123
  return { context: { ...context, tree: getAST(content, file) }, approximate: true };
104
124
  } catch (error) {
105
- return { error: toParseError(error) };
125
+ return { error: toParseError(error, content) };
106
126
  }
107
127
  };
108
128
 
@@ -186,7 +186,7 @@ export const applyEdits = (content: string, edits: Edit[]): string =>
186
186
  content
187
187
  );
188
188
 
189
- const toOffset = (content: string, line: number, column = 1) => {
189
+ export const toOffset = (content: string, line: number, column = 1) => {
190
190
  let offset = 0;
191
191
  for (let current = 1; current < line; current++) {
192
192
  const next = content.indexOf('\n', offset);