@hydranium/cli 1.0.0-next.42 → 1.0.0-next.421

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 (371) hide show
  1. package/README.md +125 -204
  2. package/bin/hydranium-cli.js +13 -0
  3. package/heap-analysis/heap/core/load.mjs +2 -2
  4. package/heap-analysis/heap/langium/langium.classifier.mjs +22 -11
  5. package/lib/cli-version.js.map +1 -1
  6. package/lib/cli.js +11 -2
  7. package/lib/cli.js.map +1 -1
  8. package/lib/commands/analyze-heap-args.d.ts.map +1 -1
  9. package/lib/commands/analyze-heap-args.js +1 -2
  10. package/lib/commands/analyze-heap-args.js.map +1 -1
  11. package/lib/commands/analyze-heap.d.ts +3 -0
  12. package/lib/commands/analyze-heap.d.ts.map +1 -1
  13. package/lib/commands/analyze-heap.js +2 -1
  14. package/lib/commands/analyze-heap.js.map +1 -1
  15. package/lib/commands/ast-ground-truth-args.d.ts +3 -2
  16. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
  17. package/lib/commands/ast-ground-truth-args.js +12 -10
  18. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  19. package/lib/commands/ast-ground-truth-driver.js +4 -2
  20. package/lib/commands/ast-ground-truth-driver.js.map +1 -1
  21. package/lib/commands/ast-ground-truth.js +1 -1
  22. package/lib/commands/ast-ground-truth.js.map +1 -1
  23. package/lib/commands/check-skills-args.d.ts +11 -0
  24. package/lib/commands/check-skills-args.d.ts.map +1 -0
  25. package/lib/commands/check-skills-args.js +71 -0
  26. package/lib/commands/check-skills-args.js.map +1 -0
  27. package/lib/commands/client-session.d.ts +17 -0
  28. package/lib/commands/client-session.d.ts.map +1 -0
  29. package/lib/commands/client-session.js +27 -0
  30. package/lib/commands/client-session.js.map +1 -0
  31. package/lib/commands/create-args.d.ts +17 -0
  32. package/lib/commands/create-args.d.ts.map +1 -0
  33. package/lib/commands/create-args.js +53 -0
  34. package/lib/commands/create-args.js.map +1 -0
  35. package/lib/commands/create.d.ts +38 -0
  36. package/lib/commands/create.d.ts.map +1 -0
  37. package/lib/commands/create.js +43 -0
  38. package/lib/commands/create.js.map +1 -0
  39. package/lib/commands/delete-args.d.ts +17 -0
  40. package/lib/commands/delete-args.d.ts.map +1 -0
  41. package/lib/commands/delete-args.js +49 -0
  42. package/lib/commands/delete-args.js.map +1 -0
  43. package/lib/commands/delete.d.ts +30 -0
  44. package/lib/commands/delete.d.ts.map +1 -0
  45. package/lib/commands/delete.js +32 -0
  46. package/lib/commands/delete.js.map +1 -0
  47. package/lib/commands/generate-ast-builder.d.ts +1 -1
  48. package/lib/commands/generate-ast-builder.d.ts.map +1 -1
  49. package/lib/commands/generate-ast-builder.js +2 -11
  50. package/lib/commands/generate-ast-builder.js.map +1 -1
  51. package/lib/commands/generate-languages.d.ts +35 -0
  52. package/lib/commands/generate-languages.d.ts.map +1 -0
  53. package/lib/commands/generate-languages.js +105 -0
  54. package/lib/commands/generate-languages.js.map +1 -0
  55. package/lib/commands/generate-transfer-builder.d.ts +33 -0
  56. package/lib/commands/generate-transfer-builder.d.ts.map +1 -0
  57. package/lib/commands/generate-transfer-builder.js +86 -0
  58. package/lib/commands/generate-transfer-builder.js.map +1 -0
  59. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
  60. package/lib/commands/generate-transfer-model-args.js +20 -5
  61. package/lib/commands/generate-transfer-model-args.js.map +1 -1
  62. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
  63. package/lib/commands/generate-transfer-model-config.js +8 -3
  64. package/lib/commands/generate-transfer-model-config.js.map +1 -1
  65. package/lib/commands/generate-transfer-model.d.ts +41 -9
  66. package/lib/commands/generate-transfer-model.d.ts.map +1 -1
  67. package/lib/commands/generate-transfer-model.js +225 -68
  68. package/lib/commands/generate-transfer-model.js.map +1 -1
  69. package/lib/commands/generated-module-path.d.ts +16 -0
  70. package/lib/commands/generated-module-path.d.ts.map +1 -0
  71. package/lib/commands/generated-module-path.js +21 -0
  72. package/lib/commands/generated-module-path.js.map +1 -0
  73. package/lib/commands/harness-args.d.ts +46 -43
  74. package/lib/commands/harness-args.d.ts.map +1 -1
  75. package/lib/commands/harness-args.js +170 -29
  76. package/lib/commands/harness-args.js.map +1 -1
  77. package/lib/commands/headless-harness.d.ts +37 -20
  78. package/lib/commands/headless-harness.d.ts.map +1 -1
  79. package/lib/commands/headless-harness.js +71 -19
  80. package/lib/commands/headless-harness.js.map +1 -1
  81. package/lib/commands/init-args.d.ts +5 -18
  82. package/lib/commands/init-args.d.ts.map +1 -1
  83. package/lib/commands/init-args.js +131 -35
  84. package/lib/commands/init-args.js.map +1 -1
  85. package/lib/commands/init-hosts.d.ts +24 -0
  86. package/lib/commands/init-hosts.d.ts.map +1 -0
  87. package/lib/commands/init-hosts.js +37 -0
  88. package/lib/commands/init-hosts.js.map +1 -0
  89. package/lib/commands/init-prompt.d.ts +3 -3
  90. package/lib/commands/init-prompt.d.ts.map +1 -1
  91. package/lib/commands/init-prompt.js +4 -5
  92. package/lib/commands/init-prompt.js.map +1 -1
  93. package/lib/commands/init-skills.d.ts +41 -0
  94. package/lib/commands/init-skills.d.ts.map +1 -0
  95. package/lib/commands/init-skills.js +110 -0
  96. package/lib/commands/init-skills.js.map +1 -0
  97. package/lib/commands/init-templates.d.ts +20 -14
  98. package/lib/commands/init-templates.d.ts.map +1 -1
  99. package/lib/commands/init-templates.js +3134 -763
  100. package/lib/commands/init-templates.js.map +1 -1
  101. package/lib/commands/init-wizard.d.ts +10 -0
  102. package/lib/commands/init-wizard.d.ts.map +1 -1
  103. package/lib/commands/init-wizard.js +76 -31
  104. package/lib/commands/init-wizard.js.map +1 -1
  105. package/lib/commands/init-workspace.d.ts +13 -11
  106. package/lib/commands/init-workspace.d.ts.map +1 -1
  107. package/lib/commands/init-workspace.js +21 -7
  108. package/lib/commands/init-workspace.js.map +1 -1
  109. package/lib/commands/init.d.ts +61 -24
  110. package/lib/commands/init.d.ts.map +1 -1
  111. package/lib/commands/init.js +244 -50
  112. package/lib/commands/init.js.map +1 -1
  113. package/lib/commands/kebab.d.ts +15 -0
  114. package/lib/commands/kebab.d.ts.map +1 -0
  115. package/lib/commands/kebab.js +17 -0
  116. package/lib/commands/kebab.js.map +1 -0
  117. package/lib/commands/lint-grammar-args.d.ts +3 -2
  118. package/lib/commands/lint-grammar-args.d.ts.map +1 -1
  119. package/lib/commands/lint-grammar-args.js +14 -11
  120. package/lib/commands/lint-grammar-args.js.map +1 -1
  121. package/lib/commands/lint-grammar-driver.js +2 -1
  122. package/lib/commands/lint-grammar-driver.js.map +1 -1
  123. package/lib/commands/lint-grammar-report.js.map +1 -1
  124. package/lib/commands/lint-grammar.js +1 -1
  125. package/lib/commands/lint-grammar.js.map +1 -1
  126. package/lib/commands/measure-memory-args.d.ts +3 -2
  127. package/lib/commands/measure-memory-args.d.ts.map +1 -1
  128. package/lib/commands/measure-memory-args.js +26 -14
  129. package/lib/commands/measure-memory-args.js.map +1 -1
  130. package/lib/commands/measure-memory-driver.js +5 -3
  131. package/lib/commands/measure-memory-driver.js.map +1 -1
  132. package/lib/commands/measure-memory.d.ts +1 -1
  133. package/lib/commands/measure-memory.d.ts.map +1 -1
  134. package/lib/commands/measure-memory.js +6 -3
  135. package/lib/commands/measure-memory.js.map +1 -1
  136. package/lib/commands/message-catalogue-args.d.ts +16 -0
  137. package/lib/commands/message-catalogue-args.d.ts.map +1 -0
  138. package/lib/commands/message-catalogue-args.js +53 -0
  139. package/lib/commands/message-catalogue-args.js.map +1 -0
  140. package/lib/commands/message-catalogue.d.ts +21 -0
  141. package/lib/commands/message-catalogue.d.ts.map +1 -0
  142. package/lib/commands/message-catalogue.js +48 -0
  143. package/lib/commands/message-catalogue.js.map +1 -0
  144. package/lib/commands/model-docs-args.d.ts +4 -3
  145. package/lib/commands/model-docs-args.d.ts.map +1 -1
  146. package/lib/commands/model-docs-args.js +13 -11
  147. package/lib/commands/model-docs-args.js.map +1 -1
  148. package/lib/commands/model-docs-driver.js +2 -1
  149. package/lib/commands/model-docs-driver.js.map +1 -1
  150. package/lib/commands/model-docs.js +1 -1
  151. package/lib/commands/model-docs.js.map +1 -1
  152. package/lib/commands/projects-args.d.ts.map +1 -1
  153. package/lib/commands/projects-args.js +5 -5
  154. package/lib/commands/projects-args.js.map +1 -1
  155. package/lib/commands/projects.d.ts +3 -4
  156. package/lib/commands/projects.d.ts.map +1 -1
  157. package/lib/commands/projects.js +2 -2
  158. package/lib/commands/projects.js.map +1 -1
  159. package/lib/commands/query-args.d.ts.map +1 -1
  160. package/lib/commands/query-args.js +6 -5
  161. package/lib/commands/query-args.js.map +1 -1
  162. package/lib/commands/query.d.ts +6 -5
  163. package/lib/commands/query.d.ts.map +1 -1
  164. package/lib/commands/query.js +5 -4
  165. package/lib/commands/query.js.map +1 -1
  166. package/lib/commands/reflect-args.d.ts +3 -2
  167. package/lib/commands/reflect-args.d.ts.map +1 -1
  168. package/lib/commands/reflect-args.js +14 -11
  169. package/lib/commands/reflect-args.js.map +1 -1
  170. package/lib/commands/reflect-driver.js +2 -1
  171. package/lib/commands/reflect-driver.js.map +1 -1
  172. package/lib/commands/reflect-report.js.map +1 -1
  173. package/lib/commands/reflect.js +1 -1
  174. package/lib/commands/reflect.js.map +1 -1
  175. package/lib/commands/save-args.d.ts.map +1 -1
  176. package/lib/commands/save-args.js +7 -4
  177. package/lib/commands/save-args.js.map +1 -1
  178. package/lib/commands/save.d.ts +8 -4
  179. package/lib/commands/save.d.ts.map +1 -1
  180. package/lib/commands/save.js +12 -23
  181. package/lib/commands/save.js.map +1 -1
  182. package/lib/commands/skills.d.ts +22 -0
  183. package/lib/commands/skills.d.ts.map +1 -0
  184. package/lib/commands/skills.js +129 -0
  185. package/lib/commands/skills.js.map +1 -0
  186. package/lib/commands/validate-args.d.ts +3 -2
  187. package/lib/commands/validate-args.d.ts.map +1 -1
  188. package/lib/commands/validate-args.js +14 -11
  189. package/lib/commands/validate-args.js.map +1 -1
  190. package/lib/commands/validate-driver.js +2 -1
  191. package/lib/commands/validate-driver.js.map +1 -1
  192. package/lib/commands/validate-report.js.map +1 -1
  193. package/lib/commands/validate.js +1 -1
  194. package/lib/commands/validate.js.map +1 -1
  195. package/lib/commands/watch-args.d.ts.map +1 -1
  196. package/lib/commands/watch-args.js +6 -4
  197. package/lib/commands/watch-args.js.map +1 -1
  198. package/lib/commands/watch.d.ts +3 -4
  199. package/lib/commands/watch.d.ts.map +1 -1
  200. package/lib/commands/watch.js +5 -2
  201. package/lib/commands/watch.js.map +1 -1
  202. package/lib/driver-heap.d.ts +52 -0
  203. package/lib/driver-heap.d.ts.map +1 -0
  204. package/lib/driver-heap.js +51 -0
  205. package/lib/driver-heap.js.map +1 -0
  206. package/lib/index.d.ts +3 -0
  207. package/lib/index.d.ts.map +1 -1
  208. package/lib/index.js +3 -0
  209. package/lib/index.js.map +1 -1
  210. package/lib/log-level.d.ts +21 -11
  211. package/lib/log-level.d.ts.map +1 -1
  212. package/lib/log-level.js +25 -14
  213. package/lib/log-level.js.map +1 -1
  214. package/lib/spawn-data-server.d.ts +9 -4
  215. package/lib/spawn-data-server.d.ts.map +1 -1
  216. package/lib/spawn-data-server.js +5 -4
  217. package/lib/spawn-data-server.js.map +1 -1
  218. package/lib/stderr-connection-logger.d.ts +12 -0
  219. package/lib/stderr-connection-logger.d.ts.map +1 -0
  220. package/lib/stderr-connection-logger.js +16 -0
  221. package/lib/stderr-connection-logger.js.map +1 -0
  222. package/lib/testing/echo-server.js +9 -1
  223. package/lib/testing/echo-server.js.map +1 -1
  224. package/package.json +99 -41
  225. package/src/cli.ts +11 -2
  226. package/src/commands/analyze-heap-args.ts +1 -2
  227. package/src/commands/analyze-heap.ts +4 -1
  228. package/src/commands/ast-ground-truth-args.ts +16 -13
  229. package/src/commands/ast-ground-truth-driver.ts +4 -2
  230. package/src/commands/ast-ground-truth.ts +1 -1
  231. package/src/commands/check-skills-args.ts +70 -0
  232. package/src/commands/client-session.ts +33 -0
  233. package/src/commands/create-args.ts +68 -0
  234. package/src/commands/create.ts +83 -0
  235. package/src/commands/delete-args.ts +64 -0
  236. package/src/commands/delete.ts +61 -0
  237. package/src/commands/generate-ast-builder.ts +2 -12
  238. package/src/commands/generate-languages.ts +132 -0
  239. package/src/commands/generate-transfer-builder.ts +101 -0
  240. package/src/commands/generate-transfer-model-args.ts +20 -5
  241. package/src/commands/generate-transfer-model-config.ts +8 -3
  242. package/src/commands/generate-transfer-model.ts +297 -66
  243. package/src/commands/generated-module-path.ts +22 -0
  244. package/src/commands/harness-args.ts +163 -33
  245. package/src/commands/headless-harness.ts +89 -28
  246. package/src/commands/init-args.ts +135 -35
  247. package/src/commands/init-hosts.ts +51 -0
  248. package/src/commands/init-prompt.ts +5 -6
  249. package/src/commands/init-skills.ts +129 -0
  250. package/src/commands/init-templates.ts +3326 -785
  251. package/src/commands/init-wizard.ts +97 -32
  252. package/src/commands/init-workspace.ts +31 -16
  253. package/src/commands/init.ts +311 -65
  254. package/src/commands/kebab.ts +17 -0
  255. package/src/commands/lint-grammar-args.ts +18 -14
  256. package/src/commands/lint-grammar-driver.ts +2 -1
  257. package/src/commands/lint-grammar.ts +1 -1
  258. package/src/commands/measure-memory-args.ts +31 -17
  259. package/src/commands/measure-memory-driver.ts +5 -3
  260. package/src/commands/measure-memory.ts +6 -3
  261. package/src/commands/message-catalogue-args.ts +55 -0
  262. package/src/commands/message-catalogue.ts +56 -0
  263. package/src/commands/model-docs-args.ts +17 -14
  264. package/src/commands/model-docs-driver.ts +2 -1
  265. package/src/commands/model-docs.ts +1 -1
  266. package/src/commands/projects-args.ts +5 -4
  267. package/src/commands/projects.ts +4 -6
  268. package/src/commands/query-args.ts +6 -4
  269. package/src/commands/query.ts +9 -9
  270. package/src/commands/reflect-args.ts +18 -14
  271. package/src/commands/reflect-driver.ts +2 -1
  272. package/src/commands/reflect.ts +1 -1
  273. package/src/commands/save-args.ts +7 -3
  274. package/src/commands/save.ts +24 -27
  275. package/src/commands/skills.ts +146 -0
  276. package/src/commands/validate-args.ts +18 -14
  277. package/src/commands/validate-driver.ts +2 -1
  278. package/src/commands/validate.ts +1 -1
  279. package/src/commands/watch-args.ts +6 -3
  280. package/src/commands/watch.ts +7 -6
  281. package/src/driver-heap.ts +65 -0
  282. package/src/index.ts +3 -0
  283. package/src/log-level.ts +42 -14
  284. package/src/spawn-data-server.ts +17 -5
  285. package/src/stderr-connection-logger.ts +18 -0
  286. package/src/testing/echo-server.ts +11 -2
  287. package/templates/client/gitignore +3 -0
  288. package/templates/client/src/__GRAMMAR_ID__-diagram-module.ts +82 -0
  289. package/templates/editorconfig +15 -0
  290. package/templates/gitignore +1 -0
  291. package/templates/host-browser/esbuild.mjs +82 -0
  292. package/templates/host-browser/gitignore +4 -0
  293. package/templates/host-browser/hydranium-mark.svg +40 -0
  294. package/templates/host-browser/index.html +287 -0
  295. package/templates/host-browser/playwright.config.mts +24 -0
  296. package/templates/host-browser/src/data-status.ts +15 -0
  297. package/templates/host-browser/src/diagram-save.ts +60 -0
  298. package/templates/host-browser/src/diagram.ts +121 -0
  299. package/templates/host-browser/src/editor.ts +116 -0
  300. package/templates/host-browser/src/log.ts +14 -0
  301. package/templates/host-browser/src/monaco-editor-core-worker.d.ts +7 -0
  302. package/templates/host-browser/src/monaco-editor-worker.ts +10 -0
  303. package/templates/host-browser/src/posix-path-shim.ts +23 -0
  304. package/templates/host-browser/src/shown-file.ts +61 -0
  305. package/templates/host-browser/src/workspace.ts +5 -0
  306. package/templates/host-browser/tsconfig.e2e.json +16 -0
  307. package/templates/host-browser/tsconfig.page.json +16 -0
  308. package/templates/host-browser/tsconfig.worker.json +16 -0
  309. package/templates/host-theia-browser/esbuild.mjs +24 -0
  310. package/templates/host-theia-browser/gitignore +9 -0
  311. package/templates/host-theia-browser/playwright.config.mts +42 -0
  312. package/templates/host-theia-browser/start.mjs +50 -0
  313. package/templates/host-theia-browser/tsconfig.e2e.json +16 -0
  314. package/templates/host-theia-electron/esbuild.mjs +27 -0
  315. package/templates/host-theia-electron/gitignore +8 -0
  316. package/templates/host-theia-electron/playwright.config.mts +12 -0
  317. package/templates/host-theia-electron/start.mjs +47 -0
  318. package/templates/host-theia-electron/tsconfig.e2e.json +16 -0
  319. package/templates/host-vscode/esbuild.mjs +38 -0
  320. package/templates/host-vscode/gitignore +7 -0
  321. package/templates/host-vscode/playwright.config.mts +14 -0
  322. package/templates/host-vscode/src/__GRAMMAR_ID__-diagram-editor.ts +19 -0
  323. package/templates/host-vscode/src/show-model-command.ts +24 -0
  324. package/templates/host-vscode/src/webview/__GRAMMAR_ID__-diagram.ts +18 -0
  325. package/templates/host-vscode/src/webview/diagram.css +40 -0
  326. package/templates/host-vscode/start.mjs +22 -0
  327. package/templates/host-vscode/test/content-server.cjs +40 -0
  328. package/templates/host-vscode/test/install-vscode.mts +21 -0
  329. package/templates/host-vscode/test/virtual-documents.spec.mts +28 -0
  330. package/templates/host-vscode/test/vscode-version.mts +10 -0
  331. package/templates/host-vscode/tsconfig.e2e.json +17 -0
  332. package/templates/host-vscode/tsconfig.json +19 -0
  333. package/templates/host-vscode/tsconfig.webview.json +17 -0
  334. package/templates/oxlintrc.json +4 -0
  335. package/templates/protocol/gitignore +3 -0
  336. package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +47 -0
  337. package/templates/server/gitignore +6 -0
  338. package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +62 -0
  339. package/templates/server/src/glsp/__GRAMMAR_ID__/delete-operation-handler.ts +63 -0
  340. package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +144 -0
  341. package/templates/server/src/glsp/__GRAMMAR_ID__/state.ts +18 -0
  342. package/templates/server/src/glsp/__GRAMMAR_ID__/storage.ts +16 -0
  343. package/templates/server/src/glsp/__GRAMMAR_ID__/submission-handler.ts +24 -0
  344. package/templates/server/src/grammar/common.langium +12 -0
  345. package/templates/server/src/language-server/__GRAMMAR_ID__-semantic-tokens.ts +10 -0
  346. package/templates/server/src/language-server/__GRAMMAR_ID__-serializer.ts +53 -0
  347. package/templates/server/src/language-server/__GRAMMAR_ID__-validation.ts +30 -0
  348. package/templates/server/src/language-server/ast.ts +23 -0
  349. package/templates/server/src/node/services.ts +16 -0
  350. package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +403 -0
  351. package/templates/server/test/__GRAMMAR_ID__-semantic-tokens.test.ts +34 -0
  352. package/templates/server/test/tsconfig.json +4 -0
  353. package/templates/server/tsconfig.test.json +10 -0
  354. package/templates/server/vitest.config.ts +14 -0
  355. package/templates/theia/gitignore +3 -0
  356. package/templates/theia/src/browser/__GRAMMAR_ID__-diagram-frontend-module.ts +65 -0
  357. package/templates/theia/src/browser/__PROJECT_ID__-connection-preload-module.ts +15 -0
  358. package/templates/theia/src/browser/__PROJECT_ID__-data-connection.ts +15 -0
  359. package/templates/theia/src/browser/__PROJECT_ID__-data-port.ts +11 -0
  360. package/templates/theia/src/browser/__PROJECT_ID__-glsp-client-contribution.ts +13 -0
  361. package/templates/theia/src/browser/__PROJECT_ID__-show-model-command.ts +51 -0
  362. package/templates/theia/src/common/__PROJECT_ID__-glsp-contribution.ts +3 -0
  363. package/templates/theia/src/node/__PROJECT_ID__-connection-backend-module.ts +12 -0
  364. package/templates/theia/src/node/__PROJECT_ID__-data-backend-module.ts +15 -0
  365. package/templates/theia/src/node/__PROJECT_ID__-glsp-backend-module.ts +16 -0
  366. package/templates/theia/style/diagram.css +20 -0
  367. package/templates/theia/tsconfig.json +24 -0
  368. package/templates/vscode-servers/gitignore +4 -0
  369. package/templates/vscode-servers/tsconfig.json +19 -0
  370. package/templates/workspace/nodes.__EXTENSION__ +5 -0
  371. package/templates/workspace/references.__EXTENSION__ +2 -0
