@mintlify/cli 4.0.1067 → 4.0.1069

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.1067",
3
+ "version": "4.0.1069",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -62,6 +62,7 @@
62
62
  "inquirer": "12.3.0",
63
63
  "js-yaml": "4.1.0",
64
64
  "mdast-util-mdx-jsx": "3.2.0",
65
+ "posthog-node": "5.17.2",
65
66
  "react": "19.2.3",
66
67
  "semver": "7.7.2",
67
68
  "unist-util-visit": "5.0.0",
@@ -87,5 +88,5 @@
87
88
  "vitest": "2.1.9",
88
89
  "vitest-mock-process": "1.0.4"
89
90
  },
90
- "gitHead": "330a8577c46d19e4581c1bf47fc31329d617eda3"
91
+ "gitHead": "10c9be1d7e8d3ccf3f86df9498a1e334090b401e"
91
92
  }
package/src/cli.tsx CHANGED
@@ -24,12 +24,11 @@ import yargs from 'yargs';
24
24
  import { hideBin } from 'yargs/helpers';
25
25
 
26
26
  import { accessibilityCheck } from './accessibilityCheck.js';
27
+ import { setTelemetryEnabled } from './config.js';
27
28
  import {
28
29
  checkPort,
29
- checkForMintJson,
30
30
  checkNodeVersion,
31
- upgradeConfig,
32
- checkForDocsJson,
31
+ autoUpgradeIfNeeded,
33
32
  getVersions,
34
33
  suppressConsoleWarnings,
35
34
  terminate,
@@ -39,17 +38,34 @@ import { init } from './init.js';
39
38
  import { mdxLinter } from './mdxLinter.js';
40
39
  import { migrateMdx } from './migrateMdx.js';
41
40
  import { scrapeSite, scrapePage, scrapeOpenApi } from './scrape.js';
41
+ import { createTelemetryMiddleware } from './telemetry/middleware.js';
42
+ import { trackTelemetryPreferenceChange } from './telemetry/track.js';
42
43
  import { update } from './update.js';
43
44
  import { addWorkflow } from './workflow.js';
44
45
 
45
46
  export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
47
+ const telemetryMiddleware = createTelemetryMiddleware();
46
48
  render(<Logs />);
47
49
 
48
50
  return (
49
51
  yargs(hideBin(process.argv))
50
52
  .scriptName(packageName)
53
+ .option('telemetry', {
54
+ type: 'boolean',
55
+ alias: 't',
56
+ description: 'Enable or disable anonymous usage telemetry',
57
+ })
58
+ .middleware(async (argv) => {
59
+ if (argv.telemetry !== undefined && argv._.length === 0) {
60
+ await setTelemetryEnabled(argv.telemetry);
61
+ await trackTelemetryPreferenceChange({ enabled: argv.telemetry });
62
+ addLog(<SuccessLog message={`telemetry ${argv.telemetry ? 'enabled' : 'disabled'}`} />);
63
+ await terminate(0);
64
+ }
65
+ }, true)
51
66
  .middleware(checkNodeVersion)
52
67
  .middleware(suppressConsoleWarnings)
68
+ .middleware(telemetryMiddleware)
53
69
  .command(
54
70
  'dev',
55
71
  'initialize a local preview environment',
@@ -91,6 +107,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
91
107
  .example('mintlify dev', 'run with default settings (opens in browser)')
92
108
  .example('mintlify dev --no-open', 'run without opening in browser'),
93
109
  async (argv) => {
110
+ await autoUpgradeIfNeeded();
94
111
  const port = await checkPort(argv);
95
112
  const { cli: cliVersion } = getVersions();
96
113
  if (port != undefined) {
@@ -255,11 +272,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
255
272
  description: 'also check links inside <Snippet> components',
256
273
  }),
257
274
  async (argv) => {
258
- const hasMintJson = await checkForMintJson();
259
- if (!hasMintJson) {
260
- await checkForDocsJson();
261
- }
262
-
275
+ await autoUpgradeIfNeeded();
263
276
  addLog(<SpinnerLog message="checking for broken links..." />);
264
277
  try {
265
278
  const graph = await buildGraph(undefined, {
@@ -337,10 +350,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
337
350
  })
338
351
  .epilog('example: `mintlify rename introduction.mdx overview.mdx`'),
339
352
  async ({ from, to, force }) => {
340
- const hasMintJson = await checkForMintJson();
341
- if (!hasMintJson) {
342
- await checkForDocsJson();
343
- }
353
+ await autoUpgradeIfNeeded();
344
354
  await renameFilesAndUpdateLinksInContent(from, to, force);
345
355
  await terminate(0);
346
356
  }
@@ -354,18 +364,6 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
354
364
  await terminate(0);
355
365
  }
356
366
  )
357
- .command(
358
- 'upgrade',
359
- 'upgrade mint.json file to docs.json (current format)',
360
- () => undefined,
361
- async () => {
362
- const hasMintJson = await checkForMintJson();
363
- if (!hasMintJson) {
364
- await checkForDocsJson();
365
- }
366
- await upgradeConfig();
367
- }
368
- )
369
367
  .command(
370
368
  'migrate-mdx',
371
369
  'migrate mdx openapi endpoint pages to x-mint extensions and docs.json',
@@ -571,6 +569,6 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
571
569
  .alias('h', 'help')
572
570
  .alias('v', 'version')
573
571
 
574
- .parse()
572
+ .parseAsync()
575
573
  );
576
574
  };
package/src/config.ts ADDED
@@ -0,0 +1,34 @@
1
+ import fs from 'fs';
2
+ import { ensureDir } from 'fs-extra';
3
+
4
+ import { CLI_CONFIG_FILE, CONFIG_DIR } from './constants.js';
5
+
6
+ interface MintlifyConfig {
7
+ telemetryEnabled?: boolean;
8
+ }
9
+
10
+ function readConfig(): MintlifyConfig {
11
+ try {
12
+ const raw = fs.readFileSync(CLI_CONFIG_FILE, 'utf-8');
13
+ return JSON.parse(raw) as MintlifyConfig;
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ async function writeConfig(updates: Partial<MintlifyConfig>): Promise<void> {
20
+ await ensureDir(CONFIG_DIR);
21
+ const existing = readConfig();
22
+ fs.writeFileSync(CLI_CONFIG_FILE, JSON.stringify({ ...existing, ...updates }, null, 2));
23
+ }
24
+
25
+ export function isTelemetryEnabled(): boolean {
26
+ if (process.env.CLI_TEST_MODE === 'true') return false;
27
+ if (process.env.MINTLIFY_TELEMETRY_DISABLED === '1') return false;
28
+ if (process.env.DO_NOT_TRACK === '1') return false;
29
+ return readConfig().telemetryEnabled !== false;
30
+ }
31
+
32
+ export async function setTelemetryEnabled(enabled: boolean): Promise<void> {
33
+ await writeConfig({ telemetryEnabled: enabled });
34
+ }
package/src/constants.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import os from 'os';
2
+ import path from 'path';
2
3
 
3
4
  export const HOME_DIR = os.homedir();
4
5
  export const CMD_EXEC_PATH = process.cwd();
6
+
7
+ export const CONFIG_DIR = path.join(HOME_DIR, '.config', 'mintlify');
8
+ export const CLI_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ export const TELEMETRY_ASYNC_TIMEOUT_MS = 10_000;
package/src/helpers.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  SpinnerLog,
10
10
  removeLastLog,
11
11
  LOCAL_LINKED_CLI_VERSION,
12
+ WarningLog,
12
13
  } from '@mintlify/previewing';
13
14
  import { upgradeToDocsConfig, validatePathWithinCwd } from '@mintlify/validation';
14
15
  import detect from 'detect-port';
@@ -23,6 +24,7 @@ import type { ArgumentsCamelCase } from 'yargs';
23
24
  import yargs from 'yargs';
24
25
 
25
26
  import { CMD_EXEC_PATH } from './constants.js';
27
+ import { shutdownPostHog } from './telemetry/client.js';
26
28
 
27
29
  export const checkPort = async (argv: ArgumentsCamelCase): Promise<number | undefined> => {
28
30
  const initialPort = typeof argv.port === 'number' ? argv.port : 3000;
@@ -93,6 +95,19 @@ export const checkForDocsJson = async () => {
93
95
  }
94
96
  };
95
97
 
98
+ export const autoUpgradeIfNeeded = async () => {
99
+ const hasMintJson = await checkForMintJson();
100
+ if (!hasMintJson) return;
101
+
102
+ const docsJsonPath = path.join(CMD_EXEC_PATH, 'docs.json');
103
+ const hasDocsJson = await fse.pathExists(docsJsonPath);
104
+ if (!hasDocsJson) {
105
+ addLog(<WarningLog message="Legacy mint.json detected, auto-upgrading to docs.json" />);
106
+ addLog(<SpinnerLog message="upgrading mint.json to docs.json..." />);
107
+ await upgradeConfig();
108
+ }
109
+ };
110
+
96
111
  export const upgradeConfig = async () => {
97
112
  try {
98
113
  const mintJsonPath = path.join(CMD_EXEC_PATH, 'mint.json');
@@ -183,6 +198,7 @@ export const readLocalOpenApiFile = async (
183
198
  export const terminate = async (code: number) => {
184
199
  // Wait for the logs to be fully rendered before exiting
185
200
  await new Promise((resolve) => setTimeout(resolve, 50));
201
+ await shutdownPostHog();
186
202
  process.exit(code);
187
203
  };
188
204
 
package/src/start.ts CHANGED
@@ -1,6 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { cli } from './cli.js';
3
+ import { shutdownPostHog } from './telemetry/client.js';
3
4
 
4
5
  const packageName = process.env.MINTLIFY_PACKAGE_NAME ?? 'mint';
5
6
 
6
- void cli({ packageName });
7
+ cli({ packageName }).catch((err: unknown) => {
8
+ console.error(err instanceof Error ? err.message : err);
9
+ process.exit(1);
10
+ });
11
+
12
+ async function shutdown(exitCode: number): Promise<never> {
13
+ await shutdownPostHog();
14
+ process.exit(exitCode);
15
+ }
16
+
17
+ process.once('beforeExit', async () => {
18
+ try {
19
+ await shutdownPostHog();
20
+ } catch {}
21
+ });
22
+ process.on('SIGINT', () => {
23
+ void shutdown(130);
24
+ });
25
+ process.on('SIGTERM', () => {
26
+ void shutdown(143);
27
+ });
@@ -0,0 +1,31 @@
1
+ import { PostHog } from 'posthog-node';
2
+
3
+ import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../constants.js';
4
+
5
+ const POSTHOG_API_KEY = 'phc_eNuN6Ojnk9O7uWfC17z12AK85fNR0BY6IiGVy0Gfwzw';
6
+ const POSTHOG_HOST = 'https://ph.mintlify.com';
7
+
8
+ let client: PostHog | null = null;
9
+
10
+ export function getPostHogClient(): PostHog {
11
+ if (!client) {
12
+ client = new PostHog(POSTHOG_API_KEY, {
13
+ host: POSTHOG_HOST,
14
+ });
15
+ }
16
+ return client;
17
+ }
18
+
19
+ export async function shutdownPostHog(): Promise<void> {
20
+ if (!client) return;
21
+ try {
22
+ await Promise.race([
23
+ client.shutdown(),
24
+ new Promise<void>((resolve) => {
25
+ const t = setTimeout(resolve, TELEMETRY_ASYNC_TIMEOUT_MS);
26
+ t.unref();
27
+ }),
28
+ ]);
29
+ } catch {}
30
+ client = null;
31
+ }
@@ -0,0 +1,62 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import { CONFIG_DIR } from '../constants.js';
7
+
8
+ const ID_FILE = path.join(CONFIG_DIR, 'anonymous-id');
9
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10
+
11
+ function readValidIdFromFile(): string | null {
12
+ try {
13
+ const raw = fs.readFileSync(ID_FILE, 'utf-8').trim();
14
+ if (UUID_RE.test(raw)) return raw;
15
+ } catch {}
16
+ return null;
17
+ }
18
+
19
+ function stableMachineDistinctId(): string {
20
+ const h = crypto.createHash('sha256');
21
+ h.update(os.hostname());
22
+ h.update('\0');
23
+ h.update(os.homedir());
24
+ h.update('\0');
25
+ h.update(process.env.USER ?? process.env.USERNAME ?? '');
26
+ const hex = h.digest('hex').slice(0, 32);
27
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
28
+ }
29
+
30
+ function tryPersistId(id: string): void {
31
+ fs.mkdirSync(path.dirname(ID_FILE), { recursive: true });
32
+ fs.writeFileSync(ID_FILE, id, { flag: 'wx' });
33
+ }
34
+
35
+ export function getDistinctId(): string {
36
+ const existing = readValidIdFromFile();
37
+ if (existing) return existing;
38
+
39
+ const randomId = crypto.randomUUID();
40
+
41
+ try {
42
+ tryPersistId(randomId);
43
+ return readValidIdFromFile() ?? randomId;
44
+ } catch (err) {
45
+ const code = (err as NodeJS.ErrnoException).code;
46
+ if (code === 'EEXIST') {
47
+ const concurrent = readValidIdFromFile();
48
+ if (concurrent) return concurrent;
49
+ try {
50
+ fs.writeFileSync(ID_FILE, randomId);
51
+ } catch {}
52
+ return readValidIdFromFile() ?? randomId;
53
+ }
54
+ }
55
+
56
+ const stable = stableMachineDistinctId();
57
+ try {
58
+ fs.mkdirSync(path.dirname(ID_FILE), { recursive: true });
59
+ fs.writeFileSync(ID_FILE, stable);
60
+ } catch {}
61
+ return readValidIdFromFile() ?? stable;
62
+ }
@@ -0,0 +1,29 @@
1
+ import { getVersions } from '../helpers.js';
2
+ import { trackCommand } from './track.js';
3
+
4
+ const SCRAPE_SUBCOMMANDS = new Set(['page', 'site', 'openapi']);
5
+
6
+ export function getSanitizedCommandForTelemetry(_: (string | number)[]): string {
7
+ const parts = _.filter((p): p is string => typeof p === 'string');
8
+ if (parts.length === 0) return '';
9
+ const first = parts[0]!;
10
+ const second = parts[1];
11
+ if (first === 'scrape' && second !== undefined && SCRAPE_SUBCOMMANDS.has(second)) {
12
+ return `scrape ${second}`;
13
+ }
14
+ return first;
15
+ }
16
+
17
+ export function createTelemetryMiddleware(): (argv: { _: (string | number)[] }) => Promise<void> {
18
+ let tracked = false;
19
+ return async function telemetryMiddleware(argv: { _: (string | number)[] }): Promise<void> {
20
+ const command = argv._[0];
21
+ if (typeof command === 'string') {
22
+ if (tracked) return;
23
+ tracked = true;
24
+ const sanitizedCommand = getSanitizedCommandForTelemetry(argv._);
25
+ const { cli: cliVersion } = getVersions();
26
+ void trackCommand({ command: sanitizedCommand, cliVersion });
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,60 @@
1
+ import os from 'os';
2
+
3
+ import { isTelemetryEnabled } from '../config.js';
4
+ import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../constants.js';
5
+ import { getVersions } from '../helpers.js';
6
+ import { getPostHogClient } from './client.js';
7
+ import { getDistinctId } from './distinctId.js';
8
+
9
+ export interface TrackCommandOptions {
10
+ command: string;
11
+ cliVersion?: string;
12
+ }
13
+
14
+ async function captureWithTimeout(
15
+ event: string,
16
+ properties: Record<string, unknown>
17
+ ): Promise<void> {
18
+ await Promise.race([
19
+ getPostHogClient()
20
+ .captureImmediate({
21
+ distinctId: getDistinctId(),
22
+ event,
23
+ properties,
24
+ })
25
+ .catch(() => {}),
26
+ new Promise<void>((resolve) => {
27
+ const t = setTimeout(resolve, TELEMETRY_ASYNC_TIMEOUT_MS);
28
+ t.unref();
29
+ }),
30
+ ]);
31
+ }
32
+
33
+ export async function trackCommand({ command, cliVersion }: TrackCommandOptions): Promise<void> {
34
+ if (!isTelemetryEnabled()) return;
35
+
36
+ try {
37
+ await captureWithTimeout('cli.command.executed', {
38
+ command,
39
+ cli_version: cliVersion,
40
+ os: os.platform(),
41
+ arch: os.arch(),
42
+ node_version: process.version,
43
+ });
44
+ } catch {}
45
+ }
46
+
47
+ export async function trackTelemetryPreferenceChange(options: { enabled: boolean }): Promise<void> {
48
+ if (process.env.CLI_TEST_MODE === 'true') return;
49
+
50
+ try {
51
+ const { cli: cliVersion } = getVersions();
52
+ await captureWithTimeout('cli.telemetry.preference_changed', {
53
+ enabled: options.enabled,
54
+ cli_version: cliVersion,
55
+ os: os.platform(),
56
+ arch: os.arch(),
57
+ node_version: process.version,
58
+ });
59
+ } catch {}
60
+ }
package/vitest.config.ts CHANGED
@@ -4,5 +4,6 @@ export default defineConfig({
4
4
  test: {
5
5
  globals: true,
6
6
  testTimeout: 10000,
7
+ exclude: ['bin/**', 'node_modules/**'],
7
8
  },
8
9
  });