@warlock.js/core 5.32.0 → 5.34.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +64 -46
  2. package/esm/application/application-config-types.d.mts +8 -0
  3. package/esm/application/application-config-types.d.mts.map +1 -1
  4. package/esm/application/boot-signal.d.mts +8 -8
  5. package/esm/application/boot-signal.d.mts.map +1 -1
  6. package/esm/application/boot-signal.mjs +15 -9
  7. package/esm/application/boot-signal.mjs.map +1 -1
  8. package/esm/cli/commands/ai/ai-command.action.mjs +140 -0
  9. package/esm/cli/commands/ai/ai-command.action.mjs.map +1 -0
  10. package/esm/cli/commands/ai.command.mjs +50 -0
  11. package/esm/cli/commands/ai.command.mjs.map +1 -0
  12. package/esm/cli/commands/check/check-command.action.mjs +4 -0
  13. package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
  14. package/esm/cli/commands/check.command.mjs +6 -1
  15. package/esm/cli/commands/check.command.mjs.map +1 -1
  16. package/esm/cli/commands/e2e.command.mjs +63 -0
  17. package/esm/cli/commands/e2e.command.mjs.map +1 -0
  18. package/esm/cli/commands/load-e2e-spec.mjs +47 -0
  19. package/esm/cli/commands/load-e2e-spec.mjs.map +1 -0
  20. package/esm/cli/commands/pm2/pm2-command.action.mjs +91 -0
  21. package/esm/cli/commands/pm2/pm2-command.action.mjs.map +1 -0
  22. package/esm/cli/commands/pm2/pm2-config.mjs +115 -0
  23. package/esm/cli/commands/pm2/pm2-config.mjs.map +1 -0
  24. package/esm/cli/commands/pm2.command.mjs +53 -0
  25. package/esm/cli/commands/pm2.command.mjs.map +1 -0
  26. package/esm/cli/framework-cli-commands.mjs +6 -0
  27. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  28. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  29. package/esm/connectors/connectors-manager.mjs +1 -4
  30. package/esm/connectors/connectors-manager.mjs.map +1 -1
  31. package/esm/connectors/shutdown-timeout.mjs +7 -0
  32. package/esm/connectors/shutdown-timeout.mjs.map +1 -0
  33. package/esm/connectors/socket-connector.d.mts.map +1 -1
  34. package/esm/connectors/socket-connector.mjs +8 -1
  35. package/esm/connectors/socket-connector.mjs.map +1 -1
  36. package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs +1 -1
  37. package/esm/dev-server/health-checker/default-health-checkers.mjs +2 -16
  38. package/esm/dev-server/health-checker/default-health-checkers.mjs.map +1 -1
  39. package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +1 -2
  40. package/esm/dev-server/health-checker/file-health-checker.contract.d.mts.map +1 -1
  41. package/esm/dev-server/health-checker/file-health-result.d.mts +1 -1
  42. package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
  43. package/esm/e2e/browser.d.mts +4 -0
  44. package/esm/e2e/browser.d.mts.map +1 -0
  45. package/esm/e2e/browser.mjs +88 -0
  46. package/esm/e2e/browser.mjs.map +1 -0
  47. package/esm/e2e/cdp.d.mts +152 -0
  48. package/esm/e2e/cdp.d.mts.map +1 -0
  49. package/esm/e2e/cdp.mjs +106 -0
  50. package/esm/e2e/cdp.mjs.map +1 -0
  51. package/esm/e2e/fixture.mjs +195 -0
  52. package/esm/e2e/fixture.mjs.map +1 -0
  53. package/esm/e2e/index.d.mts +5 -0
  54. package/esm/e2e/index.mjs +6 -0
  55. package/esm/e2e/owned-process.mjs +193 -0
  56. package/esm/e2e/owned-process.mjs.map +1 -0
  57. package/esm/e2e/page.d.mts +64 -0
  58. package/esm/e2e/page.d.mts.map +1 -0
  59. package/esm/e2e/page.mjs +376 -0
  60. package/esm/e2e/page.mjs.map +1 -0
  61. package/esm/e2e/run-e2e.d.mts +17 -0
  62. package/esm/e2e/run-e2e.d.mts.map +1 -0
  63. package/esm/e2e/run-e2e.mjs +143 -0
  64. package/esm/e2e/run-e2e.mjs.map +1 -0
  65. package/esm/e2e/types.d.mts +123 -0
  66. package/esm/e2e/types.d.mts.map +1 -0
  67. package/esm/e2e/types.mjs +24 -0
  68. package/esm/e2e/types.mjs.map +1 -0
  69. package/esm/generations/features/index.mjs +2 -0
  70. package/esm/generations/features/index.mjs.map +1 -1
  71. package/esm/generations/features/process.feature.mjs +65 -0
  72. package/esm/generations/features/process.feature.mjs.map +1 -0
  73. package/esm/http/port-preflight.d.mts.map +1 -1
  74. package/esm/http/port-preflight.mjs +2 -0
  75. package/esm/http/port-preflight.mjs.map +1 -1
  76. package/esm/index.d.mts +7 -2
  77. package/esm/index.mjs +7 -2
  78. package/esm/secrets/errors.d.mts +9 -0
  79. package/esm/secrets/errors.d.mts.map +1 -0
  80. package/esm/secrets/errors.mjs +24 -0
  81. package/esm/secrets/errors.mjs.map +1 -0
  82. package/esm/secrets/file-backend.mjs +167 -0
  83. package/esm/secrets/file-backend.mjs.map +1 -0
  84. package/esm/secrets/index.d.mts +6 -0
  85. package/esm/secrets/index.mjs +6 -0
  86. package/esm/secrets/keyring-backend.mjs +50 -0
  87. package/esm/secrets/keyring-backend.mjs.map +1 -0
  88. package/esm/secrets/memory-backend.d.mts +12 -0
  89. package/esm/secrets/memory-backend.d.mts.map +1 -0
  90. package/esm/secrets/memory-backend.mjs +28 -0
  91. package/esm/secrets/memory-backend.mjs.map +1 -0
  92. package/esm/secrets/resolve-driver.mjs +14 -0
  93. package/esm/secrets/resolve-driver.mjs.map +1 -0
  94. package/esm/secrets/secret-store.d.mts +25 -0
  95. package/esm/secrets/secret-store.d.mts.map +1 -0
  96. package/esm/secrets/secret-store.mjs +120 -0
  97. package/esm/secrets/secret-store.mjs.map +1 -0
  98. package/esm/secrets/secrets.d.mts +11 -0
  99. package/esm/secrets/secrets.d.mts.map +1 -0
  100. package/esm/secrets/secrets.mjs +72 -0
  101. package/esm/secrets/secrets.mjs.map +1 -0
  102. package/esm/secrets/types.d.mts +26 -0
  103. package/esm/secrets/types.d.mts.map +1 -0
  104. package/esm/socket/index.d.mts +2 -2
  105. package/esm/socket/index.mjs +2 -2
  106. package/esm/socket/utils.d.mts +7 -0
  107. package/esm/socket/utils.d.mts.map +1 -1
  108. package/esm/socket/utils.mjs +12 -1
  109. package/esm/socket/utils.mjs.map +1 -1
  110. package/esm/tests/fakes/index.d.mts +2 -1
  111. package/esm/tests/fakes/index.mjs +2 -1
  112. package/esm/tests/fakes/secrets-fake.d.mts +13 -0
  113. package/esm/tests/fakes/secrets-fake.d.mts.map +1 -0
  114. package/esm/tests/fakes/secrets-fake.mjs +50 -0
  115. package/esm/tests/fakes/secrets-fake.mjs.map +1 -0
  116. package/esm/tests/fakes/test-fakes.d.mts +2 -2
  117. package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
  118. package/esm/tests/fakes/test-fakes.mjs +5 -2
  119. package/esm/tests/fakes/test-fakes.mjs.map +1 -1
  120. package/esm/tests/index.d.mts +2 -1
  121. package/esm/tests/index.mjs +2 -1
  122. package/esm/tests/vitest-setup.d.mts +1 -1
  123. package/esm/tests/vitest-setup.mjs.map +1 -1
  124. package/esm/tooling/check/lint-step.d.mts +2 -8
  125. package/esm/tooling/check/lint-step.d.mts.map +1 -1
  126. package/esm/tooling/check/lint-step.mjs +1 -42
  127. package/esm/tooling/check/lint-step.mjs.map +1 -1
  128. package/esm/tooling/check/run-check.d.mts +2 -0
  129. package/esm/tooling/check/run-check.d.mts.map +1 -1
  130. package/esm/tooling/check/run-check.mjs +3 -1
  131. package/esm/tooling/check/run-check.mjs.map +1 -1
  132. package/esm/tooling/problem.d.mts +1 -1
  133. package/esm/tooling/problem.d.mts.map +1 -1
  134. package/esm/tooling/problem.mjs.map +1 -1
  135. package/esm/updater/package-manager.mjs +5 -1
  136. package/esm/updater/package-manager.mjs.map +1 -1
  137. package/esm/upgrade/codemods/index.mjs +3 -0
  138. package/esm/upgrade/codemods/index.mjs.map +1 -1
  139. package/esm/upgrade/codemods/oxc-toolchain.mjs +191 -0
  140. package/esm/upgrade/codemods/oxc-toolchain.mjs.map +1 -0
  141. package/esm/upgrade/codemods/oxlint-rule-schemas.mjs +6 -0
  142. package/esm/upgrade/codemods/oxlint-rule-schemas.mjs.map +1 -0
  143. package/esm/upgrade/codemods/port-eslint-rules.mjs +343 -0
  144. package/esm/upgrade/codemods/port-eslint-rules.mjs.map +1 -0
  145. package/esm/upgrade/codemods/toolchain-templates.mjs +244 -0
  146. package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -0
  147. package/esm/upgrade/codemods/validate-oxlint-rule.mjs +60 -0
  148. package/esm/upgrade/codemods/validate-oxlint-rule.mjs.map +1 -0
  149. package/esm/upgrade/notes.mjs +14 -7
  150. package/esm/upgrade/notes.mjs.map +1 -1
  151. package/esm/upgrade/run-upgrade.mjs +25 -6
  152. package/esm/upgrade/run-upgrade.mjs.map +1 -1
  153. package/esm/warlock-config/types.d.mts +1 -1
  154. package/llms-full.txt +538 -29
  155. package/llms.txt +5 -3
  156. package/package.json +29 -20
  157. package/skills/README.md +2 -1
  158. package/skills/create-module/SKILL.md +2 -2
  159. package/skills/index.md +1 -1
  160. package/skills/run-app/SKILL.md +115 -6
  161. package/skills/store-secrets/SKILL.md +141 -0
  162. package/skills/test-browser/SKILL.md +119 -0
  163. package/skills/test-fakes/SKILL.md +13 -11
  164. package/skills/upgrade-from-5-29/SKILL.md +85 -7
  165. package/skills/warlock-check/SKILL.md +7 -3
  166. package/skills/warlock-doctor/SKILL.md +8 -0
  167. package/skills/wire-socket/SKILL.md +19 -0
  168. package/skills/write-cli-command/SKILL.md +21 -0
  169. package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +0 -154
  170. package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +0 -1
  171. package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +0 -1
  172. package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +0 -192
  173. package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +0 -1