@@ -7,17 +7,26 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
+ import { spawnSync } from 'node:child_process';
10
11
  import * as fs from 'node:fs';
12
+ import { createRequire } from 'node:module';
11
13
  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
+ import { editingHeads, INIT_HOSTS, type InitHost, isInitHost, isTheiaHost } from './init-hosts.js';
15
+ import { buildInitTemplates, readFrameworkVersion, workspaceRootPins } from './init-templates.js';
16
+ import { kebab } from './kebab.js';
17
+ import {
18
+ createNodeWorkspaceProbe,
19
+ detectWorkspace,
20
+ parseNpmVersion,
21
+ workspaceGlobCovers,
22
+ type JsonValue,
23
+ type WorkspaceDetection,
24
+ type WorkspaceProbe
25
+ } from './init-workspace.js';
14
26
 
15
27
  /**
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.
28
+ * The protocol heads a scaffold can start, in composition order. Any non-empty
29
+ * set: without `lsp`, the socket heads bring the workspace up themselves.
21
30
  */
22
31
  export const INIT_HEADS = ['lsp', 'data', 'glsp'] as const;
23
32
 
@@ -27,6 +36,48 @@ export type InitHead = (typeof INIT_HEADS)[number];
27
36
  /** The heads a scaffold emits when `--heads` is not given. */
