@helyx/cli 1.6.0 → 1.7.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/CHANGELOG.md +29 -0
- package/dist/self-host/manifest.d.ts +13 -1
- package/dist/self-host/manifest.js +12 -4
- package/dist/self-host/project-generator.d.ts +2 -0
- package/dist/self-host/project-generator.js +78 -139
- package/dist/setup.js +4 -5
- package/package.json +2 -2
- package/self-host-catalogue.json +3 -3
- package/templates/self-host/.env.example +29 -130
- package/templates/self-host/README.md +6 -6
- package/templates/self-host/env-builder.mjs +164 -74
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add public `/proc` process information for hosted and self-hosted bots, replacing
|
|
8
|
+
the private `/dev proc` path. Keep the reply private and independent of database
|
|
9
|
+
availability.
|
|
10
|
+
|
|
11
|
+
Add a PebbleHost starter profile with direct Node CLI scripts, flat starter ZIPs,
|
|
12
|
+
compact quoted environment examples and matching guided environment builder
|
|
13
|
+
output. Preserve existing custom settings and encryption keys when rebuilding
|
|
14
|
+
an environment file, and recognize quoted placeholders during setup.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @helyx/bot@1.6.0
|
|
20
|
+
|
|
21
|
+
## 1.6.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Remove unused internal development, status reporting, OAuth, private control,
|
|
26
|
+
advanced Cloud Connect and Auto Moderation tuning examples from the self-host
|
|
27
|
+
environment template and portable documentation. The environment builder also
|
|
28
|
+
omits these examples from older templates while preserving existing active
|
|
29
|
+
custom settings. Keep Discord intent settings, basic Cloud Connect and data
|
|
30
|
+
encryption configuration; runtime defaults and scanning behavior are unchanged.
|
|
31
|
+
|
|
3
32
|
## 1.6.0
|
|
4
33
|
|
|
5
34
|
### Minor Changes
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type SelfHostEnvironmentBuilder } from "./environment-builders.js";
|
|
3
3
|
import { type SelfHostCatalogue, type SelfHostDatabaseEngine } from "./catalogue.js";
|
|
4
|
+
export declare const selfHostProviderSchema: z.ZodEnum<{
|
|
5
|
+
standard: "standard";
|
|
6
|
+
pebblehost: "pebblehost";
|
|
7
|
+
}>;
|
|
4
8
|
export declare const selfHostBuildManifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
5
9
|
schemaVersion: z.ZodLiteral<1>;
|
|
6
10
|
environmentBuilder: z.ZodOptional<z.ZodEnum<{
|
|
@@ -10,6 +14,10 @@ export declare const selfHostBuildManifestSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
10
14
|
unix: "unix";
|
|
11
15
|
all: "all";
|
|
12
16
|
}>>;
|
|
17
|
+
provider: z.ZodOptional<z.ZodEnum<{
|
|
18
|
+
standard: "standard";
|
|
19
|
+
pebblehost: "pebblehost";
|
|
20
|
+
}>>;
|
|
13
21
|
catalogueVersion: z.ZodNumber;
|
|
14
22
|
core: z.ZodObject<{
|
|
15
23
|
package: z.ZodString;
|
|
@@ -33,6 +41,10 @@ export declare const selfHostBuildManifestSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
33
41
|
unix: "unix";
|
|
34
42
|
all: "all";
|
|
35
43
|
}>>;
|
|
44
|
+
provider: z.ZodOptional<z.ZodEnum<{
|
|
45
|
+
standard: "standard";
|
|
46
|
+
pebblehost: "pebblehost";
|
|
47
|
+
}>>;
|
|
36
48
|
databaseEngine: z.ZodOptional<z.ZodEnum<{
|
|
37
49
|
postgresql: "postgresql";
|
|
38
50
|
mysql: "mysql";
|
|
@@ -67,6 +79,6 @@ export declare const selfHostBuildManifestSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
67
79
|
}, z.core.$strict>>;
|
|
68
80
|
}, z.core.$strict>], "schemaVersion">;
|
|
69
81
|
export type SelfHostBuildManifest = z.infer<typeof selfHostBuildManifestSchema>;
|
|
70
|
-
export declare function createSelfHostBuildManifest(catalogue: SelfHostCatalogue, selectedModuleIds: readonly string[], databaseEngine?: SelfHostDatabaseEngine, environmentBuilder?: SelfHostEnvironmentBuilder): SelfHostBuildManifest;
|
|
82
|
+
export declare function createSelfHostBuildManifest(catalogue: SelfHostCatalogue, selectedModuleIds: readonly string[], databaseEngine?: SelfHostDatabaseEngine, environmentBuilder?: SelfHostEnvironmentBuilder, provider?: z.infer<typeof selfHostProviderSchema>): SelfHostBuildManifest;
|
|
71
83
|
export declare function parseSelfHostBuildManifest(input: unknown, catalogue: SelfHostCatalogue): SelfHostBuildManifest;
|
|
72
84
|
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -11,6 +11,7 @@ const officialModulePackage = z
|
|
|
11
11
|
.string()
|
|
12
12
|
.regex(/^@helyx\/module-[a-z0-9][a-z0-9-]*$/u);
|
|
13
13
|
const moduleId = z.string().regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u);
|
|
14
|
+
export const selfHostProviderSchema = z.enum(["standard", "pebblehost"]);
|
|
14
15
|
const buildComponentSchema = z
|
|
15
16
|
.object({ package: officialPackage, version: exactVersion })
|
|
16
17
|
.strict();
|
|
@@ -25,6 +26,7 @@ const selfHostBuildManifestV1Schema = z
|
|
|
25
26
|
.object({
|
|
26
27
|
schemaVersion: z.literal(1),
|
|
27
28
|
environmentBuilder: selfHostEnvironmentBuilderSchema.optional(),
|
|
29
|
+
provider: selfHostProviderSchema.optional(),
|
|
28
30
|
catalogueVersion: z.number().int().positive(),
|
|
29
31
|
core: buildComponentSchema,
|
|
30
32
|
generator: buildComponentSchema,
|
|
@@ -51,6 +53,7 @@ const selfHostBuildManifestV2Schema = z
|
|
|
51
53
|
.object({
|
|
52
54
|
schemaVersion: z.literal(2),
|
|
53
55
|
environmentBuilder: selfHostEnvironmentBuilderSchema.optional(),
|
|
56
|
+
provider: selfHostProviderSchema.optional(),
|
|
54
57
|
databaseEngine: selfHostDatabaseEngineSchema.optional(),
|
|
55
58
|
catalogueVersion: z.number().int().positive(),
|
|
56
59
|
core: buildComponentSchema,
|
|
@@ -85,16 +88,17 @@ export const selfHostBuildManifestSchema = z
|
|
|
85
88
|
packages.add(module.package);
|
|
86
89
|
}
|
|
87
90
|
});
|
|
88
|
-
export function createSelfHostBuildManifest(catalogue, selectedModuleIds, databaseEngine = "postgresql", environmentBuilder = "none") {
|
|
91
|
+
export function createSelfHostBuildManifest(catalogue, selectedModuleIds, databaseEngine = "postgresql", environmentBuilder = "none", provider = "standard") {
|
|
89
92
|
selfHostDatabaseEngineSchema.parse(databaseEngine);
|
|
90
93
|
selfHostEnvironmentBuilderSchema.parse(environmentBuilder);
|
|
94
|
+
selfHostProviderSchema.parse(provider);
|
|
91
95
|
assertPublishedComponent(catalogue.core, "Helyx runtime");
|
|
92
96
|
assertPublishedComponent(catalogue.generator, "Helyx generator");
|
|
93
97
|
if (new Set(selectedModuleIds).size !== selectedModuleIds.length) {
|
|
94
98
|
throw new Error("Self-host selection contains duplicate module IDs");
|
|
95
99
|
}
|
|
96
100
|
if (catalogue.schemaVersion === 2) {
|
|
97
|
-
return createSelfHostBuildManifestV2(catalogue, selectedModuleIds, databaseEngine, environmentBuilder);
|
|
101
|
+
return createSelfHostBuildManifestV2(catalogue, selectedModuleIds, databaseEngine, environmentBuilder, provider);
|
|
98
102
|
}
|
|
99
103
|
if (databaseEngine !== "postgresql") {
|
|
100
104
|
throw new Error("Legacy self-host catalogue supports PostgreSQL only");
|
|
@@ -119,6 +123,7 @@ export function createSelfHostBuildManifest(catalogue, selectedModuleIds, databa
|
|
|
119
123
|
});
|
|
120
124
|
return {
|
|
121
125
|
schemaVersion: 1,
|
|
126
|
+
...(provider !== "standard" ? { provider } : {}),
|
|
122
127
|
...(environmentBuilder !== "none" ? { environmentBuilder } : {}),
|
|
123
128
|
catalogueVersion: catalogue.catalogueVersion,
|
|
124
129
|
core: {
|
|
@@ -132,7 +137,7 @@ export function createSelfHostBuildManifest(catalogue, selectedModuleIds, databa
|
|
|
132
137
|
modules,
|
|
133
138
|
};
|
|
134
139
|
}
|
|
135
|
-
function createSelfHostBuildManifestV2(catalogue, selectedModuleIds, databaseEngine, environmentBuilder = "none") {
|
|
140
|
+
function createSelfHostBuildManifestV2(catalogue, selectedModuleIds, databaseEngine, environmentBuilder = "none", provider = "standard") {
|
|
136
141
|
const byId = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
137
142
|
const explicitlySelected = new Set(selectedModuleIds);
|
|
138
143
|
const modules = [];
|
|
@@ -194,6 +199,7 @@ function createSelfHostBuildManifestV2(catalogue, selectedModuleIds, databaseEng
|
|
|
194
199
|
include(id);
|
|
195
200
|
return {
|
|
196
201
|
schemaVersion: 2,
|
|
202
|
+
...(provider !== "standard" ? { provider } : {}),
|
|
197
203
|
...(environmentBuilder !== "none" ? { environmentBuilder } : {}),
|
|
198
204
|
...(databaseEngine === "mysql" ? { databaseEngine } : {}),
|
|
199
205
|
catalogueVersion: catalogue.catalogueVersion,
|
|
@@ -224,9 +230,10 @@ export function parseSelfHostBuildManifest(input, catalogue) {
|
|
|
224
230
|
const selectedModuleIds = manifest.modules
|
|
225
231
|
.filter(({ moduleKind }) => moduleKind === "customer_feature")
|
|
226
232
|
.map(({ id }) => id);
|
|
227
|
-
const expected = createSelfHostBuildManifestV2(catalogue, selectedModuleIds, manifest.databaseEngine ?? "postgresql", manifest.environmentBuilder ?? "none");
|
|
233
|
+
const expected = createSelfHostBuildManifestV2(catalogue, selectedModuleIds, manifest.databaseEngine ?? "postgresql", manifest.environmentBuilder ?? "none", manifest.provider ?? "standard");
|
|
228
234
|
if (!isDeepStrictEqual({
|
|
229
235
|
...manifest,
|
|
236
|
+
provider: manifest.provider === "standard" ? undefined : manifest.provider,
|
|
230
237
|
environmentBuilder: manifest.environmentBuilder === "none"
|
|
231
238
|
? undefined
|
|
232
239
|
: manifest.environmentBuilder,
|
|
@@ -235,6 +242,7 @@ export function parseSelfHostBuildManifest(input, catalogue) {
|
|
|
235
242
|
: manifest.databaseEngine,
|
|
236
243
|
}, {
|
|
237
244
|
...expected,
|
|
245
|
+
provider: expected.provider,
|
|
238
246
|
databaseEngine: expected.databaseEngine,
|
|
239
247
|
environmentBuilder: expected.environmentBuilder,
|
|
240
248
|
})) {
|
|
@@ -22,6 +22,8 @@ type HostedOnlyBotEnvironment = "HELYX_PRIVACY_ENABLED" | "HELYX_PRIVACY_EXECUTO
|
|
|
22
22
|
type EnvironmentExample = Record<Exclude<keyof BotEnvironmentInput, HostedOnlyBotEnvironment>, {
|
|
23
23
|
value: string;
|
|
24
24
|
optional: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
omit: true;
|
|
25
27
|
}>;
|
|
26
28
|
export declare const selfHostEnvironmentContract: EnvironmentExample;
|
|
27
29
|
export {};
|
|
@@ -63,6 +63,9 @@ async function writeGeneratedFiles(targetDirectory, manifest, catalogue, databas
|
|
|
63
63
|
const databaseEngine = "databaseEngine" in manifest
|
|
64
64
|
? (manifest.databaseEngine ?? "postgresql")
|
|
65
65
|
: "postgresql";
|
|
66
|
+
const databaseSsl = manifest.provider === "pebblehost" || databaseEngine === "postgresql"
|
|
67
|
+
? "disable"
|
|
68
|
+
: "require";
|
|
66
69
|
const dependencyEntries = [
|
|
67
70
|
[manifest.core.package, manifest.core.version],
|
|
68
71
|
[manifest.generator.package, manifest.generator.version],
|
|
@@ -90,6 +93,12 @@ async function writeGeneratedFiles(targetDirectory, manifest, catalogue, databas
|
|
|
90
93
|
},
|
|
91
94
|
dependencies,
|
|
92
95
|
};
|
|
96
|
+
if (manifest.provider === "pebblehost") {
|
|
97
|
+
const cli = "node node_modules/@helyx/cli/dist/cli.js";
|
|
98
|
+
for (const key of Object.keys(packageJson.scripts)) {
|
|
99
|
+
packageJson.scripts[key] = packageJson.scripts[key].replace(/^helyx /u, `${cli} `);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
93
102
|
const moduleCatalogue = {
|
|
94
103
|
schemaVersion: 1,
|
|
95
104
|
packages: manifest.modules.map((module) => module.package),
|
|
@@ -105,12 +114,12 @@ async function writeGeneratedFiles(targetDirectory, manifest, catalogue, databas
|
|
|
105
114
|
? mysqlComposeTemplateUrl()
|
|
106
115
|
: templateUrl(fileName), "utf8");
|
|
107
116
|
const rendered = fileName === ".env.example"
|
|
108
|
-
? renderEnvironmentExample(template, databaseEngine)
|
|
117
|
+
? renderEnvironmentExample(template, databaseEngine, databaseSsl)
|
|
109
118
|
: fileName === "README.md"
|
|
110
119
|
? renderReadme(template, manifest, catalogue, databaseEngine)
|
|
111
120
|
: template;
|
|
112
121
|
if (fileName === ".env.example")
|
|
113
|
-
validateEnvironmentTemplate(rendered, databaseEngine);
|
|
122
|
+
validateEnvironmentTemplate(rendered, databaseEngine, databaseSsl);
|
|
114
123
|
const content = fileName.endsWith(".sh")
|
|
115
124
|
? rendered.replaceAll("\r\n", "\n")
|
|
116
125
|
: normalizeCrLf(rendered);
|
|
@@ -120,15 +129,13 @@ async function writeGeneratedFiles(targetDirectory, manifest, catalogue, databas
|
|
|
120
129
|
});
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
|
-
function renderEnvironmentExample(template, databaseEngine) {
|
|
124
|
-
if (databaseEngine === "postgresql")
|
|
125
|
-
return template;
|
|
132
|
+
function renderEnvironmentExample(template, databaseEngine, databaseSsl) {
|
|
126
133
|
return template
|
|
127
|
-
.
|
|
128
|
-
.replace("
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.replace("DATABASE_SSL=disable",
|
|
134
|
+
.replace("DATABASE_ENGINE='postgresql'", `DATABASE_ENGINE='${databaseEngine}'`)
|
|
135
|
+
.replace("DATABASE_URL='postgresql://database:5432/helyx'", databaseEngine === "mysql"
|
|
136
|
+
? "DATABASE_URL='mysql://user:password@host:3306/helyx'"
|
|
137
|
+
: "DATABASE_URL='postgresql://database:5432/helyx'")
|
|
138
|
+
.replace("DATABASE_SSL='disable'", `DATABASE_SSL='${databaseSsl}'`);
|
|
132
139
|
}
|
|
133
140
|
function renderReadme(template, manifest, catalogue, databaseEngine) {
|
|
134
141
|
const modulesById = new Map(catalogue.modules.map((module) => [module.id, module]));
|
|
@@ -146,11 +153,16 @@ function renderReadme(template, manifest, catalogue, databaseEngine) {
|
|
|
146
153
|
let rendered = template
|
|
147
154
|
.replaceAll("\r\n", "\n")
|
|
148
155
|
.replace("{{SELECTED_MODULES}}", selected);
|
|
156
|
+
if (manifest.provider === "pebblehost") {
|
|
157
|
+
rendered =
|
|
158
|
+
"# PebbleHost setup\n\nThis starter uses the PebbleHost npm scripts. Extract the ZIP directly into the server root, select Node.js 24.7.0 and set Bot Start File to `package.json`. Create your `.env` at https://helyx.gg/env-builder/?provider=pebblehost and upload it before pressing Start. Follow the complete guide at https://docs.helyx.gg/self-host/providers/pebblehost/. PebbleHost's application console cannot run npm installation commands or the terminal Env Builder.\n\n" +
|
|
159
|
+
rendered;
|
|
160
|
+
}
|
|
149
161
|
if (databaseEngine === "mysql") {
|
|
150
162
|
rendered = rendered
|
|
151
163
|
.replace("3. Edit `.env`, replace every placeholder, set `DATABASE_ENGINE` to `postgresql` or `mysql`, and set the matching `DATABASE_URL`. Omitting `DATABASE_ENGINE` selects PostgreSQL.", "3. Edit `.env`, replace every placeholder, keep `DATABASE_ENGINE=mysql` and `HELYX_SHARD_MODE=off`, and set `DATABASE_URL` to the MySQL-compatible URL supplied by your provider. Percent-encode special characters in URL credentials.")
|
|
152
164
|
.replace("Use this path when Docker with Compose support is available. The included configuration runs both Helyx and PostgreSQL 17.", "Use this path when Docker with Compose support is available. The included configuration runs only Helyx; it connects to your existing MySQL-compatible database and does not create a second database service.")
|
|
153
|
-
.replace("
|
|
165
|
+
.replace("1. Run `npm ci`, then `npm run envbuilder`. Choose **bundled** Docker PostgreSQL when asked for the database source. The wizard adds only the Docker credentials needed for that choice and creates the encryption key.\n2. Enter your Discord credentials and server ID, then save `.env`. The bot itself will run in Docker.\n3. Keep `DATABASE_ENGINE=postgresql` with the supplied internal `DATABASE_URL` and `DATABASE_SSL=disable`.\n4. Run `docker compose up --build -d`.\n5. Review startup with `docker compose logs -f bot`.\n6. Stop services with `docker compose down`. Do not add `--volumes` unless you deliberately intend to delete the database volume.", "1. Run `npm ci`, then `npm run envbuilder` to enter your Discord and external MySQL connection details and save `.env`.\n2. Keep `DATABASE_ENGINE=mysql` and supply your provider's reachable `DATABASE_URL` and required `DATABASE_SSL` setting.\n3. Run `docker compose up --build -d`.\n4. Review startup with `docker compose logs -f bot`.\n5. Stop the bot with `docker compose down`. Your external database is not managed by this Compose file.");
|
|
154
166
|
}
|
|
155
167
|
return rendered;
|
|
156
168
|
}
|
|
@@ -174,20 +186,26 @@ function normalizeCrLf(value) {
|
|
|
174
186
|
.replaceAll("\r", "\n")
|
|
175
187
|
.replaceAll("\n", "\r\n");
|
|
176
188
|
}
|
|
177
|
-
function validateEnvironmentTemplate(template, databaseEngine) {
|
|
189
|
+
function validateEnvironmentTemplate(template, databaseEngine, databaseSsl) {
|
|
178
190
|
const lines = new Set(template
|
|
179
191
|
.replaceAll("\r\n", "\n")
|
|
180
192
|
.split("\n")
|
|
181
193
|
.map((line) => line.trim()));
|
|
182
194
|
for (const [key, definition] of Object.entries(selfHostEnvironmentContract)) {
|
|
195
|
+
if ("omit" in definition) {
|
|
196
|
+
const setting = new RegExp(`^(?:#\\s*)?${key}\\s*=`, "u");
|
|
197
|
+
if ([...lines].some((line) => setting.test(line)))
|
|
198
|
+
throw new Error(`Self-host environment template contains an omitted setting: ${key}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
183
201
|
const value = key === "DATABASE_ENGINE"
|
|
184
202
|
? databaseEngine
|
|
185
203
|
: key === "DATABASE_URL" && databaseEngine === "mysql"
|
|
186
204
|
? "mysql://user:password@host:3306/helyx"
|
|
187
|
-
: key === "DATABASE_SSL"
|
|
188
|
-
?
|
|
205
|
+
: key === "DATABASE_SSL"
|
|
206
|
+
? databaseSsl
|
|
189
207
|
: definition.value;
|
|
190
|
-
const expected = `${definition.optional ? "# " : ""}${key}
|
|
208
|
+
const expected = `${definition.optional ? "# " : ""}${key}='${value}'`;
|
|
191
209
|
if (!lines.has(expected)) {
|
|
192
210
|
throw new Error(`Self-host environment template does not match the bot contract: ${key}`);
|
|
193
211
|
}
|
|
@@ -327,146 +345,67 @@ export const selfHostEnvironmentContract = {
|
|
|
327
345
|
optional: false,
|
|
328
346
|
},
|
|
329
347
|
DATABASE_SSL: { value: "disable", optional: false },
|
|
330
|
-
HELYX_DATABASE_STARTUP_MODE: {
|
|
348
|
+
HELYX_DATABASE_STARTUP_MODE: { omit: true },
|
|
331
349
|
HELYX_RUNTIME_PROFILE: { value: "self-hosted", optional: false },
|
|
332
350
|
HELYX_DEPLOYMENT_ENVIRONMENT: { value: "self-hosted", optional: false },
|
|
333
351
|
DISCORD_TOKEN: { value: "replace-me", optional: false },
|
|
334
352
|
DISCORD_CLIENT_ID: { value: "12345678901234567", optional: false },
|
|
335
353
|
HELYX_MESSAGE_CONTENT_ENABLED: { value: "false", optional: false },
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
},
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
},
|
|
347
|
-
DISCORD_INTERNAL_GUILD_ID: {
|
|
348
|
-
value: "12345678901234567",
|
|
349
|
-
optional: true,
|
|
350
|
-
},
|
|
351
|
-
APPROVED_ROLES: {
|
|
352
|
-
value: "12345678901234567,12345678901234568",
|
|
353
|
-
optional: true,
|
|
354
|
-
},
|
|
355
|
-
HELYX_MODULE_PACKAGES: {
|
|
356
|
-
value: "",
|
|
357
|
-
optional: true,
|
|
358
|
-
},
|
|
354
|
+
// Advanced self-host options use runtime defaults; internal/hosted settings
|
|
355
|
+
// are deliberately absent from the customer example.
|
|
356
|
+
HELYX_AUTOMOD_EMBEDDED_WORKERS: { omit: true },
|
|
357
|
+
HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS: { omit: true },
|
|
358
|
+
HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES: { omit: true },
|
|
359
|
+
HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD: { omit: true },
|
|
360
|
+
HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS: { omit: true },
|
|
361
|
+
DISCORD_INTERNAL_GUILD_ID: { omit: true },
|
|
362
|
+
APPROVED_ROLES: { omit: true },
|
|
363
|
+
HELYX_MODULE_PACKAGES: { omit: true },
|
|
359
364
|
HELYX_MODULE_CATALOGUE_PATH: {
|
|
360
365
|
value: "./helyx.modules.json",
|
|
361
366
|
optional: false,
|
|
362
367
|
},
|
|
363
|
-
HELYX_DASHBOARD_PUBLIC_URL: {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
optional: true,
|
|
370
|
-
},
|
|
371
|
-
HELYX_BOT_CONTROL_PORT: { value: "3001", optional: true },
|
|
372
|
-
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: {
|
|
373
|
-
value: "900",
|
|
374
|
-
optional: true,
|
|
375
|
-
},
|
|
376
|
-
HELYX_HOSTED_MODERATION_ENABLED: { value: "false", optional: false },
|
|
377
|
-
HELYX_GUILD_COMMAND_WRITES_ENABLED: { value: "false", optional: false },
|
|
368
|
+
HELYX_DASHBOARD_PUBLIC_URL: { omit: true },
|
|
369
|
+
HELYX_BOT_CONTROL_SECRET: { omit: true },
|
|
370
|
+
HELYX_BOT_CONTROL_PORT: { omit: true },
|
|
371
|
+
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: { omit: true },
|
|
372
|
+
HELYX_HOSTED_MODERATION_ENABLED: { omit: true },
|
|
373
|
+
HELYX_GUILD_COMMAND_WRITES_ENABLED: { value: "true", optional: false },
|
|
378
374
|
HELYX_GUILD_COMMAND_WRITE_SCOPE: { value: "allowlist", optional: false },
|
|
379
|
-
HELYX_GUILD_COMMAND_RELEASE_ID: {
|
|
380
|
-
HELYX_GLOBAL_COMMAND_MODE: { value: "
|
|
381
|
-
HELYX_GUILD_COMMAND_WRITE_GUILD_IDS: { value: "", optional:
|
|
375
|
+
HELYX_GUILD_COMMAND_RELEASE_ID: { omit: true },
|
|
376
|
+
HELYX_GLOBAL_COMMAND_MODE: { value: "platform-only", optional: false },
|
|
377
|
+
HELYX_GUILD_COMMAND_WRITE_GUILD_IDS: { value: "", optional: false },
|
|
382
378
|
HELYX_GUILD_MEMBERS_ENABLED: { value: "false", optional: false },
|
|
383
379
|
HELYX_CLOUD_CONNECT_ENABLED: { value: "true", optional: false },
|
|
384
|
-
HELYX_CLOUD_ENROLMENT_TOKEN: {
|
|
385
|
-
|
|
386
|
-
optional: true,
|
|
387
|
-
},
|
|
388
|
-
HELYX_CLOUD_ENROLMENT_FILE: {
|
|
389
|
-
value: "./helyx.cloud-enrolment.json",
|
|
390
|
-
optional: true,
|
|
391
|
-
},
|
|
380
|
+
HELYX_CLOUD_ENROLMENT_TOKEN: { omit: true },
|
|
381
|
+
HELYX_CLOUD_ENROLMENT_FILE: { omit: true },
|
|
392
382
|
HELYX_DATA_ENCRYPTION_KEY: {
|
|
393
383
|
value: "replace-with-a-base64-encoded-32-byte-key",
|
|
394
|
-
optional:
|
|
395
|
-
},
|
|
396
|
-
HELYX_CLOUD_API_URL: {
|
|
397
|
-
value: "https://api.helyx.gg",
|
|
398
|
-
optional: true,
|
|
399
|
-
},
|
|
400
|
-
HELYX_OAUTH_ENABLED: { value: "false", optional: false },
|
|
401
|
-
HELYX_OAUTH_INTERNAL_SECRET: {
|
|
402
|
-
value: "replace-with-a-base64-encoded-32-byte-key",
|
|
403
|
-
optional: true,
|
|
404
|
-
},
|
|
405
|
-
HELYX_CLOUD_ENDPOINT: {
|
|
406
|
-
value: "wss://connect.helyx.gg/v1/connect",
|
|
407
|
-
optional: true,
|
|
408
|
-
},
|
|
409
|
-
HELYX_OWNER_USER_IDS: {
|
|
410
|
-
value: "12345678901234567",
|
|
411
|
-
optional: true,
|
|
384
|
+
optional: false,
|
|
412
385
|
},
|
|
386
|
+
HELYX_CLOUD_API_URL: { omit: true },
|
|
387
|
+
HELYX_OAUTH_ENABLED: { omit: true },
|
|
388
|
+
HELYX_OAUTH_INTERNAL_SECRET: { omit: true },
|
|
389
|
+
HELYX_CLOUD_ENDPOINT: { omit: true },
|
|
390
|
+
HELYX_OWNER_USER_IDS: { omit: true },
|
|
413
391
|
HELYX_SHARD_MODE: { value: "off", optional: false },
|
|
414
|
-
HELYX_SHARD_COUNT: {
|
|
415
|
-
HELYX_SHARD_SCOPE: {
|
|
416
|
-
HELYX_SHARD_WORKERS_PER_REPLICA: {
|
|
417
|
-
HELYX_SHARD_REDIS_URL: {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
},
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
},
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
},
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
optional: true,
|
|
432
|
-
},
|
|
433
|
-
HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX: {
|
|
434
|
-
value: "2",
|
|
435
|
-
optional: true,
|
|
436
|
-
},
|
|
437
|
-
HELYX_SHARD_WORKER_DATABASE_POOL_MAX: {
|
|
438
|
-
value: "4",
|
|
439
|
-
optional: true,
|
|
440
|
-
},
|
|
441
|
-
HELYX_CLOUD_AUDIT_RETENTION_DAYS: {
|
|
442
|
-
value: "90",
|
|
443
|
-
optional: true,
|
|
444
|
-
},
|
|
445
|
-
HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: {
|
|
446
|
-
value: "365",
|
|
447
|
-
optional: true,
|
|
448
|
-
},
|
|
449
|
-
HELYX_STATUS_ENABLED: { value: "false", optional: false },
|
|
450
|
-
HELYX_STATUS_URL: {
|
|
451
|
-
value: "https://status.helyx.gg",
|
|
452
|
-
optional: true,
|
|
453
|
-
},
|
|
454
|
-
HELYX_STATUS_HEARTBEAT_SECRET: {
|
|
455
|
-
value: "replace-with-a-base64-encoded-32-byte-key",
|
|
456
|
-
optional: true,
|
|
457
|
-
},
|
|
458
|
-
HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: {
|
|
459
|
-
value: "60",
|
|
460
|
-
optional: true,
|
|
461
|
-
},
|
|
462
|
-
CF_ACCESS_CLIENT_ID: {
|
|
463
|
-
value: "replace-with-access-client-id",
|
|
464
|
-
optional: true,
|
|
465
|
-
},
|
|
466
|
-
CF_ACCESS_CLIENT_SECRET: {
|
|
467
|
-
value: "replace-with-access-client-secret",
|
|
468
|
-
optional: true,
|
|
469
|
-
},
|
|
392
|
+
HELYX_SHARD_COUNT: { omit: true },
|
|
393
|
+
HELYX_SHARD_SCOPE: { omit: true },
|
|
394
|
+
HELYX_SHARD_WORKERS_PER_REPLICA: { omit: true },
|
|
395
|
+
HELYX_SHARD_REDIS_URL: { omit: true },
|
|
396
|
+
HELYX_SHARD_REDIS_NAMESPACE: { omit: true },
|
|
397
|
+
HELYX_SHARD_GENERATION: { omit: true },
|
|
398
|
+
HELYX_SHARD_DATABASE_CONNECTION_BUDGET: { omit: true },
|
|
399
|
+
HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX: { omit: true },
|
|
400
|
+
HELYX_SHARD_WORKER_DATABASE_POOL_MAX: { omit: true },
|
|
401
|
+
HELYX_CLOUD_AUDIT_RETENTION_DAYS: { omit: true },
|
|
402
|
+
HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: { omit: true },
|
|
403
|
+
HELYX_STATUS_ENABLED: { omit: true },
|
|
404
|
+
HELYX_STATUS_URL: { omit: true },
|
|
405
|
+
HELYX_STATUS_HEARTBEAT_SECRET: { omit: true },
|
|
406
|
+
HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: { omit: true },
|
|
407
|
+
CF_ACCESS_CLIENT_ID: { omit: true },
|
|
408
|
+
CF_ACCESS_CLIENT_SECRET: { omit: true },
|
|
470
409
|
LOG_LEVEL: { value: "info", optional: false },
|
|
471
410
|
NODE_ENV: { value: "production", optional: false },
|
|
472
411
|
};
|
package/dist/setup.js
CHANGED
|
@@ -40,15 +40,14 @@ async function ensureDataEncryptionKey(environmentPath, createKey) {
|
|
|
40
40
|
const content = await readFile(environmentPath, "utf8");
|
|
41
41
|
const activeKeyPattern = new RegExp(`^[\\t ]*${dataEncryptionKeyName}[\\t ]*=[\\t ]*([^\\r\\n]*?)[\\t ]*(?=\\r?$)`, "mu");
|
|
42
42
|
const activeKey = activeKeyPattern.exec(content);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
activeKey[1] !== "" &&
|
|
46
|
-
activeKey[1] !== dataEncryptionKeyPlaceholder) {
|
|
43
|
+
const currentKey = parseEnv(content)[dataEncryptionKeyName];
|
|
44
|
+
if (currentKey && currentKey !== dataEncryptionKeyPlaceholder) {
|
|
47
45
|
await chmod(environmentPath, 0o600);
|
|
48
46
|
return false;
|
|
49
47
|
}
|
|
50
48
|
const key = createKey();
|
|
51
|
-
const
|
|
49
|
+
const quoted = activeKey?.[1]?.startsWith("'") || activeKey?.[1]?.startsWith('"');
|
|
50
|
+
const replacement = `${dataEncryptionKeyName}=${quoted ? `'${key}'` : key}`;
|
|
52
51
|
let updated;
|
|
53
52
|
if (activeKey) {
|
|
54
53
|
updated = content.replace(activeKeyPattern, replacement);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Command-line tools for running and generating self-hosted Helyx deployments.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"build": "tsc -b"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@helyx/bot": ">=1.
|
|
55
|
+
"@helyx/bot": ">=1.6.0 <2.0.0",
|
|
56
56
|
"@helyx/config": ">=1.6.0 <2.0.0",
|
|
57
57
|
"@helyx/database": ">=2.5.0 <3.0.0",
|
|
58
58
|
"semver": "7.8.5",
|
package/self-host-catalogue.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"catalogueVersion":
|
|
3
|
+
"catalogueVersion": 88,
|
|
4
4
|
"core": {
|
|
5
5
|
"package": "@helyx/bot",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.6.0",
|
|
7
7
|
"availability": "published"
|
|
8
8
|
},
|
|
9
9
|
"generator": {
|
|
10
10
|
"package": "@helyx/cli",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.7.0",
|
|
12
12
|
"command": "helyx",
|
|
13
13
|
"availability": "published"
|
|
14
14
|
},
|
|
@@ -1,134 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Helyx self-hosted environment
|
|
2
|
+
# Keep this file private. Retain the encryption key when updating.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
# Runtime
|
|
5
|
+
HELYX_RUNTIME_PROFILE='self-hosted'
|
|
6
|
+
HELYX_DEPLOYMENT_ENVIRONMENT='self-hosted'
|
|
7
|
+
NODE_ENV='production'
|
|
5
8
|
|
|
6
9
|
# Discord
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
HELYX_MESSAGE_CONTENT_ENABLED=false
|
|
12
|
-
# Enhanced Auto Moderation stays inside this bot process. One worker is the
|
|
13
|
-
# recommended default (range 1-4); increase only after measuring CPU and memory.
|
|
14
|
-
# Hosted scanning uses the separate Helyx Ecko service, not these settings.
|
|
15
|
-
HELYX_AUTOMOD_EMBEDDED_WORKERS=1
|
|
16
|
-
# Optional conservative queue overrides (hard ceilings: 5000 items, 64 MiB,
|
|
17
|
-
# 100 items per server). Defaults are used when these remain commented.
|
|
18
|
-
# HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS=5000
|
|
19
|
-
# HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES=67108864
|
|
20
|
-
# HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD=100
|
|
21
|
-
# HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS=300
|
|
22
|
-
# Enable for loaded member workflows such as Welcome, Access Control or Levels,
|
|
23
|
-
# with Server Members Intent also enabled in the Discord developer portal.
|
|
24
|
-
HELYX_GUILD_MEMBERS_ENABLED=false
|
|
25
|
-
# Database
|
|
26
|
-
# Docker: keep the URL. Direct Node.js/managed hosting: use your provider URL.
|
|
27
|
-
DATABASE_ENGINE=postgresql
|
|
28
|
-
DATABASE_URL=postgresql://database:5432/helyx
|
|
29
|
-
DATABASE_SSL=disable
|
|
30
|
-
# Self-host startup applies pending migrations automatically (the default).
|
|
31
|
-
# Hosted-only inspect mode is not a self-host setting; leave this commented.
|
|
32
|
-
# HELYX_DATABASE_STARTUP_MODE=apply
|
|
33
|
-
# Match your provider: disable=no TLS; require=TLS without certificate
|
|
34
|
-
# verification; verify-full=TLS with certificate verification.
|
|
35
|
-
|
|
36
|
-
# Included Docker database — both password values must match.
|
|
37
|
-
PGUSER=helyx
|
|
38
|
-
PGPASSWORD=replace-with-a-strong-password
|
|
39
|
-
POSTGRES_DB=helyx
|
|
40
|
-
POSTGRES_USER=helyx
|
|
41
|
-
POSTGRES_PASSWORD=replace-with-a-strong-password
|
|
42
|
-
|
|
43
|
-
# OPTIONAL EVERYDAY SETTINGS
|
|
44
|
-
|
|
45
|
-
# Ready for dashboard linking, but remains unpaired until you use a one-use code.
|
|
46
|
-
HELYX_CLOUD_CONNECT_ENABLED=true
|
|
47
|
-
# Public dashboard origin used in bot-generated report links.
|
|
48
|
-
# HELYX_DASHBOARD_PUBLIC_URL=https://app.helyx.gg
|
|
49
|
-
# HELYX_OWNER_USER_IDS=12345678901234567
|
|
50
|
-
|
|
51
|
-
# trace, debug, info, warn, error, fatal or silent
|
|
52
|
-
LOG_LEVEL=info
|
|
53
|
-
|
|
54
|
-
# Guild-scoped development commands only
|
|
55
|
-
# DISCORD_INTERNAL_GUILD_ID=12345678901234567
|
|
56
|
-
# APPROVED_ROLES=12345678901234567,12345678901234568
|
|
57
|
-
|
|
58
|
-
# SHARDING: OFF BY DEFAULT
|
|
59
|
-
|
|
60
|
-
# `off` is the compatible self-host default. Keep shard-only values commented
|
|
61
|
-
# in this mode; an empty value is still rejected as configured. MySQL must stay
|
|
62
|
-
# off. Advanced PostgreSQL self-hosting may use `auto` under one
|
|
63
|
-
# supervisor, or `fixed` with a deliberate total count. Local mode never needs
|
|
64
|
-
# Redis. Coordinated multi-host self-hosting is outside the first release.
|
|
65
|
-
HELYX_SHARD_MODE=off
|
|
66
|
-
# HELYX_SHARD_SCOPE=local
|
|
67
|
-
# HELYX_SHARD_COUNT=2
|
|
68
|
-
# HELYX_SHARD_WORKERS_PER_REPLICA=2
|
|
69
|
-
# HELYX_SHARD_REDIS_URL=rediss://private-redis.example:6379
|
|
70
|
-
# HELYX_SHARD_REDIS_NAMESPACE=helyx.self-host
|
|
71
|
-
# HELYX_SHARD_GENERATION=local-20260728
|
|
72
|
-
# HELYX_SHARD_DATABASE_CONNECTION_BUDGET=10
|
|
73
|
-
# HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX=2
|
|
74
|
-
# HELYX_SHARD_WORKER_DATABASE_POOL_MAX=4
|
|
75
|
-
# Redis URL and namespace are intentionally unsupported for the first
|
|
76
|
-
# self-hosted release: do not uncomment them for local auto or fixed mode.
|
|
77
|
-
|
|
78
|
-
# ADVANCED INTEGRATIONS — EDIT ONLY WHEN NEEDED
|
|
79
|
-
|
|
80
|
-
# Additional reviewed module packages
|
|
81
|
-
# HELYX_MODULE_PACKAGES=
|
|
82
|
-
|
|
83
|
-
# Private dashboard control endpoint — configure both together
|
|
84
|
-
# HELYX_BOT_CONTROL_SECRET=replace-with-a-base64-encoded-32-byte-key
|
|
85
|
-
# HELYX_BOT_CONTROL_PORT=3001
|
|
86
|
-
# HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS=900
|
|
87
|
-
HELYX_HOSTED_MODERATION_ENABLED=false
|
|
10
|
+
DISCORD_CLIENT_ID='12345678901234567'
|
|
11
|
+
DISCORD_TOKEN='replace-me'
|
|
12
|
+
HELYX_GUILD_MEMBERS_ENABLED='false'
|
|
13
|
+
HELYX_MESSAGE_CONTENT_ENABLED='false'
|
|
88
14
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# Generated module selection
|
|
110
|
-
HELYX_MODULE_CATALOGUE_PATH=./helyx.modules.json
|
|
111
|
-
|
|
112
|
-
# Generated privately by npm run setup. Required by Cloud Connect, Giveaways,
|
|
113
|
-
# and Invite Tracking; retain it unchanged across restarts and deployments.
|
|
114
|
-
# HELYX_DATA_ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key
|
|
115
|
-
|
|
116
|
-
# Official Cloud Connect endpoints
|
|
117
|
-
# HELYX_CLOUD_API_URL=https://api.helyx.gg
|
|
118
|
-
# HELYX_CLOUD_ENDPOINT=wss://connect.helyx.gg/v1/connect
|
|
119
|
-
|
|
120
|
-
# Security audit retention
|
|
121
|
-
# HELYX_CLOUD_AUDIT_RETENTION_DAYS=90
|
|
122
|
-
# HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS=365
|
|
123
|
-
|
|
124
|
-
# Optional public status reporting. Keep disabled unless this installation has
|
|
125
|
-
# its own approved Helyx status credentials.
|
|
126
|
-
HELYX_STATUS_ENABLED=false
|
|
127
|
-
# HELYX_STATUS_URL=https://status.helyx.gg
|
|
128
|
-
# HELYX_STATUS_HEARTBEAT_SECRET=replace-with-a-base64-encoded-32-byte-key
|
|
129
|
-
# HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS=60
|
|
130
|
-
# CF_ACCESS_CLIENT_ID=replace-with-access-client-id
|
|
131
|
-
# CF_ACCESS_CLIENT_SECRET=replace-with-access-client-secret
|
|
132
|
-
|
|
133
|
-
# Runtime mode
|
|
134
|
-
NODE_ENV=production
|
|
15
|
+
# Database
|
|
16
|
+
DATABASE_ENGINE='postgresql'
|
|
17
|
+
DATABASE_URL='postgresql://database:5432/helyx'
|
|
18
|
+
DATABASE_SSL='disable'
|
|
19
|
+
|
|
20
|
+
# Commands
|
|
21
|
+
HELYX_GUILD_COMMAND_WRITES_ENABLED='true'
|
|
22
|
+
HELYX_GUILD_COMMAND_WRITE_SCOPE='allowlist'
|
|
23
|
+
HELYX_GLOBAL_COMMAND_MODE='platform-only'
|
|
24
|
+
HELYX_GUILD_COMMAND_WRITE_GUILD_IDS=''
|
|
25
|
+
|
|
26
|
+
# Modules and dashboard
|
|
27
|
+
HELYX_MODULE_CATALOGUE_PATH='./helyx.modules.json'
|
|
28
|
+
HELYX_CLOUD_CONNECT_ENABLED='true'
|
|
29
|
+
HELYX_SHARD_MODE='off'
|
|
30
|
+
LOG_LEVEL='info'
|
|
31
|
+
|
|
32
|
+
# Keep this key unchanged across restarts and updates
|
|
33
|
+
HELYX_DATA_ENCRYPTION_KEY='replace-with-a-base64-encoded-32-byte-key'
|
|
@@ -76,8 +76,8 @@ Complete setup and credentials first. Before the start command below, run the
|
|
|
76
76
|
container doctor preflight that follows the numbered steps; a host-shell doctor
|
|
77
77
|
cannot normally resolve a container-only hostname such as `database`.
|
|
78
78
|
|
|
79
|
-
1. Run `npm ci`, then `npm run
|
|
80
|
-
2.
|
|
79
|
+
1. Run `npm ci`, then `npm run envbuilder`. Choose **bundled** Docker PostgreSQL when asked for the database source. The wizard adds only the Docker credentials needed for that choice and creates the encryption key.
|
|
80
|
+
2. Enter your Discord credentials and server ID, then save `.env`. The bot itself will run in Docker.
|
|
81
81
|
3. Keep `DATABASE_ENGINE=postgresql` with the supplied internal `DATABASE_URL` and `DATABASE_SSL=disable`.
|
|
82
82
|
4. Run `docker compose up --build -d`.
|
|
83
83
|
5. Review startup with `docker compose logs -f bot`.
|
|
@@ -151,10 +151,10 @@ Command registration is global or guild-scoped according to each reviewed comman
|
|
|
151
151
|
|
|
152
152
|
### Server-scoped module commands
|
|
153
153
|
|
|
154
|
-
The template
|
|
155
|
-
`HELYX_GLOBAL_COMMAND_MODE=
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
The compact template enables guild command writes with allowlist scope and
|
|
155
|
+
`HELYX_GLOBAL_COMMAND_MODE=platform-only`. Enter your Discord server IDs before
|
|
156
|
+
starting. Core commands remain global; feature commands are built for the
|
|
157
|
+
enabled modules in each approved server.
|
|
158
158
|
|
|
159
159
|
To use enabled-module command builds in servers you explicitly approve, set:
|
|
160
160
|
|
|
@@ -120,6 +120,12 @@ export function validateDatabaseUrl(value, engine) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function normalizeDatabaseUrl(value, engine) {
|
|
124
|
+
return engine === "mysql"
|
|
125
|
+
? value.replace(/^jdbc:(?=mysql:\/\/)/iu, "")
|
|
126
|
+
: value;
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
// Use single quotes to retain #, $, spaces and backslashes literally in Node
|
|
124
130
|
// and Compose. Unusual mixed-quote values require manual editing, not guessing.
|
|
125
131
|
export function encodeValue(value) {
|
|
@@ -188,113 +194,148 @@ export function updateEnvironment(content, changes) {
|
|
|
188
194
|
|
|
189
195
|
const environmentGroups = [
|
|
190
196
|
[
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
],
|
|
194
|
-
["DISCORD BOT", /^(?:DISCORD_TOKEN|DISCORD_CLIENT_ID)$/u],
|
|
195
|
-
[
|
|
196
|
-
"DISCORD SERVER COMMANDS",
|
|
197
|
-
/^(?:HELYX_GUILD_COMMAND_|HELYX_GLOBAL_COMMAND_MODE)/u,
|
|
197
|
+
"Runtime",
|
|
198
|
+
["HELYX_RUNTIME_PROFILE", "HELYX_DEPLOYMENT_ENVIRONMENT", "NODE_ENV"],
|
|
198
199
|
],
|
|
199
200
|
[
|
|
200
|
-
"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/^(?:HELYX_CLOUD_CONNECT_ENABLED|HELYX_DASHBOARD_PUBLIC_URL|HELYX_OWNER_USER_IDS)$/u,
|
|
201
|
+
"Discord",
|
|
202
|
+
[
|
|
203
|
+
"DISCORD_CLIENT_ID",
|
|
204
|
+
"DISCORD_TOKEN",
|
|
205
|
+
"HELYX_GUILD_MEMBERS_ENABLED",
|
|
206
|
+
"HELYX_MESSAGE_CONTENT_ENABLED",
|
|
207
|
+
],
|
|
208
208
|
],
|
|
209
|
-
["
|
|
210
|
-
["LOGGING", /^LOG_LEVEL$/u],
|
|
211
|
-
["AUTO MODERATION", /^HELYX_AUTOMOD_/u],
|
|
212
|
-
["SHARDING - ADVANCED", /^HELYX_SHARD_/u],
|
|
213
|
-
["MODULE SELECTION", /^HELYX_MODULE_/u],
|
|
209
|
+
["Database", ["DATABASE_ENGINE", "DATABASE_URL", "DATABASE_SSL"]],
|
|
214
210
|
[
|
|
215
|
-
"
|
|
216
|
-
|
|
211
|
+
"Commands",
|
|
212
|
+
[
|
|
213
|
+
"HELYX_GUILD_COMMAND_WRITES_ENABLED",
|
|
214
|
+
"HELYX_GUILD_COMMAND_WRITE_SCOPE",
|
|
215
|
+
"HELYX_GLOBAL_COMMAND_MODE",
|
|
216
|
+
"HELYX_GUILD_COMMAND_WRITE_GUILD_IDS",
|
|
217
|
+
],
|
|
217
218
|
],
|
|
218
|
-
["CLOUD CONNECT - ADVANCED", /^HELYX_CLOUD_/u],
|
|
219
|
-
["OAUTH - ADVANCED", /^HELYX_OAUTH_/u],
|
|
220
|
-
["STATUS REPORTING - OPTIONAL", /^HELYX_STATUS_|^CF_ACCESS_/u],
|
|
221
219
|
[
|
|
222
|
-
"
|
|
223
|
-
|
|
220
|
+
"Modules and dashboard",
|
|
221
|
+
[
|
|
222
|
+
"HELYX_MODULE_CATALOGUE_PATH",
|
|
223
|
+
"HELYX_CLOUD_CONNECT_ENABLED",
|
|
224
|
+
"HELYX_SHARD_MODE",
|
|
225
|
+
"LOG_LEVEL",
|
|
226
|
+
],
|
|
224
227
|
],
|
|
225
|
-
["
|
|
228
|
+
["Keep this key unchanged across restarts and updates", [encryptionKey]],
|
|
226
229
|
];
|
|
230
|
+
const standardKeys = new Set(environmentGroups.flatMap(([, keys]) => keys));
|
|
227
231
|
const oldHeadings = new Set([
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"
|
|
232
|
+
"INSTALLATION",
|
|
233
|
+
"DISCORD BOT",
|
|
234
|
+
"DISCORD SERVER COMMANDS",
|
|
235
|
+
"DISCORD GATEWAY INTENTS",
|
|
236
|
+
"DATABASE CONNECTION",
|
|
237
|
+
"BUNDLED DOCKER POSTGRESQL",
|
|
238
|
+
"DASHBOARD AND OWNERS",
|
|
239
|
+
"DATA ENCRYPTION - KEEP THIS KEY",
|
|
240
|
+
"LOGGING",
|
|
241
|
+
"SHARDING - ADVANCED",
|
|
242
|
+
"MODULE SELECTION",
|
|
243
|
+
"CUSTOM SETTINGS",
|
|
244
|
+
"AUTO MODERATION",
|
|
245
|
+
"PRIVATE CONTROL - ADVANCED",
|
|
246
|
+
"CLOUD CONNECT - ADVANCED",
|
|
247
|
+
"OAUTH - ADVANCED",
|
|
248
|
+
"STATUS REPORTING - OPTIONAL",
|
|
249
|
+
"INTERNAL DEVELOPMENT - OPTIONAL",
|
|
250
|
+
"REQUIRED ? EDIT THESE VALUES",
|
|
231
251
|
"OPTIONAL EVERYDAY SETTINGS",
|
|
232
252
|
"SHARDING: OFF BY DEFAULT",
|
|
233
|
-
"ADVANCED INTEGRATIONS
|
|
234
|
-
"DANGER ZONE
|
|
253
|
+
"ADVANCED INTEGRATIONS ? EDIT ONLY WHEN NEEDED",
|
|
254
|
+
"DANGER ZONE ? HELYX-MANAGED VALUES, DO NOT EDIT",
|
|
235
255
|
"Runtime mode",
|
|
236
256
|
"Helyx self-hosted environment",
|
|
237
257
|
"Keep this file private. Lines starting with # are comments.",
|
|
258
|
+
"Keep this file private. Retain the encryption key when updating.",
|
|
259
|
+
"Bundled Docker database",
|
|
260
|
+
"Custom settings",
|
|
261
|
+
...environmentGroups.map(([title]) => title),
|
|
238
262
|
]);
|
|
239
263
|
|
|
240
|
-
export function formatEnvironment(content) {
|
|
264
|
+
export function formatEnvironment(content, { template = false } = {}) {
|
|
241
265
|
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
242
|
-
const
|
|
266
|
+
const sections = [
|
|
267
|
+
...environmentGroups,
|
|
268
|
+
["Bundled Docker database", []],
|
|
269
|
+
["Custom settings", []],
|
|
270
|
+
];
|
|
271
|
+
const groups = sections.map(() => []);
|
|
243
272
|
let pending = [];
|
|
244
273
|
let previousGroup;
|
|
245
|
-
const append = (index, lines) => {
|
|
246
|
-
const text = lines.join("\n").trim();
|
|
247
|
-
if (!text) return;
|
|
248
|
-
if (
|
|
249
|
-
groups[index].length &&
|
|
250
|
-
text.startsWith("#") &&
|
|
251
|
-
!/^#\s*[A-Za-z_][A-Za-z0-9_]*\s*=/u.test(text)
|
|
252
|
-
)
|
|
253
|
-
groups[index].push("");
|
|
254
|
-
groups[index].push(text);
|
|
255
|
-
};
|
|
256
274
|
for (const record of records(content)) {
|
|
257
275
|
const text = record.text.replace(/\r\n/gu, "\n").replace(/\n$/u, "");
|
|
258
276
|
const heading = text.match(/^#\s*(.*?)\s*$/u)?.[1];
|
|
259
277
|
if (
|
|
260
278
|
heading !== undefined &&
|
|
261
|
-
(oldHeadings.has(heading) ||
|
|
262
|
-
/^=+$/u.test(heading) ||
|
|
263
|
-
environmentGroups.some(([title]) => heading === title))
|
|
279
|
+
(template || oldHeadings.has(heading) || /^=+$/u.test(heading))
|
|
264
280
|
)
|
|
265
281
|
continue;
|
|
266
282
|
const key =
|
|
267
283
|
record.key ?? text.match(/^\s*#\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/u)?.[1];
|
|
284
|
+
if (key && (!record.key || (template && !standardKeys.has(key)))) {
|
|
285
|
+
pending = [];
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
268
288
|
if (!key) {
|
|
269
289
|
if (!text.trim() && pending.length && previousGroup !== undefined) {
|
|
270
|
-
|
|
290
|
+
groups[previousGroup].push({ key: "", text: pending.join("\n") });
|
|
271
291
|
pending = [];
|
|
272
|
-
} else if (text.trim()) pending.push(text);
|
|
292
|
+
} else if (text.trim() && !template) pending.push(text);
|
|
273
293
|
continue;
|
|
274
294
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
295
|
+
const standardGroup = environmentGroups.findIndex(([, keys]) =>
|
|
296
|
+
keys.includes(key),
|
|
277
297
|
);
|
|
278
|
-
|
|
298
|
+
const group =
|
|
299
|
+
standardGroup >= 0
|
|
300
|
+
? standardGroup
|
|
301
|
+
: /^(?:PGUSER|PGPASSWORD|POSTGRES_)/u.test(key)
|
|
302
|
+
? environmentGroups.length
|
|
303
|
+
: environmentGroups.length + 1;
|
|
304
|
+
// Standard fields use the same compact quoting as the website. Keep custom
|
|
305
|
+
// records verbatim, including multiline values and their comments.
|
|
306
|
+
const assignment =
|
|
307
|
+
standardGroup >= 0
|
|
308
|
+
? `${key}=${encodeValue(parseEnv(record.text)[key])}${record.comment}`
|
|
309
|
+
: text;
|
|
310
|
+
groups[group].push({ key, text: [...pending, assignment].join("\n") });
|
|
279
311
|
pending = [];
|
|
280
312
|
previousGroup = group;
|
|
281
313
|
}
|
|
282
|
-
if (pending.length)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
314
|
+
if (pending.length)
|
|
315
|
+
groups[previousGroup ?? groups.length - 1].push({
|
|
316
|
+
key: "",
|
|
317
|
+
text: pending.join("\n"),
|
|
318
|
+
});
|
|
319
|
+
const formatted = (
|
|
286
320
|
[
|
|
287
|
-
"# Helyx self-hosted environment\n# Keep this file private.
|
|
288
|
-
...groups.flatMap((lines, index) =>
|
|
289
|
-
lines.length
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
321
|
+
"# Helyx self-hosted environment\n# Keep this file private. Retain the encryption key when updating.",
|
|
322
|
+
...groups.flatMap((lines, index) => {
|
|
323
|
+
if (!lines.length) return [];
|
|
324
|
+
const keys = sections[index][1];
|
|
325
|
+
if (keys.length)
|
|
326
|
+
lines.sort(
|
|
327
|
+
(left, right) => keys.indexOf(left.key) - keys.indexOf(right.key),
|
|
328
|
+
);
|
|
329
|
+
return [
|
|
330
|
+
`# ${sections[index][0]}\n${lines.map(({ text }) => text).join("\n")}`,
|
|
331
|
+
];
|
|
332
|
+
}),
|
|
333
|
+
].join("\n\n") + "\n"
|
|
334
|
+
).replace(/\n/gu, newline);
|
|
297
335
|
const before = parseEnv(content);
|
|
336
|
+
if (template)
|
|
337
|
+
for (const key of Object.keys(before))
|
|
338
|
+
if (!standardKeys.has(key)) delete before[key];
|
|
298
339
|
const after = parseEnv(formatted);
|
|
299
340
|
if (
|
|
300
341
|
Object.keys(before).length !== Object.keys(after).length ||
|
|
@@ -420,7 +461,10 @@ export async function runWizard({
|
|
|
420
461
|
"Choose a downloaded Helyx starter folder containing package.json and .env.example.",
|
|
421
462
|
);
|
|
422
463
|
const original = await optionalRead(join(directory, ".env"));
|
|
423
|
-
const template =
|
|
464
|
+
const template = formatEnvironment(
|
|
465
|
+
await readFile(join(directory, ".env.example"), "utf8"),
|
|
466
|
+
{ template: true },
|
|
467
|
+
);
|
|
424
468
|
const existing = parseEnv(original ?? "");
|
|
425
469
|
const defaults = parseEnv(template);
|
|
426
470
|
const values = { ...defaults, ...existing };
|
|
@@ -437,6 +481,7 @@ export async function runWizard({
|
|
|
437
481
|
validate = () => {},
|
|
438
482
|
fallback,
|
|
439
483
|
optional = false,
|
|
484
|
+
normalize = (value) => value,
|
|
440
485
|
} = {},
|
|
441
486
|
) => {
|
|
442
487
|
const current = fallback ?? values[key] ?? "";
|
|
@@ -450,8 +495,10 @@ export async function runWizard({
|
|
|
450
495
|
`${label} [${display}]${optional ? " (Enter keeps; - clears)" : " (Enter keeps)"}: `,
|
|
451
496
|
{ secret: secret && !visibleInput },
|
|
452
497
|
);
|
|
453
|
-
const value = optional && answer === "-" ? "" : answer || current;
|
|
454
498
|
try {
|
|
499
|
+
const value = normalize(
|
|
500
|
+
optional && answer === "-" ? "" : answer || current,
|
|
501
|
+
);
|
|
455
502
|
if (placeholder.test(value))
|
|
456
503
|
throw new WizardError(
|
|
457
504
|
"Replace the example placeholder with your own value.",
|
|
@@ -460,7 +507,14 @@ export async function runWizard({
|
|
|
460
507
|
encodeValue(value);
|
|
461
508
|
validate(value);
|
|
462
509
|
values[key] = value;
|
|
463
|
-
if (
|
|
510
|
+
if (
|
|
511
|
+
value !== existing[key] &&
|
|
512
|
+
(!optional ||
|
|
513
|
+
value ||
|
|
514
|
+
existing[key] !== undefined ||
|
|
515
|
+
standardKeys.has(key))
|
|
516
|
+
)
|
|
517
|
+
changes[key] = value;
|
|
464
518
|
return value;
|
|
465
519
|
} catch (error) {
|
|
466
520
|
io.print(error.message);
|
|
@@ -484,8 +538,7 @@ export async function runWizard({
|
|
|
484
538
|
const bundledAvailable =
|
|
485
539
|
engine === "postgresql" &&
|
|
486
540
|
defaults.DATABASE_ENGINE === "postgresql" &&
|
|
487
|
-
(await optionalRead(join(directory, "compose.yaml"))) !== null
|
|
488
|
-
Boolean(defaults.POSTGRES_PASSWORD);
|
|
541
|
+
(await optionalRead(join(directory, "compose.yaml"))) !== null;
|
|
489
542
|
let databaseMode = "external";
|
|
490
543
|
if (bundledAvailable) {
|
|
491
544
|
for (;;) {
|
|
@@ -537,7 +590,11 @@ export async function runWizard({
|
|
|
537
590
|
DATABASE_SSL: "disable",
|
|
538
591
|
});
|
|
539
592
|
} else {
|
|
540
|
-
|
|
593
|
+
if (engine === "mysql")
|
|
594
|
+
io.print(
|
|
595
|
+
"Paste a MySQL URL or JDBC connection string. The jdbc: prefix is removed automatically; missing login details are requested next.",
|
|
596
|
+
);
|
|
597
|
+
const connection = await askValue(
|
|
541
598
|
"DATABASE_URL",
|
|
542
599
|
"Database connection URL (typing and paste visible)",
|
|
543
600
|
{
|
|
@@ -549,8 +606,30 @@ export async function runWizard({
|
|
|
549
606
|
? ""
|
|
550
607
|
: values.DATABASE_URL,
|
|
551
608
|
validate: (value) => validateDatabaseUrl(value, engine),
|
|
609
|
+
normalize: (value) => normalizeDatabaseUrl(value, engine),
|
|
552
610
|
},
|
|
553
611
|
);
|
|
612
|
+
if (engine === "mysql") {
|
|
613
|
+
const url = new URL(connection);
|
|
614
|
+
for (const field of ["username", "password"]) {
|
|
615
|
+
while (!url[field]) {
|
|
616
|
+
const value = await io.ask(`Database ${field}: `, {
|
|
617
|
+
secret: field === "password",
|
|
618
|
+
});
|
|
619
|
+
if (!value || /[\p{Cc}\p{Cf}]/u.test(value)) {
|
|
620
|
+
io.print("Enter a single-line value.");
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
url[field] = encodeURIComponent(value).replace(
|
|
624
|
+
/[!'()*]/gu,
|
|
625
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
values.DATABASE_URL = url.href.replaceAll("'", "%27");
|
|
630
|
+
if (values.DATABASE_URL !== existing.DATABASE_URL)
|
|
631
|
+
changes.DATABASE_URL = values.DATABASE_URL;
|
|
632
|
+
}
|
|
554
633
|
io.print(
|
|
555
634
|
"SSL: disable = no TLS; require = TLS without certificate verification; verify-full = TLS with certificate verification. Use your provider's requirement.",
|
|
556
635
|
);
|
|
@@ -681,6 +760,17 @@ export async function runWizard({
|
|
|
681
760
|
HELYX_RUNTIME_PROFILE: "self-hosted",
|
|
682
761
|
HELYX_DEPLOYMENT_ENVIRONMENT: "self-hosted",
|
|
683
762
|
});
|
|
763
|
+
for (const [key, value] of Object.entries({
|
|
764
|
+
NODE_ENV: "production",
|
|
765
|
+
HELYX_MODULE_CATALOGUE_PATH: "./helyx.modules.json",
|
|
766
|
+
HELYX_SHARD_MODE: "off",
|
|
767
|
+
LOG_LEVEL: "info",
|
|
768
|
+
HELYX_GLOBAL_COMMAND_MODE: "platform-only",
|
|
769
|
+
HELYX_GUILD_COMMAND_WRITE_SCOPE: "allowlist",
|
|
770
|
+
})) {
|
|
771
|
+
if (existing[key] === undefined && changes[key] === undefined)
|
|
772
|
+
changes[key] = value;
|
|
773
|
+
}
|
|
684
774
|
if (engine === "mysql") Object.assign(changes, { HELYX_SHARD_MODE: "off" });
|
|
685
775
|
const updated = formatEnvironment(
|
|
686
776
|
updateEnvironment(original ?? template, changes),
|