@helyx/cli 0.2.1 → 0.3.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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 43704ad: Add the reviewed self-host update workflow with read-only checking, managed-file
8
+ backups, coherent exact dependency and lockfile updates, failure restoration,
9
+ generated project scripts, and current upgrade guidance.
10
+
11
+ First-run setup now also generates a private local data-encryption key when it
12
+ is missing, without displaying it or replacing an existing key.
13
+
14
+ Reorganise the generated environment template so required customer values are
15
+ first and Helyx-managed danger-zone settings are clearly separated.
16
+
17
+ Enable the unpaired Cloud Connect connector in generated starters so linking
18
+ requires only the one-use code rather than an additional environment edit.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [18265bc]
23
+ - Updated dependencies [b0f1932]
24
+ - @helyx/database@0.2.1
25
+ - @helyx/bot@0.2.2
26
+
27
+ ## 0.2.2
28
+
29
+ ### Patch Changes
30
+
31
+ - Add explicit Planned, Testing, Beta and Live module lifecycle metadata.
32
+
33
+ Propagate lifecycle status through the hosted control contract and permit only
34
+ Live modules in hosted distributions and generated self-host projects.
35
+
36
+ - Updated dependencies
37
+ - @helyx/bot@0.2.1
38
+
3
39
  ## 0.2.1
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -8,10 +8,14 @@ helyx start
8
8
  helyx migrate
9
9
  helyx doctor
10
10
  helyx self-host init --manifest ./helyx-build.json --output ./helyx
11
+ helyx self-host update --check
12
+ helyx self-host update --confirm
11
13
  ```
12
14
 
13
15
  `setup` safely creates `.env` from a generated starter's `.env.example`, never overwrites existing configuration and reports whether Docker Compose is available. Direct Node.js operation remains fully supported with a local or hosted PostgreSQL database. `start` loads local configuration without overriding hosting-platform variables and performs the production bot bootstrap, including database and installed-module migrations. `migrate` applies only core migrations and exits. `doctor` validates the complete bot environment, PostgreSQL connectivity, immutable core migration history and configured module package metadata without logging in to Discord or modifying the database.
14
16
 
15
17
  `self-host init` validates a build manifest against the reviewed catalogue shipped with this exact CLI version, creates a new starter-project directory, pins the runtime and selected official modules, generates a lockfile with lifecycle scripts disabled, and writes checksums. Add `--install` only when the dependencies should also be installed locally. Unknown, unpublished, duplicated or modified package selections fail closed.
16
18
 
19
+ `self-host update --check` reads the current project and the reviewed catalogue from `api.helyx.gg`, then reports the complete compatible release-set change without writing files. `--confirm` backs up the managed metadata under `.helyx/backups`, updates exact core, CLI and selected live-module versions together, refreshes and validates the lockfile with lifecycle scripts disabled, and restores the original metadata if generation fails. Run `npm ci` and `npm run doctor` after applying an update. The updater never changes `.env` or customer database data.
20
+
17
21
  The package is published under the Helyx Source Available Licence. Generated projects contain deployment configuration and exact public npm dependencies; they do not contain Helyx source code or credentials.
package/dist/cli.js CHANGED
@@ -42,8 +42,10 @@ catch (error) {
42
42
  async function setup() {
43
43
  const result = await setupSelfHostEnvironment();
44
44
  console.log(result.created
45
- ? "Created .env from .env.example. Existing files were not overwritten."
46
- : "The existing .env file was left unchanged.");
45
+ ? "Created .env from .env.example and generated its private data-encryption key."
46
+ : result.dataEncryptionKeyCreated
47
+ ? "Added the missing private data-encryption key to the existing .env file."
48
+ : "The existing .env file and data-encryption key were left unchanged.");
47
49
  if (result.dockerComposeAvailable) {
48
50
  console.log("Docker Compose is available. You may use the included bot and PostgreSQL services.");
49
51
  }
@@ -102,6 +104,10 @@ Commands:
102
104
  doctor Validate environment, database schema and module packages
103
105
  self-host init --manifest <path> [--output <path>] [--install]
104
106
  Generate a self-hosted starter project from a reviewed manifest
107
+ self-host update --check [--project <path>]
108
+ Check the current project against the reviewed release set
109
+ self-host update --confirm [--project <path>]
110
+ Back up and update exact Helyx dependencies and the lockfile
105
111
  help Show this help`);
106
112
  }
107
113
  //# sourceMappingURL=cli.js.map
package/dist/index.d.ts CHANGED
@@ -2,5 +2,6 @@ export * from "./self-host/catalogue.js";
2
2
  export * from "./self-host/command.js";
3
3
  export * from "./self-host/manifest.js";
4
4
  export * from "./self-host/project-generator.js";
5
+ export * from "./self-host/update.js";
5
6
  export * from "./setup.js";
6
7
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ export * from "./self-host/catalogue.js";
2
2
  export * from "./self-host/command.js";
3
3
  export * from "./self-host/manifest.js";
4
4
  export * from "./self-host/project-generator.js";
5
+ export * from "./self-host/update.js";
5
6
  export * from "./setup.js";
6
7
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,11 @@
1
1
  import { z } from "zod";
2
2
  export declare const selfHostCategories: readonly ["Community", "Safety", "Support", "Automation", "Engagement", "Insights", "Integrations"];
3
+ export declare const selfHostReleaseStatusSchema: z.ZodEnum<{
4
+ planned: "planned";
5
+ testing: "testing";
6
+ beta: "beta";
7
+ live: "live";
8
+ }>;
3
9
  export declare const selfHostModuleSchema: z.ZodObject<{
4
10
  id: z.ZodString;
5
11
  package: z.ZodString;
@@ -20,12 +26,12 @@ export declare const selfHostModuleSchema: z.ZodObject<{
20
26
  unpublished: "unpublished";
21
27
  published: "published";
22
28
  }>;
23
- releaseStatus: z.ZodOptional<z.ZodEnum<{
29
+ releaseStatus: z.ZodEnum<{
24
30
  planned: "planned";
25
31
  testing: "testing";
26
32
  beta: "beta";
27
33
  live: "live";
28
- }>>;
34
+ }>;
29
35
  tier: z.ZodOptional<z.ZodEnum<{
30
36
  free: "free";
31
37
  premium: "premium";
@@ -71,12 +77,12 @@ export declare const selfHostCatalogueSchema: z.ZodObject<{
71
77
  unpublished: "unpublished";
72
78
  published: "published";
73
79
  }>;
74
- releaseStatus: z.ZodOptional<z.ZodEnum<{
80
+ releaseStatus: z.ZodEnum<{
75
81
  planned: "planned";
76
82
  testing: "testing";
77
83
  beta: "beta";
78
84
  live: "live";
79
- }>>;
85
+ }>;
80
86
  tier: z.ZodOptional<z.ZodEnum<{
81
87
  free: "free";
82
88
  premium: "premium";
@@ -23,7 +23,12 @@ const moduleId = z
23
23
  .string()
24
24
  .regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u, "must be a Helyx module ID");
25
25
  const availability = z.enum(["planned", "unpublished", "published"]);
26
- const releaseStatus = z.enum(["planned", "testing", "beta", "live"]);
26
+ export const selfHostReleaseStatusSchema = z.enum([
27
+ "planned",
28
+ "testing",
29
+ "beta",
30
+ "live",
31
+ ]);
27
32
  const moduleTier = z.enum(["free", "premium"]);
28
33
  const componentSchema = z
29
34
  .object({
@@ -49,7 +54,7 @@ export const selfHostModuleSchema = z
49
54
  description: z.string().trim().min(1).max(300),
50
55
  category: z.enum(selfHostCategories),
51
56
  availability,
52
- releaseStatus: releaseStatus.optional(),
57
+ releaseStatus: selfHostReleaseStatusSchema,
53
58
  tier: moduleTier.optional(),
54
59
  })
55
60
  .strict()
@@ -68,6 +73,22 @@ export const selfHostModuleSchema = z
68
73
  message: "must be exact semver for an implemented module",
69
74
  });
