@helyx/cli 0.4.35 → 0.4.37

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.37
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden dashboard authentication, privacy execution, control routing, database transactions, and self-host configuration.
8
+
9
+ Add bounded multi-value record reads and use them to batch Polls and Reaction Roles dashboard queries. Improve dashboard presentation, Forms workflows, release safety, and operational tooling across hosted and self-hosted deployments.
10
+
11
+ - Updated dependencies
12
+ - @helyx/database@0.8.0
13
+
14
+ ## 0.4.36
15
+
16
+ ### Patch Changes
17
+
18
+ - Refresh the embedded self-host catalogue for the Forms and server-access core release.
19
+
3
20
  ## 0.4.35
4
21
 
5
22
  ### Patch Changes
@@ -1,13 +1,14 @@
1
1
  import type { BotEnvironmentInput } from "@helyx/config";
2
2
  import type { SelfHostCatalogue } from "./catalogue.js";
3
3
  import { type SelfHostBuildManifest } from "./manifest.js";
4
- export type SelfHostCommandRunner = (arguments_: readonly string[], workingDirectory: string) => Promise<void>;
4
+ export type SelfHostCommandRunner = (arguments_: readonly string[], workingDirectory: string, signal: AbortSignal) => Promise<void>;
5
5
  export interface GenerateSelfHostProjectOptions {
6
6
  catalogue: SelfHostCatalogue;
7
7
  manifest: unknown;
8
8
  targetDirectory: string;
9
9
  install?: boolean;
10
10
  commandRunner?: SelfHostCommandRunner;
11
+ signal?: AbortSignal;
11
12
  }