@@ -0,0 +1,63 @@
1
+ import { command } from "../../commands/cli-command.mjs";
2
+ import { loadE2ESpec } from "./load-e2e-spec.mjs";
3
+ import { resolve } from "node:path";
4
+ import { glob } from "glob";
5
+
6
+ //#region ../../packages/core/src/cli/commands/e2e.command.ts
7
+ function readE2EOptions({ options }) {
8
+ return {
9
+ browser: typeof options.browser === "string" ? options.browser : void 0,
10
+ outputDirectory: typeof options.out === "string" ? options.out : void 0,
11
+ path: typeof options.path === "string" ? options.path.split(",").filter(Boolean).map((path) => resolve(path)) : void 0
12
+ };
13
+ }
14
+ async function e2eCommandAction(data) {
15
+ const patterns = data.args.length ? data.args : ["e2e/**/*.e2e.{ts,mts,js,mjs}"];
16
+ const files = [...new Set(await glob(patterns, {
17
+ absolute: true,
18
+ nodir: true,
19
+ ignore: ["**/node_modules/**", "**/.warlock/**"]
20
+ }))].sort();
21
+ if (!files.length) throw new Error(`No browser e2e specs found: ${patterns.join(", ")}. Create e2e/example.e2e.ts exporting defineE2E({ app, run }).`);
22
+ const { runE2E } = await import("../../e2e/run-e2e.mjs");
23
+ const failures = [];
24
+ for (const file of files) try {
25
+ const loaded = await loadE2ESpec(file);
26
+ try {
27
+ const result = await runE2E(loaded.spec, readE2EOptions(data));
28
+ console.log(`PASS ${file}\nEvidence: ${result.evidenceFile}`);
29
+ } finally {
30
+ await loaded.dispose();
31
+ }
32
+ } catch (error) {
33
+ failures.push(error);
34
+ console.error(`FAIL ${file}\n${String(error)}`);
35
+ }
36
+ if (failures.length) throw new AggregateError(failures, `${failures.length} browser e2e spec(s) failed`);
37
+ }
38
+ const e2eCommand = command({
39
+ name: "e2e [specs...]",
40
+ description: "Run owned Chrome/Edge browser specs over CDP; write screenshots and evidence JSON",
41
+ action: e2eCommandAction,
42
+ options: [
43
+ {
44
+ text: "--browser",
45
+ description: "Chrome/Edge executable path (or WARLOCK_E2E_BROWSER)",
46
+ type: "string"
47
+ },
48
+ {
49
+ text: "--out",
50
+ description: "Evidence parent directory (default .warlock/e2e)",
51
+ type: "string"
52
+ },
53
+ {
54
+ text: "--path",
55
+ description: "Comma-separated fake-binary directories prepended to app PATH",
56
+ type: "string"
57
+ }
58
+ ]
59
+ });
60
+
61
+ //#endregion
62
+ export { e2eCommand, e2eCommandAction, readE2EOptions };
63
+ //# sourceMappingURL=e2e.command.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/e2e.command.ts"],"sourcesContent":["import { glob } from \"glob\";\nimport { resolve } from \"node:path\";\nimport { command } from \"../../commands/cli-command\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { loadE2ESpec } from \"./load-e2e-spec\";\nimport type { RunE2EOptions } from \"../../e2e/types\";\n\nexport function readE2EOptions({ options }: CommandActionData): RunE2EOptions {\n return {\n browser: typeof options.browser === \"string\" ? options.browser : undefined,\n outputDirectory: typeof options.out === \"string\" ? options.out : undefined,\n path:\n typeof options.path === \"string\"\n ? options.path\n .split(\",\")\n .filter(Boolean)\n .map((path) => resolve(path))\n : undefined,\n };\n}\n\nexport async function e2eCommandAction(data: CommandActionData): Promise<void> {\n const patterns = data.args.length ? data.args : [\"e2e/**/*.e2e.{ts,mts,js,mjs}\"];\n const files = [\n ...new Set(\n await glob(patterns, {\n absolute: true,\n nodir: true,\n ignore: [\"**/node_modules/**\", \"**/.warlock/**\"],\n }),\n ),\n ].sort();\n if (!files.length)\n throw new Error(\n `No browser e2e specs found: ${patterns.join(\", \")}. Create e2e/example.e2e.ts exporting defineE2E({ app, run }).`,\n );\n const { runE2E } = await import(\"../../e2e/run-e2e\");\n const failures: unknown[] = [];\n for (const file of files) {\n try {\n const loaded = await loadE2ESpec(file);\n try {\n const result = await runE2E(loaded.spec, readE2EOptions(data));\n console.log(`PASS ${file}\\nEvidence: ${result.evidenceFile}`);\n } finally {\n await loaded.dispose();\n }\n } catch (error) {\n failures.push(error);\n console.error(`FAIL ${file}\\n${String(error)}`);\n }\n }\n if (failures.length)\n throw new AggregateError(failures, `${failures.length} browser e2e spec(s) failed`);\n}\n\nexport const e2eCommand = command({\n name: \"e2e [specs...]\",\n description: \"Run owned Chrome/Edge browser specs over CDP; write screenshots and evidence JSON\",\n action: e2eCommandAction,\n options: [\n {\n text: \"--browser\",\n description: \"Chrome/Edge executable path (or WARLOCK_E2E_BROWSER)\",\n type: \"string\",\n },\n {\n text: \"--out\",\n description: \"Evidence parent directory (default .warlock/e2e)\",\n type: \"string\",\n },\n {\n text: \"--path\",\n description: \"Comma-separated fake-binary directories prepended to app PATH\",\n type: \"string\",\n },\n ],\n});\n"],"mappings":";;;;;;AAOA,SAAgB,eAAe,EAAE,WAA6C;CAC5E,OAAO;EACL,SAAS,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU;EACjE,iBAAiB,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;EACjE,MACE,OAAO,QAAQ,SAAS,WACpB,QAAQ,KACL,MAAM,GAAG,CAAC,CACV,OAAO,OAAO,CAAC,CACf,KAAK,SAAS,QAAQ,IAAI,CAAC,IAC9B;CACR;AACF;AAEA,eAAsB,iBAAiB,MAAwC;CAC7E,MAAM,WAAW,KAAK,KAAK,SAAS,KAAK,OAAO,CAAC,8BAA8B;CAC/E,MAAM,QAAQ,CACZ,GAAG,IAAI,IACL,MAAM,KAAK,UAAU;EACnB,UAAU;EACV,OAAO;EACP,QAAQ,CAAC,sBAAsB,gBAAgB;CACjD,CAAC,CACH,CACF,CAAC,CAAC,KAAK;CACP,IAAI,CAAC,MAAM,QACT,MAAM,IAAI,MACR,+BAA+B,SAAS,KAAK,IAAI,EAAE,+DACrD;CACF,MAAM,EAAE,WAAW,MAAM,OAAO;CAChC,MAAM,WAAsB,CAAC;CAC7B,KAAK,MAAM,QAAQ,OACjB,IAAI;EACF,MAAM,SAAS,MAAM,YAAY,IAAI;EACrC,IAAI;GACF,MAAM,SAAS,MAAM,OAAO,OAAO,MAAM,eAAe,IAAI,CAAC;GAC7D,QAAQ,IAAI,QAAQ,KAAK,cAAc,OAAO,cAAc;EAC9D,UAAU;GACR,MAAM,OAAO,QAAQ;EACvB;CACF,SAAS,OAAO;EACd,SAAS,KAAK,KAAK;EACnB,QAAQ,MAAM,QAAQ,KAAK,IAAI,OAAO,KAAK,GAAG;CAChD;CAEF,IAAI,SAAS,QACX,MAAM,IAAI,eAAe,UAAU,GAAG,SAAS,OAAO,4BAA4B;AACtF;AAEA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { mkdir, rm, writeFile } from "node:fs/promises";
5
+
6
+ //#region ../../packages/core/src/cli/commands/load-e2e-spec.ts
7
+ /** Node 22.15 does not enable TypeScript stripping by default:
8
+ * https://nodejs.org/download/release/v22.15.0/docs/api/typescript.html#type-stripping
9
+ * Bundle local
10
+ * spec imports using core's existing compiler, retaining package imports so
11
+ * the spec uses the same installed harness as the CLI. The temporary module
12
+ * lives inside the project for normal node_modules resolution.
13
+ */
14
+ async function loadE2ESpec(file) {
15
+ const directory = resolve(".warlock/e2e");
16
+ await mkdir(directory, { recursive: true });
17
+ const entry = join(directory, `spec-${randomUUID()}.mjs`);
18
+ try {
19
+ const { build } = await import("esbuild");
20
+ const compiled = (await build({
21
+ entryPoints: [file],
22
+ bundle: true,
23
+ packages: "external",
24
+ platform: "node",
25
+ target: "node22",
26
+ format: "esm",
27
+ sourcemap: "inline",
28
+ write: false,
29
+ logLevel: "silent"
30
+ })).outputFiles[0];
31
+ if (!compiled) throw new Error(`Could not compile browser spec ${file}`);
32
+ await writeFile(entry, compiled.contents, { mode: 384 });
33
+ const module = await import(pathToFileURL(entry).href);
34
+ if (!module.default) throw new Error(`${file} must default-export defineE2E({ app, run })`);
35
+ return {
36
+ spec: module.default,
37
+ dispose: () => rm(entry, { force: true })
38
+ };
39
+ } catch (error) {
40
+ await rm(entry, { force: true });
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ //#endregion
46
+ export { loadE2ESpec };
47
+ //# sourceMappingURL=load-e2e-spec.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-e2e-spec.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/load-e2e-spec.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { mkdir, rm, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { E2ESpec } from \"../../e2e/types\";\n\n/** Node 22.15 does not enable TypeScript stripping by default:\n * https://nodejs.org/download/release/v22.15.0/docs/api/typescript.html#type-stripping\n * Bundle local\n * spec imports using core's existing compiler, retaining package imports so\n * the spec uses the same installed harness as the CLI. The temporary module\n * lives inside the project for normal node_modules resolution.\n */\nexport async function loadE2ESpec(\n file: string,\n): Promise<{ spec: E2ESpec; dispose: () => Promise<void> }> {\n const directory = resolve(\".warlock/e2e\");\n await mkdir(directory, { recursive: true });\n const entry = join(directory, `spec-${randomUUID()}.mjs`);\n try {\n const { build } = await import(\"esbuild\");\n const output = await build({\n entryPoints: [file],\n bundle: true,\n packages: \"external\",\n platform: \"node\",\n target: \"node22\",\n format: \"esm\",\n sourcemap: \"inline\",\n write: false,\n logLevel: \"silent\",\n });\n const compiled = output.outputFiles[0];\n if (!compiled) throw new Error(`Could not compile browser spec ${file}`);\n await writeFile(entry, compiled.contents, { mode: 0o600 });\n const module = (await import(pathToFileURL(entry).href)) as { default?: E2ESpec };\n if (!module.default) throw new Error(`${file} must default-export defineE2E({ app, run })`);\n return { spec: module.default, dispose: () => rm(entry, { force: true }) };\n } catch (error) {\n await rm(entry, { force: true });\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,eAAsB,YACpB,MAC0D;CAC1D,MAAM,YAAY,QAAQ,cAAc;CACxC,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAC1C,MAAM,QAAQ,KAAK,WAAW,QAAQ,WAAW,EAAE,KAAK;CACxD,IAAI;EACF,MAAM,EAAE,UAAU,MAAM,OAAO;EAY/B,MAAM,YAAW,MAXI,MAAM;GACzB,aAAa,CAAC,IAAI;GAClB,QAAQ;GACR,UAAU;GACV,UAAU;GACV,QAAQ;GACR,QAAQ;GACR,WAAW;GACX,OAAO;GACP,UAAU;EACZ,CAAC,EACsB,CAAC,YAAY;EACpC,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,kCAAkC,MAAM;EACvE,MAAM,UAAU,OAAO,SAAS,UAAU,EAAE,MAAM,IAAM,CAAC;EACzD,MAAM,SAAU,MAAM,OAAO,cAAc,KAAK,CAAC,CAAC;EAClD,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,MAAM,GAAG,KAAK,6CAA6C;EAC1F,OAAO;GAAE,MAAM,OAAO;GAAS,eAAe,GAAG,OAAO,EAAE,OAAO,KAAK,CAAC;EAAE;CAC3E,SAAS,OAAO;EACd,MAAM,GAAG,OAAO,EAAE,OAAO,KAAK,CAAC;EAC/B,MAAM;CACR;AACF"}
@@ -0,0 +1,91 @@
1
+ import { config } from "../../../config/config-getter.mjs";
2
+ import { resolveBuildConfig } from "../../../production/resolve-build-config.mjs";
3
+ import { checkDistReadyToStartAsync } from "../../../production/assert-dist-ready-to-start.mjs";
4
+ import { createPm2Config, selectPm2Targets, writePm2Config } from "./pm2-config.mjs";
5
+ import path from "node:path";
6
+ import { readFile } from "node:fs/promises";
7
+ import { execFile } from "node:child_process";
8
+
9
+ //#region ../../packages/core/src/cli/commands/pm2/pm2-command.action.ts
10
+ /** No shell, no patterns, no import of PM2 or dependency on its daemon for init. */
11
+ function runPm2(args, cwd) {
12
+ return new Promise((resolve, reject) => {
13
+ const child = execFile("pm2", args, {
14
+ cwd,
15
+ encoding: "utf8",
16
+ maxBuffer: 8388608
17
+ }, (error, stdout) => {
18
+ if (error) reject(/* @__PURE__ */ new Error(`PM2 command failed (CLI pid ${child.pid ?? "unavailable"}). Install PM2 separately and ensure it is on PATH. ${error.message}`));
19
+ else resolve(stdout);
20
+ });
21
+ });
22
+ }
23
+ function stringOption(data, name) {
24
+ const value = data.options[name];
25
+ if (value === void 0) return void 0;
26
+ if (typeof value !== "string" || value.length === 0) throw new Error(`--${name} requires a value.`);
27
+ return value;
28
+ }
29
+ async function pm2CommandAction(data) {
30
+ const [action, ...extra] = data.args;
31
+ if (action !== "init" && action !== "reload" || extra.length > 0) throw new Error("Usage: warlock pm2 init | warlock pm2 reload [--id <pm2-id>]");
32
+ const allowed = new Set(action === "init" ? [
33
+ "project",
34
+ "app",
35
+ "cluster",
36
+ "instances",
37
+ "listenTimeout"
38
+ ] : [
39
+ "project",
40
+ "app",
41
+ "id"
42
+ ]);
43
+ for (const key of Object.keys(data.options)) if (!allowed.has(key)) throw new Error(`Unsupported option for pm2 ${action}: ${key}`);
44
+ if (data.options.cluster !== void 0 && typeof data.options.cluster !== "boolean") throw new Error("--cluster must be a boolean.");
45
+ const root = process.cwd();
46
+ const packageJson = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
47
+ const packageName = typeof packageJson === "object" && packageJson !== null && "name" in packageJson && typeof packageJson.name === "string" ? packageJson.name : void 0;
48
+ const project = stringOption(data, "project") ?? packageName;
49
+ if (!project) throw new Error("Set package.json name or pass --project to identify this project.");
50
+ const app = config.get("app") ?? {};
51
+ const appOverride = stringOption(data, "app");
52
+ const build = resolveBuildConfig();
53
+ const input = {
54
+ root,
55
+ project,
56
+ app: appOverride === void 0 ? app : {
57
+ ...app,
58
+ appName: appOverride
59
+ },
60
+ environment: process.env.NODE_ENV ?? "development",
61
+ entryPath: build.entryPath,
62
+ sourcemap: build.sourcemap,
63
+ http: config.get("http"),
64
+ socket: config.get("socket")
65
+ };
66
+ if (action === "init") {
67
+ const generated = createPm2Config(input, {
68
+ cluster: data.options.cluster === true,
69
+ instances: stringOption(data, "instances"),
70
+ listenTimeout: stringOption(data, "listenTimeout")
71
+ });
72
+ const target = await writePm2Config(root, generated.content);
73
+ console.log(`Created ${target} for ${generated.app.name}.`);
74
+ console.log(`Readiness URL: ${generated.readinessUrl ?? "unavailable (HTTP/health disabled or ephemeral port)"}`);
75
+ console.log(generated.app.exec_mode === "cluster" ? "Cluster reload replaces workers in sequence; a PM2 readiness timeout can still cause downtime." : "Fork mode uses a restart on reload; use --cluster for rolling HTTP reloads.");
76
+ return;
77
+ }
78
+ const readiness = await checkDistReadyToStartAsync(build.outdir);
79
+ if (!readiness.ready) throw new Error(readiness.reason);
80
+ const list = JSON.parse(await runPm2(["jlist"], root));
81
+ const targets = selectPm2Targets(list, input, stringOption(data, "id"));
82
+ if (targets.some((target) => !target.cluster)) console.warn("Fork mode reload falls back to restart; expect a brief interruption.");
83
+ for (const target of targets) {
84
+ const output = await runPm2(["reload", String(target.id)], root);
85
+ if (output) process.stdout.write(output);
86
+ }
87
+ }
88
+
89
+ //#endregion
90
+ export { pm2CommandAction, runPm2 };
91
+ //# sourceMappingURL=pm2-command.action.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pm2-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/pm2/pm2-command.action.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { AppConfigurations } from \"../../../application/application-config-types\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { config } from \"../../../config/config-getter\";\nimport type { HttpConfigurations } from \"../../../http/types\";\nimport { checkDistReadyToStartAsync } from \"../../../production/assert-dist-ready-to-start\";\nimport { resolveBuildConfig } from \"../../../production/resolve-build-config\";\nimport type { SocketOptions } from \"../../../socket/types\";\nimport { createPm2Config, selectPm2Targets, writePm2Config } from \"./pm2-config\";\n\n/** No shell, no patterns, no import of PM2 or dependency on its daemon for init. */\nexport function runPm2(args: string[], cwd: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"pm2\",\n args,\n { cwd, encoding: \"utf8\", maxBuffer: 8 * 1024 * 1024 },\n (error, stdout) => {\n if (error) {\n reject(\n new Error(\n `PM2 command failed (CLI pid ${child.pid ?? \"unavailable\"}). Install PM2 separately and ensure it is on PATH. ${error.message}`,\n ),\n );\n } else {\n resolve(stdout);\n }\n },\n );\n // Retain the child and its PID; this function never stops a daemon or any\n // application process. PM2 reload is invoked only with validated ids.\n });\n}\n\nfunction stringOption(data: CommandActionData, name: string): string | undefined {\n const value = data.options[name];\n if (value === undefined) return undefined;\n if (typeof value !== \"string\" || value.length === 0)\n throw new Error(`--${name} requires a value.`);\n return value;\n}\n\nexport async function pm2CommandAction(data: CommandActionData): Promise<void> {\n const [action, ...extra] = data.args;\n if ((action !== \"init\" && action !== \"reload\") || extra.length > 0) {\n throw new Error(\"Usage: warlock pm2 init | warlock pm2 reload [--id <pm2-id>]\");\n }\n const allowed = new Set(\n action === \"init\"\n ? [\"project\", \"app\", \"cluster\", \"instances\", \"listenTimeout\"]\n : [\"project\", \"app\", \"id\"],\n );\n for (const key of Object.keys(data.options)) {\n if (!allowed.has(key)) throw new Error(`Unsupported option for pm2 ${action}: ${key}`);\n }\n if (data.options.cluster !== undefined && typeof data.options.cluster !== \"boolean\") {\n throw new Error(\"--cluster must be a boolean.\");\n }\n\n const root = process.cwd();\n const packageJson: unknown = JSON.parse(await readFile(path.join(root, \"package.json\"), \"utf8\"));\n const packageName =\n typeof packageJson === \"object\" &&\n packageJson !== null &&\n \"name\" in packageJson &&\n typeof packageJson.name === \"string\"\n ? packageJson.name\n : undefined;\n const project = stringOption(data, \"project\") ?? packageName;\n if (!project)\n throw new Error(\"Set package.json name or pass --project to identify this project.\");\n const app = config.get<AppConfigurations | undefined>(\"app\") ?? {};\n const appOverride = stringOption(data, \"app\");\n const build = resolveBuildConfig();\n const input = {\n root,\n project,\n app: appOverride === undefined ? app : { ...app, appName: appOverride },\n environment: process.env.NODE_ENV ?? \"development\",\n entryPath: build.entryPath,\n sourcemap: build.sourcemap,\n http: config.get<HttpConfigurations | undefined>(\"http\"),\n socket: config.get<SocketOptions | undefined>(\"socket\"),\n };\n\n if (action === \"init\") {\n const generated = createPm2Config(input, {\n cluster: data.options.cluster === true,\n instances: stringOption(data, \"instances\"),\n listenTimeout: stringOption(data, \"listenTimeout\"),\n });\n const target = await writePm2Config(root, generated.content);\n console.log(`Created ${target} for ${generated.app.name}.`);\n console.log(\n `Readiness URL: ${generated.readinessUrl ?? \"unavailable (HTTP/health disabled or ephemeral port)\"}`,\n );\n console.log(\n generated.app.exec_mode === \"cluster\"\n ? \"Cluster reload replaces workers in sequence; a PM2 readiness timeout can still cause downtime.\"\n : \"Fork mode uses a restart on reload; use --cluster for rolling HTTP reloads.\",\n );\n return;\n }\n\n const readiness = await checkDistReadyToStartAsync(build.outdir);\n if (!readiness.ready) throw new Error(readiness.reason);\n // jlist is JSON per PM2's API.js. Match exact identity locally, then reload\n // numeric ids only; a name collision never broadens the operation.\n // https://github.com/Unitech/pm2/blob/master/lib/API.js\n const list: unknown = JSON.parse(await runPm2([\"jlist\"], root));\n const targets = selectPm2Targets(list, input, stringOption(data, \"id\"));\n if (targets.some((target) => !target.cluster)) {\n console.warn(\"Fork mode reload falls back to restart; expect a brief interruption.\");\n }\n for (const target of targets) {\n const output = await runPm2([\"reload\", String(target.id)], root);\n if (output) process.stdout.write(output);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import { resolveBindPort } from "../../../http/resolve-bind-port.mjs";
2
+ import { DEFAULT_SHUTDOWN_TIMEOUT } from "../../../connectors/shutdown-timeout.mjs";
3
+ import path from "node:path";
4
+ import { writeFile } from "node:fs/promises";
5
+
6
+ //#region ../../packages/core/src/cli/commands/pm2/pm2-config.ts
7
+ /** Refuse selectors PM2 could interpret as patterns, file names or options. */
8
+ function nameComponent(value, label) {
9
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value)) throw new Error(`${label} must contain only letters, digits, underscores and hyphens, starting with a letter or digit.`);
10
+ return value;
11
+ }
12
+ function pm2AppName(input) {
13
+ return [
14
+ nameComponent(input.project.replace(/^@([^/]+)\/([^/]+)$/, "$1-$2"), "Project name (--project)"),
15
+ nameComponent(input.app.appName ?? "app", "App name (--app or app.appName)"),
16
+ nameComponent(input.environment, "NODE_ENV")
17
+ ].join("-");
18
+ }
19
+ function positiveInteger(value, label) {
20
+ const number = typeof value === "number" ? value : /^\d+$/.test(value) ? Number(value) : NaN;
21
+ if (!Number.isSafeInteger(number) || number <= 0 || number > 2147483647) throw new Error(`${label} must be a positive integer no greater than 2147483647.`);
22
+ return number;
23
+ }
24
+ function assertClusterSockets(socket) {
25
+ if (!socket) return;
26
+ if (typeof socket.adapter !== "function") throw new Error("PM2 cluster mode with sockets requires socket.adapter for broadcasts between workers. Configure an adapter such as @socket.io/redis-adapter, or use fork mode.");
27
+ if (socket.options?.transports?.length !== 1 || socket.options.transports[0] !== "websocket") throw new Error("PM2 cluster sockets require socket.options.transports: [\"websocket\"] and WebSocket-only clients. Polling needs sticky routing to individual workers; use separate fork listeners behind a sticky proxy for polling.");
28
+ }
29
+ function readinessUrl(http) {
30
+ if (!http || http.health?.enabled === false) return void 0;
31
+ const port = resolveBindPort(http.port);
32
+ if (port === 0) return void 0;
33
+ const bindHost = http.host ?? "localhost";
34
+ const host = bindHost === "0.0.0.0" ? "127.0.0.1" : bindHost === "::" ? "::1" : bindHost;
35
+ const authority = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
36
+ const probePath = http.health?.readinessPath ?? "/ready";
37
+ if (!probePath.startsWith("/") || probePath.startsWith("//") || /[\r\n]/.test(probePath)) throw new Error("http.health.readinessPath must be a local absolute URL path.");
38
+ return `${http.serverOptions?.https ? "https" : "http"}://${authority}:${port}${probePath}`;
39
+ }
40
+ function createPm2Config(input, options = {}) {
41
+ const cluster = options.cluster === true;
42
+ if (!cluster && options.instances !== void 0) throw new Error("--instances requires --cluster; fork mode runs exactly one process.");
43
+ if (cluster) assertClusterSockets(input.socket);
44
+ const instances = cluster ? options.instances === "max" ? "max" : positiveInteger(options.instances ?? "2", "--instances") : 1;
45
+ const app = {
46
+ name: pm2AppName(input),
47
+ cwd: path.resolve(input.root),
48
+ script: path.resolve(input.root, input.entryPath),
49
+ interpreter: process.execPath,
50
+ node_args: input.sourcemap === false ? [] : ["--enable-source-maps"],
51
+ exec_mode: cluster ? "cluster" : "fork",
52
+ instances,
53
+ wait_ready: true,
54
+ listen_timeout: positiveInteger(options.listenTimeout ?? "30000", "--listen-timeout"),
55
+ kill_timeout: positiveInteger(input.app.shutdownTimeout ?? 1e4, "app.shutdownTimeout"),
56
+ time: false,
57
+ merge_logs: false,
58
+ env: { NODE_ENV: input.environment }
59
+ };
60
+ const url = readinessUrl(input.http);
61
+ return {
62
+ app,
63
+ content: [
64
+ "// Generated by warlock pm2 init. Rebuild the app before starting PM2.",
65
+ "// PM2 timeouts: https://pm2.keymetrics.io/docs/usage/signals-clean-restart/",
66
+ `// Readiness URL (external probe; PM2 does not poll it): ${JSON.stringify(url ?? "unavailable: HTTP/health disabled or ephemeral port").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}`,
67
+ `module.exports = ${JSON.stringify({ apps: [app] }, null, 2)};`,
68
+ ""
69
+ ].join("\n"),
70
+ readinessUrl: url
71
+ };
72
+ }
73
+ async function writePm2Config(root, content) {
74
+ const target = path.resolve(root, "ecosystem.config.cjs");
75
+ await writeFile(target, content, {
76
+ encoding: "utf8",
77
+ flag: "wx"
78
+ });
79
+ return target;
80
+ }
81
+ function record(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
83
+ }
84
+ /** Validate the entire target set before issuing any mutating PM2 command. */
85
+ function selectPm2Targets(list, input, id) {
86
+ if (!Array.isArray(list)) throw new Error("PM2 jlist did not return a process array.");
87
+ if (id !== void 0 && (!/^\d+$/.test(id) || !Number.isSafeInteger(Number(id)))) throw new Error("--id must be a numeric PM2 id.");
88
+ const name = pm2AppName(input);
89
+ const targets = [];
90
+ for (const item of list) {
91
+ const process = record(item);
92
+ if (!process) throw new Error("Invalid PM2 process entry.");
93
+ if (id !== void 0 ? process.pm_id !== Number(id) : process.name !== name) continue;
94
+ const env = record(process.pm2_env);
95
+ if (process.name !== name || !env || env.pm_cwd !== path.resolve(input.root) || env.pm_exec_path !== path.resolve(input.root, input.entryPath)) throw new Error(`Refusing to reload PM2 id ${String(process.pm_id)}: name, working directory or bundle does not belong to this app.`);
96
+ if (typeof process.pm_id !== "number" || !Number.isSafeInteger(process.pm_id) || process.pm_id < 0) throw new Error("Invalid PM2 id in process list.");
97
+ if (env.exec_mode !== "fork_mode" && env.exec_mode !== "cluster_mode") throw new Error("Unrecognized PM2 execution mode.");
98
+ const cluster = env.exec_mode === "cluster_mode";
99
+ if (cluster) {
100
+ assertClusterSockets(input.socket);
101
+ if (env.wait_ready !== true || env.status !== "online") throw new Error("Rolling reload requires an online cluster worker with wait_ready: true. Check its PM2 configuration and status.");
102
+ }
103
+ targets.push({
104
+ id: process.pm_id,
105
+ cluster
106
+ });
107
+ }
108
+ if (targets.length === 0) throw new Error(`No PM2 process belonging to ${name} was found. Start its ecosystem file first.`);
109
+ if (new Set(targets.map((target) => target.id)).size !== targets.length) throw new Error("Duplicate PM2 ids in process list.");
110
+ return targets;
111
+ }
112
+
113
+ //#endregion
114
+ export { assertClusterSockets, createPm2Config, pm2AppName, selectPm2Targets, writePm2Config };
115
+ //# sourceMappingURL=pm2-config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pm2-config.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/pm2/pm2-config.ts"],"sourcesContent":["import path from \"node:path\";\nimport { writeFile } from \"node:fs/promises\";\nimport type { AppConfigurations } from \"../../../application/application-config-types\";\nimport { DEFAULT_SHUTDOWN_TIMEOUT } from \"../../../connectors/shutdown-timeout\";\nimport { resolveBindPort } from \"../../../http/resolve-bind-port\";\nimport type { HttpConfigurations } from \"../../../http/types\";\nimport type { SocketOptions } from \"../../../socket/types\";\nimport type { ResolvedBuildConfig } from \"../../../production/resolve-build-config\";\n\nexport type Pm2AppInput = {\n root: string;\n project: string;\n app: AppConfigurations;\n environment: string;\n entryPath: string;\n sourcemap: ResolvedBuildConfig[\"sourcemap\"];\n http?: HttpConfigurations;\n socket?: SocketOptions;\n};\n\nexport type Pm2InitOptions = {\n cluster?: boolean;\n instances?: string;\n listenTimeout?: string;\n};\n\n/** Refuse selectors PM2 could interpret as patterns, file names or options. */\nfunction nameComponent(value: string, label: string): string {\n if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value)) {\n throw new Error(\n `${label} must contain only letters, digits, underscores and hyphens, starting with a letter or digit.`,\n );\n }\n return value;\n}\n\nexport function pm2AppName(input: Pick<Pm2AppInput, \"project\" | \"app\" | \"environment\">): string {\n // Preserve the npm scope in the project identity instead of dropping it.\n const project = input.project.replace(/^@([^/]+)\\/([^/]+)$/, \"$1-$2\");\n return [\n nameComponent(project, \"Project name (--project)\"),\n nameComponent(input.app.appName ?? \"app\", \"App name (--app or app.appName)\"),\n nameComponent(input.environment, \"NODE_ENV\"),\n ].join(\"-\");\n}\n\nfunction positiveInteger(value: number | string, label: string): number {\n const number = typeof value === \"number\" ? value : /^\\d+$/.test(value) ? Number(value) : NaN;\n if (!Number.isSafeInteger(number) || number <= 0 || number > 2_147_483_647) {\n throw new Error(`${label} must be a positive integer no greater than 2147483647.`);\n }\n return number;\n}\n\nexport function assertClusterSockets(socket?: SocketOptions): void {\n if (!socket) return;\n if (typeof socket.adapter !== \"function\") {\n throw new Error(\n \"PM2 cluster mode with sockets requires socket.adapter for broadcasts between workers. Configure an adapter such as @socket.io/redis-adapter, or use fork mode.\",\n );\n }\n // A proxy sticky to a shared PM2 port is not sticky to its workers. We do\n // not install primary-process routing. Fail closed for long-polling.\n // https://socket.io/docs/v4/using-multiple-nodes/\n if (socket.options?.transports?.length !== 1 || socket.options.transports[0] !== \"websocket\") {\n throw new Error(\n 'PM2 cluster sockets require socket.options.transports: [\"websocket\"] and WebSocket-only clients. Polling needs sticky routing to individual workers; use separate fork listeners behind a sticky proxy for polling.',\n );\n }\n}\n\nfunction readinessUrl(http?: HttpConfigurations): string | undefined {\n if (!http || http.health?.enabled === false) return undefined;\n const port = resolveBindPort(http.port);\n if (port === 0) return undefined;\n const bindHost = http.host ?? \"localhost\";\n const host = bindHost === \"0.0.0.0\" ? \"127.0.0.1\" : bindHost === \"::\" ? \"::1\" : bindHost;\n const authority = host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n const probePath = http.health?.readinessPath ?? \"/ready\";\n if (!probePath.startsWith(\"/\") || probePath.startsWith(\"//\") || /[\\r\\n]/.test(probePath)) {\n throw new Error(\"http.health.readinessPath must be a local absolute URL path.\");\n }\n return `${http.serverOptions?.https ? \"https\" : \"http\"}://${authority}:${port}${probePath}`;\n}\n\nexport function createPm2Config(input: Pm2AppInput, options: Pm2InitOptions = {}) {\n const cluster = options.cluster === true;\n if (!cluster && options.instances !== undefined) {\n throw new Error(\"--instances requires --cluster; fork mode runs exactly one process.\");\n }\n if (cluster) assertClusterSockets(input.socket);\n const instances = cluster\n ? options.instances === \"max\"\n ? \"max\"\n : positiveInteger(options.instances ?? \"2\", \"--instances\")\n : 1;\n const app = {\n name: pm2AppName(input),\n cwd: path.resolve(input.root),\n // Run the bundle itself, not the `warlock start` child supervisor: PM2\n // must own the process that listens and sends its readiness message.\n script: path.resolve(input.root, input.entryPath),\n interpreter: process.execPath,\n node_args: input.sourcemap === false ? [] : [\"--enable-source-maps\"],\n exec_mode: cluster ? \"cluster\" : \"fork\",\n instances,\n wait_ready: true,\n listen_timeout: positiveInteger(options.listenTimeout ?? \"30000\", \"--listen-timeout\"),\n kill_timeout: positiveInteger(\n input.app.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT,\n \"app.shutdownTimeout\",\n ),\n // Keep @warlock.js/logger messages intact; leave per-worker log suffixes.\n time: false,\n merge_logs: false,\n env: { NODE_ENV: input.environment },\n };\n const url = readinessUrl(input.http);\n // JSON encoding keeps project/config strings from becoming executable code\n // or escaping a comment in this CommonJS file.\n const content = [\n \"// Generated by warlock pm2 init. Rebuild the app before starting PM2.\",\n \"// PM2 timeouts: https://pm2.keymetrics.io/docs/usage/signals-clean-restart/\",\n `// Readiness URL (external probe; PM2 does not poll it): ${JSON.stringify(\n url ?? \"unavailable: HTTP/health disabled or ephemeral port\",\n )\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\")}`,\n `module.exports = ${JSON.stringify({ apps: [app] }, null, 2)};`,\n \"\",\n ].join(\"\\n\");\n return { app, content, readinessUrl: url };\n}\n\nexport async function writePm2Config(root: string, content: string): Promise<string> {\n const target = path.resolve(root, \"ecosystem.config.cjs\");\n // Atomic create: an existing/customized ecosystem is never overwritten.\n await writeFile(target, content, { encoding: \"utf8\", flag: \"wx\" });\n return target;\n}\n\nexport type Pm2Target = { id: number; cluster: boolean };\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/** Validate the entire target set before issuing any mutating PM2 command. */\nexport function selectPm2Targets(list: unknown, input: Pm2AppInput, id?: string): Pm2Target[] {\n if (!Array.isArray(list)) throw new Error(\"PM2 jlist did not return a process array.\");\n if (id !== undefined && (!/^\\d+$/.test(id) || !Number.isSafeInteger(Number(id)))) {\n throw new Error(\"--id must be a numeric PM2 id.\");\n }\n const name = pm2AppName(input);\n const targets: Pm2Target[] = [];\n for (const item of list) {\n const process = record(item);\n if (!process) throw new Error(\"Invalid PM2 process entry.\");\n if (id !== undefined ? process.pm_id !== Number(id) : process.name !== name) continue;\n const env = record(process.pm2_env);\n if (\n process.name !== name ||\n !env ||\n env.pm_cwd !== path.resolve(input.root) ||\n env.pm_exec_path !== path.resolve(input.root, input.entryPath)\n ) {\n throw new Error(\n `Refusing to reload PM2 id ${String(process.pm_id)}: name, working directory or bundle does not belong to this app.`,\n );\n }\n if (\n typeof process.pm_id !== \"number\" ||\n !Number.isSafeInteger(process.pm_id) ||\n process.pm_id < 0\n ) {\n throw new Error(\"Invalid PM2 id in process list.\");\n }\n if (env.exec_mode !== \"fork_mode\" && env.exec_mode !== \"cluster_mode\") {\n throw new Error(\"Unrecognized PM2 execution mode.\");\n }\n const cluster = env.exec_mode === \"cluster_mode\";\n if (cluster) {\n assertClusterSockets(input.socket);\n if (env.wait_ready !== true || env.status !== \"online\") {\n throw new Error(\n \"Rolling reload requires an online cluster worker with wait_ready: true. Check its PM2 configuration and status.\",\n );\n }\n }\n targets.push({ id: process.pm_id, cluster });\n }\n if (targets.length === 0)\n throw new Error(\n `No PM2 process belonging to ${name} was found. Start its ecosystem file first.`,\n );\n if (new Set(targets.map((target) => target.id)).size !== targets.length) {\n throw new Error(\"Duplicate PM2 ids in process list.\");\n }\n return targets;\n}\n"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { command } from "../../commands/cli-command.mjs";
2
+ import { pm2CommandAction } from "./pm2/pm2-command.action.mjs";
3
+
4
+ //#region ../../packages/core/src/cli/commands/pm2.command.ts
5
+ const pm2Command = command({
6
+ name: "pm2 <action>",
7
+ description: "Optional PM2 tooling: init an ecosystem file or reload only this app",
8
+ preload: {
9
+ warlockConfig: true,
10
+ config: [
11
+ "app",
12
+ "http",
13
+ "socket"
14
+ ]
15
+ },
16
+ action: pm2CommandAction,
17
+ options: [
18
+ {
19
+ text: "--project",
20
+ type: "string",
21
+ description: "Project name (default: package.json name)"
22
+ },
23
+ {
24
+ text: "--app",
25
+ type: "string",
26
+ description: "App name (default: app.appName or app)"
27
+ },
28
+ {
29
+ text: "--cluster",
30
+ type: "boolean",
31
+ description: "init: use cluster mode (default: fork)"
32
+ },
33
+ {
34
+ text: "--instances",
35
+ type: "string",
36
+ description: "init with --cluster: worker count or max (default: 2)"
37
+ },
38
+ {
39
+ text: "--listen-timeout",
40
+ type: "string",
41
+ description: "init: readiness timeout in ms (default: 30000)"
42
+ },
43
+ {
44
+ text: "--id",
45
+ type: "string",
46
+ description: "reload: one PM2 id, verified to belong to this app"
47
+ }
48
+ ]
49
+ });
50
+
51
+ //#endregion
52
+ export { pm2Command };
53
+ //# sourceMappingURL=pm2.command.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pm2.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/pm2.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { pm2CommandAction } from \"./pm2/pm2-command.action\";\n\nexport const pm2Command = command({\n name: \"pm2 <action>\",\n description: \"Optional PM2 tooling: init an ecosystem file or reload only this app\",\n // Read config without running bootstrap, prestart or any connector.\n preload: { warlockConfig: true, config: [\"app\", \"http\", \"socket\"] },\n action: pm2CommandAction,\n options: [\n { text: \"--project\", type: \"string\", description: \"Project name (default: package.json name)\" },\n { text: \"--app\", type: \"string\", description: \"App name (default: app.appName or app)\" },\n { text: \"--cluster\", type: \"boolean\", description: \"init: use cluster mode (default: fork)\" },\n {\n text: \"--instances\",\n type: \"string\",\n description: \"init with --cluster: worker count or max (default: 2)\",\n },\n {\n text: \"--listen-timeout\",\n type: \"string\",\n description: \"init: readiness timeout in ms (default: 30000)\",\n },\n {\n text: \"--id\",\n type: \"string\",\n description: \"reload: one PM2 id, verified to belong to this app\",\n },\n ],\n});\n"],"mappings":";;;;AAGA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,aAAa;CAEb,SAAS;EAAE,eAAe;EAAM,QAAQ;GAAC;GAAO;GAAQ;EAAQ;CAAE;CAClE,QAAQ;CACR,SAAS;EACP;GAAE,MAAM;GAAa,MAAM;GAAU,aAAa;EAA4C;EAC9F;GAAE,MAAM;GAAS,MAAM;GAAU,aAAa;EAAyC;EACvF;GAAE,MAAM;GAAa,MAAM;GAAW,aAAa;EAAyC;EAC5F;GACE,MAAM;GACN,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,MAAM;GACN,aAAa;EACf;CACF;AACF,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { aiCommand } from "./commands/ai.command.mjs";
1
2
  import { addCommand } from "./commands/add.command.mjs";
