Add OpenAI plugin submission bundle

Build a Codex-native OpenAI plugin with bundled hooks, public listing metadata, submission guidance, privacy coverage, and regression tests.

AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.
This commit is contained in:
Paul Bakaus
2026-07-09 16:42:13 -07:00
parent 4c5b3aa45a
commit 3d0312cc94
47 changed files with 489 additions and 77 deletions
+44
View File
@@ -0,0 +1,44 @@
export function buildCodexPluginManifest(rootManifest) {
return {
name: rootManifest.name,
version: rootManifest.version,
description: 'Design and refine frontend interfaces with coding agents.',
author: {
...rootManifest.author,
name: 'Renaissance Geek Inc',
url: rootManifest.homepage,
},
homepage: rootManifest.homepage,
repository: rootManifest.repository,
license: 'Apache-2.0',
keywords: [
'design',
'frontend',
'ui',
'ux',
'accessibility',
'anti-patterns',
],
skills: './skills/',
interface: {
displayName: 'Impeccable',
shortDescription: 'Design and refine interfaces',
longDescription: 'Create, critique, and refine frontend interfaces with your coding agent. Impeccable provides 23 focused design commands, live browser iteration for exploring visual directions, and automatic checks that flag common design anti-patterns as you work.',
developerName: 'Renaissance Geek Inc',
category: 'Creativity',
capabilities: ['Interactive', 'Read', 'Write'],
websiteURL: rootManifest.homepage,
privacyPolicyURL: `${rootManifest.homepage}/privacy`,
termsOfServiceURL: `${rootManifest.repository}/blob/main/LICENSE`,
defaultPrompt: [
'critique this interface and prioritize what to fix.',
'craft a distinctive landing page, then polish the result.',
'start impeccable live so I can explore bolder or more delightful variants.',
],
brandColor: '#E2AE38',
composerIcon: './assets/icon.png',
logo: './assets/icon.png',
screenshots: [],
},
};
}
+61
View File
@@ -0,0 +1,61 @@
import fs from 'fs';
import path from 'path';
import { buildCodexPluginManifest } from './codex-plugin.js';
import { buildCodexPluginHooksManifest } from './transformers/hooks.js';
function requirePath(absPath, label) {
if (!fs.existsSync(absPath)) {
throw new Error(`Cannot build OpenAI plugin: missing ${label}: ${absPath}`);
}
}
function writeJson(absPath, value) {
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, `${JSON.stringify(value, null, 2)}\n`);
}
/**
* Stage the public OpenAI plugin from the Codex-transformed skill payload.
*
* The tracked ./plugin subtree is a Claude Code marketplace artifact. Reusing
* its shared skills/ directory here silently ships Claude paths and slash
* commands inside a Codex plugin. Keep this stage independent so each plugin
* receives the provider transform it was built for.
*/
export function stageOpenAIPlugin(rootDir, distDir) {
const rootManifestPath = path.join(rootDir, '.claude-plugin', 'plugin.json');
const codexSkillSrc = path.join(distDir, 'codex', '.codex', 'skills', 'impeccable');
const iconSrc = path.join(rootDir, 'site', 'public', 'apple-touch-icon.png');
requirePath(rootManifestPath, 'root plugin manifest');
requirePath(codexSkillSrc, 'Codex skill payload');
requirePath(iconSrc, 'plugin icon');
const pluginRoot = path.join(distDir, 'openai', 'impeccable');
fs.rmSync(pluginRoot, { recursive: true, force: true });
fs.mkdirSync(pluginRoot, { recursive: true });
const rootManifest = JSON.parse(fs.readFileSync(rootManifestPath, 'utf8'));
writeJson(
path.join(pluginRoot, '.codex-plugin', 'plugin.json'),
buildCodexPluginManifest(rootManifest),
);
fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true });
fs.copyFileSync(iconSrc, path.join(pluginRoot, 'assets', 'icon.png'));
fs.mkdirSync(path.join(pluginRoot, 'skills'), { recursive: true });
fs.cpSync(
codexSkillSrc,
path.join(pluginRoot, 'skills', 'impeccable'),
{ recursive: true },
);
writeJson(
path.join(pluginRoot, 'hooks', 'hooks.json'),
buildCodexPluginHooksManifest(),
);
return pluginRoot;
}
+7 -1
View File
@@ -262,7 +262,13 @@ export function createTransformer(config) {
const scriptsOutDir = path.join(skillDir, 'scripts');
ensureDir(scriptsOutDir);
for (const script of skill.scripts) {
writeFile(path.join(scriptsOutDir, script.name), script.content);
const scriptContent = replacePlaceholders(
script.content,
placeholderKey,
[],
allSkillNames,
);
writeFile(path.join(scriptsOutDir, script.name), scriptContent);
scriptCount++;
}
}
+27
View File
@@ -11,6 +11,9 @@
* 2. Claude Code plugin package (the marketplace / `/plugin install` path):
* - `plugin/hooks/hooks.json` (${CLAUDE_PLUGIN_ROOT}-relative)
*
* 3. OpenAI plugin package:
* - `hooks/hooks.json` (${PLUGIN_ROOT}-relative)
*
* The plugin variant resolves the hook script relative to the installed plugin
* root rather than assuming a `.claude/skills/impeccable/` layout, so it stays
* correct wherever Claude Code unpacks the plugin.
@@ -22,6 +25,7 @@ const TIMEOUT_SECONDS = 5;
const STATUS_MESSAGE = 'Checking UI changes';
const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs';
const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
const CODEX_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
const CODEX_PROJECT_HOOK = '.agents/skills/impeccable/scripts/hook.mjs';
const CURSOR_BEFORE_EDIT_SCRIPT = '.cursor/skills/impeccable/scripts/hook-before-edit.mjs';
const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs';
@@ -73,6 +77,29 @@ export function buildClaudePluginHooksManifest() {
};
}
// OpenAI plugin-packaged variant. Codex exposes ${PLUGIN_ROOT} for resources
// inside the installed plugin, so the public bundle can use the native path
// instead of relying on its Claude compatibility alias.
export function buildCodexPluginHooksManifest() {
return {
hooks: {
PostToolUse: [
{
matcher: 'Edit|Write|apply_patch',
hooks: [
{
type: 'command',
command: `node "${CODEX_PLUGIN_HOOK}"`,
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
},
};
}
export function buildCodexHooksManifest() {
return {
hooks: {
+4 -2
View File
@@ -739,12 +739,14 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
.replace(/\{\{available_commands\}\}/g, commandList);
// Replace `/skillname` invocations with the correct command prefix for this provider
// (e.g., `/normalize` → `$normalize` for Codex)
// (e.g., `/normalize` → `$normalize` for Codex). Require the slash to be
// outside a path or URL so `.github/hooks/impeccable.json` and
// `.codex/skills/impeccable` remain untouched.
if (cmdPrefix !== '/' && allSkillNames.length > 0) {
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
for (const name of sorted) {
result = result.replace(
new RegExp(`\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
new RegExp(`(?<![a-zA-Z0-9_./-])\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
cmdPrefix
);
}
+6
View File
@@ -15,6 +15,8 @@
* subtree fails loudly instead of merging a drift window onto main.
* - `plugin/skills/impeccable/SKILL.md` frontmatter version — generated;
* same rationale.
* - `dist/openai/impeccable/.codex-plugin/plugin.json` version — generated
* for public OpenAI submission and checked when that build output exists.
*
* The collector is pure (filesystem-in, data-out) so it can be unit-tested
* against fixtures; build.js owns the logging and the non-zero exit.
@@ -96,6 +98,10 @@ export function collectPluginVersions(rootDir) {
relPath: 'plugin/.claude-plugin/plugin.json',
read: (raw) => JSON.parse(raw).version,
},
{
relPath: 'dist/openai/impeccable/.codex-plugin/plugin.json',
read: (raw) => JSON.parse(raw).version,
},
{
relPath: 'plugin/skills/impeccable/SKILL.md',
read: (raw) => readSkillFrontmatterVersion(raw),