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 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 = `community migrate && forum-web build ${AT_ROOT_FLAG}`;
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
- # \`community migrate\` and \`community backup\`. Migrations hold a session-level
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 \`community migrate\` against it, and
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 \`community backup\` run on your own machine is the case that
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
- community: "community"
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
- * The board's installed-plugin list.
478
- *
479
- * Inside the Meith monorepo this file is generated from board.plugins.json
480
- * by \`pnpm board:gen\` (see docs/customization/plugins.md) \u2014 that generator is
481
- * repository tooling, not something this workspace carries, so this file
482
- * starts as a plain, valid file with the same shape instead. Add a plugin by
483
- * importing its \`plugin\`/\`messages\` exports and adding an entry:
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 \`community
556
- # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
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 \`community
632
- # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
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 <image> node_modules/.bin/community --help\` should still 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 "\${COMMUNITY_ROLE:-web}" in
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/community migrate
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 COMMUNITY_ROLE: \${COMMUNITY_ROLE}. Expected 'web' or 'migrate'." >&2
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 [ "\${COMMUNITY_ROLE:-web}" = "migrate" ]; then
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
- COMMUNITY_ROLE: migrate
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
- COMMUNITY_ROLE: migrate
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 \`community task:run\`) every minute, or nothing catches up
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 community -- migrate
1218
- echo "<password>" | npm run community -- user:create --username <name> --email <address> --group administrators
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 community -- --help\`** \u2014 the operator CLI. Everything the panel does
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 community -- upgrade\` against it for the plugin migrations \u2014 see
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 \`community backup\` is how.**
1572
+ have to carry out deliberately, and \`meith backup\` is how.**
1516
1573
 
1517
- Under \`FILESTORE_DRIVER=blob\`, \`community backup\` includes the uploads **by
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 community -- backup
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 community -- restore bundle.tar.gz
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 community -- restore bundle.tar.gz --uploads-dir ./uploads
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 community migrate\`. The plugin then appears under
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
- "`community task:run`. Without it nothing catches up, and nothing errors."
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.26.1");
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.26.1",
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.26.1')
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
- '`community task:run`. Without it nothing catches up, and nothing errors.',
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 community migrate\`. The plugin then appears under
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 = `community migrate && forum-web build ${AT_ROOT_FLAG}`
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
- # \`community migrate\` and \`community backup\`. Migrations hold a session-level
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 \`community migrate\` against it, and
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 — \`community backup\` run on your own machine is the case that
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
- community: 'community',
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
- * The board's installed-plugin list.
514
- *
515
- * Inside the Meith monorepo this file is generated from board.plugins.json
516
- * by \`pnpm board:gen\` (see docs/customization/plugins.md) that generator is
517
- * repository tooling, not something this workspace carries, so this file
518
- * starts as a plain, valid file with the same shape instead. Add a plugin by
519
- * importing its \`plugin\`/\`messages\` exports and adding an entry:
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 \`community
596
- # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
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 \`community
673
- # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
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 <image> node_modules/.bin/community --help\` should still 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 "\${COMMUNITY_ROLE:-web}" in
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/community migrate
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 COMMUNITY_ROLE: \${COMMUNITY_ROLE}. Expected 'web' or 'migrate'." >&2
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 [ "\${COMMUNITY_ROLE:-web}" = "migrate" ]; then
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
- COMMUNITY_ROLE: migrate
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
- COMMUNITY_ROLE: migrate
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 \`community task:run\`) every minute, or nothing catches up
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 community -- migrate
1266
- echo "<password>" | npm run community -- user:create --username <name> --email <address> --group administrators
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 community -- --help\`** — the operator CLI. Everything the panel does
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 community -- upgrade\` against it for the plugin migrations — see
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 \`community backup\` is how.**
1637
+ have to carry out deliberately, and \`meith backup\` is how.**
1581
1638
 
1582
- Under \`FILESTORE_DRIVER=blob\`, \`community backup\` includes the uploads **by
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 community -- backup
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 community -- restore bundle.tar.gz
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 community -- restore bundle.tar.gz --uploads-dir ./uploads
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