@mintlify/cli 4.0.1123 → 4.0.1125

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.1123",
3
+ "version": "4.0.1125",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -93,5 +93,5 @@
93
93
  "vitest": "2.1.9",
94
94
  "vitest-mock-process": "1.0.4"
95
95
  },
96
- "gitHead": "511d45e72c42590140bd4568456f75bc65658752"
96
+ "gitHead": "9af367eef1aa11828c01c926c4a1cd7d3ba6b28a"
97
97
  }
package/src/helpers.tsx CHANGED
@@ -14,14 +14,15 @@ import {
14
14
  import { upgradeToDocsConfig, validatePathWithinCwd } from '@mintlify/validation';
15
15
  import detect from 'detect-port';
16
16
  import fse from 'fs-extra';
17
- import fs from 'fs/promises';
18
17
  import inquirer from 'inquirer';
19
18
  import yaml from 'js-yaml';
20
19
  import { exec, execSync } from 'node:child_process';
20
+ import { readFileSync } from 'node:fs';
21
+ import fs from 'node:fs/promises';
22
+ import { fileURLToPath } from 'node:url';
21
23
  import { promisify } from 'node:util';
22
24
  import path from 'path';
23
25
  import type { ArgumentsCamelCase } from 'yargs';
24
- import yargs from 'yargs';
25
26
 
26
27
  import { shutdownPostHog } from './telemetry/client.js';
27
28
 
@@ -128,22 +129,40 @@ export const upgradeConfig = async () => {
128
129
  }
129
130
  };
130
131
 
