@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/__test__/telemetry.test.ts +313 -0
- package/__test__/utils.ts +11 -6
- package/bin/cli.js +19 -1
- package/bin/config.js +42 -0
- package/bin/constants.js +4 -0
- package/bin/helpers.js +2 -0
- package/bin/start.js +32 -1
- package/bin/telemetry/client.js +39 -0
- package/bin/telemetry/distinctId.js +62 -0
- package/bin/telemetry/middleware.js +39 -0
- package/bin/telemetry/track.js +65 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/cli.tsx +19 -1
- package/src/config.ts +34 -0
- package/src/constants.ts +5 -0
- package/src/helpers.tsx +2 -0
- package/src/start.ts +22 -1
- package/src/telemetry/client.ts +31 -0
- package/src/telemetry/distinctId.ts +62 -0
- package/src/telemetry/middleware.ts +29 -0
- package/src/telemetry/track.ts +60 -0
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import * as fsExtra from 'fs-extra';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
import { isTelemetryEnabled, setTelemetryEnabled } from '../src/config.js';
|
|
6
|
+
import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../src/constants.js';
|
|
7
|
+
import { getDistinctId } from '../src/telemetry/distinctId.js';
|
|
8
|
+
import {
|
|
9
|
+
createTelemetryMiddleware,
|
|
10
|
+
getSanitizedCommandForTelemetry,
|
|
11
|
+
} from '../src/telemetry/middleware.js';
|
|
12
|
+
import * as trackModule from '../src/telemetry/track.js';
|
|
13
|
+
import { trackCommand, trackTelemetryPreferenceChange } from '../src/telemetry/track.js';
|
|
14
|
+
|
|
15
|
+
vi.mock('fs-extra', () => ({ ensureDir: vi.fn().mockResolvedValue(undefined) }));
|
|
16
|
+
|
|
17
|
+
const mockCaptureImmediate = vi.fn().mockResolvedValue(undefined);
|
|
18
|
+
|
|
19
|
+
vi.mock('../src/telemetry/client.js', () => ({
|
|
20
|
+
getPostHogClient: () => ({ captureImmediate: mockCaptureImmediate }),
|
|
21
|
+
shutdownPostHog: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('createTelemetryMiddleware', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.spyOn(trackModule, 'trackCommand').mockResolvedValue(undefined);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('only tracks once when middleware runs multiple times for the same parse', async () => {
|
|
34
|
+
const middleware = createTelemetryMiddleware();
|
|
35
|
+
await middleware({ _: ['dev'] });
|
|
36
|
+
await middleware({ _: ['dev'] });
|
|
37
|
+
expect(trackModule.trackCommand).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('tracks again for a new middleware instance (new cli invocation)', async () => {
|
|
41
|
+
await createTelemetryMiddleware()({ _: ['dev'] });
|
|
42
|
+
expect(trackModule.trackCommand).toHaveBeenCalledTimes(1);
|
|
43
|
+
await createTelemetryMiddleware()({ _: ['build'] });
|
|
44
|
+
expect(trackModule.trackCommand).toHaveBeenCalledTimes(2);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('getSanitizedCommandForTelemetry', () => {
|
|
49
|
+
it('includes known scrape subcommands', () => {
|
|
50
|
+
expect(getSanitizedCommandForTelemetry(['scrape', 'page', 'https://example.com'])).toBe(
|
|
51
|
+
'scrape page'
|
|
52
|
+
);
|
|
53
|
+
expect(getSanitizedCommandForTelemetry(['scrape', 'site', 'https://example.com'])).toBe(
|
|
54
|
+
'scrape site'
|
|
55
|
+
);
|
|
56
|
+
expect(getSanitizedCommandForTelemetry(['scrape', 'openapi', './spec.yaml'])).toBe(
|
|
57
|
+
'scrape openapi'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('strips positional args from all other commands', () => {
|
|
62
|
+
expect(getSanitizedCommandForTelemetry(['scrape', 'https://example.com'])).toBe('scrape');
|
|
63
|
+
expect(getSanitizedCommandForTelemetry(['openapi-check', 'https://swagger.io/spec.json'])).toBe(
|
|
64
|
+
'openapi-check'
|
|
65
|
+
);
|
|
66
|
+
expect(getSanitizedCommandForTelemetry(['rename', 'a.mdx', 'b.mdx'])).toBe('rename');
|
|
67
|
+
expect(getSanitizedCommandForTelemetry(['new', './my-docs'])).toBe('new');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('isTelemetryEnabled', () => {
|
|
72
|
+
const savedEnv = process.env;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
process.env = { ...savedEnv };
|
|
76
|
+
delete process.env.MINTLIFY_TELEMETRY_DISABLED;
|
|
77
|
+
delete process.env.DO_NOT_TRACK;
|
|
78
|
+
delete process.env.CLI_TEST_MODE;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
process.env = savedEnv;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is enabled by default when config file does not exist', () => {
|
|
86
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
87
|
+
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
88
|
+
});
|
|
89
|
+
expect(isTelemetryEnabled()).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('is disabled when config file has telemetryEnabled: false', () => {
|
|
93
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: false }));
|
|
94
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('is enabled when config file has telemetryEnabled: true', () => {
|
|
98
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: true }));
|
|
99
|
+
expect(isTelemetryEnabled()).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('is disabled via CLI_TEST_MODE=true', () => {
|
|
103
|
+
process.env.CLI_TEST_MODE = 'true';
|
|
104
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('is disabled via MINTLIFY_TELEMETRY_DISABLED=1', () => {
|
|
108
|
+
process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
|
|
109
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('is disabled via DO_NOT_TRACK=1', () => {
|
|
113
|
+
process.env.DO_NOT_TRACK = '1';
|
|
114
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('setTelemetryEnabled', () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
vi.mocked(fsExtra.ensureDir).mockResolvedValue(undefined);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('writes telemetryEnabled into config.json', async () => {
|
|
124
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
125
|
+
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
126
|
+
});
|
|
127
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
|
|
128
|
+
|
|
129
|
+
await setTelemetryEnabled(false);
|
|
130
|
+
|
|
131
|
+
expect(fsExtra.ensureDir).toHaveBeenCalledWith(expect.stringContaining('mintlify'));
|
|
132
|
+
expect(writeSpy).toHaveBeenCalledWith(
|
|
133
|
+
expect.stringContaining('config.json'),
|
|
134
|
+
JSON.stringify({ telemetryEnabled: false }, null, 2)
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('merges with existing config fields', async () => {
|
|
139
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ someOtherSetting: true }));
|
|
140
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
|
|
141
|
+
|
|
142
|
+
await setTelemetryEnabled(false);
|
|
143
|
+
|
|
144
|
+
expect(writeSpy).toHaveBeenCalledWith(
|
|
145
|
+
expect.stringContaining('config.json'),
|
|
146
|
+
JSON.stringify({ someOtherSetting: true, telemetryEnabled: false }, null, 2)
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('getDistinctId', () => {
|
|
152
|
+
it('returns the persisted UUID', () => {
|
|
153
|
+
const id = '550e8400-e29b-41d4-a716-446655440000';
|
|
154
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(id);
|
|
155
|
+
expect(getDistinctId()).toBe(id);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('generates and persists a new UUID when none exists', () => {
|
|
159
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
160
|
+
throw new Error('ENOENT');
|
|
161
|
+
});
|
|
162
|
+
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
|
|
163
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
|
|
164
|
+
|
|
165
|
+
const id = getDistinctId();
|
|
166
|
+
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
167
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('anonymous-id'), id, {
|
|
168
|
+
flag: 'wx',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('overwrites anonymous-id when it exists but is not a valid UUID', () => {
|
|
173
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue('not-a-uuid');
|
|
174
|
+
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
|
|
175
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation((...args) => {
|
|
176
|
+
const opts = args[2] as { flag?: string } | undefined;
|
|
177
|
+
if (opts?.flag === 'wx') {
|
|
178
|
+
throw Object.assign(new Error('EEXIST'), { code: 'EEXIST' });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const id = getDistinctId();
|
|
183
|
+
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
184
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('anonymous-id'), id, {
|
|
185
|
+
flag: 'wx',
|
|
186
|
+
});
|
|
187
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('anonymous-id'), id);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('reuses the same id when the file cannot be persisted (stable machine fallback)', () => {
|
|
191
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
192
|
+
throw new Error('ENOENT');
|
|
193
|
+
});
|
|
194
|
+
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
|
|
195
|
+
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {
|
|
196
|
+
throw new Error('EACCES');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const a = getDistinctId();
|
|
200
|
+
const b = getDistinctId();
|
|
201
|
+
expect(a).toBe(b);
|
|
202
|
+
expect(a).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('trackCommand', () => {
|
|
207
|
+
const savedEnv = process.env;
|
|
208
|
+
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
vi.clearAllMocks();
|
|
211
|
+
mockCaptureImmediate.mockResolvedValue(undefined);
|
|
212
|
+
process.env = { ...savedEnv };
|
|
213
|
+
delete process.env.CLI_TEST_MODE;
|
|
214
|
+
delete process.env.MINTLIFY_TELEMETRY_DISABLED;
|
|
215
|
+
delete process.env.DO_NOT_TRACK;
|
|
216
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: true }));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
afterEach(() => {
|
|
220
|
+
process.env = savedEnv;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('captures event when telemetry is enabled', async () => {
|
|
224
|
+
await trackCommand({ command: 'dev', cliVersion: '1.0.0' });
|
|
225
|
+
|
|
226
|
+
expect(mockCaptureImmediate).toHaveBeenCalledWith(
|
|
227
|
+
expect.objectContaining({
|
|
228
|
+
event: 'cli.command.executed',
|
|
229
|
+
properties: expect.objectContaining({
|
|
230
|
+
command: 'dev',
|
|
231
|
+
cli_version: '1.0.0',
|
|
232
|
+
os: os.platform(),
|
|
233
|
+
}),
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('does not capture when config has telemetryEnabled: false', async () => {
|
|
239
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: false }));
|
|
240
|
+
await trackCommand({ command: 'dev' });
|
|
241
|
+
expect(mockCaptureImmediate).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not capture when MINTLIFY_TELEMETRY_DISABLED is set', async () => {
|
|
245
|
+
process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
|
|
246
|
+
await trackCommand({ command: 'dev' });
|
|
247
|
+
expect(mockCaptureImmediate).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('resolves when captureImmediate hangs after timeout', async () => {
|
|
251
|
+
mockCaptureImmediate.mockImplementation(() => new Promise(() => {}));
|
|
252
|
+
vi.useFakeTimers();
|
|
253
|
+
try {
|
|
254
|
+
const done = trackCommand({ command: 'dev' });
|
|
255
|
+
await vi.advanceTimersByTimeAsync(TELEMETRY_ASYNC_TIMEOUT_MS);
|
|
256
|
+
await expect(done).resolves.toBeUndefined();
|
|
257
|
+
} finally {
|
|
258
|
+
vi.useRealTimers();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('trackTelemetryPreferenceChange', () => {
|
|
264
|
+
const savedEnv = process.env;
|
|
265
|
+
|
|
266
|
+
beforeEach(() => {
|
|
267
|
+
vi.clearAllMocks();
|
|
268
|
+
mockCaptureImmediate.mockResolvedValue(undefined);
|
|
269
|
+
process.env = { ...savedEnv };
|
|
270
|
+
delete process.env.CLI_TEST_MODE;
|
|
271
|
+
delete process.env.MINTLIFY_TELEMETRY_DISABLED;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
afterEach(() => {
|
|
275
|
+
process.env = savedEnv;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('captures cli.telemetry.preference_changed when enabling', async () => {
|
|
279
|
+
await trackTelemetryPreferenceChange({ enabled: true });
|
|
280
|
+
|
|
281
|
+
expect(mockCaptureImmediate).toHaveBeenCalledWith(
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
event: 'cli.telemetry.preference_changed',
|
|
284
|
+
properties: expect.objectContaining({ enabled: true, os: os.platform() }),
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('captures cli.telemetry.preference_changed when disabling', async () => {
|
|
290
|
+
await trackTelemetryPreferenceChange({ enabled: false });
|
|
291
|
+
|
|
292
|
+
expect(mockCaptureImmediate).toHaveBeenCalledWith(
|
|
293
|
+
expect.objectContaining({
|
|
294
|
+
event: 'cli.telemetry.preference_changed',
|
|
295
|
+
properties: expect.objectContaining({ enabled: false, os: os.platform() }),
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('fires even when MINTLIFY_TELEMETRY_DISABLED is set (captures opt-out)', async () => {
|
|
301
|
+
process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
|
|
302
|
+
await trackTelemetryPreferenceChange({ enabled: false });
|
|
303
|
+
expect(mockCaptureImmediate).toHaveBeenCalledWith(
|
|
304
|
+
expect.objectContaining({ event: 'cli.telemetry.preference_changed' })
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('does not capture when CLI_TEST_MODE is set', async () => {
|
|
309
|
+
process.env.CLI_TEST_MODE = 'true';
|
|
310
|
+
await trackTelemetryPreferenceChange({ enabled: true });
|
|
311
|
+
expect(mockCaptureImmediate).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
});
|
package/__test__/utils.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { cli } from '../src/cli.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Programmatically set arguments and execute the CLI script
|
|
5
|
-
*
|
|
6
|
-
* @param {...string} args - Additional command arguments.
|
|
7
|
-
*/
|
|
8
3
|
export async function runCommand(...args: string[]) {
|
|
4
|
+
const prevCliTestMode = process.env.CLI_TEST_MODE;
|
|
5
|
+
process.env.CLI_TEST_MODE = 'true';
|
|
9
6
|
process.argv = ['node', 'cli.js', ...args];
|
|
10
|
-
|
|
7
|
+
try {
|
|
8
|
+
return await cli({ packageName: 'mint' });
|
|
9
|
+
} finally {
|
|
10
|
+
if (prevCliTestMode === undefined) {
|
|
11
|
+
delete process.env.CLI_TEST_MODE;
|
|
12
|
+
} else {
|
|
13
|
+
process.env.CLI_TEST_MODE = prevCliTestMode;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export const mockValidOpenApiDocument = {
|
package/bin/cli.js
CHANGED
|
@@ -17,19 +17,37 @@ import path from 'path';
|
|
|
17
17
|
import yargs from 'yargs';
|
|
18
18
|
import { hideBin } from 'yargs/helpers';
|
|
19
19
|
import { accessibilityCheck } from './accessibilityCheck.js';
|
|
20
|
+
import { setTelemetryEnabled } from './config.js';
|
|
20
21
|
import { checkPort, checkForMintJson, checkNodeVersion, upgradeConfig, checkForDocsJson, getVersions, suppressConsoleWarnings, terminate, readLocalOpenApiFile, } from './helpers.js';
|
|
21
22
|
import { init } from './init.js';
|
|
22
23
|
import { mdxLinter } from './mdxLinter.js';
|
|
23
24
|
import { migrateMdx } from './migrateMdx.js';
|
|
24
25
|
import { scrapeSite, scrapePage, scrapeOpenApi } from './scrape.js';
|
|
26
|
+
import { createTelemetryMiddleware } from './telemetry/middleware.js';
|
|
27
|
+
import { trackTelemetryPreferenceChange } from './telemetry/track.js';
|
|
25
28
|
import { update } from './update.js';
|
|
26
29
|
import { addWorkflow } from './workflow.js';
|
|
27
30
|
export const cli = ({ packageName = 'mint' }) => {
|
|
31
|
+
const telemetryMiddleware = createTelemetryMiddleware();
|
|
28
32
|
render(_jsx(Logs, {}));
|
|
29
33
|
return (yargs(hideBin(process.argv))
|
|
30
34
|
.scriptName(packageName)
|
|
35
|
+
.option('telemetry', {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
alias: 't',
|
|
38
|
+
description: 'Enable or disable anonymous usage telemetry',
|
|
39
|
+
})
|
|
40
|
+
.middleware((argv) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
if (argv.telemetry !== undefined && argv._.length === 0) {
|
|
42
|
+
yield setTelemetryEnabled(argv.telemetry);
|
|
43
|
+
yield trackTelemetryPreferenceChange({ enabled: argv.telemetry });
|
|
44
|
+
addLog(_jsx(SuccessLog, { message: `telemetry ${argv.telemetry ? 'enabled' : 'disabled'}` }));
|
|
45
|
+
yield terminate(0);
|
|
46
|
+
}
|
|
47
|
+
}), true)
|
|
31
48
|
.middleware(checkNodeVersion)
|
|
32
49
|
.middleware(suppressConsoleWarnings)
|
|
50
|
+
.middleware(telemetryMiddleware)
|
|
33
51
|
.command('dev', 'initialize a local preview environment', (yargs) => yargs
|
|
34
52
|
.option('open', {
|
|
35
53
|
type: 'boolean',
|
|
@@ -412,5 +430,5 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
412
430
|
// Alias option flags --help = -h, default --version = -v
|
|
413
431
|
.alias('h', 'help')
|
|
414
432
|
.alias('v', 'version')
|
|
415
|
-
.
|
|
433
|
+
.parseAsync());
|
|
416
434
|
};
|
package/bin/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import { ensureDir } from 'fs-extra';
|
|
12
|
+
import { CLI_CONFIG_FILE, CONFIG_DIR } from './constants.js';
|
|
13
|
+
function readConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(CLI_CONFIG_FILE, 'utf-8');
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch (_a) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeConfig(updates) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
yield ensureDir(CONFIG_DIR);
|
|
25
|
+
const existing = readConfig();
|
|
26
|
+
fs.writeFileSync(CLI_CONFIG_FILE, JSON.stringify(Object.assign(Object.assign({}, existing), updates), null, 2));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function isTelemetryEnabled() {
|
|
30
|
+
if (process.env.CLI_TEST_MODE === 'true')
|
|
31
|
+
return false;
|
|
32
|
+
if (process.env.MINTLIFY_TELEMETRY_DISABLED === '1')
|
|
33
|
+
return false;
|
|
34
|
+
if (process.env.DO_NOT_TRACK === '1')
|
|
35
|
+
return false;
|
|
36
|
+
return readConfig().telemetryEnabled !== false;
|
|
37
|
+
}
|
|
38
|
+
export function setTelemetryEnabled(enabled) {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
yield writeConfig({ telemetryEnabled: enabled });
|
|
41
|
+
});
|
|
42
|
+
}
|
package/bin/constants.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
export const HOME_DIR = os.homedir();
|
|
3
4
|
export const CMD_EXEC_PATH = process.cwd();
|
|
5
|
+
export const CONFIG_DIR = path.join(HOME_DIR, '.config', 'mintlify');
|
|
6
|
+
export const CLI_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
export const TELEMETRY_ASYNC_TIMEOUT_MS = 10000;
|
package/bin/helpers.js
CHANGED
|
@@ -22,6 +22,7 @@ import { promisify } from 'node:util';
|
|
|
22
22
|
import path from 'path';
|
|
23
23
|
import yargs from 'yargs';
|
|
24
24
|
import { CMD_EXEC_PATH } from './constants.js';
|
|
25
|
+
import { shutdownPostHog } from './telemetry/client.js';
|
|
25
26
|
export const checkPort = (argv) => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
27
|
const initialPort = typeof argv.port === 'number' ? argv.port : 3000;
|
|
27
28
|
if (initialPort === (yield detect(initialPort)))
|
|
@@ -157,6 +158,7 @@ export const readLocalOpenApiFile = (filename) => __awaiter(void 0, void 0, void
|
|
|
157
158
|
export const terminate = (code) => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
159
|
// Wait for the logs to be fully rendered before exiting
|
|
159
160
|
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
161
|
+
yield shutdownPostHog();
|
|
160
162
|
process.exit(code);
|
|
161
163
|
});
|
|
162
164
|
export const execAsync = promisify(exec);
|
package/bin/start.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
var _a;
|
|
3
12
|
import { cli } from './cli.js';
|
|
13
|
+
import { shutdownPostHog } from './telemetry/client.js';
|
|
4
14
|
const packageName = (_a = process.env.MINTLIFY_PACKAGE_NAME) !== null && _a !== void 0 ? _a : 'mint';
|
|
5
|
-
|
|
15
|
+
cli({ packageName }).catch((err) => {
|
|
16
|
+
console.error(err instanceof Error ? err.message : err);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
function shutdown(exitCode) {
|
|
20
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
yield shutdownPostHog();
|
|
22
|
+
process.exit(exitCode);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
process.once('beforeExit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
try {
|
|
27
|
+
yield shutdownPostHog();
|
|
28
|
+
}
|
|
29
|
+
catch (_a) { }
|
|
30
|
+
}));
|
|
31
|
+
process.on('SIGINT', () => {
|
|
32
|
+
void shutdown(130);
|
|
33
|
+
});
|
|
34
|
+
process.on('SIGTERM', () => {
|
|
35
|
+
void shutdown(143);
|
|
36
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { PostHog } from 'posthog-node';
|
|
11
|
+
import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../constants.js';
|
|
12
|
+
const POSTHOG_API_KEY = 'phc_eNuN6Ojnk9O7uWfC17z12AK85fNR0BY6IiGVy0Gfwzw';
|
|
13
|
+
const POSTHOG_HOST = 'https://ph.mintlify.com';
|
|
14
|
+
let client = null;
|
|
15
|
+
export function getPostHogClient() {
|
|
16
|
+
if (!client) {
|
|
17
|
+
client = new PostHog(POSTHOG_API_KEY, {
|
|
18
|
+
host: POSTHOG_HOST,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return client;
|
|
22
|
+
}
|
|
23
|
+
export function shutdownPostHog() {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
if (!client)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
yield Promise.race([
|
|
29
|
+
client.shutdown(),
|
|
30
|
+
new Promise((resolve) => {
|
|
31
|
+
const t = setTimeout(resolve, TELEMETRY_ASYNC_TIMEOUT_MS);
|
|
32
|
+
t.unref();
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
catch (_a) { }
|
|
37
|
+
client = null;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -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
|
+
import { CONFIG_DIR } from '../constants.js';
|
|
6
|
+
const ID_FILE = path.join(CONFIG_DIR, 'anonymous-id');
|
|
7
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
8
|
+
function readValidIdFromFile() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = fs.readFileSync(ID_FILE, 'utf-8').trim();
|
|
11
|
+
if (UUID_RE.test(raw))
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
catch (_a) { }
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function stableMachineDistinctId() {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const h = crypto.createHash('sha256');
|
|
20
|
+
h.update(os.hostname());
|
|
21
|
+
h.update('\0');
|
|
22
|
+
h.update(os.homedir());
|
|
23
|
+
h.update('\0');
|
|
24
|
+
h.update((_b = (_a = process.env.USER) !== null && _a !== void 0 ? _a : process.env.USERNAME) !== null && _b !== void 0 ? _b : '');
|
|
25
|
+
const hex = h.digest('hex').slice(0, 32);
|
|
26
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
27
|
+
}
|
|
28
|
+
function tryPersistId(id) {
|
|
29
|
+
fs.mkdirSync(path.dirname(ID_FILE), { recursive: true });
|
|
30
|
+
fs.writeFileSync(ID_FILE, id, { flag: 'wx' });
|
|
31
|
+
}
|
|
32
|
+
export function getDistinctId() {
|
|
33
|
+
var _a, _b, _c;
|
|
34
|
+
const existing = readValidIdFromFile();
|
|
35
|
+
if (existing)
|
|
36
|
+
return existing;
|
|
37
|
+
const randomId = crypto.randomUUID();
|
|
38
|
+
try {
|
|
39
|
+
tryPersistId(randomId);
|
|
40
|
+
return (_a = readValidIdFromFile()) !== null && _a !== void 0 ? _a : randomId;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const code = err.code;
|
|
44
|
+
if (code === 'EEXIST') {
|
|
45
|
+
const concurrent = readValidIdFromFile();
|
|
46
|
+
if (concurrent)
|
|
47
|
+
return concurrent;
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(ID_FILE, randomId);
|
|
50
|
+
}
|
|
51
|
+
catch (_d) { }
|
|
52
|
+
return (_b = readValidIdFromFile()) !== null && _b !== void 0 ? _b : randomId;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const stable = stableMachineDistinctId();
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(path.dirname(ID_FILE), { recursive: true });
|
|
58
|
+
fs.writeFileSync(ID_FILE, stable);
|
|
59
|
+
}
|
|
60
|
+
catch (_e) { }
|
|
61
|
+
return (_c = readValidIdFromFile()) !== null && _c !== void 0 ? _c : stable;
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { getVersions } from '../helpers.js';
|
|
11
|
+
import { trackCommand } from './track.js';
|
|
12
|
+
const SCRAPE_SUBCOMMANDS = new Set(['page', 'site', 'openapi']);
|
|
13
|
+
export function getSanitizedCommandForTelemetry(_) {
|
|
14
|
+
const parts = _.filter((p) => typeof p === 'string');
|
|
15
|
+
if (parts.length === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const first = parts[0];
|
|
18
|
+
const second = parts[1];
|
|
19
|
+
if (first === 'scrape' && second !== undefined && SCRAPE_SUBCOMMANDS.has(second)) {
|
|
20
|
+
return `scrape ${second}`;
|
|
21
|
+
}
|
|
22
|
+
return first;
|
|
23
|
+
}
|
|
24
|
+
export function createTelemetryMiddleware() {
|
|
25
|
+
let tracked = false;
|
|
26
|
+
return function telemetryMiddleware(argv) {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const command = argv._[0];
|
|
29
|
+
if (typeof command === 'string') {
|
|
30
|
+
if (tracked)
|
|
31
|
+
return;
|
|
32
|
+
tracked = true;
|
|
33
|
+
const sanitizedCommand = getSanitizedCommandForTelemetry(argv._);
|
|
34
|
+
const { cli: cliVersion } = getVersions();
|
|
35
|
+
void trackCommand({ command: sanitizedCommand, cliVersion });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}
|