codex-plugin-doctor 0.1.3 → 0.1.4

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
@@ -71,11 +71,20 @@ Global install from npm:
71
71
 
72
72
  ```bash
73
73
  npm install -g codex-plugin-doctor
74
+ codex-plugin-doctor --version
74
75
  codex-plugin-doctor check path/to/plugin-package
75
76
  ```
76
77
 
77
78
  Run `codex-plugin-doctor check .` from the root of a Codex plugin package that contains `.codex-plugin/plugin.json`. The Codex Plugin Doctor source repository is not itself a plugin package.
78
79
 
80
+ If you already have Codex installed locally and do not know plugin paths, discover the installed plugin cache:
81
+
82
+ ```bash
83
+ codex-plugin-doctor list --installed
84
+ codex-plugin-doctor check --installed
85
+ codex-plugin-doctor check --installed github
86
+ ```
87
+
79
88
  Run from source:
80
89
 
81
90
  ```bash
@@ -144,6 +153,7 @@ x plugin.security.hard_coded_secret
144
153
  Run these from a Codex plugin package root:
145
154
 
146
155
  ```bash
156
+ codex-plugin-doctor --version
147
157
  codex-plugin-doctor check .
148
158
  codex-plugin-doctor check . --json
149
159
  codex-plugin-doctor check . --json --output report.json
@@ -154,6 +164,15 @@ codex-plugin-doctor check . --runtime
154
164
  codex-plugin-doctor check . --json --runtime --verbose-runtime
155
165
  ```
156
166
 
167
+ Run these when you want Codex Plugin Doctor to find plugins from the local Codex installation:
168
+
169
+ ```bash
170
+ codex-plugin-doctor list --installed
171
+ codex-plugin-doctor check --installed
172
+ codex-plugin-doctor check --installed github
173
+ codex-plugin-doctor check --installed github --runtime --no-animations
174
+ ```
175
+
157
176
  To self-test this repository after cloning it:
158
177
 
159
178
  ```bash
