@mintlify/cli 4.0.1378 → 4.0.1380

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.
@@ -147,6 +147,7 @@ var EDITOR_COMPONENT_NAMES = /* @__PURE__ */ new Set([
147
147
  "Prompt",
148
148
  "Mermaid"
149
149
  ]);
150
+ var INLINE_COMPONENTS = /* @__PURE__ */ new Set(["Badge", "Icon", "Tooltip"]);
150
151
  var IGNORED_MERGE_ATTRS = /* @__PURE__ */ new Set([
151
152
  "__childIndex",
152
153
  "__isLastInGroup",
@@ -1371,8 +1372,25 @@ function handleFlowJsxInner(node, ctx, transformNode2, mapJsxChildren2) {
1371
1372
  attrs: { mdxJsxNode: node }
1372
1373
  };
1373
1374
  }
1375
+ function flattenJsxParagraphs(node) {
1376
+ const children = node.children.flatMap((child) => {
1377
+ if (child.type !== "paragraph" || !isJsxOnly(child.children))
1378
+ return child;
1379
+ return child.children.filter((jsx) => jsx.type === "mdxJsxTextElement").map((jsx) => Object.assign(Object.assign({}, jsx), { type: "mdxJsxFlowElement" }));
1380
+ });
1381
+ return Object.assign(Object.assign({}, node), { children });
1382
+ }
1383
+ function componentChildren(node, name) {
1384
+ return flattenJsxParagraphs(node).children.filter((child) => child.type === "mdxJsxFlowElement" && (name === void 0 || child.name === name));
1385
+ }
1386
+ function isJsxOnly(children) {
1387
+ return children.every((child) => child.type === "mdxJsxTextElement" && isBlockName(child.name) || child.type === "text" && child.value.trim() === "");
1388
+ }
1389
+ function isBlockName(name) {
1390
+ return !!name && !HTML_ELEMENTS.has(name) && !INLINE_COMPONENTS.has(name);
1391
+ }
1374
1392
  function handleSteps(node, ctx, mapJsxChildren2) {
1375
- const stepChildren = node.children.filter((child) => child.type === "mdxJsxFlowElement" && child.name === BLOCK_TYPES.step);
1393
+ const stepChildren = componentChildren(node, BLOCK_TYPES.step);
1376
1394
  const pmStepChildren = stepChildren.map((stepChild, index) => {
1377
1395
  const stepAttrs = parseMdxAttributes(stepChild.attributes);
1378
1396
  const titleText = stepAttrs.title;
@@ -1445,7 +1463,7 @@ function handleToggleContent(node, ctx, transformNode2, mapJsxChildren2) {
1445
1463
  })
1446
1464
  };
1447
1465
  }
1448
- const flowChildren = node.children.filter((child) => child.type === "mdxJsxFlowElement");
1466
+ const flowChildren = componentChildren(node);
1449
1467
  if (flowChildren.length > 0) {
1450
1468
  const childType = (_c = toggleContent[(_b = node.name) !== null && _b !== void 0 ? _b : ""]) !== null && _c !== void 0 ? _c : BLOCK_TYPES.tab;
1451
1469
  const partitionChild = (parsed) => childType === BLOCK_TYPES.tab ? partitionAttrs(parsed, TAB_ATTRS) : parsed;
@@ -1469,7 +1487,7 @@ function handleToggleContent(node, ctx, transformNode2, mapJsxChildren2) {
1469
1487
  }
1470
1488
  function handleColumns(node, ctx, mapJsxChildren2) {
1471
1489
  const attrs2 = partitionAttrs(parseMdxAttributes(node.attributes), COLUMNS_ATTRS);
1472
- const columnChildren = node.children.filter((child) => child.type === "mdxJsxFlowElement" && child.name === BLOCK_TYPES.column);
1490
+ const columnChildren = componentChildren(node, BLOCK_TYPES.column);
1473
1491
  if (columnChildren.length > 0) {
1474
1492
  const pmChildren = columnChildren.map((child) => {
1475
1493
  const content = mapJsxChildren2(child, Object.assign(Object.assign({}, ctx), { hasGroupParent: true }));
@@ -1504,14 +1522,13 @@ function handleColor(node) {
1504
1522
  const colorAttrs = parseMdxAttributes(node.attributes);
1505
1523
  const variant = typeof colorAttrs.variant === "string" ? colorAttrs.variant : null;
1506
1524
  const extraColorAttrs = partitionAttrs(colorAttrs, [], ["variant"]);
1507
- const flowChildren = node.children.filter((child) => child.type === "mdxJsxFlowElement");
1508
- const colorData = flowChildren.map((child) => {
1525
+ const colorData = componentChildren(node).map((child) => {
1509
1526
  if (child.name === "Color.Item") {
1510
1527
  return parseColorItemNode(child);
1511
1528
  }
1512
1529
  if (child.name === "Color.Row") {
1513
1530
  const rowAttrs = parseMdxAttributes(child.attributes);
1514
- const rowItems = child.children.filter((c) => c.type === "mdxJsxFlowElement" && c.name === "Color.Item").map(parseColorItemNode);
1531
+ const rowItems = componentChildren(child, "Color.Item").map(parseColorItemNode);
1515
1532
  return {
1516
1533
  type: "row",
1517
1534
  title: typeof rowAttrs.title === "string" ? rowAttrs.title : void 0,
@@ -1526,7 +1543,7 @@ function handleColor(node) {
1526
1543
  };
1527
1544
  }
1528
1545
  function parseTreeChildren(node) {
1529
- return node.children.filter((child) => child.type === "mdxJsxFlowElement").map((child) => {
1546
+ return componentChildren(node).map((child) => {
1530
1547
  if (child.name === "Tree.File") {
1531
1548
  const fileAttrs = parseMdxAttributes(child.attributes);
1532
1549
  return {
@@ -2393,7 +2410,7 @@ function isInlineNode(node, schema) {
2393
2410
  }
2394
2411
  function mapJsxChildren(node, ctx) {
2395
2412
  var _a;
2396
- const nodes2 = node.children.flatMap((child) => {
2413
+ const nodes2 = flattenJsxParagraphs({ children: node.children }).children.flatMap((child) => {
2397
2414
  if (child.type === "mdxJsxFlowElement") {
2398
2415
  return handleFlowJsx(child, ctx, transformNode, mapJsxChildren);
2399
2416
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1378",
3
+ "version": "4.0.1380",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -59,6 +59,7 @@
59
59
  "ink": "6.3.0",
60
60
  "inquirer": "12.3.0",
61
61
  "js-yaml": "4.1.1",
62
+ "jsonc-parser": "^3.3.1",
62
63
  "mdast-util-mdx-jsx": "3.2.0",
63
64
  "open": "8.4.2",
64
65
  "openid-client": "6.8.2",
@@ -81,7 +82,7 @@
81
82
  "keytar": "7.9.0"
82
83
  },
83
84
  "devDependencies": {
84
- "@mintlify/editor": "0.0.280",
85
+ "@mintlify/editor": "0.0.281",
85
86
  "@mintlify/ts-config": "2.0.2",
86
87
  "@tsconfig/recommended": "1.0.2",
87
88
  "@types/adm-zip": "0.5.7",
@@ -101,5 +102,5 @@
101
102
  "vitest": "2.1.9",
102
103
  "vitest-mock-process": "1.0.4"
103
104
  },
104
- "gitHead": "ba797e7b83c5b5ec75b50bc32b25ef0349cd1f7f"
105
+ "gitHead": "db94e220403a0f6cdcb14b1549536de1356b0a93"
105
106
  }
package/src/cli.tsx CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  suppressConsoleWarnings,
39
39
  terminate,
40
40
  } from './helpers.js';
41
+ import { runIndexSetup } from './indexSetup/setup.js';
41
42
  import { init } from './init.js';
42
43
  import { getAccessToken } from './keyring.js';
43
44
  import { login } from './login.js';
@@ -582,6 +583,35 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
582
583
  )
583
584
  .demandCommand(1, 'specify a subcommand: set, get, or clear')
584
585
  )
586
+ .command(
587
+ 'index',
588
+ 'Install the Mintlify index MCP server into your coding agents',
589
+ (yargs) =>
590
+ yargs
591
+ .option('claude', { type: 'boolean', description: 'Set up Claude Code' })
592
+ .option('cursor', { type: 'boolean', description: 'Set up Cursor' })
593
+ .option('vscode', { type: 'boolean', description: 'Set up VS Code' })
594
+ .option('codex', { type: 'boolean', description: 'Set up Codex' })
595
+ .option('opencode', { type: 'boolean', description: 'Set up OpenCode' })
596
+ .option('windsurf', { type: 'boolean', description: 'Set up Windsurf' })
597
+ .option('zed', { type: 'boolean', description: 'Set up Zed' })
598
+ .option('project', {
599
+ type: 'boolean',
600
+ description: 'Write project-level config instead of global',
601
+ })
602
+ .option('yes', {
603
+ alias: 'y',
604
+ type: 'boolean',
605
+ description: 'Skip prompts and set up all detected agents',
606
+ })
607
+ .usage('usage: mintlify index [options]')
608
+ .example('mintlify index', 'interactive picker')
609
+ .example('mintlify index --claude --cursor', 'set up specific agents'),
610
+ async (argv) => {
611
+ const code = await runIndexSetup(argv);
612
+ await terminate(code);
613
+ }
614
+ )
585
615
  .command(
586
616
  'update',
587
617
  'Update the CLI to the latest version',
@@ -0,0 +1,206 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import { CURSOR_RULE, RULE_BODY, VSCODE_RULE } from './rule.js';
5
+
6
+ export const INDEX_MCP_URL = 'https://index.mintlify.com/mcp';
7
+ export const SERVER_NAME = 'mintlify-index';
8
+
9
+ export type ClientName = 'claude' | 'cursor' | 'vscode' | 'codex' | 'opencode' | 'windsurf' | 'zed';
10
+
11
+ export type Scope = 'global' | 'project';
12
+
13
+ export interface SetupContext {
14
+ home: string;
15
+ cwd: string;
16
+ platform: NodeJS.Platform;
17
+ env: Record<string, string | undefined>;
18
+ }
19
+
20
+ export interface RuleSpec {
21
+ kind: 'file' | 'append';
22
+ content: string;
23
+ globalPath: (ctx: SetupContext) => string;
24
+ projectPath?: (ctx: SetupContext) => string;
25
+ }
26
+
27
+ export interface ClientDefinition {
28
+ name: ClientName;
29
+ displayName: string;
30
+ format: 'json' | 'toml';
31
+ configKey: string;
32
+ globalConfigPaths: (ctx: SetupContext) => string[];
33
+ projectConfigPaths?: (ctx: SetupContext) => string[];
34
+ buildEntry: () => Record<string, unknown>;
35
+ rule?: RuleSpec;
36
+ detectPaths: (ctx: SetupContext) => string[];
37
+ }
38
+
39
+ function claudeConfigDir(ctx: SetupContext): string {
40
+ const override = ctx.env.CLAUDE_CONFIG_DIR;
41
+ return override !== undefined && override !== '' ? override : path.join(ctx.home, '.claude');
42
+ }
43
+
44
+ function claudeGlobalConfigPath(ctx: SetupContext): string {
45
+ const override = ctx.env.CLAUDE_CONFIG_DIR;
46
+ return override !== undefined && override !== ''
47
+ ? path.join(override, '.claude.json')
48
+ : path.join(ctx.home, '.claude.json');
49
+ }
50
+
51
+ function zedUserDir(ctx: SetupContext): string {
52
+ if (ctx.platform === 'win32') {
53
+ const appData = ctx.env.APPDATA;
54
+ const base =
55
+ appData !== undefined && appData !== '' ? appData : path.join(ctx.home, 'AppData', 'Roaming');
56
+ return path.join(base, 'Zed');
57
+ }
58
+ return path.join(ctx.home, '.config', 'zed');
59
+ }
60
+
61
+ function vscodeUserDir(ctx: SetupContext): string {
62
+ if (ctx.platform === 'darwin') {
63
+ return path.join(ctx.home, 'Library', 'Application Support', 'Code', 'User');
64
+ }
65
+ if (ctx.platform === 'win32') {
66
+ const appData = ctx.env.APPDATA;
67
+ const base =
68
+ appData !== undefined && appData !== '' ? appData : path.join(ctx.home, 'AppData', 'Roaming');
69
+ return path.join(base, 'Code', 'User');
70
+ }
71
+ return path.join(ctx.home, '.config', 'Code', 'User');
72
+ }
73
+
74
+ export const CLIENTS: Record<ClientName, ClientDefinition> = {
75
+ claude: {
76
+ name: 'claude',
77
+ displayName: 'Claude Code',
78
+ format: 'json',
79
+ configKey: 'mcpServers',
80
+ globalConfigPaths: (ctx) => [claudeGlobalConfigPath(ctx)],
81
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.mcp.json')],
82
+ buildEntry: () => ({ type: 'http', url: INDEX_MCP_URL }),
83
+ rule: {
84
+ kind: 'file',
85
+ content: RULE_BODY,
86
+ globalPath: (ctx) => path.join(claudeConfigDir(ctx), 'rules', 'mintlify-index.md'),
87
+ projectPath: (ctx) => path.join(ctx.cwd, '.claude', 'rules', 'mintlify-index.md'),
88
+ },
89
+ detectPaths: (ctx) => [claudeConfigDir(ctx)],
90
+ },
91
+ cursor: {
92
+ name: 'cursor',
93
+ displayName: 'Cursor',
94
+ format: 'json',
95
+ configKey: 'mcpServers',
96
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.cursor', 'mcp.json')],
97
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.cursor', 'mcp.json')],
98
+ buildEntry: () => ({ url: INDEX_MCP_URL }),
99
+ rule: {
100
+ kind: 'file',
101
+ content: CURSOR_RULE,
102
+ globalPath: (ctx) => path.join(ctx.home, '.cursor', 'rules', 'mintlify-index.mdc'),
103
+ projectPath: (ctx) => path.join(ctx.cwd, '.cursor', 'rules', 'mintlify-index.mdc'),
104
+ },
105
+ detectPaths: (ctx) => [path.join(ctx.home, '.cursor')],
106
+ },
107
+ vscode: {
108
+ name: 'vscode',
109
+ displayName: 'VS Code',
110
+ format: 'json',
111
+ configKey: 'servers',
112
+ globalConfigPaths: (ctx) => [path.join(vscodeUserDir(ctx), 'mcp.json')],
113
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.vscode', 'mcp.json')],
114
+ buildEntry: () => ({ type: 'http', url: INDEX_MCP_URL }),
115
+ rule: {
116
+ kind: 'file',
117
+ content: VSCODE_RULE,
118
+ globalPath: (ctx) =>
119
+ path.join(vscodeUserDir(ctx), 'prompts', 'mintlify-index.instructions.md'),
120
+ projectPath: (ctx) =>
121
+ path.join(ctx.cwd, '.github', 'instructions', 'mintlify-index.instructions.md'),
122
+ },
123
+ detectPaths: (ctx) => [vscodeUserDir(ctx)],
124
+ },
125
+ codex: {
126
+ name: 'codex',
127
+ displayName: 'Codex',
128
+ format: 'toml',
129
+ configKey: 'mcp_servers',
130
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.codex', 'config.toml')],
131
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.codex', 'config.toml')],
132
+ buildEntry: () => ({ type: 'http', url: INDEX_MCP_URL }),
133
+ rule: {
134
+ kind: 'append',
135
+ content: RULE_BODY,
136
+ globalPath: (ctx) => path.join(ctx.home, '.codex', 'AGENTS.md'),
137
+ projectPath: (ctx) => path.join(ctx.cwd, 'AGENTS.md'),
138
+ },
139
+ detectPaths: (ctx) => [path.join(ctx.home, '.codex')],
140
+ },
141
+ opencode: {
142
+ name: 'opencode',
143
+ displayName: 'OpenCode',
144
+ format: 'json',
145
+ configKey: 'mcp',
146
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.config', 'opencode', 'opencode.json')],
147
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, 'opencode.json')],
148
+ buildEntry: () => ({ type: 'remote', url: INDEX_MCP_URL, enabled: true }),
149
+ rule: {
150
+ kind: 'append',
151
+ content: RULE_BODY,
152
+ globalPath: (ctx) => path.join(ctx.home, '.config', 'opencode', 'AGENTS.md'),
153
+ projectPath: (ctx) => path.join(ctx.cwd, 'AGENTS.md'),
154
+ },
155
+ detectPaths: (ctx) => [path.join(ctx.home, '.config', 'opencode')],
156
+ },
157
+ windsurf: {
158
+ name: 'windsurf',
159
+ displayName: 'Windsurf',
160
+ format: 'json',
161
+ configKey: 'mcpServers',
162
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.codeium', 'windsurf', 'mcp_config.json')],
163
+ buildEntry: () => ({ serverUrl: INDEX_MCP_URL }),
164
+ rule: {
165
+ kind: 'append',
166
+ content: RULE_BODY,
167
+ globalPath: (ctx) =>
168
+ path.join(ctx.home, '.codeium', 'windsurf', 'memories', 'global_rules.md'),
169
+ projectPath: (ctx) => path.join(ctx.cwd, '.windsurf', 'rules', 'mintlify-index.md'),
170
+ },
171
+ detectPaths: (ctx) => [path.join(ctx.home, '.codeium', 'windsurf')],
172
+ },
173
+ zed: {
174
+ name: 'zed',
175
+ displayName: 'Zed',
176
+ format: 'json',
177
+ configKey: 'context_servers',
178
+ globalConfigPaths: (ctx) => [path.join(zedUserDir(ctx), 'settings.json')],
179
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.zed', 'settings.json')],
180
+ buildEntry: () => ({ url: INDEX_MCP_URL }),
181
+ detectPaths: (ctx) => [zedUserDir(ctx)],
182
+ },
183
+ };
184
+
185
+ export const ALL_CLIENT_NAMES: ClientName[] = [
186
+ 'claude',
187
+ 'cursor',
188
+ 'vscode',
189
+ 'codex',
190
+ 'opencode',
191
+ 'windsurf',
192
+ 'zed',
193
+ ];
194
+
195
+ export async function detectClients(ctx: SetupContext): Promise<ClientName[]> {
196
+ const detected: ClientName[] = [];
197
+ for (const name of ALL_CLIENT_NAMES) {
198
+ for (const candidate of CLIENTS[name].detectPaths(ctx)) {
199
+ if (await fs.pathExists(candidate)) {
200
+ detected.push(name);
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ return detected;
206
+ }
@@ -0,0 +1,140 @@
1
+ import fs from 'fs-extra';
2
+ import { applyEdits, modify, parse, type ParseError } from 'jsonc-parser';
3
+ import path from 'path';
4
+
5
+ function isEnoent(error: unknown): boolean {
6
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
7
+ }
8
+
9
+ export async function readJsonConfig(filePath: string): Promise<Record<string, unknown>> {
10
+ let raw: string;
11
+ try {
12
+ raw = await fs.readFile(filePath, 'utf-8');
13
+ } catch (error) {
14
+ if (isEnoent(error)) {
15
+ return {};
16
+ }
17
+ throw error;
18
+ }
19
+ if (raw.trim() === '') {
20
+ return {};
21
+ }
22
+ const errors: ParseError[] = [];
23
+ const parsed = parse(raw, errors, { allowTrailingComma: true });
24
+ if (errors.length > 0) {
25
+ throw new Error(`invalid JSON at ${filePath}`);
26
+ }
27
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
28
+ throw new Error(`expected a JSON object at ${filePath}`);
29
+ }
30
+ return parsed;
31
+ }
32
+
33
+ export function mergeServerEntry(
34
+ config: Record<string, unknown>,
35
+ configKey: string,
36
+ serverName: string,
37
+ entry: Record<string, unknown>
38
+ ): { config: Record<string, unknown>; alreadyExists: boolean } {
39
+ const section = config[configKey];
40
+ const servers: Record<string, unknown> = {};
41
+ if (typeof section === 'object' && section !== null && !Array.isArray(section)) {
42
+ Object.assign(servers, section);
43
+ }
44
+ const alreadyExists = serverName in servers;
45
+ servers[serverName] = entry;
46
+ return { config: { ...config, [configKey]: servers }, alreadyExists };
47
+ }
48
+
49
+ export async function writeJsonConfig(
50
+ filePath: string,
51
+ config: Record<string, unknown>
52
+ ): Promise<void> {
53
+ await fs.mkdirp(path.dirname(filePath));
54
+ await fs.writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`);
55
+ }
56
+
57
+ export async function writeJsonServerEntry(
58
+ filePath: string,
59
+ configKey: string,
60
+ serverName: string,
61
+ entry: Record<string, unknown>
62
+ ): Promise<{ status: 'configured' | 'reconfigured' }> {
63
+ let raw: string | undefined;
64
+ try {
65
+ raw = await fs.readFile(filePath, 'utf-8');
66
+ } catch (error) {
67
+ if (!isEnoent(error)) {
68
+ throw error;
69
+ }
70
+ }
71
+ if (raw === undefined || raw.trim() === '') {
72
+ const { config } = mergeServerEntry({}, configKey, serverName, entry);
73
+ await writeJsonConfig(filePath, config);
74
+ return { status: 'configured' };
75
+ }
76
+ const errors: ParseError[] = [];
77
+ const parsed = parse(raw, errors, { allowTrailingComma: true });
78
+ if (errors.length > 0 || typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
79
+ throw new Error(`unable to parse existing config at ${filePath}`);
80
+ }
81
+ const section = parsed[configKey];
82
+ const sectionIsObject =
83
+ typeof section === 'object' && section !== null && !Array.isArray(section);
84
+ const alreadyExists = sectionIsObject && serverName in section;
85
+ const edits = sectionIsObject
86
+ ? modify(raw, [configKey, serverName], entry, {
87
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
88
+ })
89
+ : modify(
90
+ raw,
91
+ [configKey],
92
+ { [serverName]: entry },
93
+ {
94
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
95
+ }
96
+ );
97
+ await fs.mkdirp(path.dirname(filePath));
98
+ await fs.writeFile(filePath, applyEdits(raw, edits));
99
+ return { status: alreadyExists ? 'reconfigured' : 'configured' };
100
+ }
101
+
102
+ export async function resolveConfigPath(candidates: string[]): Promise<string> {
103
+ for (const candidate of candidates) {
104
+ if (await fs.pathExists(candidate)) {
105
+ return candidate;
106
+ }
107
+ }
108
+ const fallback = candidates[0];
109
+ if (fallback === undefined) {
110
+ throw new Error('no config path candidates');
111
+ }
112
+ return fallback;
113
+ }
114
+
115
+ export function upsertTomlServer(
116
+ content: string,
117
+ serverName: string,
118
+ entry: Record<string, string>
119
+ ): { content: string; alreadyExists: boolean } {
120
+ const header = `[mcp_servers.${serverName}]`;
121
+ const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
122
+ const headerPattern = new RegExp(
123
+ `^[ \\t]*\\[mcp_servers\\.${escapedName}\\][ \\t]*(?:#.*)?$`,
124
+ 'm'
125
+ );
126
+ const lines = Object.entries(entry).map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
127
+ const block = `${[header, ...lines].join('\n')}\n`;
128
+ const match = headerPattern.exec(content);
129
+ if (match === null) {
130
+ const separator = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n';
131
+ return { content: `${content}${separator}${block}`, alreadyExists: false };
132
+ }
133
+ const headerIndex = match.index + match[0].indexOf('[');
134
+ const rest = content.slice(headerIndex + header.length);
135
+ const nextTable = rest.search(/\n\s*\[/);
136
+ const end = nextTable === -1 ? content.length : headerIndex + header.length + nextTable + 1;
137
+ const before = content.slice(0, headerIndex);
138
+ const after = content.slice(end).replace(/^\n+/, '\n');
139
+ return { content: `${before}${block}${after}`, alreadyExists: true };
140
+ }
@@ -0,0 +1,113 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import {
5
+ SERVER_NAME,
6
+ type ClientDefinition,
7
+ type ClientName,
8
+ type Scope,
9
+ type SetupContext,
10
+ } from './clients.js';
11
+ import { resolveConfigPath, upsertTomlServer, writeJsonServerEntry } from './configFile.js';
12
+ import { appendRuleSection, writeRuleFile } from './rule.js';
13
+
14
+ export interface ClientSetupResult {
15
+ client: ClientName;
16
+ displayName: string;
17
+ mcpStatus: 'configured' | 'reconfigured' | 'failed';
18
+ mcpDetail: string;
19
+ ruleStatus: 'installed' | 'updated' | 'failed' | 'none';
20
+ ruleDetail: string;
21
+ }
22
+
23
+ async function writeMcpEntry(
24
+ definition: ClientDefinition,
25
+ scope: Scope,
26
+ ctx: SetupContext
27
+ ): Promise<{ status: 'configured' | 'reconfigured'; configPath: string }> {
28
+ const projectPaths = definition.projectConfigPaths?.(ctx) ?? [];
29
+ const candidates =
30
+ scope === 'project' && projectPaths.length > 0
31
+ ? projectPaths
32
+ : definition.globalConfigPaths(ctx);
33
+ const configPath = await resolveConfigPath(candidates);
34
+ if (definition.format === 'toml') {
35
+ let existing = '';
36
+ try {
37
+ existing = await fs.readFile(configPath, 'utf-8');
38
+ } catch (error) {
39
+ if (
40
+ !(typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
41
+ ) {
42
+ throw error;
43
+ }
44
+ }
45
+ const entry: Record<string, string> = {};
46
+ for (const [key, value] of Object.entries(definition.buildEntry())) {
47
+ if (typeof value === 'string') {
48
+ entry[key] = value;
49
+ }
50
+ }
51
+ const { content, alreadyExists } = upsertTomlServer(existing, SERVER_NAME, entry);
52
+ await fs.mkdirp(path.dirname(configPath));
53
+ await fs.writeFile(configPath, content);
54
+ return { status: alreadyExists ? 'reconfigured' : 'configured', configPath };
55
+ }
56
+ const { status } = await writeJsonServerEntry(
57
+ configPath,
58
+ definition.configKey,
59
+ SERVER_NAME,
60
+ definition.buildEntry()
61
+ );
62
+ return { status, configPath };
63
+ }
64
+
65
+ async function writeRule(
66
+ definition: ClientDefinition,
67
+ scope: Scope,
68
+ ctx: SetupContext
69
+ ): Promise<{ status: 'installed' | 'updated' | 'none'; rulePath: string }> {
70
+ const rule = definition.rule;
71
+ if (rule === undefined) {
72
+ return { status: 'none', rulePath: '' };
73
+ }
74
+ const projectPath = rule.projectPath?.(ctx);
75
+ const rulePath =
76
+ scope === 'project' && projectPath !== undefined ? projectPath : rule.globalPath(ctx);
77
+ const status =
78
+ rule.kind === 'file'
79
+ ? await writeRuleFile(rulePath, rule.content)
80
+ : await appendRuleSection(rulePath, rule.content);
81
+ return { status, rulePath };
82
+ }
83
+
84
+ export async function setupClient(
85
+ definition: ClientDefinition,
86
+ scope: Scope,
87
+ ctx: SetupContext
88
+ ): Promise<ClientSetupResult> {
89
+ const result: ClientSetupResult = {
90
+ client: definition.name,
91
+ displayName: definition.displayName,
92
+ mcpStatus: 'failed',
93
+ mcpDetail: '',
94
+ ruleStatus: 'none',
95
+ ruleDetail: '',
96
+ };
97
+ try {
98
+ const { status, configPath } = await writeMcpEntry(definition, scope, ctx);
99
+ result.mcpStatus = status;
100
+ result.mcpDetail = configPath;
101
+ } catch (error) {
102
+ result.mcpDetail = error instanceof Error ? error.message : String(error);
103
+ }
104
+ try {
105
+ const { status, rulePath } = await writeRule(definition, scope, ctx);
106
+ result.ruleStatus = status;
107
+ result.ruleDetail = rulePath;
108
+ } catch (error) {
109
+ result.ruleStatus = 'failed';
110
+ result.ruleDetail = error instanceof Error ? error.message : String(error);
111
+ }
112
+ return result;
113
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export const RULE_MARKER = '<!-- mintlify-index -->';
5
+
6
+ export const RULE_BODY = `Use the Mintlify index \`context\` tool whenever you research how to use a library, framework, SDK, API, or CLI tool, including syntax, configuration, migration, and setup questions. Use it even for well-known libraries, since training data may be stale, and prefer it over web search for developer documentation. Do not use it for general programming concepts or for debugging business logic.
7
+ `;
8
+
9
+ export const CURSOR_RULE = `---
10
+ alwaysApply: true
11
+ ---
12
+
13
+ ${RULE_BODY}`;
14
+
15
+ export const VSCODE_RULE = `---
16
+ applyTo: '**'
17
+ ---
18
+
19
+ ${RULE_BODY}`;
20
+
21
+ export async function writeRuleFile(
22
+ filePath: string,
23
+ content: string
24
+ ): Promise<'installed' | 'updated'> {
25
+ const exists = await fs.pathExists(filePath);
26
+ await fs.mkdirp(path.dirname(filePath));
27
+ await fs.writeFile(filePath, content);
28
+ return exists ? 'updated' : 'installed';
29
+ }
30
+
31
+ export async function appendRuleSection(
32
+ filePath: string,
33
+ content: string
34
+ ): Promise<'installed' | 'updated'> {
35
+ const section = `${RULE_MARKER}\n${content}${RULE_MARKER}`;
36
+ let existing = '';
37
+ try {
38
+ existing = await fs.readFile(filePath, 'utf-8');
39
+ } catch (error) {
40
+ if (
41
+ !(typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
42
+ ) {
43
+ throw error;
44
+ }
45
+ }
46
+ const escaped = RULE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ const pattern = new RegExp(`${escaped}\\n[\\s\\S]*?${escaped}`);
48
+ if (pattern.test(existing)) {
49
+ await fs.writeFile(filePath, existing.replace(pattern, section));
50
+ return 'updated';
51
+ }
52
+ const separator = existing.length === 0 ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
53
+ await fs.mkdirp(path.dirname(filePath));
54
+ await fs.writeFile(filePath, `${existing}${separator}${section}\n`);
55
+ return 'installed';
56
+ }