mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 17:46:36 +03:00
Add native Cursor marketplace plugin packaging (#776)
* Add native Cursor marketplace plugin packaging AI assistance: Codex, under maintainer direction. * Document verified Cursor plugin installation and smoke tests AI assistance: Codex, under maintainer direction. * Fix Cursor plugin sync for license changes AI assistance: Codex, under maintainer direction.
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
verifyPluginAgentRewrite,
|
||||
} from './lib/plugin-paths.js';
|
||||
import { stageOpenAIPlugin } from './lib/openai-plugin.js';
|
||||
import { stageCursorPlugin } from './lib/cursor-plugin.js';
|
||||
import { stageVSCodeExtension } from './lib/vscode-extension.js';
|
||||
import { ENGINE_TARGETS, binaryName, main as fetchEngineMain, readEngineVersion } from './fetch-engine.mjs';
|
||||
// Sub-page generation is now handled by Astro content collections.
|
||||
@@ -896,6 +897,13 @@ async function build() {
|
||||
const openAiPluginRoot = stageOpenAIPlugin(ROOT_DIR, DIST_DIR);
|
||||
await createProviderZip(openAiPluginRoot, DIST_DIR, 'openai-plugin');
|
||||
|
||||
const cursorPluginRoot = stageCursorPlugin(ROOT_DIR, DIST_DIR);
|
||||
if (BUILD_OPTIONS.syncRootOutputs) {
|
||||
const destination = path.join(ROOT_DIR, 'cursor-plugin');
|
||||
fs.rmSync(destination, { recursive: true, force: true });
|
||||
fs.cpSync(cursorPluginRoot, destination, { recursive: true });
|
||||
}
|
||||
|
||||
// Declarative Marketplace skill bundle: no editor runtime or project hooks.
|
||||
// Staged before optional engine bundling to keep the VSIX launcher-only.
|
||||
stageVSCodeExtension(ROOT_DIR, DIST_DIR);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { buildCursorHooksManifest } from './transformers/hooks.js';
|
||||
|
||||
const PROJECT_SCRIPTS = '.cursor/skills/impeccable/scripts';
|
||||
|
||||
export function rewriteCursorPluginMarkdown(text, { agent = false } = {}) {
|
||||
// Installed plugins live outside the workspace. Keep the shared Setup flow,
|
||||
// but remove its project-install fallback and quote executable paths.
|
||||
text = text.replace(`, and \`${PROJECT_SCRIPTS}\` is the fallback only when the runtime reports no base directory`, '');
|
||||
text = text.replaceAll(`${PROJECT_SCRIPTS}/impeccable.cmd`, '"<skill-base-dir>/scripts/impeccable.cmd"');
|
||||
text = text.replaceAll(`${PROJECT_SCRIPTS}/impeccable`, '"<skill-base-dir>/scripts/impeccable"');
|
||||
text = text.replaceAll(PROJECT_SCRIPTS, '<skill-base-dir>/scripts');
|
||||
text = text.replace('`<skill-base-dir>/scripts/impeccable context`', '`"<skill-base-dir>/scripts/impeccable" context`');
|
||||
if (agent && text.includes('<skill-base-dir>')) {
|
||||
const end = text.indexOf('\n---', 4);
|
||||
if (end < 0) throw new Error('Cursor agent is missing frontmatter');
|
||||
const offset = end + 4;
|
||||
text = `${text.slice(0, offset)}\n\nResolve \`<skill-base-dir>\` from the skill scripts path supplied in the handoff (its parent directory), or from \`../skills/impeccable\` relative to this agent file. Keep cwd at the user's project.${text.slice(offset)}`;
|
||||
}
|
||||
return text.replace(/[\t ]+$/gm, '').trimEnd() + '\n';
|
||||
}
|
||||
|
||||
function rewriteTree(dir, options) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const file = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) rewriteTree(file, options);
|
||||
else if (entry.name.endsWith('.md')) fs.writeFileSync(file, rewriteCursorPluginMarkdown(fs.readFileSync(file, 'utf8'), options));
|
||||
}
|
||||
}
|
||||
|
||||
export function stageCursorPlugin(rootDir, distDir) {
|
||||
const provider = path.join(distDir, 'cursor', '.cursor');
|
||||
const icon = path.join(rootDir, 'scripts/lib/assets/plugin-icon.png');
|
||||
const readme = path.join(rootDir, 'docs/CURSOR-PLUGIN.md');
|
||||
for (const input of [path.join(provider, 'skills/impeccable/SKILL.md'), path.join(provider, 'agents'), icon, readme]) {
|
||||
if (!fs.existsSync(input)) throw new Error(`Cannot build Cursor plugin: missing ${input}`);
|
||||
}
|
||||
const source = JSON.parse(fs.readFileSync(path.join(rootDir, '.claude-plugin/plugin.json'), 'utf8'));
|
||||
const output = path.join(distDir, 'cursor-plugin');
|
||||
fs.rmSync(output, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(output, '.cursor-plugin'), { recursive: true });
|
||||
fs.cpSync(path.join(provider, 'skills'), path.join(output, 'skills'), { recursive: true });
|
||||
fs.cpSync(path.join(provider, 'agents'), path.join(output, 'agents'), { recursive: true });
|
||||
rewriteTree(path.join(output, 'skills'));
|
||||
rewriteTree(path.join(output, 'agents'), { agent: true });
|
||||
fs.mkdirSync(path.join(output, 'assets'));
|
||||
fs.copyFileSync(icon, path.join(output, 'assets/icon.png'));
|
||||
fs.copyFileSync(readme, path.join(output, 'README.md'));
|
||||
fs.copyFileSync(path.join(rootDir, 'LICENSE'), path.join(output, 'LICENSE'));
|
||||
fs.writeFileSync(path.join(output, '.cursor-plugin/plugin.json'), `${JSON.stringify({
|
||||
name: 'impeccable', version: source.version,
|
||||
description: 'Design and refine interfaces with Impeccable skills, specialist agents, and design checks.',
|
||||
author: { name: 'Renaissance Geek, Inc.' },
|
||||
homepage: source.homepage, repository: source.repository,
|
||||
license: 'Apache-2.0', keywords: ['design', 'frontend', 'accessibility', 'ui', 'ux'],
|
||||
logo: 'assets/icon.png', skills: './skills/', agents: './agents/', hooks: './hooks/hooks.json',
|
||||
}, null, 2)}\n`);
|
||||
fs.mkdirSync(path.join(output, 'hooks'));
|
||||
fs.writeFileSync(path.join(output, 'hooks/hooks.json'), `${JSON.stringify(
|
||||
buildCursorHooksManifest('${CURSOR_PLUGIN_ROOT}/skills/impeccable/scripts'), null, 2,
|
||||
)}\n`);
|
||||
return output;
|
||||
}
|
||||
@@ -153,13 +153,13 @@ export function buildCodexHooksManifest(skillDir = '.codex') {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCursorHooksManifest() {
|
||||
export function buildCursorHooksManifest(scriptsDir = CURSOR_SCRIPTS) {
|
||||
return {
|
||||
version: 1,
|
||||
hooks: {
|
||||
preToolUse: [
|
||||
{
|
||||
command: guardedLauncher(launcherIn(CURSOR_SCRIPTS), 'hook-before-edit'),
|
||||
command: guardedLauncher(launcherIn(scriptsDir), 'hook-before-edit'),
|
||||
timeout: TIMEOUT_SECONDS,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -128,6 +128,10 @@ export function collectPluginVersions(rootDir) {
|
||||
relPath: 'dist/openai/impeccable/.codex-plugin/plugin.json',
|
||||
read: (raw) => JSON.parse(raw).version,
|
||||
},
|
||||
...['cursor-plugin', 'dist/cursor-plugin'].flatMap((prefix) => [
|
||||
{ relPath: `${prefix}/.cursor-plugin/plugin.json`, read: (raw) => JSON.parse(raw).version },
|
||||
{ relPath: `${prefix}/skills/impeccable/SKILL.md`, read: readSkillFrontmatterVersion },
|
||||
]),
|
||||
{
|
||||
relPath: 'plugin/skills/impeccable/SKILL.md',
|
||||
read: (raw) => readSkillFrontmatterVersion(raw),
|
||||
|
||||
@@ -73,6 +73,7 @@ export const SUITES = {
|
||||
'tests/github-sheriff.test.mjs',
|
||||
'tests/hook-build.test.mjs',
|
||||
'tests/openai-plugin.test.mjs',
|
||||
'tests/cursor-plugin.test.mjs',
|
||||
'tests/vscode-extension.test.mjs',
|
||||
'tests/process-group.test.mjs',
|
||||
'tests/release.test.mjs',
|
||||
|
||||
Reference in New Issue
Block a user