@hydranium/cli 1.0.0-next.10

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 (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/heap-analysis/analyze-heap.mjs +798 -0
  4. package/heap-analysis/heap/core/aggregate.mjs +152 -0
  5. package/heap-analysis/heap/core/bucket-label.mjs +23 -0
  6. package/heap-analysis/heap/core/cutpoints.mjs +51 -0
  7. package/heap-analysis/heap/core/diff.mjs +56 -0
  8. package/heap-analysis/heap/core/format.mjs +47 -0
  9. package/heap-analysis/heap/core/holders.mjs +80 -0
  10. package/heap-analysis/heap/core/load.mjs +46 -0
  11. package/heap-analysis/heap/core/report.mjs +68 -0
  12. package/heap-analysis/heap/core/strings.mjs +80 -0
  13. package/heap-analysis/heap/langium/concepts.mjs +230 -0
  14. package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
  15. package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
  16. package/heap-analysis/heap/renderer/renderer.mjs +122 -0
  17. package/lib/cli-version.d.ts +21 -0
  18. package/lib/cli-version.d.ts.map +1 -0
  19. package/lib/cli-version.js +30 -0
  20. package/lib/cli-version.js.map +1 -0
  21. package/lib/cli.d.ts +11 -0
  22. package/lib/cli.d.ts.map +1 -0
  23. package/lib/cli.js +135 -0
  24. package/lib/cli.js.map +1 -0
  25. package/lib/commands/analyze-heap-args.d.ts +46 -0
  26. package/lib/commands/analyze-heap-args.d.ts.map +1 -0
  27. package/lib/commands/analyze-heap-args.js +174 -0
  28. package/lib/commands/analyze-heap-args.js.map +1 -0
  29. package/lib/commands/analyze-heap.d.ts +37 -0
  30. package/lib/commands/analyze-heap.d.ts.map +1 -0
  31. package/lib/commands/analyze-heap.js +63 -0
  32. package/lib/commands/analyze-heap.js.map +1 -0
  33. package/lib/commands/ast-ground-truth-args.d.ts +23 -0
  34. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
  35. package/lib/commands/ast-ground-truth-args.js +47 -0
  36. package/lib/commands/ast-ground-truth-args.js.map +1 -0
  37. package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
  38. package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
  39. package/lib/commands/ast-ground-truth-driver.js +72 -0
  40. package/lib/commands/ast-ground-truth-driver.js.map +1 -0
  41. package/lib/commands/ast-ground-truth.d.ts +31 -0
  42. package/lib/commands/ast-ground-truth.d.ts.map +1 -0
  43. package/lib/commands/ast-ground-truth.js +36 -0
  44. package/lib/commands/ast-ground-truth.js.map +1 -0
  45. package/lib/commands/generate-transfer-model-args.d.ts +38 -0
  46. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
  47. package/lib/commands/generate-transfer-model-args.js +183 -0
  48. package/lib/commands/generate-transfer-model-args.js.map +1 -0
  49. package/lib/commands/generate-transfer-model-config.d.ts +32 -0
  50. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
  51. package/lib/commands/generate-transfer-model-config.js +122 -0
  52. package/lib/commands/generate-transfer-model-config.js.map +1 -0
  53. package/lib/commands/generate-transfer-model.d.ts +110 -0
  54. package/lib/commands/generate-transfer-model.d.ts.map +1 -0
  55. package/lib/commands/generate-transfer-model.js +587 -0
  56. package/lib/commands/generate-transfer-model.js.map +1 -0
  57. package/lib/commands/harness-args.d.ts +198 -0
  58. package/lib/commands/harness-args.d.ts.map +1 -0
  59. package/lib/commands/harness-args.js +339 -0
  60. package/lib/commands/harness-args.js.map +1 -0
  61. package/lib/commands/headless-harness.d.ts +97 -0
  62. package/lib/commands/headless-harness.d.ts.map +1 -0
  63. package/lib/commands/headless-harness.js +121 -0
  64. package/lib/commands/headless-harness.js.map +1 -0
  65. package/lib/commands/init-args.d.ts +81 -0
  66. package/lib/commands/init-args.d.ts.map +1 -0
  67. package/lib/commands/init-args.js +250 -0
  68. package/lib/commands/init-args.js.map +1 -0
  69. package/lib/commands/init-prompt.d.ts +66 -0
  70. package/lib/commands/init-prompt.d.ts.map +1 -0
  71. package/lib/commands/init-prompt.js +71 -0
  72. package/lib/commands/init-prompt.js.map +1 -0
  73. package/lib/commands/init-templates.d.ts +42 -0
  74. package/lib/commands/init-templates.d.ts.map +1 -0
  75. package/lib/commands/init-templates.js +1902 -0
  76. package/lib/commands/init-templates.js.map +1 -0
  77. package/lib/commands/init-wizard.d.ts +65 -0
  78. package/lib/commands/init-wizard.d.ts.map +1 -0
  79. package/lib/commands/init-wizard.js +302 -0
  80. package/lib/commands/init-wizard.js.map +1 -0
  81. package/lib/commands/init-workspace.d.ts +168 -0
  82. package/lib/commands/init-workspace.d.ts.map +1 -0
  83. package/lib/commands/init-workspace.js +372 -0
  84. package/lib/commands/init-workspace.js.map +1 -0
  85. package/lib/commands/init.d.ts +273 -0
  86. package/lib/commands/init.d.ts.map +1 -0
  87. package/lib/commands/init.js +303 -0
  88. package/lib/commands/init.js.map +1 -0
  89. package/lib/commands/lint-grammar-args.d.ts +23 -0
  90. package/lib/commands/lint-grammar-args.d.ts.map +1 -0
  91. package/lib/commands/lint-grammar-args.js +63 -0
  92. package/lib/commands/lint-grammar-args.js.map +1 -0
  93. package/lib/commands/lint-grammar-driver.d.ts +10 -0
  94. package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
  95. package/lib/commands/lint-grammar-driver.js +72 -0
  96. package/lib/commands/lint-grammar-driver.js.map +1 -0
  97. package/lib/commands/lint-grammar-report.d.ts +24 -0
  98. package/lib/commands/lint-grammar-report.d.ts.map +1 -0
  99. package/lib/commands/lint-grammar-report.js +65 -0
  100. package/lib/commands/lint-grammar-report.js.map +1 -0
  101. package/lib/commands/lint-grammar.d.ts +33 -0
  102. package/lib/commands/lint-grammar.d.ts.map +1 -0
  103. package/lib/commands/lint-grammar.js +42 -0
  104. package/lib/commands/lint-grammar.js.map +1 -0
  105. package/lib/commands/measure-memory-args.d.ts +23 -0
  106. package/lib/commands/measure-memory-args.d.ts.map +1 -0
  107. package/lib/commands/measure-memory-args.js +106 -0
  108. package/lib/commands/measure-memory-args.js.map +1 -0
  109. package/lib/commands/measure-memory-driver.d.ts +10 -0
  110. package/lib/commands/measure-memory-driver.d.ts.map +1 -0
  111. package/lib/commands/measure-memory-driver.js +110 -0
  112. package/lib/commands/measure-memory-driver.js.map +1 -0
  113. package/lib/commands/measure-memory.d.ts +68 -0
  114. package/lib/commands/measure-memory.d.ts.map +1 -0
  115. package/lib/commands/measure-memory.js +100 -0
  116. package/lib/commands/measure-memory.js.map +1 -0
  117. package/lib/commands/model-docs-args.d.ts +23 -0
  118. package/lib/commands/model-docs-args.d.ts.map +1 -0
  119. package/lib/commands/model-docs-args.js +53 -0
  120. package/lib/commands/model-docs-args.js.map +1 -0
  121. package/lib/commands/model-docs-driver.d.ts +10 -0
  122. package/lib/commands/model-docs-driver.d.ts.map +1 -0
  123. package/lib/commands/model-docs-driver.js +61 -0
  124. package/lib/commands/model-docs-driver.js.map +1 -0
  125. package/lib/commands/model-docs-report.d.ts +18 -0
  126. package/lib/commands/model-docs-report.d.ts.map +1 -0
  127. package/lib/commands/model-docs-report.js +123 -0
  128. package/lib/commands/model-docs-report.js.map +1 -0
  129. package/lib/commands/model-docs.d.ts +30 -0
  130. package/lib/commands/model-docs.d.ts.map +1 -0
  131. package/lib/commands/model-docs.js +37 -0
  132. package/lib/commands/model-docs.js.map +1 -0
  133. package/lib/commands/projects-args.d.ts +25 -0
  134. package/lib/commands/projects-args.d.ts.map +1 -0
  135. package/lib/commands/projects-args.js +49 -0
  136. package/lib/commands/projects-args.js.map +1 -0
  137. package/lib/commands/projects.d.ts +45 -0
  138. package/lib/commands/projects.d.ts.map +1 -0
  139. package/lib/commands/projects.js +39 -0
  140. package/lib/commands/projects.js.map +1 -0
  141. package/lib/commands/query-args.d.ts +25 -0
  142. package/lib/commands/query-args.d.ts.map +1 -0
  143. package/lib/commands/query-args.js +46 -0
  144. package/lib/commands/query-args.js.map +1 -0
  145. package/lib/commands/query.d.ts +32 -0
  146. package/lib/commands/query.d.ts.map +1 -0
  147. package/lib/commands/query.js +29 -0
  148. package/lib/commands/query.js.map +1 -0
  149. package/lib/commands/reflect-args.d.ts +23 -0
  150. package/lib/commands/reflect-args.d.ts.map +1 -0
  151. package/lib/commands/reflect-args.js +59 -0
  152. package/lib/commands/reflect-args.js.map +1 -0
  153. package/lib/commands/reflect-driver.d.ts +10 -0
  154. package/lib/commands/reflect-driver.d.ts.map +1 -0
  155. package/lib/commands/reflect-driver.js +66 -0
  156. package/lib/commands/reflect-driver.js.map +1 -0
  157. package/lib/commands/reflect-report.d.ts +20 -0
  158. package/lib/commands/reflect-report.d.ts.map +1 -0
  159. package/lib/commands/reflect-report.js +127 -0
  160. package/lib/commands/reflect-report.js.map +1 -0
  161. package/lib/commands/reflect.d.ts +30 -0
  162. package/lib/commands/reflect.d.ts.map +1 -0
  163. package/lib/commands/reflect.js +38 -0
  164. package/lib/commands/reflect.js.map +1 -0
  165. package/lib/commands/save-args.d.ts +26 -0
  166. package/lib/commands/save-args.d.ts.map +1 -0
  167. package/lib/commands/save-args.js +59 -0
  168. package/lib/commands/save-args.js.map +1 -0
  169. package/lib/commands/save.d.ts +38 -0
  170. package/lib/commands/save.d.ts.map +1 -0
  171. package/lib/commands/save.js +48 -0
  172. package/lib/commands/save.js.map +1 -0
  173. package/lib/commands/validate-args.d.ts +23 -0
  174. package/lib/commands/validate-args.d.ts.map +1 -0
  175. package/lib/commands/validate-args.js +60 -0
  176. package/lib/commands/validate-args.js.map +1 -0
  177. package/lib/commands/validate-driver.d.ts +10 -0
  178. package/lib/commands/validate-driver.d.ts.map +1 -0
  179. package/lib/commands/validate-driver.js +72 -0
  180. package/lib/commands/validate-driver.js.map +1 -0
  181. package/lib/commands/validate-report.d.ts +30 -0
  182. package/lib/commands/validate-report.d.ts.map +1 -0
  183. package/lib/commands/validate-report.js +87 -0
  184. package/lib/commands/validate-report.js.map +1 -0
  185. package/lib/commands/validate.d.ts +34 -0
  186. package/lib/commands/validate.d.ts.map +1 -0
  187. package/lib/commands/validate.js +41 -0
  188. package/lib/commands/validate.js.map +1 -0
  189. package/lib/commands/watch-args.d.ts +26 -0
  190. package/lib/commands/watch-args.d.ts.map +1 -0
  191. package/lib/commands/watch-args.js +59 -0
  192. package/lib/commands/watch-args.js.map +1 -0
  193. package/lib/commands/watch.d.ts +75 -0
  194. package/lib/commands/watch.d.ts.map +1 -0
  195. package/lib/commands/watch.js +112 -0
  196. package/lib/commands/watch.js.map +1 -0
  197. package/lib/index.d.ts +16 -0
  198. package/lib/index.d.ts.map +1 -0
  199. package/lib/index.js +16 -0
  200. package/lib/index.js.map +1 -0
  201. package/lib/log-level.d.ts +39 -0
  202. package/lib/log-level.d.ts.map +1 -0
  203. package/lib/log-level.js +47 -0
  204. package/lib/log-level.js.map +1 -0
  205. package/lib/package-name.d.ts +33 -0
  206. package/lib/package-name.d.ts.map +1 -0
  207. package/lib/package-name.js +37 -0
  208. package/lib/package-name.js.map +1 -0
  209. package/lib/spawn-data-server.d.ts +102 -0
  210. package/lib/spawn-data-server.d.ts.map +1 -0
  211. package/lib/spawn-data-server.js +154 -0
  212. package/lib/spawn-data-server.js.map +1 -0
  213. package/lib/testing/echo-server.d.ts +11 -0
  214. package/lib/testing/echo-server.d.ts.map +1 -0
  215. package/lib/testing/echo-server.js +79 -0
  216. package/lib/testing/echo-server.js.map +1 -0
  217. package/package.json +105 -0
  218. package/src/cli-version.ts +32 -0
  219. package/src/cli.ts +139 -0
  220. package/src/commands/analyze-heap-args.ts +184 -0
  221. package/src/commands/analyze-heap.ts +76 -0
  222. package/src/commands/ast-ground-truth-args.ts +63 -0
  223. package/src/commands/ast-ground-truth-driver.ts +81 -0
  224. package/src/commands/ast-ground-truth.ts +49 -0
  225. package/src/commands/generate-transfer-model-args.ts +203 -0
  226. package/src/commands/generate-transfer-model-config.ts +128 -0
  227. package/src/commands/generate-transfer-model.ts +716 -0
  228. package/src/commands/harness-args.ts +429 -0
  229. package/src/commands/headless-harness.ts +165 -0
  230. package/src/commands/init-args.ts +273 -0
  231. package/src/commands/init-prompt.ts +149 -0
  232. package/src/commands/init-templates.ts +2037 -0
  233. package/src/commands/init-wizard.ts +378 -0
  234. package/src/commands/init-workspace.ts +442 -0
  235. package/src/commands/init.ts +554 -0
  236. package/src/commands/lint-grammar-args.ts +79 -0
  237. package/src/commands/lint-grammar-driver.ts +83 -0
  238. package/src/commands/lint-grammar-report.ts +74 -0
  239. package/src/commands/lint-grammar.ts +57 -0
  240. package/src/commands/measure-memory-args.ts +123 -0
  241. package/src/commands/measure-memory-driver.ts +128 -0
  242. package/src/commands/measure-memory.ts +143 -0
  243. package/src/commands/model-docs-args.ts +69 -0
  244. package/src/commands/model-docs-driver.ts +68 -0
  245. package/src/commands/model-docs-report.ts +133 -0
  246. package/src/commands/model-docs.ts +48 -0
  247. package/src/commands/projects-args.ts +62 -0
  248. package/src/commands/projects.ts +76 -0
  249. package/src/commands/query-args.ts +61 -0
  250. package/src/commands/query.ts +58 -0
  251. package/src/commands/reflect-args.ts +75 -0
  252. package/src/commands/reflect-driver.ts +76 -0
  253. package/src/commands/reflect-report.ts +138 -0
  254. package/src/commands/reflect.ts +51 -0
  255. package/src/commands/save-args.ts +74 -0
  256. package/src/commands/save.ts +84 -0
  257. package/src/commands/validate-args.ts +76 -0
  258. package/src/commands/validate-driver.ts +84 -0
  259. package/src/commands/validate-report.ts +95 -0
  260. package/src/commands/validate.ts +58 -0
  261. package/src/commands/watch-args.ts +74 -0
  262. package/src/commands/watch.ts +185 -0
  263. package/src/index.ts +16 -0
  264. package/src/log-level.ts +49 -0
  265. package/src/package-name.ts +39 -0
  266. package/src/spawn-data-server.ts +247 -0
  267. package/src/testing/echo-server.ts +93 -0
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+ /**
11
+ * Test fixture: a standalone Node script that boots a {@link DataServer} over
12
+ * stdio JSON-RPC. Run as an actual child process, so the wire crossing goes
13
+ * through real OS pipes rather than the in-process Duplex pair the data-server's
14
+ * own unit tests use.
15
+ *
16
+ * The services tree comes from {@link makeTestServices}; the one behaviour this
17
+ * fixture adds on top of the stubs is the re-parse seam below.
18
+ */
19
+ import { DataServer } from '@hydranium/data-server';
20
+ import { makeTestServices } from '@hydranium/core/testing';
21
+ import { StreamMessageReader, StreamMessageWriter, createMessageConnection } from 'vscode-jsonrpc/node';
22
+ function parseFakeRoot(text) {
23
+ const match = /^name:(.+)$/.exec(text);
24
+ return { $type: 'FakeRoot', name: match ? match[1] : 'unknown' };
25
+ }
26
+ function main() {
27
+ const bundle = makeTestServices({
28
+ serialize: (_uri, root) => `name:${root.name}`,
29
+ seedDocuments: [
30
+ {
31
+ uri: 'file:///fixture/A.fake',
32
+ root: { $type: 'FakeRoot', name: 'initial' }
33
+ },
34
+ {
35
+ // Echoes the log-level env var the child was spawned with, so the
36
+ // subprocess-integration test can assert the CLI's `--log-level`
37
+ // flag actually reaches the spawned server's environment.
38
+ uri: 'file:///fixture/env.fake',
39
+ root: { $type: 'FakeRoot', name: process.env.HYDRANIUM_LOG_LEVEL ?? 'unset' }
40
+ }
41
+ ],
42
+ seedProjects: [
43
+ { id: 'fixture-p1', referenceName: 'fixture-p1', version: '1.0.0', dependencies: undefined },
44
+ { id: 'fixture-p2', referenceName: 'fixture-p2', version: undefined, dependencies: ['fixture-p1'] }
45
+ ]
46
+ });
47
+ // Re-parse seam: when DataServer.saveModelDocument fires DocumentBuilder.update
48
+ // for a changed URI, replay the latest text into the LangiumDocuments registry
49
+ // so the post-save `getModelDocument` return reflects the new content. A real
50
+ // adopter's DocumentBuilder does this via Langium itself; the stub doesn't, so
51
+ // the fixture supplies the equivalent here.
52
+ const originalUpdate = bundle.documentBuilder.update.bind(bundle.documentBuilder);
53
+ bundle.documentBuilder.update = async (changed, deleted) => {
54
+ await originalUpdate(changed, deleted);
55
+ for (const uri of changed) {
56
+ const uriStr = uri.toString();
57
+ const recent = [...bundle.textDocuments.changes].reverse().find(change => change.uri === uriStr);
58
+ if (recent) {
59
+ bundle.documents.set(uri, parseFakeRoot(recent.text));
60
+ }
61
+ }
62
+ };
63
+ const connection = createMessageConnection(new StreamMessageReader(process.stdin), new StreamMessageWriter(process.stdout));
64
+ if (process.argv.includes('--exit-on-request')) {
65
+ // Fail-fast fixture mode: read a full request (the reader parses the whole
66
+ // message, so the parent's write completed — no partial-write race) then die
67
+ // without replying. Exercises the CLI's `whenTerminated` premature-exit gate.
68
+ connection.onRequest(() => process.exit(0));
69
+ connection.listen();
70
+ return;
71
+ }
72
+ new DataServer(connection, bundle.services);
73
+ connection.listen();
74
+ // The connection holds the event loop open via its message reader; we
75
+ // don't need to wait on anything explicitly. SIGTERM from the parent
76
+ // (the CLI's `shutdown()`) ends the process.
77
+ }
78
+ main();
79
+ //# sourceMappingURL=echo-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"echo-server.js","sourceRoot":"","sources":["../../src/testing/echo-server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@hydranium/cli",
3
+ "version": "1.0.0-next.10",
4
+ "description": "Build-time codegen tools + data-server subcommands for the hydranium framework.",
5
+ "keywords": [
6
+ "hydranium",
7
+ "langium",
8
+ "language-server",
9
+ "lsp",
10
+ "cli",
11
+ "codegen"
12
+ ],
13
+ "homepage": "https://github.com/eclipse-emfcloud/hydranium/tree/main/packages/cli",
14
+ "bugs": {
15
+ "url": "https://github.com/eclipse-emfcloud/hydranium/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/eclipse-emfcloud/hydranium.git",
20
+ "directory": "packages/cli"
21
+ },
22
+ "license": "MIT",
23
+ "author": {
24
+ "name": "Hydranium Team"
25
+ },
26
+ "sideEffects": [
27
+ "./lib/cli.js",
28
+ "./lib/commands/*-driver.js",
29
+ "./lib/testing/echo-server.js"
30
+ ],
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./lib/index.d.ts",
35
+ "default": "./lib/index.js"
36
+ },
37
+ "./lib/cli.js": {
38
+ "types": "./lib/cli.d.ts",
39
+ "default": "./lib/cli.js"
40
+ }
41
+ },
42
+ "main": "lib/index.js",
43
+ "types": "lib/index.d.ts",
44
+ "bin": {
45
+ "hydranium-cli": "lib/cli.js"
46
+ },
47
+ "files": [
48
+ "lib",
49
+ "src",
50
+ "heap-analysis",
51
+ "!lib/**/*.tsbuildinfo"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsc -b && node -e \"require('node:fs').chmodSync('lib/cli.js',0o755)\"",
55
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
56
+ "lint": "eslint src test --max-warnings 0",
57
+ "prepack": "node -e \"const m=require('./package.json'),fs=require('node:fs');const missing=[m.main,...Object.values(m.bin||{})].filter(entry=>entry&&!fs.existsSync(entry));if(missing.length){console.error('prepack '+m.name+': not built ('+missing.join(', ')+' missing). Run the build before packing: a files entry that matches nothing is skipped silently, so the tarball would ship src only.');process.exit(1);}\"",
58
+ "test": "npm run typecheck:test && vitest run",
59
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
60
+ "watch": "tsc -b -w --preserveWatchOutput"
61
+ },
62
+ "dependencies": {
63
+ "@clack/prompts": "~1.7.0",
64
+ "commander": "^14.0.3",
65
+ "ts-morph": "^25.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "@hydranium/core": "1.0.0-next.10",
69
+ "@hydranium/data-server": "1.0.0-next.10",
70
+ "@hydranium/langium": "1.0.0-next.10",
71
+ "@hydranium/protocol": "1.0.0-next.10",
72
+ "rimraf": "^5.0.0",
73
+ "typescript": "^5.8.0",
74
+ "vscode-jsonrpc": "9.0.1",
75
+ "vscode-languageserver": "~10.0.1"
76
+ },
77
+ "peerDependencies": {
78
+ "@hydranium/core": "1.0.0-next.10",
79
+ "@hydranium/data-server": "1.0.0-next.10",
80
+ "@hydranium/langium": "1.0.0-next.10",
81
+ "@hydranium/protocol": "1.0.0-next.10",
82
+ "@memlab/core": "^2.0.3",
83
+ "@memlab/heap-analysis": "^2.0.3",
84
+ "vscode-jsonrpc": "^8.0.0 || ^9.0.0"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "@memlab/core": {
88
+ "optional": true
89
+ },
90
+ "@memlab/heap-analysis": {
91
+ "optional": true
92
+ }
93
+ },
94
+ "engines": {
95
+ "node": ">=22.13"
96
+ },
97
+ "publishConfig": {
98
+ "access": "public"
99
+ },
100
+ "//build": "The chmod is load-bearing, not cruft. npm sets a bin's executable bit through bin-links' fixBin, but ONLY when it first creates the link — and `tsc` never sets it, so after `npm run clean` (which deletes lib/) a rebuild leaves lib/cli.js at 0644 and the node_modules/.bin/hydranium-cli symlink resolves to a non-executable target. Every example's `generate` step then dies with `sh: hydranium-cli: Permission denied`, surfacing as an opaque npm error code 127 during `turbo run build`. npm rebuild, npm rebuild @hydranium/cli AND a full npm install all fail to restore it, because npm will not revisit an existing workspace symlink; only a from-scratch `rm -rf node_modules` install does. Written as `node -e` rather than `chmod +x` so it stays portable — on Windows npm generates .cmd/.ps1 shims where the bit is irrelevant and chmodSync is harmless. The example packages' bins need no such step: their tests spawn them via process.execPath, never through the shim.",
101
+ "//exports": "Two keys, and the SHORT list is the point: `files` ships the whole compiled tree, so without a map every `lib/commands/*.js`, every internal helper and `lib/testing/echo-server.js` is deep-importable the moment this publishes, and semver then applies to all of it. `.` is the library barrel (the data-server subcommands an adopter calls as functions). `./lib/cli.js` is the BINARY ARTEFACT, and it is declared because a consumer that spawns the CLI resolves it BY SPECIFIER — `createRequire(import.meta.url).resolve('@hydranium/cli/lib/cli.js')` — to get the path npx would run; `bin` gives a shim on PATH, not a path, so dropping this key breaks that resolution and the failure surfaces as \"hydranium-cli is not built\", naming the wrong cause. It is declared in the `/lib/` spelling ONLY: that spelling already resolves under both of this repo's module resolvers, so the twin rule (which exists so a BARE subpath is reachable from node10) has nothing to add here and a bare twin would only be a second name for one artefact. `heap-analysis/` needs no key even though `files` ships it — `analyze-heap` reaches the analyzer through a `new URL(..., import.meta.url)` file URL, which `exports` does not gate, and the package's own tests reach it relatively. `./testing` is deliberately absent: `echo-server` is a spawnable stdio fixture, not a reusable export.",
102
+ "//memlab": "@memlab/core and @memlab/heap-analysis are OPTIONAL PEER dependencies, not optionalDependencies, and the distinction is the whole point: npm installs optionalDependencies BY DEFAULT, both memlab packages pin puppeteer exactly, and puppeteer's postinstall downloads a Chrome binary — so every adopter of this CLI paid ~86 MB and a browser download for the one subcommand almost none of them run, and the download can fail and be walked past as a warning. An optional peer is not auto-installed, so `analyze-heap` documents `npm install @memlab/core @memlab/heap-analysis` as the opt-in and every other subcommand is unaffected. Taken pre-publish deliberately: after publish the install shape is what adopters already have.",
103
+ "//prepack": "The publish guard, and it deliberately is NOT a `prepare`: npm runs a workspace `prepare` BEFORE the root `postinstall` that applies patches/vscode-jsonrpc+9.0.1.patch, so building there fails on a cold clone and npm rolls the entire install back. `prepack` runs only when a tarball is made (`npm pack`, `npm publish`) and never on install, so it cannot break the install it has no business touching. It FAILS rather than rebuilds, because the rebuild is exactly the part that ordering defeats. What it defends against: `files` lists `lib`, `lib` is gitignored, and a `files` entry matching nothing is skipped SILENTLY — so `npm publish` from an unbuilt tree emits a tarball of `src` and nothing else, with no error.",
104
+ "//sideEffects": "The listed modules RUN on load rather than exporting anything: `cli.js` is the `bin`, each `*-driver.js` is spawned as its own process and calls `main(process.argv)` at top level, and `echo-server.js` starts a server. Their whole purpose is the effect, so a bundler must never treat them as prunable. Everything else here is library code with no module-level state beyond `new URL(..., import.meta.url)` constants."
105
+ }
@@ -0,0 +1,32 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { readFileSync } from 'node:fs';
11
+
12
+ /** This package's own manifest, reachable at the same relative path from `src/` and `lib/`. */
13
+ const CLI_MANIFEST = new URL('../package.json', import.meta.url);
14
+
15
+ /**
16
+ * The version of the running binary.
17
+ *
18
+ * Read from the manifest rather than emitted as a constant at build time: a
19
+ * generated constant is a second copy of a number the manifest already carries,
20
+ * and nothing goes red while the two disagree.
21
+ *
22
+ * Throws rather than reporting `unknown`: every caller turns the number into a
23
+ * claim someone else acts on — a pinned dependency range, a version in a bug
24
+ * report — where a placeholder is worse than a failure.
25
+ */
26
+ export function readCliVersion(): string {
27
+ const manifest = JSON.parse(readFileSync(CLI_MANIFEST, 'utf-8')) as { version?: unknown };
28
+ if (typeof manifest.version !== 'string' || manifest.version === '') {
29
+ throw new Error('hydranium-cli cannot read its own version from its package manifest.');
30
+ }
31
+ return manifest.version;
32
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /********************************************************************************
3
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
4
+ *
5
+ * This program and the accompanying materials are made available under the
6
+ * terms of the MIT License which is available in the project root.
7
+ *
8
+ * SPDX-License-Identifier: MIT
9
+ ********************************************************************************/
10
+
11
+ import { Command } from 'commander';
12
+ import { readCliVersion } from './cli-version.js';
13
+ import { runAnalyzeHeapCommand } from './commands/analyze-heap-args.js';
14
+ import { runAstGroundTruthCommand } from './commands/ast-ground-truth-args.js';
15
+ import { runGenerateTransferModelCommand } from './commands/generate-transfer-model-args.js';
16
+ import { runInitCommand } from './commands/init-args.js';
17
+ import { InitWizardCancelled } from './commands/init-prompt.js';
18
+ import { runLintGrammarCommand } from './commands/lint-grammar-args.js';
19
+ import { runMeasureMemoryCommand } from './commands/measure-memory-args.js';
20
+ import { runModelDocsCommand } from './commands/model-docs-args.js';
21
+ import { runProjectsCommand } from './commands/projects-args.js';
22
+ import { runQueryCommand } from './commands/query-args.js';
23
+ import { runReflectCommand } from './commands/reflect-args.js';
24
+ import { runSaveCommand } from './commands/save-args.js';
25
+ import { runValidateCommand } from './commands/validate-args.js';
26
+ import { runWatchCommand } from './commands/watch-args.js';
27
+
28
+ /**
29
+ * The subcommand table, as name → handler over that command's own argv tail.
30
+ *
31
+ * Each handler owns its own flag parsing, in a sibling `<name>-args` module that
32
+ * also holds the command's flag list and its help text as data. `commander`
33
+ * provides the dispatch, the command list and the top-level help, but NOT the
34
+ * option parsing: it would generate the per-command help from declared options,
35
+ * at the price of its own error vocabulary replacing the messages scripts match
36
+ * on. `init` could not follow it there in any case — its grammar-scoped flags
37
+ * (`--extensions` applies to the PRECEDING `--grammar`) depend on the relative
38
+ * order of two DIFFERENT option names, which no declarative option model
39
+ * expresses.
40
+ */
41
+ const COMMANDS: Record<string, { readonly summary: string; readonly run: (args: string[]) => void | Promise<void> }> = {
42
+ 'analyze-heap': { summary: 'Analyze a V8 heap snapshot (needs optional @memlab/heap-analysis).', run: runAnalyzeHeapCommand },
43
+ 'ast-ground-truth': {
44
+ summary: "Tally a workspace's live-model $types (analyze-heap --validate input).",
45
+ run: runAstGroundTruthCommand
46
+ },
47
+ 'generate-transfer-model': {
48
+ summary: 'Generate a transfer-model TypeScript file from a Langium AST.',
49
+ run: runGenerateTransferModelCommand
50
+ },
51
+ init: { summary: 'Scaffold a new Hydranium language project (grammar + server head).', run: runInitCommand },
52
+ 'lint-grammar': { summary: 'Check a grammar against framework conventions (CI gate).', run: runLintGrammarCommand },
53
+ 'measure-memory': { summary: 'Measure model-store memory for a workspace (needs @hydranium/core).', run: runMeasureMemoryCommand },
54
+ 'model-docs': { summary: 'Generate a navigable Markdown model reference for adopter docs.', run: runModelDocsCommand },
55
+ projects: { summary: 'List projects exposed by a data-server subprocess.', run: runProjectsCommand },
56
+ query: { summary: 'Print the data-server document envelope for a URI.', run: runQueryCommand },
57
+ reflect: { summary: "Dump a head's grammar/AST reflection (types, terminals, refs).", run: runReflectCommand },
58
+ save: { summary: 'Update + persist a document via a data-server subprocess.', run: runSaveCommand },
59
+ validate: { summary: 'Validate a workspace headlessly; non-zero exit on errors (CI gate).', run: runValidateCommand },
60
+ watch: { summary: 'Subscribe to data-server document updates; print events as NDJSON.', run: runWatchCommand }
61
+ };
62
+
63
+ /**
64
+ * The `commander` program.
65
+ *
66
+ * `.usage()` and the help formatter are pinned to the shape scripts already
67
+ * read — `Usage: hydranium-cli <command> [options]` with a two-space-indented
68
+ * command list — because a CI step that greps the usage line is a contract, not
69
+ * cosmetics. Each subcommand passes its raw tail through to its own handler
70
+ * (`.allowUnknownOption()` plus a variadic operand), so commander owns dispatch
71
+ * and discovery while the handlers keep the exact flag semantics they had.
72
+ */
73
+ function buildProgram(): Command {
74
+ const program = new Command();
75
+ program
76
+ .name('hydranium-cli')
77
+ .usage('<command> [options]')
78
+ .addHelpText('after', '\nRun `hydranium-cli <command> --help` for command-specific options.')
79
+ .helpOption('-h, --help', 'Show this help.')
80
+ // Declared so the top-level help lists it; the dispatch below answers it,
81
+ // because commander would otherwise never see the token — an argv whose
82
+ // first word is not a known subcommand is rejected before `parseAsync`.
83
+ .version(readCliVersion(), '-V, --version', 'Show the version.')
84
+ // No `help <command>` alias: the surface stays exactly the COMMANDS
85
+ // table, and `<command> --help` already covers what it would offer.
86
+ .helpCommand(false)
87
+ .configureHelp({ subcommandTerm: command => command.name() });
88
+ for (const [name, { summary, run }] of Object.entries(COMMANDS)) {
89
+ program
90
+ .command(name)
91
+ .summary(summary)
92
+ .description(summary)
93
+ .allowUnknownOption()
94
+ .allowExcessArguments()
95
+ .helpOption(false)
96
+ .argument('[args...]')
97
+ .action(async (args: string[]) => {
98
+ await run(args);
99
+ });
100
+ }
101
+ return program;
102
+ }
103
+
104
+ async function main(argv: string[]): Promise<void> {
105
+ const [command] = argv;
106
+ // Handled ahead of commander so the three shapes a script can depend on keep
107
+ // their exact contract: a bare invocation prints usage on STDOUT and exits 1,
108
+ // `--help` prints the same on stdout and exits 0, and an unknown command
109
+ // names itself on STDERR with the usage still on stdout.
110
+ const program = buildProgram();
111
+ if (!command || command === '--help' || command === '-h') {
112
+ program.outputHelp();
113
+ process.exit(command ? 0 : 1);
114
+ }
115
+ // Ahead of the unknown-command branch, which claims every first token the
116
+ // COMMANDS table does not name and would report the flag as a misspelt
117
+ // subcommand on stderr with a non-zero exit.
118
+ if (command === '--version' || command === '-V') {
119
+ console.log(readCliVersion());
120
+ process.exit(0);
121
+ }
122
+ if (!Object.hasOwn(COMMANDS, command)) {
123
+ console.error(`Unknown command: ${command}`);
124
+ program.outputHelp();
125
+ process.exit(1);
126
+ }
127
+ await program.parseAsync(argv, { from: 'user' });
128
+ }
129
+
130
+ main(process.argv.slice(2)).catch(err => {
131
+ // Ctrl-C out of the wizard is a choice, not a failure: clack has already
132
+ // drawn its cancel line, so printing a stack trace over it would report the
133
+ // user's own decision back to them as an error.
134
+ if (err instanceof InitWizardCancelled) {
135
+ process.exit(130);
136
+ }
137
+ console.error(err instanceof Error ? err.message : String(err));
138
+ process.exit(1);
139
+ });
@@ -0,0 +1,184 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { DIFF_FLAG, runAnalyzeHeap, type AnalyzeHeapDeps } from './analyze-heap.js';
11
+ import { exitWithUsage, helpRequested, printHelp, type UsageError } from './harness-args.js';
12
+ import { OUT_FILE_FLAG } from './headless-harness.js';
13
+
14
+ /**
15
+ * Flags whose value is mandatory. `--diff` is here for
16
+ * {@link helpRequested}'s benefit — it takes TWO values, which the parser below
17
+ * handles as its own case.
18
+ */
19
+ const VALUE_FLAGS = [DIFF_FLAG, OUT_FILE_FLAG, '--validate', '--json', '--threshold', '--classifier'] as const;
20
+
21
+ /**
22
+ * Flags whose value may be omitted: each names a top-N the analyzer defaults
23
+ * when the token is bare, so a following token is a value only when it is not
24
+ * itself a flag.
25
+ */
26
+ const OPTIONAL_VALUE_FLAGS = ['--breakdown-depth', '--top-concepts', '--strings', '--cutpoints', '--docs', '--holders'] as const;
27
+
28
+ /** Presence-only flags. */
29
+ const BOOL_FLAGS = ['--cutpoints-all', '--renderer'] as const;
30
+
31
+ /**
32
+ * Every flag `analyze-heap` accepts, derived from the three sets the parser
33
+ * scans so the list cannot claim a flag the parser would reject. `--help` is
34
+ * absent because it selects the help block rather than describing a run.
35
+ */
36
+ export const ANALYZE_HEAP_FLAGS: readonly string[] = [...VALUE_FLAGS, ...OPTIONAL_VALUE_FLAGS, ...BOOL_FLAGS];
37
+
38
+ /** The subset whose value is mandatory, so `--help` in a value position reads as data. */
39
+ export const ANALYZE_HEAP_VALUE_FLAGS: readonly string[] = VALUE_FLAGS;
40
+
41
+ /**
42
+ * The `--help` text, as data, held to {@link ANALYZE_HEAP_FLAGS} by a test.
43
+ *
44
+ * The analyzer is a standalone ESM asset spawned as a child, so this is the only
45
+ * copy: a second one beside its argv reads is what let its flags and its help
46
+ * drift apart with nothing able to see it.
47
+ */
48
+ export const ANALYZE_HEAP_HELP: readonly string[] = [
49
+ 'Usage: hydranium-cli analyze-heap <snapshot> [options]',
50
+ ' hydranium-cli analyze-heap --diff <baseline.json> <current.json> [--threshold <pct>]',
51
+ '',
52
+ 'Langium-aware V8 heap-snapshot analyzer (memlab-based). A default run (just',
53
+ '<snapshot>) prints a Markdown report to stdout: concept table, a hierarchical',
54
+ 'breakdown (each concept group drilled into its colon-delimited sub-levels, e.g.',
55
+ 'AST node -> $type, GModel -> kind -> subtype), the duplicate-string / cut-point /',
56
+ 'per-document drill-downs, and sample retainer paths. Loading is the only real',
57
+ 'cost, so the drill-downs run by default; tune or disable them with the flags',
58
+ 'below. The Markdown reads fine as text, saves as a .md file (--out-file), and',
59
+ 'is ideal input for the memory-analysis skill.',
60
+ '',
61
+ 'Reading a snapshot needs the optional @memlab/core + @memlab/heap-analysis peer',
62
+ 'dependencies (`npm install @memlab/core @memlab/heap-analysis`); --diff and',
63
+ '--help need neither.',
64
+ '',
65
+ 'Options:',
66
+ ' <snapshot> the .heapsnapshot to read; omit only with --diff (required).',
67
+ ' --out-file <file.md> also write the Markdown report to a file',
68
+ ' --breakdown-depth [N] max DISPLAY depth of the hierarchical breakdown',
69
+ ' (default: full depth). Aggregation is always',
70
+ ' depth-agnostic; a cap only hides deeper rows — their',
71
+ ' bytes still roll up into the deepest shown row.',
72
+ ' --top-concepts [N] how many of the biggest anchor concepts get a',
73
+ ' retainer-path sample and are the default --holders',
74
+ ' targets (default 6)',
75
+ ' --strings [N] duplicate plain strings, top N (default 20; 0 = off)',
76
+ ' --cutpoints [N] lightweight sole owners, top N (default 10; 0 = off)',
77
+ ' --cutpoints-all include Node/V8/server runtime owners (default: model only)',
78
+ ' --docs [N] heaviest documents, top N (default 15; 0 = off).',
79
+ ' Langium/server-only — skipped in --renderer mode (a',
80
+ ' renderer heap has no documents).',
81
+ ' --holders [a,b,c] who holds each concept: dominator + referrer tallies,',
82
+ ' comma-separated, one heap pass (e.g. CST,Range,Map);',
83
+ ' no value = the top model concepts',
84
+ " --validate <gt.json> validate the snapshot's $type classification against",
85
+ ' the live model (ground truth from the',
86
+ ' hydranium-cli ast-ground-truth subcommand); flags',
87
+ ' drift and computed/derived types',
88
+ ' --json <file> write the analysis as JSON (baseline artifact for --diff)',
89
+ ' --diff <base> <cur> diff two analysis JSONs; prints deltas + verdict,',
90
+ ' exits non-zero on regression (no snapshot needed)',
91
+ ' --threshold <pct> regression threshold for --diff, % growth of total',
92
+ ' shallow (default 5)',
93
+ ' --renderer analyze a BROWSER/renderer heap (the browser-heap',
94
+ ' artefact, from CDP) instead of a Langium server heap:',
95
+ ' buckets by DOM vocabulary (Detached DOM / DOM node /',
96
+ ' Event listener) and the retainer paths to each detached',
97
+ ' concept ARE the listener-retainer analysis. Skips the',
98
+ ' Langium/GLSP classifiers + --validate (no ground truth).',
99
+ ' --classifier <file> ESM module default-exporting a ConceptClassifier',
100
+ ' (classify(node) -> a colon-delimited label path, +',
101
+ ' optional anchorConcepts/grammarTypes/isAnchor) to label',
102
+ " a head's own concepts; repeatable. Tried BEFORE the",
103
+ ' built-in Langium + GLSP defaults (return undefined to',
104
+ ' defer), so a more specific classifier overrides a',
105
+ ' default. Order = precedence.',
106
+ ' -h, --help show this help',
107
+ '',
108
+ 'Notes: snapshot is the language-server process only (no other processes, native',
109
+ 'memory, or browser heap).'
110
+ ];
111
+
112
+ /** What {@link parseAnalyzeHeapArgs} recovered from an argv. */
113
+ export interface AnalyzeHeapArgs {
114
+ /** `true` when the argv selects the JSON-vs-JSON diff, which reads no snapshot. */
115
+ readonly diff: boolean;
116
+ /** The `.heapsnapshot` to read; absent only on the {@link diff} path. */
117
+ readonly snapshot?: string;
118
+ }
119
+
120
+ /**
121
+ * Validate the argv the analyzer will be handed.
122
+ *
123
+ * The values themselves stay the analyzer's to read — it is a standalone ESM
124
+ * asset spawned as a child, and duplicating its numeric coercions here would be
125
+ * a second place for them to drift. What this pass adds is the rejection every
126
+ * sibling subcommand performs: an unrecognised token left inert makes a typo'd
127
+ * `--treshold 0` apply the default silently, so the caller believes a threshold
128
+ * is in force.
129
+ */
130
+ export function parseAnalyzeHeapArgs(args: string[], onError: UsageError = exitWithUsage): AnalyzeHeapArgs {
131
+ const mandatoryValue = new Set<string>(VALUE_FLAGS);
132
+ const optionalValue = new Set<string>(OPTIONAL_VALUE_FLAGS);
133
+ const presenceOnly = new Set<string>(BOOL_FLAGS);
134
+ let diff = false;
135
+ let snapshot: string | undefined;
136
+ for (let index = 0; index < args.length; index += 1) {
137
+ const flag = args[index];
138
+ if (flag === DIFF_FLAG) {
139
+ diff = true;
140
+ if (args[index + 1] === undefined || args[index + 2] === undefined) {
141
+ onError(`Missing value for ${DIFF_FLAG}`);
142
+ }
143
+ index += 2;
144
+ } else if (mandatoryValue.has(flag)) {
145
+ if (args[index + 1] === undefined) {
146
+ onError(`Missing value for ${flag}`);
147
+ }
148
+ index += 1;
149
+ } else if (optionalValue.has(flag)) {
150
+ if (args[index + 1] !== undefined && !args[index + 1].startsWith('-')) {
151
+ index += 1;
152
+ }
153
+ } else if (presenceOnly.has(flag)) {
154
+ // Nothing to claim; the analyzer reads its presence.
155
+ } else if (flag.startsWith('-')) {
156
+ onError(`Unknown option: ${flag} (hydranium-cli analyze-heap --help)`);
157
+ } else if (snapshot === undefined) {
158
+ snapshot = flag;
159
+ } else {
160
+ onError(`Unexpected argument: ${flag} (hydranium-cli analyze-heap --help)`);
161
+ }
162
+ }
163
+ if (!diff && snapshot === undefined) {
164
+ onError('Missing required option: <snapshot> (hydranium-cli analyze-heap --help)');
165
+ }
166
+ return { diff, snapshot };
167
+ }
168
+
169
+ export function runAnalyzeHeapCommand(args: string[], deps: AnalyzeHeapDeps = {}): Promise<void> {
170
+ if (args.length === 0) {
171
+ // Usage on stdout and a non-zero exit, matching the parent binary's bare
172
+ // invocation: a CI step that forgot its snapshot argument must fail rather
173
+ // than read a help page as a completed analysis.
174
+ printHelp(ANALYZE_HEAP_HELP);
175
+ process.exitCode = 1;
176
+ return Promise.resolve();
177
+ }
178
+ if (helpRequested(args, ANALYZE_HEAP_VALUE_FLAGS)) {
179
+ printHelp(ANALYZE_HEAP_HELP);
180
+ return Promise.resolve();
181
+ }
182
+ parseAnalyzeHeapArgs(args);
183
+ return runAnalyzeHeap(args, deps);
184
+ }
@@ -0,0 +1,76 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { fileURLToPath } from 'node:url';
11
+ import { spawnNodeChild } from './headless-harness.js';
12
+
13
+ /**
14
+ * Absolute path to the bundled memlab-based analyzer (a standalone ESM script
15
+ * shipped under the package's `heap-analysis/` assets, not the compiled `lib/`).
16
+ * From `lib/commands/analyze-heap.js` that is `../../heap-analysis/analyze-heap.mjs`.
17
+ */
18
+ const ANALYZER = fileURLToPath(new URL('../../heap-analysis/analyze-heap.mjs', import.meta.url));
19
+
20
+ /** `true` when the optional `@memlab/heap-analysis` dependency is installed and resolvable. */
21
+ function memlabInstalled(): boolean {
22
+ const resolve = (import.meta as { resolve?: (specifier: string) => string }).resolve;
23
+ try {
24
+ resolve?.('@memlab/heap-analysis');
25
+ return resolve !== undefined;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Selects the JSON-vs-JSON comparison, which reads two analysis artifacts and
33
+ * needs neither a snapshot nor memlab. Declared here rather than in the args
34
+ * module so the guard below and the flag list read the same constant.
35
+ */
36
+ export const DIFF_FLAG = '--diff';
37
+
38
+ /** Test-only injection seam for {@link runAnalyzeHeap}, mirroring the sibling harness commands. */
39
+ export interface AnalyzeHeapDeps {
40
+ /** Capture the node argv instead of spawning the real analyzer child. */
41
+ readonly __spawnForTest?: (execArgs: string[]) => Promise<number>;
42
+ /** Override the memlab-availability check. */
43
+ readonly __memlabInstalledForTest?: () => boolean;
44
+ }
45
+
46
+ /**
47
+ * Run the heap-snapshot analyzer. Memlab is an OPTIONAL PEER dependency and a
48
+ * heavy one — it drags in a browser download — so an install of this package
49
+ * pulls in nothing, and a caller that wants heap analysis opts in with the
50
+ * install line the guard below prints. This subcommand loads it only when
51
+ * invoked, by spawning the bundled analyzer in a child process with the large
52
+ * heap memlab needs. All arguments are forwarded verbatim — the analyzer owns
53
+ * how it reads them.
54
+ *
55
+ * The guard is skipped for {@link DIFF_FLAG}, which the analyzer short-circuits
56
+ * before any memlab import: refusing it would demand ~86 MB of dependency for a
57
+ * run that reads two JSON files. Help is intercepted earlier still, by the args
58
+ * module, so it never reaches this function.
59
+ */
60
+ export async function runAnalyzeHeap(args: string[], deps: AnalyzeHeapDeps = {}): Promise<void> {
61
+ const isMemlabInstalled = deps.__memlabInstalledForTest ?? memlabInstalled;
62
+ if (!args.includes(DIFF_FLAG) && !isMemlabInstalled()) {
63
+ console.error(
64
+ 'analyze-heap needs the optional @memlab/heap-analysis dependency (heavy: ~86 MB).\n' +
65
+ 'It is an optional peer dependency, so it is not installed by default; add it with:\n' +
66
+ ' npm install @memlab/core @memlab/heap-analysis'
67
+ );
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const spawnChild = deps.__spawnForTest ?? spawnNodeChild;
72
+ const code = await spawnChild(['--max-old-space-size=8192', ANALYZER, ...args]);
73
+ if (code) {
74
+ process.exitCode = code;
75
+ }
76
+ }