mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-03-28 17:42:55 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d88308e6dd | ||
|
|
5183a298ec | ||
|
|
fc2d0ff4bb | ||
|
|
aa131927d4 | ||
|
|
9b785d7fdc | ||
|
|
663fa54096 | ||
|
|
56d13d3e08 | ||
|
|
7349d81102 | ||
|
|
c75aab0c14 |
5
.github/workflows/deploy.yml
vendored
5
.github/workflows/deploy.yml
vendored
@@ -15,11 +15,6 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Add meshtastic remote to vendor/firmware
|
||||
run: |
|
||||
git remote add meshtastic git@github.com:meshtastic/firmware.git
|
||||
working-directory: vendor/firmware
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -16,6 +16,6 @@
|
||||
[submodule "vendor/lofs"]
|
||||
path = vendor/lofs
|
||||
url = git@github.com:MeshEnvy/lofs.git
|
||||
[submodule "vendor/lofs-test"]
|
||||
path = vendor/lofs-test
|
||||
url = git@github.com:MeshEnvy/lofs-test.git
|
||||
[submodule "vendor/meshtastic-firmware"]
|
||||
path = vendor/meshtastic-firmware
|
||||
url = git@github.com:MeshEnvy/firmware.git
|
||||
|
||||
@@ -18,9 +18,11 @@ Join our Discord community: [https://discord.gg/8KgJpvjfaJ](https://discord.gg/8
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
git submodule update --init --recursive
|
||||
bun install
|
||||
|
||||
# Run development server
|
||||
npx convex dev # Note: you will need to initialize or select a Convex project
|
||||
bun run dev
|
||||
|
||||
# Build for production
|
||||
|
||||
37
bun.lock
37
bun.lock
@@ -31,6 +31,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -753,6 +754,8 @@
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
@@ -875,8 +878,24 @@
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
@@ -897,6 +916,20 @@
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
|
||||
|
||||
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
|
||||
@@ -1027,12 +1060,16 @@
|
||||
|
||||
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.51", "", { "dependencies": { "@oxc-project/types": "=0.98.0", "@rolldown/pluginutils": "1.0.0-beta.51" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.51", "@rolldown/binding-darwin-arm64": "1.0.0-beta.51", "@rolldown/binding-darwin-x64": "1.0.0-beta.51", "@rolldown/binding-freebsd-x64": "1.0.0-beta.51", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.51", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.51", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.51", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.51", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.51", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.51", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.51", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.51", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.51", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.51" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZRLgPlS91l4JztLYEZnmMcd3Umcla1hkXJgiEiR4HloRJBBoeaX8qogTu5Jfu36rRMVLndzqYv0h+M5gJAkUfg=="],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { TARGETS } from "@/constants/targets"
|
||||
import type { Doc } from "@/convex/_generated/dataModel"
|
||||
import { ArtifactType, getArtifactFilenameBase } from "@/convex/lib/filename"
|
||||
import { computeFlagsFromConfig } from "@/convex/lib/flags"
|
||||
import modulesData from "@/convex/modules.json"
|
||||
import { getImplicitDependencies, humanizeStatus } from "@/lib/utils"
|
||||
import registryData from "@/public/registry.json"
|
||||
@@ -48,7 +49,7 @@ export function BuildProgress({ build, isAdmin = false, onRetry, showActions = t
|
||||
? `https://github.com/MeshEnvy/mesh-forge/actions/runs/${build.githubRunId}`
|
||||
: null
|
||||
|
||||
const shareUrl = `${window.location.origin}/builds?clone=${build.buildHash}`
|
||||
const shareUrl = `${window.location.origin}/builds?hash=${build.buildHash}`
|
||||
|
||||
const handleShare = async () => {
|
||||
try {
|
||||
@@ -64,7 +65,10 @@ export function BuildProgress({ build, isAdmin = false, onRetry, showActions = t
|
||||
}
|
||||
|
||||
const generateBashCommand = (): string => {
|
||||
const flags = computeFlagsFromConfig(build.config)
|
||||
const flags = computeFlagsFromConfig(
|
||||
build.config,
|
||||
registryData as Record<string, { configOptions?: Record<string, { define: string }> }>
|
||||
)
|
||||
const target = build.config.target
|
||||
const version = build.config.version
|
||||
const plugins = build.config.pluginsEnabled || []
|
||||
@@ -102,9 +106,8 @@ export function BuildProgress({ build, isAdmin = false, onRetry, showActions = t
|
||||
}
|
||||
|
||||
// Set build flags and build
|
||||
if (flags) {
|
||||
commands.push(`export PLATFORMIO_BUILD_FLAGS="${flags}"`)
|
||||
}
|
||||
// Always export PLATFORMIO_BUILD_FLAGS (even if empty) so users can see what was used
|
||||
commands.push(`export PLATFORMIO_BUILD_FLAGS="${flags || ""}"`)
|
||||
commands.push(`pio run -e ${target}`)
|
||||
|
||||
return commands.join("\n")
|
||||
@@ -129,15 +132,6 @@ export function BuildProgress({ build, isAdmin = false, onRetry, showActions = t
|
||||
}
|
||||
}
|
||||
|
||||
// Compute build flags from config (same logic as computeFlagsFromConfig in convex/builds.ts)
|
||||
const computeFlagsFromConfig = (config: typeof build.config): string => {
|
||||
return Object.keys(config.modulesExcluded)
|
||||
.sort()
|
||||
.filter(module => config.modulesExcluded[module])
|
||||
.map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
const handleRetry = async () => {
|
||||
if (!build?._id || !onRetry) return
|
||||
try {
|
||||
@@ -216,10 +210,10 @@ export function BuildProgress({ build, isAdmin = false, onRetry, showActions = t
|
||||
<h2 className="text-2xl font-semibold mb-2 flex items-center gap-2">
|
||||
{getStatusIcon()}
|
||||
<a
|
||||
href={`/builds?id=${build.buildHash}`}
|
||||
href={`/builds?hash=${build.buildHash}`}
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
navigate(`/builds?id=${build.buildHash}`)
|
||||
navigate(`/builds?hash=${build.buildHash}`)
|
||||
}}
|
||||
className="hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
|
||||
@@ -40,6 +40,7 @@ export default function Builder({ cloneHash, pluginParam }: BuilderProps) {
|
||||
const [selectedVersion, setSelectedVersion] = useState<string>(VERSIONS[0])
|
||||
const [moduleConfig, setModuleConfig] = useState<Record<string, boolean>>({})
|
||||
const [pluginConfig, setPluginConfig] = useState<Record<string, boolean>>({})
|
||||
const [pluginOptionsConfig, setPluginOptionsConfig] = useState<Record<string, Record<string, boolean>>>({})
|
||||
const [isFlashing, setIsFlashing] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [showModuleOverrides, setShowModuleOverrides] = useState(false)
|
||||
@@ -49,6 +50,12 @@ export default function Builder({ cloneHash, pluginParam }: BuilderProps) {
|
||||
// Get all enabled plugins
|
||||
const enabledPlugins = Object.keys(pluginConfig).filter(id => pluginConfig[id] === true)
|
||||
|
||||
// Compute all enabled plugins (explicit + implicit)
|
||||
const allEnabledPlugins = getDependedPlugins(
|
||||
enabledPlugins,
|
||||
registryData as Record<string, { dependencies?: Record<string, string> }>
|
||||
)
|
||||
|
||||
// Calculate plugin compatibility
|
||||
const { compatibleTargets, filteredGroupedTargets, filteredTargetCategories } = usePluginCompatibility(
|
||||
enabledPlugins,
|
||||
@@ -132,6 +139,10 @@ export default function Builder({ cloneHash, pluginParam }: BuilderProps) {
|
||||
setPluginConfig(pluginObj)
|
||||
setShowPlugins(true)
|
||||
}
|
||||
|
||||
if (config.pluginConfigs) {
|
||||
setPluginOptionsConfig(config.pluginConfigs)
|
||||
}
|
||||
}, [cloneHash, sharedBuild, handleSelectTarget, setActiveCategory, TARGET_CATEGORIES])
|
||||
|
||||
const selectedTargetLabel = (selectedTarget && TARGETS[selectedTarget]?.name) || selectedTarget
|
||||
@@ -197,6 +208,27 @@ export default function Builder({ cloneHash, pluginParam }: BuilderProps) {
|
||||
})
|
||||
}
|
||||
|
||||
const handleTogglePluginOption = (pluginId: string, optionKey: string, enabled: boolean) => {
|
||||
setPluginOptionsConfig(prev => {
|
||||
const next = { ...prev }
|
||||
if (!next[pluginId]) {
|
||||
next[pluginId] = {}
|
||||
}
|
||||
const pluginOptions = { ...next[pluginId] }
|
||||
if (enabled) {
|
||||
pluginOptions[optionKey] = true
|
||||
} else {
|
||||
delete pluginOptions[optionKey]
|
||||
}
|
||||
if (Object.keys(pluginOptions).length === 0) {
|
||||
delete next[pluginId]
|
||||
} else {
|
||||
next[pluginId] = pluginOptions
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleFlash = async () => {
|
||||
if (!selectedTarget) return
|
||||
setIsFlashing(true)
|
||||
@@ -214,13 +246,27 @@ export default function Builder({ cloneHash, pluginParam }: BuilderProps) {
|
||||
const plugin = (registryData as Record<string, { version: string }>)[slug]
|
||||
return `${slug}@${plugin.version}`
|
||||
})
|
||||
|
||||
// Filter plugin config to only include enabled plugins
|
||||
const filteredPluginConfig = Object.keys(pluginOptionsConfig).reduce(
|
||||
(acc, pluginId) => {
|
||||
if (allEnabledPlugins.includes(pluginId)) {
|
||||
acc[pluginId] = pluginOptionsConfig[pluginId]
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Record<string, boolean>>
|
||||
)
|
||||
|
||||
const result = await ensureBuildFromConfig({
|
||||
target: selectedTarget,
|
||||
version: selectedVersion,
|
||||
modulesExcluded: moduleConfig,
|
||||
pluginsEnabled: pluginsEnabled.length > 0 ? pluginsEnabled : undefined,
|
||||
pluginConfigs: Object.keys(filteredPluginConfig).length > 0 ? filteredPluginConfig : undefined,
|
||||
registryData: registryData,
|
||||
})
|
||||
navigate(`/builds?id=${result.buildHash}`)
|
||||
navigate(`/builds?hash=${result.buildHash}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
setErrorMessage("Failed to start build. Please try again.")
|
||||
@@ -273,13 +319,18 @@ export default function Builder({ cloneHash, pluginParam }: BuilderProps) {
|
||||
|
||||
<PluginConfig
|
||||
pluginConfig={pluginConfig}
|
||||
pluginOptionsConfig={pluginOptionsConfig}
|
||||
selectedTarget={selectedTarget}
|
||||
pluginParam={pluginParam}
|
||||
pluginFlashCounts={pluginFlashCounts}
|
||||
showPlugins={showPlugins}
|
||||
onToggleShow={() => setShowPlugins(prev => !prev)}
|
||||
onTogglePlugin={handleTogglePlugin}
|
||||
onReset={() => setPluginConfig({})}
|
||||
onTogglePluginOption={handleTogglePluginOption}
|
||||
onReset={() => {
|
||||
setPluginConfig({})
|
||||
setPluginOptionsConfig({})
|
||||
}}
|
||||
/>
|
||||
|
||||
<BuildActions
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Download, Star, Zap } from "lucide-react"
|
||||
import { navigate } from "vike/client/router"
|
||||
@@ -55,6 +56,10 @@ interface PluginCardLinkToggleProps extends PluginCardBaseProps {
|
||||
onToggle: (enabled: boolean) => void
|
||||
disabled?: boolean
|
||||
enabledLabel?: string
|
||||
diagnostics?: {
|
||||
checked: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
}
|
||||
}
|
||||
|
||||
type PluginCardProps = PluginCardToggleProps | PluginCardLinkProps | PluginCardLinkToggleProps
|
||||
@@ -176,49 +181,35 @@ export function PluginCard(props: PluginCardProps) {
|
||||
{isIncompatible && incompatibleReason && (
|
||||
<p className="text-xs text-red-400 mt-1 font-medium">{incompatibleReason}</p>
|
||||
)}
|
||||
{/* Diagnostics checkbox - only show for link-toggle variant when enabled */}
|
||||
{isLinkToggle && props.isEnabled && props.diagnostics && (
|
||||
<div className="mt-2">
|
||||
<label className="flex items-start gap-2 cursor-pointer group" htmlFor={`${id}-diagnostics`}>
|
||||
<Checkbox
|
||||
id={`${id}-diagnostics`}
|
||||
checked={props.diagnostics.checked}
|
||||
onCheckedChange={props.diagnostics.onCheckedChange}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-slate-300 group-hover:text-white transition-colors">
|
||||
Include Diagnostics
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">Enable diagnostic logging for this plugin</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata row */}
|
||||
<div className="flex items-center gap-3 flex-wrap text-xs text-slate-400">
|
||||
{version && <span className="text-slate-500">v{version}</span>}
|
||||
{isLinkToggle && flashCount !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="text-slate-400"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
aria-label="Download"
|
||||
>
|
||||
<path d="m14 2l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm4 18V9h-5V4H6v16zm-6-1l-4-4h2.5v-3h3v3H16z" />
|
||||
</svg>
|
||||
<span>{flashCount}</span>
|
||||
</div>
|
||||
)}
|
||||
{isLink && downloads !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>{downloads.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
{homepage &&
|
||||
homepage !== repo &&
|
||||
(isLink || isLinkToggle) &&
|
||||
(isLink ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
window.open(homepage, "_blank", "noopener,noreferrer")
|
||||
}}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
aria-label="Homepage"
|
||||
>
|
||||
{/* Footer with metadata and toggle */}
|
||||
<div className="flex items-center justify-between gap-3 mt-auto pt-2 border-t border-slate-700/50">
|
||||
{/* Metadata row */}
|
||||
<div className="flex items-center gap-3 flex-wrap text-xs text-slate-400">
|
||||
{version && <span className="text-slate-500">v{version}</span>}
|
||||
{isLinkToggle && flashCount !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@@ -227,103 +218,131 @@ export function PluginCard(props: PluginCardProps) {
|
||||
className="text-slate-400"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
aria-label="Download"
|
||||
>
|
||||
<path d="m14 2l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm4 18V9h-5V4H6v16zm-6-1l-4-4h2.5v-3h3v3H16z" />
|
||||
</svg>
|
||||
<span>{flashCount}</span>
|
||||
</div>
|
||||
)}
|
||||
{isLink && downloads !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>{downloads.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
{homepage &&
|
||||
homepage !== repo &&
|
||||
(isLink || isLinkToggle) &&
|
||||
(isLink ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
window.open(homepage, "_blank", "noopener,noreferrer")
|
||||
}}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
aria-label="Homepage"
|
||||
>
|
||||
<path
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="text-slate-400"
|
||||
fill="currentColor"
|
||||
d="m12 3l11 8.25l-1.2 1.6L20 11.5V21H4v-9.5l-1.8 1.35l-1.2-1.6zm-4.65 9.05q0 1.325 1.425 2.825T12 18q1.8-1.625 3.225-3.125t1.425-2.825q0-1.1-.75-1.825T14.1 9.5q-.65 0-1.188.263T12 10.45q-.375-.425-.937-.687T9.9 9.5q-1.05 0-1.8.725t-.75 1.825"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="text-slate-400"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
aria-label="Homepage"
|
||||
role="img"
|
||||
aria-label="Homepage"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12 3l11 8.25l-1.2 1.6L20 11.5V21H4v-9.5l-1.8 1.35l-1.2-1.6zm-4.65 9.05q0 1.325 1.425 2.825T12 18q1.8-1.625 3.225-3.125t1.425-2.825q0-1.1-.75-1.825T14.1 9.5q-.65 0-1.188.263T12 10.45q-.375-.425-.937-.687T9.9 9.5q-1.05 0-1.8.725t-.75 1.825"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<path
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="text-slate-400"
|
||||
fill="currentColor"
|
||||
d="m12 3l11 8.25l-1.2 1.6L20 11.5V21H4v-9.5l-1.8 1.35l-1.2-1.6zm-4.65 9.05q0 1.325 1.425 2.825T12 18q1.8-1.625 3.225-3.125t1.425-2.825q0-1.1-.75-1.825T14.1 9.5q-.65 0-1.188.263T12 10.45q-.375-.425-.937-.687T9.9 9.5q-1.05 0-1.8.725t-.75 1.825"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
{starsBadgeUrl &&
|
||||
repo &&
|
||||
(isLink ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
window.open(repo, "_blank", "noopener,noreferrer")
|
||||
}}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
aria-label="GitHub repository"
|
||||
>
|
||||
<img src={starsBadgeUrl} alt="GitHub stars" className="h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={repo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src={starsBadgeUrl} alt="GitHub stars" className="h-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Build Now button - absolutely positioned in lower right */}
|
||||
{isLink && (
|
||||
<div className="absolute bottom-4 right-4 z-10">
|
||||
role="img"
|
||||
aria-label="Homepage"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12 3l11 8.25l-1.2 1.6L20 11.5V21H4v-9.5l-1.8 1.35l-1.2-1.6zm-4.65 9.05q0 1.325 1.425 2.825T12 18q1.8-1.625 3.225-3.125t1.425-2.825q0-1.1-.75-1.825T14.1 9.5q-.65 0-1.188.263T12 10.45q-.375-.425-.937-.687T9.9 9.5q-1.05 0-1.8.725t-.75 1.825"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
{starsBadgeUrl &&
|
||||
repo &&
|
||||
(isLink ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
window.open(repo, "_blank", "noopener,noreferrer")
|
||||
}}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
aria-label="GitHub repository"
|
||||
>
|
||||
<img src={starsBadgeUrl} alt="GitHub stars" className="h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={repo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src={starsBadgeUrl} alt="GitHub stars" className="h-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Toggle switch or Build Now button */}
|
||||
{isLinkToggle ? (
|
||||
<Switch
|
||||
checked={props.isEnabled}
|
||||
onCheckedChange={props.onToggle}
|
||||
disabled={props.disabled}
|
||||
labelLeft="Skip"
|
||||
labelRight={props.enabledLabel || "Add"}
|
||||
className={props.isEnabled ? "bg-green-600" : "bg-slate-600"}
|
||||
/>
|
||||
) : isLink ? (
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate(`/builds?plugin=${id}`)
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-cyan-400 bg-cyan-400/10 border border-cyan-400/20 rounded hover:bg-cyan-400/20 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-cyan-400 bg-cyan-400/10 border border-cyan-400/20 rounded hover:bg-cyan-400/20 transition-colors cursor-pointer shrink-0"
|
||||
>
|
||||
<Zap className="w-3 h-3" />
|
||||
Build Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Toggle switch - absolutely positioned in lower right */}
|
||||
{isLinkToggle && (
|
||||
<div className="absolute bottom-4 right-4 z-10">
|
||||
<div className="flex flex-col items-end gap-1 shrink-0">
|
||||
<Switch
|
||||
checked={props.isEnabled}
|
||||
onCheckedChange={props.onToggle}
|
||||
disabled={props.disabled}
|
||||
labelLeft="Skip"
|
||||
labelRight={props.enabledLabel || "Add"}
|
||||
className={props.isEnabled ? "bg-green-600" : "bg-slate-600"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const baseClassName = `relative flex ${isToggle ? "items-start gap-4" : "flex-col gap-3"} p-4 rounded-lg border-2 transition-colors h-full ${
|
||||
const baseClassName = `relative flex ${isToggle ? "items-start gap-4" : "flex-col"} p-4 rounded-lg border-2 transition-colors h-full ${
|
||||
isIncompatible
|
||||
? "border-slate-800 bg-slate-900/30 opacity-60 cursor-not-allowed"
|
||||
: prominent
|
||||
|
||||
@@ -10,23 +10,27 @@ import { ChevronDown, ChevronRight } from "lucide-react"
|
||||
|
||||
interface PluginConfigProps {
|
||||
pluginConfig: Record<string, boolean>
|
||||
pluginOptionsConfig: Record<string, Record<string, boolean>>
|
||||
selectedTarget: string
|
||||
pluginParam?: string
|
||||
pluginFlashCounts: Record<string, number>
|
||||
showPlugins: boolean
|
||||
onToggleShow: () => void
|
||||
onTogglePlugin: (id: string, enabled: boolean) => void
|
||||
onTogglePluginOption: (pluginId: string, optionKey: string, enabled: boolean) => void
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
export function PluginConfig({
|
||||
pluginConfig,
|
||||
pluginOptionsConfig,
|
||||
selectedTarget,
|
||||
pluginParam,
|
||||
pluginFlashCounts,
|
||||
showPlugins,
|
||||
onToggleShow,
|
||||
onTogglePlugin,
|
||||
onTogglePluginOption,
|
||||
onReset,
|
||||
}: PluginConfigProps) {
|
||||
const pluginCount = Object.keys(pluginConfig).filter(id => pluginConfig[id] === true).length
|
||||
@@ -86,13 +90,13 @@ export function PluginConfig({
|
||||
{Object.entries(registryData)
|
||||
.sort(([, pluginA], [, pluginB]) => {
|
||||
// Featured plugins first
|
||||
const featuredA = pluginA.featured ?? false
|
||||
const featuredB = pluginB.featured ?? false
|
||||
const featuredA = (pluginA as { featured?: boolean }).featured ?? false
|
||||
const featuredB = (pluginB as { featured?: boolean }).featured ?? false
|
||||
if (featuredA !== featuredB) {
|
||||
return featuredA ? -1 : 1
|
||||
}
|
||||
// Then alphabetical by name
|
||||
return pluginA.name.localeCompare(pluginB.name)
|
||||
return (pluginA as { name: string }).name.localeCompare((pluginB as { name: string }).name)
|
||||
})
|
||||
.map(([slug, plugin]) => {
|
||||
// Check if plugin is required by another explicitly selected plugin
|
||||
@@ -129,6 +133,12 @@ export function PluginConfig({
|
||||
// Check if this is the preselected plugin from URL
|
||||
const isPreselected = pluginParam === slug
|
||||
|
||||
const pluginRegistry = plugin as {
|
||||
featured?: boolean
|
||||
}
|
||||
const isPluginEnabled = allEnabledPlugins.includes(slug)
|
||||
const pluginOptions = pluginOptionsConfig[slug] ?? {}
|
||||
|
||||
return (
|
||||
<PluginCard
|
||||
key={`${slug}-${selectedTarget}`}
|
||||
@@ -137,16 +147,24 @@ export function PluginConfig({
|
||||
name={plugin.name}
|
||||
description={plugin.description}
|
||||
imageUrl={plugin.imageUrl}
|
||||
isEnabled={allEnabledPlugins.includes(slug)}
|
||||
isEnabled={isPluginEnabled}
|
||||
onToggle={enabled => onTogglePlugin(slug, enabled)}
|
||||
disabled={isImplicit || isIncompatible || isPreselected}
|
||||
enabledLabel={isPreselected ? "Locked" : isImplicit ? "Required" : "Add"}
|
||||
incompatibleReason={isIncompatible ? "Not compatible with this target" : undefined}
|
||||
featured={plugin.featured ?? false}
|
||||
featured={pluginRegistry.featured ?? false}
|
||||
flashCount={pluginFlashCounts[slug] ?? 0}
|
||||
homepage={plugin.homepage}
|
||||
version={plugin.version}
|
||||
repo={plugin.repo}
|
||||
diagnostics={
|
||||
isPluginEnabled
|
||||
? {
|
||||
checked: pluginOptions.diagnostics ?? false,
|
||||
onCheckedChange: checked => onTogglePluginOption(slug, "diagnostics", checked === true),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -15,6 +15,7 @@ import type * as builds from "../builds.js";
|
||||
import type * as helpers from "../helpers.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as lib_filename from "../lib/filename.js";
|
||||
import type * as lib_flags from "../lib/flags.js";
|
||||
import type * as lib_r2 from "../lib/r2.js";
|
||||
import type * as plugins from "../plugins.js";
|
||||
import type * as profiles from "../profiles.js";
|
||||
@@ -33,6 +34,7 @@ declare const fullApi: ApiFromModules<{
|
||||
helpers: typeof helpers;
|
||||
http: typeof http;
|
||||
"lib/filename": typeof lib_filename;
|
||||
"lib/flags": typeof lib_flags;
|
||||
"lib/r2": typeof lib_r2;
|
||||
plugins: typeof plugins;
|
||||
profiles: typeof profiles;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api, internal } from "./_generated/api"
|
||||
import type { Doc, Id } from "./_generated/dataModel"
|
||||
import { internalMutation, mutation, query } from "./_generated/server"
|
||||
import { ArtifactType, getArtifactFilenameBase } from "./lib/filename"
|
||||
import { computeFlagsFromConfig } from "./lib/flags"
|
||||
import { generateSignedDownloadUrl } from "./lib/r2"
|
||||
import { buildFields } from "./schema"
|
||||
|
||||
@@ -31,18 +32,8 @@ export const getByHash = query({
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Computes flags string from build config.
|
||||
* Only excludes modules explicitly marked as excluded (config[id] === true).
|
||||
*/
|
||||
export function computeFlagsFromConfig(config: Doc<"builds">["config"]): string {
|
||||
// Sort modules to ensure consistent order
|
||||
return Object.keys(config.modulesExcluded)
|
||||
.sort()
|
||||
.filter(module => config.modulesExcluded[module])
|
||||
.map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`)
|
||||
.join(" ")
|
||||
}
|
||||
// Re-export for backward compatibility
|
||||
export { computeFlagsFromConfig } from "./lib/flags"
|
||||
|
||||
/**
|
||||
* Encodes a byte array to base62 string.
|
||||
@@ -77,16 +68,39 @@ async function computeBuildHashInternal(
|
||||
version: string,
|
||||
target: string,
|
||||
flags: string,
|
||||
plugins: string[]
|
||||
plugins: string[],
|
||||
pluginConfig?: Record<string, Record<string, boolean>>
|
||||
): Promise<string> {
|
||||
// Input is now the exact parameters used for the build
|
||||
// Sort plugins array for consistent hashing
|
||||
const sortedPlugins = [...plugins].sort()
|
||||
// Sort plugin config for consistent hashing
|
||||
const sortedPluginConfig = pluginConfig
|
||||
? Object.keys(pluginConfig)
|
||||
.sort()
|
||||
.reduce(
|
||||
(acc, pluginSlug) => {
|
||||
const sortedOptions = Object.keys(pluginConfig[pluginSlug])
|
||||
.sort()
|
||||
.reduce(
|
||||
(opts, optKey) => {
|
||||
opts[optKey] = pluginConfig[pluginSlug][optKey]
|
||||
return opts
|
||||
},
|
||||
{} as Record<string, boolean>
|
||||
)
|
||||
acc[pluginSlug] = sortedOptions
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Record<string, boolean>>
|
||||
)
|
||||
: undefined
|
||||
const input = JSON.stringify({
|
||||
version,
|
||||
target,
|
||||
flags,
|
||||
plugins: sortedPlugins,
|
||||
pluginConfig: sortedPluginConfig,
|
||||
})
|
||||
|
||||
// Use Web Crypto API for SHA-256 hashing
|
||||
@@ -103,10 +117,13 @@ async function computeBuildHashInternal(
|
||||
* Computes buildHash from build config.
|
||||
* This is the single source of truth for build hash computation.
|
||||
*/
|
||||
export async function computeBuildHash(config: Doc<"builds">["config"]): Promise<{ hash: string; flags: string }> {
|
||||
const flags = computeFlagsFromConfig(config)
|
||||
export async function computeBuildHash(
|
||||
config: Doc<"builds">["config"],
|
||||
registryData?: Record<string, { configOptions?: Record<string, { define: string }> }>
|
||||
): Promise<{ hash: string; flags: string }> {
|
||||
const flags = computeFlagsFromConfig(config, registryData)
|
||||
const plugins = config.pluginsEnabled ?? []
|
||||
const hash = await computeBuildHashInternal(config.version, config.target, flags, plugins)
|
||||
const hash = await computeBuildHashInternal(config.version, config.target, flags, plugins, config.pluginConfigs)
|
||||
return { hash, flags }
|
||||
}
|
||||
|
||||
@@ -185,6 +202,8 @@ export const ensureBuildFromConfig = mutation({
|
||||
version: v.string(),
|
||||
modulesExcluded: v.optional(v.record(v.string(), v.boolean())),
|
||||
pluginsEnabled: v.optional(v.array(v.string())),
|
||||
pluginConfigs: v.optional(v.record(v.string(), v.record(v.string(), v.boolean()))),
|
||||
registryData: v.optional(v.any()),
|
||||
profileName: v.optional(v.string()),
|
||||
profileDescription: v.optional(v.string()),
|
||||
},
|
||||
@@ -195,10 +214,15 @@ export const ensureBuildFromConfig = mutation({
|
||||
modulesExcluded: args.modulesExcluded ?? {},
|
||||
target: args.target,
|
||||
pluginsEnabled: args.pluginsEnabled,
|
||||
pluginConfigs: args.pluginConfigs,
|
||||
}
|
||||
|
||||
// Compute build hash (single source of truth)
|
||||
const { hash: buildHash, flags } = await computeBuildHash(config)
|
||||
// Registry data is optional - diagnostics works for all plugins without registry lookup
|
||||
const registryData = args.registryData as
|
||||
| Record<string, { configOptions?: Record<string, { define: string }> }>
|
||||
| undefined
|
||||
const { hash: buildHash, flags } = await computeBuildHash(config, registryData)
|
||||
|
||||
const existingBuild = await ctx.db
|
||||
.query("builds")
|
||||
|
||||
52
convex/lib/flags.ts
Normal file
52
convex/lib/flags.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Doc } from "../_generated/dataModel"
|
||||
|
||||
/**
|
||||
* Computes flags string from build config.
|
||||
* Only excludes modules explicitly marked as excluded (config[id] === true).
|
||||
* Also includes plugin config options (e.g., diagnostics).
|
||||
*
|
||||
* @param config - Build configuration with modulesExcluded and pluginConfigs
|
||||
* @param registryData - Optional registry data for custom config options (not needed for diagnostics)
|
||||
*/
|
||||
export function computeFlagsFromConfig(
|
||||
config: Doc<"builds">["config"],
|
||||
registryData?: Record<string, { configOptions?: Record<string, { define: string }> }>
|
||||
): string {
|
||||
const flags: string[] = []
|
||||
|
||||
// Sort modules to ensure consistent order
|
||||
const moduleFlags = Object.keys(config.modulesExcluded)
|
||||
.sort()
|
||||
.filter(module => config.modulesExcluded[module])
|
||||
.map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`)
|
||||
flags.push(...moduleFlags)
|
||||
|
||||
// Add plugin config options (diagnostics is available for all plugins)
|
||||
if (config.pluginConfigs) {
|
||||
for (const [pluginSlug, pluginOptions] of Object.entries(config.pluginConfigs)) {
|
||||
// Handle diagnostics option (available for all plugins)
|
||||
if (pluginOptions.diagnostics) {
|
||||
// Convert plugin slug to uppercase define name (e.g., "lofs" -> "LOFS_PLUGIN_DIAGNOSTICS")
|
||||
const defineName = `${pluginSlug.toUpperCase().replace(/-/g, "_")}_PLUGIN_DIAGNOSTICS`
|
||||
flags.push(`-D${defineName}`)
|
||||
}
|
||||
|
||||
// Handle other custom config options from registry (if any)
|
||||
if (registryData) {
|
||||
const plugin = registryData[pluginSlug]
|
||||
if (plugin?.configOptions) {
|
||||
for (const [optionKey, enabled] of Object.entries(pluginOptions)) {
|
||||
if (optionKey !== "diagnostics" && enabled) {
|
||||
const option = plugin.configOptions[optionKey]
|
||||
if (option?.define) {
|
||||
flags.push(`-D${option.define}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flags.join(" ")
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export const buildConfigFields = {
|
||||
modulesExcluded: v.record(v.string(), v.boolean()),
|
||||
target: v.string(),
|
||||
pluginsEnabled: v.optional(v.array(v.string())),
|
||||
pluginConfigs: v.optional(v.record(v.string(), v.record(v.string(), v.boolean()))),
|
||||
}
|
||||
|
||||
export const profileFields = {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -11,12 +11,12 @@ export default function BuildsPage() {
|
||||
const pageContext = usePageContext()
|
||||
const urlSearchParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null
|
||||
const cloneHash = urlSearchParams?.get("clone")
|
||||
const buildId = urlSearchParams?.get("id")
|
||||
const buildHash = urlSearchParams?.get("hash")
|
||||
const pluginParam = urlSearchParams?.get("plugin")
|
||||
|
||||
// If we have a build ID, show the build progress page
|
||||
if (buildId) {
|
||||
return <BuildViewPage buildHash={buildId} />
|
||||
// If we have a build hash, show the build progress page
|
||||
if (buildHash) {
|
||||
return <BuildViewPage buildHash={buildHash} />
|
||||
}
|
||||
|
||||
// Otherwise, show the builder (handles clone and plugin params)
|
||||
|
||||
@@ -28,4 +28,4 @@ While the build tools themselves (PlatformIO, pip, etc.) still require internet
|
||||
|
||||
## Getting Started
|
||||
|
||||
Ready to build your custom firmware? Visit the [builder](https://meshforge.org/builds/new) to get started, or explore the [Plugin Registry](/docs/registry) to see what's available.
|
||||
Ready to build your custom firmware? Visit the [builder](https://meshforge.org/builds) to get started, or explore the [Plugin Registry](/docs/registry) to see what's available.
|
||||
|
||||
@@ -10,9 +10,9 @@ The registry contains plugins that add new features and capabilities to Meshtast
|
||||
|
||||
### Using Mesh Forge (Recommended)
|
||||
|
||||
The easiest way to use plugins is through [Mesh Forge](https://meshforge.org/builds/new), which lets you build custom firmware with plugins directly in your browser:
|
||||
The easiest way to use plugins is through [Mesh Forge](https://meshforge.org/builds), which lets you build custom firmware with plugins directly in your browser:
|
||||
|
||||
1. Visit [meshforge.org/builds/new](https://meshforge.org/builds/new)
|
||||
1. Visit [meshforge.org/builds](https://meshforge.org/builds)
|
||||
2. Browse available plugins from the registry
|
||||
3. Select the plugins you want to include
|
||||
4. Build your custom firmware
|
||||
|
||||
@@ -111,12 +111,11 @@ export default function LandingPage() {
|
||||
<div className="text-center py-20 px-8">
|
||||
<h1 className="text-6xl md:text-7xl font-bold mb-6 leading-[1.1]">
|
||||
<span className="bg-gradient-to-r from-cyan-400 to-blue-600 bg-clip-text text-transparent inline-block pb-2">
|
||||
Mesh beyond the basics
|
||||
Mesh Apps
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-slate-400 max-w-3xl mx-auto mb-10">
|
||||
Build custom firmware with third-party plugins: BBS's, custom hardware, games, and more. An open ecosystem
|
||||
growing to hundreds of plugins.
|
||||
Mesh Forge is an open ecosystem of apps, BBS's, custom hardware, games, and more.
|
||||
</p>
|
||||
|
||||
{featuredPlugins.length > 0 && (
|
||||
|
||||
2
vendor/lobbs
vendored
2
vendor/lobbs
vendored
Submodule vendor/lobbs updated: 907f22b22c...b60527d92a
1
vendor/lofs-test
vendored
1
vendor/lofs-test
vendored
Submodule vendor/lofs-test deleted from 55802f580f
1
vendor/meshtastic-firmware
vendored
Submodule
1
vendor/meshtastic-firmware
vendored
Submodule
Submodule vendor/meshtastic-firmware added at 3ca68e7788
@@ -2,11 +2,19 @@ import mdx from "@mdx-js/rollup"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import path from "node:path"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import vike from "vike/plugin"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vike(), mdx(), react(), tailwindcss()],
|
||||
plugins: [
|
||||
vike(),
|
||||
mdx({
|
||||
remarkPlugins: [remarkGfm],
|
||||
}),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "."),
|
||||
|
||||
Reference in New Issue
Block a user