28
37
  export const DEFAULT_INIT_HEADS: readonly InitHead[] = ['lsp', 'data'];
29
38
 
39
+ /** One package of a scaffolded project, in the directory of its name — see {@link initMembers}. */
40
+ export type InitMember = 'protocol' | 'server' | 'client' | 'vscode-servers' | 'theia' | `host-${InitHost}`;
41
+
42
+ /**
43
+ * The packages a scaffold with these heads and hosts emits, in the order a
44
+ * workspace lists them. `client` comes with the `glsp` head, which always
45
+ * brings a diagram, and each host is a package of its own. `vscode-servers`
46
+ * comes with the VS Code host, whose server launch it holds, and with a Theia
47
+ * application, which sideloads it; `theia` is the Theia extension the Theia
48
+ * applications run.
49
+ */
50
+ export function initMembers(heads: readonly InitHead[], hosts: readonly InitHost[]): InitMember[] {
51
+ return [
52
+ 'protocol',
53
+ 'server',
54
+ ...(heads.includes('glsp') ? (['client'] as const) : []),
55
+ ...(hosts.includes('vscode') || hosts.some(isTheiaHost) ? (['vscode-servers'] as const) : []),
56
+ ...(hosts.some(isTheiaHost) ? (['theia'] as const) : []),
57
+ ...hosts.map(host => `host-${host}` as const)
58
+ ];
59
+ }
60
+
61
+ /** A member's path relative to the workspace root, and the `workspaces` entry already covering it. */
62
+ export interface InitMemberPlacement {
63
+ readonly path: string;
64
+ readonly coveredBy?: string;
65
+ }
66
+
67
+ /**
68
+ * Where each member lands in a detected workspace.
69
+ *
70
+ * Judged per member rather than for the project directory, which holds no
71
+ * package: a root listing its members one by one needs an entry for each,
72
+ * while a `packages/*` glob covers none of them one level deeper.
73
+ */
74
+ export function memberPlacements(detection: WorkspaceDetection, members: readonly InitMember[]): readonly InitMemberPlacement[] {
75
+ return members.map(member => {
76
+ const memberPath = `${detection.targetPath}/${member}`;
77
+ return { path: memberPath, coveredBy: detection.workspaces.find(glob => workspaceGlobCovers(glob, memberPath)) };
78
+ });
79
+ }
80
+
30
81
  /** True for a string naming one of {@link INIT_HEADS}. */
