@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,138 @@
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 { GrammarReflection, TypeReflection } from '@hydranium/core/node';
13
+
14
+ /** Escape a value for a Markdown table cell (only `|` and newlines can break a row). */
15
+ function cell(value: string): string {
16
+ return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
17
+ }
18
+
19
+ /** Wrap a value in inline-code backticks, escaping any embedded backtick run. */
20
+ function code(value: string): string {
21
+ return `\`${value}\``;
22
+ }
23
+
24
+ /** The Markdown for each language's grammar surface — extensions, entry rule, terminals table. */
25
+ function languageSection(reflection: GrammarReflection): string[] {
26
+ const lines: string[] = ['## Languages', ''];
27
+ for (const language of reflection.languages) {
28
+ lines.push(`### ${language.languageId}`, '');
29
+ const extensions = language.fileExtensions.length ? language.fileExtensions.map(code).join(', ') : '_none_';
30
+ lines.push(`- File extensions: ${extensions}`);
31
+ lines.push(`- Entry rule: ${language.entryRule ? code(language.entryRule) : '_none_'}`);
32
+ lines.push(`- Terminals: ${language.terminals.length}`, '');
33
+ if (language.terminals.length) {
34
+ lines.push('| Terminal | Pattern | Hidden |', '| --- | --- | --- |');
35
+ for (const terminal of language.terminals) {
36
+ const pattern = terminal.pattern !== undefined ? code(terminal.pattern) : '';
37
+ lines.push(`| ${cell(terminal.name)} | ${cell(pattern)} | ${terminal.hidden ? 'yes' : ''} |`);
38
+ }
39
+ lines.push('');
40
+ }
41
+ }
42
+ return lines;
43
+ }
44
+
45
+ /**
46
+ * A depth-first hierarchy tree from the roots (types with no super types) down
47
+ * through `directSubTypes`. A type reachable from several supertypes appears under
48
+ * each; a cycle (should not occur in a grammar) is broken by not re-descending a
49
+ * type already on the current path.
50
+ */
51
+ function hierarchySection(reflection: GrammarReflection): string[] {
52
+ const byName = new Map(reflection.types.map(type => [type.name, type]));
53
+ const roots = reflection.types.filter(type => type.superTypes.length === 0);
54
+ const lines: string[] = ['## Type hierarchy', ''];
55
+ const walk = (type: TypeReflection, depth: number, path: ReadonlySet<string>): void => {
56
+ lines.push(`${' '.repeat(depth)}- ${type.name}`);
57
+ const nextPath = new Set(path).add(type.name);
58
+ for (const childName of type.directSubTypes) {
59
+ const child = byName.get(childName);
60
+ if (child && !nextPath.has(childName)) {
61
+ walk(child, depth + 1, nextPath);
62
+ }
63
+ }
64
+ };
65
+ for (const root of roots) {
66
+ walk(root, 0, new Set());
67
+ }
68
+ lines.push('');
69
+ return lines;
70
+ }
71
+
72
+ /** The `Type.property → TargetType` list of every cross-reference in the grammar. */
73
+ function crossReferenceSection(reflection: GrammarReflection): string[] {
74
+ const lines: string[] = ['## Cross-reference targets', ''];
75
+ let hasCrossReferences = false;
76
+ for (const type of reflection.types) {
77
+ for (const property of type.properties) {
78
+ if (property.referenceType !== undefined) {
79
+ lines.push(`- ${code(`${type.name}.${property.name}`)} → ${code(property.referenceType)}`);
80
+ hasCrossReferences = true;
81
+ }
82
+ }
83
+ }
84
+ if (!hasCrossReferences) {
85
+ lines.push('_No cross-references._');
86
+ }
87
+ lines.push('');
88
+ return lines;
89
+ }
90
+
91
+ /** Per-type detail: super types plus a property table (name, reference target, array, default). */
92
+ function typeDetailSection(reflection: GrammarReflection): string[] {
93
+ const lines: string[] = [`## Types (${reflection.types.length})`, ''];
94
+ for (const type of reflection.types) {
95
+ lines.push(`### ${type.name}`, '');
96
+ if (type.superTypes.length) {
97
+ lines.push(`- Super types: ${type.superTypes.map(code).join(', ')}`);
98
+ }
99
+ if (type.directSubTypes.length) {
100
+ lines.push(`- Sub types: ${type.directSubTypes.map(code).join(', ')}`);
101
+ }
102
+ lines.push('');
103
+ if (type.properties.length) {
104
+ lines.push('| Property | Reference target | Array | Default |', '| --- | --- | --- | --- |');
105
+ for (const property of type.properties) {
106
+ const reference = property.referenceType !== undefined ? code(property.referenceType) : '';
107
+ lines.push(
108
+ `| ${cell(property.name)} | ${cell(reference)} | ${property.array ? 'yes' : ''} | ${property.hasDefault ? 'yes' : ''} |`
109
+ );
110
+ }
111
+ } else {
112
+ lines.push('_No properties._');
113
+ }
114
+ lines.push('');
115
+ }
116
+ return lines;
117
+ }
118
+
119
+ /**
120
+ * Render a grammar reflection for the terminal. `json` emits the raw
121
+ * {@link GrammarReflection} (the machine-readable contract); the default is a
122
+ * Markdown reference — a per-language grammar surface (terminals, entry rule), the
123
+ * type-hierarchy tree, the cross-reference target list, and a per-type property
124
+ * table — suitable for piping into a `.md` file.
125
+ */
126
+ export function formatReflectionReport(reflection: GrammarReflection, options: { json?: boolean } = {}): string {
127
+ if (options.json) {
128
+ return JSON.stringify(reflection, undefined, 2);
129
+ }
130
+ return [
131
+ '# Grammar reflection',
132
+ '',
133
+ ...languageSection(reflection),
134
+ ...hierarchySection(reflection),
135
+ ...crossReferenceSection(reflection),
136
+ ...typeDetailSection(reflection)
137
+ ].join('\n');
138
+ }
@@ -0,0 +1,51 @@
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 { OUT_FILE_FLAG, runDriverChild, type DriverSpawnOptions } from './headless-harness.js';
12
+
13
+ /**
14
+ * Absolute path to the spawned driver. From `lib/commands/reflect.js` the
15
+ * compiled driver sits beside it at `lib/commands/reflect-driver.js`.
16
+ */
17
+ const DRIVER = fileURLToPath(new URL('./reflect-driver.js', import.meta.url));
18
+
19
+ /** Options for the {@link runReflect} subcommand. */
20
+ export interface ReflectCommandOptions extends DriverSpawnOptions {
21
+ /** ESM module exporting a zero-arg `createServices(): { shared }` thunk. */
22
+ readonly servicesModule: string;
23
+ /** Emit the raw JSON reflection instead of the human markdown report. */
24
+ readonly json?: boolean;
25
+ /** Write the report to this file instead of stdout. */
26
+ readonly outFile?: string;
27
+ }
28
+
29
+ /** Build the argv the driver child parses. Pure → unit-testable. */
30
+ export function buildReflectDriverArgs(options: ReflectCommandOptions): string[] {
31
+ const args = ['--services', options.servicesModule];
32
+ if (options.json) {
33
+ args.push('--json');
34
+ }
35
+ if (options.outFile !== undefined) {
36
+ args.push(OUT_FILE_FLAG, options.outFile);
37
+ }
38
+ return args;
39
+ }
40
+
41
+ /**
42
+ * Headless grammar/AST reflection — dumps a head's type hierarchy, terminals and
43
+ * cross-reference targets. Read-only: no workspace is built (the reflection is
44
+ * available as soon as the head's services register a language), so unlike the
45
+ * sibling harnesses this subcommand takes only `--services`. The work runs in a
46
+ * spawned child that dynamic-imports the head's `createServices` module, keeping
47
+ * the binary language-agnostic.
48
+ */
49
+ export function runReflect(options: ReflectCommandOptions): Promise<void> {
50
+ return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildReflectDriverArgs(options)], options);
51
+ }
@@ -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
+ import {
11
+ assertRequired,
12
+ exitWithUsage,
13
+ helpRequested,
14
+ parseFlagOptions,
15
+ parseServerSpawnOptions,
16
+ printHelp,
17
+ SERVER_SPAWN_VALUE_FLAGS,
18
+ type UsageError
19
+ } from './harness-args.js';
20
+ import { runSave, type SaveCommandOptions } from './save.js';
21
+
22
+ const OWN_FLAGS = ['--uri', '--content', '--client-id'] as const;
23
+
24
+ /**
25
+ * Every flag `save` accepts, derived from its own list plus the shared spawn
26
+ * flags, so the list cannot claim a flag the parser would reject.
27
+ */
28
+ export const SAVE_FLAGS: readonly string[] = [...SERVER_SPAWN_VALUE_FLAGS, ...OWN_FLAGS];
29
+
30
+ /**
31
+ * The subset that takes a value, so `--help` in a value position reads as data.
32
+ * Every flag in this family takes one, so the two lists coincide — and `--content`
33
+ * is the one where a caller plausibly writes that literal text.
34
+ */
35
+ export const SAVE_VALUE_FLAGS: readonly string[] = SAVE_FLAGS;
36
+
37
+ /** The `--help` text, as data, held to {@link SAVE_FLAGS} by a test. */
38
+ export const SAVE_HELP: readonly string[] = [
39
+ 'Usage: hydranium-cli save --server "<cmd>..." --uri <uri> --content <text|@file> [--client-id <id>] [--cwd <dir>] [--log-level <level>]',
40
+ '',
41
+ 'Update the document at `<uri>` with `<content>` and persist via saveModelDocument.',
42
+ 'Output is the post-save envelope as a single JSON line.',
43
+ '',
44
+ 'Options:',
45
+ ' --server "<cmd>" Command-line for the data-server subprocess (required).',
46
+ ' --uri <uri> Document URI (required).',
47
+ ' --content <text> Content to write (required). Prefix `@` to read from a file',
48
+ ' (escape with `\\@` for literal `@`-leading content).',
49
+ ' --client-id <id> clientId for authorship attribution. Default: hydranium-cli.',
50
+ ' --cwd <dir> Working directory for the spawned child. Default: cwd.',
51
+ ' --log-level <lvl> Log threshold for the spawned server (off|error|warn|info|debug|trace).'
52
+ ];
53
+
54
+ export function parseSaveArgs(args: string[], onError: UsageError = exitWithUsage): SaveCommandOptions {
55
+ const { serverCommand, serverArgs, cwd, logLevel, extra } = parseServerSpawnOptions(args, 'save', onError);
56
+ const { uri, content, clientId } = parseFlagOptions(extra, 'save', OWN_FLAGS, onError);
57
+ return {
58
+ serverCommand,
59
+ serverArgs,
60
+ cwd,
61
+ logLevel,
62
+ uri: assertRequired(uri, '--uri', 'save', onError),
63
+ content: assertRequired(content, '--content', 'save', onError),
64
+ clientId
65
+ };
66
+ }
67
+
68
+ export function runSaveCommand(args: string[]): Promise<void> {
69
+ if (helpRequested(args, SAVE_VALUE_FLAGS)) {
70
+ printHelp(SAVE_HELP);
71
+ return Promise.resolve();
72
+ }
73
+ return runSave(parseSaveArgs(args));
74
+ }
@@ -0,0 +1,84 @@
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 { LogThreshold, TransferElement } from '@hydranium/protocol';
11
+ import type { DataServerProtocol } from '@hydranium/protocol/data';
12
+ import * as fs from 'node:fs/promises';
13
+ import { logLevelEnv } from '../log-level.js';
14
+ import { withDataServer } from '../spawn-data-server.js';
15
+
16
+ /**
17
+ * Options for the {@link runSave} subcommand. Updates the document at
18
+ * `uri` with `content` (literal text or `@<path>` for a file reference)
19
+ * and persists it via the data-server's `saveModelDocument` RPC. Writes
20
+ * the post-save envelope as a single JSON line — the same shape
21
+ * `query` emits, so a save's output is consumable by the same `jq`
22
+ * pipelines.
23
+ *
24
+ * `clientId` defaults to `'hydranium-cli'` — adopters wanting a richer
25
+ * identity (per-user, per-script) override.
26
+ */
27
+ export interface SaveCommandOptions {
28
+ readonly serverCommand: string;
29
+ readonly serverArgs?: readonly string[];
30
+ readonly cwd?: string;
31
+ /** Log threshold for the spawned server, set on its `HYDRANIUM_LOG_LEVEL` env. */
32
+ readonly logLevel?: LogThreshold;
33
+ readonly uri: string;
34
+ /** Literal content text, or `@<path>` to read from a file. */
35
+ readonly content: string;
36
+ readonly clientId?: string;
37
+ readonly write?: (line: string) => void;
38
+ readonly __proxyForTest?: DataServerProtocol<TransferElement>;
39
+ /** Test-only: override the file-reader so unit tests can stub `@<path>` expansion. */
40
+ readonly __readFileForTest?: (path: string) => Promise<string>;
41
+ }
42
+
43
+ export async function runSave(options: SaveCommandOptions): Promise<void> {
44
+ const write = options.write ?? ((line: string) => process.stdout.write(line));
45
+ const clientId = options.clientId ?? 'hydranium-cli';
46
+ const model = await resolveContent(options.content, options.__readFileForTest);
47
+
48
+ if (options.__proxyForTest) {
49
+ const doc = await options.__proxyForTest.saveModelDocument({ uri: options.uri, clientId, model });
50
+ write(`${JSON.stringify(doc)}\n`);
51
+ return;
52
+ }
53
+
54
+ await withDataServer(
55
+ {
56
+ command: options.serverCommand,
57
+ args: options.serverArgs,
58
+ cwd: options.cwd,
59
+ env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
60
+ },
61
+ async server => {
62
+ const doc = await server.saveModelDocument({ uri: options.uri, clientId, model });
63
+ write(`${JSON.stringify(doc)}\n`);
64
+ }
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Resolve the `--content` argument to its textual form. The `@<path>`
70
+ * prefix reads from a file (parses as UTF-8); anything else is taken
71
+ * literally. The leading `@` can be escaped as `\@` for content that
72
+ * legitimately starts with `@`.
73
+ */
74
+ async function resolveContent(content: string, readFile?: (path: string) => Promise<string>): Promise<string> {
75
+ if (content.startsWith('@')) {
76
+ const path = content.slice(1);
77
+ const reader = readFile ?? ((target: string) => fs.readFile(target, 'utf8'));
78
+ return reader(path);
79
+ }
80
+ if (content.startsWith('\\@')) {
81
+ return content.slice(1);
82
+ }
83
+ return content;
84
+ }
@@ -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 {
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 { runValidate, type ValidateCommandOptions } from './validate.js';
23
+
24
+ const VALUE_FLAGS = [OUT_FILE_FLAG, LOG_LEVEL_FLAG] as const;
25
+ const BOOL_FLAGS = ['--strict', '--json'] as const;
26
+
27
+ /**
28
+ * Every flag `validate` accepts, derived from the sets the parser is handed so
29
+ * the list cannot claim a flag the parser would reject. `--services` is consumed
30
+ * by the shared harness parser itself, so it is in neither set.
31
+ */
32
+ export const VALIDATE_FLAGS: readonly string[] = [SERVICES_FLAG, ...VALUE_FLAGS, ...BOOL_FLAGS];
33
+
34
+ /** The subset that takes a value, so `--help` in a value position reads as data. */
35
+ export const VALIDATE_VALUE_FLAGS: readonly string[] = [SERVICES_FLAG, ...VALUE_FLAGS];
36
+
37
+ /** The `--help` text, as data, held to {@link VALIDATE_FLAGS} by a test. */
38
+ export const VALIDATE_HELP: readonly string[] = [
39
+ 'Usage: hydranium-cli validate --services <module> <workspace> [--strict] [--json] [--out-file <file>] [--log-level <lvl>]',
40
+ '',
41
+ 'Build a workspace headlessly and report its validation diagnostics — a CI gate:',
42
+ 'the process exits non-zero when any error is found. `<module>` is an ESM module',
43
+ 'exporting a zero-arg `createServices(): { shared }` thunk (the head wires its own',
44
+ 'filesystem inside). Needs @hydranium/core.',
45
+ '',
46
+ 'Options:',
47
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
48
+ ' <workspace> Workspace root (path or file URI) to validate (required).',
49
+ ' --strict Also fail (non-zero exit) on warnings, not only errors.',
50
+ ' --json Emit the raw JSON result instead of the human report.',
51
+ ' --out-file <file> Write the report to this file instead of stdout. The gate',
52
+ ' still decides the exit code, and the file is written only',
53
+ ' once the report exists, unlike a shell redirection, which',
54
+ ' truncates the file before the workspace is even built.',
55
+ logLevelHelpLine(22)
56
+ ];
57
+
58
+ export function parseValidateArgs(args: string[], onError: UsageError = exitWithUsage): ValidateCommandOptions {
59
+ const { servicesModule, workspace, options } = parseHarnessArgs(args, 'validate', VALUE_FLAGS, BOOL_FLAGS, { onError });
60
+ return {
61
+ servicesModule,
62
+ workspace,
63
+ strict: options['--strict'] === 'true',
64
+ json: options['--json'] === 'true',
65
+ outFile: options[OUT_FILE_FLAG],
66
+ logLevel: logLevelOption(options[LOG_LEVEL_FLAG])
67
+ };
68
+ }
69
+
70
+ export function runValidateCommand(args: string[]): Promise<void> {
71
+ if (helpRequested(args, VALIDATE_VALUE_FLAGS)) {
72
+ printHelp(VALIDATE_HELP);
73
+ return Promise.resolve();
74
+ }
75
+ return runValidate(parseValidateArgs(args));
76
+ }
@@ -0,0 +1,84 @@
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 `validate` subcommand. Launched by the parent with
12
+ * `node <this> --services <module> <workspace> [--json] [--strict]
13
+ * [--out-file <file>]`. It dynamic-imports the head's services module, runs the
14
+ * framework's `validateWorkspace`, delivers the report (human or `--json`) to
15
+ * stdout or the named file, and exits with the gate code (non-zero on errors;
16
+ * `--strict` also fails on warnings). Never imported — only spawned.
17
+ */
18
+
19
+ import { emitReport, loadHeadlessContext, OUT_FILE_FLAG } from './headless-harness.js';
20
+ import { formatValidationReport, validationExitCode } from './validate-report.js';
21
+
22
+ interface DriverArgs {
23
+ servicesModule?: string;
24
+ workspace?: string;
25
+ json: boolean;
26
+ strict: boolean;
27
+ outFile?: string;
28
+ }
29
+
30
+ function parseArgs(argv: string[]): DriverArgs {
31
+ const parsed: DriverArgs = { json: false, strict: false };
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const flag = argv[index];
34
+ const next = (): string => {
35
+ const value = argv[index + 1];
36
+ if (value === undefined) {
37
+ console.error(`Missing value for ${flag}`);
38
+ process.exit(1);
39
+ }
40
+ index += 1;
41
+ return value;
42
+ };
43
+ switch (flag) {
44
+ case '--services':
45
+ parsed.servicesModule = next();
46
+ break;
47
+ case '--json':
48
+ parsed.json = true;
49
+ break;
50
+ case '--strict':
51
+ parsed.strict = true;
52
+ break;
53
+ case OUT_FILE_FLAG:
54
+ parsed.outFile = next();
55
+ break;
56
+ default:
57
+ if (flag.startsWith('--')) {
58
+ console.error(`Unknown option: ${flag}`);
59
+ process.exit(1);
60
+ }
61
+ parsed.workspace = flag;
62
+ }
63
+ }
64
+ return parsed;
65
+ }
66
+
67
+ async function main(argv: string[]): Promise<number> {
68
+ const args = parseArgs(argv);
69
+ if (!args.servicesModule || !args.workspace) {
70
+ console.error('validate-driver: --services <module> and a <workspace> are required.');
71
+ process.exit(1);
72
+ }
73
+ const { createServices, coreNode } = await loadHeadlessContext(args.servicesModule);
74
+ const result = await coreNode.validateWorkspace({ createServices, workspace: args.workspace });
75
+ emitReport(formatValidationReport(result, { json: args.json }), args.outFile);
76
+ return validationExitCode(result, args.strict);
77
+ }
78
+
79
+ main(process.argv.slice(2))
80
+ .then(code => process.exit(code))
81
+ .catch((error: unknown) => {
82
+ console.error(error instanceof Error ? error.message : String(error));
83
+ process.exit(1);
84
+ });
@@ -0,0 +1,95 @@
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 { WorkspaceValidationResult } 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
+ /** `<n> file` / `<n> files`. */
20
+ function fileCount(documents: number): string {
21
+ return `${documents} file${plural(documents)}`;
22
+ }
23
+
24
+ /**
25
+ * The exit code for a validation run: non-zero blocks the CI gate.
26
+ * Errors always fail; `strict` additionally fails on warnings. Info/hint
27
+ * findings are advisory and never affect the exit code.
28
+ */
29
+ export function validationExitCode(result: WorkspaceValidationResult, strict = false): number {
30
+ if (result.counts.error > 0) {
31
+ return 1;
32
+ }
33
+ if (strict && result.counts.warning > 0) {
34
+ return 1;
35
+ }
36
+ return 0;
37
+ }
38
+
39
+ /** One-line severity tally over the file count, or the clean-workspace message. */
40
+ function summaryLine(result: WorkspaceValidationResult): string {
41
+ const { error, warning, info, hint } = result.counts;
42
+ if (error + warning + info + hint === 0) {
43
+ return `No problems found in ${fileCount(result.documents)}.`;
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
+ if (info) {
53
+ parts.push(`${info} info`);
54
+ }
55
+ if (hint) {
56
+ parts.push(`${hint} hint${plural(hint)}`);
57
+ }
58
+ return `${parts.join(', ')} in ${fileCount(result.documents)}.`;
59
+ }
60
+
61
+ /**
62
+ * Render a validation result for the terminal. `json` emits the raw
63
+ * {@link WorkspaceValidationResult} (the machine-readable CI contract); the
64
+ * default is a human report that groups findings by document — one header per
65
+ * file followed by `line:col severity message (code)` lines — and closes
66
+ * with the {@link summaryLine} tally.
67
+ *
68
+ * Findings arrive already grouped contiguously by document (the framework
69
+ * collector iterates per document), so a header is emitted whenever the URI
70
+ * changes rather than pre-bucketing.
71
+ */
72
+ export function formatValidationReport(result: WorkspaceValidationResult, options: { json?: boolean } = {}): string {
73
+ if (options.json) {
74
+ return JSON.stringify(result, undefined, 2);
75
+ }
76
+ const lines: string[] = [];
77
+ let currentUri: string | undefined;
78
+ for (const finding of result.findings) {
79
+ if (finding.uri !== currentUri) {
80
+ if (currentUri !== undefined) {
81
+ lines.push('');
82
+ }
83
+ lines.push(finding.uri);
84
+ currentUri = finding.uri;
85
+ }
86
+ const location = `${finding.line + 1}:${finding.character + 1}`;
87
+ const code = finding.code !== undefined ? ` (${finding.code})` : '';
88
+ lines.push(` ${location.padEnd(7)} ${finding.severity.padEnd(7)} ${finding.message}${code}`);
89
+ }
90
+ if (result.findings.length > 0) {
91
+ lines.push('');
92
+ }
93
+ lines.push(summaryLine(result));
94
+ return lines.join('\n');
95
+ }
@@ -0,0 +1,58 @@
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 { OUT_FILE_FLAG, runDriverChild, type DriverSpawnOptions } from './headless-harness.js';
12
+
13
+ /**
14
+ * Absolute path to the spawned driver. From `lib/commands/validate.js` the
15
+ * compiled driver sits beside it at `lib/commands/validate-driver.js`.
16
+ */
17
+ const DRIVER = fileURLToPath(new URL('./validate-driver.js', import.meta.url));
18
+
19
+ /** Options for the {@link runValidate} subcommand. */
20
+ export interface ValidateCommandOptions extends DriverSpawnOptions {
21
+ /** ESM module exporting a zero-arg `createServices(): { shared }` thunk. */
22
+ readonly servicesModule: string;
23
+ /** Workspace root (filesystem path or file URI) to build and validate. */
24
+ readonly workspace: 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
+ /** Write the report to this file instead of stdout; the gate still sets the exit code. */
30
+ readonly outFile?: string;
31
+ }
32
+
33
+ /** Build the argv the driver child parses. Pure → unit-testable. */
34
+ export function buildValidateDriverArgs(options: ValidateCommandOptions): string[] {
35
+ const args = ['--services', options.servicesModule, options.workspace];
36
+ if (options.json) {
37
+ args.push('--json');
38
+ }
39
+ if (options.strict) {
40
+ args.push('--strict');
41
+ }
42
+ if (options.outFile !== undefined) {
43
+ args.push(OUT_FILE_FLAG, options.outFile);
44
+ }
45
+ return args;
46
+ }
47
+
48
+ /**
49
+ * Headless workspace validation — a CI gate that builds a workspace, runs the
50
+ * language's validation checks, prints the diagnostics, and exits non-zero when
51
+ * any error is found (`--strict` also fails on warnings). Like the other headless
52
+ * harnesses the binary is language-agnostic, so the head passes a `createServices`
53
+ * module (`--services`); the work runs in a spawned child that dynamic-imports it.
54
+ * The child's exit code propagates to this process so shells and CI see the gate.
55
+ */
56
+ export function runValidate(options: ValidateCommandOptions): Promise<void> {
57
+ return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildValidateDriverArgs(options)], options);
58
+ }