Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .codexignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Local dependencies and generated packaging output
node_modules/
*.tgz

# Local scratch and notes
tasks/
.DS_Store
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run check:version-sync
- run: npm run lint
- run: npm run typecheck
- run: npm run test
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.release.tag_name || inputs.ref || github.ref_name }}
- uses: actions/setup-node@v6
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 24
cache: npm
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run check:version-sync
- run: npm run check
- run: npm pack --dry-run
- run: npm publish
25 changes: 25 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Security Policy

## Supported Versions

Security fixes are applied to the latest release on `main`.

| Version | Supported |
| --- | --- |
| Latest release | Yes |
| Older releases | No |

## Reporting a Vulnerability

Please do not report security vulnerabilities in public GitHub issues.

Use one of the following channels instead:

1. Open a private vulnerability report through GitHub Security Advisories for this repository, if that option is available to you.
2. Email [jin.ku@sendbird.com](mailto:jin.ku@sendbird.com) with:
- a description of the issue
- affected files or flows
- reproduction steps or proof of concept
- any suggested mitigation

Sendbird will review the report and respond as quickly as possible. When a report is confirmed, fixes will be developed on the latest supported version and coordinated for disclosure.
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@
"scripts": {
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json",
"check": "npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e",
"check:version-sync": "node scripts/check-version-sync.mjs",
"sync:plugin-version": "node scripts/sync-plugin-version.mjs",
"check": "npm run check:version-sync && npm run lint && npm run typecheck && npm run test && npm run test:integration && npm run test:e2e",
"install:codex": "node scripts/installer-cli.mjs install",
"prepack": "npm run check:version-sync",
"setup:git-hooks": "node scripts/setup-git-hooks.mjs",
"test": "node --test tests/*.test.mjs",
"test:integration": "node --test tests/integration/*.test.mjs",
"test:e2e": "node --test tests/e2e/*.test.mjs",
"uninstall:codex": "node scripts/installer-cli.mjs uninstall",
"update:codex": "node scripts/installer-cli.mjs update"
"update:codex": "node scripts/installer-cli.mjs update",
"version": "npm run sync:plugin-version && npm run check:version-sync"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stage synced plugin.json during npm version

The new version lifecycle script syncs .codex-plugin/plugin.json but never stages that change, so npm version will create a tag/commit without the synced plugin file. npm’s own npm-version command docs state that files generated in version scripts must be explicitly added (e.g., via git add) before the commit step, and without that this leaves a dirty working tree and can publish or push a commit where package.json and plugin metadata diverge.

Useful? React with 👍 / 👎.

},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
14 changes: 14 additions & 0 deletions scripts/check-version-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node

import { assertVersionsMatch } from "./lib/version-sync.mjs";

try {
const version = assertVersionsMatch();
process.stdout.write(
`Version sync OK: package.json and .codex-plugin/plugin.json are both ${version}.\n`
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
}
69 changes: 69 additions & 0 deletions scripts/lib/version-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const ROOT_DIR = path.resolve(fileURLToPath(new URL("../..", import.meta.url)));

export function resolveVersionSyncPaths(rootDir = ROOT_DIR) {
return {
rootDir,
packageJsonPath: path.join(rootDir, "package.json"),
pluginJsonPath: path.join(rootDir, ".codex-plugin", "plugin.json"),
};
}

function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}

function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

export function readVersionPair(rootDir = ROOT_DIR) {
const { packageJsonPath, pluginJsonPath } = resolveVersionSyncPaths(rootDir);
const packageJson = readJson(packageJsonPath);
const pluginJson = readJson(pluginJsonPath);
return {
packageJson,
pluginJson,
packageVersion: String(packageJson.version ?? ""),
pluginVersion: String(pluginJson.version ?? ""),
};
}

export function assertVersionsMatch(rootDir = ROOT_DIR) {
const { packageVersion, pluginVersion } = readVersionPair(rootDir);
if (!packageVersion || !pluginVersion) {
throw new Error(
"Both package.json and .codex-plugin/plugin.json must declare a version."
);
}
if (packageVersion !== pluginVersion) {
throw new Error(
`Version mismatch: package.json is ${packageVersion} but .codex-plugin/plugin.json is ${pluginVersion}.`
);
}
return packageVersion;
}

export function syncPluginVersionFromPackage(rootDir = ROOT_DIR) {
const { pluginJsonPath } = resolveVersionSyncPaths(rootDir);
const { packageJson, pluginJson, packageVersion, pluginVersion } =
readVersionPair(rootDir);

if (!packageVersion) {
throw new Error("package.json is missing a version.");
}

if (packageVersion === pluginVersion) {
return { changed: false, version: packageVersion };
}

const nextPluginJson = {
...pluginJson,
version: packageJson.version,
};
writeJson(pluginJsonPath, nextPluginJson);
return { changed: true, version: packageVersion };
}
16 changes: 16 additions & 0 deletions scripts/sync-plugin-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env node

import { syncPluginVersionFromPackage } from "./lib/version-sync.mjs";

try {
const result = syncPluginVersionFromPackage();
process.stdout.write(
result.changed
? `Updated .codex-plugin/plugin.json to version ${result.version}.\n`
: `.codex-plugin/plugin.json already matches package.json at ${result.version}.\n`
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
}
79 changes: 79 additions & 0 deletions tests/version-sync.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import {
assertVersionsMatch,
readVersionPair,
syncPluginVersionFromPackage,
} from "../scripts/lib/version-sync.mjs";

function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

function createTempRepo({ packageVersion, pluginVersion }) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-version-sync-"));
writeJson(path.join(dir, "package.json"), {
name: "cc-plugin-codex",
version: packageVersion,
});
writeJson(path.join(dir, ".codex-plugin", "plugin.json"), {
name: "cc",
version: pluginVersion,
});
return dir;
}

describe("version sync", () => {
it("asserts the live repo versions match", () => {
assert.equal(assertVersionsMatch(), "1.0.0");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove hardcoded release version from version-sync test

This assertion hardcodes the repository version to 1.0.0, which makes normal version bumps fail test runs even when package.json and .codex-plugin/plugin.json are correctly synchronized. That introduces avoidable CI/release friction because every future release now requires manually editing this test instead of validating version equality dynamically.

Useful? React with 👍 / 👎.

});

it("detects mismatched versions", () => {
const dir = createTempRepo({
packageVersion: "1.2.3",
pluginVersion: "1.2.2",
});
try {
assert.throws(
() => assertVersionsMatch(dir),
/Version mismatch: package\.json is 1\.2\.3 but \.codex-plugin\/plugin\.json is 1\.2\.2\./
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("syncs plugin.json from package.json", () => {
const dir = createTempRepo({
packageVersion: "2.0.0",
pluginVersion: "1.0.0",
});
try {
const result = syncPluginVersionFromPackage(dir);
assert.deepEqual(result, { changed: true, version: "2.0.0" });
const { packageVersion, pluginVersion } = readVersionPair(dir);
assert.equal(packageVersion, "2.0.0");
assert.equal(pluginVersion, "2.0.0");
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("does nothing when versions already match", () => {
const dir = createTempRepo({
packageVersion: "3.1.4",
pluginVersion: "3.1.4",
});
try {
const result = syncPluginVersionFromPackage(dir);
assert.deepEqual(result, { changed: false, version: "3.1.4" });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});