@kidd-cli/cli 0.1.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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/commands/add/command.d.mts +6 -0
- package/dist/commands/add/command.mjs +137 -0
- package/dist/commands/add/index.d.mts +6 -0
- package/dist/commands/add/index.mjs +7 -0
- package/dist/commands/add/middleware.d.mts +6 -0
- package/dist/commands/add/middleware.mjs +101 -0
- package/dist/commands/build.d.mts +6 -0
- package/dist/commands/build.mjs +163 -0
- package/dist/commands/commands.d.mts +13 -0
- package/dist/commands/commands.mjs +135 -0
- package/dist/commands/dev.d.mts +12 -0
- package/dist/commands/dev.mjs +68 -0
- package/dist/commands/doctor.d.mts +6 -0
- package/dist/commands/doctor.mjs +680 -0
- package/dist/commands/init.d.mts +6 -0
- package/dist/commands/init.mjs +167 -0
- package/dist/detect-DDE1hlQ8.mjs +73 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +41 -0
- package/dist/lib/templates/command/command.ts.liquid +12 -0
- package/dist/lib/templates/middleware/middleware.ts.liquid +9 -0
- package/dist/lib/templates/project/gitignore.liquid +3 -0
- package/dist/lib/templates/project/package.json.liquid +29 -0
- package/dist/lib/templates/project/src/commands/hello.ts.liquid +12 -0
- package/dist/lib/templates/project/src/index.ts.liquid +8 -0
- package/dist/lib/templates/project/tsconfig.json.liquid +19 -0
- package/dist/lib/templates/project/tsdown.config.ts.liquid +9 -0
- package/dist/lib/templates/project/vitest.config.ts.liquid +8 -0
- package/dist/write-DDGnajpV.mjs +166 -0
- package/package.json +43 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { command } from "@kidd-cli/core";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { readManifest } from "@kidd-cli/utils/manifest";
|
|
4
|
+
import { loadConfig } from "@kidd-cli/config/loader";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { attemptAsync, err, ok } from "@kidd-cli/utils/fp";
|
|
9
|
+
import { jsonParse, jsonStringify } from "@kidd-cli/utils/json";
|
|
10
|
+
|
|
11
|
+
//#region src/lib/checks.ts
|
|
12
|
+
/**
|
|
13
|
+
* Default entry point for the CLI source.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_ENTRY = "./src/index.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Default directory for CLI commands.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_COMMANDS = "./commands";
|
|
20
|
+
/**
|
|
21
|
+
* Create a CheckContext from the gathered project data.
|
|
22
|
+
*
|
|
23
|
+
* @param params - The context parameters.
|
|
24
|
+
* @returns A frozen CheckContext.
|
|
25
|
+
*/
|
|
26
|
+
function createCheckContext(params) {
|
|
27
|
+
return {
|
|
28
|
+
configError: params.configError,
|
|
29
|
+
configResult: params.configResult,
|
|
30
|
+
cwd: params.cwd,
|
|
31
|
+
manifest: params.manifest,
|
|
32
|
+
rawPackageJson: params.rawPackageJson
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a CheckResult with the given status.
|
|
37
|
+
*
|
|
38
|
+
* @private
|
|
39
|
+
* @param params - The check result parameters.
|
|
40
|
+
* @returns A CheckResult.
|
|
41
|
+
*/
|
|
42
|
+
function checkResult(params) {
|
|
43
|
+
return {
|
|
44
|
+
hint: params.hint ?? null,
|
|
45
|
+
message: params.message,
|
|
46
|
+
name: params.name,
|
|
47
|
+
status: params.status
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a FixResult.
|
|
52
|
+
*
|
|
53
|
+
* @private
|
|
54
|
+
* @param params - The fix result parameters.
|
|
55
|
+
* @returns A FixResult.
|
|
56
|
+
*/
|
|
57
|
+
function fixResult(params) {
|
|
58
|
+
return {
|
|
59
|
+
fixed: params.fixed,
|
|
60
|
+
message: params.message,
|
|
61
|
+
name: params.name
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check whether a kidd config file exists.
|
|
66
|
+
*
|
|
67
|
+
* @private
|
|
68
|
+
* @param context - The diagnostic check context.
|
|
69
|
+
* @returns A CheckResult indicating pass or fail.
|
|
70
|
+
*/
|
|
71
|
+
async function checkKiddConfig(context) {
|
|
72
|
+
if (context.configResult && context.configResult.configFile) return checkResult({
|
|
73
|
+
message: `Config file found at ./${relative(context.cwd, context.configResult.configFile)}`,
|
|
74
|
+
name: "kidd.config",
|
|
75
|
+
status: "pass"
|
|
76
|
+
});
|
|
77
|
+
return checkResult({
|
|
78
|
+
hint: "Run \"kidd init\" to scaffold a config file",
|
|
79
|
+
message: "No config file found",
|
|
80
|
+
name: "kidd.config",
|
|
81
|
+
status: "fail"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check whether the loaded config schema is valid.
|
|
86
|
+
*
|
|
87
|
+
* @private
|
|
88
|
+
* @param context - The diagnostic check context.
|
|
89
|
+
* @returns A CheckResult indicating pass, warn, or fail.
|
|
90
|
+
*/
|
|
91
|
+
async function checkConfigSchema(context) {
|
|
92
|
+
if (context.configResult) return checkResult({
|
|
93
|
+
message: "Config is valid",
|
|
94
|
+
name: "config schema",
|
|
95
|
+
status: "pass"
|
|
96
|
+
});
|
|
97
|
+
if (context.configError) return checkResult({
|
|
98
|
+
hint: "Check your kidd.config.ts for syntax or schema errors",
|
|
99
|
+
message: `Config validation failed: ${context.configError.message}`,
|
|
100
|
+
name: "config schema",
|
|
101
|
+
status: "fail"
|
|
102
|
+
});
|
|
103
|
+
return checkResult({
|
|
104
|
+
message: "No config file, using defaults",
|
|
105
|
+
name: "config schema",
|
|
106
|
+
status: "warn"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check whether package.json exists in the project.
|
|
111
|
+
*
|
|
112
|
+
* @private
|
|
113
|
+
* @param context - The diagnostic check context.
|
|
114
|
+
* @returns A CheckResult indicating pass or fail.
|
|
115
|
+
*/
|
|
116
|
+
async function checkPackageJson(context) {
|
|
117
|
+
if (context.rawPackageJson) return checkResult({
|
|
118
|
+
message: "Found",
|
|
119
|
+
name: "package.json",
|
|
120
|
+
status: "pass"
|
|
121
|
+
});
|
|
122
|
+
return checkResult({
|
|
123
|
+
hint: "Run \"pnpm init\" to create a package.json",
|
|
124
|
+
message: "Not found",
|
|
125
|
+
name: "package.json",
|
|
126
|
+
status: "fail"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check whether the package.json has a version field.
|
|
131
|
+
*
|
|
132
|
+
* @private
|
|
133
|
+
* @param context - The diagnostic check context.
|
|
134
|
+
* @returns A CheckResult indicating pass or warn.
|
|
135
|
+
*/
|
|
136
|
+
async function checkPackageVersion(context) {
|
|
137
|
+
if (context.manifest && context.manifest.version) return checkResult({
|
|
138
|
+
message: context.manifest.version,
|
|
139
|
+
name: "package version",
|
|
140
|
+
status: "pass"
|
|
141
|
+
});
|
|
142
|
+
return checkResult({
|
|
143
|
+
hint: "Add a \"version\" field to package.json",
|
|
144
|
+
message: "No version field",
|
|
145
|
+
name: "package version",
|
|
146
|
+
status: "warn"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check whether the package.json declares ESM via `"type": "module"`.
|
|
151
|
+
*
|
|
152
|
+
* @private
|
|
153
|
+
* @param context - The diagnostic check context.
|
|
154
|
+
* @returns A CheckResult indicating pass or fail.
|
|
155
|
+
*/
|
|
156
|
+
async function checkModuleType(context) {
|
|
157
|
+
if (context.rawPackageJson && context.rawPackageJson.type === "module") return checkResult({
|
|
158
|
+
message: "ESM (\"type\": \"module\")",
|
|
159
|
+
name: "module type",
|
|
160
|
+
status: "pass"
|
|
161
|
+
});
|
|
162
|
+
return checkResult({
|
|
163
|
+
hint: "Add \"type\": \"module\" to package.json (fixable with --fix)",
|
|
164
|
+
message: "Missing or wrong \"type\" field (expected \"module\")",
|
|
165
|
+
name: "module type",
|
|
166
|
+
status: "fail"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check whether kidd is listed as a dependency or devDependency.
|
|
171
|
+
*
|
|
172
|
+
* @private
|
|
173
|
+
* @param context - The diagnostic check context.
|
|
174
|
+
* @returns A CheckResult indicating pass or fail.
|
|
175
|
+
*/
|
|
176
|
+
async function checkKiddDependency(context) {
|
|
177
|
+
if (!context.rawPackageJson) return checkResult({
|
|
178
|
+
hint: "Run \"pnpm init\" to create a package.json first",
|
|
179
|
+
message: "No package.json found",
|
|
180
|
+
name: "@kidd-cli/core dependency",
|
|
181
|
+
status: "fail"
|
|
182
|
+
});
|
|
183
|
+
const deps = context.rawPackageJson.dependencies ?? {};
|
|
184
|
+
const devDeps = context.rawPackageJson.devDependencies ?? {};
|
|
185
|
+
if ("@kidd-cli/core" in deps) return checkResult({
|
|
186
|
+
message: "Found in dependencies",
|
|
187
|
+
name: "@kidd-cli/core dependency",
|
|
188
|
+
status: "pass"
|
|
189
|
+
});
|
|
190
|
+
if ("@kidd-cli/core" in devDeps) return checkResult({
|
|
191
|
+
message: "Found in devDependencies",
|
|
192
|
+
name: "@kidd-cli/core dependency",
|
|
193
|
+
status: "pass"
|
|
194
|
+
});
|
|
195
|
+
return checkResult({
|
|
196
|
+
hint: "Run \"pnpm add kidd\" or use --fix to add it (fixable with --fix)",
|
|
197
|
+
message: "Not found in dependencies or devDependencies",
|
|
198
|
+
name: "@kidd-cli/core dependency",
|
|
199
|
+
status: "fail"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check whether the CLI entry point file exists on disk.
|
|
204
|
+
*
|
|
205
|
+
* @private
|
|
206
|
+
* @param context - The diagnostic check context.
|
|
207
|
+
* @returns A CheckResult indicating pass, warn, or fail.
|
|
208
|
+
*/
|
|
209
|
+
async function checkEntryPoint(context) {
|
|
210
|
+
const config = extractConfig(context.configResult);
|
|
211
|
+
const entryPath = resolveEntryPath(config);
|
|
212
|
+
if (await fileExists(join(context.cwd, entryPath))) return checkResult({
|
|
213
|
+
message: `Found: ${entryPath}`,
|
|
214
|
+
name: "entry point",
|
|
215
|
+
status: "pass"
|
|
216
|
+
});
|
|
217
|
+
if (!config) return checkResult({
|
|
218
|
+
hint: "Create the entry file or update \"entry\" in kidd.config.ts (fixable with --fix)",
|
|
219
|
+
message: `No config, default not found: ${entryPath}`,
|
|
220
|
+
name: "entry point",
|
|
221
|
+
status: "warn"
|
|
222
|
+
});
|
|
223
|
+
return checkResult({
|
|
224
|
+
hint: "Create the entry file or update \"entry\" in kidd.config.ts (fixable with --fix)",
|
|
225
|
+
message: `Not found: ${entryPath}`,
|
|
226
|
+
name: "entry point",
|
|
227
|
+
status: "fail"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check whether the commands directory exists on disk.
|
|
232
|
+
*
|
|
233
|
+
* @private
|
|
234
|
+
* @param context - The diagnostic check context.
|
|
235
|
+
* @returns A CheckResult indicating pass, warn, or fail.
|
|
236
|
+
*/
|
|
237
|
+
async function checkCommandsDirectory(context) {
|
|
238
|
+
const config = extractConfig(context.configResult);
|
|
239
|
+
const commandsPath = resolveCommandsPath(config);
|
|
240
|
+
if (await fileExists(join(context.cwd, commandsPath))) return checkResult({
|
|
241
|
+
message: `Found: ${commandsPath}`,
|
|
242
|
+
name: "commands directory",
|
|
243
|
+
status: "pass"
|
|
244
|
+
});
|
|
245
|
+
if (!config) return checkResult({
|
|
246
|
+
hint: "Create the directory or update \"commands\" in kidd.config.ts (fixable with --fix)",
|
|
247
|
+
message: `No config, default not found: ${commandsPath}`,
|
|
248
|
+
name: "commands directory",
|
|
249
|
+
status: "warn"
|
|
250
|
+
});
|
|
251
|
+
return checkResult({
|
|
252
|
+
hint: "Create the directory or update \"commands\" in kidd.config.ts (fixable with --fix)",
|
|
253
|
+
message: `Not found: ${commandsPath}`,
|
|
254
|
+
name: "commands directory",
|
|
255
|
+
status: "fail"
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Check whether a tsconfig.json exists in the project root.
|
|
260
|
+
*
|
|
261
|
+
* @private
|
|
262
|
+
* @param context - The diagnostic check context.
|
|
263
|
+
* @returns A CheckResult indicating pass or warn.
|
|
264
|
+
*/
|
|
265
|
+
async function checkTsconfig(context) {
|
|
266
|
+
if (await fileExists(join(context.cwd, "tsconfig.json"))) return checkResult({
|
|
267
|
+
message: "Found",
|
|
268
|
+
name: "tsconfig.json",
|
|
269
|
+
status: "pass"
|
|
270
|
+
});
|
|
271
|
+
return checkResult({
|
|
272
|
+
hint: "Run \"tsc --init\" to create a tsconfig.json",
|
|
273
|
+
message: "Not found (recommended)",
|
|
274
|
+
name: "tsconfig.json",
|
|
275
|
+
status: "warn"
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Fix the module type by adding "type": "module" to package.json.
|
|
280
|
+
*
|
|
281
|
+
* @private
|
|
282
|
+
* @param context - The diagnostic check context.
|
|
283
|
+
* @returns A FixResult indicating whether the fix was applied.
|
|
284
|
+
*/
|
|
285
|
+
async function fixModuleType(context) {
|
|
286
|
+
const [updateError] = await updatePackageJson(context.cwd, (pkg) => ({
|
|
287
|
+
...pkg,
|
|
288
|
+
type: "module"
|
|
289
|
+
}));
|
|
290
|
+
if (updateError) return fixResult({
|
|
291
|
+
fixed: false,
|
|
292
|
+
message: updateError.message,
|
|
293
|
+
name: "module type"
|
|
294
|
+
});
|
|
295
|
+
return fixResult({
|
|
296
|
+
fixed: true,
|
|
297
|
+
message: "Added \"type\": \"module\" to package.json",
|
|
298
|
+
name: "module type"
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Fix the kidd dependency by adding it to package.json dependencies.
|
|
303
|
+
*
|
|
304
|
+
* @private
|
|
305
|
+
* @param context - The diagnostic check context.
|
|
306
|
+
* @returns A FixResult indicating whether the fix was applied.
|
|
307
|
+
*/
|
|
308
|
+
async function fixKiddDependency(context) {
|
|
309
|
+
const [updateError] = await updatePackageJson(context.cwd, (pkg) => {
|
|
310
|
+
const deps = pkg.dependencies;
|
|
311
|
+
return {
|
|
312
|
+
...pkg,
|
|
313
|
+
dependencies: {
|
|
314
|
+
...deps,
|
|
315
|
+
"@kidd-cli/core": "latest"
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
if (updateError) return fixResult({
|
|
320
|
+
fixed: false,
|
|
321
|
+
message: updateError.message,
|
|
322
|
+
name: "@kidd-cli/core dependency"
|
|
323
|
+
});
|
|
324
|
+
return fixResult({
|
|
325
|
+
fixed: true,
|
|
326
|
+
message: "Added \"@kidd-cli/core\": \"latest\" to dependencies",
|
|
327
|
+
name: "@kidd-cli/core dependency"
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Fix the entry point by creating the entry file with a minimal scaffold.
|
|
332
|
+
*
|
|
333
|
+
* @private
|
|
334
|
+
* @param context - The diagnostic check context.
|
|
335
|
+
* @returns A FixResult indicating whether the fix was applied.
|
|
336
|
+
*/
|
|
337
|
+
async function fixEntryPoint(context) {
|
|
338
|
+
const entryPath = resolveEntryPath(extractConfig(context.configResult));
|
|
339
|
+
const absolutePath = join(context.cwd, entryPath);
|
|
340
|
+
const [mkdirError] = await attemptAsync(() => mkdir(dirname(absolutePath), { recursive: true }));
|
|
341
|
+
if (mkdirError) return fixResult({
|
|
342
|
+
fixed: false,
|
|
343
|
+
message: `Failed to create directory: ${mkdirError.message}`,
|
|
344
|
+
name: "entry point"
|
|
345
|
+
});
|
|
346
|
+
const content = `import { create } from '@kidd-cli/core'\n`;
|
|
347
|
+
const [writeError] = await attemptAsync(() => writeFile(absolutePath, content, "utf8"));
|
|
348
|
+
if (writeError) return fixResult({
|
|
349
|
+
fixed: false,
|
|
350
|
+
message: `Failed to create file: ${writeError.message}`,
|
|
351
|
+
name: "entry point"
|
|
352
|
+
});
|
|
353
|
+
return fixResult({
|
|
354
|
+
fixed: true,
|
|
355
|
+
message: `Created ${entryPath}`,
|
|
356
|
+
name: "entry point"
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Fix the commands directory by creating it.
|
|
361
|
+
*
|
|
362
|
+
* @private
|
|
363
|
+
* @param context - The diagnostic check context.
|
|
364
|
+
* @returns A FixResult indicating whether the fix was applied.
|
|
365
|
+
*/
|
|
366
|
+
async function fixCommandsDirectory(context) {
|
|
367
|
+
const commandsPath = resolveCommandsPath(extractConfig(context.configResult));
|
|
368
|
+
const absolutePath = join(context.cwd, commandsPath);
|
|
369
|
+
const [mkdirError] = await attemptAsync(() => mkdir(absolutePath, { recursive: true }));
|
|
370
|
+
if (mkdirError) return fixResult({
|
|
371
|
+
fixed: false,
|
|
372
|
+
message: `Failed to create directory: ${mkdirError.message}`,
|
|
373
|
+
name: "commands directory"
|
|
374
|
+
});
|
|
375
|
+
return fixResult({
|
|
376
|
+
fixed: true,
|
|
377
|
+
message: `Created ${commandsPath}`,
|
|
378
|
+
name: "commands directory"
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* All diagnostic checks to run in order.
|
|
383
|
+
*/
|
|
384
|
+
const CHECKS = [
|
|
385
|
+
{
|
|
386
|
+
name: "kidd.config",
|
|
387
|
+
run: checkKiddConfig
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "config schema",
|
|
391
|
+
run: checkConfigSchema
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "package.json",
|
|
395
|
+
run: checkPackageJson
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "package version",
|
|
399
|
+
run: checkPackageVersion
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
fix: fixModuleType,
|
|
403
|
+
name: "module type",
|
|
404
|
+
run: checkModuleType
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
fix: fixKiddDependency,
|
|
408
|
+
name: "@kidd-cli/core dependency",
|
|
409
|
+
run: checkKiddDependency
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
fix: fixEntryPoint,
|
|
413
|
+
name: "entry point",
|
|
414
|
+
run: checkEntryPoint
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
fix: fixCommandsDirectory,
|
|
418
|
+
name: "commands directory",
|
|
419
|
+
run: checkCommandsDirectory
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "tsconfig.json",
|
|
423
|
+
run: checkTsconfig
|
|
424
|
+
}
|
|
425
|
+
];
|
|
426
|
+
/**
|
|
427
|
+
* Read and parse the raw package.json from a directory.
|
|
428
|
+
*
|
|
429
|
+
* Only extracts the fields needed by diagnostic checks that Manifest does not expose.
|
|
430
|
+
*
|
|
431
|
+
* @param cwd - The directory to read from.
|
|
432
|
+
* @returns A Result tuple with the raw package.json data or an error message.
|
|
433
|
+
*/
|
|
434
|
+
async function readRawPackageJson(cwd) {
|
|
435
|
+
const filePath = join(cwd, "package.json");
|
|
436
|
+
const [readError, content] = await attemptAsync(() => readFile(filePath, "utf8"));
|
|
437
|
+
if (readError) return err(`Failed to read package.json: ${readError.message}`);
|
|
438
|
+
const [parseError, data] = jsonParse(content);
|
|
439
|
+
if (parseError) return [parseError, null];
|
|
440
|
+
return ok(data);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Check whether a path exists on disk.
|
|
444
|
+
*
|
|
445
|
+
* @private
|
|
446
|
+
* @param filePath - The path to check.
|
|
447
|
+
* @returns True when the path is accessible, false otherwise.
|
|
448
|
+
*/
|
|
449
|
+
async function fileExists(filePath) {
|
|
450
|
+
const [accessError] = await attemptAsync(() => access(filePath));
|
|
451
|
+
return accessError === null;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Extract a KiddConfig from a load result, returning null when absent.
|
|
455
|
+
*
|
|
456
|
+
* @private
|
|
457
|
+
* @param result - The config load result, or null.
|
|
458
|
+
* @returns The config object or null.
|
|
459
|
+
*/
|
|
460
|
+
function extractConfig(result) {
|
|
461
|
+
if (result) return result.config;
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolve the entry path from config, falling back to the default.
|
|
466
|
+
*
|
|
467
|
+
* @private
|
|
468
|
+
* @param config - The loaded config or null.
|
|
469
|
+
* @returns The entry path string.
|
|
470
|
+
*/
|
|
471
|
+
function resolveEntryPath(config) {
|
|
472
|
+
if (config && config.entry) return config.entry;
|
|
473
|
+
return DEFAULT_ENTRY;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Resolve the commands directory path from config, falling back to the default.
|
|
477
|
+
*
|
|
478
|
+
* @private
|
|
479
|
+
* @param config - The loaded config or null.
|
|
480
|
+
* @returns The commands directory path string.
|
|
481
|
+
*/
|
|
482
|
+
function resolveCommandsPath(config) {
|
|
483
|
+
if (config && config.commands) return config.commands;
|
|
484
|
+
return DEFAULT_COMMANDS;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Read, transform, and write back a package.json file.
|
|
488
|
+
*
|
|
489
|
+
* @private
|
|
490
|
+
* @param cwd - The directory containing the package.json.
|
|
491
|
+
* @param transform - A pure function that returns the updated package data.
|
|
492
|
+
* @returns A Result tuple indicating success or failure.
|
|
493
|
+
*/
|
|
494
|
+
async function updatePackageJson(cwd, transform) {
|
|
495
|
+
const filePath = join(cwd, "package.json");
|
|
496
|
+
const [readError, content] = await attemptAsync(() => readFile(filePath, "utf8"));
|
|
497
|
+
if (readError) return err(`Failed to read package.json: ${readError.message}`);
|
|
498
|
+
const [parseError, data] = jsonParse(content);
|
|
499
|
+
if (parseError) return [parseError, null];
|
|
500
|
+
const [stringifyError, json] = jsonStringify(transform(data), { pretty: true });
|
|
501
|
+
if (stringifyError) return [stringifyError, null];
|
|
502
|
+
const [writeError] = await attemptAsync(() => writeFile(filePath, `${json}\n`, "utf8"));
|
|
503
|
+
if (writeError) return err(`Failed to write package.json: ${writeError.message}`);
|
|
504
|
+
return ok();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/commands/doctor.ts
|
|
509
|
+
/**
|
|
510
|
+
* Diagnose common kidd project issues.
|
|
511
|
+
*
|
|
512
|
+
* Validates config, checks package.json setup, verifies entry points exist,
|
|
513
|
+
* and catches anything that could cause build or runtime failures.
|
|
514
|
+
*/
|
|
515
|
+
const doctorCommand = command({
|
|
516
|
+
args: z.object({ fix: z.boolean().describe("Auto-fix issues where possible").optional() }),
|
|
517
|
+
description: "Diagnose common kidd project issues",
|
|
518
|
+
handler: async (ctx) => {
|
|
519
|
+
const cwd = process.cwd();
|
|
520
|
+
const shouldFix = ctx.args.fix === true;
|
|
521
|
+
const [configError, configResult] = await loadConfig({ cwd });
|
|
522
|
+
const [, manifest] = await readManifest(cwd);
|
|
523
|
+
const [, rawPackageJson] = await readRawPackageJson(cwd);
|
|
524
|
+
const context = createCheckContext({
|
|
525
|
+
configError,
|
|
526
|
+
configResult,
|
|
527
|
+
cwd,
|
|
528
|
+
manifest,
|
|
529
|
+
rawPackageJson
|
|
530
|
+
});
|
|
531
|
+
ctx.spinner.start("Running diagnostics...");
|
|
532
|
+
const initialResults = await Promise.all(CHECKS.map((check) => check.run(context)));
|
|
533
|
+
const fixResults = await resolveFixResults(shouldFix, initialResults, context);
|
|
534
|
+
const fixed = fixResults.filter((r) => r.fixed).length;
|
|
535
|
+
const results = await resolveResults({
|
|
536
|
+
cwd,
|
|
537
|
+
fixed,
|
|
538
|
+
initialResults,
|
|
539
|
+
shouldFix
|
|
540
|
+
});
|
|
541
|
+
ctx.spinner.stop("Diagnostics complete");
|
|
542
|
+
displayResults(ctx, results, fixResults);
|
|
543
|
+
const passed = results.filter((r) => r.status === "pass").length;
|
|
544
|
+
const warnings = results.filter((r) => r.status === "warn").length;
|
|
545
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
546
|
+
const summary = formatSummary({
|
|
547
|
+
failed,
|
|
548
|
+
fixed,
|
|
549
|
+
passed,
|
|
550
|
+
total: results.length,
|
|
551
|
+
warnings
|
|
552
|
+
});
|
|
553
|
+
ctx.output.raw(summary);
|
|
554
|
+
if (failed > 0) ctx.fail(`${failed} ${pluralizeCheck(failed)} failed`);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
/**
|
|
558
|
+
* Resolve fix results based on whether --fix is enabled.
|
|
559
|
+
*
|
|
560
|
+
* @private
|
|
561
|
+
* @param shouldFix - Whether the --fix flag was set.
|
|
562
|
+
* @param results - The initial check results.
|
|
563
|
+
* @param context - The diagnostic check context.
|
|
564
|
+
* @returns Fix results when --fix is enabled, empty array otherwise.
|
|
565
|
+
*/
|
|
566
|
+
async function resolveFixResults(shouldFix, results, context) {
|
|
567
|
+
if (!shouldFix) return [];
|
|
568
|
+
return applyFixes(results, context);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Resolve the final check results, re-running after fixes when needed.
|
|
572
|
+
*
|
|
573
|
+
* Rebuilds the context from disk so that fixes to package.json are reflected.
|
|
574
|
+
*
|
|
575
|
+
* @private
|
|
576
|
+
* @param params - The resolution parameters.
|
|
577
|
+
* @returns Updated check results if fixes were applied, otherwise the initial results.
|
|
578
|
+
*/
|
|
579
|
+
async function resolveResults(params) {
|
|
580
|
+
if (!params.shouldFix || params.fixed === 0) return params.initialResults;
|
|
581
|
+
const [configError, configResult] = await loadConfig({ cwd: params.cwd });
|
|
582
|
+
const [, manifest] = await readManifest(params.cwd);
|
|
583
|
+
const [, rawPackageJson] = await readRawPackageJson(params.cwd);
|
|
584
|
+
const freshContext = createCheckContext({
|
|
585
|
+
configError,
|
|
586
|
+
configResult,
|
|
587
|
+
cwd: params.cwd,
|
|
588
|
+
manifest,
|
|
589
|
+
rawPackageJson
|
|
590
|
+
});
|
|
591
|
+
return Promise.all(CHECKS.map((check) => check.run(freshContext)));
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Apply fixes for all non-passing checks that have a fix function.
|
|
595
|
+
*
|
|
596
|
+
* @private
|
|
597
|
+
* @param results - The initial check results.
|
|
598
|
+
* @param context - The diagnostic check context.
|
|
599
|
+
* @returns An array of FixResults for checks that were attempted.
|
|
600
|
+
*/
|
|
601
|
+
async function applyFixes(results, context) {
|
|
602
|
+
const fixable = CHECKS.filter((check) => {
|
|
603
|
+
if (!check.fix) return false;
|
|
604
|
+
const result = results.find((r) => r.name === check.name);
|
|
605
|
+
return result !== void 0 && result.status !== "pass";
|
|
606
|
+
}).flatMap((check) => {
|
|
607
|
+
if (check.fix) return [check.fix];
|
|
608
|
+
return [];
|
|
609
|
+
});
|
|
610
|
+
return Promise.all(fixable.map((fix) => fix(context)));
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Display check results with hints and fix indicators.
|
|
614
|
+
*
|
|
615
|
+
* @private
|
|
616
|
+
* @param ctx - The command context.
|
|
617
|
+
* @param results - The check results to display.
|
|
618
|
+
* @param fixResults - The fix results (empty when --fix was not used).
|
|
619
|
+
*/
|
|
620
|
+
function displayResults(ctx, results, fixResults) {
|
|
621
|
+
results.map((result) => formatResultLine(ctx, result, fixResults));
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Format and display a single check result line with optional hint.
|
|
625
|
+
*
|
|
626
|
+
* @private
|
|
627
|
+
* @param ctx - The command context.
|
|
628
|
+
* @param result - The check result to display.
|
|
629
|
+
* @param fixResults - The fix results to check for applied fixes.
|
|
630
|
+
*/
|
|
631
|
+
function formatResultLine(ctx, result, fixResults) {
|
|
632
|
+
const appliedFix = fixResults.find((f) => f.name === result.name && f.fixed);
|
|
633
|
+
if (appliedFix) {
|
|
634
|
+
ctx.output.raw(` ${formatDisplayStatus("fix")} ${result.name} - ${appliedFix.message}\n`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
ctx.output.raw(` ${formatDisplayStatus(result.status)} ${result.name} - ${result.message}\n`);
|
|
638
|
+
if (result.hint && result.status !== "pass") ctx.output.raw(` ${pc.dim(`→ ${result.hint}`)}\n`);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Format a display status with color.
|
|
642
|
+
*
|
|
643
|
+
* Accepts both CheckStatus values and the 'fix' display status.
|
|
644
|
+
*
|
|
645
|
+
* @private
|
|
646
|
+
* @param status - The status to format.
|
|
647
|
+
* @returns A colored string representation of the status.
|
|
648
|
+
*/
|
|
649
|
+
function formatDisplayStatus(status) {
|
|
650
|
+
if (status === "pass") return pc.green("pass");
|
|
651
|
+
if (status === "warn") return pc.yellow("warn");
|
|
652
|
+
if (status === "fix") return pc.blue("fix ");
|
|
653
|
+
return pc.red("fail");
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Format the summary line with counts.
|
|
657
|
+
*
|
|
658
|
+
* @private
|
|
659
|
+
* @param params - The count parameters.
|
|
660
|
+
* @returns A formatted summary string.
|
|
661
|
+
*/
|
|
662
|
+
function formatSummary(params) {
|
|
663
|
+
const base = `\n${params.total} checks, ${params.passed} passed, ${params.warnings} warnings, ${params.failed} failed`;
|
|
664
|
+
if (params.fixed > 0) return `${base}, ${params.fixed} fixed\n`;
|
|
665
|
+
return `${base}\n`;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Pluralize the word "check" based on count.
|
|
669
|
+
*
|
|
670
|
+
* @private
|
|
671
|
+
* @param count - The number of checks.
|
|
672
|
+
* @returns "check" or "checks".
|
|
673
|
+
*/
|
|
674
|
+
function pluralizeCheck(count) {
|
|
675
|
+
if (count === 1) return "check";
|
|
676
|
+
return "checks";
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//#endregion
|
|
680
|
+
export { doctorCommand as default };
|