mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 00:26:41 +03:00
Add OpenAI plugin submission bundle (#363)
* 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. * Fix provider script command rendering Replace heuristic rewrites across executable scripts with one explicit provider marker, render pinned shortcuts per target harness, and remove the personal email from the public publisher manifest. Addresses automated review feedback on PR #363. 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,43 @@
|
||||
export function buildCodexPluginManifest(rootManifest) {
|
||||
return {
|
||||
name: rootManifest.name,
|
||||
version: rootManifest.version,
|
||||
description: 'Design and refine frontend interfaces with coding agents.',
|
||||
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;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
generateYamlFrontmatter,
|
||||
generateYamlDocument,
|
||||
replacePlaceholders,
|
||||
replaceScriptProviderMarker,
|
||||
compileProviderBlocks,
|
||||
stripRuleMarkers,
|
||||
} from '../utils.js';
|
||||
@@ -262,7 +263,8 @@ 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 = replaceScriptProviderMarker(script.content, placeholderKey);
|
||||
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: {
|
||||
|
||||
+20
-2
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -753,6 +755,22 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the one explicit provider marker allowed in executable skill scripts.
|
||||
*
|
||||
* Do not run replacePlaceholders() across JavaScript source: slash-command
|
||||
* heuristics can collide with regex literals and runtime paths. Scripts import
|
||||
* their command prefix from lib/provider.mjs, whose declaration is replaced
|
||||
* here by an exact string match.
|
||||
*/
|
||||
export function replaceScriptProviderMarker(content, provider) {
|
||||
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor;
|
||||
const commandPrefix = placeholders.command_prefix || '/';
|
||||
const marker = "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix";
|
||||
const rendered = `export const IMPECCABLE_COMMAND_PREFIX = ${JSON.stringify(commandPrefix)};`;
|
||||
return content.replace(marker, rendered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether a YAML scalar string value must be quoted to survive parsing.
|
||||
*
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -25,11 +25,11 @@ export const SUITES = {
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated))/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated|lib\/provider|pin))/,
|
||||
/^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|pin|shiki-theme|skills-cli|target-args|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/,
|
||||
/^tests\/lib\//,
|
||||
],
|
||||
commands: [
|
||||
@@ -62,6 +62,8 @@ export const SUITES = {
|
||||
'tests/hook-build.test.mjs',
|
||||
'tests/hook.test.mjs',
|
||||
'tests/impeccable-paths.test.mjs',
|
||||
'tests/openai-plugin.test.mjs',
|
||||
'tests/pin.test.mjs',
|
||||
'tests/target-args.test.mjs',
|
||||
'tests/shiki-theme.test.mjs',
|
||||
'tests/test-suites.test.mjs',
|
||||
|
||||
Reference in New Issue
Block a user