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 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.7.0
258
+ - uses: Esquetta/CodexPluginDoctor@v0.8.0
259
259
  with:
260
- version: "0.7.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
- "Codex Plugin Doctor Environment",
33
- "===============================",
34
- `Version: ${packageVersion}`,
35
- `Platform: ${terminalContext.platform ?? process.platform}`,
36
- `Node: ${process.version}`,
37
- `npm global prefix: ${npmPrefix}`,
38
- `Codex home: ${codexHomeExists ? "PASS" : "WARN"}${codexHome ? ` (${codexHome})` : ""}`,
39
- `Codex plugin cache: ${pluginCacheExists ? "PASS" : "WARN"}${pluginCache ? ` (${pluginCache})` : ""}`
40
- ].join("\n");
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
  }
@@ -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;
@@ -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,5 @@
1
+ export interface InitCiResult {
2
+ rootPath: string;
3
+ workflowPath: string;
4
+ }
5
+ export declare function initCiWorkflow(targetPath: string): Promise<InitCiResult>;
@@ -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(await renderEnvironmentDoctor(terminalContext));
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
- io.writeStdout(dryRun
215
- ? renderFixPlan(await buildFixPlan(maybePath), "dry-run")
216
- : renderApplyFixResult(await applyFixPlan(maybePath)));
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
- io.writeStderr("--install-preview and --apply require --client claude-desktop, cursor, or cline.");
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
- : await buildClaudeDesktopInstallPreview(targetPath, {
270
- env: terminalContext.env,
271
- platform: terminalContext.platform
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
- : "Claude Desktop", preview))
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
- : renderClaudeDesktopInstallPreview(preview);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",