132
+ // Resolve the CLI's own package.json via `import.meta.url` so the version is
133
+ // always the one shipped with this package, regardless of cwd. See #7121.
134
+ const CLI_PACKAGE_JSON_PATH = (() => {
135
+ try {
136
+ return fileURLToPath(new URL('../package.json', import.meta.url));
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ })();
141
+
142
+ const readCliPackageVersion = (): string | undefined => {
143
+ if (!CLI_PACKAGE_JSON_PATH) return undefined;
144
+ try {
145
+ const pkg = JSON.parse(readFileSync(CLI_PACKAGE_JSON_PATH, 'utf8')) as { version?: string };
146
+ return typeof pkg.version === 'string' ? pkg.version : undefined;
147
+ } catch {
148
+ return undefined;
149
+ }
150
+ };
151
+
152
+ // Outside `node_modules` means `yarn dev` or `npm link`: treat as linked. See #7121.
153
+ const isRunningFromLinkedBuild = (): boolean => {
154
+ if (!CLI_PACKAGE_JSON_PATH) return false;
155
+ return !CLI_PACKAGE_JSON_PATH.split(path.sep).includes('node_modules');
156
+ };
157
+
131
158
  export const getCliVersion = (): string | undefined => {
132
- const y = yargs();
133
- let version = undefined;
134
- y.showVersion((s) => {
135
- version = s;
136
- return false;
137
- });
138
159
  if (process.env.CLI_TEST_MODE === 'true') {
139
160
  return 'test-cli';
140
161
  }
141
- // when running `npm link` the version is 'unknown'
142
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
143
- if (version === 'unknown') {
144
- version = LOCAL_LINKED_CLI_VERSION;
162
+ if (isRunningFromLinkedBuild()) {
163
+ return LOCAL_LINKED_CLI_VERSION;
145
164
  }
146
- return version;
165
+ return readCliPackageVersion();
147
166
  };
148
167
 
149
168
  export const getVersions = (): {
@@ -1,8 +1,8 @@
1
1
  import { authenticatedFetch } from '../authenticatedFetch.js';
2
2
  import { API_URL } from '../constants.js';
3
- import type { ScoreResponse } from './types.js';
3
+ import type { ResolveResult, ScoreResponse } from './types.js';
4
4
 
5
- export async function getScore(url: string): Promise<ScoreResponse> {
5
+ export async function resolveScoreForUrl(url: string): Promise<ResolveResult> {
6
6
  const endpoint = new URL(`${API_URL}/api/cli/score`);
7
7
  endpoint.searchParams.set('url', url);
8
8
 
@@ -10,6 +10,23 @@ export async function getScore(url: string): Promise<ScoreResponse> {
10
10
  headers: { Accept: 'application/json' },
11
11
  });
12
12
 
13
+ if (res.status !== 200 && res.status !== 202) {
14
+ const body = await res.text().catch(() => '');
15
+ throw new Error(`API error (${res.status}): ${body || res.statusText}`);
16
+ }
17
+
18
+ return res.json() as Promise<ResolveResult>;
19
+ }
20
+
21
+ export async function fetchScoreBySlug(slug: string): Promise<ScoreResponse | null> {
22
+ const endpoint = new URL(`${API_URL}/api/cli/score/${encodeURIComponent(slug)}`);
23
+
24
+ const res = await authenticatedFetch(endpoint.toString(), {
25
+ headers: { Accept: 'application/json' },
26
+ });
27
+
28
+ if (res.status === 202) return null;
29
+
13
30
  if (!res.ok) {
14
31
  const body = await res.text().catch(() => '');
15
32
  throw new Error(`API error (${res.status}): ${body || res.statusText}`);
@@ -4,11 +4,14 @@ import { Text } from 'ink';
4
4
 
5
5
  import { terminate } from '../helpers.js';
6
6
  import { trackEvent } from '../telemetry/track.js';
7
- import { getScore } from './client.js';
8
- import type { Check } from './types.js';
7
+ import { fetchScoreBySlug, resolveScoreForUrl } from './client.js';
8
+ import type { Check, ScoreResponse } from './types.js';
9
9
 
10
10
  type OutputFormat = 'table' | 'plain' | 'json';
11
11
 
12
+ const POLL_INTERVAL_MS = 3_000;
13
+ const POLL_TIMEOUT_MS = 120_000;
14
+
12
15
  function resolveFormat(argv: { format?: string }): OutputFormat {
13
16
  if (argv.format === 'table' || argv.format === 'plain' || argv.format === 'json')
14
17
  return argv.format;
@@ -49,6 +52,21 @@ function renderPlain(checks: Check[], prefix = ''): string[] {
49
52
  return lines;
50
53
  }
51
54
 
55
+ function renderTable(score: ScoreResponse, opts?: { stale?: boolean }): string {
56
+ const scoreColor =
57
+ score.overallScore >= 70 ? 'green' : score.overallScore >= 40 ? 'yellow' : 'red';
58
+ const header = opts?.stale
59
+ ? chalk.bold(`\nAgent Readiness Score — ${score.canonicalUrl}`) +
60
+ chalk.dim(` (last run ${new Date(score.computedAt).toLocaleString()})\n`)
61
+ : chalk.bold(`\nAgent Readiness Score — ${score.canonicalUrl}\n`);
62
+
63
+ const lines: string[] = [header];
64
+ lines.push(` Score: ${chalk[scoreColor].bold(`${score.overallScore}%`)}\n`);
65
+ lines.push(chalk.bold(' Checks'));
66
+ lines.push(...renderChecks(score.checks));
67
+ return lines.join('\n');
68
+ }
69
+
52
70
  function output(format: OutputFormat, text: string) {
53
71
  if (format === 'table') {
54
72
  addLog(<Text>{text}</Text>);
@@ -57,39 +75,80 @@ function output(format: OutputFormat, text: string) {
57
75
  }
58
76
  }
59
77
 
78
+ async function sleep(ms: number): Promise<void> {
79
+ return new Promise((resolve) => setTimeout(resolve, ms));
80
+ }
81
+
82
+ async function pollForScore(
83
+ slug: string,
84
+ afterIso: string | null,
85
+ onAttempt?: () => void
86
+ ): Promise<ScoreResponse> {
87
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
88
+ while (Date.now() < deadline) {
89
+ await sleep(POLL_INTERVAL_MS);
90
+ onAttempt?.();
91
+ const score = await fetchScoreBySlug(slug);
92
+ if (!score) continue;
93
+ if (afterIso === null) return score;
94
+ if (new Date(score.computedAt).getTime() > new Date(afterIso).getTime()) return score;
95
+ }
96
+ throw new Error(`Score did not complete within ${POLL_TIMEOUT_MS / 1000}s`);
97
+ }
98
+
60
99
  export const scoreHandler = async (argv: { url: string; format?: string }) => {
61
100
  const format = resolveFormat(argv);
62
101
  try {
63
- if (format === 'table') addLog(<SpinnerLog message="Running agent readiness checks..." />);
64
- const data = await getScore(argv.url);
102
+ if (format === 'table') addLog(<SpinnerLog message="Checking agent readiness..." />);
103
+ const resolved = await resolveScoreForUrl(argv.url);
104
+
105
+ let final: ScoreResponse;
106
+
107
+ if (resolved.status === 'ready') {
108
+ const score = await fetchScoreBySlug(resolved.slug);
109
+ if (format === 'table') removeLastLog();
110
+ if (!score) throw new Error('Score was unexpectedly unavailable');
111
+ final = score;
112
+ } else if (resolved.status === 'stale_refresh_queued') {
113
+ const stale = await fetchScoreBySlug(resolved.slug);
114
+ if (format === 'table') removeLastLog();
115
+ if (!stale) throw new Error('Stale score was unexpectedly unavailable');
116
+
117
+ if (format === 'table') {
118
+ output('table', renderTable(stale, { stale: true }));
119
+ addLog(<SpinnerLog message="Refreshing score in the background..." />);
120
+ } else {
121
+ process.stderr.write('Refreshing score in the background...\n');
122
+ }
123
+ final = await pollForScore(resolved.slug, stale.computedAt);
124
+ if (format === 'table') removeLastLog();
125
+ } else {
126
+ if (format === 'table') {
127
+ removeLastLog();
128
+ addLog(<SpinnerLog message="Queued a new scoring run. Waiting for first result..." />);
129
+ } else {
130
+ process.stderr.write('Queued a new scoring run. Waiting for first result...\n');
131
+ }
132
+ final = await pollForScore(resolved.slug, null);
133
+ if (format === 'table') removeLastLog();
134
+ }
135
+
65
136
  void trackEvent('cli.score.executed', {
66
137
  url: argv.url,
67
- score: data.overallScore,
138
+ score: final.overallScore,
68
139
  format,
140
+ status: resolved.status,
69
141
  });
70
- if (format === 'table') removeLastLog();
71
142
 
72
143
  if (format === 'json') {
73
- output(format, JSON.stringify(data, null, 2));
74
- await terminate(0);
75
- return;
76
- }
77
-
78
- if (format === 'plain') {
79
- const lines = [`SCORE\t${data.overallScore}`, ...renderPlain(data.checks)];
144
+ output(format, JSON.stringify(final, null, 2));
145
+ } else if (format === 'plain') {
146
+ const lines = [`SCORE\t${final.overallScore}`, ...renderPlain(final.checks)];
80
147
  output(format, lines.join('\n'));
81
- await terminate(0);
82
- return;
148
+ } else {
149
+ output(format, renderTable(final));
83
150
  }
84
151
 
85
- const scoreColor =
86
- data.overallScore >= 70 ? 'green' : data.overallScore >= 40 ? 'yellow' : 'red';
87
- const lines: string[] = [];
88
- lines.push(chalk.bold(`\nAgent Readiness Score — ${data.url}\n`));
89
- lines.push(` Score: ${chalk[scoreColor].bold(`${data.overallScore}%`)}\n`);
90
- lines.push(chalk.bold(' Checks'));
91
- lines.push(...renderChecks(data.checks));
92
- output(format, lines.join('\n'));
93
152
  await terminate(0);
94
153
  } catch (err) {
95
154
  const message = err instanceof Error ? err.message : 'unknown error';
@@ -5,8 +5,22 @@ export type Check = {
5
5
  children?: Check[];
6
6
  };
7
7
 
8
+ export type ResolveStatus = 'ready' | 'stale_refresh_queued' | 'queued';
9
+
10
+ export type ResolveResult = {
11
+ canonicalUrl: string;
12
+ slug: string;
13
+ status: ResolveStatus;
14
+ trackedSiteId: string;
15
+ };
16
+
8
17
  export type ScoreResponse = {
9
- url: string;
10
- overallScore: number;
18
+ canonicalUrl: string;
11
19
  checks: Check[];
20
+ computedAt: string;
21
+ name: string;
22
+ overallScore: number;
23
+ passedChecks: number;
24
+ slug: string;
25
+ totalChecks: number;
12
26
  };