12
13
  export interface GeneratedSelfHostProject {
13
14
  directory: string;
@@ -14,6 +14,8 @@ const generatedFileNames = [
14
14
  export async function generateSelfHostProject(options) {
15
15
  const manifest = parseSelfHostBuildManifest(options.manifest, options.catalogue);
16
16
  const targetDirectory = resolve(options.targetDirectory);
17
+ const signal = options.signal ?? AbortSignal.timeout(120_000);
18
+ signal.throwIfAborted();
17
19
  assertSafeTargetDirectory(targetDirectory);
18
20
  await mkdir(dirname(targetDirectory), { recursive: true });
19
21
  let created = false;
@@ -21,6 +23,7 @@ export async function generateSelfHostProject(options) {
21
23
  await mkdir(targetDirectory);
22
24
  created = true;
23
25
  await writeGeneratedFiles(targetDirectory, manifest, options.catalogue);
26
+ signal.throwIfAborted();
24
27
  const runner = options.commandRunner ?? runNpmCommand;
25
28
  await runner([
26
29
  "install",
@@ -28,10 +31,12 @@ export async function generateSelfHostProject(options) {
28
31
  "--ignore-scripts",
29
32
  "--no-audit",
30
33
  "--no-fund",
31
- ], targetDirectory);
34
+ ], targetDirectory, signal);
35
+ signal.throwIfAborted();
32
36
  await validateGeneratedLockfile(targetDirectory, manifest);
37
+ signal.throwIfAborted();
33
38
  if (options.install) {
34
- await runner(["ci", "--no-audit", "--no-fund"], targetDirectory);
39
+ await runner(["ci", "--no-audit", "--no-fund"], targetDirectory, signal);
35
40
  }
36
41
  await writeChecksums(targetDirectory);
37
42
  const files = (await readdir(targetDirectory)).sort((left, right) => left.localeCompare(right, "en"));
@@ -63,7 +68,7 @@ async function writeGeneratedFiles(targetDirectory, manifest, catalogue) {
63
68
  private: true,
64
69
  license: "UNLICENSED",
65
70
  type: "module",
66
- packageManager: "npm@11.18.0",
71
+ packageManager: "npm@11.19.0",
67
72
  engines: { node: ">=24 <27", npm: ">=11" },
68
73
  scripts: {
69
74
  setup: "helyx setup",
@@ -177,7 +182,8 @@ function assertSafeTargetDirectory(targetDirectory) {
177
182
  throw new Error("Refusing to generate a self-host project at a filesystem root");
178
183
  }
179
184
  }
180
- async function runNpmCommand(arguments_, workingDirectory) {
185
+ async function runNpmCommand(arguments_, workingDirectory, signal) {
186
+ signal.throwIfAborted();
181
187
  const npmCli = process.env.npm_execpath;
182
188
  const executable = npmCli ? process.execPath : "npm";
183
189
  const commandArguments = npmCli ? [npmCli, ...arguments_] : [...arguments_];
@@ -188,27 +194,66 @@ async function runNpmCommand(arguments_, workingDirectory) {
188
194
  ? { ...process.env, npm_config_ignore_scripts: "true" }
189
195
  : process.env,
190
196
  shell: process.platform === "win32" && npmCli === undefined,
197
+ detached: process.platform !== "win32",
191
198
  stdio: ["ignore", "pipe", "pipe"],
192
199
  });
193
200
  child.stdout.resume();
194
201
  child.stderr.resume();
195
- const timeout = setTimeout(() => {
196
- child.kill("SIGTERM");
197
- reject(new Error("npm command exceeded the two-minute generation limit"));
198
- }, 120_000);
202
+ let aborting = false;
203
+ const cleanup = () => {
204
+ signal.removeEventListener("abort", onAbort);
205
+ };
206
+ const onAbort = () => {
207
+ if (aborting)
208
+ return;
209
+ aborting = true;
210
+ if (process.platform === "win32" && child.pid) {
211
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
212
+ killer.once("error", () => child.kill("SIGTERM"));
213
+ killer.once("close", (code) => {
214
+ if (code !== 0)
215
+ child.kill("SIGTERM");
216
+ });
217
+ }
218
+ else {
219
+ killProcessGroup(child.pid, "SIGKILL", () => child.kill("SIGKILL"));
220
+ }
221
+ };
222
+ signal.addEventListener("abort", onAbort, { once: true });
199
223
  child.once("error", (error) => {
200
- clearTimeout(timeout);
201
- reject(error);
224
+ cleanup();
225
+ reject(aborting ? abortReason(signal) : error);
202
226
  });
203
227
  child.once("close", (code) => {
204
- clearTimeout(timeout);
205
- if (code === 0)
228
+ cleanup();
229
+ if (aborting)
230
+ reject(abortReason(signal));
231
+ else if (code === 0)
206
232
  resolvePromise();
207
233
  else
208
234
  reject(new Error(`npm command failed with exit code ${String(code)}; rerun it in the generated directory for detailed diagnostics`));
209
235
  });
236
+ if (signal.aborted)
237
+ onAbort();
210
238
  });
211
239
  }
240
+ function abortReason(signal) {
241
+ return signal.reason instanceof Error
242
+ ? signal.reason
243
+ : new Error("Self-host generation aborted");
244
+ }
245
+ function killProcessGroup(pid, signal, fallback) {
246
+ if (!pid) {
247
+ fallback();
248
+ return;
249
+ }
250
+ try {
251
+ process.kill(-pid, signal);
252
+ }
253
+ catch {
254
+ fallback();
255
+ }
256
+ }
212
257
  function isRecord(value) {
213
258
  return typeof value === "object" && value !== null && !Array.isArray(value);
214
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/cli",
3
- "version": "0.4.35",
3
+ "version": "0.4.37",
4
4
  "description": "Command-line tools for running and generating self-hosted Helyx deployments.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@helyx/bot": ">=0.4.0 <0.5.0",
56
56
  "@helyx/config": ">=0.5.0 <0.6.0",
57
- "@helyx/database": ">=0.7.0 <0.8.0",
57
+ "@helyx/database": ">=0.8.0 <0.9.0",
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": 31,
3
+ "catalogueVersion": 35,
4
4
  "core": {
5
5
  "package": "@helyx/bot",
6
- "version": "0.4.27",
6
+ "version": "0.4.29",
7
7
  "availability": "published"
8
8
  },
9
9
  "generator": {
10
10
  "package": "@helyx/cli",
11
- "version": "0.4.35",
11
+ "version": "0.4.37",
12
12
  "command": "helyx",
13
13
  "availability": "published"
14
14
  },
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "id": "helyx.feedback",
18
18
  "package": "@helyx/module-feedback",
19
- "version": "1.1.13",
19
+ "version": "1.1.14",
20
20
  "name": "Feedback",
21
21
  "description": "Collects simple feedback from Discord server members.",
22
22
  "category": "Community",
@@ -26,7 +26,7 @@
26
26
  {
27
27
  "id": "helyx.welcome",
28
28
  "package": "@helyx/module-welcome",
29
- "version": "1.1.12",
29
+ "version": "1.1.13",
30
30
  "name": "Welcome",
31
31
  "description": "Greets new members and can give them a starting role.",
32
32
  "category": "Community",
@@ -36,7 +36,7 @@
36
36
  {
37
37
  "id": "helyx.suggestions",
38
38
  "package": "@helyx/module-suggestions",
39
- "version": "1.1.13",
39
+ "version": "1.1.14",
40
40
  "name": "Suggestions",
41
41
  "description": "Collects community ideas with configurable reaction or button voting.",
42
42
  "category": "Community",
@@ -46,7 +46,7 @@
46
46
  {
47
47
  "id": "helyx.polls",
48
48
  "package": "@helyx/module-polls",
49
- "version": "1.0.11",
49
+ "version": "1.0.12",
50
50
  "name": "Polls",
51
51
  "description": "Run clear single or multiple-choice polls.",
52
52
  "category": "Community",
@@ -66,7 +66,7 @@
66
66
  {
67
67
  "id": "helyx.levels",
68
68
  "package": "@helyx/module-levels",
69
- "version": "1.0.8",
69
+ "version": "1.0.9",
70
70
  "name": "Levels",
71
71
  "description": "Optional participation levels and rewards.",
72
72
  "category": "Community",
@@ -146,7 +146,7 @@
146
146
  {
147
147
  "id": "helyx.account-gate",
148
148
  "package": "@helyx/module-account-gate",
149
- "version": "1.0.10",
149
+ "version": "1.0.11",
150
150
  "name": "Access Control",
151
151
  "description": "Require new members to complete configurable Discord interaction checks before gaining access.",
152
152
  "category": "Safety",
@@ -176,12 +176,12 @@
176
176
  {
177
177
  "id": "helyx.forms",
178
178
  "package": "@helyx/module-forms",
179
- "version": null,
179
+ "version": "1.0.1",
180
180
  "name": "Forms",
181
- "description": "Collect structured requests through Discord modals.",
181
+ "description": "Collect reusable structured responses through Discord modals.",
182
182
  "category": "Support & Staff",
183
- "availability": "planned",
184
- "releaseStatus": "planned"
183
+ "availability": "published",
184
+ "releaseStatus": "live"
185
185
  },
186
186
  {
187
187
  "id": "helyx.faq",
@@ -296,7 +296,7 @@
296
296
  {
297
297
  "id": "helyx.reactionroles",
298
298
  "package": "@helyx/module-reactionroles",
299
- "version": "1.0.9",
299
+ "version": "1.0.10",
300
300
  "name": "Reaction Roles",
301
301
  "description": "Let members select approved roles themselves.",
302
302
  "category": "Automation",
@@ -396,7 +396,7 @@
396
396
  {
397
397
  "id": "helyx.giveaways",
398
398
  "package": "@helyx/module-giveaways",
399
- "version": "1.0.11",
399
+ "version": "1.0.12",
400
400
  "name": "Giveaways",
401
401
  "description": "Run auditable giveaways with eligibility rules.",
402
402
  "category": "Engagement",
@@ -416,7 +416,7 @@
416
416
  {
417
417
  "id": "helyx.counting",
418
418
  "package": "@helyx/module-counting",
419
- "version": "1.0.1",
419
+ "version": "1.0.2",
420
420
  "name": "Counting",
421
421
  "description": "A focused, server-scoped Discord counting game.",
422
422
  "category": "Gaming",
@@ -426,7 +426,7 @@
426
426
  {
427
427
  "id": "helyx.trivia",
428
428
  "package": "@helyx/module-trivia",
429
- "version": "1.0.1",
429
+ "version": "1.0.2",
430
430
  "name": "Trivia",
431
431
  "description": "Play private trivia rounds from a saved question catalogue.",
432
432
  "category": "Gaming",
@@ -486,7 +486,7 @@
486
486
  {
487
487
  "id": "helyx.achievements",
488
488
  "package": "@helyx/module-achievements",
489
- "version": "1.0.1",
489
+ "version": "1.0.2",
490
490
  "name": "Achievements",
491
491
  "description": "Configurable member milestones, scores and optional rewards.",
492
492
  "category": "Gaming",
@@ -506,7 +506,7 @@
506
506
  {
507
507
  "id": "helyx.logging",
508
508
  "package": "@helyx/module-logging",
509
- "version": "1.1.12",
509
+ "version": "1.1.13",
510
510
  "name": "Logging",
511
511
  "description": "Posts configurable activity records from enabled Helyx modules.",
512
512
  "category": "Insights",