@@ -0,0 +1,12 @@
1
+ export interface InstalledPlugin {
2
+ name: string;
3
+ version?: string;
4
+ rootPath: string;
5
+ manifestPath: string;
6
+ relativePath: string;
7
+ }
8
+ export interface InstalledPluginDiscoveryOptions {
9
+ env?: Record<string, string | undefined>;
10
+ }
11
+ export declare function discoverInstalledPlugins(options?: InstalledPluginDiscoveryOptions): Promise<InstalledPlugin[]>;
12
+ export declare function filterInstalledPlugins(plugins: InstalledPlugin[], query: string | null): InstalledPlugin[];
@@ -0,0 +1,69 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ async function directoryExists(targetPath) {
5
+ try {
6
+ const details = await stat(targetPath);
7
+ return details.isDirectory();
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ function getCodexHomeCandidates(env) {
14
+ const candidates = env.CODEX_HOME
15
+ ? [env.CODEX_HOME]
16
+ : [path.join(os.homedir(), ".codex")];
17
+ return [...new Set(candidates.map((candidate) => path.resolve(candidate)))];
18
+ }
19
+ async function findManifestPaths(rootPath) {
20
+ const entries = await readdir(rootPath, { withFileTypes: true });
21
+ const manifestPaths = [];
22
+ for (const entry of entries) {
23
+ const entryPath = path.join(rootPath, entry.name);
24
+ if (entry.isDirectory()) {
25
+ if (entry.name === ".codex-plugin") {
26
+ manifestPaths.push(path.join(entryPath, "plugin.json"));
27
+ continue;
28
+ }
29
+ manifestPaths.push(...(await findManifestPaths(entryPath)));
30
+ }
31
+ }
32
+ return manifestPaths;
33
+ }
34
+ export async function discoverInstalledPlugins(options = {}) {
35
+ const env = options.env ?? process.env;
36
+ const plugins = [];
37
+ for (const codexHome of getCodexHomeCandidates(env)) {
38
+ const cacheRoot = path.join(codexHome, "plugins", "cache");
39
+ if (!(await directoryExists(cacheRoot))) {
40
+ continue;
41
+ }
42
+ for (const manifestPath of await findManifestPaths(cacheRoot)) {
43
+ try {
44
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
45
+ const rootPath = path.dirname(path.dirname(manifestPath));
46
+ const relativePath = path.relative(cacheRoot, rootPath);
47
+ plugins.push({
48
+ name: typeof manifest.name === "string" ? manifest.name : path.basename(rootPath),
49
+ version: typeof manifest.version === "string" ? manifest.version : undefined,
50
+ rootPath,
51
+ manifestPath,
52
+ relativePath
53
+ });
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ }
59
+ }
60
+ return plugins.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
61
+ }
62
+ export function filterInstalledPlugins(plugins, query) {
63
+ if (!query) {
64
+ return plugins;
65
+ }
66
+ const normalizedQuery = query.toLowerCase();
67
+ return plugins.filter((plugin) => [plugin.name, plugin.relativePath, plugin.rootPath]
68
+ .some((value) => value.toLowerCase().includes(normalizedQuery)));
69
+ }
package/dist/run-cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { writeFile } from "node:fs/promises";
2
+ import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
2
3
  import { runCheck } from "./index.js";
3
4
  import { renderJsonReport } from "./reporting/render-json-report.js";
4
5
  import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
@@ -6,6 +7,7 @@ import { renderTextReport } from "./reporting/render-text-report.js";
6
7
  import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
7
8
  import { determineOutputPolicy } from "./terminal/output-policy.js";
8
9
  import { getSpinner } from "./terminal/spinner-registry.js";
10
+ import { packageVersion } from "./version.js";
9
11
  const defaultIo = {
10
12
  writeStdout(message) {
11
13
  process.stdout.write(`${message}\n`);
@@ -15,18 +17,60 @@ const defaultIo = {
15
17
  }
16
18
  };
17
19
  function printUsage(io) {
18
- io.writeStderr("Usage: codex-plugin-doctor check <path> [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]");
20
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor list --installed\n codex-plugin-doctor --version");
21
+ }
22
+ function renderInstalledPlugins(plugins) {
23
+ const lines = [
24
+ "Installed Codex Plugins",
25
+ "======================="
26
+ ];
27
+ if (plugins.length === 0) {
28
+ lines.push("", "No installed Codex plugins found.");
29
+ return lines.join("\n");
30
+ }
31
+ for (const plugin of plugins) {
32
+ const version = plugin.version ? `@${plugin.version}` : "";
33
+ lines.push("", `- ${plugin.name}${version}`);
34
+ lines.push(` Path: ${plugin.rootPath}`);
35
+ lines.push(` Cache: ${plugin.relativePath}`);
36
+ }
37
+ return lines.join("\n");
19
38
  }
20
39
  export async function runCli(args, io = defaultIo, options = {}) {
21
40
  const [command, maybePath, ...remainingArgs] = args;
41
+ if (command === "--version" || command === "-v" || command === "version") {
42
+ io.writeStdout(packageVersion);
43
+ return 0;
44
+ }
45
+ const terminalContext = options.terminalContext ?? {
46
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
47
+ stderrIsTTY: Boolean(process.stderr.isTTY),
48
+ env: process.env
49
+ };
50
+ if (command === "list" && maybePath === "--installed") {
51
+ const installedPlugins = await discoverInstalledPlugins({
52
+ env: terminalContext.env
53
+ });
54
+ io.writeStdout(renderInstalledPlugins(installedPlugins));
55
+ return 0;
56
+ }
22
57
  if (command !== "check") {
23
58
  printUsage(io);
24
59
  return 2;
25
60
  }
26
- const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
27
- const normalizedFlags = maybePath && maybePath.startsWith("--")
28
- ? [maybePath, ...remainingArgs]
61
+ const checkInstalled = maybePath === "--installed";
62
+ const installedFilter = checkInstalled && remainingArgs[0] && !remainingArgs[0].startsWith("--")
63
+ ? remainingArgs[0]
64
+ : null;
65
+ const flagsAfterInstalledFilter = checkInstalled && installedFilter
66
+ ? remainingArgs.slice(1)
29
67
  : remainingArgs;
68
+ const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
69
+ const normalizedFlags = checkInstalled
70
+ ? [maybePath, ...flagsAfterInstalledFilter]
71
+ : maybePath && maybePath.startsWith("--")
72
+ ? [maybePath, ...remainingArgs]
73
+ : remainingArgs;
30
74
  const jsonOutput = normalizedFlags.includes("--json");
31
75
  const markdownOutput = normalizedFlags.includes("--markdown");
32
76
  const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
@@ -39,11 +83,6 @@ export async function runCli(args, io = defaultIo, options = {}) {
39
83
  io.writeStderr("Missing path after --output.");
40
84
  return 2;
41
85
  }
42
- const terminalContext = options.terminalContext ?? {
43
- stdoutIsTTY: Boolean(process.stdout.isTTY),
44
- stderrIsTTY: Boolean(process.stderr.isTTY),
45
- env: process.env
46
- };
47
86
  const outputPolicy = determineOutputPolicy({
48
87
  jsonOutput,
49
88
  markdownOutput,
@@ -55,6 +94,36 @@ export async function runCli(args, io = defaultIo, options = {}) {
55
94
  env: terminalContext.env
56
95
  });
57
96
  const runCheckImpl = options.runCheckImpl ?? runCheck;
97
+ if (checkInstalled) {
98
+ const installedPlugins = filterInstalledPlugins(await discoverInstalledPlugins({ env: terminalContext.env }), installedFilter);
99
+ if (installedPlugins.length === 0) {
100
+ io.writeStderr(installedFilter
101
+ ? `No installed Codex plugins matched '${installedFilter}'.`
102
+ : "No installed Codex plugins found.");
103
+ return 1;
104
+ }
105
+ const results = [];
106
+ for (const plugin of installedPlugins) {
107
+ results.push(await runCheckImpl(plugin.rootPath, {
108
+ runtime: runtimeProbeEnabled,
109
+ runtimeTranscript: runtimeProbeEnabled && verboseRuntime
110
+ ? (line) => io.writeStderr(line)
111
+ : undefined
112
+ }));
113
+ }
114
+ const report = results
115
+ .map((result) => markdownOutput
116
+ ? buildMarkdownReport(result, { runtimeProbeEnabled })
117
+ : jsonOutput
118
+ ? renderJsonReport(result, { runtimeProbeEnabled })
119
+ : renderTextReport(result, { ascii: outputPolicy.style === "ascii" }))
120
+ .join("\n\n");
121
+ if (outputPath) {
122
+ await writeFile(outputPath, report, "utf8");
123
+ }
124
+ io.writeStdout(report);
125
+ return results.some((result) => result.exitCode === 1) ? 1 : 0;
126
+ }
58
127
  const renderer = outputPolicy.interactive
59
128
  && !verboseRuntime
60
129
  ? createLiveStatusRenderer(io, getSpinner(outputPolicy.style === "ascii" ? "ascii" : "doctor"))
@@ -0,0 +1 @@
1
+ export declare const packageVersion: string;
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const packageJson = require("../package.json");
4
+ export const packageVersion = packageJson.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",