@helyx/cli 0.3.0 → 0.3.1

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 9e290e1: Load the current reviewed self-host catalogue from the fixed Helyx API endpoint when generating a downloaded starter project, allowing compatible module-only releases without republishing the CLI. Catalogue requests now reject redirects, non-JSON responses, oversized bodies, invalid schemas and unavailable or stale catalogue data.
8
+
3
9
  ## 0.3.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -14,7 +14,7 @@ helyx self-host update --confirm
14
14
 
15
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.
16
16
 
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.
17
+ `self-host init` retrieves the current reviewed catalogue from the fixed Helyx API endpoint, validates a downloaded build manifest against it, creates a new starter-project directory, pins the runtime and selected official modules, generates a lockfile with lifecycle scripts disabled, and writes checksums. The request has strict timeout, response-size, JSON-schema and official-package checks; no arbitrary catalogue URL or stale bundled fallback is accepted. Add `--install` only when the dependencies should also be installed locally. Unknown, unpublished, duplicated, stale or modified package selections fail closed.
18
18
 
19
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
20
 
@@ -1,11 +1,16 @@
1
- import { type GeneratedSelfHostProject } from "./project-generator.js";
1
+ import type { SelfHostCatalogue } from "./catalogue.js";
2
+ import { generateSelfHostProject, type GeneratedSelfHostProject } from "./project-generator.js";
2
3
  import { type SelfHostUpdatePlan } from "./update.js";
3
4
  export interface SelfHostInitArguments {
4
5
  manifestPath: string;
5
6
  outputPath: string;
6
7
  install: boolean;
7
8
  }