31
82
  export function isInitHead(value: string): value is InitHead {
32
83
  return (INIT_HEADS as readonly string[]).includes(value);
@@ -55,10 +106,9 @@ export interface InitGrammarSpec {
55
106
  /**
56
107
  * Scaffold a GLSP diagram for this grammar. Requires the `glsp` head.
57
108
  *
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.
109
+ * Per grammar rather than per project because the scaffolded diagram is typed
110
+ * over one grammar's model, and a grammar with no diagram sitting on the same
111
+ * server is the normal case.
62
112
  */
63
113
  readonly diagram?: boolean;
64
114
  }
@@ -128,10 +178,16 @@ export interface InitWorkspacePlacement {
128
178
  readonly baseTsconfig?: string;
129
179
  /** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
130
180
  readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
131
- /** The workspace's prettier `printWidth`, so emitted sources wrap where the repo wraps. */
181
+ /** The workspace's formatter `printWidth`, so emitted sources wrap where the repo wraps. */
132
182
  readonly printWidth?: number;
133
183
  /** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
134
184
  readonly eslintConfig?: string;
185
+ /** Filename of the workspace Oxlint config, when present. */
186
+ readonly oxlintConfig?: string;
187
+ /** The npm version the workspace root's `packageManager` declares; with {@link npmVersion} it decides the scaffold's `vitest` range. */
188
+ readonly packageManagerNpm?: string;
189
+ /** The npm running `init`, when known. npm does not enforce `packageManager`, so a declaration alone cannot lift `vitest`. */
190
+ readonly npmVersion?: string;
135
191
  }
136
192
 
137
193
  /**
@@ -185,8 +241,10 @@ export interface InitComposition {
185
241
  * one data server per project, not one per language).
186
242
  */
187
243
  readonly projectId: string;
188
- /** The protocol heads to start, always including `lsp`. */
244
+ /** The protocol heads to start, at least one. */
189
245
  readonly heads: readonly InitHead[];
246
+ /** The host applications emitted around the server, each a package of its own. */
247
+ readonly hosts: readonly InitHost[];
190
248
  /** The grammars, in the order given. The first is the configuration root. */
191
249
  readonly grammars: readonly InitGrammarNames[];
192
250
  /** How the emitted `package.json` presents itself, and where it sits. */
@@ -222,14 +280,17 @@ export interface InitCommandOptions {
222
280
  readonly grammars?: readonly InitGrammarSpec[];
223
281
  /** The protocol heads to start. Defaults to {@link DEFAULT_INIT_HEADS}. */
224
282
  readonly heads?: readonly InitHead[];
283
+ /** The host applications to emit around the server. Defaults to none. */
284
+ readonly hosts?: readonly InitHost[];
225
285
  /** Scaffold into a non-empty directory anyway. */
226
286
  readonly force?: boolean;
287
+ /** Install dependencies after scaffolding. Only supported for standalone projects. */
288
+ readonly install?: boolean;
227
289
  /**
228
290
  * 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.
291
+ * tsconfig and address the package by `--prefix` in the scripts it prints
292
+ * back at itself. Errors when no ancestor declares `workspaces`, rather than
293
+ * silently degrading to a standalone project the caller did not ask for.
233
294
  */
234
295
  readonly monorepo?: boolean;
235
296
  /** npm scope for the package name, e.g. `@acme`. */
@@ -244,21 +305,26 @@ export interface InitCommandOptions {
244
305
  readonly __writeFilesForTest?: (targetDir: string, files: readonly InitFile[]) => void;
245
306
  }
246
307
 
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
308
  /** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
253
309
  function isPascalIdentifier(value: string): boolean {
254
310
  return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
255
311
  }
256
312
 
257
- /** Strip a leading dot and reject an extension that is empty or dotted through. */
313
+ /**
314
+ * True for a file extension, without its leading dot, that is safe in an
315
+ * emitted file name and string literal: letters, digits and dashes.
316
+ */
317
+ export function isUsableExtension(extension: string): boolean {
318
+ return /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(extension);
319
+ }
320
+
321
+ /** Strip a leading dot and reject an extension {@link isUsableExtension} refuses. */
258
322
  function normalizeExtension(value: string, grammar: string): string {
259
323
  const extension = value.replace(/^\./, '');
260
- if (extension.length === 0) {
261
- throw new Error(`Invalid extension '${value}' for grammar '${grammar}': an extension cannot be empty.`);
324
+ if (!isUsableExtension(extension)) {
325
+ throw new Error(
326
+ `Invalid extension '${value}' for grammar '${grammar}': use letters, digits and dashes, starting with a letter or digit.`
327
+ );
262
328
  }
263
329
  return extension;
264
330
  }
@@ -282,6 +348,11 @@ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boole
282
348
  if (extensions.length === 0) {
283
349
  throw new Error(`Invalid --extensions for grammar '${spec.name}': at least one extension is required.`);
284
350
  }
351
+ if (spec.languageId !== undefined && !/^[a-z0-9][a-z0-9-]*$/.test(spec.languageId)) {
352
+ throw new Error(
353
+ `Invalid --language-id '${spec.languageId}' for grammar '${spec.name}': use lowercase letters, digits and dashes, e.g. 'my-lang'.`
354
+ );
355
+ }
285
356
  return {
286
357
  grammar: spec.name,
287
358
  grammarId,
@@ -298,8 +369,9 @@ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boole
298
369
  *
299
370
  * With the `glsp` head on and nothing marked, a lone grammar is unambiguous and
300
371
  * 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.
372
+ * rather than guessed — the scaffolded state, storage and GModel factory are
373
+ * typed over the chosen grammar's model, so a wrong guess emits a diagram of
374
+ * the wrong model.
303
375
  */
304
376
  function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly InitHead[]): boolean[] {
305
377
  const marked = specs.map(spec => spec.diagram === true);
@@ -325,23 +397,34 @@ function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly Init
325
397
  );
326
398
  }
327
399
 
328
- /** Validate the head set: known names only, and `lsp` present. */
400
+ /** Validate the head set: known names only. */
329
401
  function resolveHeads(heads: readonly InitHead[] | undefined): readonly InitHead[] {
330
402
  const resolved = heads === undefined || heads.length === 0 ? DEFAULT_INIT_HEADS : heads;
331
403
  const unknown = resolved.filter(head => !isInitHead(head));
332
404
  if (unknown.length > 0) {
333
405
  throw new Error(`Unknown head(s) '${unknown.join(', ')}': expected one of ${INIT_HEADS.join(', ')}.`);
334
406
  }
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
407
  // De-duplicated and put back in composition order, so the order the heads
341
408
  // were named in does not change the emitted project.
342
409
  return INIT_HEADS.filter(head => resolved.includes(head));
343
410
  }
344
411
 
412
+ /** Validate the host set against the heads: known names only, and one of the heads each host edits through. */
413
+ function resolveHosts(hosts: readonly InitHost[], heads: readonly InitHead[]): readonly InitHost[] {
414
+ const unknown = hosts.filter(host => !isInitHost(host));
415
+ if (unknown.length > 0) {
416
+ throw new Error(`Unknown host(s) '${unknown.join(', ')}': expected one of ${INIT_HOSTS.join(', ')}.`);
417
+ }
418
+ for (const host of hosts) {
419
+ const editing = editingHeads(host);
420
+ if (!editing.some(head => heads.includes(head))) {
421
+ const needed = editing.length === 1 ? `the '${editing[0]}' head` : `one of the '${editing.join(', ')}' heads`;
422
+ throw new Error(`--hosts ${host} needs ${needed}, but the head set is '${heads.join(',')}'.`);
423
+ }
424
+ }
425
+ return INIT_HOSTS.filter(host => hosts.includes(host));
426
+ }
427
+
345
428
  /**
346
429
  * Reject the collisions that would otherwise surface as a confusing generator
347
430
  * or runtime failure rather than as a bad invocation.
@@ -368,15 +451,18 @@ function assertNoCollisions(grammars: readonly InitGrammarNames[]): void {
368
451
  }
369
452
  byLanguageId.set(grammar.languageId, grammar.grammar);
370
453
 
454
+ // Case-folded: a starter file is named by extension, and `nodes.Foo` and
455
+ // `nodes.foo` are one file on a case-insensitive file system.
371
456
  for (const extension of grammar.extensions) {
372
- const clashingExtension = byExtension.get(extension);
457
+ const key = extension.toLowerCase();
458
+ const clashingExtension = byExtension.get(key);
373
459
  if (clashingExtension !== undefined) {
374
460
  throw new Error(
375
461
  `File extension '.${extension}' is claimed by both '${clashingExtension}' and '${grammar.grammar}'. ` +
376
462
  'Langium routes documents by extension, so each grammar needs its own.'
377
463
  );
378
464
  }
379
- byExtension.set(extension, grammar.grammar);
465
+ byExtension.set(key, grammar.grammar);
380
466
  }
381
467
  }
382
468
  }
@@ -393,7 +479,8 @@ export function resolveInitComposition(
393
479
  name: string,
394
480
  grammars: readonly InitGrammarSpec[] = [],
395
481
  heads?: readonly InitHead[],
396
- packaging: InitPackaging = STANDALONE_PACKAGING
482
+ packaging: InitPackaging = STANDALONE_PACKAGING,
483
+ hosts: readonly InitHost[] = []
397
484
  ): InitComposition {
398
485
  if (!isPascalIdentifier(name)) {
399
486
  throw new Error(
@@ -405,11 +492,20 @@ export function resolveInitComposition(
405
492
  throw new Error(`Invalid --scope '${packaging.scope}': an npm scope starts with '@', e.g. '@acme'.`);
406
493
  }
407
494
  const resolvedHeads = resolveHeads(heads);
495
+ const resolvedHosts = resolveHosts(hosts, resolvedHeads);
408
496
  const specs = grammars.length > 0 ? grammars : [{ name }];
409
497
  const diagrams = resolveDiagrams(specs, resolvedHeads);
410
498
  const resolved = specs.map((spec, index) => resolveGrammar(spec, projectId, specs.length > 1, diagrams[index]));
411
499
  assertNoCollisions(resolved);
412
- return { name, projectId, heads: resolvedHeads, grammars: resolved, packaging, frameworkVersion: readFrameworkVersion() };
500
+ return {
501
+ name,
502
+ projectId,
503
+ heads: resolvedHeads,
504
+ hosts: resolvedHosts,
505
+ grammars: resolved,
506
+ packaging,
507
+ frameworkVersion: readFrameworkVersion()
508
+ };
413
509
  }
414
510
 
415
511
  /**
@@ -432,6 +528,8 @@ export interface InitPackagingOptions {
432
528
  readonly scope?: string;
433
529
  readonly public?: boolean;
434
530
  readonly probe?: WorkspaceProbe;
531
+ /** The npm running `init`. Defaults to the one `npm_config_user_agent` names, which `npx` and `npm run` set. */
532
+ readonly npmVersion?: string;
435
533
  }
436
534
 
437
535
  /**
@@ -466,24 +564,66 @@ export function resolveInitPackaging(targetDir: string, options: InitPackagingOp
466
564
  baseTsconfig: detection.baseTsconfig,
467
565
  baseCompilerOptions: detection.baseCompilerOptions,
468
566
  printWidth: detection.printWidth,
469
- eslintConfig: detection.eslintConfig
567
+ eslintConfig: detection.eslintConfig,
568
+ oxlintConfig: detection.oxlintConfig,
569
+ packageManagerNpm: detection.packageManagerNpm,
570
+ npmVersion: options.npmVersion ?? parseNpmVersion(process.env.npm_config_user_agent)
470
571
  }
471
572
  };
472
573
  }
473
574
 
575
+ /**
576
+ * Format a standalone scaffold as its own `format` script would, with the
577
+ * `oxfmt` its root pins and the config it emits. A template is written at one
578
+ * length of the names its tokens stand for, so only a formatter run lets every
579
+ * project name pass `format:check`. Only the written files, because `--force`
580
+ * scaffolds beside files that are not ours to restyle. A failure leaves the
581
+ * files as written, which build all the same, and says so.
582
+ */
583
+ function formatScaffold(targetDir: string, files: readonly InitFile[], write: (line: string) => void): void {
584
+ let reason: string | undefined;
585
+ try {
586
+ // Not `findPackageJSON`: Node 22.13, which `engines` admits, lacks it.
587
+ const manifestPath = createRequire(import.meta.url).resolve('oxfmt/package.json');
588
+ const bin = (JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { bin: Record<string, string> }).bin.oxfmt;
589
+ const args = [path.join(path.dirname(manifestPath), bin), ...files.map(file => file.path)];
590
+ const result = spawnSync(process.execPath, args, { cwd: targetDir, encoding: 'utf-8' });
591
+ if (result.status !== 0) {
592
+ reason = result.error?.message ?? `${result.stdout}${result.stderr}${result.signal === null ? '' : `killed by ${result.signal}`}`;
593
+ }
594
+ } catch (error: unknown) {
595
+ reason = error instanceof Error ? error.message : String(error);
596
+ }
597
+ if (reason !== undefined) {
598
+ write(`Warning: oxfmt did not format the scaffold, so run \`npm run format\` after \`npm install\`:\n${reason.trim()}\n`);
599
+ }
600
+ }
601
+
602
+ export function validateInitInstallation(options: Pick<InitCommandOptions, 'install' | 'monorepo'>): void {
603
+ if (options.install && options.monorepo) {
604
+ throw new Error('--install cannot be combined with --monorepo: update the parent workspace manifest before running npm install.');
605
+ }
606
+ }
607
+
474
608
  /**
475
609
  * Scaffold a new Hydranium language project end-to-end: one starter grammar per
476
610
  * `--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`.
611
+ * `--heads`, `langium-config.json`, `package.json`, and scripts. Stays offline
612
+ * unless `install` opts into npm and its lifecycle scripts. Building remains a
613
+ * next step. Refuses a non-empty target directory unless `force`.
481
614
  */
482
615
  export function runInit(options: InitCommandOptions): void {
616
+ validateInitInstallation(options);
483
617
  const write = options.write ?? ((line: string) => process.stdout.write(line));
484
618
  const targetDir = path.resolve(options.targetDir);
485
619
  const detection = options.monorepo ? detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe()) : undefined;
486
- const composition = resolveInitComposition(options.name, options.grammars, options.heads, resolveInitPackaging(targetDir, options));
620
+ const composition = resolveInitComposition(
621
+ options.name,
622
+ options.grammars,
623
+ options.heads,
624
+ resolveInitPackaging(targetDir, options),
625
+ options.hosts
626
+ );
487
627
 
488
628
  if (!options.force && isNonEmptyDir(targetDir)) {
489
629
  throw new Error(`Target directory '${targetDir}' is not empty. Pass --force to scaffold into it anyway.`);
@@ -498,56 +638,162 @@ export function runInit(options: InitCommandOptions): void {
498
638
  fs.mkdirSync(path.dirname(absolute), { recursive: true });
499
639
  fs.writeFileSync(absolute, file.content, 'utf-8');
500
640
  }
641
+ if (composition.packaging.workspace === undefined) {
642
+ formatScaffold(targetDir, files, write);
643
+ }
501
644
  }
502
645
 
503
646
  const grammarList = composition.grammars.map(grammar => (grammar.diagram ? `${grammar.grammar} (diagram)` : grammar.grammar)).join(', ');
504
647
  write(
505
648
  `Scaffolded ${composition.name} (${files.length} files, heads: ${composition.heads.join(',')}, ` +
649
+ (composition.hosts.length > 0 ? `hosts: ${composition.hosts.join(',')}, ` : '') +
506
650
  `${composition.grammars.length} grammar(s): ${grammarList}) into ${targetDir}\n`
507
651
  );
508
652
  if (detection !== undefined) {
509
653
  write('\n');
510
654
  write(`Workspace root: ${detection.rootDir}\n`);
511
655
  if (detection.baseTsconfig !== undefined) {
512
- write(` tsconfig.json extends ${detection.baseTsconfig}, minus the options it inherits unchanged.\n`);
656
+ write(
657
+ ` Each library package's tsconfig.json extends the root's ${path.posix.basename(detection.baseTsconfig)}, minus the options it inherits unchanged.\n`
658
+ );
659
+ if (composition.hosts.length > 0) {
660
+ // A host's projects compile its page, worker and tests apart, each under options of its own.
661
+ const members = initMembers(composition.heads, composition.hosts);
662
+ const extensions = [
663
+ ...(members.includes('vscode-servers') ? ["the VS Code servers extension's"] : []),
664
+ ...(members.includes('theia') ? ["the Theia extension's"] : [])
665
+ ];
666
+ const also = extensions.length === 0 ? '' : `, as ${extensions.join(' and ')} ${extensions.length === 1 ? 'does' : 'do'}`;
667
+ write(` Each host's tsconfigs stand alone${also}.\n`);
668
+ }
513
669
  } else {
514
- write(' No root tsconfig carries compilerOptions, so the emitted one stands alone.\n');
670
+ write(" No root tsconfig carries compilerOptions, so each package's stands alone.\n");
515
671
  }
516
- write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
517
672
  if (detection.printWidth !== undefined) {
518
- write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s prettier config.\n`);
673
+ write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s formatter config.\n`);
519
674
  }
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`);
675
+ if (detection.oxlintConfig !== undefined || detection.eslintConfig !== undefined) {
676
+ write(
677
+ ` Each package got a lint script — ${detection.oxlintConfig ?? detection.eslintConfig} says this repo lints. Adjust them if your invocation differs.\n`
678
+ );
522
679
  }
523
680
  // Printed rather than applied: `init` writes inside the target directory
524
681
  // and nowhere else, so a scaffold can never surprise-edit a manifest that
525
682
  // 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`);
