Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/actions/sync-instructions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ and overwrites local copies.
Synced files:

- `.github/copilot-instructions.md` β€” repo-wide Copilot instructions
- `.github/instructions/*.instructions.md` β€” path-specific guidelines (markdown, openscad, python, renovate)
- `.github/instructions/*.instructions.md` β€” all path-specific guidelines (discovered dynamically via GitHub API)
- `.github/pull_request_template.md` β€” PR template

### Requirements

- `curl`, `jq` β€” both pre-installed on GitHub Actions runners
- `GITHUB_TOKEN` (optional) β€” used for API authentication to avoid rate limits; automatically available in GitHub Actions via `${{ github.token }}`

## πŸ€” Why

HomeRacker maintains a well-proven, optimized instruction set that ensures consistent AI behavior
Expand Down
34 changes: 27 additions & 7 deletions .github/actions/sync-instructions/sync-instructions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,39 @@ set -euo pipefail
REPO="kellerlabs/homeracker"
REF="${1:-main}"
BASE_URL="https://raw.githubusercontent.com/${REPO}/${REF}"
API_BASE="https://api.github.com/repos/${REPO}/contents/.github/instructions"

# Files to sync (source path relative to repo root)
FILES=(
AUTH_ARGS=()
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
AUTH_ARGS=(-H "Authorization: token ${GITHUB_TOKEN}")
fi

# Explicit files outside .github/instructions/
EXPLICIT_FILES=(
".github/copilot-instructions.md"
".github/instructions/markdown.instructions.md"
".github/instructions/openscad.instructions.md"
".github/instructions/python.instructions.md"
".github/instructions/renovate.instructions.md"
".github/pull_request_template.md"
)

echo "Syncing Copilot instructions from ${REPO}@${REF}..."

# Discover all .instructions.md files dynamically
instruction_names="$(
curl -fsSL "${AUTH_ARGS[@]}" --get --data-urlencode "ref=${REF}" "${API_BASE}" \
| jq -r '
if type == "array" then
.[].name | select(endswith(".instructions.md"))
else
error("Expected array from GitHub contents API")
end
'
)"

FILES=("${EXPLICIT_FILES[@]}")
while read -r name; do
[[ -z "${name}" ]] && continue
FILES+=(".github/instructions/${name}")
done <<< "${instruction_names}"

TMPDIR=$(mktemp -d)
trap 'rm -rf "${TMPDIR}"' EXIT

Expand All @@ -44,7 +64,7 @@ for file in "${FILES[@]}"; do
fi
done

if [ "${FAILED}" -eq 1 ]; then
if [[ "${FAILED}" -eq 1 ]]; then
echo ""
echo "ERROR: One or more files failed to download"
exit 1
Expand Down
4 changes: 2 additions & 2 deletions .github/agents/model-polish.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ Review **every** `.scad` file in `lib/` and `parts/`:
```bash
./cmd/export/export-png.sh models/<name>/parts/<part>.scad
```
2. Verify the PNG was created next to the source file.
3. Update the model README πŸ“Έ Catalog table to reference each PNG.
2. Verify the PNG was created in the `renders/` subfolder (e.g., `parts/renders/<part>.png`).
3. Update the model README πŸ“Έ Catalog table to reference each PNG from `parts/renders/`.

### Phase 5 β€” Test File

