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
+14 -1
View File
@@ -22,8 +22,9 @@ import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProj
import { generateApiData } from './lib/api-data.js';
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
import { hooksJsonFor, buildClaudePluginHooksManifest } from './lib/transformers/hooks.js';
import { createAllZips } from './lib/zip.js';
import { createAllZips, createProviderZip } from './lib/zip.js';
import { collectPluginVersions } from './lib/validate-plugin-versions.js';
import { stageOpenAIPlugin } from './lib/openai-plugin.js';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
// Sub-page generation is now handled by Astro content collections.
@@ -748,6 +749,12 @@ async function build() {
if (fs.existsSync(pluginSkillsDir)) fs.rmSync(pluginSkillsDir, { recursive: true });
if (fs.existsSync(pluginAgentsDir)) fs.rmSync(pluginAgentsDir, { recursive: true });
if (fs.existsSync(pluginHooksDir)) fs.rmSync(pluginHooksDir, { recursive: true });
// Clean up the short-lived mixed-provider subtree from early OpenAI plugin
// development. The canonical Codex preview now lives in dist/openai/.
for (const staleRel of ['.codex-plugin', 'assets']) {
const stalePath = path.join(pluginRoot, staleRel);
if (fs.existsSync(stalePath)) fs.rmSync(stalePath, { recursive: true });
}
const rootManifest = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, '.claude-plugin/plugin.json'), 'utf-8'));
const claudeAgentsSrc = path.join(DIST_DIR, 'claude-code', '.claude', 'agents');
@@ -798,6 +805,12 @@ async function build() {
console.log('📋 Skipped root harness and plugin sync (--skip-root-sync)');
}
// The public OpenAI plugin is a Codex artifact, not a copy of the tracked
// Claude marketplace subtree. Build it on every source-first build so the
// upload ZIP and local preview directory cannot drift behind provider output.
const openAiPluginRoot = stageOpenAIPlugin(ROOT_DIR, DIST_DIR);
await createProviderZip(openAiPluginRoot, DIST_DIR, 'openai-plugin');
// Generate authoritative counts and validate references
const countErrors = generateCounts(ROOT_DIR, skills, buildDir);
+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),
+2 -1
View File
@@ -29,7 +29,7 @@ export const SUITES = {
/^site\/(pages|content|components|layouts)\//,
/^README(\.npm)?\.md$/,
/^cli\/bin\//,
/^tests\/(build|cleanup-deprecated|cli-ignores|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|shiki-theme|skills-cli|target-args|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/,
/^tests\/(build|cleanup-deprecated|cli-ignores|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|shiki-theme|skills-cli|target-args|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/,
/^tests\/lib\//,
],
commands: [
@@ -62,6 +62,7 @@ export const SUITES = {
'tests/hook-build.test.mjs',
'tests/hook.test.mjs',
'tests/impeccable-paths.test.mjs',
'tests/openai-plugin.test.mjs',
'tests/target-args.test.mjs',
'tests/shiki-theme.test.mjs',
'tests/test-suites.test.mjs',