@mintlify/cli 4.0.1193 → 4.0.1195

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.
@@ -1,6 +1,6 @@
1
1
  import { LOCAL_LINKED_CLI_VERSION } from '@mintlify/previewing';
2
2
 
3
- import { getCliVersion } from '../src/helpers.js';
3
+ import { assertValidPackageName, getCliVersion } from '../src/helpers.js';
4
4
 
5
5
  describe('getCliVersion', () => {
6
6
  const originalTestMode = process.env.CLI_TEST_MODE;
@@ -27,3 +27,26 @@ describe('getCliVersion', () => {
27
27
  }
28
28
  );
29
29
  });
30
+
31
+ describe('assertValidPackageName', () => {
32
+ it.each(['mint', 'mintlify', '@mintlify/cli', 'some-pkg.name~2'])(
33
+ 'accepts valid npm package name %s',
34
+ (name) => {
35
+ expect(() => assertValidPackageName(name)).not.toThrow();
36
+ }
37
+ );
38
+
39
+ it.each([
40
+ 'mint; rm -rf /',
41
+ 'mint && calc.exe',
42
+ 'mint | whoami',
43
+ 'mint`whoami`',
44
+ 'mint$(whoami)',
45
+ 'mint @latest',
46
+ '../mint',
47
+ 'Mint',
48
+ '',
49
+ ])('rejects %s', (name) => {
50
+ expect(() => assertValidPackageName(name)).toThrow('Invalid package name');
51
+ });
52
+ });
@@ -1,6 +1,11 @@
1
1
  import * as previewing from '@mintlify/previewing';
2
2
 
3
- import { getLatestCliVersion, getVersions, execAsync } from '../src/helpers.js';
3
+ import {
4
+ getLatestCliVersion,
5
+ getVersions,
6
+ execFileAsync,
7
+ windowsCompatibleSpawnOptions,
8
+ } from '../src/helpers.js';
4
9
  import { update } from '../src/update.js';
5
10
 
