@helyx/cli 0.2.2 → 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 +24 -0
- package/README.md +4 -0
- package/dist/cli.js +8 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/self-host/command.d.ts +8 -1
- package/dist/self-host/command.js +63 -1
- package/dist/self-host/project-generator.js +3 -1
- package/dist/self-host/update.d.ts +26 -0
- package/dist/self-host/update.js +305 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +43 -1
- package/package.json +3 -3
- package/self-host-catalogue.json +3 -3
- package/templates/self-host/.env.example +38 -18
- package/templates/self-host/README.md +10 -5
- package/templates/self-host/gitignore.template +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
3
27
|
## 0.2.2
|
|
4
28
|
|
|
5
29
|
### 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
|
|
46
|
-
:
|
|
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,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
|
|
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";
|
|
@@ -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: "
|
|
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.
|
|
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.
|
|
55
|
+
"@helyx/bot": "0.2.2",
|
|
56
56
|
"@helyx/config": "0.2.1",
|
|
57
|
-
"@helyx/database": "0.2.
|
|
57
|
+
"@helyx/database": "0.2.1",
|
|
58
58
|
"semver": "7.8.5",
|
|
59
59
|
"zod": "4.4.3"
|
|
60
60
|
},
|
package/self-host-catalogue.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"catalogueVersion":
|
|
3
|
+
"catalogueVersion": 11,
|
|
4
4
|
"core": {
|
|
5
5
|
"package": "@helyx/bot",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.2",
|
|
7
7
|
"availability": "published"
|
|
8
8
|
},
|
|
9
9
|
"generator": {
|
|
10
10
|
"package": "@helyx/cli",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.3.0",
|
|
12
12
|
"command": "helyx",
|
|
13
13
|
"availability": "published"
|
|
14
14
|
},
|
|
@@ -1,41 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# REQUIRED — EDIT THESE VALUES
|
|
2
|
+
|
|
3
|
+
# Discord
|
|
2
4
|
DISCORD_TOKEN=replace-me
|
|
3
5
|
DISCORD_CLIENT_ID=12345678901234567
|
|
4
6
|
|
|
5
|
-
# Database
|
|
6
|
-
#
|
|
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
|
-
#
|
|
14
|
-
HELYX_MODULE_CATALOGUE_PATH=./helyx.modules.json
|
|
19
|
+
# OPTIONAL EVERYDAY SETTINGS
|
|
15
20
|
|
|
16
|
-
#
|
|
17
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|