mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 23:56:29 +03:00
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:
+14
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user