6
11
  vi.mock('@mintlify/previewing', async () => {
@@ -19,7 +24,7 @@ vi.mock('../src/helpers.js', async () => {
19
24
  await vi.importActual<typeof import('../src/helpers.js')>('../src/helpers.js');
20
25
  return {
21
26
  ...originalModule,
22
- execAsync: vi.fn(),
27
+ execFileAsync: vi.fn(),
23
28
  getLatestCliVersion: vi.fn(),
24
29
  getVersions: vi.fn().mockReturnValue({
25
30
  cli: '1.0.0',
@@ -46,12 +51,16 @@ describe('update', () => {
46
51
  client: '1.0.0',
47
52
  });
48
53
  vi.mocked(getLatestCliVersion).mockReturnValue('2.0.0');
49
- vi.mocked(execAsync).mockResolvedValue({ stdout: '', stderr: '' });
54
+ vi.mocked(execFileAsync).mockResolvedValue({ stdout: '', stderr: '' });
50
55
  vi.mocked(previewing.downloadTargetMint).mockResolvedValue();
51
56
 
52
57
  await update({ packageName: 'mintlify' });
53
58
 
54
- expect(execAsync).toHaveBeenCalledWith('npm install -g mintlify@latest --silent');
59
+ expect(execFileAsync).toHaveBeenCalledWith(
60
+ 'npm',
61
+ ['install', '-g', 'mintlify@latest', '--silent'],
62
+ windowsCompatibleSpawnOptions
63
+ );
55
64
  expect(previewing.downloadTargetMint).toHaveBeenCalledWith({
56
65
  targetVersion: '2.0.0',
57
66
  existingVersion: '1.0.0',
@@ -74,7 +83,7 @@ describe('update', () => {
74
83
 
75
84
  await update({ packageName: 'mintlify' });
76
85
 
77
- expect(execAsync).not.toHaveBeenCalled();
86
+ expect(execFileAsync).not.toHaveBeenCalled();
78
87
  expect(previewing.downloadTargetMint).not.toHaveBeenCalled();
79
88
  expect(addLogSpy).toHaveBeenCalledWith(
80
89
  expect.objectContaining({ props: { message: 'already up to date' } })
@@ -87,7 +96,7 @@ describe('update', () => {
87
96
  client: '1.0.0',
88
97
  });
89
98
  vi.mocked(getLatestCliVersion).mockReturnValue('2.0.0');
90
- vi.mocked(execAsync).mockRejectedValue(new Error('Update failed'));
99
+ vi.mocked(execFileAsync).mockRejectedValue(new Error('Update failed'));
91
100
 
92
101
  await update({ packageName: 'mintlify' });
93
102
 
package/bin/helpers.js CHANGED
@@ -16,7 +16,7 @@ import detect from 'detect-port';
16
16
  import fse from 'fs-extra';
17
17
  import inquirer from 'inquirer';
18
18
  import yaml from 'js-yaml';
19
- import { exec, execSync } from 'node:child_process';
19
+ import { execFile, execFileSync } from 'node:child_process';
20
20
  import { readFileSync } from 'node:fs';
21
21
  import fs from 'node:fs/promises';
22
22
  import { createRequire } from 'node:module';
@@ -149,11 +149,24 @@ export const getVersions = (packageName) => {
149
149
  const client = getClientVersion().trim();
150
150
  return { cli, client };
151
151
  };
152
+ // On Windows, npm/pnpm entrypoints are .cmd shims that Node refuses to spawn
153
+ // without a shell (EINVAL since the CVE-2024-27980 fix). Everywhere else we
154
+ // keep the shell disabled so arguments can't be interpreted as shell syntax.
155
+ export const windowsCompatibleSpawnOptions = {
156
+ shell: process.platform === 'win32',
157
+ };
158
+ // npm package name grammar (optionally scoped). Rejecting anything else before
159
+ // it reaches a package-manager spawn keeps the win32 shell path (above) safe
160
+ // from shell metacharacters.
161
+ const VALID_PACKAGE_NAME_PATTERN = /^(@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
162
+ export const assertValidPackageName = (packageName) => {
163
+ if (!VALID_PACKAGE_NAME_PATTERN.test(packageName)) {
164
+ throw new Error(`Invalid package name: ${packageName}`);
165
+ }
166
+ };
152
167
  export const getLatestCliVersion = (packageName) => {
153
- return execSync(`npm view ${packageName} version --silent`, {
154
- encoding: 'utf-8',
155
- stdio: ['pipe', 'pipe', 'pipe'],
156
- }).trim();
168
+ assertValidPackageName(packageName);
169
+ return execFileSync('npm', ['view', packageName, 'version', '--silent'], Object.assign({ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, windowsCompatibleSpawnOptions)).trim();
157
170
  };
158
171
  export const suppressConsoleWarnings = () => {
159
172
  // Ignore tailwind warnings and punycode deprecation warning
@@ -191,13 +204,14 @@ export const terminate = (code) => __awaiter(void 0, void 0, void 0, function* (
191
204
  yield shutdownPostHog();
192
205
  process.exit(code);
193
206
  });
194
- export const execAsync = promisify(exec);
207
+ export const execFileAsync = promisify(execFile);
195
208
  export function isAI() {
196
209
  return (!process.stdin.isTTY || process.env.CLAUDECODE === '1' || process.env.TERM_PROGRAM === 'claude');
197
210
  }
198
211
  export const detectPackageManager = (_a) => __awaiter(void 0, [_a], void 0, function* ({ packageName }) {
212
+ assertValidPackageName(packageName);
199
213
  try {
200
- const { stdout: packagePath } = yield execAsync(`which ${packageName}`);
214
+ const { stdout: packagePath } = yield execFileAsync('which', [packageName]);
201
215
  if (packagePath.includes('pnpm')) {
202
216
  return 'pnpm';
203
217
  }