683
+ const placements = memberPlacements(detection, initMembers(composition.heads, composition.hosts));
684
+ const uncovered = placements.filter(placement => placement.coveredBy === undefined);
685
+ if (uncovered.length > 0) {
686
+ write(` Add ${uncovered.length === 1 ? 'this' : 'these'} to "workspaces" in ${path.join(detection.rootDir, 'package.json')}:\n`);
687
+ write(`${uncovered.map(placement => ` "${placement.path}"`).join(',\n')}\n`);
688
+ }
689
+ for (const placement of placements.filter(candidate => candidate.coveredBy !== undefined)) {
690
+ write(` ${placement.path} is already covered by the "${placement.coveredBy}" workspaces entry.\n`);
531
691
  }
692
+ const pins = workspaceRootPins(composition).map(([name, version]) => ` "${name}": "${version}"`);
693
+ write(
694
+ ` Add these to "devDependencies" in ${path.join(detection.rootDir, 'package.json')}, so another member's version of one ` +
695
+ `cannot displace the copy these packages share with the framework:\n${pins.join(',\n')}\n`
696
+ );
532
697
  if (composition.packaging.scope === undefined && detection.scope !== undefined) {
533
698
  write(` Sibling packages all use the "${detection.scope}" scope; pass --scope ${detection.scope} to match them.\n`);
534
699
  }
