create-meith 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +106 -49
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/scaffold-extension.ts +1 -1
- package/src/scaffold.ts +103 -46
package/dist/bin.mjs
CHANGED
|
@@ -23,7 +23,7 @@ var MATERIALIZED_AT_ROOT = [
|
|
|
23
23
|
"tsconfig.json",
|
|
24
24
|
"next-env.d.ts"
|
|
25
25
|
];
|
|
26
|
-
var VERCEL_BUILD_COMMAND = `
|
|
26
|
+
var VERCEL_BUILD_COMMAND = `meith migrate && forum-web build ${AT_ROOT_FLAG}`;
|
|
27
27
|
var TICK_PATH = "/api/system/tick";
|
|
28
28
|
var TICK_SCHEDULE = "0 3 * * *";
|
|
29
29
|
var RESEND_SENDER_MAILBOX = "noreply";
|
|
@@ -85,7 +85,7 @@ var ENV_DATABASE_URL_PROSE = `# Your Postgres connection string.
|
|
|
85
85
|
# database rather than the cause. Your own Postgres, with a fixed number of
|
|
86
86
|
# processes in front of it, does not need one.`;
|
|
87
87
|
var ENV_DIRECT_DATABASE_URL_PROSE = `# The other half of that pair: the DIRECT (non-pooler) string, used only by
|
|
88
|
-
# \`
|
|
88
|
+
# \`meith migrate\` and \`meith backup\`. Migrations hold a session-level
|
|
89
89
|
# advisory lock so that two deploys landing together queue instead of both
|
|
90
90
|
# applying the same migration, and a transaction-mode pooler cannot hold that
|
|
91
91
|
# lock: it takes the connection back the moment the lock statement ends, which
|
|
@@ -213,7 +213,7 @@ DATABASE_URL=
|
|
|
213
213
|
${ENV_DIRECT_DATABASE_URL_PROSE}
|
|
214
214
|
#
|
|
215
215
|
# On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
|
|
216
|
-
# here is the pooler string, the build runs \`
|
|
216
|
+
# here is the pooler string, the build runs \`meith migrate\` against it, and
|
|
217
217
|
# /install takes the second of those two session locks on first run. Left blank,
|
|
218
218
|
# the board reads Neon's own direct string \u2014 \`DATABASE_URL_UNPOOLED\` first, then
|
|
219
219
|
# \`POSTGRES_URL_NON_POOLING\` \u2014 and refuses to boot if neither is there, naming
|
|
@@ -250,7 +250,7 @@ CRON_SECRET=
|
|
|
250
250
|
#
|
|
251
251
|
# BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
|
|
252
252
|
# store. Set it when something has to reach the store from OUTSIDE a Vercel
|
|
253
|
-
# deployment \u2014 \`
|
|
253
|
+
# deployment \u2014 \`meith backup\` run on your own machine is the case that
|
|
254
254
|
# matters \u2014 because there is no OIDC identity there to borrow. Set both and the
|
|
255
255
|
# board prefers the store id, unless the token names a different store, in which
|
|
256
256
|
# case the token wins: naming another store is a deliberate act.
|
|
@@ -401,7 +401,7 @@ function scaffold(options) {
|
|
|
401
401
|
dev: `forum-web dev${atRootFlag}`,
|
|
402
402
|
build: `forum-web build${atRootFlag}`,
|
|
403
403
|
start: `forum-web start${atRootFlag}`,
|
|
404
|
-
|
|
404
|
+
meith: "meith"
|
|
405
405
|
},
|
|
406
406
|
dependencies: {
|
|
407
407
|
"@meith/web": version,
|
|
@@ -473,25 +473,14 @@ export default defineForumConfig({
|
|
|
473
473
|
`);
|
|
474
474
|
files.set(
|
|
475
475
|
"meith.plugins.ts",
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
*
|
|
485
|
-
* import { messages as greeterMessages, plugin as greeterPlugin } from '@meith/plugin-greeter'
|
|
486
|
-
*
|
|
487
|
-
* export const INSTALLED_PLUGINS: readonly InstalledPlugin<PluginDefinition>[] = [
|
|
488
|
-
* { key: 'greeter', enabled: true, plugin: greeterPlugin, messages: greeterMessages },
|
|
489
|
-
* ]
|
|
490
|
-
*
|
|
491
|
-
* and the matching entry in board.plugins.json, which is what
|
|
492
|
-
* \`community plugin:add\`/\`plugin:remove\` read inside the monorepo \u2014 kept
|
|
493
|
-
* here too so the two files agree about what is installed.
|
|
494
|
-
*/
|
|
476
|
+
`// Generated from board.plugins.json by \`meith plugin:add\` and \`meith plugin:remove\`.
|
|
477
|
+
//
|
|
478
|
+
// The simple path is those commands, or editing board.plugins.json and running one of
|
|
479
|
+
// them. A plugin that does not fit that convention can be added here by hand instead \u2014
|
|
480
|
+
// keep it out of board.plugins.json so a regenerate does not drop it.
|
|
481
|
+
//
|
|
482
|
+
// docs/customization/plugins.md explains both.
|
|
483
|
+
|
|
495
484
|
import type { InstalledPlugin } from '@meith/web/config'
|
|
496
485
|
|
|
497
486
|
export const INSTALLED_PLUGINS: readonly InstalledPlugin[] = []
|
|
@@ -552,8 +541,8 @@ updates:
|
|
|
552
541
|
# "Custom boards").
|
|
553
542
|
#
|
|
554
543
|
# Two stages, not three: unlike the official image, this does not prune down
|
|
555
|
-
# to Next's own standalone output. The migrate role below runs \`
|
|
556
|
-
# migrate\`, and \`
|
|
544
|
+
# to Next's own standalone output. The migrate role below runs \`meith
|
|
545
|
+
# migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
|
|
557
546
|
# with tsx at the moment it runs (see the meith repository's
|
|
558
547
|
# docs/contributing/development.md, "Consuming the board from a workspace") \u2014 it needs
|
|
559
548
|
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
@@ -594,6 +583,23 @@ ENV PORT=3000
|
|
|
594
583
|
ENV HOSTNAME=0.0.0.0
|
|
595
584
|
EXPOSE 3000
|
|
596
585
|
|
|
586
|
+
# Uploaded files \u2014 avatars, board images, attachments \u2014 land here, and the
|
|
587
|
+
# compose file mounts the persistent "uploads" volume over this path. Creating
|
|
588
|
+
# it in the image, owned by node, is what lets the fresh volume inherit that
|
|
589
|
+
# ownership; UPLOADS_DIR gives the board an absolute path so the working
|
|
590
|
+
# directory never decides where uploads go. Without both, uploads land on the
|
|
591
|
+
# container's own layer and a redeploy discards them.
|
|
592
|
+
ENV UPLOADS_DIR=/app/.uploads
|
|
593
|
+
RUN mkdir -p /app/.uploads && chown node:node /app/.uploads
|
|
594
|
+
|
|
595
|
+
# \`meith <command>\` on PATH runs this board's own operator CLI \u2014 the same one
|
|
596
|
+
# node_modules/.bin/meith is \u2014 so a Coolify terminal or \`docker compose exec web
|
|
597
|
+
# meith ...\` needs no path. It cd's to /board so the CLI finds this board's
|
|
598
|
+
# config, and overrides any wrapper an inherited base image installed, which
|
|
599
|
+
# would target the board that image was built from, not this one.
|
|
600
|
+
RUN printf '#!/bin/sh\\ncd /board\\nexec node_modules/.bin/meith "$@"\\n' > /usr/local/bin/meith \\
|
|
601
|
+
&& chmod +x /usr/local/bin/meith
|
|
602
|
+
|
|
597
603
|
# node:alpine already carries a non-root "node" user; the board's own files
|
|
598
604
|
# are copied in as root above, so they need handing over before this drops
|
|
599
605
|
# privilege.
|
|
@@ -628,8 +634,8 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
|
628
634
|
# of minutes rather than a cold toolchain build.
|
|
629
635
|
#
|
|
630
636
|
# Two stages, not three: unlike the official image, this does not prune down
|
|
631
|
-
# to Next's own standalone output. The migrate role below runs \`
|
|
632
|
-
# migrate\`, and \`
|
|
637
|
+
# to Next's own standalone output. The migrate role below runs \`meith
|
|
638
|
+
# migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
|
|
633
639
|
# with tsx at the moment it runs (see the meith repository's
|
|
634
640
|
# docs/contributing/development.md, "Consuming the board from a workspace") \u2014 it needs
|
|
635
641
|
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
@@ -671,6 +677,23 @@ ENV PORT=3000
|
|
|
671
677
|
ENV HOSTNAME=0.0.0.0
|
|
672
678
|
EXPOSE 3000
|
|
673
679
|
|
|
680
|
+
# Uploaded files \u2014 avatars, board images, attachments \u2014 land here, and the
|
|
681
|
+
# compose file mounts the persistent "uploads" volume over this path. Creating
|
|
682
|
+
# it in the image, owned by node, is what lets the fresh volume inherit that
|
|
683
|
+
# ownership; UPLOADS_DIR gives the board an absolute path so the working
|
|
684
|
+
# directory never decides where uploads go. Without both, uploads land on the
|
|
685
|
+
# container's own layer and a redeploy discards them.
|
|
686
|
+
ENV UPLOADS_DIR=/app/.uploads
|
|
687
|
+
RUN mkdir -p /app/.uploads && chown node:node /app/.uploads
|
|
688
|
+
|
|
689
|
+
# \`meith <command>\` on PATH runs this board's own operator CLI \u2014 the same one
|
|
690
|
+
# node_modules/.bin/meith is \u2014 so a Coolify terminal or \`docker compose exec web
|
|
691
|
+
# meith ...\` needs no path. It cd's to /board so the CLI finds this board's
|
|
692
|
+
# config, and overrides any wrapper an inherited base image installed, which
|
|
693
|
+
# would target the board that image was built from, not this one.
|
|
694
|
+
RUN printf '#!/bin/sh\\ncd /board\\nexec node_modules/.bin/meith "$@"\\n' > /usr/local/bin/meith \\
|
|
695
|
+
&& chmod +x /usr/local/bin/meith
|
|
696
|
+
|
|
674
697
|
# node:alpine already carries a non-root "node" user; the board's own files
|
|
675
698
|
# are copied in as root above, so they need handing over before this drops
|
|
676
699
|
# privilege.
|
|
@@ -699,22 +722,22 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
|
699
722
|
set -e
|
|
700
723
|
|
|
701
724
|
# An explicit command wins over the role, the same as the official image \u2014
|
|
702
|
-
# \`docker run
|
|
703
|
-
# the CLI rather than silently starting the web server.
|
|
725
|
+
# \`docker compose run --rm web meith --help\` (or \`exec\` into the running
|
|
726
|
+
# container) should run the CLI rather than silently starting the web server.
|
|
704
727
|
if [ "$#" -gt 0 ]; then
|
|
705
728
|
exec "$@"
|
|
706
729
|
fi
|
|
707
730
|
|
|
708
|
-
case "\${
|
|
731
|
+
case "\${MEITH_ROLE:-web}" in
|
|
709
732
|
migrate)
|
|
710
733
|
# Runs to completion and exits; compose's one-shot service waits on it.
|
|
711
|
-
exec node_modules/.bin/
|
|
734
|
+
exec node_modules/.bin/meith migrate
|
|
712
735
|
;;
|
|
713
736
|
web)
|
|
714
737
|
exec node_modules/.bin/forum-web start
|
|
715
738
|
;;
|
|
716
739
|
*)
|
|
717
|
-
echo "Unknown
|
|
740
|
+
echo "Unknown MEITH_ROLE: \${MEITH_ROLE}. Expected 'web' or 'migrate'." >&2
|
|
718
741
|
exit 1
|
|
719
742
|
;;
|
|
720
743
|
esac
|
|
@@ -728,7 +751,7 @@ esac
|
|
|
728
751
|
# a health probe taken while it runs has no opinion.
|
|
729
752
|
set -e
|
|
730
753
|
|
|
731
|
-
if [ "\${
|
|
754
|
+
if [ "\${MEITH_ROLE:-web}" = "migrate" ]; then
|
|
732
755
|
exit 0
|
|
733
756
|
fi
|
|
734
757
|
|
|
@@ -865,7 +888,7 @@ services:
|
|
|
865
888
|
build: .
|
|
866
889
|
image: ${name}
|
|
867
890
|
environment:
|
|
868
|
-
|
|
891
|
+
MEITH_ROLE: migrate
|
|
869
892
|
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
870
893
|
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
871
894
|
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
@@ -997,7 +1020,7 @@ services:
|
|
|
997
1020
|
# runs the old code.
|
|
998
1021
|
pull_policy: always
|
|
999
1022
|
environment:
|
|
1000
|
-
|
|
1023
|
+
MEITH_ROLE: migrate
|
|
1001
1024
|
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
1002
1025
|
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
1003
1026
|
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
@@ -1196,7 +1219,7 @@ Two things nothing configures for you, on either path:
|
|
|
1196
1219
|
loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
|
|
1197
1220
|
worker package is not something a board outside the meith monorepo can
|
|
1198
1221
|
depend on yet. Deploy some other way and something still has to call that
|
|
1199
|
-
route (or run \`
|
|
1222
|
+
route (or run \`meith task:run\`) every minute, or nothing catches up
|
|
1200
1223
|
and nothing errors.
|
|
1201
1224
|
|
|
1202
1225
|
## Local
|
|
@@ -1214,8 +1237,8 @@ Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
|
|
|
1214
1237
|
\`DATABASE_URL\` and the two secrets in it, then:
|
|
1215
1238
|
|
|
1216
1239
|
\`\`\`sh
|
|
1217
|
-
npm run
|
|
1218
|
-
echo "<password>" | npm run
|
|
1240
|
+
npm run meith -- migrate
|
|
1241
|
+
echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
|
|
1219
1242
|
\`\`\`
|
|
1220
1243
|
|
|
1221
1244
|
## Configuring
|
|
@@ -1226,9 +1249,43 @@ echo "<password>" | npm run community -- user:create --username <name> --email <
|
|
|
1226
1249
|
- **\`/admin\`** \u2014 settings, forums, groups, members, themes, maintenance. An
|
|
1227
1250
|
administrator re-enters their password to get in, and again for anything
|
|
1228
1251
|
destructive.
|
|
1229
|
-
- **\`npm run
|
|
1252
|
+
- **\`npm run meith -- --help\`** \u2014 the operator CLI. Everything the panel does
|
|
1230
1253
|
and a few things it cannot, without a browser.
|
|
1231
1254
|
|
|
1255
|
+
## Installing plugins and themes
|
|
1256
|
+
|
|
1257
|
+
Nothing installs into a running container \u2014 a plugin or theme has to be
|
|
1258
|
+
built into the image. In this repository:
|
|
1259
|
+
|
|
1260
|
+
1. **Add it.** A **plugin** is one command, which installs the package and
|
|
1261
|
+
registers it:
|
|
1262
|
+
|
|
1263
|
+
\`\`\`sh
|
|
1264
|
+
npm run meith -- plugin:add @meith/plugin-dues
|
|
1265
|
+
\`\`\`
|
|
1266
|
+
|
|
1267
|
+
It writes \`board.plugins.json\` and regenerates \`meith.plugins.ts\` for you
|
|
1268
|
+
(\`npm run meith -- plugin:remove <key>\` reverses it). A **theme** is
|
|
1269
|
+
\`npm install --save-exact @meith/theme-midnight\`, then an entry in
|
|
1270
|
+
\`meith.config.ts\`'s \`themes\` map following the shape of the \`default\` one
|
|
1271
|
+
already there \u2014 set \`defaultTheme\` to its key to make it the board's
|
|
1272
|
+
default.
|
|
1273
|
+
|
|
1274
|
+
2. **Commit and push**, then **Redeploy** from Coolify \u2014 pushing alone does
|
|
1275
|
+
not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
|
|
1276
|
+
waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
|
|
1277
|
+
what actually pulls the result.
|
|
1278
|
+
|
|
1279
|
+
3. **If it ships database changes, apply them once it is up** \u2014 from
|
|
1280
|
+
**Admin \u2192 System** (**Version & migrations**) in the browser, or:
|
|
1281
|
+
|
|
1282
|
+
\`\`\`sh
|
|
1283
|
+
docker compose run --rm web meith upgrade
|
|
1284
|
+
\`\`\`
|
|
1285
|
+
|
|
1286
|
+
See [Installing plugins and themes](${repositoryUrl}/blob/main/docs/customization/installing.md)
|
|
1287
|
+
for the full guide.
|
|
1288
|
+
|
|
1232
1289
|
## Upgrading
|
|
1233
1290
|
|
|
1234
1291
|
\`\`\`sh
|
|
@@ -1269,7 +1326,7 @@ project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
|
|
|
1269
1326
|
\`npm install\` of anything else here \u2014 a plugin, say \u2014 stays pinned too; the
|
|
1270
1327
|
build workflow also refuses to build from anything but an exact version, as
|
|
1271
1328
|
a second line of defense. Once the rebuilt image is deployed, run
|
|
1272
|
-
\`npm run
|
|
1329
|
+
\`npm run meith -- upgrade\` against it for the plugin migrations \u2014 see
|
|
1273
1330
|
[the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
|
|
1274
1331
|
for running it against this deployment.
|
|
1275
1332
|
|
|
@@ -1512,9 +1569,9 @@ A board must stay movable, and the Blob store is the one part of this shape that
|
|
|
1512
1569
|
is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
|
|
1513
1570
|
that any host accepts, but a Vercel Blob store is reachable only through Vercel's
|
|
1514
1571
|
own API and there is no bucket to sync out of it. **The uploads are the thing you
|
|
1515
|
-
have to carry out deliberately, and \`
|
|
1572
|
+
have to carry out deliberately, and \`meith backup\` is how.**
|
|
1516
1573
|
|
|
1517
|
-
Under \`FILESTORE_DRIVER=blob\`, \`
|
|
1574
|
+
Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
|
|
1518
1575
|
default** \u2014 it walks the Blob store, pulls every object, and puts them in the
|
|
1519
1576
|
bundle beside the database dump. This is the opposite of the \`s3\` default, which
|
|
1520
1577
|
skips them, because a bucket has its own backup story you can drive yourself and
|
|
@@ -1525,7 +1582,7 @@ DATABASE_URL=\u2026 # Neon's pooled string
|
|
|
1525
1582
|
DIRECT_DATABASE_URL=\u2026 # Neon's DATABASE_URL_UNPOOLED
|
|
1526
1583
|
FILESTORE_DRIVER=blob
|
|
1527
1584
|
BLOB_READ_WRITE_TOKEN=\u2026 # create one on the store; see below
|
|
1528
|
-
npm run
|
|
1585
|
+
npm run meith -- backup
|
|
1529
1586
|
\`\`\`
|
|
1530
1587
|
|
|
1531
1588
|
Run that from a checkout of this repository, with those four values in the
|
|
@@ -1546,10 +1603,10 @@ so the same bundle moves the board either onward or away:
|
|
|
1546
1603
|
|
|
1547
1604
|
\`\`\`sh
|
|
1548
1605
|
# onto a self-hosted board with a bucket
|
|
1549
|
-
FILESTORE_DRIVER=s3 S3_BUCKET=\u2026 RESTORE_DATABASE_URL=\u2026 npm run
|
|
1606
|
+
FILESTORE_DRIVER=s3 S3_BUCKET=\u2026 RESTORE_DATABASE_URL=\u2026 npm run meith -- restore bundle.tar.gz
|
|
1550
1607
|
|
|
1551
1608
|
# onto a board that keeps uploads on its own disk
|
|
1552
|
-
RESTORE_DATABASE_URL=\u2026 npm run
|
|
1609
|
+
RESTORE_DATABASE_URL=\u2026 npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
|
|
1553
1610
|
\`\`\`
|
|
1554
1611
|
|
|
1555
1612
|
Take one before you need it. A Blob store deleted with the Vercel project takes
|
|
@@ -1893,7 +1950,7 @@ and add the matching entry to \`board.plugins.json\`:
|
|
|
1893
1950
|
{ "plugins": [{ "key": "${name}", "package": "${name}", "enabled": true }] }
|
|
1894
1951
|
|
|
1895
1952
|
Rebuild the board (\`npm run build\`) and, because this plugin ships a
|
|
1896
|
-
migration, run \`npx
|
|
1953
|
+
migration, run \`npx meith migrate\`. The plugin then appears under
|
|
1897
1954
|
**Admin \u2192 Plugins**.
|
|
1898
1955
|
|
|
1899
1956
|
## Publish and list it
|
|
@@ -2152,7 +2209,7 @@ async function run(argv, version) {
|
|
|
2152
2209
|
...kind === null ? [
|
|
2153
2210
|
"Then set DATABASE_URL, AUTH_SECRET and TICK_SECRET and deploy.",
|
|
2154
2211
|
"Something must run the tick every minute \u2014 the worker process, or",
|
|
2155
|
-
"`
|
|
2212
|
+
"`meith task:run`. Without it nothing catches up, and nothing errors."
|
|
2156
2213
|
] : [
|
|
2157
2214
|
`Then follow README.md \u2014 it walks through running the ${kind} inside a`,
|
|
2158
2215
|
"scaffolded board and submitting it to the meith.dev marketplace."
|
|
@@ -2162,7 +2219,7 @@ async function run(argv, version) {
|
|
|
2162
2219
|
}
|
|
2163
2220
|
|
|
2164
2221
|
// src/bin.ts
|
|
2165
|
-
var result = await run(process.argv.slice(2), "0.
|
|
2222
|
+
var result = await run(process.argv.slice(2), "0.28.0");
|
|
2166
2223
|
for (const line of result.lines) {
|
|
2167
2224
|
if (result.code === 0) console.log(line);
|
|
2168
2225
|
else console.error(line);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-meith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Scaffold a Meith board, plugin or theme — npx create-meith <name> writes a deployable board workspace; --plugin and --theme write extension workspaces built on the published kits.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/bin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from './cli'
|
|
3
3
|
|
|
4
|
-
const result = await run(process.argv.slice(2), '0.
|
|
4
|
+
const result = await run(process.argv.slice(2), '0.28.0')
|
|
5
5
|
for (const line of result.lines) {
|
|
6
6
|
if (result.code === 0) console.log(line)
|
|
7
7
|
else console.error(line)
|
package/src/cli.ts
CHANGED
|
@@ -145,7 +145,7 @@ export async function run(argv: readonly string[], version: string): Promise<Cli
|
|
|
145
145
|
? [
|
|
146
146
|
'Then set DATABASE_URL, AUTH_SECRET and TICK_SECRET and deploy.',
|
|
147
147
|
'Something must run the tick every minute — the worker process, or',
|
|
148
|
-
'`
|
|
148
|
+
'`meith task:run`. Without it nothing catches up, and nothing errors.',
|
|
149
149
|
]
|
|
150
150
|
: [
|
|
151
151
|
`Then follow README.md — it walks through running the ${kind} inside a`,
|
|
@@ -206,7 +206,7 @@ and add the matching entry to \`board.plugins.json\`:
|
|
|
206
206
|
{ "plugins": [{ "key": "${name}", "package": "${name}", "enabled": true }] }
|
|
207
207
|
|
|
208
208
|
Rebuild the board (\`npm run build\`) and, because this plugin ships a
|
|
209
|
-
migration, run \`npx
|
|
209
|
+
migration, run \`npx meith migrate\`. The plugin then appears under
|
|
210
210
|
**Admin → Plugins**.
|
|
211
211
|
|
|
212
212
|
## Publish and list it
|
package/src/scaffold.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const MATERIALIZED_AT_ROOT = [
|
|
|
29
29
|
'next-env.d.ts',
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
-
export const VERCEL_BUILD_COMMAND = `
|
|
32
|
+
export const VERCEL_BUILD_COMMAND = `meith migrate && forum-web build ${AT_ROOT_FLAG}`
|
|
33
33
|
|
|
34
34
|
export const TICK_PATH = '/api/system/tick'
|
|
35
35
|
|
|
@@ -103,7 +103,7 @@ const ENV_DATABASE_URL_PROSE = `# Your Postgres connection string.
|
|
|
103
103
|
# processes in front of it, does not need one.`
|
|
104
104
|
|
|
105
105
|
const ENV_DIRECT_DATABASE_URL_PROSE = `# The other half of that pair: the DIRECT (non-pooler) string, used only by
|
|
106
|
-
# \`
|
|
106
|
+
# \`meith migrate\` and \`meith backup\`. Migrations hold a session-level
|
|
107
107
|
# advisory lock so that two deploys landing together queue instead of both
|
|
108
108
|
# applying the same migration, and a transaction-mode pooler cannot hold that
|
|
109
109
|
# lock: it takes the connection back the moment the lock statement ends, which
|
|
@@ -238,7 +238,7 @@ DATABASE_URL=
|
|
|
238
238
|
${ENV_DIRECT_DATABASE_URL_PROSE}
|
|
239
239
|
#
|
|
240
240
|
# On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
|
|
241
|
-
# here is the pooler string, the build runs \`
|
|
241
|
+
# here is the pooler string, the build runs \`meith migrate\` against it, and
|
|
242
242
|
# /install takes the second of those two session locks on first run. Left blank,
|
|
243
243
|
# the board reads Neon's own direct string — \`DATABASE_URL_UNPOOLED\` first, then
|
|
244
244
|
# \`POSTGRES_URL_NON_POOLING\` — and refuses to boot if neither is there, naming
|
|
@@ -275,7 +275,7 @@ CRON_SECRET=
|
|
|
275
275
|
#
|
|
276
276
|
# BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
|
|
277
277
|
# store. Set it when something has to reach the store from OUTSIDE a Vercel
|
|
278
|
-
# deployment — \`
|
|
278
|
+
# deployment — \`meith backup\` run on your own machine is the case that
|
|
279
279
|
# matters — because there is no OIDC identity there to borrow. Set both and the
|
|
280
280
|
# board prefers the store id, unless the token names a different store, in which
|
|
281
281
|
# case the token wins: naming another store is a deliberate act.
|
|
@@ -435,7 +435,7 @@ export function scaffold(options: ScaffoldOptions): ReadonlyMap<string, string>
|
|
|
435
435
|
dev: `forum-web dev${atRootFlag}`,
|
|
436
436
|
build: `forum-web build${atRootFlag}`,
|
|
437
437
|
start: `forum-web start${atRootFlag}`,
|
|
438
|
-
|
|
438
|
+
meith: 'meith',
|
|
439
439
|
},
|
|
440
440
|
dependencies: {
|
|
441
441
|
'@meith/web': version,
|
|
@@ -509,25 +509,14 @@ export default defineForumConfig({
|
|
|
509
509
|
|
|
510
510
|
files.set(
|
|
511
511
|
'meith.plugins.ts',
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
*
|
|
521
|
-
* import { messages as greeterMessages, plugin as greeterPlugin } from '@meith/plugin-greeter'
|
|
522
|
-
*
|
|
523
|
-
* export const INSTALLED_PLUGINS: readonly InstalledPlugin<PluginDefinition>[] = [
|
|
524
|
-
* { key: 'greeter', enabled: true, plugin: greeterPlugin, messages: greeterMessages },
|
|
525
|
-
* ]
|
|
526
|
-
*
|
|
527
|
-
* and the matching entry in board.plugins.json, which is what
|
|
528
|
-
* \`community plugin:add\`/\`plugin:remove\` read inside the monorepo — kept
|
|
529
|
-
* here too so the two files agree about what is installed.
|
|
530
|
-
*/
|
|
512
|
+
`// Generated from board.plugins.json by \`meith plugin:add\` and \`meith plugin:remove\`.
|
|
513
|
+
//
|
|
514
|
+
// The simple path is those commands, or editing board.plugins.json and running one of
|
|
515
|
+
// them. A plugin that does not fit that convention can be added here by hand instead —
|
|
516
|
+
// keep it out of board.plugins.json so a regenerate does not drop it.
|
|
517
|
+
//
|
|
518
|
+
// docs/customization/plugins.md explains both.
|
|
519
|
+
|
|
531
520
|
import type { InstalledPlugin } from '@meith/web/config'
|
|
532
521
|
|
|
533
522
|
export const INSTALLED_PLUGINS: readonly InstalledPlugin[] = []
|
|
@@ -592,8 +581,8 @@ updates:
|
|
|
592
581
|
# "Custom boards").
|
|
593
582
|
#
|
|
594
583
|
# Two stages, not three: unlike the official image, this does not prune down
|
|
595
|
-
# to Next's own standalone output. The migrate role below runs \`
|
|
596
|
-
# migrate\`, and \`
|
|
584
|
+
# to Next's own standalone output. The migrate role below runs \`meith
|
|
585
|
+
# migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
|
|
597
586
|
# with tsx at the moment it runs (see the meith repository's
|
|
598
587
|
# docs/contributing/development.md, "Consuming the board from a workspace") — it needs
|
|
599
588
|
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
@@ -634,6 +623,23 @@ ENV PORT=3000
|
|
|
634
623
|
ENV HOSTNAME=0.0.0.0
|
|
635
624
|
EXPOSE 3000
|
|
636
625
|
|
|
626
|
+
# Uploaded files — avatars, board images, attachments — land here, and the
|
|
627
|
+
# compose file mounts the persistent "uploads" volume over this path. Creating
|
|
628
|
+
# it in the image, owned by node, is what lets the fresh volume inherit that
|
|
629
|
+
# ownership; UPLOADS_DIR gives the board an absolute path so the working
|
|
630
|
+
# directory never decides where uploads go. Without both, uploads land on the
|
|
631
|
+
# container's own layer and a redeploy discards them.
|
|
632
|
+
ENV UPLOADS_DIR=/app/.uploads
|
|
633
|
+
RUN mkdir -p /app/.uploads && chown node:node /app/.uploads
|
|
634
|
+
|
|
635
|
+
# \`meith <command>\` on PATH runs this board's own operator CLI — the same one
|
|
636
|
+
# node_modules/.bin/meith is — so a Coolify terminal or \`docker compose exec web
|
|
637
|
+
# meith ...\` needs no path. It cd's to /board so the CLI finds this board's
|
|
638
|
+
# config, and overrides any wrapper an inherited base image installed, which
|
|
639
|
+
# would target the board that image was built from, not this one.
|
|
640
|
+
RUN printf '#!/bin/sh\\ncd /board\\nexec node_modules/.bin/meith "$@"\\n' > /usr/local/bin/meith \\
|
|
641
|
+
&& chmod +x /usr/local/bin/meith
|
|
642
|
+
|
|
637
643
|
# node:alpine already carries a non-root "node" user; the board's own files
|
|
638
644
|
# are copied in as root above, so they need handing over before this drops
|
|
639
645
|
# privilege.
|
|
@@ -669,8 +675,8 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
|
669
675
|
# of minutes rather than a cold toolchain build.
|
|
670
676
|
#
|
|
671
677
|
# Two stages, not three: unlike the official image, this does not prune down
|
|
672
|
-
# to Next's own standalone output. The migrate role below runs \`
|
|
673
|
-
# migrate\`, and \`
|
|
678
|
+
# to Next's own standalone output. The migrate role below runs \`meith
|
|
679
|
+
# migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
|
|
674
680
|
# with tsx at the moment it runs (see the meith repository's
|
|
675
681
|
# docs/contributing/development.md, "Consuming the board from a workspace") — it needs
|
|
676
682
|
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
@@ -712,6 +718,23 @@ ENV PORT=3000
|
|
|
712
718
|
ENV HOSTNAME=0.0.0.0
|
|
713
719
|
EXPOSE 3000
|
|
714
720
|
|
|
721
|
+
# Uploaded files — avatars, board images, attachments — land here, and the
|
|
722
|
+
# compose file mounts the persistent "uploads" volume over this path. Creating
|
|
723
|
+
# it in the image, owned by node, is what lets the fresh volume inherit that
|
|
724
|
+
# ownership; UPLOADS_DIR gives the board an absolute path so the working
|
|
725
|
+
# directory never decides where uploads go. Without both, uploads land on the
|
|
726
|
+
# container's own layer and a redeploy discards them.
|
|
727
|
+
ENV UPLOADS_DIR=/app/.uploads
|
|
728
|
+
RUN mkdir -p /app/.uploads && chown node:node /app/.uploads
|
|
729
|
+
|
|
730
|
+
# \`meith <command>\` on PATH runs this board's own operator CLI — the same one
|
|
731
|
+
# node_modules/.bin/meith is — so a Coolify terminal or \`docker compose exec web
|
|
732
|
+
# meith ...\` needs no path. It cd's to /board so the CLI finds this board's
|
|
733
|
+
# config, and overrides any wrapper an inherited base image installed, which
|
|
734
|
+
# would target the board that image was built from, not this one.
|
|
735
|
+
RUN printf '#!/bin/sh\\ncd /board\\nexec node_modules/.bin/meith "$@"\\n' > /usr/local/bin/meith \\
|
|
736
|
+
&& chmod +x /usr/local/bin/meith
|
|
737
|
+
|
|
715
738
|
# node:alpine already carries a non-root "node" user; the board's own files
|
|
716
739
|
# are copied in as root above, so they need handing over before this drops
|
|
717
740
|
# privilege.
|
|
@@ -741,22 +764,22 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
|
741
764
|
set -e
|
|
742
765
|
|
|
743
766
|
# An explicit command wins over the role, the same as the official image —
|
|
744
|
-
# \`docker run
|
|
745
|
-
# the CLI rather than silently starting the web server.
|
|
767
|
+
# \`docker compose run --rm web meith --help\` (or \`exec\` into the running
|
|
768
|
+
# container) should run the CLI rather than silently starting the web server.
|
|
746
769
|
if [ "$#" -gt 0 ]; then
|
|
747
770
|
exec "$@"
|
|
748
771
|
fi
|
|
749
772
|
|
|
750
|
-
case "\${
|
|
773
|
+
case "\${MEITH_ROLE:-web}" in
|
|
751
774
|
migrate)
|
|
752
775
|
# Runs to completion and exits; compose's one-shot service waits on it.
|
|
753
|
-
exec node_modules/.bin/
|
|
776
|
+
exec node_modules/.bin/meith migrate
|
|
754
777
|
;;
|
|
755
778
|
web)
|
|
756
779
|
exec node_modules/.bin/forum-web start
|
|
757
780
|
;;
|
|
758
781
|
*)
|
|
759
|
-
echo "Unknown
|
|
782
|
+
echo "Unknown MEITH_ROLE: \${MEITH_ROLE}. Expected 'web' or 'migrate'." >&2
|
|
760
783
|
exit 1
|
|
761
784
|
;;
|
|
762
785
|
esac
|
|
@@ -771,7 +794,7 @@ esac
|
|
|
771
794
|
# a health probe taken while it runs has no opinion.
|
|
772
795
|
set -e
|
|
773
796
|
|
|
774
|
-
if [ "\${
|
|
797
|
+
if [ "\${MEITH_ROLE:-web}" = "migrate" ]; then
|
|
775
798
|
exit 0
|
|
776
799
|
fi
|
|
777
800
|
|
|
@@ -911,7 +934,7 @@ services:
|
|
|
911
934
|
build: .
|
|
912
935
|
image: ${name}
|
|
913
936
|
environment:
|
|
914
|
-
|
|
937
|
+
MEITH_ROLE: migrate
|
|
915
938
|
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
916
939
|
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
917
940
|
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
@@ -1044,7 +1067,7 @@ services:
|
|
|
1044
1067
|
# runs the old code.
|
|
1045
1068
|
pull_policy: always
|
|
1046
1069
|
environment:
|
|
1047
|
-
|
|
1070
|
+
MEITH_ROLE: migrate
|
|
1048
1071
|
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
1049
1072
|
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
1050
1073
|
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
@@ -1244,7 +1267,7 @@ Two things nothing configures for you, on either path:
|
|
|
1244
1267
|
loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
|
|
1245
1268
|
worker package is not something a board outside the meith monorepo can
|
|
1246
1269
|
depend on yet. Deploy some other way and something still has to call that
|
|
1247
|
-
route (or run \`
|
|
1270
|
+
route (or run \`meith task:run\`) every minute, or nothing catches up
|
|
1248
1271
|
and nothing errors.
|
|
1249
1272
|
|
|
1250
1273
|
## Local
|
|
@@ -1262,8 +1285,8 @@ Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
|
|
|
1262
1285
|
\`DATABASE_URL\` and the two secrets in it, then:
|
|
1263
1286
|
|
|
1264
1287
|
\`\`\`sh
|
|
1265
|
-
npm run
|
|
1266
|
-
echo "<password>" | npm run
|
|
1288
|
+
npm run meith -- migrate
|
|
1289
|
+
echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
|
|
1267
1290
|
\`\`\`
|
|
1268
1291
|
|
|
1269
1292
|
## Configuring
|
|
@@ -1274,9 +1297,43 @@ echo "<password>" | npm run community -- user:create --username <name> --email <
|
|
|
1274
1297
|
- **\`/admin\`** — settings, forums, groups, members, themes, maintenance. An
|
|
1275
1298
|
administrator re-enters their password to get in, and again for anything
|
|
1276
1299
|
destructive.
|
|
1277
|
-
- **\`npm run
|
|
1300
|
+
- **\`npm run meith -- --help\`** — the operator CLI. Everything the panel does
|
|
1278
1301
|
and a few things it cannot, without a browser.
|
|
1279
1302
|
|
|
1303
|
+
## Installing plugins and themes
|
|
1304
|
+
|
|
1305
|
+
Nothing installs into a running container — a plugin or theme has to be
|
|
1306
|
+
built into the image. In this repository:
|
|
1307
|
+
|
|
1308
|
+
1. **Add it.** A **plugin** is one command, which installs the package and
|
|
1309
|
+
registers it:
|
|
1310
|
+
|
|
1311
|
+
\`\`\`sh
|
|
1312
|
+
npm run meith -- plugin:add @meith/plugin-dues
|
|
1313
|
+
\`\`\`
|
|
1314
|
+
|
|
1315
|
+
It writes \`board.plugins.json\` and regenerates \`meith.plugins.ts\` for you
|
|
1316
|
+
(\`npm run meith -- plugin:remove <key>\` reverses it). A **theme** is
|
|
1317
|
+
\`npm install --save-exact @meith/theme-midnight\`, then an entry in
|
|
1318
|
+
\`meith.config.ts\`'s \`themes\` map following the shape of the \`default\` one
|
|
1319
|
+
already there — set \`defaultTheme\` to its key to make it the board's
|
|
1320
|
+
default.
|
|
1321
|
+
|
|
1322
|
+
2. **Commit and push**, then **Redeploy** from Coolify — pushing alone does
|
|
1323
|
+
not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
|
|
1324
|
+
waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
|
|
1325
|
+
what actually pulls the result.
|
|
1326
|
+
|
|
1327
|
+
3. **If it ships database changes, apply them once it is up** — from
|
|
1328
|
+
**Admin → System** (**Version & migrations**) in the browser, or:
|
|
1329
|
+
|
|
1330
|
+
\`\`\`sh
|
|
1331
|
+
docker compose run --rm web meith upgrade
|
|
1332
|
+
\`\`\`
|
|
1333
|
+
|
|
1334
|
+
See [Installing plugins and themes](${repositoryUrl}/blob/main/docs/customization/installing.md)
|
|
1335
|
+
for the full guide.
|
|
1336
|
+
|
|
1280
1337
|
## Upgrading
|
|
1281
1338
|
|
|
1282
1339
|
\`\`\`sh
|
|
@@ -1317,7 +1374,7 @@ project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
|
|
|
1317
1374
|
\`npm install\` of anything else here — a plugin, say — stays pinned too; the
|
|
1318
1375
|
build workflow also refuses to build from anything but an exact version, as
|
|
1319
1376
|
a second line of defense. Once the rebuilt image is deployed, run
|
|
1320
|
-
\`npm run
|
|
1377
|
+
\`npm run meith -- upgrade\` against it for the plugin migrations — see
|
|
1321
1378
|
[the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
|
|
1322
1379
|
for running it against this deployment.
|
|
1323
1380
|
|
|
@@ -1577,9 +1634,9 @@ A board must stay movable, and the Blob store is the one part of this shape that
|
|
|
1577
1634
|
is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
|
|
1578
1635
|
that any host accepts, but a Vercel Blob store is reachable only through Vercel's
|
|
1579
1636
|
own API and there is no bucket to sync out of it. **The uploads are the thing you
|
|
1580
|
-
have to carry out deliberately, and \`
|
|
1637
|
+
have to carry out deliberately, and \`meith backup\` is how.**
|
|
1581
1638
|
|
|
1582
|
-
Under \`FILESTORE_DRIVER=blob\`, \`
|
|
1639
|
+
Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
|
|
1583
1640
|
default** — it walks the Blob store, pulls every object, and puts them in the
|
|
1584
1641
|
bundle beside the database dump. This is the opposite of the \`s3\` default, which
|
|
1585
1642
|
skips them, because a bucket has its own backup story you can drive yourself and
|
|
@@ -1590,7 +1647,7 @@ DATABASE_URL=… # Neon's pooled string
|
|
|
1590
1647
|
DIRECT_DATABASE_URL=… # Neon's DATABASE_URL_UNPOOLED
|
|
1591
1648
|
FILESTORE_DRIVER=blob
|
|
1592
1649
|
BLOB_READ_WRITE_TOKEN=… # create one on the store; see below
|
|
1593
|
-
npm run
|
|
1650
|
+
npm run meith -- backup
|
|
1594
1651
|
\`\`\`
|
|
1595
1652
|
|
|
1596
1653
|
Run that from a checkout of this repository, with those four values in the
|
|
@@ -1611,10 +1668,10 @@ so the same bundle moves the board either onward or away:
|
|
|
1611
1668
|
|
|
1612
1669
|
\`\`\`sh
|
|
1613
1670
|
# onto a self-hosted board with a bucket
|
|
1614
|
-
FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run
|
|
1671
|
+
FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz
|
|
1615
1672
|
|
|
1616
1673
|
# onto a board that keeps uploads on its own disk
|
|
1617
|
-
RESTORE_DATABASE_URL=… npm run
|
|
1674
|
+
RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
|
|
1618
1675
|
\`\`\`
|
|
1619
1676
|
|
|
1620
1677
|
Take one before you need it. A Blob store deleted with the Vercel project takes
|