2
3
  import { agentsMdCommand } from "./commands/agents-md.command.mjs";
3
4
  import { boardCommand } from "./commands/board.command.mjs";
@@ -5,6 +6,7 @@ import { buildCommand } from "./commands/build.command.mjs";
5
6
  import { checkCommand } from "./commands/check.command.mjs";
6
7
  import { createDatabaseCommand } from "./commands/create-database.command.mjs";
7
8
  import { devServerCommand } from "./commands/dev-server.command.mjs";
9
+ import { e2eCommand } from "./commands/e2e.command.mjs";
8
10
  import { doctorCommand } from "./commands/doctor.command.mjs";
9
11
  import { dropTablesCommand } from "./commands/drop-tables.command.mjs";
10
12
  import { explainCommand } from "./commands/explain.command.mjs";
@@ -15,6 +17,7 @@ import { mcpCommand } from "./commands/mcp.command.mjs";
15
17
  import { migrateCommand } from "./commands/migrate.command.mjs";
16
18
  import { openApiGeneratorCommand } from "./commands/openapi-generator.command.mjs";
17
19
  import { postmanGeneratorCommand } from "./commands/postman-generator.command.mjs";
20
+ import { pm2Command } from "./commands/pm2.command.mjs";
18
21
  import { routesCommand, routesDiffCommand } from "./commands/routes.command.mjs";
