mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Improve generated output sync workflow
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
name: Sync Generated Provider Output
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".claude-plugin/**"
|
||||
- "cli/engine/**"
|
||||
- "skill/**"
|
||||
- "scripts/**"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: sync-generated-output
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GENERATED_PATHS: >-
|
||||
.agents
|
||||
.claude
|
||||
.cursor
|
||||
.gemini
|
||||
.github/skills
|
||||
.kiro
|
||||
.opencode
|
||||
.pi
|
||||
.qoder
|
||||
.rovodev
|
||||
.trae
|
||||
.trae-cn
|
||||
plugin
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Optional PAT or GitHub App token. With the default GITHUB_TOKEN,
|
||||
# GitHub suppresses follow-up workflow runs from the generated commit.
|
||||
token: ${{ secrets.SYNC_GENERATED_OUTPUT_TOKEN || github.token }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build release output
|
||||
run: bun run build:release
|
||||
|
||||
- name: Check generated output drift
|
||||
id: drift
|
||||
run: |
|
||||
changes="$(git status --porcelain -- $GENERATED_PATHS)"
|
||||
if [ -z "$changes" ]; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No generated provider output drift."
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
printf '%s\n' "$changes"
|
||||
fi
|
||||
|
||||
- name: Commit generated output
|
||||
if: steps.drift.outputs.changed == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add $GENERATED_PATHS
|
||||
git commit -m "Sync generated provider output"
|
||||
|
||||
git fetch origin main
|
||||
if ! git merge-base --is-ancestor origin/main HEAD; then
|
||||
echo "::error::main advanced while generated output was building; rerun this workflow on the latest main."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Summarize generated output commit
|
||||
if: steps.drift.outputs.changed == 'true'
|
||||
run: |
|
||||
echo "Committed generated provider output directly to main." >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -2,31 +2,39 @@
|
||||
|
||||
## Skill bootstrap: skip the post-update-cleanup step
|
||||
|
||||
If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `<post-update-cleanup>` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `skill/SKILL.src.md` on every `bun run build`, so any deletion churns back. Skip it silently and proceed with the task.
|
||||
If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `<post-update-cleanup>` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `skill/SKILL.src.md` whenever `bun run build:release` syncs tracked harness output, so any deletion churns back. Skip it silently and proceed with the task.
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
`source/` is the source of truth. Author skills in `skill/` and keep provider output in `dist/` generated, not hand-edited. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. Runtime detection code ships from `src/`. The website lives in `public/`, local API/dev serving lives in `server/`, and regression coverage lives in `tests/` with fixtures under `tests/fixtures/`.
|
||||
`skill/` is the source of truth for the Impeccable skill: `SKILL.src.md`, `reference/`, `scripts/`, and `agents/`. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. The CLI and anti-pattern detector live in `cli/`, the browser extension in `extension/`, the Astro website in `site/`, Cloudflare Pages Functions in `functions/`, and regression coverage in `tests/` with fixtures under `tests/fixtures/`. `dist/` and `build/` are generated and gitignored. The root harness folders (`.agents/`, `.claude/`, `.cursor/`, etc.) and `plugin/` are generated distribution artifacts that are tracked for direct repo installs, not hand-authored source.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `bun run dev` - start the local Bun server.
|
||||
- `bun run build` - regenerate `dist/`, derived site assets, and validation output.
|
||||
- `bun run rebuild` - clean and rebuild everything from scratch.
|
||||
- `bun run build` - source-first build: regenerate `dist/`, derived site assets, and validation output without syncing tracked harness folders.
|
||||
- `bun run build:release` - release/distribution build: run the full build and sync tracked root harness folders plus `plugin/`.
|
||||
- `bun run rebuild` - clean and rebuild everything from scratch without syncing tracked harness folders.
|
||||
- `bun run rebuild:release` - clean and rebuild everything, including tracked harness output sync.
|
||||
- `bun test tests/build.test.js` - run a focused Bun test.
|
||||
- `bun run test` - run the full Bun + Node test suite.
|
||||
- `bun run test:live-e2e` - opt-in live-mode E2E against framework fixtures (~2 min; needs `npx playwright install chromium` once).
|
||||
- `bun run test:skill-behavior` - opt-in LLM-backed checks that the SKILL.md Setup flow actually drives the agent (~5 min; runs claude-sonnet-4-6 / gpt-5.5 / gemini-3.1-flash-lite, roughly $0.50-1.50 per run on the production-tier models, needs `.env` with provider keys).
|
||||
- `bun run build:browser` / `bun run build:extension` - rebuild browser-specific bundles.
|
||||
|
||||
Run `bun run build` after changing anything in `source/`, transformer code, or user-facing counts.
|
||||
Run `bun run build` after changing anything in `skill/`, transformer code, or user-facing counts. It validates the generated distribution under `dist/` without touching tracked root harness outputs. Use `bun run build:release` only when intentionally refreshing generated provider permutations for release/main-sync or build-system work.
|
||||
|
||||
## Generated Provider Output Policy
|
||||
|
||||
The root harness folders (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, `.gemini/skills/`, `.github/skills/`, `.kiro/skills/`, `.opencode/skills/`, `.pi/skills/`, `.qoder/skills/`, `.rovodev/skills/`, `.trae*/skills/`) and `plugin/` stay tracked so `main` remains installable for direct GitHub, `npx skills`, and submodule users. They are still generated artifacts.
|
||||
|
||||
Normal development should be source-first: stage changes in `skill/`, `scripts/`, `cli/`, `site/`, `extension/`, `functions/`, and `tests/`; leave generated harness churn unstaged unless the user asked for it. After source changes land on `main`, `.github/workflows/sync-generated-output.yml` runs `bun run build:release` and commits generated provider output directly back to `main`. Treat generated harness diffs as release artifacts and keep them out of feature PRs unless they are the point of the PR.
|
||||
|
||||
## Sandbox gotchas for Codex agents
|
||||
|
||||
Some repo workflows need to run outside the sandbox in the desktop app:
|
||||
|
||||
- GitHub SSH operations that depend on the 1Password SSH agent, such as `gh pr checkout`, may fail in the sandbox with `sign_and_send_pubkey` or no 1Password approval prompt. Rerun them outside the sandbox instead of falling back to unrelated workarounds.
|
||||
- `bun run build` rewrites committed harness directories such as `.agents/skills/`. In the sandbox, Bun can hit filesystem errors while removing/recreating those trees (for example `EFAULT` on `.agents/skills`). Rerun the build outside the sandbox before treating it as a real build failure.
|
||||
- `bun run build:release` rewrites committed harness directories such as `.agents/skills/`. In the sandbox, Bun can hit filesystem errors while removing/recreating those trees (for example `EFAULT` on `.agents/skills`). Rerun the release build outside the sandbox before treating it as a real build failure.
|
||||
- Puppeteer/headless-Chrome tests, especially `node --test tests/detect-antipatterns-browser.test.mjs` and the browser portion of `bun run test`, can hang in the sandbox while launching Chrome. Run them outside the sandbox for authoritative results.
|
||||
- The jsdom fixture suite is intentionally run with Node, not Bun: use `node --test tests/detect-antipatterns-fixtures.test.mjs` or the `bun run test` script. A direct `bun test tests/detect-antipatterns-fixtures.test.mjs` can time out and is not the supported signal.
|
||||
|
||||
@@ -61,12 +69,12 @@ Conventions: wrap the identifying heading text in straight double quotes inside
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent history favors short, imperative subjects such as `Fix: ...`, `Add ...`, `Improve ...`, or `Bump ...`. Keep commits focused and explain the user-facing impact when it is not obvious. PRs should summarize what changed, list validation performed, and call out regenerated artifacts like `dist/` or `build/`. Include screenshots for visible `site/` changes and mention affected providers when transform behavior changes.
|
||||
Recent history favors short, imperative subjects such as `Fix: ...`, `Add ...`, `Improve ...`, or `Bump ...`. Keep commits focused and explain the user-facing impact when it is not obvious. PRs should summarize what changed, list validation performed, and call out whether generated provider output was intentionally omitted or intentionally refreshed. Include screenshots for visible `site/` changes and mention affected providers when transform behavior changes.
|
||||
|
||||
## Releases
|
||||
|
||||
Tags are per-component because the three components ship independently: `skill-v` (`.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json`), `cli-v` (`package.json`), `ext-v` (`extension/manifest.json`). Flow: bump the relevant manifest, add a changelog entry to `site/pages/index.astro` (skill = bare `vX.Y.Z`; CLI = `CLI vX.Y.Z`; extension = `Extension vX.Y.Z` — the prefix is how `scripts/release.mjs` finds the right block), commit, push, then `bun run release:<skill|cli|ext>` (or `--dry-run` first). The script refuses on a dirty tree, an unpushed HEAD, a missing changelog entry, or stale build outputs; skill and extension reruns of `bun run build` / `bun run build:extension` must produce zero diff. Skill releases attach `dist/universal.zip`; extension releases attach `dist/extension.zip`. CLI ships to npm via a separate `npm publish`, and the extension zip uploads to the Chrome Web Store manually — both reminded at the end of the script. Fix already-shipped notes with `gh release edit <tag> --notes-file <md>`.
|
||||
Tags are per-component because the three components ship independently: `skill-v` (`.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json`), `cli-v` (`package.json`), `ext-v` (`extension/manifest.json`). Flow: bump the relevant manifest, add a changelog entry to `site/pages/index.astro` (skill = bare `vX.Y.Z`; CLI = `CLI vX.Y.Z`; extension = `Extension vX.Y.Z` — the prefix is how `scripts/release.mjs` finds the right block), commit, push, then `bun run release:<skill|cli|ext>` (or `--dry-run` first). The script refuses on a dirty tree, an unpushed HEAD, a missing changelog entry, or stale build outputs; skill and extension reruns of `bun run build:release` / `bun run build:extension` must produce zero diff. Skill releases attach `dist/universal.zip`; extension releases attach `dist/extension.zip`. CLI ships to npm via a separate `npm publish`, and the extension zip uploads to the Chrome Web Store manually — both reminded at the end of the script. Fix already-shipped notes with `gh release edit <tag> --notes-file <md>`.
|
||||
|
||||
## Contributor Notes
|
||||
|
||||
Do not edit generated provider files directly unless you are intentionally patching generated output as part of a build-system change. Prefer fixing the root source in `skill/`, `scripts/`, or `cli/`, then regenerate artifacts.
|
||||
Do not edit generated provider files directly unless you are intentionally patching generated output as part of a build-system change. Prefer fixing the root source in `skill/`, `scripts/`, or `cli/`, then regenerate artifacts for validation. Stage generated harness artifacts only for release/main-sync or build-system work.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Skill bootstrap: skip the post-update-cleanup step
|
||||
|
||||
The `<post-update-cleanup>` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `skill/`. The block is also regenerated from `skill/SKILL.src.md` on every `bun run build`, so any deletion churns back on the next build. Skip the step silently and proceed.
|
||||
The `<post-update-cleanup>` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `skill/`. The block is also regenerated from `skill/SKILL.src.md` whenever `bun run build:release` syncs tracked harness output, so any deletion churns back on the next sync. Skip the step silently and proceed.
|
||||
|
||||
Same rule for AGENTS.md and every other harness-specific instruction file: treat post-update-cleanup as a no-op in this repo.
|
||||
|
||||
@@ -10,7 +10,7 @@ Same rule for AGENTS.md and every other harness-specific instruction file: treat
|
||||
|
||||
There is **one** user-invocable skill, `impeccable`, with **23 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`:
|
||||
|
||||
- `SKILL.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design laws, and the **Commands** router table.
|
||||
- `SKILL.src.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design laws, and the **Commands** router table. Provider `SKILL.md` files are generated from this source.
|
||||
- `reference/` — one `<command>.md` per command (`audit.md`, `polish.md`, `critique.md`, etc.) plus the domain reference files (`typography.md`, `color-and-contrast.md`, etc.). When a sub-command is matched, the router loads its reference file.
|
||||
- `reference/brand.md` and `reference/product.md` — the two register references. SKILL.md's Setup section selects one based on the task cue, the surface in focus, or the `register` field in PRODUCT.md (first match wins).
|
||||
- `scripts/command-metadata.json` — single source of truth for each command's description, argument hint, and (eventually) category. Both the build and `pin.mjs` read from this.
|
||||
@@ -26,7 +26,7 @@ Every design task belongs to one of two registers:
|
||||
- **Brand** — design IS the product: marketing, landing pages, brand sites, campaign surfaces, portfolios, long-form content. Distinctiveness is the bar. Spans every visual lane (tech-minimal, luxury, editorial-magazine, consumer-warm, brutalist, etc.) — do not default to only one.
|
||||
- **Product** — design SERVES the product: app UI, admin, dashboards, tools. Earned familiarity is the bar — fluent users of Linear / Figma / Notion / Raycast / Stripe should trust it.
|
||||
|
||||
PRODUCT.md at the project root carries a `## Register` section with a bare value (`brand` or `product`). `/impeccable teach` asks about register first because it shapes every downstream answer.
|
||||
PRODUCT.md at the project root carries a `## Register` section with a bare value (`brand` or `product`). `/impeccable init` asks about register first because it shapes every downstream answer.
|
||||
|
||||
Sub-command reference files add a short `## Register` section near the top *only where the answer diverges between the two*. Don't restate the register files' content in sub-commands — link instead. Sub-commands where register meaningfully diverges today: `typeset`, `animate`, `bolder`, `delight`, `colorize`, `layout`, `quieter`.
|
||||
|
||||
@@ -104,11 +104,13 @@ The card is referenced as a **sitewide default** in `site/layouts/Base.astro` (e
|
||||
|
||||
## Build System
|
||||
|
||||
The build system compiles the impeccable skill from `skill/` to provider-specific formats in `dist/`:
|
||||
The build system compiles the impeccable skill from `skill/` to provider-specific formats in `dist/`. The default build is source-first and does not sync tracked root harness folders; the release build performs the tracked distribution sync:
|
||||
|
||||
```bash
|
||||
bun run build # Build all providers
|
||||
bun run rebuild # Clean and rebuild
|
||||
bun run build # Build dist/site output without syncing root harness dirs
|
||||
bun run build:release # Build dist/site output and sync root harness dirs + plugin/
|
||||
bun run rebuild # Clean and rebuild without root harness sync
|
||||
bun run rebuild:release # Clean and rebuild with root harness sync
|
||||
```
|
||||
|
||||
Source files use placeholders that get replaced per-provider:
|
||||
@@ -119,9 +121,13 @@ Source files use placeholders that get replaced per-provider:
|
||||
- `{{available_commands}}` — auto-populated list of commands (from `IMPECCABLE_SUB_COMMANDS` in `scripts/lib/utils.js`)
|
||||
- `{{scripts_path}}` — provider-aware path to the skill's scripts directory
|
||||
|
||||
### Harness output directories are tracked
|
||||
### Generated provider output policy
|
||||
|
||||
`.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, and the other 8 harness directories are **intentionally committed to the repo**. `npx skills` reads them directly from this repo at install time, and they enable clean submodule use. Do not gitignore them. Run `bun run build` to refresh them after editing `skill/`.
|
||||
`.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, and the other harness directories are **intentionally committed to the repo**. `npx skills` reads them directly from this repo at install time, and they enable clean submodule use. Do not gitignore them.
|
||||
|
||||
They are generated distribution artifacts, not authoring surfaces. Normal development PRs should be source-first: edit and stage `skill/`, `scripts/`, `cli/`, `site/`, `extension/`, `functions/`, and `tests/`; do not stage regenerated provider permutations unless the task is explicitly a release/generated-output sync or a build-system change. Run `bun run build` for validation after editing `skill/`, transformer code, generated site counts, or provider behavior. Use `bun run build:release` only when intentionally refreshing tracked harness outputs.
|
||||
|
||||
After source changes land on `main`, `.github/workflows/sync-generated-output.yml` runs `bun run build:release` and commits generated provider output directly back to `main`. Treat generated harness diffs as release artifacts and keep them out of feature PRs unless they are the point of the PR.
|
||||
|
||||
Local state files inside harness directories (e.g. `.claude/scheduled_tasks.lock`, `.claude/settings.local.json`) ARE gitignored.
|
||||
|
||||
@@ -163,7 +169,7 @@ Adding a new fixture is a matter of cloning a directory under `tests/framework-f
|
||||
|
||||
### Skill-behavior tests
|
||||
|
||||
`tests/skill-behavior/scenarios.test.mjs` is the LLM-backed safety net for edits to `skill/SKILL.src.md` and the Setup-adjacent reference files (`teach.md`, `document.md`, `brand.md`, `product.md`, sub-command refs). It inlines the source `skill/SKILL.src.md` into the system prompt of a real LLM, gives the agent `bash` / `read` / `write` / `list` tools scoped to a temp workspace, and asserts on the tool-call trace — not on the model's free-form output. The trace is the source of truth.
|
||||
`tests/skill-behavior/scenarios.test.mjs` is the LLM-backed safety net for edits to `skill/SKILL.src.md` and the Setup-adjacent reference files (`init.md`, `document.md`, `brand.md`, `product.md`, sub-command refs). It inlines the source `skill/SKILL.src.md` into the system prompt of a real LLM, gives the agent `bash` / `read` / `write` / `list` tools scoped to a temp workspace, and asserts on the tool-call trace — not on the model's free-form output. The trace is the source of truth.
|
||||
|
||||
```bash
|
||||
bun run test:skill-behavior # full suite (27 tests, ~5 min, ~$0.50-1.50 across providers)
|
||||
@@ -176,7 +182,7 @@ IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1 bun run test:skill-behavior # dump
|
||||
**Auth** lives in repo-root `.env` (copied from `~/code/impeccable-evals/.env`, gitignored). Providers skip cleanly when their key is unset; they don't fail.
|
||||
|
||||
**Nine scenarios:**
|
||||
1. empty workspace → agent loads `reference/teach.md`
|
||||
1. empty workspace → agent loads `reference/init.md`
|
||||
2. PRODUCT.md only → loads `brand.md`
|
||||
3. PRODUCT.md + DESIGN.md → loads `brand.md` + consults the design system
|
||||
4. context already loaded in turn 1 → turn 2 does **not** re-run `context.mjs`
|
||||
@@ -249,7 +255,7 @@ Workflow for any component:
|
||||
3. Commit and push to `main`.
|
||||
4. Run `bun run release:<skill|cli|ext>`. Preview first with `node scripts/release.mjs <component> --dry-run`.
|
||||
|
||||
The script refuses to run if: the working tree is dirty, HEAD is ahead of origin, the tag already exists, the matching changelog entry is missing, or (for skill/extension) `bun run build` / `bun run build:extension` produces uncommitted changes — meaning the harness output dirs or `extension/detector/` files weren't refreshed before the bump was committed.
|
||||
The script refuses to run if: the working tree is dirty, HEAD is ahead of origin, the tag already exists, the matching changelog entry is missing, or (for skill/extension) `bun run build:release` / `bun run build:extension` produces uncommitted changes — meaning the harness output dirs or `extension/detector/` files weren't refreshed before the bump was committed.
|
||||
|
||||
Skill releases attach `dist/universal.zip`. Extension releases run `bun run build:extension` first and attach `dist/extension.zip`. CLI releases print a reminder to run `npm publish` separately; extension releases print a reminder to upload the zip to the Chrome Web Store dashboard.
|
||||
|
||||
|
||||
+4
-1
@@ -40,13 +40,16 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build:skills": "bun run scripts/build.js",
|
||||
"build:skills": "bun run scripts/build.js --skip-root-sync",
|
||||
"build:skills:release": "bun run scripts/build.js",
|
||||
"build:site": "npx astro build",
|
||||
"build": "bun run build:skills && bun run build:site && cp -R dist build/_data/dist",
|
||||
"build:release": "bun run build:skills:release && bun run build:site && cp -R dist build/_data/dist",
|
||||
"build:browser": "node scripts/build-browser-detector.js",
|
||||
"build:extension": "node scripts/build-extension.js",
|
||||
"clean": "rm -rf dist build",
|
||||
"rebuild": "bun run clean && bun run build",
|
||||
"rebuild:release": "bun run clean && bun run build:release",
|
||||
"dev": "bun run scripts/gen-dev-api.mjs && npx astro dev",
|
||||
"preview": "bun run build && npx astro preview",
|
||||
"deploy": "bun run build && wrangler pages deploy build/",
|
||||
|
||||
+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.`,
|
||||
|
||||
+19
-1
@@ -489,6 +489,25 @@ Impeccable design instructions.`;
|
||||
expect(skills[0].references[0].name).toBe('typography');
|
||||
});
|
||||
|
||||
test('should read nested skill script files with portable relative names', () => {
|
||||
const skillDir = path.join(testRootDir, 'skill');
|
||||
ensureDir(skillDir);
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), '---\nname: test-skill\n---\nBody');
|
||||
|
||||
const scriptsDir = path.join(skillDir, 'scripts');
|
||||
ensureDir(path.join(scriptsDir, 'live'));
|
||||
fs.writeFileSync(path.join(scriptsDir, 'context.mjs'), 'export const context = true;\n');
|
||||
fs.writeFileSync(path.join(scriptsDir, 'live/session-store.mjs'), 'export const nested = true;\n');
|
||||
fs.writeFileSync(path.join(scriptsDir, 'config.json'), '{"local":true}\n');
|
||||
|
||||
const { skills } = readSourceFiles(testRootDir);
|
||||
const scripts = skills[0].scripts;
|
||||
const scriptNames = scripts.map(script => script.name).sort();
|
||||
|
||||
expect(scriptNames).toEqual(['context.mjs', 'live/session-store.mjs']);
|
||||
expect(scripts.find(script => script.name === 'live/session-store.mjs').content).toContain('nested = true');
|
||||
});
|
||||
|
||||
test('should handle missing skill directory', () => {
|
||||
const { skills } = readSourceFiles(testRootDir);
|
||||
expect(skills).toEqual([]);
|
||||
@@ -636,4 +655,3 @@ describe('replacePlaceholders', () => {
|
||||
expect(result).toBe('the model .cursorrules');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user