70
75
  }
76
+ if (module.releaseStatus === "live" &&
77
+ (module.availability !== "published" || module.version === null)) {
78
+ context.addIssue({
79
+ code: "custom",
80
+ path: ["releaseStatus"],
81
+ message: "live modules must have an exact published version",
82
+ });
83
+ }
84
+ if (module.releaseStatus === "planned" &&
85
+ module.availability !== "planned") {
86
+ context.addIssue({
87
+ code: "custom",
88
+ path: ["releaseStatus"],
89
+ message: "planned modules cannot be published",
90
+ });
91
+ }
71
92
  });
72
93
  export const selfHostCatalogueSchema = z
73
94
  .object({
@@ -1,9 +1,16 @@
1
1
  import { type GeneratedSelfHostProject } from "./project-generator.js";
2
+ import { type SelfHostUpdatePlan } from "./update.js";
2
3
  export interface SelfHostInitArguments {
3
4
  manifestPath: string;
4
5
  outputPath: string;
5
6
  install: boolean;
6
7
  }
7
- export declare function runSelfHostCommand(arguments_: readonly string[]): Promise<GeneratedSelfHostProject>;
8
+ export declare function runSelfHostCommand(arguments_: readonly string[]): Promise<GeneratedSelfHostProject | SelfHostUpdatePlan>;
9
+ export interface SelfHostUpdateArguments {
10
+ projectPath: string;
11
+ check: boolean;
12
+ confirm: boolean;
13
+ }
14
+ export declare function parseSelfHostUpdateArguments(arguments_: readonly string[]): SelfHostUpdateArguments;
8
15
  export declare function parseSelfHostInitArguments(arguments_: readonly string[]): SelfHostInitArguments;
9
16
  //# sourceMappingURL=command.d.ts.map
@@ -2,11 +2,21 @@ import { readFile, stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { loadBundledSelfHostCatalogue } from "./catalogue.js";
4
4
  import { generateSelfHostProject, } from "./project-generator.js";
5
+ import { updateSelfHostProject } from "./update.js";
5
6
  const maximumManifestBytes = 16 * 1_024;
6
7
  export async function runSelfHostCommand(arguments_) {
7
8
  const [subcommand, ...rest] = arguments_;
9
+ if (subcommand === "update") {
10
+ const parsed = parseSelfHostUpdateArguments(rest);
11
+ const plan = await updateSelfHostProject({
12
+ projectDirectory: parsed.projectPath,
13
+ apply: parsed.confirm,
14
+ });
15
+ printUpdatePlan(plan);
16
+ return plan;
17
+ }
8
18
  if (subcommand !== "init") {
9
- throw new Error("Usage: helyx self-host init --manifest <path> [--output <path>] [--install]");
19
+ throw new Error("Usage: helyx self-host <init|update>; run helyx help for details");
10
20
  }
11
21
  const parsed = parseSelfHostInitArguments(rest);
12
22
  const manifestPath = resolve(parsed.manifestPath);
@@ -33,6 +43,58 @@ export async function runSelfHostCommand(arguments_) {
33
43
  console.log(`Created Helyx Self-Hosted starter at ${generated.directory}`);
34
44
  return generated;
35
45
  }
46
+ export function parseSelfHostUpdateArguments(arguments_) {
47
+ let projectPath = ".";
48
+ let projectConfigured = false;
49
+ let check = false;
50
+ let confirm = false;
51
+ for (let index = 0; index < arguments_.length; index += 1) {
52
+ const argument = arguments_[index];
53
+ if (argument === "--check") {
54
+ if (check)
55
+ throw new Error("--check may be specified only once");
56
+ check = true;
57
+ continue;
58
+ }
59
+ if (argument === "--confirm") {
60
+ if (confirm)
61
+ throw new Error("--confirm may be specified only once");
62
+ confirm = true;
63
+ continue;
64
+ }
65
+ if (argument === "--project") {
66
+ const value = arguments_[index + 1];
67
+ if (!value || value.startsWith("--")) {
68
+ throw new Error("--project requires a path");
69
+ }
70
+ if (projectConfigured) {
71
+ throw new Error("--project may be specified only once");
72
+ }
73
+ projectPath = value;
74
+ projectConfigured = true;
75
+ index += 1;
76
+ continue;
77
+ }
78
+ throw new Error(`Unknown self-host update option: ${argument ?? ""}`);
79
+ }
80
+ if (check === confirm) {
81
+ throw new Error("Choose exactly one update mode: --check for a read-only report or --confirm to apply");
82
+ }
83
+ return { projectPath, check, confirm };
84
+ }
85
+ function printUpdatePlan(plan) {
86
+ if (plan.upToDate) {
87
+ console.log(`Helyx is current with reviewed catalogue ${plan.catalogueVersion}.`);
88
+ return;
89
+ }
90
+ console.log(`Reviewed catalogue ${plan.catalogueVersion} changes:`);
91
+ for (const change of plan.changes) {
92
+ console.log(` ${change.package}: ${change.currentVersion} -> ${change.targetVersion}`);
93
+ }
94
+ console.log(plan.changes.length === 1
95
+ ? "1 Helyx package requires an update."
96
+ : `${plan.changes.length} Helyx packages require an update.`);
97
+ }
36
98
  export function parseSelfHostInitArguments(arguments_) {
37
99
  let manifestPath;
38
100
  let outputPath = "./helyx";
@@ -62,8 +62,10 @@ export function createSelfHostBuildManifest(catalogue, selectedModuleIds) {
62
62
  const module = byId.get(id);
63
63
  if (!module)
64
64
  throw new Error(`Unknown self-host module: ${id}`);
65
- if (module.availability !== "published" || module.version === null) {
66
- throw new Error(`Self-host module is not published: ${id}`);
65
+ if (module.availability !== "published" ||
66
+ module.releaseStatus !== "live" ||
67
+ module.version === null) {
68
+ throw new Error(`Self-host module is not live: ${id}`);
67
69
  }
68
70
  return {
69
71
  id: module.id,
@@ -99,6 +101,7 @@ export function parseSelfHostBuildManifest(input, catalogue) {
99
101
  const module = byId.get(selected.id);
100
102
  if (!module ||
101
103
  module.availability !== "published" ||
104
+ module.releaseStatus !== "live" ||
102
105
  module.version === null ||
103
106
  module.package !== selected.package ||
104
107
  module.version !== selected.version) {
@@ -70,6 +70,8 @@ async function writeGeneratedFiles(targetDirectory, manifest, catalogue) {
70
70
  start: "helyx start",
71
71
  doctor: "helyx doctor",
72
72
  migrate: "helyx migrate",
73
+ "update:check": "helyx self-host update --check",
74
+ update: "helyx self-host update --confirm && npm ci",
73
75
  },
74
76
  dependencies,
75
77
  };
@@ -238,7 +240,7 @@ export const selfHostEnvironmentContract = {
238
240
  optional: true,
239
241
  },
240
242
  HELYX_BOT_CONTROL_PORT: { value: "3001", optional: true },
241
- HELYX_CLOUD_CONNECT_ENABLED: { value: "false", optional: false },
243
+ HELYX_CLOUD_CONNECT_ENABLED: { value: "true", optional: false },
242
244
  HELYX_CLOUD_ENROLMENT_TOKEN: {
243
245
  value: "replace-with-a-short-lived-enrolment-code",
244
246
  optional: true,
@@ -0,0 +1,26 @@
1
+ import { type SelfHostCatalogue } from "./catalogue.js";
2
+ export declare const DEFAULT_SELF_HOST_CATALOGUE_URL = "https://api.helyx.gg/self-host/catalogue";
3
+ export interface SelfHostUpdateChange {
4
+ package: string;
5
+ currentVersion: string;
6
+ targetVersion: string;
7
+ direction: "upgrade" | "downgrade";
8
+ }
9
+ export interface SelfHostUpdatePlan {
10
+ projectDirectory: string;
11
+ catalogueVersion: number;
12
+ changes: SelfHostUpdateChange[];
13
+ upToDate: boolean;
14
+ }
15
+ export type SelfHostUpdateCommandRunner = (arguments_: readonly string[], workingDirectory: string) => Promise<void>;
16
+ export interface SelfHostUpdateOptions {
17
+ projectDirectory?: string;
18
+ apply?: boolean;
19
+ catalogue?: SelfHostCatalogue;
20
+ catalogueLoader?: () => Promise<SelfHostCatalogue>;
21
+ commandRunner?: SelfHostUpdateCommandRunner;
22
+ now?: () => Date;
23
+ }
24
+ export declare function updateSelfHostProject(options?: SelfHostUpdateOptions): Promise<SelfHostUpdatePlan>;
25
+ export declare function loadCurrentSelfHostCatalogue(fetcher?: typeof fetch): Promise<SelfHostCatalogue>;
26
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1,305 @@
1
+ import semver from "semver";
2
+ import { createHash } from "node:crypto";
3
+ import { spawn } from "node:child_process";
4
+ import { copyFile, mkdir, readFile, readdir, rename, stat, writeFile, } from "node:fs/promises";
5
+ import { parse, resolve } from "node:path";
6
+ import { z } from "zod";
7
+ import { parseSelfHostCatalogue } from "./catalogue.js";
8
+ export const DEFAULT_SELF_HOST_CATALOGUE_URL = "https://api.helyx.gg/self-host/catalogue";
9
+ const packageName = z.string().regex(/^@helyx\/[a-z0-9][a-z0-9-]*$/u);
10
+ const modulePackage = z.string().regex(/^@helyx\/module-[a-z0-9][a-z0-9-]*$/u);
11
+ const exactVersion = z
12
+ .string()
13
+ .refine((value) => semver.valid(value) === value, "must be exact semver");
14
+ const packageJsonSchema = z
15
+ .object({
16
+ name: z.literal("helyx-self-hosted"),
17
+ private: z.literal(true),
18
+ dependencies: z.record(z.string(), z.string()),
19
+ scripts: z.record(z.string(), z.string()).optional(),
20
+ })
21
+ .passthrough();
22
+ const moduleSelectionSchema = z
23
+ .object({
24
+ schemaVersion: z.literal(1),
25
+ packages: z.array(modulePackage).max(30),
26
+ })
27
+ .strict()
28
+ .superRefine((value, context) => {
29
+ if (new Set(value.packages).size !== value.packages.length) {
30
+ context.addIssue({
31
+ code: "custom",
32
+ path: ["packages"],
33
+ message: "module package names must be unique",
34
+ });
35
+ }
36
+ });
37
+ const managedFiles = [
38
+ ".gitignore",
39
+ "package.json",
40
+ "package-lock.json",
41
+ "helyx.modules.json",
42
+ "SHA256SUMS",
43
+ ];
44
+ export async function updateSelfHostProject(options = {}) {
45
+ const projectDirectory = resolve(options.projectDirectory ?? ".");
46
+ assertSafeProjectDirectory(projectDirectory);
47
+ const [packageJson, selection, catalogue] = await Promise.all([
48
+ readProjectJson(projectDirectory),
49
+ readModuleSelection(projectDirectory),
50
+ options.catalogue
51
+ ? Promise.resolve(options.catalogue)
52
+ : (options.catalogueLoader ?? loadCurrentSelfHostCatalogue)(),
53
+ ]);
54
+ const targetDependencies = targetHelyxDependencies(catalogue, selection);
55
+ validateManagedDependencies(packageJson.dependencies, targetDependencies);
56
+ const changes = collectChanges(packageJson.dependencies, targetDependencies);
57
+ const plan = {
58
+ projectDirectory,
59
+ catalogueVersion: catalogue.catalogueVersion,
60
+ changes,
61
+ upToDate: changes.length === 0,
62
+ };
63
+ if (!options.apply || plan.upToDate)
64
+ return plan;
65
+ const backupDirectory = await createBackup(projectDirectory, options.now?.() ?? new Date());
66
+ try {
67
+ await ensureBackupIgnored(projectDirectory);
68
+ const nextPackageJson = {
69
+ ...packageJson,
70
+ scripts: {
71
+ ...packageJson.scripts,
72
+ "update:check": "helyx self-host update --check",
73
+ update: "helyx self-host update --confirm && npm ci",
74
+ },
75
+ dependencies: {
76
+ ...packageJson.dependencies,
77
+ ...Object.fromEntries(targetDependencies),
78
+ },
79
+ };
80
+ await atomicWriteJson(resolve(projectDirectory, "package.json"), nextPackageJson);
81
+ const runner = options.commandRunner ?? runNpmCommand;
82
+ await runner([
83
+ "install",
84
+ "--package-lock-only",
85
+ "--ignore-scripts",
86
+ "--no-audit",
87
+ "--no-fund",
88
+ ], projectDirectory);
89
+ await validateUpdatedLockfile(projectDirectory, targetDependencies);
90
+ await writeChecksums(projectDirectory);
91
+ return plan;
92
+ }
93
+ catch (error) {
94
+ await restoreBackup(projectDirectory, backupDirectory);
95
+ throw new Error("The self-host update failed and the managed project files were restored", { cause: error });
96
+ }
97
+ }
98
+ export async function loadCurrentSelfHostCatalogue(fetcher = fetch) {
99
+ let response;
100
+ try {
101
+ response = await fetcher(DEFAULT_SELF_HOST_CATALOGUE_URL, {
102
+ headers: { Accept: "application/json" },
103
+ signal: AbortSignal.timeout(15_000),
104
+ });
105
+ }
106
+ catch (error) {
107
+ throw new Error("The reviewed Helyx update catalogue is unavailable", {
108
+ cause: error,
109
+ });
110
+ }
111
+ if (!response.ok) {
112
+ throw new Error(`The reviewed Helyx update catalogue returned HTTP ${response.status}`);
113
+ }
114
+ const declaredLength = Number(response.headers.get("content-length") ?? "0");
115
+ if (declaredLength > 512 * 1_024) {
116
+ throw new Error("The reviewed Helyx update catalogue was too large");
117
+ }
118
+ const text = await response.text();
119
+ if (Buffer.byteLength(text, "utf8") > 512 * 1_024) {
120
+ throw new Error("The reviewed Helyx update catalogue was too large");
121
+ }
122
+ try {
123
+ return parseSelfHostCatalogue(JSON.parse(text));
124
+ }
125
+ catch (error) {
126
+ throw new Error("The reviewed Helyx update catalogue was invalid", {
127
+ cause: error,
128
+ });
129
+ }
130
+ }
131
+ function targetHelyxDependencies(catalogue, selection) {
132
+ if (catalogue.core.availability !== "published" ||
133
+ catalogue.generator.availability !== "published") {
134
+ throw new Error("The reviewed Helyx core release is not available");
135
+ }
136
+ const modulesByPackage = new Map(catalogue.modules.map((module) => [module.package, module]));
137
+ const selected = selection.packages.map((selectedPackage) => {
138
+ const module = modulesByPackage.get(selectedPackage);
139
+ if (!module ||
140
+ module.availability !== "published" ||
141
+ module.releaseStatus !== "live" ||
142
+ module.tier === "premium" ||
143
+ module.version === null) {
144
+ throw new Error(`Selected module is not in the current reviewed free live release: ${selectedPackage}`);
145
+ }
146
+ return [module.package, module.version];
147
+ });
148
+ return new Map([
149
+ [catalogue.core.package, catalogue.core.version],
150
+ [catalogue.generator.package, catalogue.generator.version],
151
+ ...selected,
152
+ ]);
153
+ }
154
+ function validateManagedDependencies(current, target) {
155
+ for (const [name, version] of Object.entries(current)) {
156
+ if (!name.startsWith("@helyx/"))
157
+ continue;
158
+ packageName.parse(name);
159
+ exactVersion.parse(version);
160
+ if (!target.has(name)) {
161
+ throw new Error(`Unexpected Helyx dependency is not controlled by helyx.modules.json: ${name}`);
162
+ }
163
+ }
164
+ for (const name of target.keys()) {
165
+ if (!(name in current)) {
166
+ throw new Error(`Generated project is missing Helyx dependency: ${name}`);
167
+ }
168
+ }
169
+ }
170
+ function collectChanges(current, target) {
171
+ return [...target]
172
+ .filter(([name, version]) => current[name] !== version)
173
+ .map(([name, version]) => ({
174
+ package: name,
175
+ currentVersion: current[name],
176
+ targetVersion: version,
177
+ direction: semver.gt(version, current[name])
178
+ ? "upgrade"
179
+ : "downgrade",
180
+ }))
181
+ .sort((left, right) => left.package.localeCompare(right.package, "en"));
182
+ }
183
+ async function readProjectJson(projectDirectory) {
184
+ const value = JSON.parse(await readFile(resolve(projectDirectory, "package.json"), "utf8"));
185
+ return packageJsonSchema.parse(value);
186
+ }
187
+ async function readModuleSelection(projectDirectory) {
188
+ const value = JSON.parse(await readFile(resolve(projectDirectory, "helyx.modules.json"), "utf8"));
189
+ return moduleSelectionSchema.parse(value);
190
+ }
191
+ async function createBackup(projectDirectory, now) {
192
+ const timestamp = now.toISOString().replaceAll(":", "-");
193
+ const backupDirectory = resolve(projectDirectory, ".helyx", "backups", timestamp);
194
+ await mkdir(resolve(projectDirectory, ".helyx", "backups"), {
195
+ recursive: true,
196
+ });
197
+ await mkdir(backupDirectory);
198
+ for (const fileName of managedFiles) {
199
+ await copyFile(resolve(projectDirectory, fileName), resolve(backupDirectory, fileName));
200
+ }
201
+ return backupDirectory;
202
+ }
203
+ async function ensureBackupIgnored(projectDirectory) {
204
+ const path = resolve(projectDirectory, ".gitignore");
205
+ const current = await readFile(path, "utf8");
206
+ const lines = current
207
+ .replaceAll("\r\n", "\n")
208
+ .replaceAll("\r", "\n")
209
+ .split("\n");
210
+ if (lines.some((line) => line.trim() === ".helyx/"))
211
+ return;
212
+ await atomicWriteText(path, `${current.trimEnd()}\n.helyx/\n`);
213
+ }
214
+ async function restoreBackup(projectDirectory, backupDirectory) {
215
+ for (const fileName of managedFiles) {
216
+ await copyFile(resolve(backupDirectory, fileName), resolve(projectDirectory, fileName));
217
+ }
218
+ }
219
+ async function atomicWriteJson(path, value) {
220
+ const temporaryPath = `${path}.helyx-update`;
221
+ await writeFile(temporaryPath, formatJson(value), "utf8");
222
+ await rename(temporaryPath, path);
223
+ }
224
+ async function validateUpdatedLockfile(projectDirectory, target) {
225
+ const value = JSON.parse(await readFile(resolve(projectDirectory, "package-lock.json"), "utf8"));
226
+ if (!isRecord(value) || !isRecord(value.packages)) {
227
+ throw new Error("Updated package-lock.json is invalid");
228
+ }
229
+ const root = value.packages[""];
230
+ if (!isRecord(root) || !isRecord(root.dependencies)) {
231
+ throw new Error("Updated package-lock.json has no root dependencies");
232
+ }
233
+ for (const [name, version] of target) {
234
+ if (root.dependencies[name] !== version) {
235
+ throw new Error(`Updated package-lock.json does not pin ${name}@${version}`);
236
+ }
237
+ }
238
+ }
239
+ async function writeChecksums(projectDirectory) {
240
+ const fileNames = (await readdir(projectDirectory))
241
+ .filter((fileName) => fileName !== "SHA256SUMS")
242
+ .sort((left, right) => left.localeCompare(right, "en"));
243
+ const lines = [];
244
+ for (const fileName of fileNames) {
245
+ const file = await stat(resolve(projectDirectory, fileName));
246
+ if (!file.isFile())
247
+ continue;
248
+ const digest = createHash("sha256")
249
+ .update(await readFile(resolve(projectDirectory, fileName)))
250
+ .digest("hex");
251
+ lines.push(`${digest} ${fileName}`);
252
+ }
253
+ await atomicWriteText(resolve(projectDirectory, "SHA256SUMS"), `${lines.join("\n")}\n`);
254
+ }
255
+ async function atomicWriteText(path, value) {
256
+ const temporaryPath = `${path}.helyx-update`;
257
+ await writeFile(temporaryPath, normalizeCrLf(value), "utf8");
258
+ await rename(temporaryPath, path);
259
+ }
260
+ function formatJson(value) {
261
+ return normalizeCrLf(`${JSON.stringify(value, null, 2)}\n`);
262
+ }
263
+ function normalizeCrLf(value) {
264
+ return value
265
+ .replaceAll("\r\n", "\n")
266
+ .replaceAll("\r", "\n")
267
+ .replaceAll("\n", "\r\n");
268
+ }
269
+ function assertSafeProjectDirectory(projectDirectory) {
270
+ if (projectDirectory === parse(projectDirectory).root) {
271
+ throw new Error("Refusing to update a filesystem root");
272
+ }
273
+ }
274
+ async function runNpmCommand(arguments_, workingDirectory) {
275
+ const npmCli = process.env.npm_execpath;
276
+ const executable = npmCli ? process.execPath : "npm";
277
+ const commandArguments = npmCli ? [npmCli, ...arguments_] : [...arguments_];
278
+ await new Promise((resolvePromise, reject) => {
279
+ const child = spawn(executable, commandArguments, {
280
+ cwd: workingDirectory,
281
+ env: { ...process.env, npm_config_ignore_scripts: "true" },
282
+ shell: process.platform === "win32" && npmCli === undefined,
283
+ stdio: "inherit",
284
+ });
285
+ const timeout = setTimeout(() => {
286
+ child.kill("SIGTERM");
287
+ reject(new Error("npm command exceeded the two-minute update limit"));
288
+ }, 120_000);
289
+ child.once("error", (error) => {
290
+ clearTimeout(timeout);
291
+ reject(error);
292
+ });
293
+ child.once("close", (code) => {
294
+ clearTimeout(timeout);
295
+ if (code === 0)
296
+ resolvePromise();
297
+ else
298
+ reject(new Error(`npm command failed with exit code ${String(code)}`));
299
+ });
300
+ });
301
+ }
302
+ function isRecord(value) {
303
+ return typeof value === "object" && value !== null && !Array.isArray(value);
304
+ }
305
+ //# sourceMappingURL=update.js.map
package/dist/setup.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export interface SetupSelfHostEnvironmentOptions {
2
2
  cwd?: string;
3
3
  detectDockerCompose?: () => Promise<boolean>;
4
+ createDataEncryptionKey?: () => string;
4
5
  }
5
6
  export interface SetupSelfHostEnvironmentResult {
6
7
  created: boolean;
8
+ dataEncryptionKeyCreated: boolean;
7
9
  dockerComposeAvailable: boolean;
8
10
  environmentPath: string;
9
11
  }
package/dist/setup.js CHANGED
@@ -1,9 +1,12 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { constants } from "node:fs";
2
- import { chmod, copyFile, readFile } from "node:fs/promises";
3
+ import { chmod, copyFile, readFile, writeFile } from "node:fs/promises";
3
4
  import { resolve } from "node:path";
4
5
  import { spawn } from "node:child_process";
5
6
  import { parseEnv } from "node:util";
6
7
  const starterPackageName = "helyx-self-hosted";
8
+ const dataEncryptionKeyName = "HELYX_DATA_ENCRYPTION_KEY";
9
+ const dataEncryptionKeyPlaceholder = "replace-with-a-base64-encoded-32-byte-key";
7
10
  export async function setupSelfHostEnvironment(options = {}) {
8
11
  const cwd = resolve(options.cwd ?? process.cwd());
9
12
  await assertGeneratedStarter(cwd);
@@ -19,12 +22,51 @@ export async function setupSelfHostEnvironment(options = {}) {
19
22
  if (!isFileSystemError(error, "EEXIST"))
20
23
  throw error;
21
24
  }
25
+ const dataEncryptionKeyCreated = await ensureDataEncryptionKey(environmentPath, options.createDataEncryptionKey ?? createDataEncryptionKey);
22
26
  return {
23
27
  created,
28
+ dataEncryptionKeyCreated,
24
29
  dockerComposeAvailable: await (options.detectDockerCompose ?? detectDockerCompose)(),
25
30
  environmentPath,
26
31
  };
27
32
  }
33
+ async function ensureDataEncryptionKey(environmentPath, createKey) {
34
+ const content = await readFile(environmentPath, "utf8");
35
+ const activeKeyPattern = new RegExp(`^[\\t ]*${dataEncryptionKeyName}[\\t ]*=[\\t ]*([^\\r\\n]*?)[\\t ]*(?=\\r?$)`, "mu");
36
+ const activeKey = activeKeyPattern.exec(content);
37
+ if (activeKey &&
38
+ activeKey[1] !== undefined &&
39
+ activeKey[1] !== "" &&
40
+ activeKey[1] !== dataEncryptionKeyPlaceholder) {
41
+ await chmod(environmentPath, 0o600);
42
+ return false;
43
+ }
44
+ const key = createKey();
45
+ const replacement = `${dataEncryptionKeyName}=${key}`;
46
+ let updated;
47
+ if (activeKey) {
48
+ updated = content.replace(activeKeyPattern, replacement);
49
+ }
50
+ else {
51
+ const commentedKeyPattern = new RegExp(`^[\\t ]*#[\\t ]*${dataEncryptionKeyName}[\\t ]*=[^\\r\\n]*(?=\\r?$)`, "mu");
52
+ if (commentedKeyPattern.test(content)) {
53
+ updated = content.replace(commentedKeyPattern, replacement);
54
+ }
55
+ else {
56
+ const newline = content.includes("\r\n") ? "\r\n" : "\n";
57
+ updated = `${content.trimEnd()}${newline}${replacement}${newline}`;
58
+ }
59
+ }
60
+ await writeFile(environmentPath, updated, {
61
+ encoding: "utf8",
62
+ mode: 0o600,
63
+ });
64
+ await chmod(environmentPath, 0o600);
65
+ return true;
66
+ }
67
+ function createDataEncryptionKey() {
68
+ return randomBytes(32).toString("base64");
69
+ }
28
70
  export async function loadLocalEnvironment(cwd = process.cwd(), environment = process.env) {
29
71
  let content;
30
72
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.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": "0.2.0",
56
- "@helyx/config": "0.2.0",
57
- "@helyx/database": "0.2.0",
55
+ "@helyx/bot": "0.2.2",
56
+ "@helyx/config": "0.2.1",
57
+ "@helyx/database": "0.2.1",
58
58
  "semver": "7.8.5",
59
59
  "zod": "4.4.3"
60
60
  },
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "catalogueVersion": 9,
3
+ "catalogueVersion": 11,
4
4
  "core": {
5
5
  "package": "@helyx/bot",
6
- "version": "0.2.0",
6
+ "version": "0.2.2",
7
7
  "availability": "published"
8
8
  },
9
9
  "generator": {
10
10
  "package": "@helyx/cli",
11
- "version": "0.2.1",
11
+ "version": "0.3.0",
12
12
  "command": "helyx",
13
13
  "availability": "published"
14
14
  },
@@ -16,29 +16,32 @@
16
16
  {
17
17
  "id": "helyx.feedback",
18
18
  "package": "@helyx/module-feedback",
19
- "version": "1.0.4",
19
+ "version": "1.0.5",
20
20
  "name": "Feedback",
21
- "description": "Collect structured member feedback in a channel you choose.",
21
+ "description": "Collects simple feedback from Discord server members.",
22
22
  "category": "Community",
23
- "availability": "published"
23
+ "availability": "published",
24
+ "releaseStatus": "live"
24
25
  },
25
26
  {
26
27
  "id": "helyx.welcome",
27
28
  "package": "@helyx/module-welcome",
28
- "version": "1.0.4",
29
+ "version": "1.0.5",
29
30
  "name": "Welcome",
30
- "description": "Greet new members and optionally give them a starting role.",
31
+ "description": "Greets new members and can give them a starting role.",
31
32
  "category": "Community",
32
- "availability": "published"
33
+ "availability": "published",
34
+ "releaseStatus": "live"
33
35
  },
34
36
  {
35
37
  "id": "helyx.suggestions",
36
38
  "package": "@helyx/module-suggestions",
37
- "version": "1.0.4",
39
+ "version": "1.0.5",
38
40
  "name": "Suggestions",
39
- "description": "Collect community ideas with reaction or button voting.",
41
+ "description": "Collects community ideas with configurable reaction or button voting.",
40
42
  "category": "Community",
41
- "availability": "published"
43
+ "availability": "published",
44
+ "releaseStatus": "live"
42
45
  },
43
46
  {
44
47
  "id": "helyx.polls",
@@ -47,7 +50,8 @@
47
50
  "name": "Polls",
48
51
  "description": "Run clear single or multiple-choice polls.",
49
52
  "category": "Community",
50
- "availability": "planned"
53
+ "availability": "planned",
54
+ "releaseStatus": "planned"
51
55
  },
52
56
  {
53
57
  "id": "helyx.starboard",
@@ -56,7 +60,8 @@
56
60
  "name": "Starboard",
57
61
  "description": "Highlight messages your community values.",
58
62
  "category": "Community",
59
- "availability": "planned"
63
+ "availability": "planned",
64
+ "releaseStatus": "planned"
60
65
  },
61
66
  {
62
67
  "id": "helyx.levels",
@@ -65,7 +70,8 @@
65
70
  "name": "Levels",
66
71
  "description": "Optional participation levels and rewards.",
67
72
  "category": "Community",
68
- "availability": "planned"
73
+ "availability": "planned",
74
+ "releaseStatus": "planned"
69
75
  },
70
76
  {
71
77
  "id": "helyx.moderation",
@@ -74,7 +80,8 @@
74
80
  "name": "Moderation",
75
81
  "description": "Warnings, timeouts, kicks and bans with audit context.",
76
82
  "category": "Safety",
77
- "availability": "planned"
83
+ "availability": "planned",
84
+ "releaseStatus": "planned"
78
85
  },
79
86
  {
80
87
  "id": "helyx.automod",
@@ -83,7 +90,8 @@
83
90
  "name": "Auto moderation",
84
91
  "description": "Apply transparent rules to spam and unsafe content.",
85
92
  "category": "Safety",
86
- "availability": "planned"
93
+ "availability": "planned",
94
+ "releaseStatus": "planned"
87
95
  },
88
96
  {
89
97
  "id": "helyx.verification",
@@ -103,7 +111,8 @@
103
111
  "name": "Raid protection",
104
112
  "description": "Detect and slow unusual join activity.",
105
113
  "category": "Safety",
106
- "availability": "planned"
114
+ "availability": "planned",
115
+ "releaseStatus": "planned"
107
116
  },
108
117
  {
109
118
  "id": "helyx.tickets",
@@ -112,7 +121,8 @@
112
121
  "name": "Tickets",
113
122
  "description": "Private support requests with ownership and transcripts.",
114
123
  "category": "Support",
115
- "availability": "planned"
124
+ "availability": "planned",
125
+ "releaseStatus": "planned"
116
126
  },
117
127
  {
118
128
  "id": "helyx.forms",
@@ -121,7 +131,8 @@
121
131
  "name": "Forms",
122
132
  "description": "Collect structured requests through Discord modals.",
123
133
  "category": "Support",
124
- "availability": "planned"
134
+ "availability": "planned",
135
+ "releaseStatus": "planned"
125
136
  },
126
137
  {
127
138
  "id": "helyx.faq",
@@ -130,7 +141,8 @@
130
141
  "name": "Knowledge base",
131
142
  "description": "Answer common questions with maintained server guidance.",
132
143
  "category": "Support",
133
- "availability": "planned"
144
+ "availability": "planned",
145
+ "releaseStatus": "planned"
134
146
  },
135
147
  {
136
148
  "id": "helyx.autoroles",
@@ -139,7 +151,8 @@
139
151
  "name": "Auto roles",
140
152
  "description": "Assign appropriate roles when members join.",
141
153
  "category": "Automation",
142
- "availability": "planned"
154
+ "availability": "planned",
155
+ "releaseStatus": "planned"
143
156
  },
144
157
  {
145
158
  "id": "helyx.reactionroles",
@@ -148,7 +161,8 @@
148
161
  "name": "Self roles",
149
162
  "description": "Let members select approved roles themselves.",
150
163
  "category": "Automation",
151
- "availability": "planned"
164
+ "availability": "planned",
165
+ "releaseStatus": "planned"
152
166
  },
153
167
  {
154
168
  "id": "helyx.reminders",
@@ -157,7 +171,8 @@
157
171
  "name": "Reminders",
158
172
  "description": "Schedule personal and server reminders.",
159
173
  "category": "Automation",
160
- "availability": "planned"
174
+ "availability": "planned",
175
+ "releaseStatus": "planned"
161
176
  },
162
177
  {
163
178
  "id": "helyx.tempvoice",
@@ -166,7 +181,8 @@
166
181
  "name": "Temporary voice",
167
182
  "description": "Create voice rooms that clean themselves up.",
168
183
  "category": "Automation",
169
- "availability": "planned"
184
+ "availability": "planned",
185
+ "releaseStatus": "planned"
170
186
  },
171
187
  {
172
188
  "id": "helyx.scheduler",
@@ -175,7 +191,8 @@
175
191
  "name": "Scheduler",
176
192
  "description": "Run approved messages and actions on a schedule.",
177
193
  "category": "Automation",
178
- "availability": "planned"
194
+ "availability": "planned",
195
+ "releaseStatus": "planned"
179
196
  },
180
197
  {
181
198
  "id": "helyx.events",
@@ -184,7 +201,8 @@
184
201
  "name": "Events",
185
202
  "description": "Plan events and track attendance.",
186
203
  "category": "Engagement",
187
- "availability": "planned"
204
+ "availability": "planned",
205
+ "releaseStatus": "planned"
188
206
  },
189
207
  {
190
208
  "id": "helyx.giveaways",
@@ -193,7 +211,8 @@
193
211
  "name": "Giveaways",
194
212
  "description": "Run auditable giveaways with eligibility rules.",
195
213
  "category": "Engagement",
196
- "availability": "planned"
214
+ "availability": "planned",
215
+ "releaseStatus": "planned"
197
216
  },
198
217
  {
199
218
  "id": "helyx.birthdays",
@@ -202,7 +221,8 @@
202
221
  "name": "Birthdays",
203
222
  "description": "Celebrate opted-in member birthdays.",
204
223
  "category": "Engagement",
205
- "availability": "planned"
224
+ "availability": "planned",
225
+ "releaseStatus": "planned"
206
226
  },
207
227
  {
208
228
  "id": "helyx.counting",
@@ -211,16 +231,18 @@
211
231
  "name": "Counting",
212
232
  "description": "Run a configurable community counting channel.",
213
233
  "category": "Engagement",
214
- "availability": "planned"
234
+ "availability": "planned",
235
+ "releaseStatus": "planned"
215
236
  },
216
237
  {
217
238
  "id": "helyx.logging",
218
239
  "package": "@helyx/module-logging",
219
- "version": "1.0.4",
240
+ "version": "1.0.5",
220
241
  "name": "Logging",
221
- "description": "Post selected Helyx module activity to one server channel.",
242
+ "description": "Posts configurable activity records from enabled Helyx modules.",
222
243
  "category": "Insights",
223
- "availability": "published"
244
+ "availability": "published",
245
+ "releaseStatus": "live"
224
246
  },
225
247
  {
226
248
  "id": "helyx.analytics",
@@ -229,7 +251,8 @@
229
251
  "name": "Analytics",
230
252
  "description": "Understand server activity with privacy-conscious summaries.",
231
253
  "category": "Insights",
232
- "availability": "planned"
254
+ "availability": "planned",
255
+ "releaseStatus": "planned"
233
256
  },
234
257
  {
235
258
  "id": "helyx.health",
@@ -238,7 +261,8 @@
238
261
  "name": "Server health",
239
262
  "description": "Surface configuration problems and missing permissions.",
240
263
  "category": "Insights",
241
- "availability": "planned"
264
+ "availability": "planned",
265
+ "releaseStatus": "planned"
242
266
  },
243
267
  {
244
268
  "id": "helyx.youtube",
@@ -247,7 +271,8 @@
247
271
  "name": "YouTube alerts",
248
272
  "description": "Post when selected channels publish or go live.",
249
273
  "category": "Integrations",
250
- "availability": "planned"
274
+ "availability": "planned",
275
+ "releaseStatus": "planned"
251
276
  },
252
277
  {
253
278
  "id": "helyx.twitch",
@@ -256,7 +281,8 @@
256
281
  "name": "Twitch alerts",
257
282
  "description": "Announce selected live streams.",
258
283
  "category": "Integrations",
259
- "availability": "planned"
284
+ "availability": "planned",
285
+ "releaseStatus": "planned"
260
286
  },
261
287
  {
262
288
  "id": "helyx.rss",
@@ -265,7 +291,8 @@
265
291
  "name": "RSS feeds",
266
292
  "description": "Publish updates from selected feeds.",
267
293
  "category": "Integrations",
268
- "availability": "planned"
294
+ "availability": "planned",
295
+ "releaseStatus": "planned"
269
296
  },
270
297
  {
271
298
  "id": "helyx.github",
@@ -274,7 +301,8 @@
274
301
  "name": "GitHub updates",
275
302
  "description": "Follow releases and repository activity.",
276
303
  "category": "Integrations",
277
- "availability": "planned"
304
+ "availability": "planned",
305
+ "releaseStatus": "planned"
278
306
  }
279
307
  ]
280
308
  }
@@ -1,41 +1,61 @@
1
- # Required Discord configuration.
1
+ # REQUIRED EDIT THESE VALUES
2
+
3
+ # Discord
2
4
  DISCORD_TOKEN=replace-me
3
5
  DISCORD_CLIENT_ID=12345678901234567
4
6
 
5
- # Database defaults for the included Docker Compose service.
6
- # For direct Node.js or managed hosting, replace DATABASE_URL with the
7
- # connection string for your local or hosted PostgreSQL database.
7
+ # Database
8
+ # Docker: keep the URL. Direct Node.js/managed hosting: use your provider URL.
8
9
  DATABASE_URL=postgresql://database:5432/helyx
9
10
  DATABASE_SSL=disable
11
+
12
+ # Included Docker database — both password values must match.
10
13
  PGUSER=helyx
11
14
  PGPASSWORD=replace-with-a-strong-password
15
+ POSTGRES_DB=helyx
16
+ POSTGRES_USER=helyx
17
+ POSTGRES_PASSWORD=replace-with-a-strong-password
12
18
 
13
- # Generated module selection. Keep this file under version control.
14
- HELYX_MODULE_CATALOGUE_PATH=./helyx.modules.json
19
+ # OPTIONAL EVERYDAY SETTINGS
15
20
 
16
- # Optional explicit additions must still be official reviewed packages.
17
- # HELYX_MODULE_PACKAGES=
21
+ # Ready for dashboard linking, but remains unpaired until you use a one-use code.
22
+ HELYX_CLOUD_CONNECT_ENABLED=true
23
+ # HELYX_OWNER_USER_IDS=12345678901234567
24
+
25
+ # trace, debug, info, warn, error, fatal or silent
26
+ LOG_LEVEL=info
27
+
28
+ # Guild-scoped development commands only
18
29
  # DISCORD_INTERNAL_GUILD_ID=12345678901234567
19
30
 
20
- # Optional private dashboard control endpoint. Configure both values together.
31
+ # ADVANCED INTEGRATIONS EDIT ONLY WHEN NEEDED
32
+
33
+ # Additional reviewed module packages
34
+ # HELYX_MODULE_PACKAGES=
35
+
36
+ # Private dashboard control endpoint — configure both together
21
37
  # HELYX_BOT_CONTROL_SECRET=replace-with-a-base64-encoded-32-byte-key
22
38
  # HELYX_BOT_CONTROL_PORT=3001
23
39
 
24
- # Optional Helyx Cloud Connect. It remains fully disabled unless set to true.
25
- HELYX_CLOUD_CONNECT_ENABLED=false
40
+ # Alternative Cloud Connect enrolment the Discord command is preferred
26
41
  # HELYX_CLOUD_ENROLMENT_TOKEN=replace-with-a-short-lived-enrolment-code
27
42
  # HELYX_CLOUD_ENROLMENT_FILE=./helyx.cloud-enrolment.json
43
+
44
+ # DANGER ZONE — HELYX-MANAGED VALUES, DO NOT EDIT
45
+
46
+ # Generated module selection
47
+ HELYX_MODULE_CATALOGUE_PATH=./helyx.modules.json
48
+
49
+ # Generated privately by npm run setup. Never change it after Cloud Connect.
28
50
  # HELYX_DATA_ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key
51
+
52
+ # Official Cloud Connect endpoints
29
53
  # HELYX_CLOUD_API_URL=https://api.helyx.gg
30
54
  # HELYX_CLOUD_ENDPOINT=wss://connect.helyx.gg/v1/connect
31
- # HELYX_OWNER_USER_IDS=12345678901234567
55
+
56
+ # Security audit retention
32
57
  # HELYX_CLOUD_AUDIT_RETENTION_DAYS=90
33
58
  # HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS=365
34
59
 
35
- LOG_LEVEL=info
60
+ # Runtime mode
36
61
  NODE_ENV=production
37
-
38
- # Used only by the included Docker Compose PostgreSQL service.
39
- POSTGRES_DB=helyx
40
- POSTGRES_USER=helyx
41
- POSTGRES_PASSWORD=replace-with-a-strong-password
@@ -1,6 +1,6 @@
1
1
  # Helyx Self-Hosted starter
2
2
 
3
- This private starter was generated from the reviewed Helyx self-host catalogue. It contains deployment configuration and exact npm dependencies, not Helyx source code or credentials.
3
+ This self-host starter was generated from the reviewed Helyx catalogue. It contains deployment configuration and exact npm dependencies, not Helyx source code or credentials.
4
4
 
5
5
  ## Selected modules
6
6
 
@@ -22,7 +22,7 @@ Package installation and module activation are separate. Installed feature modul
22
22
  Use this path on a local machine or VPS without Docker. It requires Node.js `>=24 <27`, npm `>=11`, and a local or hosted PostgreSQL database. Helyx does not require Docker, but PostgreSQL remains mandatory.
23
23
 
24
24
  1. Run `npm ci`.
25
- 2. Run `npm run setup`. Helyx creates `.env` from the supplied example without overwriting an existing file.
25
+ 2. Run `npm run setup`. Helyx creates `.env` from the supplied example and generates its private local data-encryption key. Repeating setup never replaces an existing key or configuration value.
26
26
  3. Edit `.env`, replace every placeholder and set `DATABASE_URL` for your PostgreSQL server.
27
27
  4. Set `DATABASE_SSL=disable` for a trusted local connection, or `require`/`verify-full` according to your database provider.
28
28
  5. Run `npm run doctor`.
@@ -32,7 +32,7 @@ Use this path on a local machine or VPS without Docker. It requires Node.js `>=2
32
32
 
33
33
  Use this path when Docker with Compose support is available. The included configuration runs both Helyx and PostgreSQL 17.
34
34
 
35
- 1. Run `npm ci`, then `npm run setup`. This creates the local configuration safely; the bot itself will still run in Docker.
35
+ 1. Run `npm ci`, then `npm run setup`. This creates the local configuration and data-encryption key safely; the bot itself will still run in Docker.
36
36
  2. Edit `.env` and replace all placeholder credentials and passwords.
37
37
  3. Keep the supplied internal `DATABASE_URL` and `DATABASE_SSL=disable`.
38
38
  4. Run `docker compose up --build -d`.
@@ -41,7 +41,7 @@ Use this path when Docker with Compose support is available. The included config
41
41
 
42
42
  ### Managed bot hosting
43
43
 
44
- Use this path when a provider installs npm packages and runs the package start script for you. Configure `npm start` as the start command and supply every required variable through the provider's environment or variables panel. Use a PostgreSQL database reachable from that provider. A local `.env` file is not required when the host supplies the variables.
44
+ Use this path when a provider installs npm packages and runs the package start script for you. Configure `npm start` as the start command and supply every required variable through the provider's environment or variables panel. Use a PostgreSQL database reachable from that provider. A local `.env` file is not required when the host supplies the variables. Generate `HELYX_DATA_ENCRYPTION_KEY` once with `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"`, store it in the provider's secret-variable panel and retain it across every restart and deployment.
45
45
 
46
46
  If a provider cannot run `npm ci`, execute `npm start`, or connect to PostgreSQL, it cannot run this Helyx starter. A future hosting-specific download may package additional provider files, but it cannot remove the PostgreSQL requirement.
47
47
 
@@ -57,12 +57,17 @@ If a provider cannot run `npm ci`, execute `npm start`, or connect to PostgreSQL
57
57
 
58
58
  Command registration is global or guild-scoped according to each reviewed command contribution. Discord may take time to refresh global commands. Installing a package does not enable its feature for a server; run `/helyx` in the server after startup to enable modules and configure their settings. Use `/permissions` when module or command access policies need to be changed.
59
59
 
60
+ Generated self-host configuration enables the Cloud Connect connector by default, but it remains unpaired and shares nothing with the hosted dashboard until a deployment owner enters a one-use code with `/helyx-cloud connect`. Set `HELYX_CLOUD_CONNECT_ENABLED=false` if this deployment will remain entirely local.
61
+
60
62
  ## Backups and upgrades
61
63
 
62
64
  - Back up PostgreSQL and the complete project directory before an upgrade.
63
65
  - Retain `package-lock.json`; use `npm ci` for repeatable installation.
64
66
  - Do not edit dependency versions or `helyx.modules.json` independently.
65
- - Review Helyx release notes before changing exact dependency versions. There is no automatic updater in the first self-host release.
67
+ - Run `npm run update:check` to compare the complete installed Helyx release set with the current reviewed catalogue.
68
+ - Review the release notes, stop the bot, back up PostgreSQL, then run `npm run update`. Helyx backs up its managed project files, refreshes all exact Helyx versions and the lockfile together, and runs a clean install.
69
+ - Run `npm run doctor` before restarting with `npm start`.
70
+ - Existing starters that do not yet contain these scripts can use `npx --yes @helyx/cli@latest self-host update --check`, followed by the same command with `--confirm`, then `npm ci`.
66
71
  - Removing a package does not automatically delete retained module configuration or data.
67
72
  - Roll back application files and restore a compatible database backup if an upgrade cannot be completed safely.
68
73
 
@@ -1,4 +1,5 @@
1
1
  .env
2
+ .helyx/
2
3
  helyx.cloud-enrolment.json
3
4
  node_modules/
4
5
  npm-debug.log*