mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add API baseline checks for Obsidian and Codex
This commit is contained in:
parent
e6966995a6
commit
6e5c99d0c8
6 changed files with 269 additions and 7 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: codex-panel-app-server-update
|
||||
description: Use when updating Codex Panel for a new Codex CLI or app-server API version, regenerating app-server TypeScript bindings, handling generated binding diffs, adjusting app-server compatibility, or updating the README tested Codex CLI version.
|
||||
description: Use when updating Codex Panel for a new Codex CLI or app-server API version, regenerating app-server TypeScript bindings, handling generated binding diffs, adjusting app-server compatibility, or updating the README Compatibility table.
|
||||
---
|
||||
|
||||
# Codex Panel App-Server Update
|
||||
|
|
@ -17,12 +17,12 @@ Use this skill when Codex Panel needs to follow Codex CLI or experimental `codex
|
|||
## Procedure
|
||||
|
||||
1. Read the README Compatibility and Development sections, `package.json`, and app-server-related source around requests, notifications, threads, approvals, Plan mode, hooks, and model listing.
|
||||
2. Compare the README tested Codex CLI version with the target `codex --version`.
|
||||
2. Compare the README Compatibility table's `codex.testedCliVersion` with the target `codex --version`.
|
||||
3. Check official Codex CLI or app-server release information when the target version is newer or behavior is uncertain.
|
||||
4. Regenerate bindings with `npm run generate:app-server-types`.
|
||||
5. Review generated diffs for protocol changes that affect runtime behavior. If mechanical normalization is needed, update `scripts/normalize-generated-types.mjs` and regenerate instead of patching generated files by hand.
|
||||
6. Implement only the compatibility changes needed for the target Codex CLI version.
|
||||
7. Update the README tested Codex CLI version only after validating against that version.
|
||||
7. Update the README Compatibility table's `codex.testedCliVersion` only after validating against that version.
|
||||
|
||||
## Verification
|
||||
|
||||
|
|
|
|||
|
|
@ -82,9 +82,12 @@ The plugin does not store API keys.
|
|||
|
||||
## Compatibility
|
||||
|
||||
Codex Panel depends on the experimental `codex app-server` API. Later Codex CLI releases may require regenerated app-server bindings or compatibility fixes.
|
||||
| Key | Version | Policy |
|
||||
| ------------------------ | --------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `obsidian.minAppVersion` | `1.12.0` | Track the latest patch in this minor; raise this baseline when adopting a newer Obsidian API minor. |
|
||||
| `codex.testedCliVersion` | `0.130.0` | Manage app-server compatibility by Codex CLI minor version. |
|
||||
|
||||
The current release is developed and tested with Codex CLI 0.130.0.
|
||||
Codex Panel depends on the experimental `codex app-server` API. Later Codex CLI releases may require regenerated app-server bindings or compatibility fixes.
|
||||
|
||||
## Project Docs
|
||||
|
||||
|
|
|
|||
|
|
@ -37,3 +37,16 @@ npm run check
|
|||
```
|
||||
|
||||
The generation script uses `codex app-server generate-ts --experimental` because the panel depends on experimental app-server fields such as collaboration mode and generated v2 types.
|
||||
|
||||
## API Baselines
|
||||
|
||||
Use the local API baseline report when checking whether the development environment matches the supported API policy:
|
||||
|
||||
```sh
|
||||
npm run api:baseline
|
||||
npm run api:baseline:check
|
||||
```
|
||||
|
||||
Obsidian API compatibility follows semver within the guaranteed minor. `manifest.json` and `versions.json` define the minimum supported Obsidian app minor with a `.0` patch version, while the `obsidian` npm dev dependency tracks the latest patch in that same minor using a `~` range. When the Obsidian API minor is raised, update `manifest.json`, `versions.json`, README requirements, and the `obsidian` dev dependency together.
|
||||
|
||||
Codex app-server compatibility is managed by Codex CLI minor version. README records the tested Codex CLI patch version, and the baseline check verifies that the local `codex --version` is in the same minor before app-server binding or compatibility work.
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -16,7 +16,7 @@
|
|||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"obsidian": "^1.12.3",
|
||||
"obsidian": "~1.12.3",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.5"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
"dev": "node esbuild.config.mjs --watch",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,mjs,json,css,md}\" \"tests/**/*.{ts,tsx,js,mjs,json,css,md}\" \"scripts/**/*.{js,mjs}\" \"*.{json,mjs,md,css}\" \"!src/generated/**\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,mjs,json,css,md}\" \"tests/**/*.{ts,tsx,js,mjs,json,css,md}\" \"scripts/**/*.{js,mjs}\" \"*.{json,mjs,md,css}\" \"!src/generated/**\"",
|
||||
"api:baseline": "node scripts/report-api-baseline.mjs",
|
||||
"api:baseline:check": "node scripts/report-api-baseline.mjs --check",
|
||||
"generate:app-server-types": "codex app-server generate-ts --experimental --out src/generated/app-server && node scripts/normalize-generated-types.mjs",
|
||||
"lint": "eslint src tests --max-warnings=0",
|
||||
"release:check": "node scripts/check-release.mjs",
|
||||
|
|
@ -36,7 +38,7 @@
|
|||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"obsidian": "^1.12.3",
|
||||
"obsidian": "~1.12.3",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.5"
|
||||
|
|
|
|||
244
scripts/report-api-baseline.mjs
Normal file
244
scripts/report-api-baseline.mjs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const shouldCheck = args.has("--check");
|
||||
const asJson = args.has("--json");
|
||||
|
||||
function fail(message) {
|
||||
failures.push(message);
|
||||
}
|
||||
|
||||
function warn(message) {
|
||||
warnings.push(message);
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
}
|
||||
|
||||
function parseSemver(value) {
|
||||
const match = String(value ?? "").match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
version: match[0],
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
function minorKey(version) {
|
||||
return version ? `${version.major}.${version.minor}` : null;
|
||||
}
|
||||
|
||||
function readCodexVersion() {
|
||||
const result = spawnSync("codex", ["--version"], {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: false,
|
||||
});
|
||||
if (result.error || result.status !== 0) return null;
|
||||
return parseSemver(`${result.stdout}\n${result.stderr}`)?.version ?? null;
|
||||
}
|
||||
|
||||
function readCompatibilityBaselines(readme) {
|
||||
const section = markdownSection(readme, "Compatibility");
|
||||
const table = readMarkdownTableValues(section);
|
||||
return {
|
||||
codexTestedCliVersion: table.get("codex.testedCliVersion") ?? null,
|
||||
obsidianMinAppVersion: table.get("obsidian.minAppVersion") ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function markdownSection(markdown, heading) {
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
const headingLine = `## ${heading}`;
|
||||
const start = lines.findIndex((line) => line.trim() === headingLine);
|
||||
if (start === -1) return "";
|
||||
const end = lines.findIndex((line, index) => index > start && /^##\s+\S/.test(line));
|
||||
return lines.slice(start + 1, end === -1 ? undefined : end).join("\n");
|
||||
}
|
||||
|
||||
function readMarkdownTableValues(markdown) {
|
||||
const values = new Map();
|
||||
const lines = markdown.split("\n").map((line) => line.trim());
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const header = splitMarkdownTableRow(lines[index]);
|
||||
if (!header) continue;
|
||||
const separator = splitMarkdownTableRow(lines[index + 1]);
|
||||
if (!separator || !separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
||||
|
||||
const keyColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "key");
|
||||
const versionColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "version");
|
||||
if (keyColumn === -1 || versionColumn === -1) continue;
|
||||
|
||||
for (index += 2; index < lines.length; index += 1) {
|
||||
const row = splitMarkdownTableRow(lines[index]);
|
||||
if (!row) break;
|
||||
const key = inlineCodeValue(row[keyColumn]);
|
||||
const version = inlineCodeValue(row[versionColumn]);
|
||||
if (key && version) values.set(key, version);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function splitMarkdownTableRow(line) {
|
||||
if (!line?.startsWith("|") || !line.endsWith("|")) return null;
|
||||
return line
|
||||
.slice(1, -1)
|
||||
.split("|")
|
||||
.map((cell) => cell.trim());
|
||||
}
|
||||
|
||||
function normalizeTableHeader(value) {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function inlineCodeValue(value) {
|
||||
return value?.match(/`([^`]+)`/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
function packageRangeKind(spec) {
|
||||
const value = String(spec ?? "");
|
||||
if (value.startsWith("~")) return "patch";
|
||||
if (/^\d+\.\d+\.\d+$/.test(value)) return "exact";
|
||||
if (value.startsWith("^")) return "minor";
|
||||
return "other";
|
||||
}
|
||||
|
||||
function displayValue(value) {
|
||||
return value ?? "(missing)";
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
|
||||
const packageJson = await readJson("package.json");
|
||||
const packageLockJson = await readJson("package-lock.json");
|
||||
const manifestJson = await readJson("manifest.json");
|
||||
const versionsJson = await readJson("versions.json");
|
||||
const readme = await readFile("README.md", "utf8");
|
||||
const clientSource = await readFile("src/app-server/client.ts", "utf8");
|
||||
const readmeBaselines = readCompatibilityBaselines(readme);
|
||||
|
||||
const codexReadmeVersion = readmeBaselines.codexTestedCliVersion;
|
||||
const codexLocalVersion = readCodexVersion();
|
||||
const codexReadmeSemver = parseSemver(codexReadmeVersion);
|
||||
const codexLocalSemver = parseSemver(codexLocalVersion);
|
||||
|
||||
const obsidianMinVersion = manifestJson.minAppVersion;
|
||||
const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion;
|
||||
const obsidianVersionEntry = versionsJson[packageJson.version] ?? null;
|
||||
const obsidianSpec = packageJson.devDependencies?.obsidian ?? null;
|
||||
const obsidianLockVersion = packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null;
|
||||
const obsidianSpecSemver = parseSemver(obsidianSpec);
|
||||
const obsidianLockSemver = parseSemver(obsidianLockVersion);
|
||||
const obsidianMinSemver = parseSemver(obsidianMinVersion);
|
||||
|
||||
const appServerGenerateScript = packageJson.scripts?.["generate:app-server-types"] ?? "";
|
||||
const appServerGenerationExperimental =
|
||||
appServerGenerateScript.includes("codex app-server generate-ts") && appServerGenerateScript.includes("--experimental");
|
||||
const initializeExperimentalApi = /experimentalApi:\s*true/.test(clientSource);
|
||||
|
||||
if (!codexReadmeSemver) {
|
||||
fail("README.md Compatibility table must define `codex.testedCliVersion` as X.Y.Z.");
|
||||
}
|
||||
if (!codexLocalSemver) {
|
||||
const message = "local codex --version could not be read.";
|
||||
if (shouldCheck) fail(message);
|
||||
else warn(message);
|
||||
}
|
||||
if (codexReadmeSemver && codexLocalSemver && minorKey(codexReadmeSemver) !== minorKey(codexLocalSemver)) {
|
||||
fail(`local Codex CLI minor ${minorKey(codexLocalSemver)} does not match compatibility table minor ${minorKey(codexReadmeSemver)}.`);
|
||||
}
|
||||
if (!appServerGenerationExperimental) fail("generate:app-server-types must use codex app-server generate-ts --experimental.");
|
||||
if (!initializeExperimentalApi) fail("app-server initialize must declare experimentalApi: true.");
|
||||
|
||||
if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z.");
|
||||
if (obsidianMinSemver && obsidianMinSemver.patch !== 0) {
|
||||
fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`);
|
||||
}
|
||||
if (!parseSemver(obsidianReadmeMinVersion)) {
|
||||
fail("README.md Compatibility table must define `obsidian.minAppVersion` as X.Y.Z.");
|
||||
} else if (obsidianReadmeMinVersion !== obsidianMinVersion) {
|
||||
fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`);
|
||||
}
|
||||
if (obsidianVersionEntry !== obsidianMinVersion) {
|
||||
fail(`versions.json must map ${packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`);
|
||||
}
|
||||
if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version.");
|
||||
if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z.");
|
||||
if (packageRangeKind(obsidianSpec) !== "patch") {
|
||||
fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`);
|
||||
}
|
||||
if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) {
|
||||
fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
|
||||
}
|
||||
if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) {
|
||||
fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
|
||||
}
|
||||
|
||||
const report = {
|
||||
codex: {
|
||||
policy: "managed by minor version",
|
||||
readmeTestedCliVersion: codexReadmeVersion,
|
||||
readmeTestedMinor: minorKey(codexReadmeSemver),
|
||||
localCliVersion: codexLocalVersion,
|
||||
localCliMinor: minorKey(codexLocalSemver),
|
||||
localCliMatchesTestedMinor: codexReadmeSemver && codexLocalSemver ? minorKey(codexReadmeSemver) === minorKey(codexLocalSemver) : null,
|
||||
appServerGenerationExperimental,
|
||||
initializeExperimentalApi,
|
||||
},
|
||||
obsidian: {
|
||||
policy: "track latest patch within the guaranteed minor; raise minAppVersion when the minor changes",
|
||||
minAppVersion: obsidianMinVersion,
|
||||
minAppVersionMinor: minorKey(obsidianMinSemver),
|
||||
readmeMinAppVersion: obsidianReadmeMinVersion,
|
||||
versionsJsonCurrentMinAppVersion: obsidianVersionEntry,
|
||||
packageDependency: obsidianSpec,
|
||||
packageDependencyRange: packageRangeKind(obsidianSpec),
|
||||
packageDependencyMinor: minorKey(obsidianSpecSemver),
|
||||
lockedPackageVersion: obsidianLockVersion,
|
||||
lockedPackageMinor: minorKey(obsidianLockSemver),
|
||||
},
|
||||
warnings,
|
||||
failures,
|
||||
};
|
||||
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
console.log("API baseline");
|
||||
console.log("");
|
||||
console.log("Codex app-server");
|
||||
console.log(` policy: ${report.codex.policy}`);
|
||||
console.log(` compatibility table CLI: ${displayValue(report.codex.readmeTestedCliVersion)}`);
|
||||
console.log(` compatibility table minor: ${displayValue(report.codex.readmeTestedMinor)}`);
|
||||
console.log(` local codex CLI: ${displayValue(report.codex.localCliVersion)}`);
|
||||
console.log(` local codex minor: ${displayValue(report.codex.localCliMinor)}`);
|
||||
console.log(` generate-ts --experimental: ${report.codex.appServerGenerationExperimental ? "yes" : "no"}`);
|
||||
console.log(` initialize experimentalApi: ${report.codex.initializeExperimentalApi ? "yes" : "no"}`);
|
||||
console.log("");
|
||||
console.log("Obsidian API");
|
||||
console.log(` policy: ${report.obsidian.policy}`);
|
||||
console.log(` manifest minAppVersion: ${displayValue(report.obsidian.minAppVersion)}`);
|
||||
console.log(` compatibility table minAppVersion: ${displayValue(report.obsidian.readmeMinAppVersion)}`);
|
||||
console.log(` versions.json current minAppVersion: ${displayValue(report.obsidian.versionsJsonCurrentMinAppVersion)}`);
|
||||
console.log(` package obsidian: ${displayValue(report.obsidian.packageDependency)}`);
|
||||
console.log(` package range: ${report.obsidian.packageDependencyRange}`);
|
||||
console.log(` package-lock obsidian: ${displayValue(report.obsidian.lockedPackageVersion)}`);
|
||||
if (warnings.length > 0) {
|
||||
console.log("");
|
||||
console.log("Warnings");
|
||||
for (const message of warnings) console.log(` - ${message}`);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
console.log("");
|
||||
console.log("Failures");
|
||||
for (const message of failures) console.log(` - ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheck && failures.length > 0) process.exit(1);
|
||||
Loading…
Reference in a new issue