codex-plugin-doctor 0.7.0 → 0.8.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/README.md +2 -2
- package/dist/compatibility/compatibility-matrix.d.ts +1 -0
- package/dist/compatibility/compatibility-matrix.js +78 -1
- package/dist/compatibility/windsurf-install-preview.d.ts +10 -0
- package/dist/compatibility/windsurf-install-preview.js +65 -0
- package/dist/core/environment-doctor.d.ts +17 -0
- package/dist/core/environment-doctor.js +31 -10
- package/dist/core/fix-plan.d.ts +15 -0
- package/dist/core/fix-plan.js +128 -1
- package/dist/core/init-ci.d.ts +5 -0
- package/dist/core/init-ci.js +36 -0
- package/dist/run-cli.js +54 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,9 +255,9 @@ jobs:
|
|
|
255
255
|
runs-on: ubuntu-latest
|
|
256
256
|
steps:
|
|
257
257
|
- uses: actions/checkout@v4
|
|
258
|
-
- uses: Esquetta/CodexPluginDoctor@v0.
|
|
258
|
+
- uses: Esquetta/CodexPluginDoctor@v0.8.0
|
|
259
259
|
with:
|
|
260
|
-
version: "0.
|
|
260
|
+
version: "0.8.0"
|
|
261
261
|
path: .
|
|
262
262
|
runtime: "false"
|
|
263
263
|
```
|
|
@@ -18,5 +18,6 @@ export declare function readMcpConfigPath(targetPath: string): Promise<string |
|
|
|
18
18
|
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
19
|
export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
|
|
20
20
|
export declare function getClineMcpConfigPath(environment?: CompatibilityEnvironment): string;
|
|
21
|
+
export declare function getWindsurfMcpConfigPath(environment?: CompatibilityEnvironment): string;
|
|
21
22
|
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
22
23
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -141,6 +141,9 @@ export function getClineMcpConfigPath(environment = {}) {
|
|
|
141
141
|
: path.join(getHomeDirectory(environment), ".cline");
|
|
142
142
|
return path.join(clineDirectory, "data", "settings", "cline_mcp_settings.json");
|
|
143
143
|
}
|
|
144
|
+
export function getWindsurfMcpConfigPath(environment = {}) {
|
|
145
|
+
return path.join(getHomeDirectory(environment), ".codeium", "windsurf", "mcp_config.json");
|
|
146
|
+
}
|
|
144
147
|
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
145
148
|
if (genericMcpResult.status !== "pass") {
|
|
146
149
|
return {
|
|
@@ -365,12 +368,85 @@ async function checkCline(targetPath, genericMcpResult, environment = {}) {
|
|
|
365
368
|
};
|
|
366
369
|
}
|
|
367
370
|
}
|
|
371
|
+
async function checkWindsurf(targetPath, genericMcpResult, environment = {}) {
|
|
372
|
+
if (genericMcpResult.status !== "pass") {
|
|
373
|
+
return {
|
|
374
|
+
client: "Windsurf",
|
|
375
|
+
status: "skipped",
|
|
376
|
+
summary: "No valid MCP package config is available for Windsurf.",
|
|
377
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const configPath = getWindsurfMcpConfigPath(environment);
|
|
381
|
+
if (!(await fileExists(configPath))) {
|
|
382
|
+
const configDirectory = path.dirname(configPath);
|
|
383
|
+
return {
|
|
384
|
+
client: "Windsurf",
|
|
385
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
386
|
+
summary: await directoryExists(configDirectory)
|
|
387
|
+
? "Windsurf MCP config directory exists and a config file can be created."
|
|
388
|
+
: "Windsurf was not detected on this machine.",
|
|
389
|
+
details: [
|
|
390
|
+
configPath,
|
|
391
|
+
"Windsurf stores Cascade MCP servers in `mcp_config.json` under `~/.codeium/windsurf`."
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const parsed = await readJsonFile(configPath);
|
|
397
|
+
const servers = parsed.mcpServers;
|
|
398
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
399
|
+
servers === null ||
|
|
400
|
+
Array.isArray(servers))) {
|
|
401
|
+
return {
|
|
402
|
+
client: "Windsurf",
|
|
403
|
+
status: "fail",
|
|
404
|
+
summary: "Windsurf MCP config has an invalid `mcpServers` shape.",
|
|
405
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
409
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
410
|
+
? Object.keys(servers)
|
|
411
|
+
: [];
|
|
412
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
413
|
+
if (duplicateServerNames.length > 0) {
|
|
414
|
+
return {
|
|
415
|
+
client: "Windsurf",
|
|
416
|
+
status: "warn",
|
|
417
|
+
summary: "Windsurf already has MCP server names from this package.",
|
|
418
|
+
details: [
|
|
419
|
+
configPath,
|
|
420
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
421
|
+
]
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
client: "Windsurf",
|
|
426
|
+
status: "pass",
|
|
427
|
+
summary: "Windsurf MCP config is valid and this package can be added.",
|
|
428
|
+
details: [
|
|
429
|
+
configPath,
|
|
430
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
431
|
+
]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return {
|
|
436
|
+
client: "Windsurf",
|
|
437
|
+
status: "fail",
|
|
438
|
+
summary: "Windsurf MCP config is not valid JSON.",
|
|
439
|
+
details: [configPath, "Repair the local Windsurf MCP config before adding new MCP servers."]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
368
443
|
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
369
444
|
const rootPath = path.resolve(targetPath);
|
|
370
445
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
371
446
|
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
372
447
|
const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
|
|
373
448
|
const clineResult = await checkCline(rootPath, genericMcpResult, environment);
|
|
449
|
+
const windsurfResult = await checkWindsurf(rootPath, genericMcpResult, environment);
|
|
374
450
|
const codexResult = await validatePlugin(rootPath);
|
|
375
451
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
376
452
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -394,7 +470,8 @@ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
|
394
470
|
genericMcpResult,
|
|
395
471
|
claudeDesktopResult,
|
|
396
472
|
cursorResult,
|
|
397
|
-
clineResult
|
|
473
|
+
clineResult,
|
|
474
|
+
windsurfResult
|
|
398
475
|
];
|
|
399
476
|
return {
|
|
400
477
|
targetPath: rootPath,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface WindsurfInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildWindsurfInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<WindsurfInstallPreview>;
|
|
10
|
+
export declare function renderWindsurfInstallPreview(preview: WindsurfInstallPreview): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getWindsurfMcpConfigPath, readMcpConfigPath } from "./compatibility-matrix.js";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isRelativeLocalPath(value) {
|
|
8
|
+
return value.startsWith("./") ||
|
|
9
|
+
value.startsWith("../") ||
|
|
10
|
+
value.startsWith(".\\") ||
|
|
11
|
+
value.startsWith("..\\");
|
|
12
|
+
}
|
|
13
|
+
function normalizeLocalPathArgument(value, rootPath) {
|
|
14
|
+
return typeof value === "string" && isRelativeLocalPath(value)
|
|
15
|
+
? path.resolve(rootPath, value)
|
|
16
|
+
: value;
|
|
17
|
+
}
|
|
18
|
+
function normalizeServerConfig(serverConfig, rootPath) {
|
|
19
|
+
if (!isRecord(serverConfig)) {
|
|
20
|
+
return serverConfig;
|
|
21
|
+
}
|
|
22
|
+
const normalized = { ...serverConfig };
|
|
23
|
+
if (typeof normalized.command === "string" && isRelativeLocalPath(normalized.command)) {
|
|
24
|
+
normalized.command = path.resolve(rootPath, normalized.command);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(normalized.args)) {
|
|
27
|
+
normalized.args = normalized.args.map((argument) => normalizeLocalPathArgument(argument, rootPath));
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
export async function buildWindsurfInstallPreview(targetPath, environment = {}) {
|
|
32
|
+
const rootPath = path.resolve(targetPath);
|
|
33
|
+
const mcpConfigPath = await readMcpConfigPath(rootPath);
|
|
34
|
+
if (!mcpConfigPath) {
|
|
35
|
+
throw new Error("No MCP config found for install preview.");
|
|
36
|
+
}
|
|
37
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
38
|
+
const servers = parsed.mcpServers;
|
|
39
|
+
if (!isRecord(servers) || Object.keys(servers).length === 0) {
|
|
40
|
+
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
targetPath: rootPath,
|
|
44
|
+
configPath: getWindsurfMcpConfigPath(environment),
|
|
45
|
+
snippet: {
|
|
46
|
+
mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
|
|
47
|
+
serverName,
|
|
48
|
+
normalizeServerConfig(serverConfig, rootPath)
|
|
49
|
+
]))
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function renderWindsurfInstallPreview(preview) {
|
|
54
|
+
return [
|
|
55
|
+
"Windsurf Install Preview",
|
|
56
|
+
"========================",
|
|
57
|
+
`Target: ${preview.targetPath}`,
|
|
58
|
+
`Config: ${preview.configPath}`,
|
|
59
|
+
"",
|
|
60
|
+
"Add or merge this snippet into `mcp_config.json`:",
|
|
61
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
62
|
+
"",
|
|
63
|
+
"No files were modified."
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
import type { CliTerminalContext } from "../run-cli.js";
|
|
2
|
+
export interface EnvironmentDoctorReport {
|
|
3
|
+
schemaVersion: "1.0.0";
|
|
4
|
+
version: string;
|
|
5
|
+
platform: string;
|
|
6
|
+
node: string;
|
|
7
|
+
npmGlobalPrefix: string;
|
|
8
|
+
codexHome: {
|
|
9
|
+
status: "pass" | "warn";
|
|
10
|
+
path: string | null;
|
|
11
|
+
};
|
|
12
|
+
codexPluginCache: {
|
|
13
|
+
status: "pass" | "warn";
|
|
14
|
+
path: string | null;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
2
17
|
export declare function renderEnvironmentDoctor(terminalContext: CliTerminalContext): Promise<string>;
|
|
18
|
+
export declare function buildEnvironmentDoctorReport(terminalContext: CliTerminalContext): Promise<EnvironmentDoctorReport>;
|
|
19
|
+
export declare function renderEnvironmentDoctorJson(terminalContext: CliTerminalContext): Promise<string>;
|
|
@@ -23,19 +23,40 @@ function resolveCodexHome(env) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
export async function renderEnvironmentDoctor(terminalContext) {
|
|
26
|
+
const report = await buildEnvironmentDoctorReport(terminalContext);
|
|
27
|
+
return [
|
|
28
|
+
"Codex Plugin Doctor Environment",
|
|
29
|
+
"===============================",
|
|
30
|
+
`Version: ${report.version}`,
|
|
31
|
+
`Platform: ${report.platform}`,
|
|
32
|
+
`Node: ${report.node}`,
|
|
33
|
+
`npm global prefix: ${report.npmGlobalPrefix}`,
|
|
34
|
+
`Codex home: ${report.codexHome.status.toUpperCase()}${report.codexHome.path ? ` (${report.codexHome.path})` : ""}`,
|
|
35
|
+
`Codex plugin cache: ${report.codexPluginCache.status.toUpperCase()}${report.codexPluginCache.path ? ` (${report.codexPluginCache.path})` : ""}`
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
export async function buildEnvironmentDoctorReport(terminalContext) {
|
|
26
39
|
const codexHome = resolveCodexHome(terminalContext.env);
|
|
27
40
|
const codexHomeExists = codexHome ? await pathExists(codexHome) : false;
|
|
28
41
|
const pluginCache = codexHome ? path.join(codexHome, "plugins", "cache") : null;
|
|
29
42
|
const pluginCacheExists = pluginCache ? await pathExists(pluginCache) : false;
|
|
30
43
|
const npmPrefix = terminalContext.env.npm_config_prefix ?? "(unknown)";
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: "1.0.0",
|
|
46
|
+
version: packageVersion,
|
|
47
|
+
platform: terminalContext.platform ?? process.platform,
|
|
48
|
+
node: process.version,
|
|
49
|
+
npmGlobalPrefix: npmPrefix,
|
|
50
|
+
codexHome: {
|
|
51
|
+
status: codexHomeExists ? "pass" : "warn",
|
|
52
|
+
path: codexHome
|
|
53
|
+
},
|
|
54
|
+
codexPluginCache: {
|
|
55
|
+
status: pluginCacheExists ? "pass" : "warn",
|
|
56
|
+
path: pluginCache
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export async function renderEnvironmentDoctorJson(terminalContext) {
|
|
61
|
+
return JSON.stringify(await buildEnvironmentDoctorReport(terminalContext), null, 2);
|
|
41
62
|
}
|
package/dist/core/fix-plan.d.ts
CHANGED
|
@@ -14,7 +14,22 @@ export interface ApplyFixPlanResult {
|
|
|
14
14
|
filesChanged: number;
|
|
15
15
|
backupDirectory: string;
|
|
16
16
|
}
|
|
17
|
+
export interface FixPlanJsonReport {
|
|
18
|
+
schemaVersion: "1.0.0";
|
|
19
|
+
mode: "dry-run" | "apply";
|
|
20
|
+
targetPath: string;
|
|
21
|
+
filesChanged: number;
|
|
22
|
+
backupDirectory: string | null;
|
|
23
|
+
actions: Array<FixPlanAction & {
|
|
24
|
+
relativePath: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
17
27
|
export declare function buildFixPlan(targetPath: string): Promise<FixPlan>;
|
|
18
28
|
export declare function renderFixPlan(plan: FixPlan, mode: "dry-run"): string;
|
|
19
29
|
export declare function applyFixPlan(targetPath: string): Promise<ApplyFixPlanResult>;
|
|
20
30
|
export declare function renderApplyFixResult(result: ApplyFixPlanResult): string;
|
|
31
|
+
export declare function renderFixPlanJsonReport(plan: FixPlan, options: {
|
|
32
|
+
mode: "dry-run" | "apply";
|
|
33
|
+
filesChanged?: number;
|
|
34
|
+
backupDirectory?: string | null;
|
|
35
|
+
}): string;
|
package/dist/core/fix-plan.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { validatePlugin } from "./validate-plugin.js";
|
|
4
4
|
function relativeToTarget(targetPath, candidatePath) {
|
|
@@ -33,6 +33,53 @@ export async function buildFixPlan(targetPath) {
|
|
|
33
33
|
details: "Create the skills directory referenced by the manifest."
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
const manifest = await readFile(manifestPath, "utf8")
|
|
37
|
+
.then((content) => JSON.parse(content))
|
|
38
|
+
.catch(() => ({}));
|
|
39
|
+
if (typeof manifest.skills === "string") {
|
|
40
|
+
const skillsPath = path.resolve(rootPath, manifest.skills);
|
|
41
|
+
if (await directoryExists(skillsPath)) {
|
|
42
|
+
for (const entry of await readdir(skillsPath, { withFileTypes: true })) {
|
|
43
|
+
if (!entry.isDirectory()) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const skillFilePath = path.join(skillsPath, entry.name, "SKILL.md");
|
|
47
|
+
if (!(await fileExists(skillFilePath))) {
|
|
48
|
+
actions.push({
|
|
49
|
+
id: "skill.scaffold_skill_md",
|
|
50
|
+
title: `Create missing SKILL.md for ${entry.name}`,
|
|
51
|
+
targetPath: skillFilePath,
|
|
52
|
+
operation: "update-json",
|
|
53
|
+
details: `Create ${relativeToTarget(rootPath, skillFilePath)} with safe frontmatter.`
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const skillContent = await readFile(skillFilePath, "utf8");
|
|
58
|
+
const frontmatter = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
|
|
59
|
+
if (!/^name\s*:/im.test(frontmatter) || !/^description\s*:/im.test(frontmatter)) {
|
|
60
|
+
actions.push({
|
|
61
|
+
id: "skill.safe_frontmatter_defaults",
|
|
62
|
+
title: `Add missing skill frontmatter defaults for ${entry.name}`,
|
|
63
|
+
targetPath: skillFilePath,
|
|
64
|
+
operation: "update-json",
|
|
65
|
+
details: `Set missing name/description fields in ${relativeToTarget(rootPath, skillFilePath)}.`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof manifest.mcpServers === "string") {
|
|
72
|
+
const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
|
|
73
|
+
if (!(await fileExists(mcpConfigPath))) {
|
|
74
|
+
actions.push({
|
|
75
|
+
id: "mcp.scaffold_config",
|
|
76
|
+
title: "Create missing MCP config",
|
|
77
|
+
targetPath: mcpConfigPath,
|
|
78
|
+
operation: "update-json",
|
|
79
|
+
details: `Create ${relativeToTarget(rootPath, mcpConfigPath)} with an empty mcpServers object.`
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
36
83
|
return {
|
|
37
84
|
targetPath: rootPath,
|
|
38
85
|
actions
|
|
@@ -66,6 +113,54 @@ function timestampForPath() {
|
|
|
66
113
|
function defaultManifestDescription() {
|
|
67
114
|
return "Codex plugin package.";
|
|
68
115
|
}
|
|
116
|
+
async function fileExists(targetPath) {
|
|
117
|
+
try {
|
|
118
|
+
const details = await stat(targetPath);
|
|
119
|
+
return details.isFile();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function directoryExists(targetPath) {
|
|
126
|
+
try {
|
|
127
|
+
const details = await stat(targetPath);
|
|
128
|
+
return details.isDirectory();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function skillDescription(skillName) {
|
|
135
|
+
return `Use when running the ${skillName} skill.`;
|
|
136
|
+
}
|
|
137
|
+
function renderSkillScaffold(skillName, body = "") {
|
|
138
|
+
return [
|
|
139
|
+
"---",
|
|
140
|
+
`name: ${skillName}`,
|
|
141
|
+
`description: ${skillDescription(skillName)}`,
|
|
142
|
+
"---",
|
|
143
|
+
"",
|
|
144
|
+
body.trim() || `# ${skillName}`
|
|
145
|
+
].join("\n") + "\n";
|
|
146
|
+
}
|
|
147
|
+
function replaceFrontmatter(content, skillName) {
|
|
148
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
149
|
+
const frontmatter = frontmatterMatch?.[1] ?? "";
|
|
150
|
+
const body = frontmatterMatch ? content.slice(frontmatterMatch[0].length) : content;
|
|
151
|
+
const lines = frontmatter.split(/\r?\n/).filter((line) => line.trim());
|
|
152
|
+
const hasName = lines.some((line) => /^name\s*:/i.test(line));
|
|
153
|
+
const hasDescription = lines.some((line) => /^description\s*:/i.test(line));
|
|
154
|
+
return [
|
|
155
|
+
"---",
|
|
156
|
+
...(hasName ? [] : [`name: ${skillName}`]),
|
|
157
|
+
...(hasDescription ? [] : [`description: ${skillDescription(skillName)}`]),
|
|
158
|
+
...lines,
|
|
159
|
+
"---",
|
|
160
|
+
"",
|
|
161
|
+
body.trim() || `# ${skillName}`
|
|
162
|
+
].join("\n") + "\n";
|
|
163
|
+
}
|
|
69
164
|
async function backupFile(rootPath, backupDirectory, filePath) {
|
|
70
165
|
const relativePath = path.relative(rootPath, filePath);
|
|
71
166
|
const backupPath = path.join(backupDirectory, relativePath);
|
|
@@ -94,6 +189,24 @@ export async function applyFixPlan(targetPath) {
|
|
|
94
189
|
if (action.operation === "mkdir" && action.id === "skills.create_directory") {
|
|
95
190
|
await mkdir(action.targetPath, { recursive: true });
|
|
96
191
|
filesChanged += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (action.operation === "update-json" && action.id === "skill.scaffold_skill_md") {
|
|
195
|
+
await mkdir(path.dirname(action.targetPath), { recursive: true });
|
|
196
|
+
await writeFile(action.targetPath, renderSkillScaffold(path.basename(path.dirname(action.targetPath))), "utf8");
|
|
197
|
+
filesChanged += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (action.operation === "update-json" && action.id === "skill.safe_frontmatter_defaults") {
|
|
201
|
+
await backupFile(plan.targetPath, backupDirectory, action.targetPath);
|
|
202
|
+
await writeFile(action.targetPath, replaceFrontmatter(await readFile(action.targetPath, "utf8"), path.basename(path.dirname(action.targetPath))), "utf8");
|
|
203
|
+
filesChanged += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (action.operation === "update-json" && action.id === "mcp.scaffold_config") {
|
|
207
|
+
await mkdir(path.dirname(action.targetPath), { recursive: true });
|
|
208
|
+
await writeFile(action.targetPath, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`, "utf8");
|
|
209
|
+
filesChanged += 1;
|
|
97
210
|
}
|
|
98
211
|
}
|
|
99
212
|
return {
|
|
@@ -112,3 +225,17 @@ export function renderApplyFixResult(result) {
|
|
|
112
225
|
`Backup: ${relativeToTarget(result.plan.targetPath, result.backupDirectory)}`
|
|
113
226
|
].join("\n");
|
|
114
227
|
}
|
|
228
|
+
export function renderFixPlanJsonReport(plan, options) {
|
|
229
|
+
const report = {
|
|
230
|
+
schemaVersion: "1.0.0",
|
|
231
|
+
mode: options.mode,
|
|
232
|
+
targetPath: plan.targetPath,
|
|
233
|
+
filesChanged: options.filesChanged ?? 0,
|
|
234
|
+
backupDirectory: options.backupDirectory ?? null,
|
|
235
|
+
actions: plan.actions.map((action) => ({
|
|
236
|
+
...action,
|
|
237
|
+
relativePath: relativeToTarget(plan.targetPath, action.targetPath)
|
|
238
|
+
}))
|
|
239
|
+
};
|
|
240
|
+
return JSON.stringify(report, null, 2);
|
|
241
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { packageVersion } from "../version.js";
|
|
4
|
+
function buildWorkflow() {
|
|
5
|
+
return [
|
|
6
|
+
"name: Validate Codex plugin",
|
|
7
|
+
"",
|
|
8
|
+
"on:",
|
|
9
|
+
" pull_request:",
|
|
10
|
+
" push:",
|
|
11
|
+
" branches:",
|
|
12
|
+
" - main",
|
|
13
|
+
"",
|
|
14
|
+
"jobs:",
|
|
15
|
+
" doctor:",
|
|
16
|
+
" runs-on: ubuntu-latest",
|
|
17
|
+
" steps:",
|
|
18
|
+
" - uses: actions/checkout@v4",
|
|
19
|
+
` - uses: Esquetta/CodexPluginDoctor@v${packageVersion}`,
|
|
20
|
+
" with:",
|
|
21
|
+
` version: \"${packageVersion}\"`,
|
|
22
|
+
" path: .",
|
|
23
|
+
" runtime: \"true\"",
|
|
24
|
+
""
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
export async function initCiWorkflow(targetPath) {
|
|
28
|
+
const rootPath = path.resolve(targetPath);
|
|
29
|
+
const workflowPath = path.join(rootPath, ".github", "workflows", "codex-plugin-doctor.yml");
|
|
30
|
+
await mkdir(path.dirname(workflowPath), { recursive: true });
|
|
31
|
+
await writeFile(workflowPath, buildWorkflow(), "utf8");
|
|
32
|
+
return {
|
|
33
|
+
rootPath,
|
|
34
|
+
workflowPath
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -8,9 +8,11 @@ import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/a
|
|
|
8
8
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
9
9
|
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
10
10
|
import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibility/cline-install-preview.js";
|
|
11
|
+
import { buildWindsurfInstallPreview, renderWindsurfInstallPreview } from "./compatibility/windsurf-install-preview.js";
|
|
11
12
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
12
|
-
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlan } from "./core/fix-plan.js";
|
|
13
|
-
import { renderEnvironmentDoctor } from "./core/environment-doctor.js";
|
|
13
|
+
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
14
|
+
import { renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
15
|
+
import { initCiWorkflow } from "./core/init-ci.js";
|
|
14
16
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
15
17
|
import { runCheck } from "./index.js";
|
|
16
18
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
@@ -37,7 +39,7 @@ const defaultIo = {
|
|
|
37
39
|
}
|
|
38
40
|
};
|
|
39
41
|
function printUsage(io) {
|
|
40
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
42
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
41
43
|
}
|
|
42
44
|
function renderInstalledPlugins(plugins) {
|
|
43
45
|
const lines = [
|
|
@@ -64,7 +66,8 @@ const compatibilityClientAliases = {
|
|
|
64
66
|
"claude-desktop": "Claude Desktop",
|
|
65
67
|
claude: "Claude Desktop",
|
|
66
68
|
cursor: "Cursor",
|
|
67
|
-
cline: "Cline"
|
|
69
|
+
cline: "Cline",
|
|
70
|
+
windsurf: "Windsurf"
|
|
68
71
|
};
|
|
69
72
|
const checkProfiles = ["ci", "strict", "publish"];
|
|
70
73
|
function parseCheckProfile(value) {
|
|
@@ -130,7 +133,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
130
133
|
return 0;
|
|
131
134
|
}
|
|
132
135
|
if (command === "doctor") {
|
|
133
|
-
io.writeStdout(
|
|
136
|
+
io.writeStdout(maybePath === "--json"
|
|
137
|
+
? await renderEnvironmentDoctorJson(terminalContext)
|
|
138
|
+
: await renderEnvironmentDoctor(terminalContext));
|
|
134
139
|
return 0;
|
|
135
140
|
}
|
|
136
141
|
if (command === "explain") {
|
|
@@ -195,6 +200,16 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
195
200
|
].join("\n"));
|
|
196
201
|
return 0;
|
|
197
202
|
}
|
|
203
|
+
if (command === "init-ci") {
|
|
204
|
+
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
205
|
+
const result = await initCiWorkflow(targetPath);
|
|
206
|
+
io.writeStdout([
|
|
207
|
+
"Initialized Codex Plugin Doctor workflow",
|
|
208
|
+
`Root: ${result.rootPath}`,
|
|
209
|
+
`Workflow: ${result.workflowPath}`
|
|
210
|
+
].join("\n"));
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
198
213
|
if (command === "fix") {
|
|
199
214
|
if (!maybePath || maybePath.startsWith("--")) {
|
|
200
215
|
io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--apply --backup)");
|
|
@@ -203,6 +218,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
203
218
|
const dryRun = remainingArgs.includes("--dry-run");
|
|
204
219
|
const apply = remainingArgs.includes("--apply");
|
|
205
220
|
const backup = remainingArgs.includes("--backup");
|
|
221
|
+
const jsonOutput = remainingArgs.includes("--json");
|
|
206
222
|
if (apply && !backup) {
|
|
207
223
|
io.writeStderr("Fix apply requires --backup.");
|
|
208
224
|
return 2;
|
|
@@ -211,9 +227,21 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
211
227
|
io.writeStderr("Choose exactly one fix mode: --dry-run or --apply --backup.");
|
|
212
228
|
return 2;
|
|
213
229
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
if (dryRun) {
|
|
231
|
+
const plan = await buildFixPlan(maybePath);
|
|
232
|
+
io.writeStdout(jsonOutput
|
|
233
|
+
? renderFixPlanJsonReport(plan, { mode: "dry-run" })
|
|
234
|
+
: renderFixPlan(plan, "dry-run"));
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
const result = await applyFixPlan(maybePath);
|
|
238
|
+
io.writeStdout(jsonOutput
|
|
239
|
+
? renderFixPlanJsonReport(result.plan, {
|
|
240
|
+
mode: "apply",
|
|
241
|
+
filesChanged: result.filesChanged,
|
|
242
|
+
backupDirectory: result.backupDirectory
|
|
243
|
+
})
|
|
244
|
+
: renderApplyFixResult(result));
|
|
217
245
|
return 0;
|
|
218
246
|
}
|
|
219
247
|
if (command === "compat") {
|
|
@@ -241,8 +269,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
241
269
|
if ((installPreview || applyInstall) &&
|
|
242
270
|
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
243
271
|
clientFilter?.toLowerCase() !== "cursor" &&
|
|
244
|
-
clientFilter?.toLowerCase() !== "cline"
|
|
245
|
-
|
|
272
|
+
clientFilter?.toLowerCase() !== "cline" &&
|
|
273
|
+
clientFilter?.toLowerCase() !== "windsurf") {
|
|
274
|
+
io.writeStderr("--install-preview and --apply require --client claude-desktop, cursor, cline, or windsurf.");
|
|
246
275
|
return 2;
|
|
247
276
|
}
|
|
248
277
|
if (installPreview && applyInstall) {
|
|
@@ -266,21 +295,30 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
266
295
|
env: terminalContext.env,
|
|
267
296
|
platform: terminalContext.platform
|
|
268
297
|
})
|
|
269
|
-
:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
298
|
+
: normalizedClient === "windsurf"
|
|
299
|
+
? await buildWindsurfInstallPreview(targetPath, {
|
|
300
|
+
env: terminalContext.env,
|
|
301
|
+
platform: terminalContext.platform
|
|
302
|
+
})
|
|
303
|
+
: await buildClaudeDesktopInstallPreview(targetPath, {
|
|
304
|
+
env: terminalContext.env,
|
|
305
|
+
platform: terminalContext.platform
|
|
306
|
+
});
|
|
273
307
|
const report = applyInstall
|
|
274
308
|
? renderApplyInstallResult(await applyInstallPreview(normalizedClient === "cursor"
|
|
275
309
|
? "Cursor"
|
|
276
310
|
: normalizedClient === "cline"
|
|
277
311
|
? "Cline"
|
|
278
|
-
:
|
|
312
|
+
: normalizedClient === "windsurf"
|
|
313
|
+
? "Windsurf"
|
|
314
|
+
: "Claude Desktop", preview))
|
|
279
315
|
: normalizedClient === "cursor"
|
|
280
316
|
? renderCursorInstallPreview(preview)
|
|
281
317
|
: normalizedClient === "cline"
|
|
282
318
|
? renderClineInstallPreview(preview)
|
|
283
|
-
:
|
|
319
|
+
: normalizedClient === "windsurf"
|
|
320
|
+
? renderWindsurfInstallPreview(preview)
|
|
321
|
+
: renderClaudeDesktopInstallPreview(preview);
|
|
284
322
|
if (outputPath) {
|
|
285
323
|
await writeFile(outputPath, report, "utf8");
|
|
286
324
|
}
|
package/package.json
CHANGED