create-meith 0.16.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/LICENSE.md +165 -0
- package/package.json +24 -0
- package/src/bin.ts +9 -0
- package/src/cli.ts +76 -0
- package/src/index.ts +8 -0
- package/src/scaffold.ts +613 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
This version of the GNU Lesser General Public License incorporates
|
|
10
|
+
the terms and conditions of version 3 of the GNU General Public
|
|
11
|
+
License, supplemented by the additional permissions listed below.
|
|
12
|
+
|
|
13
|
+
0. Additional Definitions.
|
|
14
|
+
|
|
15
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
16
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
|
17
|
+
General Public License.
|
|
18
|
+
|
|
19
|
+
"The Library" refers to a covered work governed by this License,
|
|
20
|
+
other than an Application or a Combined Work as defined below.
|
|
21
|
+
|
|
22
|
+
An "Application" is any work that makes use of an interface provided
|
|
23
|
+
by the Library, but which is not otherwise based on the Library.
|
|
24
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
|
25
|
+
of using an interface provided by the Library.
|
|
26
|
+
|
|
27
|
+
A "Combined Work" is a work produced by combining or linking an
|
|
28
|
+
Application with the Library. The particular version of the Library
|
|
29
|
+
with which the Combined Work was made is also called the "Linked
|
|
30
|
+
Version".
|
|
31
|
+
|
|
32
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
|
33
|
+
Corresponding Source for the Combined Work, excluding any source code
|
|
34
|
+
for portions of the Combined Work that, considered in isolation, are
|
|
35
|
+
based on the Application, and not on the Linked Version.
|
|
36
|
+
|
|
37
|
+
The "Corresponding Application Code" for a Combined Work means the
|
|
38
|
+
object code and/or source code for the Application, including any data
|
|
39
|
+
and utility programs needed for reproducing the Combined Work from the
|
|
40
|
+
Application, but excluding the System Libraries of the Combined Work.
|
|
41
|
+
|
|
42
|
+
1. Exception to Section 3 of the GNU GPL.
|
|
43
|
+
|
|
44
|
+
You may convey a covered work under sections 3 and 4 of this License
|
|
45
|
+
without being bound by section 3 of the GNU GPL.
|
|
46
|
+
|
|
47
|
+
2. Conveying Modified Versions.
|
|
48
|
+
|
|
49
|
+
If you modify a copy of the Library, and, in your modifications, a
|
|
50
|
+
facility refers to a function or data to be supplied by an Application
|
|
51
|
+
that uses the facility (other than as an argument passed when the
|
|
52
|
+
facility is invoked), then you may convey a copy of the modified
|
|
53
|
+
version:
|
|
54
|
+
|
|
55
|
+
a) under this License, provided that you make a good faith effort to
|
|
56
|
+
ensure that, in the event an Application does not supply the
|
|
57
|
+
function or data, the facility still operates, and performs
|
|
58
|
+
whatever part of its purpose remains meaningful, or
|
|
59
|
+
|
|
60
|
+
b) under the GNU GPL, with none of the additional permissions of
|
|
61
|
+
this License applicable to that copy.
|
|
62
|
+
|
|
63
|
+
3. Object Code Incorporating Material from Library Header Files.
|
|
64
|
+
|
|
65
|
+
The object code form of an Application may incorporate material from
|
|
66
|
+
a header file that is part of the Library. You may convey such object
|
|
67
|
+
code under terms of your choice, provided that, if the incorporated
|
|
68
|
+
material is not limited to numerical parameters, data structure
|
|
69
|
+
layouts and accessors, or small macros, inline functions and templates
|
|
70
|
+
(ten or fewer lines in length), you do both of the following:
|
|
71
|
+
|
|
72
|
+
a) Give prominent notice with each copy of the object code that the
|
|
73
|
+
Library is used in it and that the Library and its use are
|
|
74
|
+
covered by this License.
|
|
75
|
+
|
|
76
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
|
77
|
+
document.
|
|
78
|
+
|
|
79
|
+
4. Combined Works.
|
|
80
|
+
|
|
81
|
+
You may convey a Combined Work under terms of your choice that,
|
|
82
|
+
taken together, effectively do not restrict modification of the
|
|
83
|
+
portions of the Library contained in the Combined Work and reverse
|
|
84
|
+
engineering for debugging such modifications, if you also do each of
|
|
85
|
+
the following:
|
|
86
|
+
|
|
87
|
+
a) Give prominent notice with each copy of the Combined Work that
|
|
88
|
+
the Library is used in it and that the Library and its use are
|
|
89
|
+
covered by this License.
|
|
90
|
+
|
|
91
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
|
92
|
+
document.
|
|
93
|
+
|
|
94
|
+
c) For a Combined Work that displays copyright notices during
|
|
95
|
+
execution, include the copyright notice for the Library among
|
|
96
|
+
these notices, as well as a reference directing the user to the
|
|
97
|
+
copies of the GNU GPL and this license document.
|
|
98
|
+
|
|
99
|
+
d) Do one of the following:
|
|
100
|
+
|
|
101
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
|
102
|
+
License, and the Corresponding Application Code in a form
|
|
103
|
+
suitable for, and under terms that permit, the user to
|
|
104
|
+
recombine or relink the Application with a modified version of
|
|
105
|
+
the Linked Version to produce a modified Combined Work, in the
|
|
106
|
+
manner specified by section 6 of the GNU GPL for conveying
|
|
107
|
+
Corresponding Source.
|
|
108
|
+
|
|
109
|
+
1) Use a suitable shared library mechanism for linking with the
|
|
110
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
|
111
|
+
a copy of the Library already present on the user's computer
|
|
112
|
+
system, and (b) will operate properly with a modified version
|
|
113
|
+
of the Library that is interface-compatible with the Linked
|
|
114
|
+
Version.
|
|
115
|
+
|
|
116
|
+
e) Provide Installation Information, but only if you would otherwise
|
|
117
|
+
be required to provide such information under section 6 of the
|
|
118
|
+
GNU GPL, and only to the extent that such information is
|
|
119
|
+
necessary to install and execute a modified version of the
|
|
120
|
+
Combined Work produced by recombining or relinking the
|
|
121
|
+
Application with a modified version of the Linked Version. (If
|
|
122
|
+
you use option 4d0, the Installation Information must accompany
|
|
123
|
+
the Minimal Corresponding Source and Corresponding Application
|
|
124
|
+
Code. If you use option 4d1, you must provide the Installation
|
|
125
|
+
Information in the manner specified by section 6 of the GNU GPL
|
|
126
|
+
for conveying Corresponding Source.)
|
|
127
|
+
|
|
128
|
+
5. Combined Libraries.
|
|
129
|
+
|
|
130
|
+
You may place library facilities that are a work based on the
|
|
131
|
+
Library side by side in a single library together with other library
|
|
132
|
+
facilities that are not Applications and are not covered by this
|
|
133
|
+
License, and convey such a combined library under terms of your
|
|
134
|
+
choice, if you do both of the following:
|
|
135
|
+
|
|
136
|
+
a) Accompany the combined library with a copy of the same work based
|
|
137
|
+
on the Library, uncombined with any other library facilities,
|
|
138
|
+
conveyed under the terms of this License.
|
|
139
|
+
|
|
140
|
+
b) Give prominent notice with the combined library that part of it
|
|
141
|
+
is a work based on the Library, and explaining where to find the
|
|
142
|
+
accompanying uncombined form of the same work.
|
|
143
|
+
|
|
144
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
|
145
|
+
|
|
146
|
+
The Free Software Foundation may publish revised and/or new versions
|
|
147
|
+
of the GNU Lesser General Public License from time to time. Such new
|
|
148
|
+
versions will be similar in spirit to the present version, but may
|
|
149
|
+
differ in detail to address new problems or concerns.
|
|
150
|
+
|
|
151
|
+
Each version is given a distinguishing version number. If the
|
|
152
|
+
Library as you received it specifies that a certain numbered version
|
|
153
|
+
of the GNU Lesser General Public License "or any later version"
|
|
154
|
+
applies to it, you have the option of following the terms and
|
|
155
|
+
conditions either of that published version or of any later version
|
|
156
|
+
published by the Free Software Foundation. If the Library as you
|
|
157
|
+
received it does not specify a version number of the GNU Lesser
|
|
158
|
+
General Public License, you may choose any version of the GNU Lesser
|
|
159
|
+
General Public License ever published by the Free Software Foundation.
|
|
160
|
+
|
|
161
|
+
If the Library as you received it specifies that a proxy can decide
|
|
162
|
+
whether future versions of the GNU Lesser General Public License shall
|
|
163
|
+
apply, that proxy's public statement of acceptance of any version is
|
|
164
|
+
permanent authorization for you to choose that version for the
|
|
165
|
+
Library.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-meith",
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "Scaffold a Meith board — npx create-meith <name> writes a workspace that depends on @meith/web and @meith/cli.",
|
|
5
|
+
"license": "LGPL-3.0-or-later",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/meith-dev/meith.git",
|
|
9
|
+
"directory": "packages/create-meith"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"create-meith": "./src/bin.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"!src/**/*.test.*"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/bin.ts
ADDED
package/src/cli.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_REPOSITORY_URL, nextSteps, scaffold, validateName } from './scaffold'
|
|
5
|
+
|
|
6
|
+
export interface CliResult {
|
|
7
|
+
readonly code: number
|
|
8
|
+
readonly lines: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function isSafeTarget(target: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
return (await readdir(target)).length === 0
|
|
14
|
+
} catch {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function run(argv: readonly string[], version: string): Promise<CliResult> {
|
|
20
|
+
const positional = argv.filter((arg) => !arg.startsWith('-'))
|
|
21
|
+
const name = positional[0] ?? ''
|
|
22
|
+
|
|
23
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
24
|
+
return {
|
|
25
|
+
code: 0,
|
|
26
|
+
lines: [
|
|
27
|
+
'create-meith — scaffold a forum project.',
|
|
28
|
+
'',
|
|
29
|
+
' npx create-meith <name> [--repo <url>]',
|
|
30
|
+
'',
|
|
31
|
+
'Writes package.json, community.config.ts, .env.example, .gitignore and',
|
|
32
|
+
'README.md into ./<name>, then tells you what to run.',
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const invalid = validateName(name)
|
|
38
|
+
if (invalid !== null) {
|
|
39
|
+
return { code: 1, lines: [`create-meith: ${invalid}`, '', 'Usage: npx create-meith <name>'] }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const repoIndex = argv.indexOf('--repo')
|
|
43
|
+
const repositoryUrl =
|
|
44
|
+
repoIndex === -1 ? DEFAULT_REPOSITORY_URL : (argv[repoIndex + 1] ?? DEFAULT_REPOSITORY_URL)
|
|
45
|
+
|
|
46
|
+
const target = resolve(process.cwd(), name)
|
|
47
|
+
if (!(await isSafeTarget(target))) {
|
|
48
|
+
return {
|
|
49
|
+
code: 1,
|
|
50
|
+
lines: [
|
|
51
|
+
`create-meith: ${name} already exists and is not empty.`,
|
|
52
|
+
'Refusing to write into it — pick another name, or empty it first.',
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = scaffold({ name, version, repositoryUrl })
|
|
58
|
+
for (const [relative, contents] of files) {
|
|
59
|
+
const path = join(target, relative)
|
|
60
|
+
await mkdir(dirname(path), { recursive: true })
|
|
61
|
+
await writeFile(path, contents, 'utf8')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
code: 0,
|
|
66
|
+
lines: [
|
|
67
|
+
`Created ${name} — ${files.size} files.`,
|
|
68
|
+
'',
|
|
69
|
+
...nextSteps(name).map((step) => ` ${step}`),
|
|
70
|
+
'',
|
|
71
|
+
'Then set DATABASE_URL, AUTH_SECRET and TICK_SECRET and deploy.',
|
|
72
|
+
'Something must run the tick every minute — the worker process, or',
|
|
73
|
+
'`community task:run`. Without it nothing catches up, and nothing errors.',
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/index.ts
ADDED
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
export interface ScaffoldOptions {
|
|
2
|
+
readonly name: string
|
|
3
|
+
readonly version: string
|
|
4
|
+
readonly repositoryUrl: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_REPOSITORY_URL = 'https://github.com/meith-dev/meith'
|
|
8
|
+
|
|
9
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/
|
|
10
|
+
|
|
11
|
+
export function validateName(name: string): string | null {
|
|
12
|
+
if (name === '') return 'A project name is required.'
|
|
13
|
+
if (name === '.' || name === '..') return 'That name would write outside the new directory.'
|
|
14
|
+
if (name.includes('/') || name.includes('\\'))
|
|
15
|
+
return 'A project name cannot contain a path separator.'
|
|
16
|
+
if (name !== name.toLowerCase()) return 'npm package names must be lower-case.'
|
|
17
|
+
if (!NAME_PATTERN.test(name)) {
|
|
18
|
+
return 'Use lower-case letters, digits, dots, hyphens and underscores, starting with a letter or digit.'
|
|
19
|
+
}
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function scaffold(options: ScaffoldOptions): ReadonlyMap<string, string> {
|
|
24
|
+
const { name, version, repositoryUrl } = options
|
|
25
|
+
const files = new Map<string, string>()
|
|
26
|
+
|
|
27
|
+
files.set(
|
|
28
|
+
'package.json',
|
|
29
|
+
`${JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
name,
|
|
32
|
+
version: '0.1.0',
|
|
33
|
+
private: true,
|
|
34
|
+
type: 'module',
|
|
35
|
+
scripts: {
|
|
36
|
+
dev: 'forum-web dev',
|
|
37
|
+
build: 'forum-web build',
|
|
38
|
+
start: 'forum-web start',
|
|
39
|
+
community: 'community',
|
|
40
|
+
},
|
|
41
|
+
dependencies: {
|
|
42
|
+
'@meith/web': version,
|
|
43
|
+
'@meith/cli': version,
|
|
44
|
+
'@meith/theme-default': version,
|
|
45
|
+
},
|
|
46
|
+
engines: { node: '>=22' },
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2,
|
|
50
|
+
)}\n`,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
files.set(
|
|
54
|
+
'community.config.ts',
|
|
55
|
+
`/**
|
|
56
|
+
* The board's build-time registry.
|
|
57
|
+
*
|
|
58
|
+
* Everything installable is named here, statically, so the bundler can see it
|
|
59
|
+
* and the compiler can check it. Nothing is discovered by scanning a directory
|
|
60
|
+
* at runtime — a production build contains only what the bundler could see, so a
|
|
61
|
+
* directory walked at request time is empty and a plugin "installed" that way is
|
|
62
|
+
* not there at all.
|
|
63
|
+
*
|
|
64
|
+
* Adding a theme is: \`npm install\` it, add a line here, redeploy. Adding a
|
|
65
|
+
* plugin is the same, through board.plugins.json and community.plugins.ts —
|
|
66
|
+
* see docs/plugin-api.md.
|
|
67
|
+
*/
|
|
68
|
+
import { defineForumConfig } from '@meith/web/config'
|
|
69
|
+
import {
|
|
70
|
+
BROWSER_THEME_COLOR,
|
|
71
|
+
DARK_TOKENS,
|
|
72
|
+
defaultTheme,
|
|
73
|
+
LIGHT_TOKENS,
|
|
74
|
+
} from '@meith/theme-default'
|
|
75
|
+
|
|
76
|
+
import { INSTALLED_PLUGINS } from './community.plugins'
|
|
77
|
+
|
|
78
|
+
export default defineForumConfig({
|
|
79
|
+
themes: {
|
|
80
|
+
default: {
|
|
81
|
+
key: 'default',
|
|
82
|
+
title: 'Default',
|
|
83
|
+
tokens: { light: LIGHT_TOKENS, dark: DARK_TOKENS },
|
|
84
|
+
browserThemeColor: BROWSER_THEME_COLOR,
|
|
85
|
+
theme: defaultTheme,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
defaultTheme: 'default',
|
|
89
|
+
|
|
90
|
+
plugins: INSTALLED_PLUGINS,
|
|
91
|
+
})
|
|
92
|
+
`,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
files.set('board.plugins.json', `${JSON.stringify({ plugins: [] }, null, 2)}\n`)
|
|
96
|
+
|
|
97
|
+
files.set(
|
|
98
|
+
'community.plugins.ts',
|
|
99
|
+
`/**
|
|
100
|
+
* The board's installed-plugin list.
|
|
101
|
+
*
|
|
102
|
+
* Inside the Meith monorepo this file is generated from board.plugins.json
|
|
103
|
+
* by \`pnpm board:gen\` (see docs/plugin-api.md) — that generator is
|
|
104
|
+
* repository tooling, not something this workspace carries, so this file
|
|
105
|
+
* starts as a plain, valid file with the same shape instead. Add a plugin by
|
|
106
|
+
* importing its \`plugin\`/\`messages\` exports and adding an entry:
|
|
107
|
+
*
|
|
108
|
+
* import { messages as greeterMessages, plugin as greeterPlugin } from '@meith/plugin-greeter'
|
|
109
|
+
*
|
|
110
|
+
* export const INSTALLED_PLUGINS: readonly InstalledPlugin<PluginDefinition>[] = [
|
|
111
|
+
* { key: 'greeter', enabled: true, plugin: greeterPlugin, messages: greeterMessages },
|
|
112
|
+
* ]
|
|
113
|
+
*
|
|
114
|
+
* and the matching entry in board.plugins.json, which is what
|
|
115
|
+
* \`community plugin:add\`/\`plugin:remove\` read inside the monorepo — kept
|
|
116
|
+
* here too so the two files agree about what is installed.
|
|
117
|
+
*/
|
|
118
|
+
import type { InstalledPlugin } from '@meith/web/config'
|
|
119
|
+
|
|
120
|
+
export const INSTALLED_PLUGINS: readonly InstalledPlugin[] = []
|
|
121
|
+
|
|
122
|
+
export function installedPluginDefinitions() {
|
|
123
|
+
return INSTALLED_PLUGINS.filter(
|
|
124
|
+
(entry) => entry.enabled !== false && entry.plugin !== undefined,
|
|
125
|
+
).map((entry) => entry.plugin)
|
|
126
|
+
}
|
|
127
|
+
`,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
files.set(
|
|
131
|
+
'.env.example',
|
|
132
|
+
`# ${name} — environment.
|
|
133
|
+
#
|
|
134
|
+
# Copy to .env.local for development. On the server this is \`.env\` beside the
|
|
135
|
+
# compose file; nothing here belongs in git.
|
|
136
|
+
|
|
137
|
+
# ─── Required ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
# Your Postgres connection string.
|
|
140
|
+
#
|
|
141
|
+
# If it is a managed database that offers a TRANSACTION-MODE POOLER string, use
|
|
142
|
+
# that rather than the direct one — Neon, Supabase and their kind hand out both,
|
|
143
|
+
# and on the direct string a board works in testing and starts refusing
|
|
144
|
+
# connections under the first real traffic, with an error that names the
|
|
145
|
+
# database rather than the cause. Your own Postgres, with a fixed number of
|
|
146
|
+
# processes in front of it, does not need one.
|
|
147
|
+
DATABASE_URL=
|
|
148
|
+
|
|
149
|
+
# Session and token signing. No default, deliberately: a shipped default is a
|
|
150
|
+
# board every reader of the source can sign a session for.
|
|
151
|
+
#
|
|
152
|
+
# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
|
153
|
+
AUTH_SECRET=
|
|
154
|
+
|
|
155
|
+
# The shared secret the tick caller presents to GET /api/system/tick. Generate
|
|
156
|
+
# it the same way. Without it the tick is unauthenticated, and the tick is how
|
|
157
|
+
# bans expire and digests send.
|
|
158
|
+
TICK_SECRET=
|
|
159
|
+
|
|
160
|
+
# ─── Optional ────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
# fixture = deterministic in-memory sample data, no database needed. This is
|
|
163
|
+
# what \`npm run build\` uses, and what a checkout with no database falls back to.
|
|
164
|
+
DATA_SOURCE=postgres
|
|
165
|
+
|
|
166
|
+
# Absolute, no trailing slash. Used in mail, feeds and canonical URLs — every
|
|
167
|
+
# place a relative URL cannot work because there is no request to be relative to.
|
|
168
|
+
#
|
|
169
|
+
# Optional: leave it blank and the installer asks, prefilled from the address you
|
|
170
|
+
# load /install at, and stores the answer on the board where the settings screen
|
|
171
|
+
# can change it without a redeploy. Set it here and it wins outright.
|
|
172
|
+
APP_URL=
|
|
173
|
+
|
|
174
|
+
# Mail. Leave these alone and the installer asks for mail on first run, storing
|
|
175
|
+
# it on the board — a settings screen with a test button, no redeploy. Set
|
|
176
|
+
# MAIL_DRIVER here instead and the environment wins outright, which is what you
|
|
177
|
+
# want if the credential must not live in the database.
|
|
178
|
+
#
|
|
179
|
+
# The default sends NOTHING: each message goes to the server log, so password
|
|
180
|
+
# reset fails silently until mail is configured one way or the other.
|
|
181
|
+
# MAIL_DRIVER=smtp
|
|
182
|
+
# MAIL_SMTP_HOST=smtp.example.com
|
|
183
|
+
# MAIL_SMTP_PORT=465
|
|
184
|
+
# MAIL_SMTP_SECURITY=tls # tls (465) | starttls (587) | none
|
|
185
|
+
# MAIL_SMTP_USERNAME=
|
|
186
|
+
# MAIL_SMTP_PASSWORD=
|
|
187
|
+
# MAIL_FROM=noreply@yourdomain.com
|
|
188
|
+
|
|
189
|
+
`,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
files.set(
|
|
193
|
+
'.gitignore',
|
|
194
|
+
`node_modules
|
|
195
|
+
.next
|
|
196
|
+
.meith
|
|
197
|
+
.env
|
|
198
|
+
.env.local
|
|
199
|
+
.env*.local
|
|
200
|
+
*.log
|
|
201
|
+
.DS_Store
|
|
202
|
+
`,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
files.set(
|
|
206
|
+
'Dockerfile',
|
|
207
|
+
`# syntax=docker/dockerfile:1.7-labs
|
|
208
|
+
# ${name}'s deploy image.
|
|
209
|
+
#
|
|
210
|
+
# FROM the published framework base image — deps + framework layers only,
|
|
211
|
+
# locked to this exact release (see the meith repository's
|
|
212
|
+
# docs/self-hosting.md, "Custom boards", and docker/Dockerfile.base for what
|
|
213
|
+
# it is and is not). This board's own Dockerfile only ever installs its own
|
|
214
|
+
# delta on top of it — a new plugin's own dependency, typically nothing more
|
|
215
|
+
# — which is what keeps a rebuild after \`npm install some-plugin\` a matter
|
|
216
|
+
# of minutes rather than a cold toolchain build.
|
|
217
|
+
#
|
|
218
|
+
# Two stages, not three: unlike the official image, this does not prune down
|
|
219
|
+
# to Next's own standalone output. The migrate role below runs \`community
|
|
220
|
+
# migrate\`, and \`community\` materializes @meith/cli's sources and runs them
|
|
221
|
+
# with tsx at the moment it runs (see the meith repository's
|
|
222
|
+
# docs/development.md, "Consuming the board from a workspace") — it needs
|
|
223
|
+
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
224
|
+
# traced as reachable from the web server alone. The tick itself is driven
|
|
225
|
+
# by compose.yml's own \`worker\` service — a lightweight loop against
|
|
226
|
+
# /api/system/tick, not a compiled worker process, because @meith/worker is
|
|
227
|
+
# not published (see the meith repository's docs/release.md).
|
|
228
|
+
FROM ghcr.io/meith-dev/meith-base:${version} AS deps
|
|
229
|
+
WORKDIR /board
|
|
230
|
+
|
|
231
|
+
# This board's own manifest, cached independently of its source — editing
|
|
232
|
+
# community.config.ts should not re-run npm install. The base image above
|
|
233
|
+
# already carries node_modules for @meith/web, @meith/cli and
|
|
234
|
+
# @meith/theme-default at this exact version, so installing this file on top
|
|
235
|
+
# of it only fetches what changed: a plugin newly added to \`dependencies\`,
|
|
236
|
+
# typically nothing at all.
|
|
237
|
+
COPY package.json ./
|
|
238
|
+
RUN npm install
|
|
239
|
+
|
|
240
|
+
FROM deps AS runtime
|
|
241
|
+
WORKDIR /board
|
|
242
|
+
COPY . .
|
|
243
|
+
|
|
244
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
245
|
+
ENV NODE_ENV=production
|
|
246
|
+
|
|
247
|
+
# DATA_SOURCE is scoped to this one RUN, not declared with ENV — an ENV
|
|
248
|
+
# persists into every container started from this image afterward, and this
|
|
249
|
+
# Dockerfile has no later stage to reset it in (see "Two stages, not three"
|
|
250
|
+
# above). The build needs neither a database nor a production secret (see
|
|
251
|
+
# the meith repository's docs/development.md, "Fixture mode"), but baking
|
|
252
|
+
# DATA_SOURCE=fixture into the image itself would silently force fixture
|
|
253
|
+
# mode — and with it the in-memory queue driver — at runtime too, no matter
|
|
254
|
+
# what DATABASE_URL an operator supplies to \`docker run\`.
|
|
255
|
+
RUN DATA_SOURCE=fixture npx forum-web build
|
|
256
|
+
|
|
257
|
+
ENV PORT=3000
|
|
258
|
+
ENV HOSTNAME=0.0.0.0
|
|
259
|
+
EXPOSE 3000
|
|
260
|
+
|
|
261
|
+
# node:alpine already carries a non-root "node" user; the board's own files
|
|
262
|
+
# are copied in as root above, so they need handing over before this drops
|
|
263
|
+
# privilege.
|
|
264
|
+
RUN chown -R node:node /board
|
|
265
|
+
USER node
|
|
266
|
+
|
|
267
|
+
COPY --chown=node:node docker-entrypoint.sh docker-healthcheck.sh ./
|
|
268
|
+
RUN chmod +x docker-entrypoint.sh docker-healthcheck.sh
|
|
269
|
+
|
|
270
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
|
|
271
|
+
CMD ["./docker-healthcheck.sh"]
|
|
272
|
+
|
|
273
|
+
ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
274
|
+
`,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
files.set(
|
|
278
|
+
'docker-entrypoint.sh',
|
|
279
|
+
`#!/bin/sh
|
|
280
|
+
# One image, two roles — see Dockerfile and README.md.
|
|
281
|
+
#
|
|
282
|
+
# "web" (the default) runs the board; "migrate" applies the schema and
|
|
283
|
+
# exits. There is no "worker" role in this image: @meith/worker is not
|
|
284
|
+
# published, so nothing here can run it — compose.yml's own \`worker\`
|
|
285
|
+
# service drives the tick a different way, calling this image's web role
|
|
286
|
+
# over HTTP instead of running as a role of this image.
|
|
287
|
+
set -e
|
|
288
|
+
|
|
289
|
+
# An explicit command wins over the role, the same as the official image —
|
|
290
|
+
# \`docker run <image> node_modules/.bin/community --help\` should still run
|
|
291
|
+
# the CLI rather than silently starting the web server.
|
|
292
|
+
if [ "$#" -gt 0 ]; then
|
|
293
|
+
exec "$@"
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
case "\${COMMUNITY_ROLE:-web}" in
|
|
297
|
+
migrate)
|
|
298
|
+
# Runs to completion and exits; compose's one-shot service waits on it.
|
|
299
|
+
exec node_modules/.bin/community migrate
|
|
300
|
+
;;
|
|
301
|
+
web)
|
|
302
|
+
exec node_modules/.bin/forum-web start
|
|
303
|
+
;;
|
|
304
|
+
*)
|
|
305
|
+
echo "Unknown COMMUNITY_ROLE: \${COMMUNITY_ROLE}. Expected 'web' or 'migrate'." >&2
|
|
306
|
+
exit 1
|
|
307
|
+
;;
|
|
308
|
+
esac
|
|
309
|
+
`,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
files.set(
|
|
313
|
+
'docker-healthcheck.sh',
|
|
314
|
+
`#!/bin/sh
|
|
315
|
+
# What "healthy" means depends on the role — see docker-entrypoint.sh.
|
|
316
|
+
# "migrate" runs to completion and exits; its exit code is the verdict, and
|
|
317
|
+
# a health probe taken while it runs has no opinion.
|
|
318
|
+
set -e
|
|
319
|
+
|
|
320
|
+
if [ "\${COMMUNITY_ROLE:-web}" = "migrate" ]; then
|
|
321
|
+
exit 0
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
node -e "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
325
|
+
`,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
files.set(
|
|
329
|
+
'.dockerignore',
|
|
330
|
+
`node_modules
|
|
331
|
+
.next
|
|
332
|
+
.meith
|
|
333
|
+
.git
|
|
334
|
+
.env
|
|
335
|
+
.env.local
|
|
336
|
+
*.log
|
|
337
|
+
`,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
files.set(
|
|
341
|
+
'.github/workflows/build.yml',
|
|
342
|
+
`# Builds this board's image and pushes it to your own GHCR, on every push to
|
|
343
|
+
# main. No secret to configure: GITHUB_TOKEN is provided automatically by
|
|
344
|
+
# GitHub Actions and is enough to push to ghcr.io/<this repository>. See
|
|
345
|
+
# README.md for the rest of the three-step deploy story.
|
|
346
|
+
name: Build and push
|
|
347
|
+
|
|
348
|
+
on:
|
|
349
|
+
push:
|
|
350
|
+
branches: [main]
|
|
351
|
+
workflow_dispatch:
|
|
352
|
+
|
|
353
|
+
permissions:
|
|
354
|
+
contents: read
|
|
355
|
+
packages: write
|
|
356
|
+
|
|
357
|
+
jobs:
|
|
358
|
+
image:
|
|
359
|
+
name: Build and push the board image
|
|
360
|
+
runs-on: ubuntu-latest
|
|
361
|
+
steps:
|
|
362
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
363
|
+
|
|
364
|
+
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
|
365
|
+
with:
|
|
366
|
+
registry: ghcr.io
|
|
367
|
+
username: \${{ github.actor }}
|
|
368
|
+
password: \${{ secrets.GITHUB_TOKEN }}
|
|
369
|
+
|
|
370
|
+
# GHCR requires a lower-case image name, and neither your GitHub
|
|
371
|
+
# username nor this repository's name is guaranteed to be.
|
|
372
|
+
- name: Build and push
|
|
373
|
+
run: |
|
|
374
|
+
IMAGE=$(echo "ghcr.io/\${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
|
|
375
|
+
docker build -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
|
|
376
|
+
docker push "$IMAGE:\${{ github.sha }}"
|
|
377
|
+
docker push "$IMAGE:latest"
|
|
378
|
+
`,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
files.set(
|
|
382
|
+
'compose.yml',
|
|
383
|
+
`# ${name}, deployed by Coolify — the same shape as the meith repository's own
|
|
384
|
+
# docker/compose.coolify.yml: db, migrate, web, worker. See README.md for
|
|
385
|
+
# the three-step deploy story this file is the last step of.
|
|
386
|
+
#
|
|
387
|
+
# No published ports — Coolify's proxy routes to the container and issues
|
|
388
|
+
# the certificate. The two secrets and the database password are Coolify's
|
|
389
|
+
# own "magic variables": it fills them in on the first deploy and shows them
|
|
390
|
+
# in the panel, so nothing here needs a value typed into it except
|
|
391
|
+
# MEITH_IMAGE, which only you can know — see README.md for where it comes
|
|
392
|
+
# from. Requires Coolify v4.0.0-beta.411 or newer, which is when magic
|
|
393
|
+
# variables in a compose file from a Git source arrived.
|
|
394
|
+
services:
|
|
395
|
+
postgres:
|
|
396
|
+
image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
|
|
397
|
+
restart: unless-stopped
|
|
398
|
+
mem_limit: \${POSTGRES_MEM_LIMIT:-1g}
|
|
399
|
+
cpus: \${POSTGRES_CPUS:-1}
|
|
400
|
+
environment:
|
|
401
|
+
POSTGRES_USER: community
|
|
402
|
+
POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES
|
|
403
|
+
POSTGRES_DB: community
|
|
404
|
+
volumes:
|
|
405
|
+
- pgdata:/var/lib/postgresql
|
|
406
|
+
healthcheck:
|
|
407
|
+
test: ['CMD-SHELL', 'pg_isready -U community -d community']
|
|
408
|
+
interval: 10s
|
|
409
|
+
timeout: 5s
|
|
410
|
+
retries: 5
|
|
411
|
+
|
|
412
|
+
# Runs to completion, then exits. web waits for it, so the schema is
|
|
413
|
+
# always applied before the first request rather than racing it.
|
|
414
|
+
migrate:
|
|
415
|
+
image: \${MEITH_IMAGE:?set this to the image .github/workflows/build.yml just pushed, e.g. ghcr.io/<you>/${name}:latest — see README.md}
|
|
416
|
+
environment:
|
|
417
|
+
COMMUNITY_ROLE: migrate
|
|
418
|
+
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
419
|
+
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
420
|
+
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
421
|
+
depends_on:
|
|
422
|
+
postgres:
|
|
423
|
+
condition: service_healthy
|
|
424
|
+
restart: 'no'
|
|
425
|
+
|
|
426
|
+
web:
|
|
427
|
+
image: \${MEITH_IMAGE:?set this to the image .github/workflows/build.yml just pushed, e.g. ghcr.io/<you>/${name}:latest — see README.md}
|
|
428
|
+
restart: unless-stopped
|
|
429
|
+
mem_limit: \${WEB_MEM_LIMIT:-1g}
|
|
430
|
+
cpus: \${WEB_CPUS:-2}
|
|
431
|
+
environment:
|
|
432
|
+
# Ask Coolify for a domain on port 3000, then hand the board the same
|
|
433
|
+
# thing with a scheme in front.
|
|
434
|
+
- SERVICE_FQDN_WEB_3000
|
|
435
|
+
- APP_URL=$SERVICE_URL_WEB
|
|
436
|
+
- DATABASE_URL=postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
437
|
+
- AUTH_SECRET=$SERVICE_BASE64_64_AUTH
|
|
438
|
+
- TICK_SECRET=$SERVICE_BASE64_64_TICK
|
|
439
|
+
- QUEUE_DRIVER=postgres
|
|
440
|
+
- CACHE_DRIVER=next
|
|
441
|
+
- FILESTORE_DRIVER=local
|
|
442
|
+
# Left unset, mail is configured on the board itself — the installer
|
|
443
|
+
# asks on first run. Set MAIL_DRIVER here and this file wins instead.
|
|
444
|
+
- MAIL_DRIVER=\${MAIL_DRIVER:-log}
|
|
445
|
+
- MAIL_SMTP_HOST=\${MAIL_SMTP_HOST:-}
|
|
446
|
+
- MAIL_SMTP_PORT=\${MAIL_SMTP_PORT:-}
|
|
447
|
+
- MAIL_SMTP_SECURITY=\${MAIL_SMTP_SECURITY:-}
|
|
448
|
+
- MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
|
|
449
|
+
- MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
|
|
450
|
+
- MAIL_FROM=\${MAIL_FROM:-}
|
|
451
|
+
volumes:
|
|
452
|
+
- uploads:/app/.uploads
|
|
453
|
+
depends_on:
|
|
454
|
+
postgres:
|
|
455
|
+
condition: service_healthy
|
|
456
|
+
migrate:
|
|
457
|
+
condition: service_completed_successfully
|
|
458
|
+
|
|
459
|
+
# @meith/worker is not published (see the meith repository's
|
|
460
|
+
# docs/release.md), so there is no compiled worker binary a scaffolded
|
|
461
|
+
# board can run — this drives the tick the alternative way the meith
|
|
462
|
+
# repository documents in docs/self-hosting.md, "Running the tick without
|
|
463
|
+
# a second set of credentials": a small loop calling /api/system/tick.
|
|
464
|
+
worker:
|
|
465
|
+
image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
|
|
466
|
+
restart: unless-stopped
|
|
467
|
+
mem_limit: \${WORKER_MEM_LIMIT:-64m}
|
|
468
|
+
cpus: \${WORKER_CPUS:-0.25}
|
|
469
|
+
environment:
|
|
470
|
+
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
471
|
+
command:
|
|
472
|
+
- sh
|
|
473
|
+
- -c
|
|
474
|
+
- |
|
|
475
|
+
apk add --no-cache curl >/dev/null
|
|
476
|
+
while true; do
|
|
477
|
+
curl -fsS -m 55 -H "Authorization: Bearer $$TICK_SECRET" \\
|
|
478
|
+
http://web:3000/api/system/tick >/dev/null 2>&1 \\
|
|
479
|
+
|| echo "tick failed at $$(date -Is)"
|
|
480
|
+
sleep 60
|
|
481
|
+
done
|
|
482
|
+
depends_on:
|
|
483
|
+
- web
|
|
484
|
+
|
|
485
|
+
volumes:
|
|
486
|
+
pgdata:
|
|
487
|
+
uploads:
|
|
488
|
+
`,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
files.set(
|
|
492
|
+
'README.md',
|
|
493
|
+
`# ${name}
|
|
494
|
+
|
|
495
|
+
A forum, built on [Meith](${repositoryUrl}).
|
|
496
|
+
|
|
497
|
+
## Deploy
|
|
498
|
+
|
|
499
|
+
Nothing here builds on your own server — a 2 GB VPS OOMs on a Next.js build,
|
|
500
|
+
which is the whole reason \`Dockerfile\`, \`compose.yml\` and
|
|
501
|
+
\`.github/workflows/build.yml\` exist: something else builds the image, the
|
|
502
|
+
server only ever pulls one. Three steps, nothing to configure by hand beyond
|
|
503
|
+
one value only you know:
|
|
504
|
+
|
|
505
|
+
1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
|
|
506
|
+
\`Dockerfile\` on every push to \`main\` and pushes the result to your own
|
|
507
|
+
GitHub Container Registry, \`ghcr.io/<you>/${name}\` — using only the
|
|
508
|
+
\`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
|
|
509
|
+
add, no registry account beyond the GitHub account you already have.
|
|
510
|
+
|
|
511
|
+
The package GitHub creates for it starts **private**. Make it public —
|
|
512
|
+
the package's own Settings → Change visibility — or Coolify's pull fails
|
|
513
|
+
with an authentication error no operator can act on; the meith project's
|
|
514
|
+
own release process has the identical one-time step for its official
|
|
515
|
+
image.
|
|
516
|
+
|
|
517
|
+
2. **Point [Coolify](https://coolify.io) at \`compose.yml\`** — a Docker
|
|
518
|
+
Compose resource, this repository as its source. \`compose.yml\` already
|
|
519
|
+
carries Coolify's own "magic variables" for \`AUTH_SECRET\`,
|
|
520
|
+
\`TICK_SECRET\` and the database password, generated on the first deploy
|
|
521
|
+
and never typed in. The one thing Coolify cannot generate is the image
|
|
522
|
+
step 1 just pushed: set \`MEITH_IMAGE\` in the resource's own environment
|
|
523
|
+
to \`ghcr.io/<you>/${name}:latest\` (or a commit sha, once you want a
|
|
524
|
+
pin that only moves when you say so — \`compose.yml\` refuses to start
|
|
525
|
+
without this set, with a message saying why).
|
|
526
|
+
|
|
527
|
+
3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
|
|
528
|
+
certificate; the installer from there is the one
|
|
529
|
+
[docs/quickstart.md](${repositoryUrl}/blob/main/docs/quickstart.md#4-run-the-installer)
|
|
530
|
+
walks through, screen for screen. It seals itself when it finishes, and
|
|
531
|
+
\`/install\` answers 404 from then on — run it **against the database you
|
|
532
|
+
are going to keep**. Every push to \`main\` after this rebuilds the
|
|
533
|
+
image; Coolify's own **Redeploy** button is what actually pulls it —
|
|
534
|
+
pushing alone does not.
|
|
535
|
+
|
|
536
|
+
No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
|
|
537
|
+
build side of this, for a board of any size.
|
|
538
|
+
|
|
539
|
+
**Building it yourself**: \`docker build -t ${name} .\` works on any machine
|
|
540
|
+
with Docker, if you would rather not use GitHub Actions for the build —
|
|
541
|
+
push the result wherever \`compose.yml\`'s \`MEITH_IMAGE\` can reach.
|
|
542
|
+
|
|
543
|
+
**Without a panel**: [docs/self-hosting.md](${repositoryUrl}/blob/main/docs/self-hosting.md)
|
|
544
|
+
is the same four containers by hand — your own \`.env\`, a reverse proxy you
|
|
545
|
+
already run, no Coolify. \`Dockerfile\` and \`compose.yml\` here are this
|
|
546
|
+
board's own version of exactly that shape.
|
|
547
|
+
|
|
548
|
+
Two things nothing configures for you:
|
|
549
|
+
|
|
550
|
+
- **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
|
|
551
|
+
written to the log and delivered to nobody, so password reset fails silently.
|
|
552
|
+
- **The tick.** \`compose.yml\`'s \`worker\` service drives it here — a small
|
|
553
|
+
loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
|
|
554
|
+
worker package is not something a board outside the meith monorepo can
|
|
555
|
+
depend on yet. Deploy some other way and something still has to call that
|
|
556
|
+
route (or run \`community task:run\`) every minute, or nothing catches up
|
|
557
|
+
and nothing errors.
|
|
558
|
+
|
|
559
|
+
## Local
|
|
560
|
+
|
|
561
|
+
\`\`\`sh
|
|
562
|
+
npm install
|
|
563
|
+
cp .env.example .env.local
|
|
564
|
+
npm run dev
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
With no \`DATABASE_URL\`, the board runs on deterministic in-memory sample data —
|
|
568
|
+
enough to click through every reading surface. Posting needs a database:
|
|
569
|
+
|
|
570
|
+
\`\`\`sh
|
|
571
|
+
npm run forum -- migrate
|
|
572
|
+
npm run forum -- user:create --admin
|
|
573
|
+
\`\`\`
|
|
574
|
+
|
|
575
|
+
## Configuring
|
|
576
|
+
|
|
577
|
+
- **\`community.config.ts\`** — installed themes and plugins. Everything installable
|
|
578
|
+
is named here so the bundler can see it; nothing is found by scanning a
|
|
579
|
+
directory at runtime.
|
|
580
|
+
- **\`/admin\`** — settings, forums, groups, members, themes, maintenance. An
|
|
581
|
+
administrator re-enters their password to get in, and again for anything
|
|
582
|
+
destructive.
|
|
583
|
+
- **\`npm run forum -- --help\`** — the operator CLI. Everything the panel does
|
|
584
|
+
and a few things it cannot, without a browser.
|
|
585
|
+
|
|
586
|
+
## Upgrading
|
|
587
|
+
|
|
588
|
+
\`\`\`sh
|
|
589
|
+
npm install @meith/web@latest @meith/cli@latest
|
|
590
|
+
npm run forum -- upgrade
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
\`Dockerfile\`'s \`FROM ghcr.io/meith-dev/meith-base:...\` line pins the same
|
|
594
|
+
version as \`@meith/web\` — bump it to match what \`npm install\` just
|
|
595
|
+
resolved (\`npm ls @meith/web\` prints it) in the same commit as the
|
|
596
|
+
\`package.json\` change above, before the next \`git push\` triggers a
|
|
597
|
+
rebuild. The two pins drifting is not a broken board — \`npm install\` inside
|
|
598
|
+
the Docker build still resolves the newer \`@meith/web\` correctly — only a
|
|
599
|
+
slower one, rebuilding more than the base image already had to. There is no
|
|
600
|
+
script for this yet: it is one line, by hand.
|
|
601
|
+
|
|
602
|
+
Migrations are forward-only. Recovery is by restore, so take a backup first —
|
|
603
|
+
there is no down migration to undo a destructive one, and a button that pretended
|
|
604
|
+
otherwise would be worse than its absence.
|
|
605
|
+
`,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
return files
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function nextSteps(name: string): readonly string[] {
|
|
612
|
+
return [`cd ${name}`, 'npm install', 'cp .env.example .env.local', 'npm run dev']
|
|
613
|
+
}
|