codex-plugin-doctor 0.1.2 → 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 +19 -0
- package/dist/core/discover-installed-plugins.d.ts +12 -0
- package/dist/core/discover-installed-plugins.js +69 -0
- package/dist/core/validate-plugin.js +23 -4
- package/dist/run-cli.js +78 -9
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +1 -1
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
|
+
}
|
|
@@ -38,6 +38,26 @@ async function fileExists(targetPath) {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
async function readPackageName(rootPath) {
|
|
42
|
+
const packageJsonPath = path.join(rootPath, "package.json");
|
|
43
|
+
try {
|
|
44
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
45
|
+
return typeof packageJson.name === "string" ? packageJson.name : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function buildMissingManifestFailure(rootPath) {
|
|
52
|
+
const packageName = await readPackageName(rootPath);
|
|
53
|
+
if (packageName === "codex-plugin-doctor") {
|
|
54
|
+
return buildFailure("plugin.manifest.missing", "This looks like the Codex Plugin Doctor source repo, not a Codex plugin package.", "Codex Plugin Doctor validates plugin package roots that contain `.codex-plugin/plugin.json`; the tool source repo intentionally does not expose that manifest.", "For a local self-test, run `codex-plugin-doctor check examples/codex-doctor-runtime --runtime --no-animations`. To check your own plugin, pass that plugin package root instead.");
|
|
55
|
+
}
|
|
56
|
+
if (packageName) {
|
|
57
|
+
return buildFailure("plugin.manifest.missing", "This directory has a `package.json`, but it does not look like a Codex plugin package.", "Codex cannot treat a normal project directory as a plugin package without the required `.codex-plugin/plugin.json` entry point.", "Run from a Codex plugin package root, or pass the path to a directory that contains `.codex-plugin/plugin.json`.");
|
|
58
|
+
}
|
|
59
|
+
return buildFailure("plugin.manifest.missing", "This directory does not look like a Codex plugin package.", "Codex cannot treat this directory as a plugin package without the required `.codex-plugin/plugin.json` manifest entry point.", "Create `.codex-plugin/plugin.json` with at least `name`, `version`, and `description`, or pass the path to an existing Codex plugin package.");
|
|
60
|
+
}
|
|
41
61
|
function isPlainObject(value) {
|
|
42
62
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
63
|
}
|
|
@@ -299,13 +319,12 @@ async function validateMcpConfig(discoveredPackage) {
|
|
|
299
319
|
export async function validatePlugin(targetPath, options = {}) {
|
|
300
320
|
const discoveredPackage = await discoverPackage(targetPath);
|
|
301
321
|
if (!discoveredPackage) {
|
|
322
|
+
const rootPath = path.resolve(targetPath);
|
|
302
323
|
return {
|
|
303
|
-
targetPath:
|
|
324
|
+
targetPath: rootPath,
|
|
304
325
|
status: "fail",
|
|
305
326
|
exitCode: 1,
|
|
306
|
-
findings: [
|
|
307
|
-
buildFailure("plugin.manifest.missing", "Missing required `.codex-plugin/plugin.json` manifest.", "Codex cannot treat this directory as a plugin package without the required manifest entry point.", "Create `.codex-plugin/plugin.json` with at least `name`, `version`, and `description`.")
|
|
308
|
-
]
|
|
327
|
+
findings: [await buildMissingManifestFailure(rootPath)]
|
|
309
328
|
};
|
|
310
329
|
}
|
|
311
330
|
const runtimeResult = options.runtime
|
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
|
|
27
|
-
const
|
|
28
|
-
? [
|
|
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;
|
package/dist/version.js
ADDED
package/package.json
CHANGED