Expand Down
25 changes: 16 additions & 9 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ HomeRacker is a modular 3D-printable rack-building system. Core components use p
- Follow the What / Why / How / References structure defined in the markdown instructions.
- When renaming or restructuring code, update or rename the associated docs to keep everything tidy.
- When adding or modifying model parts (`parts/*.scad`), **generate preview PNGs** with `cmd/export/export-png.sh` and update both the model's README πŸ“Έ Catalog and the parent `models/README.md` index.
- **Assets Policy**: All manually-created images (photos, diagrams, logos, MakerWorld description images) are hosted in [`kellerlabs/assets`](https://github.com/kellerlabs/assets). Push directly to its `main` branch. Reference via `https://raw.githubusercontent.com/kellerlabs/assets/main/<repo>/<path>`. Only auto-generated render PNGs (`**/renders/*.png`) are tracked in source repos. See [ADR-001](docs/decisions/ADR-001-image-hosting-assets-repo.md).
- **Architecture Decision Records (ADRs)**:
- When the user makes an architectural or design decision during a session, create an ADR in `docs/decisions/`.
- Format: `ADR-NNN-<slug>.md`, numbered sequentially.
- Cross-link the ADR from related docs (READMEs, instructions, other ADRs) where viable.
- **Commits**: Use [Conventional Commits](https://www.conventionalcommits.org/) format
- Types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`
- Format: `type(scope): description` or `type: description`
Expand All @@ -40,16 +45,18 @@ Before terminal operations, consider running these steps (use best judgement):
> **Note**: `scadm` upgrade and `scadm install` run automatically via a `SessionStart` hook (see `.github/hooks/`).

## **MANDATORY** Workflow
> [!IMPORTANT]
> **On errors**: Step back, check docs, ask user if stuckβ€”don't iterate blindly

1. **Check repo patterns** first for consistency
2. **Consult online docs** (especially BOSL2: https://github.com/BelfrySCAD/BOSL2/wiki). Use Context7 MCP Server for quick access to docs and codebase where applicable.
3. **Ask before proceeding** if requirements conflict with best practices or patterns in the repo
4. **Provide outline** before implementation for confirmation
5. **Make the change** and immediately test it - do NOT announce completion before testing
6. **Update** existing documentation (.md files) and create new ones where applicable
7. **Run pre-commit hooks** to catch formatting/linting issues before commit. Fix any issues found (no ignores allowed).
8. **Code review**: Review ALL changes made in the session β€” check for consistency, missed edge cases, and unintended side effects before presenting to the user.
9. **Creating PRs**: Use the **GitHub MCP Server** (never `gh` CLI). Read `.github/pull_request_template.md` and fill in every section. Keep it brief per project conventions.
10. **On errors**: Step back, check docs, ask user if stuckβ€”don't iterate blindly
1. **Consult online docs** (especially BOSL2: https://github.com/BelfrySCAD/BOSL2/wiki). Use Context7 MCP Server for quick access to docs and codebase where applicable.
1. **Ask before proceeding** if requirements conflict with best practices or patterns in the repo
1. **Provide outline** before implementation for confirmation
1. **Make the change** and immediately test it - do NOT announce completion before testing
1. **Update** existing documentation (.md files) and create new ones where applicable
1. **Run pre-commit hooks** to catch formatting/linting issues before commit. Fix any issues found (no ignores allowed).
1. **Code review**: When done implementing review ALL changes made from a holistic perspective β€” check for consistency, missed edge cases, and unintended side effects before presenting to the user.
1. **Creating PRs**: Use the **GitHub MCP Server** (never `gh` CLI). Read `.github/pull_request_template.md` and fill in every section. Keep it brief per project conventions.

## Technology-Specific Guidelines
- Documentation: See .github/instructions/markdown.instructions.md
Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/openscad.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ applyTo: "**/*.scad"
## Preview PNGs

- When adding or modifying a parts file, **generate a preview PNG** with `cmd/export/export-png.sh`.
- PNGs are stored next to their source `.scad` file (e.g., `parts/foo.scad` β†’ `parts/foo.png`).
- PNGs are stored in a `renders/` subfolder next to their source (e.g., `parts/foo.scad` β†’ `parts/renders/foo.png`).
- Update the model's README πŸ“Έ Catalog table and the `models/README.md` index accordingly.
152 changes: 152 additions & 0 deletions .github/skills/makerworld-description/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
name: makerworld-description
description: >
Extract MakerWorld model descriptions into git-tracked DESCRIPTION.md files,
or convert them back to pasteable HTML for MakerWorld's CKEditor.
USE FOR: extracting descriptions from MakerWorld model pages, creating new
DESCRIPTION.md files, updating existing descriptions, converting markdown
descriptions to HTML for MakerWorld publishing.
DO NOT USE FOR: uploading files to MakerWorld, managing print profiles, or
OpenSCAD model creation (use @makerworld-model agent instead).
---

# 🌐 MakerWorld Description Skill

## πŸ“Œ What

Manages MakerWorld model descriptions as git-tracked `DESCRIPTION.md` files. Supports two flows:

1. **Extract** (MakerWorld β†’ Git): Scrape a MakerWorld model page and produce a clean `DESCRIPTION.md` + images
2. **Publish** (Git β†’ MakerWorld): Convert `DESCRIPTION.md` to HTML for pasting into MakerWorld's CKEditor

## πŸ”§ Extract Flow

Given a MakerWorld model URL, extract the description into a `DESCRIPTION.md` file.

### Steps

1. **Ask the user** which model this is for and where to save it:
- Target repo (`homeracker` or `homeracker-exclusive`)
- Model name (e.g. `core`, `foot`, `frontpanel`)
- Output path defaults to `models/<name>/makerworld/DESCRIPTION.md`

2. **Fetch the page** using `fetch_webpage` with query `"model description details"` and the MakerWorld URL.

3. **Parse the Description section**. The description content is between the `### Description` heading and the `### Comment & Rating` section. Extract only this content. Ignore:
- Navigation elements, cookie banners, footer
- Print Profile section, Bill of Materials
- Comment & Rating section and all comments
- Remixes, Related Models sections
- "We use cookies" banner

4. **Clean the markdown**:
- Convert extracted content to well-formatted markdown
- Preserve: headings (##, ###), bold/italic, bullet lists, numbered lists, links, images, linked images (`[![alt](img)](url)`), horizontal rules
- Strip: `[Image: Image]` placeholders, duplicate blank lines, trailing whitespace
- Keep image URLs as-is initially (they'll be downloaded next)
- **Detect orphan linked images**: `fetch_webpage` drops `<img>` elements inside `<a>` tags, producing empty links like `[](https://...)`. Collect all such `[](url)` patterns β€” these are linked images whose `src` was lost. See step 5a for resolution
- **Be aware of invisible drops**: `fetch_webpage` silently drops `<iframe>` embeds (YouTube videos, etc.) with no trace at all β€” just blank whitespace. See step 5b for resolution

5. **Download images**:
- Identify all image URLs in the description (from `makerworld.bblmw.com` CDN or other sources)
- Skip external reference images (e.g. `encrypted-tbn0.gstatic.com` meme images) β€” keep those as URLs
- Download MakerWorld CDN images to the **assets repo**: `assets/<target-repo>/models/<name>/makerworld/images/`
- Use descriptive filenames based on context (e.g. `diagonal_supports.png`, `showcase_rack.jpg`)
- Reference images using absolute URLs: `![alt](https://raw.githubusercontent.com/kellerlabs/assets/main/<target-repo>/models/<name>/makerworld/images/filename.png)`

5a. **Resolve orphan linked images** (from step 4):
- For each `[](url)` pattern found, present the user with a numbered list showing the link target URL
- Ask the user to open the MakerWorld page in a browser and identify what image is displayed for each link (e.g. a logo, a model thumbnail, a collection banner)
- The user should provide the image URL (right-click β†’ "Copy image address") or a description
- Once URLs are obtained: download to `assets/<target-repo>/models/<name>/makerworld/images/`, replace the empty link with a proper linked image using absolute URL: `[![alt text](https://raw.githubusercontent.com/kellerlabs/assets/main/<target-repo>/models/<name>/makerworld/images/filename.png)](url)`
- If the user cannot provide the URL, insert a TODO comment in the markdown: `<!-- TODO: linked image missing for url -->`

5b. **Resolve missing embeds** (from step 4):
- Ask the user: "Are there any embedded videos (YouTube, etc.) on the MakerWorld description page that should be included?"
- If yes, ask for the YouTube video URL(s) and where they appear in the description
- Insert a markdown-compatible YouTube link in the appropriate location. Use a linked thumbnail image:
```markdown
[![Video Title](https://img.youtube.com/vi/<VIDEO_ID>/maxresdefault.jpg)](https://www.youtube.com/watch?v=<VIDEO_ID>)
```
- `md-to-mw.py` automatically converts these to CKEditor `<figure class="media">` embeds during publish

6. **Add YAML frontmatter** to the top of `DESCRIPTION.md`:
```yaml
---
makerworld_url: https://makerworld.com/en/models/<id>-<slug>
extracted: YYYY-MM-DD
---
```

7. **Save** the file to the target path.

8. **Verify** by reading back the file and checking:
- Frontmatter is valid YAML
- All images have been downloaded
- No `[Image: Image]` placeholders remain
- No orphan `[](url)` empty links remain (all resolved or marked with TODO)
- Embedded videos are accounted for (user confirmed none missing, or added)
- Markdown renders with proper heading hierarchy

## πŸ“€ Publish Flow

Convert a `DESCRIPTION.md` file to HTML for pasting into MakerWorld.

### Steps

1. Direct the user to run the conversion script:
```bash
python cmd/export/md-to-mw.py models/<name>/makerworld/DESCRIPTION.md
```
2. This generates `DESCRIPTION.html` in the same directory (gitignored).
3. Instruct the user to open the HTML file in a browser, select all (`Ctrl+A`), copy (`Ctrl+C`), then paste into MakerWorld's description editor.

## πŸ“ File Convention

```
models/<name>/makerworld/
β”œβ”€β”€ <name>.scad # MakerWorld parametric source
β”œβ”€β”€ DESCRIPTION.md # Model description (source of truth)
β”œβ”€β”€ DESCRIPTION.html # Generated HTML (gitignored)
β”œβ”€β”€ renders/ # Auto-generated render previews
β”‚ β”œβ”€β”€ <name>_mw_assembly_view.png
β”‚ └── <name>_mw_plate_1.png
```

Images are stored in the **assets repo** (`kellerlabs/assets`):
```
assets/<repo>/models/<name>/makerworld/images/
β”œβ”€β”€ showcase.jpg
└── diagonal_supports.png
```

## ⚠️ Important Notes

- `DESCRIPTION.md` is the **source of truth**. Always edit it in git, never in MakerWorld directly.
- After editing `DESCRIPTION.md`, re-run `md-to-mw.py` and re-paste into MakerWorld.
- The conversion script embeds local images as base64 data URIs so the HTML is fully self-contained β€” no broken links, no browser permissions needed. External image URLs (http/https) are passed through unchanged.
- Since images are now hosted in the `kellerlabs/assets` repo with absolute URLs, `md-to-mw.py` passes them through directly β€” no base64 encoding needed for assets-hosted images.
- MakerWorld CDN images (from `makerworld.bblmw.com`) should be downloaded to the assets repo during extraction. External reference images (memes, badges, etc.) can stay as external URLs.

## πŸ› Known Limitations

### `fetch_webpage` drops linked images

`fetch_webpage` converts HTML to markdown but loses `<img>` elements nested inside `<a>` tags. This produces empty links like `[](https://some-url)` where a linked image (`<a href="..."><img src="..."></a>`) existed in the original HTML.

**Common patterns affected**:
- Logo/banner images linking to external sites (e.g. HomeRacker logo β†’ homeracker.org)
- Model thumbnail images linking to other MakerWorld models
- Collection banner images linking to MakerWorld collections

**Workaround**: Step 5a in the Extract Flow asks the user to manually supply the missing image URLs by inspecting the page in a browser.

### `fetch_webpage` silently drops embedded iframes

`fetch_webpage` completely discards `<iframe>` elements (YouTube embeds, etc.) with **no trace** β€” no placeholder, no URL, just blank whitespace. Unlike linked images which leave a detectable `[](url)` pattern, embedded videos are entirely invisible in the output.

**Common patterns affected**:
- YouTube video embeds in description sections
- Any other `<iframe>`-based content

**Workaround**: Step 5b in the Extract Flow explicitly asks the user whether embedded videos exist on the page. Videos are stored as linked YouTube thumbnails in `DESCRIPTION.md`. During publish, `md-to-mw.py` automatically converts these to CKEditor 5 `<figure class="media">` embeds with `data-oembed-url`, which MakerWorld's editor recognises as native video embeds.
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ renovate_base/
# Flatten checksums (cached in CI, not tracked)
models/.flatten-checksums

# Generated MakerWorld HTML (from md-to-mw.py)
DESCRIPTION.html

# Images β€” hosted in kellerlabs/assets repo
*.png
*.jpg
*.jpeg
*.gif
*.webp
*.svg
*.ico
*.xcf

# Whitelist: auto-generated render previews + favicon
!**/renders/*.png
!favicon.ico

# Python
__pycache__/
*.py[cod]
Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ repos:
[
"--line-length=121"
]
- repo: https://github.com/kellerlabs/pre-commit-hooks
rev: pre-commit-hooks-v0.3.0
hooks:
- id: optimize-images
- id: flatten-validate
pass_filenames: false
files: '^(scadm\.json|models/.*\.scad|cmd/scadm/scadm/flatten\.py)'
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
Expand Down Expand Up @@ -51,12 +58,6 @@ repos:
[
"--rcfile=.lint/pylint/.pylintrc"
]
- repo: https://github.com/kellerlabs/pre-commit-hooks
rev: pre-commit-hooks-v0.2.2
hooks:
- id: flatten-validate
pass_filenames: false
files: '^(scadm\.json|models/.*\.scad|cmd/scadm/scadm/flatten\.py)'
- repo: local
hooks:
- id: scadm-tests
Expand Down
Loading
Loading