@mintlify/cli 4.0.1067 → 4.0.1068

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.1068",
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": "3401569a70e020cc6f08559f5f6feed9715c8324"
91
92
  }
package/src/cli.tsx CHANGED
@@ -24,6 +24,7 @@ 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
30
  checkForMintJson,
@@ -39,17 +40,34 @@ import { init } from './init.js';
39
40
  import { mdxLinter } from './mdxLinter.js';
40
41
  import { migrateMdx } from './migrateMdx.js';
41
42
  import { scrapeSite, scrapePage, scrapeOpenApi } from './scrape.js';
43
+ import { createTelemetryMiddleware } from './telemetry/middleware.js';
44
+ import { trackTelemetryPreferenceChange } from './telemetry/track.js';
42
45
  import { update } from './update.js';
43
46
  import { addWorkflow } from './workflow.js';
44
47
 
45
48
  export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
49
+ const telemetryMiddleware = createTelemetryMiddleware();
46
50
  render(<Logs />);
47
51
 
48
52
  return (
49
53
  yargs(hideBin(process.argv))
50
54
  .scriptName(packageName)
55
+ .option('telemetry', {
56
+ type: 'boolean',
57
+ alias: 't',
58
+ description: 'Enable or disable anonymous usage telemetry',
59
+ })
60
+ .middleware(async (argv) => {
61
+ if (argv.telemetry !== undefined && argv._.length === 0) {
62
+ await setTelemetryEnabled(argv.telemetry);
63
+ await trackTelemetryPreferenceChange({ enabled: argv.telemetry });
64
+ addLog(<SuccessLog message={`telemetry ${argv.telemetry ? 'enabled' : 'disabled'}`} />);
65
+ await terminate(0);
66
+ }
67
+ }, true)
51
68
  .middleware(checkNodeVersion)
52
69
  .middleware(suppressConsoleWarnings)
70
+ .middleware(telemetryMiddleware)
53
71
  .command(
54
72
  'dev',
55
73
  'initialize a local preview environment',
@@ -571,6 +589,6 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
571
589
  .alias('h', 'help')
572
590
  .alias('v', 'version')
573
591
 
574
- .parse()
592
+ .parseAsync()
575
593
  );
576
594
  };
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
@@ -23,6 +23,7 @@ import type { ArgumentsCamelCase } from 'yargs';
23
23
  import yargs from 'yargs';
24
24
 
25
25
  import { CMD_EXEC_PATH } from './constants.js';
26
+ import { shutdownPostHog } from './telemetry/client.js';
26
27
 
27
28
  export const checkPort = async (argv: ArgumentsCamelCase): Promise<number | undefined> => {
28
29
  const initialPort = typeof argv.port === 'number' ? argv.port : 3000;
@@ -183,6 +184,7 @@ export const readLocalOpenApiFile = async (
183
184
  export const terminate = async (code: number) => {
184
185
  // Wait for the logs to be fully rendered before exiting
185
186
  await new Promise((resolve) => setTimeout(resolve, 50));
187
+ await shutdownPostHog();
186
188
  process.exit(code);
187
189
  };
188
190
 
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
  });