@ankhorage/doctor 0.2.0
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/analysis.d.ts +17 -0
- package/dist/analysis.d.ts.map +1 -0
- package/dist/analysis.js +181 -0
- package/dist/analysis.js.map +1 -0
- package/dist/ankh.provider.d.ts +10 -0
- package/dist/ankh.provider.d.ts.map +1 -0
- package/dist/ankh.provider.js +27 -0
- package/dist/ankh.provider.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -0
- package/dist/commandContext.d.ts +12 -0
- package/dist/commandContext.d.ts.map +1 -0
- package/dist/commandContext.js +15 -0
- package/dist/commandContext.js.map +1 -0
- package/dist/commands.d.ts +71 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +207 -0
- package/dist/commands.js.map +1 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +9 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/packageMetadata.d.ts +10 -0
- package/dist/packageMetadata.d.ts.map +1 -0
- package/dist/packageMetadata.js +16 -0
- package/dist/packageMetadata.js.map +1 -0
- package/dist/readme-usage.d.ts +2 -0
- package/dist/readme-usage.d.ts.map +1 -0
- package/dist/readme-usage.js +32 -0
- package/dist/readme-usage.js.map +1 -0
- package/package.json +85 -0
- package/src/analysis.ts +354 -0
- package/src/ankh.provider.ts +53 -0
- package/src/cli.ts +67 -0
- package/src/commandContext.ts +27 -0
- package/src/commands.ts +312 -0
- package/src/diagnostics.ts +33 -0
- package/src/index.ts +26 -0
- package/src/packageMetadata.ts +23 -0
- package/src/readme-usage.ts +32 -0
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { DoctorDiagnostic } from './diagnostics.js';
|
|
5
|
+
|
|
6
|
+
export type DoctorTargetCheck = 'package' | 'repo';
|
|
7
|
+
export type DoctorTargetMode = 'fix' | 'package' | 'repo' | 'validate';
|
|
8
|
+
|
|
9
|
+
export interface DoctorAnalysisRequest {
|
|
10
|
+
readonly cwd: string;
|
|
11
|
+
readonly inputPath?: string;
|
|
12
|
+
readonly mode: DoctorTargetMode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DoctorAnalysisResult {
|
|
16
|
+
readonly appliedChecks: readonly DoctorTargetCheck[];
|
|
17
|
+
readonly diagnostics: readonly DoctorDiagnostic[];
|
|
18
|
+
readonly hasPackageJson: boolean;
|
|
19
|
+
readonly repoMarkers: readonly string[];
|
|
20
|
+
readonly targetPath: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const REPO_MARKERS = [
|
|
24
|
+
{ label: '.git', relativePath: '.git' },
|
|
25
|
+
{ label: '.github/workflows', relativePath: path.join('.github', 'workflows') },
|
|
26
|
+
{ label: '.changeset', relativePath: '.changeset' },
|
|
27
|
+
{ label: 'package.json', relativePath: 'package.json' },
|
|
28
|
+
{ label: 'pnpm-workspace.yaml', relativePath: 'pnpm-workspace.yaml' },
|
|
29
|
+
{ label: 'bun.lock', relativePath: 'bun.lock' },
|
|
30
|
+
{ label: 'bun.lockb', relativePath: 'bun.lockb' },
|
|
31
|
+
{ label: 'package-lock.json', relativePath: 'package-lock.json' },
|
|
32
|
+
{ label: 'yarn.lock', relativePath: 'yarn.lock' },
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export async function analyzeDoctorTarget(
|
|
36
|
+
request: DoctorAnalysisRequest,
|
|
37
|
+
): Promise<DoctorAnalysisResult> {
|
|
38
|
+
const targetPath = path.resolve(request.cwd, request.inputPath ?? '.');
|
|
39
|
+
const stats = await statOrNull(targetPath);
|
|
40
|
+
|
|
41
|
+
if (stats === null) {
|
|
42
|
+
return {
|
|
43
|
+
appliedChecks: [],
|
|
44
|
+
diagnostics: [
|
|
45
|
+
createDiagnostic(
|
|
46
|
+
'error',
|
|
47
|
+
'target-not-found',
|
|
48
|
+
targetPath,
|
|
49
|
+
`Target path does not exist: ${targetPath}`,
|
|
50
|
+
),
|
|
51
|
+
],
|
|
52
|
+
hasPackageJson: false,
|
|
53
|
+
repoMarkers: [],
|
|
54
|
+
targetPath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!stats.isDirectory()) {
|
|
59
|
+
return {
|
|
60
|
+
appliedChecks: [],
|
|
61
|
+
diagnostics: [
|
|
62
|
+
createDiagnostic(
|
|
63
|
+
'error',
|
|
64
|
+
'target-not-directory',
|
|
65
|
+
targetPath,
|
|
66
|
+
`Target path must be a directory: ${targetPath}`,
|
|
67
|
+
),
|
|
68
|
+
],
|
|
69
|
+
hasPackageJson: false,
|
|
70
|
+
repoMarkers: [],
|
|
71
|
+
targetPath,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const repoMarkers = (
|
|
76
|
+
await Promise.all(
|
|
77
|
+
REPO_MARKERS.map(async (marker) =>
|
|
78
|
+
(await pathExists(path.join(targetPath, marker.relativePath))) ? marker.label : null,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
).filter((marker): marker is (typeof REPO_MARKERS)[number]['label'] => marker !== null);
|
|
82
|
+
|
|
83
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
84
|
+
const hasPackageJson = repoMarkers.includes('package.json');
|
|
85
|
+
const appliedChecks: DoctorTargetCheck[] = [];
|
|
86
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
87
|
+
|
|
88
|
+
const shouldRunRepoChecks =
|
|
89
|
+
request.mode === 'repo' ||
|
|
90
|
+
((request.mode === 'validate' || request.mode === 'fix') && repoMarkers.length > 0);
|
|
91
|
+
const shouldRunPackageChecks =
|
|
92
|
+
request.mode === 'package' ||
|
|
93
|
+
((request.mode === 'validate' || request.mode === 'fix') && hasPackageJson);
|
|
94
|
+
|
|
95
|
+
if (request.mode === 'repo' && repoMarkers.length === 0) {
|
|
96
|
+
diagnostics.push(
|
|
97
|
+
createDiagnostic(
|
|
98
|
+
'error',
|
|
99
|
+
'repo-markers-missing',
|
|
100
|
+
targetPath,
|
|
101
|
+
'Target is not a repo/workspace-root candidate because no repo markers were found.',
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (request.mode === 'package' && !hasPackageJson) {
|
|
107
|
+
diagnostics.push(
|
|
108
|
+
createDiagnostic(
|
|
109
|
+
'error',
|
|
110
|
+
'package-json-missing',
|
|
111
|
+
packageJsonPath,
|
|
112
|
+
'Target does not contain a readable package.json file.',
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
(request.mode === 'validate' || request.mode === 'fix') &&
|
|
119
|
+
repoMarkers.length === 0 &&
|
|
120
|
+
!hasPackageJson
|
|
121
|
+
) {
|
|
122
|
+
diagnostics.push(
|
|
123
|
+
createDiagnostic(
|
|
124
|
+
'error',
|
|
125
|
+
'unsupported-target',
|
|
126
|
+
targetPath,
|
|
127
|
+
'Target is neither a repo/workspace-root candidate nor a package root.',
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (shouldRunRepoChecks && repoMarkers.length > 0) {
|
|
133
|
+
appliedChecks.push('repo');
|
|
134
|
+
diagnostics.push(...(await analyzeRepoLight(targetPath)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (shouldRunPackageChecks && hasPackageJson) {
|
|
138
|
+
appliedChecks.push('package');
|
|
139
|
+
diagnostics.push(...(await analyzePackageLight(targetPath, packageJsonPath)));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
appliedChecks,
|
|
144
|
+
diagnostics,
|
|
145
|
+
hasPackageJson,
|
|
146
|
+
repoMarkers,
|
|
147
|
+
targetPath,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function analyzeRepoLight(targetPath: string): Promise<DoctorDiagnostic[]> {
|
|
152
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
153
|
+
|
|
154
|
+
await maybeWarnForMissingPath(diagnostics, targetPath, 'README.md', 'missing-readme');
|
|
155
|
+
await maybeWarnForMissingPath(diagnostics, targetPath, 'CHANGELOG.md', 'missing-changelog');
|
|
156
|
+
await maybeWarnForMissingPath(diagnostics, targetPath, 'LICENSE', 'missing-license');
|
|
157
|
+
await maybeWarnForMissingPath(
|
|
158
|
+
diagnostics,
|
|
159
|
+
targetPath,
|
|
160
|
+
'.changeset',
|
|
161
|
+
'missing-changeset-directory',
|
|
162
|
+
);
|
|
163
|
+
await maybeWarnForMissingPath(
|
|
164
|
+
diagnostics,
|
|
165
|
+
targetPath,
|
|
166
|
+
path.join('.github', 'workflows'),
|
|
167
|
+
'missing-workflows-directory',
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return diagnostics;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function analyzePackageLight(
|
|
174
|
+
targetPath: string,
|
|
175
|
+
packageJsonPath: string,
|
|
176
|
+
): Promise<DoctorDiagnostic[]> {
|
|
177
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
178
|
+
let parsed: unknown;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
diagnostics.push(
|
|
184
|
+
createDiagnostic(
|
|
185
|
+
'error',
|
|
186
|
+
'invalid-package-json',
|
|
187
|
+
packageJsonPath,
|
|
188
|
+
createInvalidPackageJsonMessage(error),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
return diagnostics;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!isRecord(parsed)) {
|
|
195
|
+
diagnostics.push(
|
|
196
|
+
createDiagnostic(
|
|
197
|
+
'error',
|
|
198
|
+
'invalid-package-json',
|
|
199
|
+
packageJsonPath,
|
|
200
|
+
'package.json must contain a JSON object.',
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
return diagnostics;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!isNonEmptyString(parsed.name)) {
|
|
207
|
+
diagnostics.push(
|
|
208
|
+
createDiagnostic(
|
|
209
|
+
'error',
|
|
210
|
+
'package-name-missing',
|
|
211
|
+
packageJsonPath,
|
|
212
|
+
'package.json must define a non-empty "name" field.',
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!isNonEmptyString(parsed.version)) {
|
|
218
|
+
diagnostics.push(
|
|
219
|
+
createDiagnostic(
|
|
220
|
+
'error',
|
|
221
|
+
'package-version-missing',
|
|
222
|
+
packageJsonPath,
|
|
223
|
+
'package.json must define a non-empty "version" field.',
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!isNonEmptyString(parsed.type)) {
|
|
229
|
+
diagnostics.push(
|
|
230
|
+
createDiagnostic(
|
|
231
|
+
'error',
|
|
232
|
+
'package-type-missing',
|
|
233
|
+
packageJsonPath,
|
|
234
|
+
'package.json must define a non-empty "type" field.',
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if ('ankh' in parsed) {
|
|
240
|
+
const validationError = validateAnkhMetadataShape(parsed.ankh);
|
|
241
|
+
if (validationError !== null) {
|
|
242
|
+
diagnostics.push(
|
|
243
|
+
createDiagnostic('error', 'invalid-ankh-metadata', targetPath, validationError),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return diagnostics;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function maybeWarnForMissingPath(
|
|
252
|
+
diagnostics: DoctorDiagnostic[],
|
|
253
|
+
rootPath: string,
|
|
254
|
+
relativePath: string,
|
|
255
|
+
code:
|
|
256
|
+
| 'missing-changelog'
|
|
257
|
+
| 'missing-changeset-directory'
|
|
258
|
+
| 'missing-license'
|
|
259
|
+
| 'missing-readme'
|
|
260
|
+
| 'missing-workflows-directory',
|
|
261
|
+
): Promise<void> {
|
|
262
|
+
const absolutePath = path.join(rootPath, relativePath);
|
|
263
|
+
if (await pathExists(absolutePath)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
diagnostics.push(
|
|
268
|
+
createDiagnostic(
|
|
269
|
+
'warning',
|
|
270
|
+
code,
|
|
271
|
+
absolutePath,
|
|
272
|
+
`Missing expected repo artifact: ${relativePath}`,
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function validateAnkhMetadataShape(value: unknown): string | null {
|
|
278
|
+
if (!isRecord(value)) {
|
|
279
|
+
return 'package.json "ankh" metadata must be an object when present.';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!isNonEmptyString(value.category)) {
|
|
283
|
+
return 'package.json "ankh.category" must be a non-empty string.';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!(value.provider === null || isPackageRelativeProviderPath(value.provider))) {
|
|
287
|
+
return 'package.json "ankh.provider" must be null or a package-relative "./..." string.';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
!Array.isArray(value.capabilities) ||
|
|
292
|
+
value.capabilities.some((capability) => !isNonEmptyString(capability))
|
|
293
|
+
) {
|
|
294
|
+
return 'package.json "ankh.capabilities" must be an array of non-empty strings.';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createDiagnostic(
|
|
301
|
+
severity: DoctorDiagnostic['severity'],
|
|
302
|
+
code: DoctorDiagnostic['code'],
|
|
303
|
+
diagnosticPath: string,
|
|
304
|
+
message: string,
|
|
305
|
+
): DoctorDiagnostic {
|
|
306
|
+
return {
|
|
307
|
+
code,
|
|
308
|
+
message,
|
|
309
|
+
path: diagnosticPath,
|
|
310
|
+
severity,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createInvalidPackageJsonMessage(error: unknown): string {
|
|
315
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
316
|
+
return `package.json contains invalid JSON: ${message}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function pathExists(targetPath: string): Promise<boolean> {
|
|
320
|
+
return (await statOrNull(targetPath)) !== null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function statOrNull(targetPath: string) {
|
|
324
|
+
try {
|
|
325
|
+
return await fs.stat(targetPath);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (isMissingPathError(error)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isMissingPathError(error: unknown): error is NodeJS.ErrnoException {
|
|
336
|
+
return (
|
|
337
|
+
error instanceof Error &&
|
|
338
|
+
'code' in error &&
|
|
339
|
+
typeof error.code === 'string' &&
|
|
340
|
+
error.code === 'ENOENT'
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
345
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isPackageRelativeProviderPath(value: unknown): value is string {
|
|
349
|
+
return isNonEmptyString(value) && value.startsWith('./');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
353
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
354
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AnkhRuntimeCommandProvider } from '@ankhorage/ankh';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createProviderCommandDescriptors,
|
|
5
|
+
DOCTOR_COMMANDS,
|
|
6
|
+
type DoctorCommandServices,
|
|
7
|
+
runDoctorCommand,
|
|
8
|
+
type RunDoctorCommandImpl,
|
|
9
|
+
} from './commands.js';
|
|
10
|
+
import {
|
|
11
|
+
DOCTOR_CAPABILITIES,
|
|
12
|
+
DOCTOR_COMMAND_CATEGORY,
|
|
13
|
+
DOCTOR_PACKAGE_NAME,
|
|
14
|
+
DOCTOR_PACKAGE_VERSION,
|
|
15
|
+
} from './packageMetadata.js';
|
|
16
|
+
|
|
17
|
+
export interface CreateDoctorRuntimeProviderOptions {
|
|
18
|
+
readonly runCommandImpl?: RunDoctorCommandImpl;
|
|
19
|
+
readonly services?: Partial<DoctorCommandServices>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDoctorRuntimeProvider(
|
|
23
|
+
options: CreateDoctorRuntimeProviderOptions = {},
|
|
24
|
+
): AnkhRuntimeCommandProvider {
|
|
25
|
+
const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
id: DOCTOR_PACKAGE_NAME,
|
|
29
|
+
category: DOCTOR_COMMAND_CATEGORY,
|
|
30
|
+
version: DOCTOR_PACKAGE_VERSION,
|
|
31
|
+
capabilities: [...DOCTOR_CAPABILITIES],
|
|
32
|
+
commands: createProviderCommandDescriptors(),
|
|
33
|
+
handlers: DOCTOR_COMMANDS.map((command) => ({
|
|
34
|
+
path: command.path,
|
|
35
|
+
handler(request) {
|
|
36
|
+
return runCommandImpl(
|
|
37
|
+
{
|
|
38
|
+
argv: request.argv,
|
|
39
|
+
command,
|
|
40
|
+
context: request.context,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
services: options.services,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
})),
|
|
48
|
+
} satisfies AnkhRuntimeCommandProvider;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const provider = createDoctorRuntimeProvider();
|
|
52
|
+
|
|
53
|
+
export default provider;
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
|
|
4
|
+
import { createDefaultCommandContext } from './commandContext.js';
|
|
5
|
+
import {
|
|
6
|
+
type DoctorCommandServices,
|
|
7
|
+
findDoctorCommandByStandaloneName,
|
|
8
|
+
renderRootHelp,
|
|
9
|
+
renderUnknownCommand,
|
|
10
|
+
runDoctorCommand,
|
|
11
|
+
type RunDoctorCommandImpl,
|
|
12
|
+
} from './commands.js';
|
|
13
|
+
|
|
14
|
+
export interface DoctorCliOptions {
|
|
15
|
+
readonly context?: DoctorCommandContext;
|
|
16
|
+
readonly runCommandImpl?: RunDoctorCommandImpl;
|
|
17
|
+
readonly services?: Partial<DoctorCommandServices>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runCli(
|
|
21
|
+
argv: readonly string[],
|
|
22
|
+
options: DoctorCliOptions = {},
|
|
23
|
+
): Promise<DoctorCommandRunResult> {
|
|
24
|
+
const context = options.context ?? createDefaultCommandContext();
|
|
25
|
+
const runCommand = options.runCommandImpl ?? runDoctorCommand;
|
|
26
|
+
const [firstToken, ...restTokens] = argv;
|
|
27
|
+
|
|
28
|
+
if (firstToken === undefined || isHelpToken(firstToken)) {
|
|
29
|
+
context.writeStdout(renderRootHelp(context.version));
|
|
30
|
+
return { exitCode: 0 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isVersionToken(firstToken)) {
|
|
34
|
+
context.writeStdout(`${context.version}\n`);
|
|
35
|
+
return { exitCode: 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const command = findDoctorCommandByStandaloneName(firstToken);
|
|
39
|
+
if (command === null) {
|
|
40
|
+
context.writeStderr(renderUnknownCommand(firstToken));
|
|
41
|
+
return { exitCode: 1 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return runCommand(
|
|
45
|
+
{
|
|
46
|
+
argv: restTokens,
|
|
47
|
+
command,
|
|
48
|
+
context,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
services: options.services,
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isHelpToken(value: string): boolean {
|
|
57
|
+
return value === '--help' || value === '-h' || value === 'help';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isVersionToken(value: string): boolean {
|
|
61
|
+
return value === '--version' || value === '-v' || value === 'version';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (import.meta.main) {
|
|
65
|
+
const result = await runCli(process.argv.slice(2));
|
|
66
|
+
process.exit(result.exitCode);
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import packageJson from '../package.json';
|
|
2
|
+
|
|
3
|
+
export interface DoctorCommandContext {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly env: NodeJS.ProcessEnv;
|
|
6
|
+
readonly version: string;
|
|
7
|
+
writeStdout(text: string): void;
|
|
8
|
+
writeStderr(text: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DoctorCommandRunResult {
|
|
12
|
+
readonly exitCode: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createDefaultCommandContext(): DoctorCommandContext {
|
|
16
|
+
return {
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
env: process.env,
|
|
19
|
+
version: packageJson.version,
|
|
20
|
+
writeStdout(text: string) {
|
|
21
|
+
process.stdout.write(text);
|
|
22
|
+
},
|
|
23
|
+
writeStderr(text: string) {
|
|
24
|
+
process.stderr.write(text);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|