535
700
  }
701
+ if (options.install) {
702
+ write('\nRunning npm install...\n');
703
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
704
+ // --prefix also changes npm's default globalconfig. Resolve it first without walking into an enclosing workspace.
705
+ const config = spawnSync(npm, ['config', 'get', 'globalconfig', '--workspaces=false'], {
706
+ cwd: targetDir,
707
+ encoding: 'utf8',
708
+ stdio: ['ignore', 'pipe', 'inherit'],
709
+ shell: process.platform === 'win32'
710
+ });
711
+ const globalconfig = config.stdout?.trim();
712
+ if (config.error || config.signal || config.status !== 0 || !globalconfig) {
713
+ const reason = config.error?.message ?? (config.signal ? `signal ${config.signal}` : `exit code ${config.status}`);
714
+ throw new Error(
715
+ `Could not read npm global configuration (${reason}). Scaffold kept at ${targetDir}; installation was not started.`
716
+ );
717
+ }
718
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'npm_config_globalconfig'));
719
+ env.npm_config_globalconfig = globalconfig;
720
+ // An explicit prefix keeps npm from selecting an enclosing workspace instead of this standalone root.
721
+ // Windows resolves npm through a command shim; only fixed arguments reach the shell.
722
+ const result = spawnSync(npm, ['--prefix', '.', 'install'], {
723
+ cwd: targetDir,
724
+ stdio: 'inherit',
725
+ shell: process.platform === 'win32',
726
+ env
727
+ });
728
+ if (result.error || result.signal || result.status !== 0) {
729
+ const reason = result.error?.message ?? (result.signal ? `signal ${result.signal}` : `exit code ${result.status}`);
730
+ throw new Error(
731
+ `npm install failed (${reason}). Scaffold kept at ${targetDir}. ` +
732
+ `Set npm_config_globalconfig to ${globalconfig} to preserve npm's global configuration. Run npm --prefix . install there to retry.`
733
+ );
734
+ }
735
+ }
536
736
  write('\n');