19
22
  import { seedCommand } from "./commands/seed.command.mjs";
20
23
  import { startProductionCommand } from "./commands/start-production.command.mjs";
@@ -33,6 +36,8 @@ const frameworkCommands = [
33
36
  openApiGeneratorCommand,
34
37
  postmanGeneratorCommand,
35
38
  testCommand,
39
+ e2eCommand,
40
+ aiCommand,
36
41
  doctorCommand,
37
42
  routesCommand,
38
43
  routesDiffCommand,
@@ -47,6 +52,7 @@ const frameworkCommands = [
47
52
  boardCommand,
48
53
  buildCommand,
49
54
  startProductionCommand,
55
+ pm2Command,
50
56
  migrateCommand,
51
57
  seedCommand,
52
58
  createDatabaseCommand,
@@ -1 +1 @@
1
- {"version":3,"file":"framework-cli-commands.mjs","names":[],"sources":["../../../../../../../../packages/core/src/cli/framework-cli-commands.ts"],"sourcesContent":["import { addCommand } from \"./commands/add.command\";\nimport { agentsMdCommand } from \"./commands/agents-md.command\";\nimport { boardCommand } from \"./commands/board.command\";\nimport { buildCommand } from \"./commands/build.command\";\nimport { checkCommand } from \"./commands/check.command\";\nimport { createDatabaseCommand } from \"./commands/create-database.command\";\nimport { devServerCommand } from \"./commands/dev-server.command\";\nimport { doctorCommand } from \"./commands/doctor.command\";\nimport { dropTablesCommand } from \"./commands/drop-tables.command\";\nimport { explainCommand } from \"./commands/explain.command\";\nimport {\n generateCommand,\n generateControllerCommand,\n generateEndpointCommand,\n generateMigrationCommand,\n generateModelCommand,\n generateModuleCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n makeCommand,\n} from \"./commands/generate/generate.command\";\nimport { inspectCommand } from \"./commands/inspect.command\";\nimport { issueCommand } from \"./commands/issue.command\";\nimport { mcpCommand } from \"./commands/mcp.command\";\nimport { migrateCommand } from \"./commands/migrate.command\";\nimport { openApiGeneratorCommand } from \"./commands/openapi-generator.command\";\nimport { postmanGeneratorCommand } from \"./commands/postman-generator.command\";\nimport { routesCommand, routesDiffCommand } from \"./commands/routes.command\";\nimport { seedCommand } from \"./commands/seed.command\";\nimport { startProductionCommand } from \"./commands/start-production.command\";\nimport { traceCommand } from \"./commands/trace.command\";\nimport { storagePutCommand } from \"./commands/storage-put.command\";\nimport { typecheckCommand } from \"./commands/typecheck.command\";\nimport { testCommand } from \"./commands/test.command\";\nimport { typingsGeneratorCommand } from \"./commands/typings-generator.command\";\nimport { updateCommand } from \"./commands/update.command\";\nimport { upgradeCommand } from \"./commands/upgrade.command\";\n\nexport const frameworkCommands = [\n // development commands\n devServerCommand,\n typingsGeneratorCommand,\n openApiGeneratorCommand,\n postmanGeneratorCommand,\n\n // testing commands\n testCommand,\n\n // diagnostics commands\n doctorCommand,\n routesCommand,\n routesDiffCommand,\n inspectCommand,\n explainCommand,\n issueCommand,\n checkCommand,\n typecheckCommand,\n\n // agent tooling commands\n mcpCommand,\n traceCommand,\n agentsMdCommand,\n boardCommand,\n\n // production commands\n buildCommand,\n startProductionCommand,\n\n // database commands\n migrateCommand,\n seedCommand,\n createDatabaseCommand,\n dropTablesCommand,\n\n // generation/installation commands\n addCommand,\n updateCommand,\n upgradeCommand,\n\n // scaffolding commands\n generateCommand,\n makeCommand,\n generateEndpointCommand,\n generateModuleCommand,\n generateControllerCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n generateModelCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateMigrationCommand,\n\n // storage commands\n storagePutCommand,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAa,oBAAoB;CAE/B;CACA;CACA;CACA;CAGA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CAGA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;AACF"}
1
+ {"version":3,"file":"framework-cli-commands.mjs","names":[],"sources":["../../../../../../../../packages/core/src/cli/framework-cli-commands.ts"],"sourcesContent":["import { aiCommand } from \"./commands/ai.command\";\nimport { addCommand } from \"./commands/add.command\";\nimport { agentsMdCommand } from \"./commands/agents-md.command\";\nimport { boardCommand } from \"./commands/board.command\";\nimport { buildCommand } from \"./commands/build.command\";\nimport { checkCommand } from \"./commands/check.command\";\nimport { createDatabaseCommand } from \"./commands/create-database.command\";\nimport { devServerCommand } from \"./commands/dev-server.command\";\nimport { e2eCommand } from \"./commands/e2e.command\";\nimport { doctorCommand } from \"./commands/doctor.command\";\nimport { dropTablesCommand } from \"./commands/drop-tables.command\";\nimport { explainCommand } from \"./commands/explain.command\";\nimport {\n generateCommand,\n generateControllerCommand,\n generateEndpointCommand,\n generateMigrationCommand,\n generateModelCommand,\n generateModuleCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n makeCommand,\n} from \"./commands/generate/generate.command\";\nimport { inspectCommand } from \"./commands/inspect.command\";\nimport { issueCommand } from \"./commands/issue.command\";\nimport { mcpCommand } from \"./commands/mcp.command\";\nimport { migrateCommand } from \"./commands/migrate.command\";\nimport { openApiGeneratorCommand } from \"./commands/openapi-generator.command\";\nimport { postmanGeneratorCommand } from \"./commands/postman-generator.command\";\nimport { pm2Command } from \"./commands/pm2.command\";\nimport { routesCommand, routesDiffCommand } from \"./commands/routes.command\";\nimport { seedCommand } from \"./commands/seed.command\";\nimport { startProductionCommand } from \"./commands/start-production.command\";\nimport { traceCommand } from \"./commands/trace.command\";\nimport { storagePutCommand } from \"./commands/storage-put.command\";\nimport { typecheckCommand } from \"./commands/typecheck.command\";\nimport { testCommand } from \"./commands/test.command\";\nimport { typingsGeneratorCommand } from \"./commands/typings-generator.command\";\nimport { updateCommand } from \"./commands/update.command\";\nimport { upgradeCommand } from \"./commands/upgrade.command\";\n\nexport const frameworkCommands = [\n // development commands\n devServerCommand,\n typingsGeneratorCommand,\n openApiGeneratorCommand,\n postmanGeneratorCommand,\n\n // testing commands\n testCommand,\n e2eCommand,\n\n // diagnostics commands\n aiCommand,\n doctorCommand,\n routesCommand,\n routesDiffCommand,\n inspectCommand,\n explainCommand,\n issueCommand,\n checkCommand,\n typecheckCommand,\n\n // agent tooling commands\n mcpCommand,\n traceCommand,\n agentsMdCommand,\n boardCommand,\n\n // production commands\n buildCommand,\n startProductionCommand,\n pm2Command,\n\n // database commands\n migrateCommand,\n seedCommand,\n createDatabaseCommand,\n dropTablesCommand,\n\n // generation/installation commands\n addCommand,\n updateCommand,\n upgradeCommand,\n\n // scaffolding commands\n generateCommand,\n makeCommand,\n generateEndpointCommand,\n generateModuleCommand,\n generateControllerCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n generateModelCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateMigrationCommand,\n\n // storage commands\n storagePutCommand,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,oBAAoB;CAE/B;CACA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CAGA;CACA;CACA;CAGA;CACA;CACA;CACA;CAGA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"connectors-manager.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/connectors/connectors-manager.ts"],"mappings":";;qBAsBa;;;;mBAIM;;;;;;;;;;;;;mBAcA;;;;EAKjB;;;;;;;;UAqBQ;;;;;;;;;EAgBD,cAAc;;;;EAOd,YAAY,YAAY;;;;EASxB,QAAQ;;;;;;;;EAWR,IAAI,MAAM;;;;EAOJ,MAAM,kBAAkB,kBAAkB;;;;;;;;;;EAuB1C,WAAW,OAAO,0BAA0B;;;;EAiB5C,aAAa,oBAAoB,kBAAkB;;;;EAWnD,YAAY;;;;UA8CX;;;;;;;;;;;;;EAgCP;;qBAyBI,mBAAiB"}
1
+ {"version":3,"file":"connectors-manager.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/connectors/connectors-manager.ts"],"mappings":";;qBAkBa;;;;mBAIM;;;;;;;;;;;;;mBAcA;;;;EAKjB;;;;;;;;UAqBQ;;;;;;;;;EAgBD,cAAc;;;;EAOd,YAAY,YAAY;;;;EASxB,QAAQ;;;;;;;;EAWR,IAAI,MAAM;;;;EAOJ,MAAM,kBAAkB,kBAAkB;;;;;;;;;;EAuB1C,WAAW,OAAO,0BAA0B;;;;EAiB5C,aAAa,oBAAoB,kBAAkB;;;;EAWnD,YAAY;;;;UA8CX;;;;;;;;;;;;;EAgCP;;qBAyBI,mBAAiB"}
@@ -11,14 +11,11 @@ import { MailerConnector } from "./mail-connector.mjs";
11
11
  import { NotificationsConnector } from "./notifications-connector.mjs";
12
12
  import { SocketConnector } from "./socket-connector.mjs";
13
13
  import { StorageConnector } from "./storage.connector.mjs";
14
+ import { DEFAULT_SHUTDOWN_TIMEOUT } from "./shutdown-timeout.mjs";
14
15
  import baseConfig from "@mongez/config";
15
16
  import { log } from "@warlock.js/logger";
16
17
 
17
18
  //#region ../../packages/core/src/connectors/connectors-manager.ts
18
- /**
19
- * Overall shutdown budget in ms. Override via `app.shutdownTimeout`.
20
- */
21
- const DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
22
19
  var ConnectorsManager = class {
23
20
  /**
24
21
  * Constructor
@@ -1 +1 @@
1
- {"version":3,"file":"connectors-manager.mjs","names":["config"],"sources":["../../../../../../../../packages/core/src/connectors/connectors-manager.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { log } from \"@warlock.js/logger\";\nimport { AccessConnector } from \"./access-connector\";\nimport { AiConnector } from \"./ai-connector\";\nimport { CacheConnector } from \"./cache-connector\";\nimport { DatabaseConnector } from \"./database-connector\";\nimport { HeraldConnector } from \"./herald-connector\";\nimport { HttpConnector } from \"./http-connector\";\nimport { LoggerConnector } from \"./logger-connector\";\nimport { MailerConnector } from \"./mail-connector\";\nimport { NotificationsConnector } from \"./notifications-connector\";\nimport { SocketConnector } from \"./socket-connector\";\nimport { StorageConnector } from \"./storage.connector\";\nimport { ConnectorLifecyclePhase } from \"./types\";\nimport type { Connector, ConnectorName } from \"./types\";\n\n/**\n * Overall shutdown budget in ms. Override via `app.shutdownTimeout`.\n */\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\n\nexport class ConnectorsManager {\n /**\n * Connectors list\n */\n private readonly connectors: Connector[] = [];\n\n /**\n * The names the framework's own connectors hold.\n *\n * Filled by the constructor's own registration calls rather than declared\n * beside them, so there is exactly ONE place a built-in name exists: the\n * connector that owns it. A second list — in the builder, in a config\n * validator, in a test double — is a list that drifts the moment a built-in\n * is added or renamed, and it drifts silently, because nothing compares the\n * two. Both halves of a connector's life read this one through\n * {@link isBuiltInName}: the build refuses a `connectors` array that claims\n * a built-in name, and so does registration.\n */\n private readonly builtInNames = new Set<string>();\n\n /**\n * Constructor\n */\n public constructor() {\n this.registerBuiltIn(new LoggerConnector());\n this.registerBuiltIn(new MailerConnector());\n this.registerBuiltIn(new HttpConnector());\n this.registerBuiltIn(new DatabaseConnector());\n this.registerBuiltIn(new HeraldConnector());\n this.registerBuiltIn(new CacheConnector());\n this.registerBuiltIn(new StorageConnector());\n this.registerBuiltIn(new SocketConnector());\n this.registerBuiltIn(new NotificationsConnector());\n this.registerBuiltIn(new AccessConnector());\n this.registerBuiltIn(new AiConnector());\n }\n\n /**\n * Register a framework-owned connector and reserve its name.\n *\n * Private on purpose: an app connector must not be able to reserve a name,\n * and the set is only correct if every entry in it came from a connector\n * this class constructed itself.\n */\n private registerBuiltIn(...connectors: Connector[]): void {\n for (const connector of connectors) {\n this.builtInNames.add(connector.name);\n }\n\n this.register(...connectors);\n }\n\n /**\n * Whether the given name belongs to a framework built-in connector.\n *\n * Reserved, not merely taken: a configured connector carrying one of these\n * names would be skipped by {@link has} at boot while the build had already\n * drained its contribution, so the artifact would carry a connector the\n * server never runs. Callers refuse the name instead of skipping it.\n */\n public isBuiltInName(name: string): boolean {\n return this.builtInNames.has(name);\n }\n\n /**\n * Register a connector\n */\n public register(...connectors: Connector[]): void {\n this.connectors.push(...connectors);\n // sort connectors by priority\n this.connectors.sort((a, b) => a.priority - b.priority);\n }\n\n /**\n * Get all connectors\n */\n public list(): Connector[] {\n return this.connectors;\n }\n\n /**\n * Whether a connector with the given name is already registered.\n *\n * Names are the identity of a connector here, which is what lets a caller\n * that may run more than once per process register idempotently instead of\n * stacking duplicates that would each boot and shut down.\n */\n public has(name: ConnectorName): boolean {\n return this.connectors.some((connector) => connector.name === name);\n }\n\n /**\n * start all connectors\n */\n public async start(connectorsNames?: ConnectorName[]): Promise<void> {\n const connectorsList = connectorsNames\n ? this.connectors.filter((connector) => connectorsNames.includes(connector.name))\n : this.connectors;\n\n for (const connector of connectorsList) {\n await connector.boot();\n }\n\n for (const connector of connectorsList) {\n await connector.start();\n }\n }\n\n /**\n * Start all connectors in the given lifecycle phase.\n *\n * The production builder and dev preload split startup around app\n * code: early phase before app imports, late phase after. Within a\n * phase, all connectors `boot()` first, then all `start()`, so\n * cross-connector wiring inside the phase still works (e.g. socket\n * reads http's instance during its own boot).\n */\n public async startPhase(phase: ConnectorLifecyclePhase): Promise<void> {\n const phaseConnectors = this.connectors.filter(\n (connector) => connector.lifecyclePhase === phase,\n );\n\n for (const connector of phaseConnectors) {\n await connector.boot();\n }\n\n for (const connector of phaseConnectors) {\n await connector.start();\n }\n }\n\n /**\n * Start all connectors except the given ones\n */\n public async startWithout(excludedConnectors: ConnectorName[]): Promise<void> {\n await this.start(\n this.connectors\n .filter((connector) => !excludedConnectors.includes(connector.name))\n .map((connector) => connector.name),\n );\n }\n\n /**\n * Shutdown all connectors\n */\n public async shutdown(): Promise<void> {\n // App-owned teardown runs first, while every connector (db, cache, http) is\n // still up, so cleanup hooks can use them. Idempotent + error-isolated.\n await Application.runShutdownHooks();\n\n // HTTP stops accepting and drains in-flight requests FIRST, while storage,\n // socket, etc. are still up; then the rest go in reverse priority. Copy the\n // list first — `reverse()` mutates in place, so reversing the live array\n // would corrupt the order on a second shutdown pass.\n const reversed = [...this.connectors].reverse();\n const ordered = [\n ...reversed.filter((connector) => connector.name === \"http\"),\n ...reversed.filter((connector) => connector.name !== \"http\"),\n ];\n\n const timeout = config.get(\"app.shutdownTimeout\", DEFAULT_SHUTDOWN_TIMEOUT);\n let current: string | undefined;\n\n const timer = setTimeout(() => {\n // Sync write: the process exits right after, so an async log would be lost.\n console.error(\n `Shutdown timed out after ${timeout}ms; connector still shutting down: ${current ?? \"none\"}`,\n );\n process.exit(1);\n }, timeout);\n timer.unref?.();\n\n try {\n for (const connector of ordered) {\n current = connector.name;\n await this.shutdownOne(connector);\n }\n current = undefined;\n } finally {\n clearTimeout(timer);\n }\n\n // Log calls return before an async channel (file, Sentry) has written the\n // entry, so a channel would lose it when the process exits; `flush()` waits\n // for those deliveries.\n await log.flush();\n }\n\n /**\n * Shut one connector down, isolating and reporting its failure.\n */\n private async shutdownOne(connector: Connector): Promise<void> {\n try {\n await connector.shutdown();\n } catch (error) {\n try {\n // Synchronous: the entry is handed to every channel before this\n // returns. `shutdown()` then flushes async channels before resolving,\n // because the caller (`shutdownOnProcessKill`) calls `process.exit(0)`\n // as soon as it does.\n log.error(\"connectors\", \"shutdown\", error as Error, { connector: connector.name });\n } catch {\n // Reporting a failure must never become a worse failure than the one\n // being reported: anything thrown here (a redact rule, say) would\n // otherwise abort the remaining teardown. Swallowed\n // rather than re-reported: the only channel we could report it through\n // is the one that just threw.\n }\n }\n }\n\n /**\n * Shutdown connectors on process kill\n *\n * Registers signal handlers that run an idempotent graceful shutdown:\n * - SIGINT: Ctrl+C on Unix, also caught on Windows but unreliable in child processes\n * - SIGTERM: Termination signal (Unix primarily)\n * - SIGHUP: registered on Windows (`win32`) only, to catch Ctrl+C / console close\n *\n * Note: no `beforeExit` handler is registered — `beforeExit` cannot reliably\n * await async teardown (the process exits once the callback returns), so\n * shutdown is driven entirely by the signal handlers above.\n */\n public shutdownOnProcessKill(): void {\n let isShuttingDown = false;\n\n const gracefulShutdown = async (signal: string) => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n\n console.log(`\\nExiting...`);\n await this.shutdown();\n process.exit(0);\n };\n\n // Unix signals\n process.on(\"SIGINT\", () => gracefulShutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => gracefulShutdown(\"SIGTERM\"));\n\n // Windows-specific: handle when process is about to exit\n // This is more reliable on Windows for spawned child processes\n if (process.platform === \"win32\") {\n // Handle Ctrl+C on Windows specifically\n process.on(\"SIGHUP\", () => gracefulShutdown(\"SIGHUP\"));\n }\n }\n}\n\nexport const connectorsManager = new ConnectorsManager();\n"],"mappings":"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"}
1
+ {"version":3,"file":"connectors-manager.mjs","names":["config"],"sources":["../../../../../../../../packages/core/src/connectors/connectors-manager.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { log } from \"@warlock.js/logger\";\nimport { AccessConnector } from \"./access-connector\";\nimport { AiConnector } from \"./ai-connector\";\nimport { CacheConnector } from \"./cache-connector\";\nimport { DatabaseConnector } from \"./database-connector\";\nimport { HeraldConnector } from \"./herald-connector\";\nimport { HttpConnector } from \"./http-connector\";\nimport { LoggerConnector } from \"./logger-connector\";\nimport { MailerConnector } from \"./mail-connector\";\nimport { NotificationsConnector } from \"./notifications-connector\";\nimport { SocketConnector } from \"./socket-connector\";\nimport { StorageConnector } from \"./storage.connector\";\nimport { DEFAULT_SHUTDOWN_TIMEOUT } from \"./shutdown-timeout\";\nimport { ConnectorLifecyclePhase } from \"./types\";\nimport type { Connector, ConnectorName } from \"./types\";\n\nexport class ConnectorsManager {\n /**\n * Connectors list\n */\n private readonly connectors: Connector[] = [];\n\n /**\n * The names the framework's own connectors hold.\n *\n * Filled by the constructor's own registration calls rather than declared\n * beside them, so there is exactly ONE place a built-in name exists: the\n * connector that owns it. A second list — in the builder, in a config\n * validator, in a test double — is a list that drifts the moment a built-in\n * is added or renamed, and it drifts silently, because nothing compares the\n * two. Both halves of a connector's life read this one through\n * {@link isBuiltInName}: the build refuses a `connectors` array that claims\n * a built-in name, and so does registration.\n */\n private readonly builtInNames = new Set<string>();\n\n /**\n * Constructor\n */\n public constructor() {\n this.registerBuiltIn(new LoggerConnector());\n this.registerBuiltIn(new MailerConnector());\n this.registerBuiltIn(new HttpConnector());\n this.registerBuiltIn(new DatabaseConnector());\n this.registerBuiltIn(new HeraldConnector());\n this.registerBuiltIn(new CacheConnector());\n this.registerBuiltIn(new StorageConnector());\n this.registerBuiltIn(new SocketConnector());\n this.registerBuiltIn(new NotificationsConnector());\n this.registerBuiltIn(new AccessConnector());\n this.registerBuiltIn(new AiConnector());\n }\n\n /**\n * Register a framework-owned connector and reserve its name.\n *\n * Private on purpose: an app connector must not be able to reserve a name,\n * and the set is only correct if every entry in it came from a connector\n * this class constructed itself.\n */\n private registerBuiltIn(...connectors: Connector[]): void {\n for (const connector of connectors) {\n this.builtInNames.add(connector.name);\n }\n\n this.register(...connectors);\n }\n\n /**\n * Whether the given name belongs to a framework built-in connector.\n *\n * Reserved, not merely taken: a configured connector carrying one of these\n * names would be skipped by {@link has} at boot while the build had already\n * drained its contribution, so the artifact would carry a connector the\n * server never runs. Callers refuse the name instead of skipping it.\n */\n public isBuiltInName(name: string): boolean {\n return this.builtInNames.has(name);\n }\n\n /**\n * Register a connector\n */\n public register(...connectors: Connector[]): void {\n this.connectors.push(...connectors);\n // sort connectors by priority\n this.connectors.sort((a, b) => a.priority - b.priority);\n }\n\n /**\n * Get all connectors\n */\n public list(): Connector[] {\n return this.connectors;\n }\n\n /**\n * Whether a connector with the given name is already registered.\n *\n * Names are the identity of a connector here, which is what lets a caller\n * that may run more than once per process register idempotently instead of\n * stacking duplicates that would each boot and shut down.\n */\n public has(name: ConnectorName): boolean {\n return this.connectors.some((connector) => connector.name === name);\n }\n\n /**\n * start all connectors\n */\n public async start(connectorsNames?: ConnectorName[]): Promise<void> {\n const connectorsList = connectorsNames\n ? this.connectors.filter((connector) => connectorsNames.includes(connector.name))\n : this.connectors;\n\n for (const connector of connectorsList) {\n await connector.boot();\n }\n\n for (const connector of connectorsList) {\n await connector.start();\n }\n }\n\n /**\n * Start all connectors in the given lifecycle phase.\n *\n * The production builder and dev preload split startup around app\n * code: early phase before app imports, late phase after. Within a\n * phase, all connectors `boot()` first, then all `start()`, so\n * cross-connector wiring inside the phase still works (e.g. socket\n * reads http's instance during its own boot).\n */\n public async startPhase(phase: ConnectorLifecyclePhase): Promise<void> {\n const phaseConnectors = this.connectors.filter(\n (connector) => connector.lifecyclePhase === phase,\n );\n\n for (const connector of phaseConnectors) {\n await connector.boot();\n }\n\n for (const connector of phaseConnectors) {\n await connector.start();\n }\n }\n\n /**\n * Start all connectors except the given ones\n */\n public async startWithout(excludedConnectors: ConnectorName[]): Promise<void> {\n await this.start(\n this.connectors\n .filter((connector) => !excludedConnectors.includes(connector.name))\n .map((connector) => connector.name),\n );\n }\n\n /**\n * Shutdown all connectors\n */\n public async shutdown(): Promise<void> {\n // App-owned teardown runs first, while every connector (db, cache, http) is\n // still up, so cleanup hooks can use them. Idempotent + error-isolated.\n await Application.runShutdownHooks();\n\n // HTTP stops accepting and drains in-flight requests FIRST, while storage,\n // socket, etc. are still up; then the rest go in reverse priority. Copy the\n // list first — `reverse()` mutates in place, so reversing the live array\n // would corrupt the order on a second shutdown pass.\n const reversed = [...this.connectors].reverse();\n const ordered = [\n ...reversed.filter((connector) => connector.name === \"http\"),\n ...reversed.filter((connector) => connector.name !== \"http\"),\n ];\n\n const timeout = config.get(\"app.shutdownTimeout\", DEFAULT_SHUTDOWN_TIMEOUT);\n let current: string | undefined;\n\n const timer = setTimeout(() => {\n // Sync write: the process exits right after, so an async log would be lost.\n console.error(\n `Shutdown timed out after ${timeout}ms; connector still shutting down: ${current ?? \"none\"}`,\n );\n process.exit(1);\n }, timeout);\n timer.unref?.();\n\n try {\n for (const connector of ordered) {\n current = connector.name;\n await this.shutdownOne(connector);\n }\n current = undefined;\n } finally {\n clearTimeout(timer);\n }\n\n // Log calls return before an async channel (file, Sentry) has written the\n // entry, so a channel would lose it when the process exits; `flush()` waits\n // for those deliveries.\n await log.flush();\n }\n\n /**\n * Shut one connector down, isolating and reporting its failure.\n */\n private async shutdownOne(connector: Connector): Promise<void> {\n try {\n await connector.shutdown();\n } catch (error) {\n try {\n // Synchronous: the entry is handed to every channel before this\n // returns. `shutdown()` then flushes async channels before resolving,\n // because the caller (`shutdownOnProcessKill`) calls `process.exit(0)`\n // as soon as it does.\n log.error(\"connectors\", \"shutdown\", error as Error, { connector: connector.name });\n } catch {\n // Reporting a failure must never become a worse failure than the one\n // being reported: anything thrown here (a redact rule, say) would\n // otherwise abort the remaining teardown. Swallowed\n // rather than re-reported: the only channel we could report it through\n // is the one that just threw.\n }\n }\n }\n\n /**\n * Shutdown connectors on process kill\n *\n * Registers signal handlers that run an idempotent graceful shutdown:\n * - SIGINT: Ctrl+C on Unix, also caught on Windows but unreliable in child processes\n * - SIGTERM: Termination signal (Unix primarily)\n * - SIGHUP: registered on Windows (`win32`) only, to catch Ctrl+C / console close\n *\n * Note: no `beforeExit` handler is registered — `beforeExit` cannot reliably\n * await async teardown (the process exits once the callback returns), so\n * shutdown is driven entirely by the signal handlers above.\n */\n public shutdownOnProcessKill(): void {\n let isShuttingDown = false;\n\n const gracefulShutdown = async (signal: string) => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n\n console.log(`\\nExiting...`);\n await this.shutdown();\n process.exit(0);\n };\n\n // Unix signals\n process.on(\"SIGINT\", () => gracefulShutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => gracefulShutdown(\"SIGTERM\"));\n\n // Windows-specific: handle when process is about to exit\n // This is more reliable on Windows for spawned child processes\n if (process.platform === \"win32\") {\n // Handle Ctrl+C on Windows specifically\n process.on(\"SIGHUP\", () => gracefulShutdown(\"SIGHUP\"));\n }\n }\n}\n\nexport const connectorsManager = new ConnectorsManager();\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ //#region ../../packages/core/src/connectors/shutdown-timeout.ts
2
+ /** Overall connector shutdown budget; overridden by `app.shutdownTimeout`. */
3
+ const DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
4
+
5
+ //#endregion
6
+ export { DEFAULT_SHUTDOWN_TIMEOUT };
7
+ //# sourceMappingURL=shutdown-timeout.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shutdown-timeout.mjs","names":[],"sources":["../../../../../../../../packages/core/src/connectors/shutdown-timeout.ts"],"sourcesContent":["/** Overall connector shutdown budget; overridden by `app.shutdownTimeout`. */\nexport const DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\n"],"mappings":";;AACA,MAAa,2BAA2B"}
@@ -1 +1 @@
1
- {"version":3,"file":"socket-connector.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/connectors/socket-connector.ts"],"mappings":";;;;;;;;qBAkCa,wBAAwB;WACnB,MAAM;WACN,WAAQ,kBAAA;WACR,iBAAc,wBAAA;;;;;;qBAOX;YAET,SAAS;;;;;;;;;YAUT;;;;YAKA;;;;;;;YAQA;IACR,OAAO,mBAAmB;IAC1B,SAAS,mBAAmB;;;;;EAMjB,QAAI;;;;EA2EJ,SAAS;;;;;EAgBT,WAAW;;;;;;;;;;EAeX,YAAY;;;;UA8BjB;UAUA;;;;;UAsBA"}
1
+ {"version":3,"file":"socket-connector.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/connectors/socket-connector.ts"],"mappings":";;;;;;;;qBAkCa,wBAAwB;WACnB,MAAM;WACN,WAAQ,kBAAA;WACR,iBAAc,wBAAA;;;;;;qBAOX;YAET,SAAS;;;;;;;;;YAUT;;;;YAKA;;;;;;;YAQA;IACR,OAAO,mBAAmB;IAC1B,SAAS,mBAAmB;;;;;EAMjB,QAAI;;;;EAqFJ,SAAS;;;;;EAgBT,WAAW;;;;;;;;;;EAeX,YAAY;;;;UA8BjB;UAUA;;;;;UAsBA"}