@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,378 @@
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
+ * The interactive front end of `hydranium-cli init`.
12
+ *
13
+ * **The wizard's product is a command, not files.** It gathers answers, composes
14
+ * an argv, echoes it, and hands it to the same flag parser a non-interactive
15
+ * caller uses. Three properties fall out of that, and they are the reason to
16
+ * prefer it over an interactive mode reaching into the scaffolder:
17
+ *
18
+ * - every answer is flag-expressible by construction, so no wizard-only
19
+ * capability can drift in;
20
+ * - the echoed command is copy-pasteable into a README or CI, which is what
21
+ * keeps a recorded invocation re-runnable;
22
+ * - the wizard reduces to a pure `answers → argv` function
23
+ * ({@link composeInitArgv}), far cheaper to test than a flow that also writes
24
+ * files.
25
+ *
26
+ * What it asks is deliberately short. Most `init` inputs are *derived* and stay
27
+ * derived; the ones here are the ones no rule can reach:
28
+ *
29
+ * - the **file extension**, which is the defect the wizard exists to fix. A
30
+ * project name is silently also an extension choice, because the grammar
31
+ * defaults to the project name and the extension follows the grammar. No
32
+ * derivation can do better — with no second name supplied there is nothing to
33
+ * derive from — so it is asked, with the derived value merely offered as the
34
+ * default.
35
+ * - **workspace membership** and scope, which are facts about the surrounding
36
+ * repo rather than about the language, and so are asked only where a workspace
37
+ * was detected.
38
+ * - **publishability**, which is asked on every path because it is a licence
39
+ * posture rather than a packaging detail: the emission withholds publication
40
+ * AND declares itself UNLICENSED, so a default nobody was shown would settle
41
+ * the licence question by silence.
42
+ *
43
+ * The **language id is deliberately not asked**, though it is the third of the
44
+ * three independent names: its derivation was settled and built separately
45
+ * (`<project>`, qualified to `<project>-<grammar>` when the grammar is
46
+ * separately named), and offering it as a question invites overriding a rule
47
+ * that is right, in the one place a wrong answer renames a routing key every
48
+ * host has already bound.
49
+ *
50
+ * Grammars are collected **one at a time** — each one's own questions together,
51
+ * then an explicit "add another" — rather than as a count followed by N rounds
52
+ * of interrogation. A language project has one grammar far more often than it
53
+ * has three, and this shape charges the common case a single keystroke while
54
+ * letting the multi-grammar case grow naturally.
55
+ */
56
+
57
+ import * as path from 'node:path';
58
+ import { DEFAULT_INIT_HEADS, INIT_HEADS, type InitHead, isNonEmptyDir, kebab } from './init.js';
59
+ import { InitWizardCancelled, OPTIONAL_HEAD_CHOICES, type PromptPort } from './init-prompt.js';
60
+ import { type WorkspaceDetection } from './init-workspace.js';
61
+
62
+ /** One grammar as the wizard collected it. */
63
+ export interface InitGrammarAnswer {
64
+ /** PascalCase grammar name. */
65
+ readonly name: string;
66
+ /** File extension for its documents, without a leading dot. */
67
+ readonly extension: string;
68
+ /** Scaffold a GLSP diagram for this grammar. */
69
+ readonly diagram: boolean;
70
+ }
71
+
72
+ /** Everything the wizard collects. Pure data — {@link composeInitArgv} turns it into flags. */
73
+ export interface InitAnswers {
74
+ /** Directory to scaffold into. */
75
+ readonly targetDir: string;
76
+ /** PascalCase project name. */
77
+ readonly name: string;
78
+ /** The protocol heads to start. */
79
+ readonly heads: readonly InitHead[];
80
+ /** The grammars, in the order they were given. */
81
+ readonly grammars: readonly InitGrammarAnswer[];
82
+ /** Scaffold into a non-empty directory anyway. */
83
+ readonly force: boolean;
84
+ /** Join the surrounding npm workspace. */
85
+ readonly monorepo: boolean;
86
+ /** npm scope for the package name, e.g. `@acme`. */
87
+ readonly scope?: string;
88
+ /** Leave the package publishable, which the scaffold does not do by default. */
89
+ readonly public: boolean;
90
+ }
91
+
92
+ /** PascalCase a directory name, splitting on any non-alphanumeric run. */
93
+ export function pascalCase(value: string): string {
94
+ return value
95
+ .split(/[^A-Za-z0-9]+/)
96
+ .filter(part => part.length > 0)
97
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
98
+ .join('');
99
+ }
100
+
101
+ /** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
102
+ function isPascalIdentifier(value: string): boolean {
103
+ return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
104
+ }
105
+
106
+ /**
107
+ * Compose the argv the answers stand for — the wizard's entire product.
108
+ *
109
+ * Minimal by design: a flag is emitted only where the answer differs from what
110
+ * `init` would derive anyway, so the echoed command reads as the short thing a
111
+ * person would have typed rather than as a transcript of the questions. The
112
+ * extension rides on `--grammar` because `--extensions` is grammar-scoped, and
113
+ * naming the grammar after the project leaves the derived language id alone.
114
+ */
115
+ export function composeInitArgv(answers: InitAnswers): string[] {
116
+ const argv = [answers.targetDir, '--name', answers.name];
117
+ if (answers.heads.join(',') !== DEFAULT_INIT_HEADS.join(',')) {
118
+ argv.push('--heads', answers.heads.join(','));
119
+ }
120
+ const several = answers.grammars.length > 1;
121
+ for (const grammar of answers.grammars) {
122
+ // A lone grammar named after the project, taking the derived extension, is
123
+ // exactly what `--name` alone already means — so it contributes no flags
124
+ // and the echoed command stays the short one a person would have typed.
125
+ const customExtension = grammar.extension !== kebab(grammar.name);
126
+ if (!several && grammar.name === answers.name && !customExtension && !grammar.diagram) {
127
+ continue;
128
+ }
129
+ argv.push('--grammar', grammar.name);
130
+ if (customExtension) {
131
+ argv.push('--extensions', grammar.extension);
132
+ }
133
+ if (grammar.diagram) {
134
+ argv.push('--diagram');
135
+ }
136
+ }
137
+ if (answers.force) {
138
+ argv.push('--force');
139
+ }
140
+ if (answers.monorepo) {
141
+ argv.push('--monorepo');
142
+ }
143
+ if (answers.scope !== undefined && answers.scope.length > 0) {
144
+ argv.push('--scope', answers.scope);
145
+ }
146
+ if (answers.public) {
147
+ argv.push('--public');
148
+ }
149
+ return argv;
150
+ }
151
+
152
+ /** Render an argv as a copy-pasteable command line, quoting only what a shell would mangle. */
153
+ export function formatCommand(argv: readonly string[]): string {
154
+ const quoted = argv.map(argument => (/^[A-Za-z0-9@,._/-]+$/.test(argument) ? argument : `'${argument.replace(/'/g, "'\\''")}'`));
155
+ return `hydranium-cli init ${quoted.join(' ')}`;
156
+ }
157
+
158
+ /** Validate a file extension, and reject one another grammar already claims. */
159
+ function extensionProblem(value: string, taken: ReadonlySet<string>): string | undefined {
160
+ const extension = value.replace(/^\./, '');
161
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(extension)) {
162
+ return `'${value}' is not a usable file extension.`;
163
+ }
164
+ // Checked here rather than left to the scaffolder so the answer is corrected
165
+ // while the grammar that owns it is still on screen. Langium routes documents
166
+ // to a language BY extension, so a duplicate is a silent mis-route.
167
+ return taken.has(extension) ? `'.${extension}' is already claimed by another grammar.` : undefined;
168
+ }
169
+
170
+ /**
171
+ * Collect one grammar's settings as a group — its own name, then its own
172
+ * extension — so the answers stay next to the thing they describe.
173
+ */
174
+ async function askGrammar(
175
+ prompt: PromptPort,
176
+ index: number,
177
+ defaultName: string | undefined,
178
+ takenNames: ReadonlySet<string>,
179
+ takenExtensions: ReadonlySet<string>
180
+ ): Promise<InitGrammarAnswer> {
181
+ const label = `Grammar ${index + 1}`;
182
+ const name = await prompt.text({
183
+ message: `${label} · name (PascalCase)`,
184
+ initialValue: defaultName,
185
+ placeholder: 'Domain',
186
+ validate: value => {
187
+ if (!isPascalIdentifier(value)) {
188
+ return `'${value}' is not a PascalCase identifier — letters and digits, starting with a letter.`;
189
+ }
190
+ return takenNames.has(value) ? `'${value}' is already used by another grammar.` : undefined;
191
+ }
192
+ });
193
+ const extension = await prompt.text({
194
+ message: `${label} · file extension`,
195
+ initialValue: kebab(name),
196
+ validate: value => extensionProblem(value, takenExtensions)
197
+ });
198
+ return { name, extension: extension.replace(/^\./, ''), diagram: false };
199
+ }
200
+
201
+ /**
202
+ * Ask for the protocol head set.
203
+ *
204
+ * `lsp` is not offered, it is stated: it owns the workspace, the build pipeline
205
+ * and the shared tier the other heads read through, so a project without it has
206
+ * nothing for them to serve. Presenting it as a checkbox would offer a choice
207
+ * the scaffolder then refuses.
208
+ */
209
+ async function askHeads(prompt: PromptPort): Promise<InitHead[]> {
210
+ const optional = await prompt.multiselect({
211
+ message: 'Protocol heads — lsp is always included',
212
+ choices: OPTIONAL_HEAD_CHOICES,
213
+ initialValues: DEFAULT_INIT_HEADS.filter((head): head is Exclude<InitHead, 'lsp'> => head !== 'lsp')
214
+ });
215
+ const chosen = new Set<InitHead>(['lsp', ...optional]);
216
+ return INIT_HEADS.filter(head => chosen.has(head));
217
+ }
218
+
219
+ /**
220
+ * Mark the grammar a scaffolded diagram edits.
221
+ *
222
+ * Only a real question with `glsp` on AND several grammars: a diagram type
223
+ * binds exactly ONE grammar, and with a single grammar the scaffolder derives
224
+ * that answer already — so asking would offer a choice whose "no" it would
225
+ * silently overrule.
226
+ */
227
+ async function askDiagramGrammar(
228
+ prompt: PromptPort,
229
+ heads: readonly InitHead[],
230
+ grammars: readonly InitGrammarAnswer[]
231
+ ): Promise<InitGrammarAnswer[]> {
232
+ if (!heads.includes('glsp') || grammars.length < 2) {
233
+ return [...grammars];
234
+ }
235
+ const chosen = await prompt.select({
236
+ message: 'Which grammar does the GLSP diagram edit?',
237
+ choices: grammars.map(grammar => ({ value: grammar.name, label: grammar.name, hint: `.${grammar.extension}` })),
238
+ initialValue: grammars[0].name
239
+ });
240
+ return grammars.map(grammar => ({ ...grammar, diagram: grammar.name === chosen }));
241
+ }
242
+
243
+ /**
244
+ * Confirm scaffolding into a directory that already has content, returning
245
+ * whether `--force` is needed.
246
+ *
247
+ * Declining ends the session rather than looping back to the target question:
248
+ * the answer is "not here", and re-prompting for a directory would leave the
249
+ * echoed command describing a different run than the one that was started.
250
+ */
251
+ async function confirmOccupiedTarget(prompt: PromptPort, targetDir: string, isOccupied: (targetDir: string) => boolean): Promise<boolean> {
252
+ if (!isOccupied(path.resolve(targetDir))) {
253
+ return false;
254
+ }
255
+ const force = await prompt.confirm({ message: `${targetDir} is not empty — scaffold into it anyway?`, initialValue: false });
256
+ if (!force) {
257
+ prompt.outro(`Nothing scaffolded — ${targetDir} is not empty.`);
258
+ throw new InitWizardCancelled();
259
+ }
260
+ return true;
261
+ }
262
+
263
+ /** Ask the workspace questions, or answer them "no" when there is no workspace to join. */
264
+ async function askWorkspaceMembership(
265
+ prompt: PromptPort,
266
+ detection: WorkspaceDetection | undefined
267
+ ): Promise<{ monorepo: boolean; scope?: string }> {
268
+ if (detection === undefined) {
269
+ return { monorepo: false };
270
+ }
271
+ const covered =
272
+ detection.coveredBy === undefined
273
+ ? `The root manifest has no entry covering it, so one is printed for you to add.`
274
+ : `Already covered by the "${detection.coveredBy}" workspaces entry.`;
275
+ // The placement is named, not just the root. The root alone reads as "this
276
+ // ran somewhere I did not ask for" whenever the command was issued from a
277
+ // different directory than the target, which is the normal case.
278
+ prompt.note(`Root: ${detection.rootDir}\nPlacing: ${detection.targetPath}\n${covered}`, 'Detected an npm workspace');
279
+
280
+ const monorepo = await prompt.confirm({ message: 'Scaffold as a member of that workspace?', initialValue: true });
281
+ if (!monorepo) {
282
+ return { monorepo: false };
283
+ }
284
+ const scope = await prompt.text({
285
+ message: "Package scope ('-' for none)",
286
+ initialValue: detection.scope ?? '-',
287
+ validate: value =>
288
+ value === '-' || /^@[a-z0-9][a-z0-9._-]*$/.test(value)
289
+ ? undefined
290
+ : `'${value}' is not an npm scope — try '@acme', or '-' for none.`
291
+ });
292
+ return { monorepo, scope: scope === '-' ? undefined : scope };
293
+ }
294
+
295
+ /**
296
+ * Ask the packaging questions.
297
+ *
298
+ * Workspace membership and scope are facts about a surrounding repo, so they are
299
+ * reached only where one was detected. **Publishability is asked on every path**,
300
+ * including a standalone project with no workspace anywhere near it: the scaffold
301
+ * withholds publication because it also emits an UNLICENSED manifest, which makes
302
+ * the answer a licence posture rather than a packaging detail, and a licence
303
+ * posture must not be settled by a default nobody was shown. It knowingly spends
304
+ * a question on the shortest path — the alternative was stating the packaging in
305
+ * the echoed command instead, which informs whoever reads the output and still
306
+ * decides for whoever does not.
307
+ *
308
+ * Asked in the negative, so the offered answer is the emission's own default and
309
+ * a "yes" is the licence decision the adopter has to have made first.
310
+ */
311
+ async function askPackaging(
312
+ prompt: PromptPort,
313
+ detection: WorkspaceDetection | undefined
314
+ ): Promise<{ monorepo: boolean; scope?: string; public: boolean }> {
315
+ const membership = await askWorkspaceMembership(prompt, detection);
316
+ const isPublic = await prompt.confirm({ message: 'Publishable to npm (the manifest says UNLICENSED)?', initialValue: false });
317
+ return { ...membership, public: isPublic };
318
+ }
319
+
320
+ /**
321
+ * Run the wizard and return the argv it composed.
322
+ *
323
+ * `detect` is injected rather than called here so the flow stays testable
324
+ * without a real workspace on disk, and because detection needs the target
325
+ * directory — which is itself one of the answers.
326
+ */
327
+ export async function runInitWizard(
328
+ prompt: PromptPort,
329
+ detect: (targetDir: string) => WorkspaceDetection | undefined,
330
+ options: { readonly targetDir?: string; readonly isOccupied?: (targetDir: string) => boolean } = {}
331
+ ): Promise<string[]> {
332
+ prompt.intro('Scaffold a Hydranium language project');
333
+
334
+ const targetDir =
335
+ options.targetDir ??
336
+ (await prompt.text({
337
+ message: 'Target directory',
338
+ placeholder: './my-language',
339
+ validate: value => (value.length > 0 ? undefined : 'A target directory is required.')
340
+ }));
341
+
342
+ // Asked FIRST, not left to the scaffolder — which checks only after the whole
343
+ // wizard has run, so a scaffold that cannot happen costs every remaining
344
+ // question before saying so. Answering yes is exactly `--force`, which keeps
345
+ // the recovery flag-expressible rather than a wizard-only escape.
346
+ const force = await confirmOccupiedTarget(prompt, targetDir, options.isOccupied ?? isNonEmptyDir);
347
+
348
+ const name = await prompt.text({
349
+ message: 'Project name (PascalCase)',
350
+ initialValue: pascalCase(path.basename(path.resolve(targetDir))),
351
+ validate: value =>
352
+ isPascalIdentifier(value) ? undefined : `'${value}' is not a PascalCase identifier — letters and digits, starting with a letter.`
353
+ });
354
+
355
+ const heads = await askHeads(prompt);
356
+
357
+ // One grammar at a time, each with its own settings, then an explicit "add
358
+ // another" — so the single-grammar case costs two keystrokes and the
359
+ // multi-grammar one never asks for a count up front.
360
+ const grammars: InitGrammarAnswer[] = [];
361
+ const takenNames = new Set<string>();
362
+ const takenExtensions = new Set<string>();
363
+ do {
364
+ const grammar = await askGrammar(prompt, grammars.length, grammars.length === 0 ? name : undefined, takenNames, takenExtensions);
365
+ grammars.push(grammar);
366
+ takenNames.add(grammar.name);
367
+ takenExtensions.add(grammar.extension);
368
+ } while (await prompt.confirm({ message: 'Add another grammar?', initialValue: false }));
369
+
370
+ const withDiagram = await askDiagramGrammar(prompt, heads, grammars);
371
+ const packaging = await askPackaging(prompt, detect(targetDir));
372
+
373
+ const argv = composeInitArgv({ targetDir, name, heads, grammars: withDiagram, force, ...packaging });
374
+ // Echoed as the LAST thing before the scaffold runs, because it is the
375
+ // artefact the session produces: everything above is how it was reached.
376
+ prompt.outro(formatCommand(argv));
377
+ return argv;
378
+ }