537
737
  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');
738
+ // A workspace member's project has no manifest of its own, so its steps run
739
+ // from the workspace root and name the server by `--prefix`.
740
+ const server = detection === undefined ? 'server' : `${detection.targetPath}/server`;
741
+ if (detection === undefined) {
742
+ write(` cd ${options.targetDir}\n`);
743
+ // oxfmt and oxlint skip whatever an enclosing repository ignores, this
744
+ // folder included: https://github.com/oxc-project/oxc/issues/25603
745
+ if (spawnSync('git', ['check-ignore', '-q', targetDir], { cwd: targetDir }).status === 0) {
746
+ write(' git init # an enclosing repository ignores this folder, so lint and format find no file until then\n');
747
+ }
748
+ if (!options.install) {
749
+ write(' npm install\n');
750
+ }
751
+ write(' npm run build # langium generate + tsc, every package\n');
752
+ write(' npm test # the server tests\n');
753
+ write(' npm run watch # rebuild the members on every change; watch:<host> adds a host\n');
754
+ } else {
755
+ write(` cd ${detection.rootDir}\n`);
756
+ write(' npm install\n');
757
+ write(` npm --prefix ${server} run build # langium generate + tsc, protocol included\n`);
758
+ // The server's build reaches protocol through its project reference and
759
+ // nothing else, so each further member is built on its own.
760
+ for (const member of initMembers(composition.heads, composition.hosts).filter(name => name !== 'protocol' && name !== 'server')) {
761
+ write(` npm --prefix ${detection.targetPath}/${member} run build\n`);
762
+ }
763
+ write(` npm --prefix ${server} test\n`);
764
+ }
548
765
  // `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
