@helyx/cli 0.1.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 +725 -0
- package/README.md +16 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +85 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/self-host/catalogue.d.ts +72 -0
- package/dist/self-host/catalogue.js +115 -0
- package/dist/self-host/command.d.ts +9 -0
- package/dist/self-host/command.js +76 -0
- package/dist/self-host/manifest.d.ts +23 -0
- package/dist/self-host/manifest.js +127 -0
- package/dist/self-host/project-generator.d.ts +24 -0
- package/dist/self-host/project-generator.js +236 -0
- package/package.json +63 -0
- package/self-host-catalogue.json +278 -0
- package/templates/self-host/.env.example +26 -0
- package/templates/self-host/Dockerfile +15 -0
- package/templates/self-host/README.md +56 -0
- package/templates/self-host/compose.yaml +27 -0
- package/templates/self-host/npmrc.template +3 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { dirname, parse, resolve } from "node:path";
|
|
5
|
+
import { parseSelfHostBuildManifest, } from "./manifest.js";
|
|
6
|
+
const generatedFileNames = [
|
|
7
|
+
".env.example",
|
|
8
|
+
".npmrc",
|
|
9
|
+
"Dockerfile",
|
|
10
|
+
"compose.yaml",
|
|
11
|
+
"README.md",
|
|
12
|
+
];
|
|
13
|
+
export async function generateSelfHostProject(options) {
|
|
14
|
+
const manifest = parseSelfHostBuildManifest(options.manifest, options.catalogue);
|
|
15
|
+
const targetDirectory = resolve(options.targetDirectory);
|
|
16
|
+
assertSafeTargetDirectory(targetDirectory);
|
|
17
|
+
await mkdir(dirname(targetDirectory), { recursive: true });
|
|
18
|
+
let created = false;
|
|
19
|
+
try {
|
|
20
|
+
await mkdir(targetDirectory);
|
|
21
|
+
created = true;
|
|
22
|
+
await writeGeneratedFiles(targetDirectory, manifest, options.catalogue);
|
|
23
|
+
const runner = options.commandRunner ?? runNpmCommand;
|
|
24
|
+
await runner([
|
|
25
|
+
"install",
|
|
26
|
+
"--package-lock-only",
|
|
27
|
+
"--ignore-scripts",
|
|
28
|
+
"--no-audit",
|
|
29
|
+
"--no-fund",
|
|
30
|
+
], targetDirectory);
|
|
31
|
+
await validateGeneratedLockfile(targetDirectory, manifest);
|
|
32
|
+
if (options.install) {
|
|
33
|
+
await runner(["ci", "--no-audit", "--no-fund"], targetDirectory);
|
|
34
|
+
}
|
|
35
|
+
await writeChecksums(targetDirectory);
|
|
36
|
+
const files = (await readdir(targetDirectory)).sort((left, right) => left.localeCompare(right, "en"));
|
|
37
|
+
return { directory: targetDirectory, manifest, files };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (created) {
|
|
41
|
+
await rm(targetDirectory, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
force: true,
|
|
44
|
+
maxRetries: 5,
|
|
45
|
+
retryDelay: 100,
|
|
46
|
+
}).catch(() => undefined);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function writeGeneratedFiles(targetDirectory, manifest, catalogue) {
|
|
52
|
+
const dependencyEntries = [
|
|
53
|
+
[manifest.core.package, manifest.core.version],
|
|
54
|
+
[manifest.generator.package, manifest.generator.version],
|
|
55
|
+
...manifest.modules.map((module) => [module.package, module.version]),
|
|
56
|
+
];
|
|
57
|
+
dependencyEntries.sort(([left], [right]) => left.localeCompare(right, "en"));
|
|
58
|
+
const dependencies = Object.fromEntries(dependencyEntries);
|
|
59
|
+
const packageJson = {
|
|
60
|
+
name: "helyx-self-hosted",
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
private: true,
|
|
63
|
+
license: "UNLICENSED",
|
|
64
|
+
type: "module",
|
|
65
|
+
packageManager: "npm@11.18.0",
|
|
66
|
+
engines: { node: ">=24 <27", npm: ">=11" },
|
|
67
|
+
scripts: {
|
|
68
|
+
start: "helyx start",
|
|
69
|
+
doctor: "helyx doctor",
|
|
70
|
+
migrate: "helyx migrate",
|
|
71
|
+
},
|
|
72
|
+
dependencies,
|
|
73
|
+
};
|
|
74
|
+
const moduleCatalogue = {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
packages: manifest.modules.map((module) => module.package),
|
|
77
|
+
};
|
|
78
|
+
await writeFile(resolve(targetDirectory, "package.json"), formatJson(packageJson), "utf8");
|
|
79
|
+
await writeFile(resolve(targetDirectory, "helyx.modules.json"), formatJson(moduleCatalogue), "utf8");
|
|
80
|
+
for (const fileName of generatedFileNames) {
|
|
81
|
+
const template = await readFile(templateUrl(fileName), "utf8");
|
|
82
|
+
if (fileName === ".env.example")
|
|
83
|
+
validateEnvironmentTemplate(template);
|
|
84
|
+
const content = fileName === "README.md"
|
|
85
|
+
? renderReadme(template, manifest, catalogue)
|
|
86
|
+
: normalizeCrLf(template);
|
|
87
|
+
await writeFile(resolve(targetDirectory, fileName), content, "utf8");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function renderReadme(template, manifest, catalogue) {
|
|
91
|
+
const modulesById = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
92
|
+
const selected = manifest.modules.length
|
|
93
|
+
? manifest.modules
|
|
94
|
+
.map((module) => {
|
|
95
|
+
const definition = modulesById.get(module.id);
|
|
96
|
+
return `- ${definition?.name ?? module.id} (\`${module.package}@${module.version}\`)`;
|
|
97
|
+
})
|
|
98
|
+
.join("\n")
|
|
99
|
+
: "- No feature modules selected. This is a core-only installation.";
|
|
100
|
+
return normalizeCrLf(template.replace("{{SELECTED_MODULES}}", selected));
|
|
101
|
+
}
|
|
102
|
+
function templateUrl(fileName) {
|
|
103
|
+
const templateName = fileName === ".npmrc" ? "npmrc.template" : fileName;
|
|
104
|
+
return new URL(`../../templates/self-host/${templateName}`, import.meta.url);
|
|
105
|
+
}
|
|
106
|
+
function formatJson(value) {
|
|
107
|
+
return normalizeCrLf(`${JSON.stringify(value, null, 2)}\n`);
|
|
108
|
+
}
|
|
109
|
+
function normalizeCrLf(value) {
|
|
110
|
+
return value
|
|
111
|
+
.replaceAll("\r\n", "\n")
|
|
112
|
+
.replaceAll("\r", "\n")
|
|
113
|
+
.replaceAll("\n", "\r\n");
|
|
114
|
+
}
|
|
115
|
+
function validateEnvironmentTemplate(template) {
|
|
116
|
+
const lines = new Set(template
|
|
117
|
+
.replaceAll("\r\n", "\n")
|
|
118
|
+
.split("\n")
|
|
119
|
+
.map((line) => line.trim()));
|
|
120
|
+
for (const [key, definition] of Object.entries(selfHostEnvironmentContract)) {
|
|
121
|
+
const expected = `${definition.optional ? "# " : ""}${key}=${definition.value}`;
|
|
122
|
+
if (!lines.has(expected)) {
|
|
123
|
+
throw new Error(`Self-host environment template does not match the bot contract: ${key}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function validateGeneratedLockfile(targetDirectory, manifest) {
|
|
128
|
+
const lockfilePath = resolve(targetDirectory, "package-lock.json");
|
|
129
|
+
const lockfile = JSON.parse(await readFile(lockfilePath, "utf8"));
|
|
130
|
+
if (!isRecord(lockfile) || !isRecord(lockfile.packages)) {
|
|
131
|
+
throw new Error("Generated package-lock.json is invalid");
|
|
132
|
+
}
|
|
133
|
+
const root = lockfile.packages[""];
|
|
134
|
+
if (!isRecord(root)) {
|
|
135
|
+
throw new Error("Generated package-lock.json has no root dependencies");
|
|
136
|
+
}
|
|
137
|
+
const lockedDependencies = root.dependencies;
|
|
138
|
+
if (!isRecord(lockedDependencies)) {
|
|
139
|
+
throw new Error("Generated package-lock.json has no root dependencies");
|
|
140
|
+
}
|
|
141
|
+
const expected = new Map([
|
|
142
|
+
[manifest.core.package, manifest.core.version],
|
|
143
|
+
[manifest.generator.package, manifest.generator.version],
|
|
144
|
+
...manifest.modules.map((module) => [module.package, module.version]),
|
|
145
|
+
]);
|
|
146
|
+
if (Object.keys(lockedDependencies).length !== expected.size ||
|
|
147
|
+
[...expected].some(([packageName, version]) => lockedDependencies[packageName] !== version)) {
|
|
148
|
+
throw new Error("Generated package-lock.json does not match the selected exact dependencies");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function writeChecksums(targetDirectory) {
|
|
152
|
+
const fileNames = (await readdir(targetDirectory))
|
|
153
|
+
.filter((fileName) => fileName !== "SHA256SUMS")
|
|
154
|
+
.sort((left, right) => left.localeCompare(right, "en"));
|
|
155
|
+
const lines = [];
|
|
156
|
+
for (const fileName of fileNames) {
|
|
157
|
+
const file = await stat(resolve(targetDirectory, fileName));
|
|
158
|
+
if (!file.isFile())
|
|
159
|
+
continue;
|
|
160
|
+
const digest = createHash("sha256")
|
|
161
|
+
.update(await readFile(resolve(targetDirectory, fileName)))
|
|
162
|
+
.digest("hex");
|
|
163
|
+
lines.push(`${digest} ${fileName}`);
|
|
164
|
+
}
|
|
165
|
+
await writeFile(resolve(targetDirectory, "SHA256SUMS"), normalizeCrLf(`${lines.join("\n")}\n`), "utf8");
|
|
166
|
+
}
|
|
167
|
+
function assertSafeTargetDirectory(targetDirectory) {
|
|
168
|
+
if (targetDirectory === parse(targetDirectory).root) {
|
|
169
|
+
throw new Error("Refusing to generate a self-host project at a filesystem root");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function runNpmCommand(arguments_, workingDirectory) {
|
|
173
|
+
const executable = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
174
|
+
await new Promise((resolvePromise, reject) => {
|
|
175
|
+
const child = spawn(executable, [...arguments_], {
|
|
176
|
+
cwd: workingDirectory,
|
|
177
|
+
env: arguments_.includes("--ignore-scripts")
|
|
178
|
+
? { ...process.env, npm_config_ignore_scripts: "true" }
|
|
179
|
+
: process.env,
|
|
180
|
+
shell: false,
|
|
181
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
182
|
+
});
|
|
183
|
+
child.stdout.resume();
|
|
184
|
+
child.stderr.resume();
|
|
185
|
+
const timeout = setTimeout(() => {
|
|
186
|
+
child.kill("SIGTERM");
|
|
187
|
+
reject(new Error("npm command exceeded the two-minute generation limit"));
|
|
188
|
+
}, 120_000);
|
|
189
|
+
child.once("error", (error) => {
|
|
190
|
+
clearTimeout(timeout);
|
|
191
|
+
reject(error);
|
|
192
|
+
});
|
|
193
|
+
child.once("close", (code) => {
|
|
194
|
+
clearTimeout(timeout);
|
|
195
|
+
if (code === 0)
|
|
196
|
+
resolvePromise();
|
|
197
|
+
else
|
|
198
|
+
reject(new Error(`npm command failed with exit code ${String(code)}; rerun it in the generated directory for detailed diagnostics`));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function isRecord(value) {
|
|
203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
204
|
+
}
|
|
205
|
+
// This compile-time map intentionally stays adjacent to the generator. Adding a
|
|
206
|
+
// bot environment field makes the generated contract fail to compile until the
|
|
207
|
+
// self-host example receives an explicit safe value or omission decision.
|
|
208
|
+
export const selfHostEnvironmentContract = {
|
|
209
|
+
DATABASE_URL: {
|
|
210
|
+
value: "postgresql://database:5432/helyx",
|
|
211
|
+
optional: false,
|
|
212
|
+
},
|
|
213
|
+
DATABASE_SSL: { value: "disable", optional: false },
|
|
214
|
+
DISCORD_TOKEN: { value: "replace-me", optional: false },
|
|
215
|
+
DISCORD_CLIENT_ID: { value: "12345678901234567", optional: false },
|
|
216
|
+
DISCORD_INTERNAL_GUILD_ID: {
|
|
217
|
+
value: "12345678901234567",
|
|
218
|
+
optional: true,
|
|
219
|
+
},
|
|
220
|
+
HELYX_MODULE_PACKAGES: {
|
|
221
|
+
value: "",
|
|
222
|
+
optional: true,
|
|
223
|
+
},
|
|
224
|
+
HELYX_MODULE_CATALOGUE_PATH: {
|
|
225
|
+
value: "./helyx.modules.json",
|
|
226
|
+
optional: false,
|
|
227
|
+
},
|
|
228
|
+
HELYX_BOT_CONTROL_SECRET: {
|
|
229
|
+
value: "replace-with-a-base64-encoded-32-byte-key",
|
|
230
|
+
optional: true,
|
|
231
|
+
},
|
|
232
|
+
HELYX_BOT_CONTROL_PORT: { value: "3001", optional: true },
|
|
233
|
+
LOG_LEVEL: { value: "info", optional: false },
|
|
234
|
+
NODE_ENV: { value: "production", optional: false },
|
|
235
|
+
};
|
|
236
|
+
//# sourceMappingURL=project-generator.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helyx/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line tools for running and generating self-hosted Helyx deployments.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"helyx",
|
|
7
|
+
"discord",
|
|
8
|
+
"cli",
|
|
9
|
+
"self-hosted"
|
|
10
|
+
],
|
|
11
|
+
"private": false,
|
|
12
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
13
|
+
"homepage": "https://helyx.gg/",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/ZyC0R3/Helyx/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ZyC0R3/Helyx.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24 <27",
|
|
24
|
+
"npm": ">=11"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"provenance": true
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"!dist/*.tsbuildinfo",
|
|
34
|
+
"!dist/**/*.map",
|
|
35
|
+
"self-host-catalogue.json",
|
|
36
|
+
"templates",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"bin": {
|
|
41
|
+
"helyx": "./dist/cli.js"
|
|
42
|
+
},
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"default": "./dist/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./catalogue.json": "./self-host-catalogue.json"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -b"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@helyx/bot": "0.1.0",
|
|
55
|
+
"@helyx/config": "0.1.0",
|
|
56
|
+
"@helyx/database": "0.1.0",
|
|
57
|
+
"semver": "7.8.5",
|
|
58
|
+
"zod": "4.4.3"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/semver": "7.7.1"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"catalogueVersion": 3,
|
|
4
|
+
"core": {
|
|
5
|
+
"package": "@helyx/bot",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"availability": "unpublished"
|
|
8
|
+
},
|
|
9
|
+
"generator": {
|
|
10
|
+
"package": "@helyx/cli",
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"command": "helyx",
|
|
13
|
+
"availability": "unpublished"
|
|
14
|
+
},
|
|
15
|
+
"modules": [
|
|
16
|
+
{
|
|
17
|
+
"id": "helyx.feedback",
|
|
18
|
+
"package": "@helyx/module-feedback",
|
|
19
|
+
"version": "1.0.1",
|
|
20
|
+
"name": "Feedback",
|
|
21
|
+
"description": "Collect structured member feedback in a channel you choose.",
|
|
22
|
+
"category": "Community",
|
|
23
|
+
"availability": "published"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "helyx.welcome",
|
|
27
|
+
"package": "@helyx/module-welcome",
|
|
28
|
+
"version": "1.0.1",
|
|
29
|
+
"name": "Welcome",
|
|
30
|
+
"description": "Greet new members and optionally give them a starting role.",
|
|
31
|
+
"category": "Community",
|
|
32
|
+
"availability": "published"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "helyx.suggestions",
|
|
36
|
+
"package": "@helyx/module-suggestions",
|
|
37
|
+
"version": "1.0.1",
|
|
38
|
+
"name": "Suggestions",
|
|
39
|
+
"description": "Collect community ideas with reaction or button voting.",
|
|
40
|
+
"category": "Community",
|
|
41
|
+
"availability": "published"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "helyx.polls",
|
|
45
|
+
"package": "@helyx/module-polls",
|
|
46
|
+
"version": null,
|
|
47
|
+
"name": "Polls",
|
|
48
|
+
"description": "Run clear single or multiple-choice polls.",
|
|
49
|
+
"category": "Community",
|
|
50
|
+
"availability": "planned"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "helyx.starboard",
|
|
54
|
+
"package": "@helyx/module-starboard",
|
|
55
|
+
"version": null,
|
|
56
|
+
"name": "Starboard",
|
|
57
|
+
"description": "Highlight messages your community values.",
|
|
58
|
+
"category": "Community",
|
|
59
|
+
"availability": "planned"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "helyx.levels",
|
|
63
|
+
"package": "@helyx/module-levels",
|
|
64
|
+
"version": null,
|
|
65
|
+
"name": "Levels",
|
|
66
|
+
"description": "Optional participation levels and rewards.",
|
|
67
|
+
"category": "Community",
|
|
68
|
+
"availability": "planned"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "helyx.moderation",
|
|
72
|
+
"package": "@helyx/module-moderation",
|
|
73
|
+
"version": null,
|
|
74
|
+
"name": "Moderation",
|
|
75
|
+
"description": "Warnings, timeouts, kicks and bans with audit context.",
|
|
76
|
+
"category": "Safety",
|
|
77
|
+
"availability": "planned"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "helyx.automod",
|
|
81
|
+
"package": "@helyx/module-automod",
|
|
82
|
+
"version": null,
|
|
83
|
+
"name": "Auto moderation",
|
|
84
|
+
"description": "Apply transparent rules to spam and unsafe content.",
|
|
85
|
+
"category": "Safety",
|
|
86
|
+
"availability": "planned"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "helyx.verification",
|
|
90
|
+
"package": "@helyx/module-verification",
|
|
91
|
+
"version": null,
|
|
92
|
+
"name": "Verification",
|
|
93
|
+
"description": "Add a deliberate member verification step.",
|
|
94
|
+
"category": "Safety",
|
|
95
|
+
"availability": "planned"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "helyx.antiraid",
|
|
99
|
+
"package": "@helyx/module-antiraid",
|
|
100
|
+
"version": null,
|
|
101
|
+
"name": "Raid protection",
|
|
102
|
+
"description": "Detect and slow unusual join activity.",
|
|
103
|
+
"category": "Safety",
|
|
104
|
+
"availability": "planned"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "helyx.tickets",
|
|
108
|
+
"package": "@helyx/module-tickets",
|
|
109
|
+
"version": null,
|
|
110
|
+
"name": "Tickets",
|
|
111
|
+
"description": "Private support requests with ownership and transcripts.",
|
|
112
|
+
"category": "Support",
|
|
113
|
+
"availability": "planned"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "helyx.forms",
|
|
117
|
+
"package": "@helyx/module-forms",
|
|
118
|
+
"version": null,
|
|
119
|
+
"name": "Forms",
|
|
120
|
+
"description": "Collect structured requests through Discord modals.",
|
|
121
|
+
"category": "Support",
|
|
122
|
+
"availability": "planned"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "helyx.faq",
|
|
126
|
+
"package": "@helyx/module-faq",
|
|
127
|
+
"version": null,
|
|
128
|
+
"name": "Knowledge base",
|
|
129
|
+
"description": "Answer common questions with maintained server guidance.",
|
|
130
|
+
"category": "Support",
|
|
131
|
+
"availability": "planned"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "helyx.autoroles",
|
|
135
|
+
"package": "@helyx/module-autoroles",
|
|
136
|
+
"version": null,
|
|
137
|
+
"name": "Auto roles",
|
|
138
|
+
"description": "Assign appropriate roles when members join.",
|
|
139
|
+
"category": "Automation",
|
|
140
|
+
"availability": "planned"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "helyx.reactionroles",
|
|
144
|
+
"package": "@helyx/module-reactionroles",
|
|
145
|
+
"version": null,
|
|
146
|
+
"name": "Self roles",
|
|
147
|
+
"description": "Let members select approved roles themselves.",
|
|
148
|
+
"category": "Automation",
|
|
149
|
+
"availability": "planned"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "helyx.reminders",
|
|
153
|
+
"package": "@helyx/module-reminders",
|
|
154
|
+
"version": null,
|
|
155
|
+
"name": "Reminders",
|
|
156
|
+
"description": "Schedule personal and server reminders.",
|
|
157
|
+
"category": "Automation",
|
|
158
|
+
"availability": "planned"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "helyx.tempvoice",
|
|
162
|
+
"package": "@helyx/module-tempvoice",
|
|
163
|
+
"version": null,
|
|
164
|
+
"name": "Temporary voice",
|
|
165
|
+
"description": "Create voice rooms that clean themselves up.",
|
|
166
|
+
"category": "Automation",
|
|
167
|
+
"availability": "planned"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "helyx.scheduler",
|
|
171
|
+
"package": "@helyx/module-scheduler",
|
|
172
|
+
"version": null,
|
|
173
|
+
"name": "Scheduler",
|
|
174
|
+
"description": "Run approved messages and actions on a schedule.",
|
|
175
|
+
"category": "Automation",
|
|
176
|
+
"availability": "planned"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"id": "helyx.events",
|
|
180
|
+
"package": "@helyx/module-events",
|
|
181
|
+
"version": null,
|
|
182
|
+
"name": "Events",
|
|
183
|
+
"description": "Plan events and track attendance.",
|
|
184
|
+
"category": "Engagement",
|
|
185
|
+
"availability": "planned"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"id": "helyx.giveaways",
|
|
189
|
+
"package": "@helyx/module-giveaways",
|
|
190
|
+
"version": null,
|
|
191
|
+
"name": "Giveaways",
|
|
192
|
+
"description": "Run auditable giveaways with eligibility rules.",
|
|
193
|
+
"category": "Engagement",
|
|
194
|
+
"availability": "planned"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"id": "helyx.birthdays",
|
|
198
|
+
"package": "@helyx/module-birthdays",
|
|
199
|
+
"version": null,
|
|
200
|
+
"name": "Birthdays",
|
|
201
|
+
"description": "Celebrate opted-in member birthdays.",
|
|
202
|
+
"category": "Engagement",
|
|
203
|
+
"availability": "planned"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "helyx.counting",
|
|
207
|
+
"package": "@helyx/module-counting",
|
|
208
|
+
"version": null,
|
|
209
|
+
"name": "Counting",
|
|
210
|
+
"description": "Run a configurable community counting channel.",
|
|
211
|
+
"category": "Engagement",
|
|
212
|
+
"availability": "planned"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"id": "helyx.logging",
|
|
216
|
+
"package": "@helyx/module-logging",
|
|
217
|
+
"version": "1.0.1",
|
|
218
|
+
"name": "Logging",
|
|
219
|
+
"description": "Post selected Helyx module activity to one server channel.",
|
|
220
|
+
"category": "Insights",
|
|
221
|
+
"availability": "published"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"id": "helyx.analytics",
|
|
225
|
+
"package": "@helyx/module-analytics",
|
|
226
|
+
"version": null,
|
|
227
|
+
"name": "Analytics",
|
|
228
|
+
"description": "Understand server activity with privacy-conscious summaries.",
|
|
229
|
+
"category": "Insights",
|
|
230
|
+
"availability": "planned"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"id": "helyx.health",
|
|
234
|
+
"package": "@helyx/module-health",
|
|
235
|
+
"version": null,
|
|
236
|
+
"name": "Server health",
|
|
237
|
+
"description": "Surface configuration problems and missing permissions.",
|
|
238
|
+
"category": "Insights",
|
|
239
|
+
"availability": "planned"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "helyx.youtube",
|
|
243
|
+
"package": "@helyx/module-youtube",
|
|
244
|
+
"version": null,
|
|
245
|
+
"name": "YouTube alerts",
|
|
246
|
+
"description": "Post when selected channels publish or go live.",
|
|
247
|
+
"category": "Integrations",
|
|
248
|
+
"availability": "planned"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"id": "helyx.twitch",
|
|
252
|
+
"package": "@helyx/module-twitch",
|
|
253
|
+
"version": null,
|
|
254
|
+
"name": "Twitch alerts",
|
|
255
|
+
"description": "Announce selected live streams.",
|
|
256
|
+
"category": "Integrations",
|
|
257
|
+
"availability": "planned"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "helyx.rss",
|
|
261
|
+
"package": "@helyx/module-rss",
|
|
262
|
+
"version": null,
|
|
263
|
+
"name": "RSS feeds",
|
|
264
|
+
"description": "Publish updates from selected feeds.",
|
|
265
|
+
"category": "Integrations",
|
|
266
|
+
"availability": "planned"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"id": "helyx.github",
|
|
270
|
+
"package": "@helyx/module-github",
|
|
271
|
+
"version": null,
|
|
272
|
+
"name": "GitHub updates",
|
|
273
|
+
"description": "Follow releases and repository activity.",
|
|
274
|
+
"category": "Integrations",
|
|
275
|
+
"availability": "planned"
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Required Discord and database configuration.
|
|
2
|
+
DATABASE_URL=postgresql://database:5432/helyx
|
|
3
|
+
DATABASE_SSL=disable
|
|
4
|
+
PGUSER=helyx
|
|
5
|
+
PGPASSWORD=replace-with-a-strong-password
|
|
6
|
+
DISCORD_TOKEN=replace-me
|
|
7
|
+
DISCORD_CLIENT_ID=12345678901234567
|
|
8
|
+
|
|
9
|
+
# Generated module selection. Keep this file under version control.
|
|
10
|
+
HELYX_MODULE_CATALOGUE_PATH=./helyx.modules.json
|
|
11
|
+
|
|
12
|
+
# Optional explicit additions must still be official reviewed packages.
|
|
13
|
+
# HELYX_MODULE_PACKAGES=
|
|
14
|
+
# DISCORD_INTERNAL_GUILD_ID=12345678901234567
|
|
15
|
+
|
|
16
|
+
# Optional private dashboard control endpoint. Configure both values together.
|
|
17
|
+
# HELYX_BOT_CONTROL_SECRET=replace-with-a-base64-encoded-32-byte-key
|
|
18
|
+
# HELYX_BOT_CONTROL_PORT=3001
|
|
19
|
+
|
|
20
|
+
LOG_LEVEL=info
|
|
21
|
+
NODE_ENV=production
|
|
22
|
+
|
|
23
|
+
# Used only by the included Docker Compose database.
|
|
24
|
+
POSTGRES_DB=helyx
|
|
25
|
+
POSTGRES_USER=helyx
|
|
26
|
+
POSTGRES_PASSWORD=replace-with-a-strong-password
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM node:24-bookworm-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY package.json package-lock.json ./
|
|
6
|
+
RUN npm ci --omit=dev
|
|
7
|
+
|
|
8
|
+
COPY helyx.modules.json ./
|
|
9
|
+
|
|
10
|
+
ENV HELYX_MODULE_CATALOGUE_PATH=/app/helyx.modules.json
|
|
11
|
+
ENV NODE_ENV=production
|
|
12
|
+
|
|
13
|
+
USER node
|
|
14
|
+
|
|
15
|
+
CMD ["npm", "start"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Helyx Self-Hosted starter
|
|
2
|
+
|
|
3
|
+
This private starter was generated from the reviewed Helyx self-host catalogue. It contains deployment configuration and exact npm dependencies, not Helyx source code or credentials.
|
|
4
|
+
|
|
5
|
+
## Selected modules
|
|
6
|
+
|
|
7
|
+
{{SELECTED_MODULES}}
|
|
8
|
+
|
|
9
|
+
Package installation and module activation are separate. Installed feature modules start disabled for every Discord server. Enable and configure them only after the bot is running.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Node.js `>=24 <27` and npm `>=11`, or Docker with Compose support
|
|
14
|
+
- A Discord application and bot token
|
|
15
|
+
- PostgreSQL 17 or another currently supported PostgreSQL release
|
|
16
|
+
- A safe backup location for the database and this project's lockfile
|
|
17
|
+
|
|
18
|
+
## Local setup
|
|
19
|
+
|
|
20
|
+
1. In the Discord Developer Portal, create or select an application and add its bot user.
|
|
21
|
+
2. Copy the application ID into `DISCORD_CLIENT_ID`. Generate a bot token and copy it into `DISCORD_TOKEN`; never place that token in Discord, Git, screenshots or support messages.
|
|
22
|
+
3. Install the bot using both the `bot` and `applications.commands` scopes. Review the permissions required by your selected modules and grant only what you intend; administrator access is convenient but not required by Helyx core.
|
|
23
|
+
4. Copy `.env.example` to `.env` and replace every placeholder credential. Do not commit `.env`.
|
|
24
|
+
5. For a non-Compose PostgreSQL server, replace `DATABASE_URL` and set `DATABASE_SSL=require` or `verify-full` as required by the provider. `PGUSER`, `PGPASSWORD` and `POSTGRES_*` are for the included Compose database only.
|
|
25
|
+
6. Run `npm ci`.
|
|
26
|
+
7. Run `npm run doctor` to validate the environment, database and selected packages without signing in to Discord or changing the database.
|
|
27
|
+
8. Run `npm start`.
|
|
28
|
+
|
|
29
|
+
`npm start` validates the environment, connects to PostgreSQL, applies core and selected-module migrations safely, validates installed modules, synchronises Discord commands and then reports readiness. Do not run a separate migration command before every start. `npm run migrate` is available when you deliberately need to apply core migrations without starting Discord.
|
|
30
|
+
|
|
31
|
+
Command registration is global or guild-scoped according to each reviewed command contribution. Discord may take time to refresh global commands. Installing a package does not enable its feature for a server; run `/helyx` in the server after startup to enable modules and configure their settings. Use `/permissions` when module or command access policies need to be changed.
|
|
32
|
+
|
|
33
|
+
## Docker Compose
|
|
34
|
+
|
|
35
|
+
1. Copy `.env.example` to `.env` and replace all placeholder credentials.
|
|
36
|
+
2. Keep the included internal `DATABASE_URL` and `DATABASE_SSL=disable` only when using the Compose database service on its private network.
|
|
37
|
+
3. Run `docker compose up --build -d`.
|
|
38
|
+
4. Review startup with `docker compose logs -f bot`.
|
|
39
|
+
5. Stop services with `docker compose down`. Do not add `--volumes` unless you deliberately intend to delete the database volume.
|
|
40
|
+
|
|
41
|
+
## Backups and upgrades
|
|
42
|
+
|
|
43
|
+
- Back up PostgreSQL and the complete project directory before an upgrade.
|
|
44
|
+
- Retain `package-lock.json`; use `npm ci` for repeatable installation.
|
|
45
|
+
- Do not edit dependency versions or `helyx.modules.json` independently.
|
|
46
|
+
- Review Helyx release notes before changing exact dependency versions. There is no automatic updater in the first self-host release.
|
|
47
|
+
- Removing a package does not automatically delete retained module configuration or data.
|
|
48
|
+
- Roll back application files and restore a compatible database backup if an upgrade cannot be completed safely.
|
|
49
|
+
|
|
50
|
+
## Adding or removing modules
|
|
51
|
+
|
|
52
|
+
Use a newly generated manifest or the supported Helyx self-host command once module-change commands are released. A package being present does not enable it for a Discord server. Unknown packages, Git URLs, file paths and unreviewed registries are not supported.
|
|
53
|
+
|
|
54
|
+
## Support and security
|
|
55
|
+
|
|
56
|
+
Use the official Helyx support route for ordinary help. Report suspected vulnerabilities privately through the security contact published by Helyx; do not include bot tokens, database credentials or private server content in a report.
|