@helyx/cli 1.6.1 → 1.8.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 +24 -0
- package/dist/cli.js +6 -0
- package/dist/help-command.d.ts +8 -0
- package/dist/help-command.js +71 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/self-host/manifest.d.ts +13 -1
- package/dist/self-host/manifest.js +12 -4
- package/dist/self-host/project-generator.js +45 -60
- package/dist/setup.js +4 -5
- package/package.json +4 -4
- package/self-host-catalogue.json +7 -7
- package/templates/self-host/.env.example +29 -85
- package/templates/self-host/Dockerfile +1 -0
- package/templates/self-host/README.md +6 -6
- package/templates/self-host/compose.mysql.yaml +5 -0
- package/templates/self-host/compose.yaml +3 -0
- package/templates/self-host/env-builder.mjs +148 -64
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add helyx help connect, status and disconnect for opt-in online Help, encrypted local credentials and credential revocation. Package the compatible bot and updated self-host catalogue while retaining offline Help without enrolment.
|
|
8
|
+
|
|
9
|
+
## 1.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Add public `/proc` process information for hosted and self-hosted bots, replacing
|
|
14
|
+
the private `/dev proc` path. Keep the reply private and independent of database
|
|
15
|
+
availability.
|
|
16
|
+
|
|
17
|
+
Add a PebbleHost starter profile with direct Node CLI scripts, flat starter ZIPs,
|
|
18
|
+
compact quoted environment examples and matching guided environment builder
|
|
19
|
+
output. Preserve existing custom settings and encryption keys when rebuilding
|
|
20
|
+
an environment file, and recognize quoted placeholders during setup.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies
|
|
25
|
+
- @helyx/bot@1.6.0
|
|
26
|
+
|
|
3
27
|
## 1.6.1
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { resolveDatabaseAdapter } from "@helyx/database/adapters";
|
|
|
7
7
|
import { formatCommandError } from "./errors.js";
|
|
8
8
|
import { runSelfHostCommand } from "./self-host/command.js";
|
|
9
9
|
import { loadLocalEnvironment, setupSelfHostEnvironment } from "./setup.js";
|
|
10
|
+
import { runHelpCommand } from "./help-command.js";
|
|
10
11
|
const command = process.argv[2];
|
|
11
12
|
try {
|
|
12
13
|
if (command === "start") {
|
|
@@ -31,6 +32,9 @@ try {
|
|
|
31
32
|
else if (command === "self-host") {
|
|
32
33
|
await runSelfHostCommand(process.argv.slice(3));
|
|
33
34
|
}
|
|
35
|
+
else if (command === "help" && process.argv[3]) {
|
|
36
|
+
await runHelpCommand(process.argv.slice(3));
|
|
37
|
+
}
|
|
34
38
|
else if (command === "help" || command === "--help" || command === "-h") {
|
|
35
39
|
printHelp();
|
|
36
40
|
}
|
|
@@ -149,6 +153,8 @@ Commands:
|
|
|
149
153
|
start Start the Helyx Discord bot
|
|
150
154
|
migrate Apply core migrations for the selected database engine and exit
|
|
151
155
|
doctor Validate environment, database schema and module packages
|
|
156
|
+
help connect|status|disconnect [--project <path>] [--confirm]
|
|
157
|
+
Manage optional Help-only online access; local Help needs no account
|
|
152
158
|
self-host init --manifest <path> [--output <path>] [--install]
|
|
153
159
|
Generate a self-hosted starter project from a reviewed manifest
|
|
154
160
|
self-host update --check [--project <path>]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function runHelpCommand(args: readonly string[], options?: {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
environment?: NodeJS.ProcessEnv;
|
|
4
|
+
question?: (prompt: string) => Promise<string>;
|
|
5
|
+
write?: (message: string) => void;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=help-command.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { HelpCredentialStore, exchangeHelpEnrolment, revokeHelpCredential, } from "@helyx/bot";
|
|
4
|
+
import { loadLocalEnvironment } from "./setup.js";
|
|
5
|
+
export async function runHelpCommand(args, options = {}) {
|
|
6
|
+
const [action, ...rest] = args;
|
|
7
|
+
let directory = resolve(options.cwd ?? process.cwd()), confirm = false;
|
|
8
|
+
if (!["connect", "status", "disconnect"].includes(action ?? ""))
|
|
9
|
+
throw new Error("Use: helyx help connect|status|disconnect [--project <path>] [--confirm]");
|
|
10
|
+
for (let index = 0; index < rest.length; index++) {
|
|
11
|
+
if (rest[index] === "--project" && rest[index + 1])
|
|
12
|
+
directory = resolve(directory, rest[++index]);
|
|
13
|
+
else if (rest[index] === "--confirm")
|
|
14
|
+
confirm = true;
|
|
15
|
+
else
|
|
16
|
+
throw new Error("Unsupported Help option. Paste enrolment codes at the prompt, not in command arguments.");
|
|
17
|
+
}
|
|
18
|
+
const environment = { ...(options.environment ?? process.env) };
|
|
19
|
+
await loadLocalEnvironment(directory, environment);
|
|
20
|
+
if (!environment.HELYX_DATA_ENCRYPTION_KEY)
|
|
21
|
+
throw new Error("Set up the local HELYX_DATA_ENCRYPTION_KEY before managing online Help. Local Help remains available.");
|
|
22
|
+
if (action === "connect" &&
|
|
23
|
+
environment.HELYX_RUNTIME_PROFILE !== "self-hosted")
|
|
24
|
+
throw new Error("Online Help enrolment is for a self-hosted installation. Set HELYX_RUNTIME_PROFILE=self-hosted in that project's environment.");
|
|
25
|
+
const store = new HelpCredentialStore(directory, environment.HELYX_DATA_ENCRYPTION_KEY), write = options.write ?? console.log;
|
|
26
|
+
if (action === "status") {
|
|
27
|
+
const credential = await store.read();
|
|
28
|
+
write(!credential
|
|
29
|
+
? "Help: local bundled FAQs. No online credential saved."
|
|
30
|
+
: `Help: ${Date.parse(credential.expiresAt) > Date.now() ? "online credential saved" : "credential expired; local FAQs remain available"}. Installation ${credential.installationId}. Expires ${credential.expiresAt}.`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (action === "disconnect") {
|
|
34
|
+
const credential = await store.read();
|
|
35
|
+
let remote = true;
|
|
36
|
+
if (credential && Date.parse(credential.expiresAt) > Date.now())
|
|
37
|
+
try {
|
|
38
|
+
await revokeHelpCredential(credential.token, options.fetch);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
remote = false;
|
|
42
|
+
}
|
|
43
|
+
await store.remove();
|
|
44
|
+
write(remote
|
|
45
|
+
? "Online Help disconnected. Local bundled FAQs remain available."
|
|
46
|
+
: "Online Help disconnected locally. Revoke its credential from your dashboard when the service is available.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const reader = options.question
|
|
50
|
+
? null
|
|
51
|
+
: createInterface({ input: process.stdin, output: process.stdout }), question = options.question ?? ((prompt) => reader.question(prompt));
|
|
52
|
+
try {
|
|
53
|
+
write("Optional online Help sends your public Helyx question to Helyx and its embedding provider. It does not connect guild settings or require Cloud Connect.");
|
|
54
|
+
if (!confirm &&
|
|
55
|
+
(await question("Type CONNECT to save an encrypted Help-only credential: ")).trim() !== "CONNECT") {
|
|
56
|
+
write("Cancelled. Local Help remains available.");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const code = (await question("Paste the one-time Help code from your self-host dashboard: ")).trim();
|
|
60
|
+
const credential = await exchangeHelpEnrolment(code, options.fetch);
|
|
61
|
+
await store.save(credential);
|
|
62
|
+
write(`Online Help connected for installation ${credential.installationId}. Credential expires ${credential.expiresAt}.`);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new Error("Online Help could not be connected. Check the one-time code, service availability, local encryption key and write permissions. No credential value was printed.");
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
reader?.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=help-command.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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
|
})) {
|
|
@@ -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,7 +186,7 @@ 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")
|
|
@@ -190,10 +202,10 @@ function validateEnvironmentTemplate(template, databaseEngine) {
|
|
|
190
202
|
? databaseEngine
|
|
191
203
|
: key === "DATABASE_URL" && databaseEngine === "mysql"
|
|
192
204
|
? "mysql://user:password@host:3306/helyx"
|
|
193
|
-
: key === "DATABASE_SSL"
|
|
194
|
-
?
|
|
205
|
+
: key === "DATABASE_SSL"
|
|
206
|
+
? databaseSsl
|
|
195
207
|
: definition.value;
|
|
196
|
-
const expected = `${definition.optional ? "# " : ""}${key}
|
|
208
|
+
const expected = `${definition.optional ? "# " : ""}${key}='${value}'`;
|
|
197
209
|
if (!lines.has(expected)) {
|
|
198
210
|
throw new Error(`Self-host environment template does not match the bot contract: ${key}`);
|
|
199
211
|
}
|
|
@@ -333,7 +345,7 @@ export const selfHostEnvironmentContract = {
|
|
|
333
345
|
optional: false,
|
|
334
346
|
},
|
|
335
347
|
DATABASE_SSL: { value: "disable", optional: false },
|
|
336
|
-
HELYX_DATABASE_STARTUP_MODE: {
|
|
348
|
+
HELYX_DATABASE_STARTUP_MODE: { omit: true },
|
|
337
349
|
HELYX_RUNTIME_PROFILE: { value: "self-hosted", optional: false },
|
|
338
350
|
HELYX_DEPLOYMENT_ENVIRONMENT: { value: "self-hosted", optional: false },
|
|
339
351
|
DISCORD_TOKEN: { value: "replace-me", optional: false },
|
|
@@ -348,71 +360,44 @@ export const selfHostEnvironmentContract = {
|
|
|
348
360
|
HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS: { omit: true },
|
|
349
361
|
DISCORD_INTERNAL_GUILD_ID: { omit: true },
|
|
350
362
|
APPROVED_ROLES: { omit: true },
|
|
351
|
-
HELYX_MODULE_PACKAGES: {
|
|
352
|
-
value: "",
|
|
353
|
-
optional: true,
|
|
354
|
-
},
|
|
363
|
+
HELYX_MODULE_PACKAGES: { omit: true },
|
|
355
364
|
HELYX_MODULE_CATALOGUE_PATH: {
|
|
356
365
|
value: "./helyx.modules.json",
|
|
357
366
|
optional: false,
|
|
358
367
|
},
|
|
359
|
-
HELYX_DASHBOARD_PUBLIC_URL: {
|
|
360
|
-
value: "https://app.helyx.gg",
|
|
361
|
-
optional: true,
|
|
362
|
-
},
|
|
368
|
+
HELYX_DASHBOARD_PUBLIC_URL: { omit: true },
|
|
363
369
|
HELYX_BOT_CONTROL_SECRET: { omit: true },
|
|
364
370
|
HELYX_BOT_CONTROL_PORT: { omit: true },
|
|
365
371
|
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: { omit: true },
|
|
366
372
|
HELYX_HOSTED_MODERATION_ENABLED: { omit: true },
|
|
367
|
-
HELYX_GUILD_COMMAND_WRITES_ENABLED: { value: "
|
|
373
|
+
HELYX_GUILD_COMMAND_WRITES_ENABLED: { value: "true", optional: false },
|
|
368
374
|
HELYX_GUILD_COMMAND_WRITE_SCOPE: { value: "allowlist", optional: false },
|
|
369
|
-
HELYX_GUILD_COMMAND_RELEASE_ID: {
|
|
370
|
-
HELYX_GLOBAL_COMMAND_MODE: { value: "
|
|
371
|
-
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 },
|
|
372
378
|
HELYX_GUILD_MEMBERS_ENABLED: { value: "false", optional: false },
|
|
373
379
|
HELYX_CLOUD_CONNECT_ENABLED: { value: "true", optional: false },
|
|
374
380
|
HELYX_CLOUD_ENROLMENT_TOKEN: { omit: true },
|
|
375
381
|
HELYX_CLOUD_ENROLMENT_FILE: { omit: true },
|
|
376
382
|
HELYX_DATA_ENCRYPTION_KEY: {
|
|
377
383
|
value: "replace-with-a-base64-encoded-32-byte-key",
|
|
378
|
-
optional:
|
|
384
|
+
optional: false,
|
|
379
385
|
},
|
|
380
386
|
HELYX_CLOUD_API_URL: { omit: true },
|
|
381
387
|
HELYX_OAUTH_ENABLED: { omit: true },
|
|
382
388
|
HELYX_OAUTH_INTERNAL_SECRET: { omit: true },
|
|
383
389
|
HELYX_CLOUD_ENDPOINT: { omit: true },
|
|
384
|
-
HELYX_OWNER_USER_IDS: {
|
|
385
|
-
value: "12345678901234567",
|
|
386
|
-
optional: true,
|
|
387
|
-
},
|
|
390
|
+
HELYX_OWNER_USER_IDS: { omit: true },
|
|
388
391
|
HELYX_SHARD_MODE: { value: "off", optional: false },
|
|
389
|
-
HELYX_SHARD_COUNT: {
|
|
390
|
-
HELYX_SHARD_SCOPE: {
|
|
391
|
-
HELYX_SHARD_WORKERS_PER_REPLICA: {
|
|
392
|
-
HELYX_SHARD_REDIS_URL: {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
optional: true,
|
|
399
|
-
},
|
|
400
|
-
HELYX_SHARD_GENERATION: {
|
|
401
|
-
value: "local-20260728",
|
|
402
|
-
optional: true,
|
|
403
|
-
},
|
|
404
|
-
HELYX_SHARD_DATABASE_CONNECTION_BUDGET: {
|
|
405
|
-
value: "10",
|
|
406
|
-
optional: true,
|
|
407
|
-
},
|
|
408
|
-
HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX: {
|
|
409
|
-
value: "2",
|
|
410
|
-
optional: true,
|
|
411
|
-
},
|
|
412
|
-
HELYX_SHARD_WORKER_DATABASE_POOL_MAX: {
|
|
413
|
-
value: "4",
|
|
414
|
-
optional: true,
|
|
415
|
-
},
|
|
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 },
|
|
416
401
|
HELYX_CLOUD_AUDIT_RETENTION_DAYS: { omit: true },
|
|
417
402
|
HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: { omit: true },
|
|
418
403
|
HELYX_STATUS_ENABLED: { omit: true },
|
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.8.0",
|
|
4
4
|
"description": "Command-line tools for running and generating self-hosted Helyx deployments.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"build": "tsc -b"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@helyx/bot": ">=1.
|
|
56
|
-
"@helyx/config": ">=1.
|
|
57
|
-
"@helyx/database": ">=2.5.
|
|
55
|
+
"@helyx/bot": ">=1.7.0 <2.0.0",
|
|
56
|
+
"@helyx/config": ">=1.7.0 <2.0.0",
|
|
57
|
+
"@helyx/database": ">=2.5.1 <3.0.0",
|
|
58
58
|
"semver": "7.8.5",
|
|
59
59
|
"zod": "4.6.7"
|
|
60
60
|
},
|
package/self-host-catalogue.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"catalogueVersion":
|
|
3
|
+
"catalogueVersion": 89,
|
|
4
4
|
"core": {
|
|
5
5
|
"package": "@helyx/bot",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.7.0",
|
|
7
7
|
"availability": "published"
|
|
8
8
|
},
|
|
9
9
|
"generator": {
|
|
10
10
|
"package": "@helyx/cli",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.8.0",
|
|
12
12
|
"command": "helyx",
|
|
13
13
|
"availability": "published"
|
|
14
14
|
},
|
|
15
15
|
"databaseAdapters": [
|
|
16
16
|
{
|
|
17
17
|
"package": "@helyx/database",
|
|
18
|
-
"version": "2.5.
|
|
18
|
+
"version": "2.5.1"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"package": "@helyx/database-mysql",
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
{
|
|
274
274
|
"id": "helyx.account-gate",
|
|
275
275
|
"package": "@helyx/module-account-gate",
|
|
276
|
-
"version": "2.1.
|
|
276
|
+
"version": "2.1.3",
|
|
277
277
|
"name": "Access Control",
|
|
278
278
|
"description": "Require new members to complete configurable Discord interaction checks before gaining access.",
|
|
279
279
|
"category": "Safety",
|
|
@@ -558,7 +558,7 @@
|
|
|
558
558
|
{
|
|
559
559
|
"id": "helyx.reactionroles",
|
|
560
560
|
"package": "@helyx/module-reactionroles",
|
|
561
|
-
"version": "1.1.
|
|
561
|
+
"version": "1.1.3",
|
|
562
562
|
"name": "Reaction Roles",
|
|
563
563
|
"description": "Let members select approved roles themselves.",
|
|
564
564
|
"category": "Automation",
|
|
@@ -938,7 +938,7 @@
|
|
|
938
938
|
{
|
|
939
939
|
"id": "helyx.achievements",
|
|
940
940
|
"package": "@helyx/module-achievements",
|
|
941
|
-
"version": "2.1.
|
|
941
|
+
"version": "2.1.4",
|
|
942
942
|
"name": "Achievements",
|
|
943
943
|
"description": "Configurable member milestones, scores and optional rewards.",
|
|
944
944
|
"category": "Gaming",
|
|
@@ -1,89 +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
|
-
# Enable for loaded member workflows such as Welcome, Access Control or Levels,
|
|
13
|
-
# with Server Members Intent also enabled in the Discord developer portal.
|
|
14
|
-
HELYX_GUILD_MEMBERS_ENABLED=false
|
|
15
|
-
# Database
|
|
16
|
-
# Docker: keep the URL. Direct Node.js/managed hosting: use your provider URL.
|
|
17
|
-
DATABASE_ENGINE=postgresql
|
|
18
|
-
DATABASE_URL=postgresql://database:5432/helyx
|
|
19
|
-
DATABASE_SSL=disable
|
|
20
|
-
# Self-host startup applies pending migrations automatically (the default).
|
|
21
|
-
# Hosted-only inspect mode is not a self-host setting; leave this commented.
|
|
22
|
-
# HELYX_DATABASE_STARTUP_MODE=apply
|
|
23
|
-
# Match your provider: disable=no TLS; require=TLS without certificate
|
|
24
|
-
# verification; verify-full=TLS with certificate verification.
|
|
25
|
-
|
|
26
|
-
# Included Docker database — both password values must match.
|
|
27
|
-
PGUSER=helyx
|
|
28
|
-
PGPASSWORD=replace-with-a-strong-password
|
|
29
|
-
POSTGRES_DB=helyx
|
|
30
|
-
POSTGRES_USER=helyx
|
|
31
|
-
POSTGRES_PASSWORD=replace-with-a-strong-password
|
|
32
|
-
|
|
33
|
-
# OPTIONAL EVERYDAY SETTINGS
|
|
34
|
-
|
|
35
|
-
# Ready for dashboard linking, but remains unpaired until you use a one-use code.
|
|
36
|
-
HELYX_CLOUD_CONNECT_ENABLED=true
|
|
37
|
-
# Public dashboard origin used in bot-generated report links.
|
|
38
|
-
# HELYX_DASHBOARD_PUBLIC_URL=https://app.helyx.gg
|
|
39
|
-
# HELYX_OWNER_USER_IDS=12345678901234567
|
|
40
|
-
|
|
41
|
-
# trace, debug, info, warn, error, fatal or silent
|
|
42
|
-
LOG_LEVEL=info
|
|
43
|
-
|
|
44
|
-
# SHARDING: OFF BY DEFAULT
|
|
45
|
-
|
|
46
|
-
# `off` is the compatible self-host default. Keep shard-only values commented
|
|
47
|
-
# in this mode; an empty value is still rejected as configured. MySQL must stay
|
|
48
|
-
# off. Advanced PostgreSQL self-hosting may use `auto` under one
|
|
49
|
-
# supervisor, or `fixed` with a deliberate total count. Local mode never needs
|
|
50
|
-
# Redis. Coordinated multi-host self-hosting is outside the first release.
|
|
51
|
-
HELYX_SHARD_MODE=off
|
|
52
|
-
# HELYX_SHARD_SCOPE=local
|
|
53
|
-
# HELYX_SHARD_COUNT=2
|
|
54
|
-
# HELYX_SHARD_WORKERS_PER_REPLICA=2
|
|
55
|
-
# HELYX_SHARD_REDIS_URL=rediss://private-redis.example:6379
|
|
56
|
-
# HELYX_SHARD_REDIS_NAMESPACE=helyx.self-host
|
|
57
|
-
# HELYX_SHARD_GENERATION=local-20260728
|
|
58
|
-
# HELYX_SHARD_DATABASE_CONNECTION_BUDGET=10
|
|
59
|
-
# HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX=2
|
|
60
|
-
# HELYX_SHARD_WORKER_DATABASE_POOL_MAX=4
|
|
61
|
-
# Redis URL and namespace are intentionally unsupported for the first
|
|
62
|
-
# self-hosted release: do not uncomment them for local auto or fixed mode.
|
|
10
|
+
DISCORD_CLIENT_ID='12345678901234567'
|
|
11
|
+
DISCORD_TOKEN='replace-me'
|
|
12
|
+
HELYX_GUILD_MEMBERS_ENABLED='false'
|
|
13
|
+
HELYX_MESSAGE_CONTENT_ENABLED='false'
|
|
63
14
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Generated privately by npm run setup. Required by Cloud Connect, Giveaways,
|
|
85
|
-
# and Invite Tracking; retain it unchanged across restarts and deployments.
|
|
86
|
-
# HELYX_DATA_ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key
|
|
87
|
-
|
|
88
|
-
# Runtime mode
|
|
89
|
-
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,116 +194,148 @@ export function updateEnvironment(content, changes) {
|
|
|
188
194
|
|
|
189
195
|
const environmentGroups = [
|
|
190
196
|
[
|
|
191
|
-
"
|
|
192
|
-
|
|
197
|
+
"Runtime",
|
|
198
|
+
["HELYX_RUNTIME_PROFILE", "HELYX_DEPLOYMENT_ENVIRONMENT", "NODE_ENV"],
|
|
193
199
|
],
|
|
194
|
-
["DISCORD BOT", /^(?:DISCORD_TOKEN|DISCORD_CLIENT_ID)$/u],
|
|
195
200
|
[
|
|
196
|
-
"
|
|
197
|
-
|
|
201
|
+
"Discord",
|
|
202
|
+
[
|
|
203
|
+
"DISCORD_CLIENT_ID",
|
|
204
|
+
"DISCORD_TOKEN",
|
|
205
|
+
"HELYX_GUILD_MEMBERS_ENABLED",
|
|
206
|
+
"HELYX_MESSAGE_CONTENT_ENABLED",
|
|
207
|
+
],
|
|
198
208
|
],
|
|
209
|
+
["Database", ["DATABASE_ENGINE", "DATABASE_URL", "DATABASE_SSL"]],
|
|
199
210
|
[
|
|
200
|
-
"
|
|
201
|
-
|
|
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
|
+
],
|
|
202
218
|
],
|
|
203
|
-
["DATABASE CONNECTION", /^DATABASE_|^HELYX_DATABASE_STARTUP_MODE$/u],
|
|
204
|
-
["BUNDLED DOCKER POSTGRESQL", /^(?:PGUSER|PGPASSWORD|POSTGRES_)/u],
|
|
205
219
|
[
|
|
206
|
-
"
|
|
207
|
-
|
|
220
|
+
"Modules and dashboard",
|
|
221
|
+
[
|
|
222
|
+
"HELYX_MODULE_CATALOGUE_PATH",
|
|
223
|
+
"HELYX_CLOUD_CONNECT_ENABLED",
|
|
224
|
+
"HELYX_SHARD_MODE",
|
|
225
|
+
"LOG_LEVEL",
|
|
226
|
+
],
|
|
208
227
|
],
|
|
209
|
-
["
|
|
210
|
-
["LOGGING", /^LOG_LEVEL$/u],
|
|
211
|
-
["SHARDING - ADVANCED", /^HELYX_SHARD_/u],
|
|
212
|
-
["MODULE SELECTION", /^HELYX_MODULE_/u],
|
|
213
|
-
["CUSTOM SETTINGS", /./u],
|
|
228
|
+
["Keep this key unchanged across restarts and updates", [encryptionKey]],
|
|
214
229
|
];
|
|
215
|
-
const
|
|
216
|
-
/^(?:HELYX_AUTOMOD_|HELYX_BOT_CONTROL_|HELYX_HOSTED_MODERATION_ENABLED$|HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS$|HELYX_CLOUD_(?!CONNECT_ENABLED$)|HELYX_OAUTH_|HELYX_STATUS_|CF_ACCESS_|DISCORD_INTERNAL_GUILD_ID$|APPROVED_ROLES$)/u;
|
|
230
|
+
const standardKeys = new Set(environmentGroups.flatMap(([, keys]) => keys));
|
|
217
231
|
const oldHeadings = new Set([
|
|
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",
|
|
218
244
|
"AUTO MODERATION",
|
|
219
245
|
"PRIVATE CONTROL - ADVANCED",
|
|
220
246
|
"CLOUD CONNECT - ADVANCED",
|
|
221
247
|
"OAUTH - ADVANCED",
|
|
222
248
|
"STATUS REPORTING - OPTIONAL",
|
|
223
249
|
"INTERNAL DEVELOPMENT - OPTIONAL",
|
|
224
|
-
"REQUIRED
|
|
225
|
-
"Discord",
|
|
226
|
-
"Database",
|
|
250
|
+
"REQUIRED ? EDIT THESE VALUES",
|
|
227
251
|
"OPTIONAL EVERYDAY SETTINGS",
|
|
228
252
|
"SHARDING: OFF BY DEFAULT",
|
|
229
|
-
"ADVANCED INTEGRATIONS
|
|
230
|
-
"DANGER ZONE
|
|
253
|
+
"ADVANCED INTEGRATIONS ? EDIT ONLY WHEN NEEDED",
|
|
254
|
+
"DANGER ZONE ? HELYX-MANAGED VALUES, DO NOT EDIT",
|
|
231
255
|
"Runtime mode",
|
|
232
256
|
"Helyx self-hosted environment",
|
|
233
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),
|
|
234
262
|
]);
|
|
235
263
|
|
|
236
264
|
export function formatEnvironment(content, { template = false } = {}) {
|
|
237
265
|
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
238
|
-
const
|
|
266
|
+
const sections = [
|
|
267
|
+
...environmentGroups,
|
|
268
|
+
["Bundled Docker database", []],
|
|
269
|
+
["Custom settings", []],
|
|
270
|
+
];
|
|
271
|
+
const groups = sections.map(() => []);
|
|
239
272
|
let pending = [];
|
|
240
273
|
let previousGroup;
|
|
241
|
-
const append = (index, lines) => {
|
|
242
|
-
const text = lines.join("\n").trim();
|
|
243
|
-
if (!text) return;
|
|
244
|
-
if (
|
|
245
|
-
groups[index].length &&
|
|
246
|
-
text.startsWith("#") &&
|
|
247
|
-
!/^#\s*[A-Za-z_][A-Za-z0-9_]*\s*=/u.test(text)
|
|
248
|
-
)
|
|
249
|
-
groups[index].push("");
|
|
250
|
-
groups[index].push(text);
|
|
251
|
-
};
|
|
252
274
|
for (const record of records(content)) {
|
|
253
275
|
const text = record.text.replace(/\r\n/gu, "\n").replace(/\n$/u, "");
|
|
254
276
|
const heading = text.match(/^#\s*(.*?)\s*$/u)?.[1];
|
|
255
277
|
if (
|
|
256
278
|
heading !== undefined &&
|
|
257
|
-
(oldHeadings.has(heading) ||
|
|
258
|
-
/^=+$/u.test(heading) ||
|
|
259
|
-
environmentGroups.some(([title]) => heading === title))
|
|
279
|
+
(template || oldHeadings.has(heading) || /^=+$/u.test(heading))
|
|
260
280
|
)
|
|
261
281
|
continue;
|
|
262
282
|
const key =
|
|
263
283
|
record.key ?? text.match(/^\s*#\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/u)?.[1];
|
|
264
|
-
if (key &&
|
|
284
|
+
if (key && (!record.key || (template && !standardKeys.has(key)))) {
|
|
265
285
|
pending = [];
|
|
266
286
|
continue;
|
|
267
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);
|
|
298
336
|
if (template)
|
|
299
337
|
for (const key of Object.keys(before))
|
|
300
|
-
if (
|
|
338
|
+
if (!standardKeys.has(key)) delete before[key];
|
|
301
339
|
const after = parseEnv(formatted);
|
|
302
340
|
if (
|
|
303
341
|
Object.keys(before).length !== Object.keys(after).length ||
|
|
@@ -443,6 +481,7 @@ export async function runWizard({
|
|
|
443
481
|
validate = () => {},
|
|
444
482
|
fallback,
|
|
445
483
|
optional = false,
|
|
484
|
+
normalize = (value) => value,
|
|
446
485
|
} = {},
|
|
447
486
|
) => {
|
|
448
487
|
const current = fallback ?? values[key] ?? "";
|
|
@@ -456,8 +495,10 @@ export async function runWizard({
|
|
|
456
495
|
`${label} [${display}]${optional ? " (Enter keeps; - clears)" : " (Enter keeps)"}: `,
|
|
457
496
|
{ secret: secret && !visibleInput },
|
|
458
497
|
);
|
|
459
|
-
const value = optional && answer === "-" ? "" : answer || current;
|
|
460
498
|
try {
|
|
499
|
+
const value = normalize(
|
|
500
|
+
optional && answer === "-" ? "" : answer || current,
|
|
501
|
+
);
|
|
461
502
|
if (placeholder.test(value))
|
|
462
503
|
throw new WizardError(
|
|
463
504
|
"Replace the example placeholder with your own value.",
|
|
@@ -466,7 +507,14 @@ export async function runWizard({
|
|
|
466
507
|
encodeValue(value);
|
|
467
508
|
validate(value);
|
|
468
509
|
values[key] = value;
|
|
469
|
-
if (
|
|
510
|
+
if (
|
|
511
|
+
value !== existing[key] &&
|
|
512
|
+
(!optional ||
|
|
513
|
+
value ||
|
|
514
|
+
existing[key] !== undefined ||
|
|
515
|
+
standardKeys.has(key))
|
|
516
|
+
)
|
|
517
|
+
changes[key] = value;
|
|
470
518
|
return value;
|
|
471
519
|
} catch (error) {
|
|
472
520
|
io.print(error.message);
|
|
@@ -490,8 +538,7 @@ export async function runWizard({
|
|
|
490
538
|
const bundledAvailable =
|
|
491
539
|
engine === "postgresql" &&
|
|
492
540
|
defaults.DATABASE_ENGINE === "postgresql" &&
|
|
493
|
-
(await optionalRead(join(directory, "compose.yaml"))) !== null
|
|
494
|
-
Boolean(defaults.POSTGRES_PASSWORD);
|
|
541
|
+
(await optionalRead(join(directory, "compose.yaml"))) !== null;
|
|
495
542
|
let databaseMode = "external";
|
|
496
543
|
if (bundledAvailable) {
|
|
497
544
|
for (;;) {
|
|
@@ -543,7 +590,11 @@ export async function runWizard({
|
|
|
543
590
|
DATABASE_SSL: "disable",
|
|
544
591
|
});
|
|
545
592
|
} else {
|
|
546
|
-
|
|
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(
|
|
547
598
|
"DATABASE_URL",
|
|
548
599
|
"Database connection URL (typing and paste visible)",
|
|
549
600
|
{
|
|
@@ -555,8 +606,30 @@ export async function runWizard({
|
|
|
555
606
|
? ""
|
|
556
607
|
: values.DATABASE_URL,
|
|
557
608
|
validate: (value) => validateDatabaseUrl(value, engine),
|
|
609
|
+
normalize: (value) => normalizeDatabaseUrl(value, engine),
|
|
558
610
|
},
|
|
559
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
|
+
}
|
|
560
633
|
io.print(
|
|
561
634
|
"SSL: disable = no TLS; require = TLS without certificate verification; verify-full = TLS with certificate verification. Use your provider's requirement.",
|
|
562
635
|
);
|
|
@@ -687,6 +760,17 @@ export async function runWizard({
|
|
|
687
760
|
HELYX_RUNTIME_PROFILE: "self-hosted",
|
|
688
761
|
HELYX_DEPLOYMENT_ENVIRONMENT: "self-hosted",
|
|
689
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
|
+
}
|
|
690
774
|
if (engine === "mysql") Object.assign(changes, { HELYX_SHARD_MODE: "off" });
|
|
691
775
|
const updated = formatEnvironment(
|
|
692
776
|
updateEnvironment(original ?? template, changes),
|