8
- export declare function runSelfHostCommand(arguments_: readonly string[]): Promise<GeneratedSelfHostProject | SelfHostUpdatePlan>;
9
+ export interface SelfHostCommandDependencies {
10
+ catalogueLoader?: () => Promise<SelfHostCatalogue>;
11
+ projectGenerator?: typeof generateSelfHostProject;
12
+ }
13
+ export declare function runSelfHostCommand(arguments_: readonly string[], dependencies?: SelfHostCommandDependencies): Promise<GeneratedSelfHostProject | SelfHostUpdatePlan>;
9
14
  export interface SelfHostUpdateArguments {
10
15
  projectPath: string;
11
16
  check: boolean;
@@ -1,16 +1,18 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
- import { loadBundledSelfHostCatalogue } from "./catalogue.js";
4
3
  import { generateSelfHostProject, } from "./project-generator.js";
5
- import { updateSelfHostProject } from "./update.js";
4
+ import { loadCurrentSelfHostCatalogue, updateSelfHostProject, } from "./update.js";
6
5
  const maximumManifestBytes = 16 * 1_024;
7
- export async function runSelfHostCommand(arguments_) {
6
+ export async function runSelfHostCommand(arguments_, dependencies = {}) {
8
7
  const [subcommand, ...rest] = arguments_;
9
8
  if (subcommand === "update") {
10
9
  const parsed = parseSelfHostUpdateArguments(rest);
11
10
  const plan = await updateSelfHostProject({
12
11
  projectDirectory: parsed.projectPath,
13
12
  apply: parsed.confirm,
13
+ ...(dependencies.catalogueLoader
14
+ ? { catalogueLoader: dependencies.catalogueLoader }
15
+ : {}),
14
16
  });
15
17
  printUpdatePlan(plan);
16
18
  return plan;
@@ -33,8 +35,8 @@ export async function runSelfHostCommand(arguments_) {
33
35
  cause: error,
34
36
  });
35
37
  }
36
- const catalogue = await loadBundledSelfHostCatalogue();
37
- const generated = await generateSelfHostProject({
38
+ const catalogue = await (dependencies.catalogueLoader ?? loadCurrentSelfHostCatalogue)();
39
+ const generated = await (dependencies.projectGenerator ?? generateSelfHostProject)({
38
40
  catalogue,
39
41
  manifest,
40
42
  targetDirectory: parsed.outputPath,
@@ -6,6 +6,8 @@ import { parse, resolve } from "node:path";
6
6
  import { z } from "zod";
7
7
  import { parseSelfHostCatalogue } from "./catalogue.js";
8
8
  export const DEFAULT_SELF_HOST_CATALOGUE_URL = "https://api.helyx.gg/self-host/catalogue";
9
+ const maximumCatalogueBytes = 512 * 1_024;
10
+ const catalogueTimeoutMilliseconds = 15_000;
9
11
  const packageName = z.string().regex(/^@helyx\/[a-z0-9][a-z0-9-]*$/u);
10
12
  const modulePackage = z.string().regex(/^@helyx\/module-[a-z0-9][a-z0-9-]*$/u);
11
13
  const exactVersion = z
@@ -100,34 +102,74 @@ export async function loadCurrentSelfHostCatalogue(fetcher = fetch) {
100
102
  try {
101
103
  response = await fetcher(DEFAULT_SELF_HOST_CATALOGUE_URL, {
102
104
  headers: { Accept: "application/json" },
103
- signal: AbortSignal.timeout(15_000),
105
+ redirect: "error",
106
+ signal: AbortSignal.timeout(catalogueTimeoutMilliseconds),
104
107
  });
105
108
  }
106
109
  catch (error) {
107
- throw new Error("The reviewed Helyx update catalogue is unavailable", {
110
+ throw new Error("The reviewed Helyx self-host catalogue is unavailable", {
108
111
  cause: error,
109
112
  });
110
113
  }
111
114
  if (!response.ok) {
112
- throw new Error(`The reviewed Helyx update catalogue returned HTTP ${response.status}`);
115
+ throw new Error(`The reviewed Helyx self-host catalogue returned HTTP ${response.status}`);
113
116
  }
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
+ const contentType = response.headers.get("content-type")?.toLowerCase();
118
+ if (!contentType?.startsWith("application/json")) {
119
+ throw new Error("The reviewed Helyx self-host catalogue did not return JSON");
117
120
  }
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
+ const declaredLengthHeader = response.headers.get("content-length");
122
+ if (declaredLengthHeader !== null) {
123
+ const declaredLength = Number(declaredLengthHeader);
124
+ if (!Number.isSafeInteger(declaredLength) ||
125
+ declaredLength < 0 ||
126
+ declaredLength > maximumCatalogueBytes) {
127
+ throw new Error("The reviewed Helyx self-host catalogue had an invalid size");
128
+ }
121
129
  }
130
+ const text = await readBoundedCatalogueBody(response);
122
131
  try {
123
132
  return parseSelfHostCatalogue(JSON.parse(text));
124
133
  }
125
134
  catch (error) {
126
- throw new Error("The reviewed Helyx update catalogue was invalid", {
135
+ throw new Error("The reviewed Helyx self-host catalogue was invalid", {
127
136
  cause: error,
128
137
  });
129
138
  }
130
139
  }
140
+ async function readBoundedCatalogueBody(response) {
141
+ if (!response.body) {
142
+ throw new Error("The reviewed Helyx self-host catalogue was empty");
143
+ }
144
+ const reader = response.body.getReader();
145
+ const chunks = [];
146
+ let totalBytes = 0;
147
+ try {
148
+ while (true) {
149
+ const { done, value } = await reader.read();
150
+ if (done)
151
+ break;
152
+ totalBytes += value.byteLength;
153
+ if (totalBytes > maximumCatalogueBytes) {
154
+ throw new Error("The reviewed Helyx self-host catalogue was too large");
155
+ }
156
+ chunks.push(value);
157
+ }
158
+ }
159
+ finally {
160
+ reader.releaseLock();
161
+ }
162
+ if (totalBytes === 0) {
163
+ throw new Error("The reviewed Helyx self-host catalogue was empty");
164
+ }
165
+ const bytes = new Uint8Array(totalBytes);
166
+ let offset = 0;
167
+ for (const chunk of chunks) {
168
+ bytes.set(chunk, offset);
169
+ offset += chunk.byteLength;
170
+ }
171
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
172
+ }
131
173
  function targetHelyxDependencies(catalogue, selection) {
132
174
  if (catalogue.core.availability !== "published" ||
133
175
  catalogue.generator.availability !== "published") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Command-line tools for running and generating self-hosted Helyx deployments.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "catalogueVersion": 11,
3
+ "catalogueVersion": 12,
4
4
  "core": {
5
5
  "package": "@helyx/bot",
6
6
  "version": "0.2.2",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "generator": {
10
10
  "package": "@helyx/cli",
11
- "version": "0.3.0",
11
+ "version": "0.3.1",
12
12
  "command": "helyx",
13
13
  "availability": "published"
14
14
  },