549
766
  // 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');
767
+ write(
768
+ detection === undefined
769
+ ? ' npx hydranium-cli reflect\n'
770
+ : ` npx hydranium-cli reflect --services ./${server}/lib/node/services.js\n`
771
+ );
772
+ const hostDir = (host: InitHost): string => (detection === undefined ? `host-${host}` : `${detection.targetPath}/host-${host}`);
773
+ // A standalone root runs a host as `start:<host>`. A surrounding workspace's root is not ours to add to.
774
+ const start = (host: InitHost): string => (detection === undefined ? `npm run start:${host}` : `npm --prefix ${hostDir(host)} start`);
775
+ if (composition.hosts.includes('browser')) {
776
+ const browser = hostDir('browser');
777
+ write(` ${start('browser')} # serve the page at http://localhost:4937, Ctrl+C to stop\n`);
778
+ // The smoke test serves the page itself, on the same port.
779
+ write(` npm --prefix ${browser} run test:e2e:install && npm --prefix ${browser} run test:e2e # with start stopped\n`);
780
+ }
781
+ if (composition.hosts.includes('vscode')) {
782
+ const vscode = hostDir('vscode');
783
+ write(` ${start('vscode')} # VS Code with the extension, on the workspace (needs \`code\` on the PATH)\n`);
784
+ write(` npm --prefix ${vscode} run test:e2e:install && npm --prefix ${vscode} run test:e2e\n`);
785
+ }
786
+ if (composition.hosts.includes('theia-browser')) {
787
+ const theia = hostDir('theia-browser');
788
+ write(` ${start('theia-browser')} # Theia at http://localhost:3000 on the workspace, Ctrl+C to stop\n`);
789
+ // The smoke test starts its own backend, on a port of its own.
790
+ write(` npm --prefix ${theia} run test:e2e:install && npm --prefix ${theia} run test:e2e\n`);
791
+ }
792
+ if (composition.hosts.includes('theia-electron')) {
793
+ const electron = hostDir('theia-electron');
794
+ write(` ${start('theia-electron')} # the Theia desktop application on the workspace\n`);
795
+ write(` npm --prefix ${electron} run test:e2e\n`);
796
+ }
551
797
  write('\n');
552
798
  write('The emitted sources carry no license header — run your own header tool if\n');
553
799
  write('your repo gates them. See the README section on that.\n');
@@ -0,0 +1,17 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 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
+ * Kebab-case a PascalCase/camelCase name. Shared by `init`, which derives a
12
+ * grammar's id from it, and the languages emitter, whose constant names have
13
+ * to split a grammar name the same way.
14
+ */
15
+ export function kebab(name: string): string {
16
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
17
+ }