@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,554 @@
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 * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import { buildInitTemplates, readFrameworkVersion, UNPUBLISHED_FRAMEWORK_VERSION } from './init-templates.js';
13
+ import { createNodeWorkspaceProbe, detectWorkspace, type JsonValue, type WorkspaceProbe } from './init-workspace.js';
14
+
15
+ /**
16
+ * The protocol heads a scaffold can start, in composition order.
17
+ *
18
+ * `lsp` is mandatory rather than optional: it owns the workspace, the build
19
+ * pipeline and the shared service tier every other head reads through, so a
20
+ * project without it has nothing for `data` or `glsp` to serve.
21
+ */
22
+ export const INIT_HEADS = ['lsp', 'data', 'glsp'] as const;
23
+
24
+ /** One protocol head — see {@link INIT_HEADS}. */
25
+ export type InitHead = (typeof INIT_HEADS)[number];
26
+
27
+ /** The heads a scaffold emits when `--heads` is not given. */
28
+ export const DEFAULT_INIT_HEADS: readonly InitHead[] = ['lsp', 'data'];
29
+
30
+ /** True for a string naming one of {@link INIT_HEADS}. */
31
+ export function isInitHead(value: string): value is InitHead {
32
+ return (INIT_HEADS as readonly string[]).includes(value);
33
+ }
34
+
35
+ /**
36
+ * One grammar as the command line collected it, before any derivation.
37
+ *
38
+ * Everything but {@link name} is optional: the derivation rules produce a usable
39
+ * routing key and extension from the project and grammar names alone. An
40
+ * override is for the case derivation cannot reach — a file extension that is
41
+ * not the kebab grammar name.
42
+ */
43
+ export interface InitGrammarSpec {
44
+ /** PascalCase grammar name — the `grammar X` declaration. */
45
+ readonly name: string;
46
+ /**
47
+ * File extensions, leading dot optional. Defaults to the kebab grammar id.
48
+ *
49
+ * A list rather than a single value because `langium-config.json`'s
50
+ * `fileExtensions` is a list: one language may answer for several suffixes.
51
+ */
52
+ readonly extensions?: readonly string[];
53
+ /** Override the derived routing key. */
54
+ readonly languageId?: string;
55
+ /**
56
+ * Scaffold a GLSP diagram for this grammar. Requires the `glsp` head.
57
+ *
58
+ * Per grammar rather than per project because
59
+ * `AbstractHydraniumGlspDiagramModule.declareLanguage()` returns ONE grammar's
60
+ * `LanguageMetaData` — a diagram type has exactly one grammar, and a grammar
61
+ * with no diagram sitting on the same server is the normal case.
62
+ */
63
+ readonly diagram?: boolean;
64
+ }
65
+
66
+ /**
67
+ * The resolved names of ONE grammar — the per-language tier.
68
+ *
69
+ * Everything here gains a sibling per additional grammar, which is what
70
+ * separates it from {@link InitComposition}'s project-level fields. The
71
+ * generated-symbol split is the reason the tiers cannot be collapsed:
72
+ * `langium-cli` emits `<grammar>GeneratedModule` and `<grammar>LanguageMetaData`
73
+ * per grammar, but `<projectName>AstReflection` and
74
+ * `<projectName>GeneratedSharedModule` once per project.
75
+ */
76
+ export interface InitGrammarNames {
77
+ /** PascalCase grammar name — the `grammar X` declaration. */
78
+ readonly grammar: string;
79
+ /** Kebab grammar id — names the `.langium` file. */
80
+ readonly grammarId: string;
81
+ /**
82
+ * Kebab routing key — the `langium-config.json` entry id.
83
+ *
84
+ * Qualified as `<projectId>-<grammarId>` whenever the grammar is separately
85
+ * named or the project holds more than one, because a second grammar needs a
86
+ * second id and retro-fitting the suffix later renames a routing key every
87
+ * host has already bound.
88
+ */
89
+ readonly languageId: string;
90
+ /** File extensions without leading dots. At least one. */
91
+ readonly extensions: readonly string[];
92
+ /**
93
+ * The grammar's entry rule, always `<Grammar>Model`.
94
+ *
95
+ * **Derived rather than fixed, because one `langium-cli` run over N grammars
96
+ * emits ONE combined `ast.ts` sharing one reflection.** Two grammars both
97
+ * declaring `entry Model:` would put the same interface name in that module
98
+ * twice.
99
+ */
100
+ readonly entryRule: string;
101
+ /**
102
+ * The starter node rule, always `<Grammar>Node`.
103
+ *
104
+ * **`Node` and not `Element`:** the transfer-model generator emits
105
+ * `export interface <projectName>Element` as the base type every rule
106
+ * extends, so a rule named `<Grammar>Element` would collide with it in the
107
+ * single-grammar case — the same interface declared twice, the second
108
+ * extending itself.
109
+ */
110
+ readonly nodeRule: string;
111
+ /** Whether a GLSP diagram is scaffolded for this grammar. */
112
+ readonly diagram: boolean;
113
+ }
114
+
115
+ /**
116
+ * Where a scaffold sits inside an existing npm workspace.
117
+ *
118
+ * Detected rather than asked, because both members are things a wrong answer
119
+ * makes actively broken rather than merely unidiomatic: a `--prefix` that names
120
+ * the wrong directory runs the wrong package's scripts, and an `extends` that
121
+ * points at a *solution* tsconfig (`files: []` plus `references`, which is what
122
+ * a monorepo root usually has) inherits no compiler options at all.
123
+ */
124
+ export interface InitWorkspacePlacement {
125
+ /** POSIX path of the target relative to the workspace root, e.g. `packages/foo`. */
126
+ readonly targetPath: string;
127
+ /** Relative specifier of the tsconfig carrying `compilerOptions`, e.g. `../../tsconfig.base.json`. */
128
+ readonly baseTsconfig?: string;
129
+ /** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
130
+ readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
131
+ /** The workspace's prettier `printWidth`, so emitted sources wrap where the repo wraps. */
132
+ readonly printWidth?: number;
133
+ /** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
134
+ readonly eslintConfig?: string;
135
+ }
136
+
137
+ /**
138
+ * How the emitted `package.json` presents itself.
139
+ *
140
+ * Separate from {@link InitWorkspacePlacement} because the two answer to
141
+ * different authorities: the placement is *detected* from the surrounding tree,
142
+ * while the scope and the publishability are **intent** and are never inferred —
143
+ * a repo's root manifest is routinely named for the repo rather than for the
144
+ * scope it publishes under, so guessing would confidently produce the wrong
145
+ * package name. The wizard offers a sibling-derived default; the flags decide.
146
+ *
147
+ * This is the one place the two polarities meet: every option surface upstream
148
+ * carries `public`, because a flag names a DEVIATION from the default, while
149
+ * this type carries `private`, because that is the manifest key the template
150
+ * substitutes.
151
+ */
152
+ export interface InitPackaging {
153
+ /** npm scope for the package name, e.g. `@acme`. Absent leaves it unscoped. */
154
+ readonly scope?: string;
155
+ /** Emit `"private": true` — a package npm refuses to publish. */
156
+ readonly private: boolean;
157
+ /** The workspace the package joins. Absent for a standalone project. */
158
+ readonly workspace?: InitWorkspacePlacement;
159
+ }
160
+
161
+ /**
162
+ * A standalone, unscoped, unpublishable package — what `init` emits without the
163
+ * packaging flags.
164
+ *
165
+ * `private` defaults to TRUE because the manifest also declares `license:
166
+ * "UNLICENSED"`, and a package that grants no rights while being publishable to
167
+ * a public registry asserts two contradictory things about itself. npm pairs
168
+ * those two keys for exactly this reason. The scaffold cannot pick a licence for
169
+ * a stranger's project, so it withholds publication instead of granting rights
170
+ * nobody chose; `--public` is the opt-out for a project that has picked one.
171
+ */
172
+ export const STANDALONE_PACKAGING: InitPackaging = { private: true };
173
+
174
+ /**
175
+ * The full scaffold description: one project tier, N grammar tiers. Templates
176
+ * read this and are rendered per tier.
177
+ */
178
+ export interface InitComposition {
179
+ /** PascalCase project name — the Langium `projectName`. */
180
+ readonly name: string;
181
+ /**
182
+ * Kebab project id, always derived from {@link name} and never from a
183
+ * language id. Names what a second grammar must not rename: the package and
184
+ * `bin` key, the DI module file, and the data-server port command (there is
185
+ * one data server per project, not one per language).
186
+ */
187
+ readonly projectId: string;
188
+ /** The protocol heads to start, always including `lsp`. */
189
+ readonly heads: readonly InitHead[];
190
+ /** The grammars, in the order given. The first is the configuration root. */
191
+ readonly grammars: readonly InitGrammarNames[];
192
+ /** How the emitted `package.json` presents itself, and where it sits. */
193
+ readonly packaging: InitPackaging;
194
+ /**
195
+ * The framework version every emitted `@hydranium/*` pin is derived from —
196
+ * the scaffolding CLI's own, since the scope is versioned as one line.
197
+ *
198
+ * Carried on the composition rather than read where the templates render, so
199
+ * {@link planInitFiles} stays pure over its input. That is what lets a golden
200
+ * pin the emitted shape at a fixed token without pinning the number, which
201
+ * would otherwise turn every release into a red suite.
202
+ */
203
+ readonly frameworkVersion: string;
204
+ }
205
+
206
+ /** One file the scaffold writes: relative path + fully-substituted content. */
207
+ export interface InitFile {
208
+ path: string;
209
+ content: string;
210
+ }
211
+
212
+ /** Options for the {@link runInit} subcommand. */
213
+ export interface InitCommandOptions {
214
+ /** Target directory to scaffold into. */
215
+ readonly targetDir: string;
216
+ /** PascalCase project name (drives the services + shared generated module names). */
217
+ readonly name: string;
218
+ /**
219
+ * The grammars to scaffold, in order. Defaults to a single grammar named
220
+ * after the project, which is the single-language starter shape.
221
+ */
222
+ readonly grammars?: readonly InitGrammarSpec[];
223
+ /** The protocol heads to start. Defaults to {@link DEFAULT_INIT_HEADS}. */
224
+ readonly heads?: readonly InitHead[];
225
+ /** Scaffold into a non-empty directory anyway. */
226
+ readonly force?: boolean;
227
+ /**
228
+ * Scaffold a member of the surrounding npm workspace: extend its base
229
+ * tsconfig, leave `.gitignore` to the root, and address the package by
230
+ * `--prefix` in the scripts it prints back at itself. Errors when no ancestor
231
+ * declares `workspaces`, rather than silently degrading to a standalone
232
+ * project the caller did not ask for.
233
+ */
234
+ readonly monorepo?: boolean;
235
+ /** npm scope for the package name, e.g. `@acme`. */
236
+ readonly scope?: string;
237
+ /** Omit `"private": true`, leaving the package publishable. */
238
+ readonly public?: boolean;
239
+ /** Output sink. Default: `process.stdout.write`. Tests inject a capturing stub. */
240
+ readonly write?: (line: string) => void;
241
+ /** Filesystem port for workspace detection. Default: the real filesystem. */
242
+ readonly probe?: WorkspaceProbe;
243
+ /** Test-only: capture the planned files instead of writing to disk. */
244
+ readonly __writeFilesForTest?: (targetDir: string, files: readonly InitFile[]) => void;
245
+ }
246
+
247
+ /** Kebab-case a PascalCase/camelCase name. */
248
+ export function kebab(name: string): string {
249
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
250
+ }
251
+
252
+ /** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
253
+ function isPascalIdentifier(value: string): boolean {
254
+ return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
255
+ }
256
+
257
+ /** Strip a leading dot and reject an extension that is empty or dotted through. */
258
+ function normalizeExtension(value: string, grammar: string): string {
259
+ const extension = value.replace(/^\./, '');
260
+ if (extension.length === 0) {
261
+ throw new Error(`Invalid extension '${value}' for grammar '${grammar}': an extension cannot be empty.`);
262
+ }
263
+ return extension;
264
+ }
265
+
266
+ /**
267
+ * Resolve one grammar's names.
268
+ *
269
+ * `qualify` comes from the project rather than the grammar: a lone grammar
270
+ * whose name matches the project keeps the bare project id, but as soon as
271
+ * there are several every id is qualified, so the set reads consistently
272
+ * instead of one member being special.
273
+ */
274
+ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boolean, diagram: boolean): InitGrammarNames {
275
+ if (!isPascalIdentifier(spec.name)) {
276
+ throw new Error(
277
+ `Invalid --grammar '${spec.name}': must be a PascalCase identifier (letters and digits, starting with a letter), e.g. 'Domain'.`
278
+ );
279
+ }
280
+ const grammarId = kebab(spec.name);
281
+ const extensions = (spec.extensions ?? [grammarId]).map(extension => normalizeExtension(extension, spec.name));
282
+ if (extensions.length === 0) {
283
+ throw new Error(`Invalid --extensions for grammar '${spec.name}': at least one extension is required.`);
284
+ }
285
+ return {
286
+ grammar: spec.name,
287
+ grammarId,
288
+ languageId: spec.languageId ?? (qualify || grammarId !== projectId ? `${projectId}-${grammarId}` : projectId),
289
+ extensions,
290
+ entryRule: `${spec.name}Model`,
291
+ nodeRule: `${spec.name}Node`,
292
+ diagram
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Resolve which grammars get a diagram.
298
+ *
299
+ * With the `glsp` head on and nothing marked, a lone grammar is unambiguous and
300
+ * is taken as the diagram's; with several it is a real choice, so it is required
301
+ * rather than guessed — binding a diagram to the wrong grammar surfaces much
302
+ * later as references resolving against the wrong scope.
303
+ */
304
+ function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly InitHead[]): boolean[] {
305
+ const marked = specs.map(spec => spec.diagram === true);
306
+ if (!heads.includes('glsp')) {
307
+ const named = specs.filter(spec => spec.diagram === true).map(spec => spec.name);
308
+ if (named.length > 0) {
309
+ throw new Error(
310
+ `--diagram was given for ${named.join(', ')} but the head set is '${heads.join(',')}'. ` +
311
+ "Add 'glsp' to --heads, since the emitted dependencies follow the head set."
312
+ );
313
+ }
314
+ return marked;
315
+ }
316
+ if (marked.some(Boolean)) {
317
+ return marked;
318
+ }
319
+ if (specs.length === 1) {
320
+ return [true];
321
+ }
322
+ throw new Error(
323
+ `--heads includes 'glsp' but no grammar carries --diagram. Mark the one the diagram edits: ` +
324
+ `${specs.map(spec => spec.name).join(', ')}.`
325
+ );
326
+ }
327
+
328
+ /** Validate the head set: known names only, and `lsp` present. */
329
+ function resolveHeads(heads: readonly InitHead[] | undefined): readonly InitHead[] {
330
+ const resolved = heads === undefined || heads.length === 0 ? DEFAULT_INIT_HEADS : heads;
331
+ const unknown = resolved.filter(head => !isInitHead(head));
332
+ if (unknown.length > 0) {
333
+ throw new Error(`Unknown head(s) '${unknown.join(', ')}': expected one of ${INIT_HEADS.join(', ')}.`);
334
+ }
335
+ if (!resolved.includes('lsp')) {
336
+ throw new Error(
337
+ "--heads must include 'lsp': it owns the workspace, the build pipeline and the shared service tier the other heads read through."
338
+ );
339
+ }
340
+ // De-duplicated and put back in composition order, so the order the heads
341
+ // were named in does not change the emitted project.
342
+ return INIT_HEADS.filter(head => resolved.includes(head));
343
+ }
344
+
345
+ /**
346
+ * Reject the collisions that would otherwise surface as a confusing generator
347
+ * or runtime failure rather than as a bad invocation.
348
+ *
349
+ * The extension check is the one worth having: Langium routes documents to a
350
+ * language BY extension, so two grammars claiming `.foo` is not a name clash
351
+ * but a silent mis-route of every `.foo` file to whichever language registered
352
+ * first.
353
+ */
354
+ function assertNoCollisions(grammars: readonly InitGrammarNames[]): void {
355
+ const byGrammarId = new Map<string, string>();
356
+ const byLanguageId = new Map<string, string>();
357
+ const byExtension = new Map<string, string>();
358
+ for (const grammar of grammars) {
359
+ const clashingName = byGrammarId.get(grammar.grammarId);
360
+ if (clashingName !== undefined) {
361
+ throw new Error(`Duplicate grammar '${grammar.grammar}': it resolves to the same file as '${clashingName}'.`);
362
+ }
363
+ byGrammarId.set(grammar.grammarId, grammar.grammar);
364
+
365
+ const clashingLanguage = byLanguageId.get(grammar.languageId);
366
+ if (clashingLanguage !== undefined) {
367
+ throw new Error(`Duplicate language id '${grammar.languageId}': claimed by both '${clashingLanguage}' and '${grammar.grammar}'.`);
368
+ }
369
+ byLanguageId.set(grammar.languageId, grammar.grammar);
370
+
371
+ for (const extension of grammar.extensions) {
372
+ const clashingExtension = byExtension.get(extension);
373
+ if (clashingExtension !== undefined) {
374
+ throw new Error(
375
+ `File extension '.${extension}' is claimed by both '${clashingExtension}' and '${grammar.grammar}'. ` +
376
+ 'Langium routes documents by extension, so each grammar needs its own.'
377
+ );
378
+ }
379
+ byExtension.set(extension, grammar.grammar);
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Derive the whole scaffold description from `--name` plus the collected
386
+ * grammars. Throws on a name that is not a valid PascalCase identifier — both
387
+ * project and grammar names become prefixes of generated TypeScript symbols.
388
+ *
389
+ * With no grammars given, one is synthesised from the project name, which is
390
+ * the single-language starter shape.
391
+ */
392
+ export function resolveInitComposition(
393
+ name: string,
394
+ grammars: readonly InitGrammarSpec[] = [],
395
+ heads?: readonly InitHead[],
396
+ packaging: InitPackaging = STANDALONE_PACKAGING
397
+ ): InitComposition {
398
+ if (!isPascalIdentifier(name)) {
399
+ throw new Error(
400
+ `Invalid --name '${name}': must be a PascalCase identifier (letters and digits, starting with a letter), e.g. 'Bookstore'.`
401
+ );
402
+ }
403
+ const projectId = kebab(name);
404
+ if (packaging.scope !== undefined && !/^@[a-z0-9][a-z0-9._-]*$/.test(packaging.scope)) {
405
+ throw new Error(`Invalid --scope '${packaging.scope}': an npm scope starts with '@', e.g. '@acme'.`);
406
+ }
407
+ const resolvedHeads = resolveHeads(heads);
408
+ const specs = grammars.length > 0 ? grammars : [{ name }];
409
+ const diagrams = resolveDiagrams(specs, resolvedHeads);
410
+ const resolved = specs.map((spec, index) => resolveGrammar(spec, projectId, specs.length > 1, diagrams[index]));
411
+ assertNoCollisions(resolved);
412
+ return { name, projectId, heads: resolvedHeads, grammars: resolved, packaging, frameworkVersion: readFrameworkVersion() };
413
+ }
414
+
415
+ /**
416
+ * Plan the files a scaffold emits — every template rendered for this
417
+ * composition. Pure over its input → unit-testable without touching disk;
418
+ * {@link runInit} is the thin IO wrapper.
419
+ */
420
+ export function planInitFiles(composition: InitComposition): InitFile[] {
421
+ return buildInitTemplates(composition);
422
+ }
423
+
424
+ /** True when `dir` exists and contains at least one entry. */
425
+ export function isNonEmptyDir(dir: string): boolean {
426
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length > 0;
427
+ }
428
+
429
+ /** The packaging flags, plus the workspace they were given in. */
430
+ export interface InitPackagingOptions {
431
+ readonly monorepo?: boolean;
432
+ readonly scope?: string;
433
+ readonly public?: boolean;
434
+ readonly probe?: WorkspaceProbe;
435
+ }
436
+
437
+ /**
438
+ * Resolve the packaging tier for a target directory, running workspace
439
+ * detection when `--monorepo` asked for it.
440
+ *
441
+ * Exported because the `init`-provenance gate re-runs the reference example's
442
+ * recorded invocation and must reach the same composition `runInit` would: a
443
+ * second copy of these rules there would drift from these, and the gate exists
444
+ * precisely to catch drift.
445
+ */
446
+ export function resolveInitPackaging(targetDir: string, options: InitPackagingOptions): InitPackaging {
447
+ if (options.monorepo !== true) {
448
+ return { scope: options.scope, private: options.public !== true };
449
+ }
450
+ const detection = detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe());
451
+ if (detection === undefined) {
452
+ throw new Error(
453
+ `--monorepo was given but no ancestor of '${path.resolve(targetDir)}' has a package.json declaring 'workspaces'. ` +
454
+ 'Scaffold without it for a standalone project.'
455
+ );
456
+ }
457
+ return {
458
+ scope: options.scope,
459
+ private: options.public !== true,
460
+ // The scope and the publishability deliberately do NOT fall back to what
461
+ // detection found: the emitted package name stays a function of the argv
462
+ // alone, which is what lets the wizard's echoed command reproduce the
463
+ // scaffold anywhere. Detection only supplies the wizard's DEFAULT answer.
464
+ workspace: {
465
+ targetPath: detection.targetPath,
466
+ baseTsconfig: detection.baseTsconfig,
467
+ baseCompilerOptions: detection.baseCompilerOptions,
468
+ printWidth: detection.printWidth,
469
+ eslintConfig: detection.eslintConfig
470
+ }
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Scaffold a new Hydranium language project end-to-end: one starter grammar per
476
+ * `--grammar`, `create<Name>Services` DI wiring, a launcher for each head in
477
+ * `--heads`, `langium-config.json`, `package.json`, and scripts. Does NOT run `npm install`
478
+ * or `langium generate` — those are printed as next steps, so the command stays
479
+ * offline and side-effect-free beyond the files it writes. Refuses a non-empty
480
+ * target directory unless `force`.
481
+ */
482
+ export function runInit(options: InitCommandOptions): void {
483
+ const write = options.write ?? ((line: string) => process.stdout.write(line));
484
+ const targetDir = path.resolve(options.targetDir);
485
+ const detection = options.monorepo ? detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe()) : undefined;
486
+ const composition = resolveInitComposition(options.name, options.grammars, options.heads, resolveInitPackaging(targetDir, options));
487
+
488
+ if (!options.force && isNonEmptyDir(targetDir)) {
489
+ throw new Error(`Target directory '${targetDir}' is not empty. Pass --force to scaffold into it anyway.`);
490
+ }
491
+
492
+ const files = planInitFiles(composition);
493
+ if (options.__writeFilesForTest) {
494
+ options.__writeFilesForTest(targetDir, files);
495
+ } else {
496
+ for (const file of files) {
497
+ const absolute = path.join(targetDir, file.path);
498
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
499
+ fs.writeFileSync(absolute, file.content, 'utf-8');
500
+ }
501
+ }
502
+
503
+ const grammarList = composition.grammars.map(grammar => (grammar.diagram ? `${grammar.grammar} (diagram)` : grammar.grammar)).join(', ');
504
+ write(
505
+ `Scaffolded ${composition.name} (${files.length} files, heads: ${composition.heads.join(',')}, ` +
506
+ `${composition.grammars.length} grammar(s): ${grammarList}) into ${targetDir}\n`
507
+ );
508
+ if (detection !== undefined) {
509
+ write('\n');
510
+ write(`Workspace root: ${detection.rootDir}\n`);
511
+ if (detection.baseTsconfig !== undefined) {
512
+ write(` tsconfig.json extends ${detection.baseTsconfig}, minus the options it inherits unchanged.\n`);
513
+ } else {
514
+ write(' No root tsconfig carries compilerOptions, so the emitted one stands alone.\n');
515
+ }
516
+ write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
517
+ if (detection.printWidth !== undefined) {
518
+ write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s prettier config.\n`);
519
+ }
520
+ if (detection.eslintConfig !== undefined) {
521
+ write(` A lint script was added — ${detection.eslintConfig} says this repo lints. Adjust it if your invocation differs.\n`);
522
+ }
523
+ // Printed rather than applied: `init` writes inside the target directory
524
+ // and nowhere else, so a scaffold can never surprise-edit a manifest that
525
+ // is under review, mid-rebase, or simply not the caller's to change.
526
+ if (detection.coveredBy === undefined) {
527
+ write(` Add this to "workspaces" in ${path.join(detection.rootDir, 'package.json')}:\n`);
528
+ write(` "${detection.targetPath}"\n`);
529
+ } else {
530
+ write(` Already covered by the "${detection.coveredBy}" workspaces entry — no root manifest change needed.\n`);
531
+ }
532
+ if (composition.packaging.scope === undefined && detection.scope !== undefined) {
533
+ write(` Sibling packages all use the "${detection.scope}" scope; pass --scope ${detection.scope} to match them.\n`);
534
+ }
535
+ }
536
+ write('\n');
537
+ write('Next steps:\n');
538
+ write(` cd ${options.targetDir}\n`);
539
+ // The warning is conditional on the pins this scaffold actually carries — see
540
+ // `UNPUBLISHED_FRAMEWORK_VERSION`.
541
+ write(
542
+ composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
543
+ ? ' npm install # 404s until @hydranium/* is published — see README\n'
544
+ : ' npm install\n'
545
+ );
546
+ write(' npm run build # langium generate + tsc\n');
547
+ write(' npm test # the scaffolded DI-composition test\n');
548
+ // `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
549
+ // scaffold, so the binary is on PATH inside an npm script and nowhere else.
550
+ write(' npx hydranium-cli reflect --services ./lib/services.js\n');
551
+ write('\n');
552
+ write('The emitted sources carry no license header — run your own header tool if\n');
553
+ write('your repo gates them. See the README section on that.\n');
554
+ }
@@ -0,0 +1,79 @@
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 { runLintGrammar, type LintGrammarCommandOptions } from './lint-grammar.js';
22
+
23
+ const VALUE_FLAGS = [LOG_LEVEL_FLAG] as const;
24
+ const BOOL_FLAGS = ['--strict', '--json'] as const;
25
+ const REPEATABLE_VALUE_FLAGS = ['--name-property'] as const;
26
+
27
+ /**
28
+ * Every flag `lint-grammar` accepts, derived from the sets the parser is handed
29
+ * so the list cannot claim a flag the parser would reject. `--services` is
30
+ * consumed by the shared harness parser itself, so it is in neither set.
31
+ */
32
+ export const LINT_GRAMMAR_FLAGS: readonly string[] = [SERVICES_FLAG, ...REPEATABLE_VALUE_FLAGS, ...VALUE_FLAGS, ...BOOL_FLAGS];
33
+
34
+ /** The subset that takes a value, so `--help` in a value position reads as data. */
35
+ export const LINT_GRAMMAR_VALUE_FLAGS: readonly string[] = [SERVICES_FLAG, ...REPEATABLE_VALUE_FLAGS, ...VALUE_FLAGS];
36
+
37
+ /** The `--help` text, as data, held to {@link LINT_GRAMMAR_FLAGS} by a test. */
38
+ export const LINT_GRAMMAR_HELP: readonly string[] = [
39
+ 'Usage: hydranium-cli lint-grammar --services <module> [--name-property <p>]... [--strict] [--json] [--log-level <lvl>]',
40
+ '',
41
+ "Check a head's grammar against the framework's conventions — a CI gate: the",
42
+ 'process exits non-zero when a violation is found. Read-only: no workspace is',
43
+ 'built (the grammar is static once the head registers its language), so',
44
+ '`lint-grammar` takes no `<workspace>`. Checks that every concrete cross-reference',
45
+ 'target carries a name property (else references cannot resolve) and that each',
46
+ 'language declares an entry rule. `<module>` is an ESM module exporting a',
47
+ 'zero-arg `createServices(): { shared }` thunk. Needs @hydranium/core.',
48
+ '',
49
+ 'Options:',
50
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
51
+ ' --name-property <p> Property that satisfies the nameability convention',
52
+ ' (repeatable). Default: name.',
53
+ ' --strict Also fail (non-zero exit) on warnings, not only errors.',
54
+ ' --json Emit the raw JSON result instead of the human report.',
55
+ logLevelHelpLine(23)
56
+ ];
57
+
58
+ export function parseLintGrammarArgs(args: string[], onError: UsageError = exitWithUsage): LintGrammarCommandOptions {
59
+ const { servicesModule, options, values } = parseHarnessArgs(args, 'lint-grammar', VALUE_FLAGS, BOOL_FLAGS, {
60
+ requireWorkspace: false,
61
+ repeatableValueFlags: REPEATABLE_VALUE_FLAGS,
62
+ onError
63
+ });
64
+ return {
65
+ servicesModule,
66
+ nameProperties: values['--name-property'],
67
+ strict: options['--strict'] === 'true',
68
+ json: options['--json'] === 'true',
69
+ logLevel: logLevelOption(options[LOG_LEVEL_FLAG])
70
+ };
71
+ }
72
+
73
+ export function runLintGrammarCommand(args: string[]): Promise<void> {
74
+ if (helpRequested(args, LINT_GRAMMAR_VALUE_FLAGS)) {
75
+ printHelp(LINT_GRAMMAR_HELP);
76
+ return Promise.resolve();
77
+ }
78
+ return runLintGrammar(parseLintGrammarArgs(args));
79
+ }