mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Improve generated output sync workflow
This commit is contained in:
+100
-87
@@ -409,6 +409,15 @@ const __dirname = path.dirname(__filename);
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
function parseBuildOptions(argv = process.argv.slice(2)) {
|
||||
const skipRootSync = argv.includes('--skip-root-sync') || argv.includes('--no-root-sync');
|
||||
return {
|
||||
syncRootOutputs: !skipRootSync,
|
||||
};
|
||||
}
|
||||
|
||||
const BUILD_OPTIONS = parseBuildOptions();
|
||||
|
||||
// buildStaticSite (Bun HTML bundler) removed — now handled by Astro.
|
||||
|
||||
/**
|
||||
@@ -616,107 +625,111 @@ async function build() {
|
||||
generateApiData(publicDir, skills, patterns, ROOT_DIR);
|
||||
generateCFConfig(publicDir);
|
||||
|
||||
// Copy all provider outputs to project root for local testing.
|
||||
// `.codex/` is intentionally excluded: Codex no longer consumes that layout; keep
|
||||
// generated bundles under dist/ only.
|
||||
const syncConfigs = Object.values(PROVIDERS).filter(({ configDir }) => configDir !== '.codex');
|
||||
if (BUILD_OPTIONS.syncRootOutputs) {
|
||||
// Copy all provider outputs to project root for direct GitHub installs and
|
||||
// submodule users. `.codex/` is intentionally excluded: Codex no longer
|
||||
// consumes that layout; keep generated Codex bundles under dist/ only.
|
||||
const syncConfigs = Object.values(PROVIDERS).filter(({ configDir }) => configDir !== '.codex');
|
||||
|
||||
for (const { provider, configDir } of syncConfigs) {
|
||||
const skillsSrc = path.join(DIST_DIR, provider, configDir, 'skills');
|
||||
const skillsDest = path.join(ROOT_DIR, configDir, 'skills');
|
||||
for (const { provider, configDir } of syncConfigs) {
|
||||
const skillsSrc = path.join(DIST_DIR, provider, configDir, 'skills');
|
||||
const skillsDest = path.join(ROOT_DIR, configDir, 'skills');
|
||||
|
||||
if (fs.existsSync(skillsSrc)) {
|
||||
// Preserve legacy per-project script artifacts (e.g. live-mode config.json)
|
||||
// across the rm + recopy. The build intentionally doesn't ship them,
|
||||
// so without this the sync destroys local state on every rebuild.
|
||||
const stashed = stashPerProjectArtifacts(skillsDest);
|
||||
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
|
||||
copyDirSync(skillsSrc, skillsDest);
|
||||
restorePerProjectArtifacts(skillsDest, stashed);
|
||||
if (fs.existsSync(skillsSrc)) {
|
||||
// Preserve legacy per-project script artifacts (e.g. live-mode config.json)
|
||||
// across the rm + recopy. The build intentionally doesn't ship them,
|
||||
// so without this the sync destroys local state on every rebuild.
|
||||
const stashed = stashPerProjectArtifacts(skillsDest);
|
||||
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
|
||||
copyDirSync(skillsSrc, skillsDest);
|
||||
restorePerProjectArtifacts(skillsDest, stashed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { provider, configDir, agentFormat } of Object.values(PROVIDERS)) {
|
||||
if (!agentFormat) continue;
|
||||
for (const { provider, configDir, agentFormat } of Object.values(PROVIDERS)) {
|
||||
if (!agentFormat) continue;
|
||||
|
||||
const agentsSrc = path.join(DIST_DIR, provider, configDir, 'agents');
|
||||
const agentsDest = path.join(ROOT_DIR, configDir, 'agents');
|
||||
const agentsSrc = path.join(DIST_DIR, provider, configDir, 'agents');
|
||||
const agentsDest = path.join(ROOT_DIR, configDir, 'agents');
|
||||
|
||||
if (fs.existsSync(agentsDest)) fs.rmSync(agentsDest, { recursive: true, force: true });
|
||||
if (fs.existsSync(agentsSrc)) {
|
||||
copyDirSync(agentsSrc, agentsDest);
|
||||
if (fs.existsSync(agentsDest)) fs.rmSync(agentsDest, { recursive: true, force: true });
|
||||
if (fs.existsSync(agentsSrc)) {
|
||||
copyDirSync(agentsSrc, agentsDest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove deprecated skill stubs from local harness dirs. They exist
|
||||
// in dist/ so the cleanup script can redirect users, but they should
|
||||
// not clutter the repo's own skill directories.
|
||||
const deprecatedLocalSkills = [
|
||||
'frontend-design', 'teach-impeccable',
|
||||
'arrange', 'normalize', 'onboard', 'extract',
|
||||
// v3.0 consolidation: standalone skills -> /impeccable sub-commands
|
||||
'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize',
|
||||
'critique', 'delight', 'distill', 'harden', 'layout', 'optimize',
|
||||
'overdrive', 'polish', 'quieter', 'shape', 'typeset',
|
||||
];
|
||||
for (const { configDir } of syncConfigs) {
|
||||
for (const name of deprecatedLocalSkills) {
|
||||
const p = path.join(ROOT_DIR, configDir, 'skills', name);
|
||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||
// Remove deprecated skill stubs from local harness dirs. They exist
|
||||
// in dist/ so the cleanup script can redirect users, but they should
|
||||
// not clutter the repo's own skill directories.
|
||||
const deprecatedLocalSkills = [
|
||||
'frontend-design', 'teach-impeccable',
|
||||
'arrange', 'normalize', 'onboard', 'extract',
|
||||
// v3.0 consolidation: standalone skills -> /impeccable sub-commands
|
||||
'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize',
|
||||
'critique', 'delight', 'distill', 'harden', 'layout', 'optimize',
|
||||
'overdrive', 'polish', 'quieter', 'shape', 'typeset',
|
||||
];
|
||||
for (const { configDir } of syncConfigs) {
|
||||
for (const name of deprecatedLocalSkills) {
|
||||
const p = path.join(ROOT_DIR, configDir, 'skills', name);
|
||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📋 Synced skills to: ${syncConfigs.map(p => p.configDir).join(', ')}`);
|
||||
console.log(`📋 Synced skills to: ${syncConfigs.map(p => p.configDir).join(', ')}`);
|
||||
|
||||
// Build the Claude Code plugin subtree at ./plugin/.
|
||||
// The Claude Code marketplace is configured with `source: "./plugin"`, so
|
||||
// the plugin cache only copies this slim directory (~0.3 MB) instead of
|
||||
// the entire monorepo (~291 MB on the previous "./" source). The harness
|
||||
// dirs above stay where they are because `npx skills add pbakaus/impeccable`
|
||||
// reads them directly from the GitHub repo at install time.
|
||||
const pluginRoot = path.join(ROOT_DIR, 'plugin');
|
||||
const pluginManifestDir = path.join(pluginRoot, '.claude-plugin');
|
||||
const pluginSkillsDir = path.join(pluginRoot, 'skills');
|
||||
const pluginAgentsDir = path.join(pluginRoot, 'agents');
|
||||
if (fs.existsSync(pluginManifestDir)) fs.rmSync(pluginManifestDir, { recursive: true });
|
||||
if (fs.existsSync(pluginSkillsDir)) fs.rmSync(pluginSkillsDir, { recursive: true });
|
||||
if (fs.existsSync(pluginAgentsDir)) fs.rmSync(pluginAgentsDir, { recursive: true });
|
||||
// Build the Claude Code plugin subtree at ./plugin/.
|
||||
// The Claude Code marketplace is configured with `source: "./plugin"`, so
|
||||
// the plugin cache only copies this slim directory (~0.3 MB) instead of
|
||||
// the entire monorepo (~291 MB on the previous "./" source). The harness
|
||||
// dirs above stay where they are because `npx skills add pbakaus/impeccable`
|
||||
// reads them directly from the GitHub repo at install time.
|
||||
const pluginRoot = path.join(ROOT_DIR, 'plugin');
|
||||
const pluginManifestDir = path.join(pluginRoot, '.claude-plugin');
|
||||
const pluginSkillsDir = path.join(pluginRoot, 'skills');
|
||||
const pluginAgentsDir = path.join(pluginRoot, 'agents');
|
||||
if (fs.existsSync(pluginManifestDir)) fs.rmSync(pluginManifestDir, { recursive: true });
|
||||
if (fs.existsSync(pluginSkillsDir)) fs.rmSync(pluginSkillsDir, { recursive: true });
|
||||
if (fs.existsSync(pluginAgentsDir)) fs.rmSync(pluginAgentsDir, { 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');
|
||||
const pluginAgentEntries = fs.existsSync(claudeAgentsSrc)
|
||||
? fs.readdirSync(claudeAgentsSrc)
|
||||
.filter(file => file.endsWith('.md'))
|
||||
.sort()
|
||||
.map(file => `./agents/${file}`)
|
||||
: [];
|
||||
// Trailing slash on the skills path matches the documented schema in
|
||||
// code.claude.com/docs/en/plugins-reference. Issue #86 has 3 reporters
|
||||
// converging on "add trailing slash to fix slash commands not registering";
|
||||
// the docs schema example consistently uses `"./custom/skills/"` form.
|
||||
const pluginManifest = { ...rootManifest, skills: './skills/' };
|
||||
if (pluginAgentEntries.length) {
|
||||
pluginManifest.agents = pluginAgentEntries;
|
||||
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');
|
||||
const pluginAgentEntries = fs.existsSync(claudeAgentsSrc)
|
||||
? fs.readdirSync(claudeAgentsSrc)
|
||||
.filter(file => file.endsWith('.md'))
|
||||
.sort()
|
||||
.map(file => `./agents/${file}`)
|
||||
: [];
|
||||
// Trailing slash on the skills path matches the documented schema in
|
||||
// code.claude.com/docs/en/plugins-reference. Issue #86 has 3 reporters
|
||||
// converging on "add trailing slash to fix slash commands not registering";
|
||||
// the docs schema example consistently uses `"./custom/skills/"` form.
|
||||
const pluginManifest = { ...rootManifest, skills: './skills/' };
|
||||
if (pluginAgentEntries.length) {
|
||||
pluginManifest.agents = pluginAgentEntries;
|
||||
} else {
|
||||
delete pluginManifest.agents;
|
||||
}
|
||||
fs.mkdirSync(pluginManifestDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginManifestDir, 'plugin.json'),
|
||||
JSON.stringify(pluginManifest, null, 2) + '\n',
|
||||
);
|
||||
|
||||
const claudeSkillsSrc = path.join(DIST_DIR, 'claude-code', '.claude', 'skills', 'impeccable');
|
||||
if (fs.existsSync(claudeSkillsSrc)) {
|
||||
fs.mkdirSync(pluginSkillsDir, { recursive: true });
|
||||
copyDirSync(claudeSkillsSrc, path.join(pluginSkillsDir, 'impeccable'));
|
||||
}
|
||||
|
||||
if (fs.existsSync(claudeAgentsSrc)) {
|
||||
copyDirSync(claudeAgentsSrc, pluginAgentsDir);
|
||||
}
|
||||
|
||||
console.log('📦 Built Claude Code plugin subtree at ./plugin/');
|
||||
} else {
|
||||
delete pluginManifest.agents;
|
||||
console.log('📋 Skipped root harness and plugin sync (--skip-root-sync)');
|
||||
}
|
||||
fs.mkdirSync(pluginManifestDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginManifestDir, 'plugin.json'),
|
||||
JSON.stringify(pluginManifest, null, 2) + '\n',
|
||||
);
|
||||
|
||||
const claudeSkillsSrc = path.join(DIST_DIR, 'claude-code', '.claude', 'skills', 'impeccable');
|
||||
if (fs.existsSync(claudeSkillsSrc)) {
|
||||
fs.mkdirSync(pluginSkillsDir, { recursive: true });
|
||||
copyDirSync(claudeSkillsSrc, path.join(pluginSkillsDir, 'impeccable'));
|
||||
}
|
||||
|
||||
if (fs.existsSync(claudeAgentsSrc)) {
|
||||
copyDirSync(claudeAgentsSrc, pluginAgentsDir);
|
||||
}
|
||||
|
||||
console.log('📦 Built Claude Code plugin subtree at ./plugin/');
|
||||
|
||||
// Generate authoritative counts and validate references
|
||||
const countErrors = generateCounts(ROOT_DIR, skills, buildDir);
|
||||
|
||||
+30
-12
@@ -67,6 +67,35 @@ function readDetectorBundleScripts(rootDir) {
|
||||
return scripts;
|
||||
}
|
||||
|
||||
function readSkillScripts(scriptsDir) {
|
||||
const scripts = [];
|
||||
|
||||
const walk = (dir) => {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(entryPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(entry.name)) continue;
|
||||
|
||||
const relPath = path.relative(scriptsDir, entryPath).split(path.sep).join('/');
|
||||
scripts.push({
|
||||
name: relPath,
|
||||
content: fs.readFileSync(entryPath, 'utf-8'),
|
||||
filePath: entryPath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
walk(scriptsDir);
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse frontmatter from markdown content
|
||||
* Returns { frontmatter: object, body: string }
|
||||
@@ -224,18 +253,7 @@ export function readSourceFiles(rootDir) {
|
||||
const scripts = [];
|
||||
const scriptsDir = path.join(skillDir, 'scripts');
|
||||
if (fs.existsSync(scriptsDir)) {
|
||||
const scriptFiles = fs.readdirSync(scriptsDir).filter(f => {
|
||||
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(f)) return false;
|
||||
return fs.statSync(path.join(scriptsDir, f)).isFile();
|
||||
});
|
||||
for (const scriptFile of scriptFiles) {
|
||||
const scriptPath = path.join(scriptsDir, scriptFile);
|
||||
scripts.push({
|
||||
name: scriptFile,
|
||||
content: fs.readFileSync(scriptPath, 'utf-8'),
|
||||
filePath: scriptPath
|
||||
});
|
||||
}
|
||||
scripts.push(...readSkillScripts(scriptsDir));
|
||||
}
|
||||
scripts.push(...readDetectorBundleScripts(rootDir));
|
||||
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
// Usage: node scripts/release.mjs <skill|cli|extension> [--dry-run]
|
||||
//
|
||||
// Refuses on a dirty tree, an unpushed HEAD, or a missing changelog entry.
|
||||
// For the skill component, also reruns `bun run build` and refuses if the
|
||||
// For the skill component, also reruns `bun run build:release` and refuses if the
|
||||
// regenerated harness directories drift from what is committed.
|
||||
|
||||
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
||||
@@ -23,7 +23,7 @@ const COMPONENTS = {
|
||||
tagPrefix: 'skill-v',
|
||||
label: 'Skill',
|
||||
changelogLabel: 'v',
|
||||
buildCmd: 'bun run build',
|
||||
buildCmd: 'bun run build:release',
|
||||
artifacts: ['dist/universal.zip'],
|
||||
postReleaseHint: null,
|
||||
tweetHeader: (v) => `Impeccable v${v} is out.`,
|
||||
|
||||
Reference in New Issue
Block a user