mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(contrib): add od-contribute skill for non-coder contributors
Adds a Claude Code skill at .claude/skills/od-contribute/ that walks any OD
user — including non-coders — through a first-PR contribution flow:
- Ship a Skill / Design System made with OD
- Translate README / QUICKSTART / CONTRIBUTING to a new language
- Fix a typo / dead link / write a use-case blog post
- Report a high-quality bug (issue path, no PR)
The skill replaces the test-driven dev-loop of auto-github-contributor with
type-specific no-code validators (frontmatter parse, markdown link check,
code-fence balance, structural overlap with reference DESIGN.md files), so
artifact-only contributions don't have to pretend to be code.
This commit only adds files under .claude/ — no product code, no build
config, no runtime dependencies. .gitignore is amended with three explicit
exceptions so the skill is tracked while personal Claude state (sessions,
settings, etc.) stays ignored as before.
Next steps (separate PRs):
- Wire the OD app to mount this skill for its embedded agent
- Add a "Ship to GitHub" UI button in OD that invokes /od-contribute
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* feat(contrib): English-by-default skill + zip installer for non-coders
Two follow-ups to the initial od-contribute skill:
1. Skill content is now English with an explicit instruction at the top
telling the agent to mirror the user's chat language for every user-
facing prompt. Generated artifacts (PR titles, commit messages, PR/
issue body) stay English regardless — GitHub convention.
2. tools/od-contribute-installer/ ships a cross-platform installer that
drops the skill into every supported agent's home dir without the
user opening a terminal:
install.command macOS double-click
install.bat Windows double-click
install.sh Linux
Targets covered:
~/.claude/skills/od-contribute/ Claude Code (native)
~/.claude/commands/od-contribute.md Claude Code slash command
~/.agents/skills/od-contribute/ Codex CLI (canonical)
~/.codex/skills/od-contribute/ Codex CLI (legacy, only
written if ~/.codex/ exists)
Verified Codex CLI reads the same SKILL.md frontmatter format as
Claude Code (source: openai/codex codex-rs/core-skills/src/loader.rs).
Added agents/openai.yaml sidecar inside the skill for Codex picker UX.
3. build-zip.sh produces od-contribute-installer.zip (~37KB) from the
in-repo skill. The zip is meant to be hosted as a GitHub Release
asset; the marketing site button points at:
github.com/nexu-io/open-design/releases/latest/download/od-contribute-installer.zip
(See tools/od-contribute-installer/HOSTING.md for the manual release
recipe; CI workflow can come later.)
The zip itself is gitignored — distribute via Releases, not source.
Still no product code touched, no build config changed.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* refactor(contrib): drop zip installer; ship single curl one-liner
Replace tools/od-contribute-installer/ (4 install scripts + zip build
machinery) with a single self-bootstrapping tools/install-od-contribute.sh.
User flow becomes:
1. Click button on opendesign.so
2. Modal shows: paste this into your AI agent's chat:
curl -sSL https://raw.githubusercontent.com/nexu-io/open-design/main/tools/install-od-contribute.sh | bash
3. Agent runs it via its Bash tool. User never touches a terminal.
4. /od-contribute is live in their next chat.
Why this is better than the zip approach:
* Zero downloads visible to the user — no .zip in their Downloads folder
* Zero unzip step
* Zero terminal window flash (the agent's Bash tool runs in-process)
* Zero per-OS installer files (.command/.bat/.sh) to maintain
* Auto-updates: re-running the one-liner pulls the latest skill from main
The script downloads only the skill subtree (.claude/skills/od-contribute/
and .claude/commands/od-contribute.md) from a GitHub tarball — no `git`
dependency, just curl + tar (universally available).
Targets remain the same:
~/.claude/skills/od-contribute/
~/.claude/commands/od-contribute.md
~/.agents/skills/od-contribute/
~/.codex/skills/od-contribute/ (only if ~/.codex/ exists)
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* chore(contrib): remove leftover zip artifact
Build artifact accidentally committed in the previous commit.
Cleaning up so the binary doesn't live in git history.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): make skill work in sandboxed agents (Codex.app, Cursor)
macOS App Sandbox apps like Codex.app cannot reach the system keychain
where `gh auth login` stores the GitHub token by default. Result: the
skill's check-prereqs.sh fails on `gh auth status` with a misleading
"not authenticated" error, even when gh works fine in the user's regular
shell.
Two changes:
1. config.sh: if GH_TOKEN isn't set in the env, fall back to reading a
.gh-token file at the skill root. Lets a user (or the OD app, or a
future OAuth Device Flow bootstrapper) drop a token there once and
have every skill script pick it up automatically.
2. check-prereqs.sh: accept GH_TOKEN-from-env as a valid auth path
alongside `gh auth status`. When neither works, the error hint now
shows BOTH options:
A) gh auth login from a regular terminal (any agent)
B) gh auth token > <skill>/.gh-token (sandboxed agents)
Verified: in my local Claude Code (where gh has keychain access), the
keychain path still wins and nothing changes. With GH_TOKEN exported,
check-prereqs.sh succeeds without even consulting gh auth status.
Future: implement OAuth Device Flow inside the skill so non-coder users
hitting this in Codex.app can authenticate by clicking a link, no
terminal involved. That's a separate PR.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* chore(contrib): move install script into skill folder (CI policy fix)
The repo's tools/ directory has a strict allowlist policy enforced by
scripts/guard.ts — only AGENTS.md, dev/, pack/, and serve/ are permitted
top-level entries. Moving install-od-contribute.sh out of tools/ and into
.claude/skills/od-contribute/install.sh:
- Satisfies the guard policy (no scripts/guard.ts edit needed)
- Co-locates the install script with the skill it installs (cleaner
mental model: skill folder is self-contained)
- The install URL stays inside the gitignore exception we already
established for .claude/skills/od-contribute/
Public install URL changes from
raw.githubusercontent.com/.../main/tools/install-od-contribute.sh
to
raw.githubusercontent.com/.../main/.claude/skills/od-contribute/install.sh
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): address @nettee/looper review feedback (3 blocking issues)
Three real bugs caught by the looper review bot, all fixed:
1) create-pr.sh:48 — git diff missed untracked files
`git diff --quiet || git diff --cached --quiet` ignored untracked paths,
so the most common contribution shape (a brand-new Skill folder, a new
translation file, a new doc) hit the else branch and pushed an empty
commit. Replaced with `git status --porcelain` which sees untracked,
plus a post-stage sanity check via `git diff --cached --quiet` so we
skip the commit cleanly if everything turned out to be in .gitignore.
2) validate-skill-submission.sh:34 — frontmatter parse too lenient
The awk fence-counter accepted `---` anywhere in the file as the
opening fence. A SKILL.md with prose before the YAML block parsed as
"valid frontmatter" by this script while the actual loaders (Claude
Code + codex-rs/core-skills) required the fence on line 1 and would
reject it. Added an explicit head -n 1 check so leading prose is
rejected with a clear error before awk runs.
3) check-prereqs.sh:87 — gh api user failure swallowed
`GH_USER="$(gh api user --jq .login 2>/dev/null || echo '?')"` set
GH_USER to literal "?" when the API call failed (revoked token,
missing 'repo' scope, network), then the script exited READY=1.
Downstream that propagated to TARGET_FORK="?/open-design" and
blew up at push time.
Dropped the `|| echo '?'` fallback. An empty GH_USER now triggers a
structured error with three common causes and the recovery command,
and exits 2.
While here, also fixed a related bug: this script sources config.sh
which has `set -euo pipefail`, so -e leaked in and aborted the
script silently the moment any check failed (instead of accumulating
diagnostics like the original auto-github-contributor design
intended). Added explicit `set +e; set -uo pipefail` after sourcing
to restore the "keep checking past failures" behavior the comment
on line 7 promised.
Smoke-tested all four fixes locally:
- create-pr.sh: git status --porcelain correctly sees untracked files
- validator: rejects SKILL.md starting with prose, passes well-formed
- check-prereqs.sh: with stubbed gh that fails `gh api user`, now
exits 2 with the structured error (was: silent exit 1)
- check-prereqs.sh: happy path on real machine unchanged
Thanks @nettee for the careful review.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): macOS Bash 3.2 + over-strict link validator (review round 2)
Two more blocking issues from the looper review, plus one related bug I
caught while re-testing on real OD docs.
1) discover-i18n-gaps.sh: removed Bash 4 dep (declare -A)
macOS still ships Bash 3.2.57 by default and most agent-spawned bash
subprocesses inherit that. `declare -A SEEN_LANG=()` failed with
`declare: -A: invalid option`, crashing Step 3b before any translation
target could be shown.
Replaced the associative array with a newline-delimited string set
(\n<lang>\n bracket form to avoid prefix-overlap false matches like
zh vs zh-CN). Verified end-to-end on /bin/bash 3.2.57 against the
actual OD repo: returns the correct 28 stale-translation rows
across the four English source docs.
Also fixed a latent path-stripping bug in the same loop: `find`
emits `./README.zh-CN.md` with leading `./`, so `${path#README.}`
wasn't stripping the prefix at all. Switched to basename-first.
2) validate-markdown.sh: --reference flag for i18n / docs-edit flows
The validator was treating every relative link target as a file path
and failing on slugs like `skills/blog-post/` that are website
router routes, not files in the checkout. A structure-preserving
translation of README.md couldn't pass even when the user changed
nothing except language.
Added --reference <orig> flag. The validator now builds a "known
already-broken" set of refs from the source file and excuses those
in the new file. Newly-introduced broken refs still fail.
Without --reference (e.g. brand-new blog file with no prior version),
the relative-ref check is skipped entirely with a SKIP note — since
we can't tell route slugs from file paths in isolation, failing
would be wrong. Code-fence balance + external-link health still run.
Updated SKILL.md so the i18n branch (3b.6) and the docs branch
(3c.6) call validate-markdown.sh with --reference pointing at the
English source / HEAD revision respectively.
3) (caught while testing) URL extraction regex too loose
`grep -oE 'https?://[^) ]+'` was capturing trailing quotes from HTML
<img src="..."> tags in OD's README, e.g.
https://cms-assets.youmind.com/.../foo.jpg"
The trailing `"` made the curl HEAD return 404. Tightened the
character class to also stop at `"`, `'`, `<`, `>`, `[`, `]`.
With this fix, README.md now passes all checks (20 external links
verified 2xx/3xx).
Smoke-tested on macOS /bin/bash 3.2.57 with the actual nexu-io/open-design
working copy. All four scenarios behave correctly:
- README.md without --reference → SKIP relative-ref check, PASS overall
- README.md with --reference itself → 34 refs excused as pre-existing, PASS
- Newly-introduced broken ref → FAIL (regression catch preserved)
- Old test cases (skill validator, prereq check) → still pass
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): preserve .gh-token across install.sh reruns
`install_skill_to()` did `rm -rf $dest` before copying in the new skill,
which wiped any user-local state files. The most consequential one is
`.gh-token` — sandboxed agents (Codex.app, Cursor) write a GitHub token
there because they can't reach the macOS keychain (see check-prereqs.sh's
hint and config.sh's fallback path).
Effect: the documented upgrade path ("re-run the curl one-liner to pull
the latest skill") would silently lose the token on every refresh, and
the very next /od-contribute run would fail at the prereq gate with
"no GitHub credentials available", forcing the user back through manual
token setup. This affects exactly the audience the PR is aimed at.
Fix: stash any file in PRESERVE=(.gh-token) to a tempdir before rm -rf,
restore after the copy, re-chmod 600 on the way back. Test:
1. Pre-seed .gh-token in all three target dirs
2. Run installer
3. Verify all three tokens still present, contents unchanged, perms 600
Centralized the preserved-state list as PRESERVE=() so future per-user
state (e.g. an OAuth-flow-saved refresh token) only has to be added in
one place.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): i18n stale false-positive + tier markdown link check (round 4)
Two more blocking issues from looper, both real.
1) discover-i18n-gaps.sh: false-stale on same-commit translations
`git log --since=@<epoch>` is INCLUSIVE of the boundary epoch, so when
the English source and a translation get touched in the SAME commit
(a very common pattern: bulk i18n refresh, structural edits applied
across all locales), the shared commit was counted toward
english_commits_since_translation. Result: an already-current
translation was reported with `status="stale", english_commits_since_
translation=1`, and Step 3b would suggest it for refresh — driving
users into no-op PRs.
Reproduced exactly per looper's case: README.md and README.uk.md both
have last commit 338cb4d at epoch 1779948707; the OLD predicate
returned 1, the NEW predicate returns 0.
Switched commits_between() from `--since=@<epoch>` math to commit
ancestry: `git rev-list <tr_sha>..HEAD -- <newer>`. tr_sha..HEAD reads
"commits reachable from HEAD but not from tr_sha", which correctly
excludes the shared tip when both files were last touched together.
2) validate-markdown.sh: brand-new files bypassed local link check
The previous fix skipped relative-ref validation entirely when
--reference was absent. That covered slug-style refs (good) but also
covered explicit `./foo.md` and `../bar/baz.md` style refs (bad).
Step 3c (new blog post) doesn't pass --reference, so a contribution
could ship with `[broken](./missing.md)` and pass the validator.
Tiered the relative-ref check:
- Image refs (``) — ALWAYS validated. Markdown image
syntax is never a website route.
- Refs starting with `./` or `../` — ALWAYS validated. Explicit
relative paths are unambiguous file references.
- Other link refs (`skills/blog-post/` style) — only validated
when --reference is supplied; otherwise skipped (could be route).
In all cases, refs already broken in --reference (when supplied) are
excused as pre-existing rather than reported as regressions.
Verified against looper's exact repro (`[new broken](./missing.md)`
in a brand-new file with no --reference): now correctly fails. Also
verified ambiguous-slug test (`skills/blog-post/`) still skips
without --reference, image refs always check, and README.md regression
tests both with and without --reference still pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): catch bare-path refs in validators (review round 5)
Two narrow follow-ups to the round-4 tiered link checks:
- validate-skill-submission.sh: scan every non-URL, non-anchor markdown
link target in SKILL.md (not just `./` / `../` prefixed paths). Plain
intra-skill refs like `[ref](references/foo.md)` were previously
ignored by the regex, letting broken bundles pass. Escape detection
switches to lexical (segment count) instead of `cd … && pwd -P`, so a
missing intermediate directory no longer masquerades as an escape.
- validate-markdown.sh: treat file-like targets (`*.md`, `*.png`,
`*.svg`, image/asset/script extensions) as on-disk refs even without
`--reference`. `[doc](missing.md)` is unambiguously a sibling file,
not a website route, and Step 3c (new docs/blog) had no `--reference`
to fall back on. Slug-style refs without an extension still get
skipped without `--reference`.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(contrib): scratch leak + dedupe gate + workdir reuse (review round 6)
Three blocking issues from looper round 6, all fixed.
1) create-pr.sh + setup-workspace.sh: .od-contrib/ scratch leaked into PR
`git add -A` in create-pr.sh staged everything in the worktree, including
the skill's internal scratch dir (.od-contrib/type.txt, .od-contrib/
slug.txt, .od-contrib/PR-BODY.md created by setup-workspace.sh and the
render step). OD's .gitignore doesn't exclude .od-contrib/, so every PR
opened through this flow shipped those bookkeeping files in the user's
contribution diff.
Two layers of defense:
- setup-workspace.sh now writes `.od-contrib/` to .git/info/exclude
when preparing the workdir (repo-local exclude, not committed).
- create-pr.sh now uses an explicit pathspec `:!:.od-contrib` on its
git status / git add calls. So even if a workdir was prepared
differently, this script alone refuses to stage the scratch dir.
Verified with a temp repo containing both .od-contrib/PR-BODY.md and a
user file: only the user file lands in the index after `git add -A
-- . :!:.od-contrib`.
2) create-issue.sh: dedupe gate didn't actually gate
The --dedupe-keywords flag printed search hits to stderr but then
unconditionally fell through to `gh issue create`. The `|| true` after
the gh search pipeline also swallowed network/jq failures, so a broken
search looked identical to "no duplicates found" — and the issue got
created either way. The user never got a real chance to choose
"comment on existing / open anyway / cancel".
Now:
- Run gh search and jq as separate steps; either failure exits 2 with
a structured REASON=search_failed/parse_failed.
- If matches > 0 AND --allow-duplicates was NOT passed, exit 3 with
REASON=duplicates_found and MATCH_COUNT=N. Caller must explicitly
re-run with --allow-duplicates after surfacing matches to the user.
- The script now requires `jq` (added od::require jq) since we
actually parse JSON.
- Updated the docstring at the top so the caller contract (ask the
user, then re-invoke with --allow-duplicates) is explicit.
Verified: searching keyword "preview" against nexu-io/open-design
matches 5 open issues; the script exits 3 and never calls
`gh issue create`.
3) setup-workspace.sh: same-day workdir reuse leaked stale state
`SESSION_DIR=<TYPE>-<SLUG>-<YYYYMMDD>` reused the same directory for
every same-day, same-(type,slug) invocation. The most acute case:
SKILL.md 3b.1 calls `setup-workspace.sh i18n translate` BEFORE the
user has picked a doc/language, so every i18n attempt on the same
day landed in `i18n-translate-<date>/` — and untracked files from an
abandoned earlier translation survived `git checkout`/`pull` and
leaked into the next user's run.
Two changes:
- Bumped tag to second precision: `<YYYYMMDD>-<HHMMSS>`. Two human-
paced sessions in the same second is vanishingly rare. Verified
two rapid runs produce different tags (114208 vs 114209).
- When a workdir IS reused (same SESSION_TAG passed in explicitly,
or rare clock collision), now does `git reset --hard HEAD` and
`git clean -fdx` first so the run starts from a known-good base
instead of inheriting prior occupant state.
The branch name now also tracks the timestamp tag, so two runs can't
accidentally end up on the same feature branch either.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: leilei926524-tech <leilei926524-tech@users.noreply.github.com>
Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
2.2 KiB
2.2 KiB
OD repo map — what goes where
Mirrors nexu-io/open-design CONTRIBUTING.md so the skill doesn't need to re-fetch it on every run. If this drifts from upstream CONTRIBUTING.md, upstream wins — re-read the live file when in doubt.
Three high-leverage contribution surfaces (per OD's CONTRIBUTING.md)
| If you want to… | You're really adding | Where it lives | Ship size |
|---|---|---|---|
| Make OD render a new kind of artifact | a Skill | skills/<your-skill>/ |
one folder, ~2 files |
| Make OD speak a new brand's visual language | a Design System | design-systems/<brand>/DESIGN.md |
one Markdown file |
| Hook up a new coding-agent CLI | an Agent adapter | apps/daemon/src/agents.ts |
~10 lines (code — out of scope for this skill) |
| Improve docs, port a section to fr / de / zh-CN, fix typos | docs | README.md, README.fr.md, README.de.md, README.zh-CN.md, docs/, QUICKSTART.md |
one PR |
Localized doc files we know about
| Doc family | English source | Translations seen on disk (as of plan time) |
|---|---|---|
| README | README.md |
ar, de, es, fr, ja-JP, ko, pt-BR, ru, tr, uk, zh-CN, zh-TW |
| QUICKSTART | QUICKSTART.md |
de, fr, ja-JP, pt-BR, zh-CN, zh-TW |
| CONTRIBUTING | CONTRIBUTING.md |
de, fr, ja-JP, pt-BR, zh-CN |
| MAINTAINERS | MAINTAINERS.md |
de, fr, ja-JP, pt-BR, zh-CN |
The skill discover-i18n-gaps.sh does NOT trust this table — it scans the workspace at runtime. Use this list only when you need to seed an AskUserQuestion card without a workspace.
Issue templates
bug-report.yml— required fields: description, steps to reproduce, expected, version, platform.feature-request.yml— out of scope for this skill (feature requests should come from product, not auto-routed.)preview-v0.8.0-feedback.yml— branch-specific.
Out-of-scope surfaces (don't touch from this skill)
apps/daemon/src/— daemon code. Requires real review.apps/web/src/— web app code. Requires real review.packages/,plugins/,tools/— internal libs.e2e/— Playwright-driven; non-trivial to author.
If a user asks to contribute to those surfaces, suggest the original auto-github-contributor skill (TDD pipeline) instead.