@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,83 @@
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
+ /*
11
+ * Spawned child of the `lint-grammar` subcommand. Launched by the parent with
12
+ * `node <this> --services <module> [--name-property <p>]... [--json] [--strict]`.
13
+ * It dynamic-imports the head's services module, runs the framework's
14
+ * `lintGrammar`, prints the report (human or `--json`) and exits with the gate
15
+ * code (non-zero on errors; `--strict` also fails on warnings). Never imported —
16
+ * only spawned.
17
+ */
18
+
19
+ import { loadHeadlessContext } from './headless-harness.js';
20
+ import { formatLintReport, lintExitCode } from './lint-grammar-report.js';
21
+
22
+ interface DriverArgs {
23
+ servicesModule?: string;
24
+ nameProperties: string[];
25
+ json: boolean;
26
+ strict: boolean;
27
+ }
28
+
29
+ function parseArgs(argv: string[]): DriverArgs {
30
+ const parsed: DriverArgs = { nameProperties: [], json: false, strict: false };
31
+ for (let index = 0; index < argv.length; index += 1) {
32
+ const flag = argv[index];
33
+ const next = (): string => {
34
+ const value = argv[index + 1];
35
+ if (value === undefined) {
36
+ console.error(`Missing value for ${flag}`);
37
+ process.exit(1);
38
+ }
39
+ index += 1;
40
+ return value;
41
+ };
42
+ switch (flag) {
43
+ case '--services':
44
+ parsed.servicesModule = next();
45
+ break;
46
+ case '--name-property':
47
+ parsed.nameProperties.push(next());
48
+ break;
49
+ case '--json':
50
+ parsed.json = true;
51
+ break;
52
+ case '--strict':
53
+ parsed.strict = true;
54
+ break;
55
+ default:
56
+ console.error(`Unknown option: ${flag}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+ return parsed;
61
+ }
62
+
63
+ async function main(argv: string[]): Promise<number> {
64
+ const args = parseArgs(argv);
65
+ if (!args.servicesModule) {
66
+ console.error('lint-grammar-driver: --services <module> is required.');
67
+ process.exit(1);
68
+ }
69
+ const { createServices, coreNode } = await loadHeadlessContext(args.servicesModule);
70
+ const result = coreNode.lintGrammar({
71
+ createServices,
72
+ nameProperties: args.nameProperties.length ? args.nameProperties : undefined
73
+ });
74
+ console.log(formatLintReport(result, { json: args.json }));
75
+ return lintExitCode(result, args.strict);
76
+ }
77
+
78
+ main(process.argv.slice(2))
79
+ .then(code => process.exit(code))
80
+ .catch((error: unknown) => {
81
+ console.error(error instanceof Error ? error.message : String(error));
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,74 @@
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
+ // Type-only import: the CLI is head-neutral and carries no runtime `@hydranium/core`
11
+ // dependency (the head's copy is resolved at run time via `loadHeadlessContext`).
12
+ import type { GrammarLintResult } from '@hydranium/core/node';
13
+
14
+ /** `s` when `count !== 1`, else empty. */
15
+ function plural(count: number): string {
16
+ return count === 1 ? '' : 's';
17
+ }
18
+
19
+ /**
20
+ * The exit code for a lint run: non-zero blocks the CI gate. Errors always fail;
21
+ * `strict` additionally fails on warnings.
22
+ */
23
+ export function lintExitCode(result: GrammarLintResult, strict = false): number {
24
+ if (result.counts.error > 0) {
25
+ return 1;
26
+ }
27
+ if (strict && result.counts.warning > 0) {
28
+ return 1;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ /** One-line severity tally, or the clean-grammar message. */
34
+ function summaryLine(result: GrammarLintResult): string {
35
+ const { error, warning } = result.counts;
36
+ if (error + warning === 0) {
37
+ // "target type", not "target": the tally counts distinct TYPES reachable
38
+ // as a cross-reference target, not cross-reference properties. Several
39
+ // references can share one target type, so the bare word reads as though
40
+ // the lint had missed the rest.
41
+ return `No grammar-convention problems found (${result.checkedReferenceTargets} reference target type${plural(
42
+ result.checkedReferenceTargets
43
+ )} checked).`;
44
+ }
45
+ const parts: string[] = [];
46
+ if (error) {
47
+ parts.push(`${error} error${plural(error)}`);
48
+ }
49
+ if (warning) {
50
+ parts.push(`${warning} warning${plural(warning)}`);
51
+ }
52
+ return `${parts.join(', ')}.`;
53
+ }
54
+
55
+ /**
56
+ * Render a grammar-convention lint result for the terminal. `json` emits the raw
57
+ * {@link GrammarLintResult} (the machine-readable CI contract); the default is a
58
+ * human report — one `severity [rule] message` line per finding — closing with
59
+ * the {@link summaryLine} tally.
60
+ */
61
+ export function formatLintReport(result: GrammarLintResult, options: { json?: boolean } = {}): string {
62
+ if (options.json) {
63
+ return JSON.stringify(result, undefined, 2);
64
+ }
65
+ const lines: string[] = [];
66
+ for (const finding of result.findings) {
67
+ lines.push(` ${finding.severity.padEnd(7)} [${finding.rule}] ${finding.message}`);
68
+ }
69
+ if (result.findings.length > 0) {
70
+ lines.push('');
71
+ }
72
+ lines.push(summaryLine(result));
73
+ return lines.join('\n');
74
+ }
@@ -0,0 +1,57 @@
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 { runDriverChild, type DriverSpawnOptions } from './headless-harness.js';
12
+
13
+ /**
14
+ * Absolute path to the spawned driver. From `lib/commands/lint-grammar.js` the
15
+ * compiled driver sits beside it at `lib/commands/lint-grammar-driver.js`.
16
+ */
17
+ const DRIVER = fileURLToPath(new URL('./lint-grammar-driver.js', import.meta.url));
18
+
19
+ /** Options for the {@link runLintGrammar} subcommand. */
20
+ export interface LintGrammarCommandOptions extends DriverSpawnOptions {
21
+ /** ESM module exporting a zero-arg `createServices(): { shared }` thunk. */
22
+ readonly servicesModule: string;
23
+ /** Property names that satisfy the nameability convention. Empty → the framework default `name`. */
24
+ readonly nameProperties?: readonly string[];
25
+ /** Emit the raw JSON result instead of the human report. */
26
+ readonly json?: boolean;
27
+ /** Also fail (non-zero exit) on warnings, not only errors. */
28
+ readonly strict?: boolean;
29
+ }
30
+
31
+ /** Build the argv the driver child parses. Pure → unit-testable. */
32
+ export function buildLintGrammarDriverArgs(options: LintGrammarCommandOptions): string[] {
33
+ const args = ['--services', options.servicesModule];
34
+ for (const property of options.nameProperties ?? []) {
35
+ args.push('--name-property', property);
36
+ }
37
+ if (options.json) {
38
+ args.push('--json');
39
+ }
40
+ if (options.strict) {
41
+ args.push('--strict');
42
+ }
43
+ return args;
44
+ }
45
+
46
+ /**
47
+ * Headless grammar-convention lint — a CI gate that checks a head's grammar against
48
+ * the framework's expectations (nameable cross-reference targets, an entry rule) and
49
+ * exits non-zero when a violation is found (`--strict` also fails on warnings).
50
+ * Read-only: no workspace is built (the reflection is static once the head registers
51
+ * its language), so this subcommand takes only `--services` and the name-property
52
+ * overrides. The work runs in a spawned child that dynamic-imports the head's
53
+ * `createServices` module, keeping the binary language-agnostic.
54
+ */
55
+ export function runLintGrammar(options: LintGrammarCommandOptions): Promise<void> {
56
+ return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildLintGrammarDriverArgs(options)], options);
57
+ }
@@ -0,0 +1,123 @@
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 {
11
+ exitWithUsage,
12
+ helpRequested,
13
+ LOG_LEVEL_FLAG,
14
+ logLevelHelpLine,
15
+ logLevelOption,
16
+ numericOption,
17
+ parseHarnessArgs,
18
+ printHelp,
19
+ SERVICES_FLAG,
20
+ type UsageError
21
+ } from './harness-args.js';
22
+ import { parseProfileDimensions, runMeasureMemory, type MeasureMemoryCommandOptions } from './measure-memory.js';
23
+
24
+ const VALUE_FLAGS = [
25
+ '--edits',
26
+ '--edit-docs',
27
+ '--churn-suffix',
28
+ '--settle',
29
+ '--snapshot-path',
30
+ '--profile',
31
+ '--session-out',
32
+ LOG_LEVEL_FLAG
33
+ ] as const;
34
+ const BOOL_FLAGS = ['--snapshot', '--json'] as const;
35
+
36
+ /**
37
+ * Every flag `measure-memory` accepts, derived from the sets the parser is
38
+ * handed so the list cannot claim a flag the parser would reject. `--services`
39
+ * is consumed by the shared harness parser itself, so it is in neither set.
40
+ */
41
+ export const MEASURE_MEMORY_FLAGS: readonly string[] = [SERVICES_FLAG, ...VALUE_FLAGS, ...BOOL_FLAGS];
42
+
43
+ /** The subset that takes a value, so `--help` in a value position reads as data. */
44
+ export const MEASURE_MEMORY_VALUE_FLAGS: readonly string[] = [SERVICES_FLAG, ...VALUE_FLAGS];
45
+
46
+ /** The `--help` text, as data, held to {@link MEASURE_MEMORY_FLAGS} by a test. */
47
+ export const MEASURE_MEMORY_HELP: readonly string[] = [
48
+ 'Usage: hydranium-cli measure-memory --services <module> <workspace> [options]',
49
+ '',
50
+ 'Measure model-store memory for a workspace. `<module>` is an ESM module that',
51
+ 'exports a zero-arg `createServices(): { shared }` thunk (the head wires its own',
52
+ 'filesystem inside). Runs in a `--expose-gc` child for post-GC readings; the',
53
+ 'baseline / after-build / churn lines print on stdout. Needs @hydranium/core.',
54
+ '',
55
+ 'Options:',
56
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
57
+ ' <workspace> Workspace root (path or file URI) to build (required).',
58
+ ' --edits <N> Rebuild-churn cycles to probe for retention. Default: 0 (skip).',
59
+ ' --edit-docs <N> Documents to churn per cycle. Default: 25.',
60
+ ' --churn-suffix <ext> Only churn documents whose URI ends with this suffix.',
61
+ ' --settle <ms> Wait <ms> after the build before measuring, so a',
62
+ ' timer-driven residency policy (shed-closed-when-idle)',
63
+ " sheds first. Pass above the policy's idleMs; omit to",
64
+ ' measure the pre-shed (always-keep) baseline.',
65
+ ' --snapshot Write a `.heapsnapshot` after the build.',
66
+ ' --snapshot-path <p> Snapshot path. Default: <cwd>/<workspace-basename>.heapsnapshot.',
67
+ ' --profile <dims> Capture sampled profiles around the build (+churn) into a',
68
+ ' profiling session. Comma-separated: cpu,alloc,gc,eld,heap.',
69
+ ' --session-out <dir> Parent directory for the profiling session folder (with --profile).',
70
+ ' --json Emit the measurement as one JSON document instead of the',
71
+ ' progress lines: documentCount, buildMs, emptyHeapBytes,',
72
+ ' afterBuildHeapBytes, and churnGrowthBytes / snapshotPath /',
73
+ ' profilingSession where the run produced them.',
74
+ logLevelHelpLine(22)
75
+ ];
76
+
77
+ /**
78
+ * Validate `--profile` in the PARENT, where the numeric flags beside it are
79
+ * already validated.
80
+ *
81
+ * Returns the RAW string rather than the parsed dimensions on purpose: the child
82
+ * re-parses them, because that is where they are used, and this exists only so a
83
+ * typo fails as a usage error before a process is spawned and a whole head
84
+ * imported. Parsing here and passing the result would change what crosses to the
85
+ * child for no gain.
86
+ */
87
+ function validatedProfile(csv: string | undefined, onError: UsageError): string | undefined {
88
+ if (csv === undefined) {
89
+ return undefined;
90
+ }
91
+ try {
92
+ parseProfileDimensions(csv);
93
+ } catch (err: unknown) {
94
+ onError(`Option --profile: ${err instanceof Error ? err.message : String(err)}`);
95
+ }
96
+ return csv;
97
+ }
98
+
99
+ export function parseMeasureMemoryArgs(args: string[], onError: UsageError = exitWithUsage): MeasureMemoryCommandOptions {
100
+ const { servicesModule, workspace, options } = parseHarnessArgs(args, 'measure-memory', VALUE_FLAGS, BOOL_FLAGS, { onError });
101
+ return {
102
+ servicesModule,
103
+ workspace,
104
+ editCycles: numericOption(options['--edits'], '--edits', onError),
105
+ editDocs: numericOption(options['--edit-docs'], '--edit-docs', onError),
106
+ churnSuffix: options['--churn-suffix'],
107
+ settleMs: numericOption(options['--settle'], '--settle', onError),
108
+ writeSnapshot: options['--snapshot'] === 'true',
109
+ snapshotPath: options['--snapshot-path'],
110
+ profile: validatedProfile(options['--profile'], onError),
111
+ sessionOut: options['--session-out'],
112
+ json: options['--json'] === 'true',
113
+ logLevel: logLevelOption(options[LOG_LEVEL_FLAG])
114
+ };
115
+ }
116
+
117
+ export function runMeasureMemoryCommand(args: string[]): Promise<void> {
118
+ if (helpRequested(args, MEASURE_MEMORY_VALUE_FLAGS)) {
119
+ printHelp(MEASURE_MEMORY_HELP);
120
+ return Promise.resolve();
121
+ }
122
+ return runMeasureMemory(parseMeasureMemoryArgs(args));
123
+ }
@@ -0,0 +1,128 @@
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
+ /*
11
+ * Spawned child of the `measure-memory` subcommand. Launched by the parent with
12
+ * `node --expose-gc --max-old-space-size=8192 <this> --services <module> <workspace> ...`
13
+ * so the underlying harness gets post-GC readings. It dynamic-imports the head's
14
+ * services module and runs the framework's `measureModelMemory`, streaming the
15
+ * baseline / after-build / churn lines to stdout. Never imported — only spawned.
16
+ */
17
+
18
+ import { loadHeadlessContext } from './headless-harness.js';
19
+ import { parseProfileDimensions } from './measure-memory.js';
20
+
21
+ interface DriverArgs {
22
+ servicesModule?: string;
23
+ workspace?: string;
24
+ editCycles?: number;
25
+ editDocs?: number;
26
+ churnDocSuffix?: string;
27
+ settleMs?: number;
28
+ writeSnapshot: boolean;
29
+ snapshotPath?: string;
30
+ profile?: string;
31
+ sessionOut?: string;
32
+ json: boolean;
33
+ }
34
+
35
+ function parseArgs(argv: string[]): DriverArgs {
36
+ const parsed: DriverArgs = { writeSnapshot: false, json: false };
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const flag = argv[index];
39
+ const next = (): string => {
40
+ const value = argv[index + 1];
41
+ if (value === undefined) {
42
+ console.error(`Missing value for ${flag}`);
43
+ process.exit(1);
44
+ }
45
+ index += 1;
46
+ return value;
47
+ };
48
+ switch (flag) {
49
+ case '--services':
50
+ parsed.servicesModule = next();
51
+ break;
52
+ case '--edits':
53
+ parsed.editCycles = Number(next());
54
+ break;
55
+ case '--edit-docs':
56
+ parsed.editDocs = Number(next());
57
+ break;
58
+ case '--churn-suffix':
59
+ parsed.churnDocSuffix = next();
60
+ break;
61
+ case '--settle':
62
+ parsed.settleMs = Number(next());
63
+ break;
64
+ case '--snapshot':
65
+ parsed.writeSnapshot = true;
66
+ break;
67
+ case '--snapshot-path':
68
+ parsed.snapshotPath = next();
69
+ break;
70
+ case '--profile':
71
+ parsed.profile = next();
72
+ break;
73
+ case '--session-out':
74
+ parsed.sessionOut = next();
75
+ break;
76
+ case '--json':
77
+ parsed.json = true;
78
+ break;
79
+ default:
80
+ if (flag.startsWith('--')) {
81
+ console.error(`Unknown option: ${flag}`);
82
+ process.exit(1);
83
+ }
84
+ parsed.workspace = flag;
85
+ }
86
+ }
87
+ return parsed;
88
+ }
89
+
90
+ async function main(argv: string[]): Promise<void> {
91
+ const args = parseArgs(argv);
92
+ if (!args.servicesModule || !args.workspace) {
93
+ console.error('measure-memory-driver: --services <module> and a <workspace> are required.');
94
+ process.exit(1);
95
+ }
96
+ const { createServices, coreNode } = await loadHeadlessContext(args.servicesModule);
97
+ const result = await coreNode.measureModelMemory({
98
+ createServices,
99
+ workspace: args.workspace,
100
+ editCycles: args.editCycles,
101
+ editDocs: args.editDocs,
102
+ churnDocSuffix: args.churnDocSuffix,
103
+ settleMs: args.settleMs,
104
+ writeSnapshot: args.writeSnapshot,
105
+ snapshotPath: args.snapshotPath,
106
+ profile: args.profile ? parseProfileDimensions(args.profile) : undefined,
107
+ sessionOut: args.sessionOut,
108
+ // Omitted under `--json` rather than redirected: every progress line would
109
+ // otherwise land on the same stream as the document, so the caller's
110
+ // `JSON.parse` would see a preamble it has no way to bound.
111
+ log: args.json ? undefined : line => console.log(line)
112
+ });
113
+ if (args.json) {
114
+ console.log(JSON.stringify(result, null, 2));
115
+ return;
116
+ }
117
+ console.log(`SUMMARY: docs=${result.documentCount} build=${Math.round(result.buildMs)}ms`);
118
+ if (result.profilingSession) {
119
+ console.log(`SESSION: ${result.profilingSession}`);
120
+ }
121
+ }
122
+
123
+ main(process.argv.slice(2))
124
+ .then(() => process.exit(0))
125
+ .catch((error: unknown) => {
126
+ console.error(error instanceof Error ? error.message : String(error));
127
+ process.exit(1);
128
+ });
@@ -0,0 +1,143 @@
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 type { ProfileCaptureOptions } from '@hydranium/core/node';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { runDriverChild, type DriverSpawnOptions } from './headless-harness.js';
13
+
14
+ /**
15
+ * Absolute path to the spawned driver. From `lib/commands/measure-memory.js`
16
+ * the compiled driver sits beside it at `lib/commands/measure-memory-driver.js`.
17
+ */
18
+ const DRIVER = fileURLToPath(new URL('./measure-memory-driver.js', import.meta.url));
19
+
20
+ /** Options for the {@link runMeasureMemory} subcommand. */
21
+ export interface MeasureMemoryCommandOptions extends DriverSpawnOptions {
22
+ /** ESM module exporting a zero-arg `createServices(): { shared }` thunk. */
23
+ readonly servicesModule: string;
24
+ /** Workspace root (filesystem path or file URI) to build. */
25
+ readonly workspace: string;
26
+ /** Rebuild-churn cycles to probe for retention (default 0 = skip). */
27
+ readonly editCycles?: number;
28
+ /** Documents to churn per cycle. */
29
+ readonly editDocs?: number;
30
+ /** Only churn documents whose URI path ends with this suffix. */
31
+ readonly churnSuffix?: string;
32
+ /**
33
+ * Wait this many milliseconds after the build before measuring, so a
34
+ * timer-driven residency policy (`CstResidencyService`'s
35
+ * `shed-closed-when-idle`) sheds before the reading is taken. Pass a value
36
+ * comfortably above the policy's `idleMs`; omit (or `0`) to measure the
37
+ * pre-shed heap (the always-keep-equivalent baseline).
38
+ */
39
+ readonly settleMs?: number;
40
+ /** Write a `.heapsnapshot` after the build. */
41
+ readonly writeSnapshot?: boolean;
42
+ /** Snapshot path (default `<cwd>/<workspace-basename>.heapsnapshot`). */
43
+ readonly snapshotPath?: string;
44
+ /** Comma-separated profile dimensions to capture; see {@link parseProfileDimensions} for the tokens. */
45
+ readonly profile?: string;
46
+ /** Parent directory for the profiling session folder. Only used with {@link profile}. */
47
+ readonly sessionOut?: string;
48
+ /**
49
+ * Emit the `MeasureModelMemoryResult` as JSON instead of the progress lines.
50
+ *
51
+ * The progress lines are suppressed rather than kept alongside it, because the
52
+ * flag exists so a script can read stdout as one document — a preamble on the
53
+ * same stream would leave every caller stripping it by hand.
54
+ */
55
+ readonly json?: boolean;
56
+ }
57
+
58
+ /**
59
+ * Parse a `--profile` dimension list (`cpu,alloc,gc,eld,heap`) into
60
+ * {@link ProfileCaptureOptions}. Case- and whitespace-tolerant; throws on an
61
+ * unknown token so a typo fails loud rather than silently capturing nothing.
62
+ */
63
+ export function parseProfileDimensions(csv: string): ProfileCaptureOptions {
64
+ const options: ProfileCaptureOptions = {};
65
+ for (const raw of csv.split(',')) {
66
+ const token = raw.trim().toLowerCase();
67
+ if (!token) {
68
+ continue;
69
+ }
70
+ switch (token) {
71
+ case 'cpu':
72
+ options.cpu = true;
73
+ break;
74
+ case 'alloc':
75
+ case 'allocation':
76
+ options.allocation = true;
77
+ break;
78
+ case 'gc':
79
+ options.gc = true;
80
+ break;
81
+ case 'eld':
82
+ case 'eventloop':
83
+ case 'event-loop':
84
+ options.eventLoopDelay = true;
85
+ break;
86
+ case 'heap':
87
+ case 'heapsnapshot':
88
+ options.heapSnapshot = true;
89
+ break;
90
+ default:
91
+ throw new Error(`Unknown profile dimension: ${token} (expected cpu, alloc, gc, eld, heap)`);
92
+ }
93
+ }
94
+ return options;
95
+ }
96
+
97
+ /**
98
+ * Build the argv the driver child parses. Pure → unit-testable; mirrors the
99
+ * driver's own option names.
100
+ */
101
+ export function buildDriverArgs(options: MeasureMemoryCommandOptions): string[] {
102
+ const args = ['--services', options.servicesModule, options.workspace];
103
+ if (options.editCycles !== undefined) {
104
+ args.push('--edits', String(options.editCycles));
105
+ }
106
+ if (options.editDocs !== undefined) {
107
+ args.push('--edit-docs', String(options.editDocs));
108
+ }
109
+ if (options.churnSuffix !== undefined) {
110
+ args.push('--churn-suffix', options.churnSuffix);
111
+ }
112
+ if (options.settleMs !== undefined) {
113
+ args.push('--settle', String(options.settleMs));
114
+ }
115
+ if (options.writeSnapshot) {
116
+ args.push('--snapshot');
117
+ }
118
+ if (options.snapshotPath !== undefined) {
119
+ args.push('--snapshot-path', options.snapshotPath);
120
+ }
121
+ if (options.profile !== undefined) {
122
+ args.push('--profile', options.profile);
123
+ }
124
+ if (options.sessionOut !== undefined) {
125
+ args.push('--session-out', options.sessionOut);
126
+ }
127
+ if (options.json) {
128
+ args.push('--json');
129
+ }
130
+ return args;
131
+ }
132
+
133
+ /**
134
+ * Headless model-store memory measurement. `hydranium-cli` is language-agnostic
135
+ * and cannot import a head's `create<Lang>Services`, so the head passes a module
136
+ * that exports a zero-arg `createServices` thunk (`--services`). Because the
137
+ * underlying `measureModelMemory` harness needs post-GC readings, the work runs
138
+ * in a spawned child launched with `--expose-gc`; the driver dynamic-imports the
139
+ * module and reports the baseline / after-build / churn lines on stdout.
140
+ */
141
+ export function runMeasureMemory(options: MeasureMemoryCommandOptions): Promise<void> {
142
+ return runDriverChild(['--expose-gc', '--max-old-space-size=8192', DRIVER, ...buildDriverArgs(options)], options);
143
+ }
@@ -0,0 +1,69 @@
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 {
11
+ exitWithUsage,
12
+ helpRequested,
13
+ LOG_LEVEL_FLAG,
14
+ logLevelHelpLine,
15
+ logLevelOption,
16
+ parseHarnessArgs,
17
+ printHelp,
18
+ SERVICES_FLAG,
19
+ type UsageError
20
+ } from './harness-args.js';
21
+ import { OUT_FILE_FLAG } from './headless-harness.js';
22
+ import { runModelDocs, type ModelDocsCommandOptions } from './model-docs.js';
23
+
24
+ const VALUE_FLAGS = [OUT_FILE_FLAG, LOG_LEVEL_FLAG] as const;
25
+
26
+ /**
27
+ * Every flag `model-docs` accepts, derived from the sets the parser is handed so
28
+ * the list cannot claim a flag the parser would reject. `--services` is consumed
29
+ * by the shared harness parser itself, so it is in neither set.
30
+ */
31
+ export const MODEL_DOCS_FLAGS: readonly string[] = [SERVICES_FLAG, ...VALUE_FLAGS];
32
+
33
+ /** The subset that takes a value, so `--help` in a value position reads as data. */
34
+ export const MODEL_DOCS_VALUE_FLAGS: readonly string[] = MODEL_DOCS_FLAGS;
35
+
36
+ /** The `--help` text, as data, held to {@link MODEL_DOCS_FLAGS} by a test. */
37
+ export const MODEL_DOCS_HELP: readonly string[] = [
38
+ 'Usage: hydranium-cli model-docs --services <module> [--out-file <file>] [--log-level <lvl>]',
39
+ '',
40
+ 'Generate a navigable Markdown reference of every AST node type — a type index,',
41
+ 'cross-linked super/sub types, a reverse "referenced by" index, and a per-language',
42
+ 'grammar summary — for publishing into adopter docs. Read-only: no workspace is',
43
+ 'built. Emits Markdown on stdout, or into `--out-file`; the machine-readable form',
44
+ 'is `hydranium-cli reflect --json`. `<module>` is an ESM module exporting a',
45
+ 'zero-arg `createServices(): { shared }` thunk. Needs @hydranium/core.',
46
+ '',
47
+ 'Options:',
48
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
49
+ ' --out-file <file> Write the Markdown to this file instead of stdout. Written',
50
+ ' only once the report exists, unlike a shell redirection,',
51
+ ' which truncates the file before the head is even booted.',
52
+ logLevelHelpLine(22)
53
+ ];
54
+
55
+ export function parseModelDocsArgs(args: string[], onError: UsageError = exitWithUsage): ModelDocsCommandOptions {
56
+ const { servicesModule, options } = parseHarnessArgs(args, 'model-docs', VALUE_FLAGS, [], {
57
+ requireWorkspace: false,
58
+ onError
59
+ });
60
+ return { servicesModule, outFile: options[OUT_FILE_FLAG], logLevel: logLevelOption(options[LOG_LEVEL_FLAG]) };
61
+ }
62
+
63
+ export function runModelDocsCommand(args: string[]): Promise<void> {
64
+ if (helpRequested(args, MODEL_DOCS_VALUE_FLAGS)) {
65
+ printHelp(MODEL_DOCS_HELP);
66
+ return Promise.resolve();
67
+ }
68
+ return runModelDocs(parseModelDocsArgs(args));
69
+ }