@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,74 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import {
11
+ assertRequired,
12
+ exitWithUsage,
13
+ helpRequested,
14
+ parseFlagOptions,
15
+ parseServerSpawnOptions,
16
+ printHelp,
17
+ SERVER_SPAWN_VALUE_FLAGS,
18
+ type UsageError
19
+ } from './harness-args.js';
20
+ import { runWatch, wireSigintAbort, type WatchCommandOptions } from './watch.js';
21
+
22
+ const OWN_FLAGS = ['--uri', '--client-id'] as const;
23
+
24
+ /**
25
+ * Every flag `watch` accepts, derived from its own list plus the shared spawn
26
+ * flags, so the list cannot claim a flag the parser would reject.
27
+ */
28
+ export const WATCH_FLAGS: readonly string[] = [...SERVER_SPAWN_VALUE_FLAGS, ...OWN_FLAGS];
29
+
30
+ /**
31
+ * The subset that takes a value, so `--help` in a value position reads as data.
32
+ * Every flag in this family takes one, so the two lists coincide.
33
+ */
34
+ export const WATCH_VALUE_FLAGS: readonly string[] = WATCH_FLAGS;
35
+
36
+ /** The `--help` text, as data, held to {@link WATCH_FLAGS} by a test. */
37
+ export const WATCH_HELP: readonly string[] = [
38
+ 'Usage: hydranium-cli watch --server "<cmd>..." --uri <uri> [--client-id <id>] [--cwd <dir>] [--log-level <level>]',
39
+ '',
40
+ 'Subscribe to document updates at `<uri>` and print events as newline-delimited',
41
+ 'JSON. Long-running — exits cleanly on SIGINT (Ctrl-C).',
42
+ '',
43
+ 'Options:',
44
+ ' --server "<cmd>" Command-line for the data-server subprocess (required).',
45
+ ' --uri <uri> Document URI (required).',
46
+ ' --client-id <id> Subscriber identity. Default: hydranium-cli.',
47
+ ' --cwd <dir> Working directory for the spawned child. Default: cwd.',
48
+ ' --log-level <lvl> Log threshold for the spawned server (off|error|warn|info|debug|trace).'
49
+ ];
50
+
51
+ /** Parse the argv; the abort signal is the entry point's to supply, not the command line's. */
52
+ export function parseWatchArgs(args: string[], onError: UsageError = exitWithUsage): WatchCommandOptions {
53
+ const { serverCommand, serverArgs, cwd, logLevel, extra } = parseServerSpawnOptions(args, 'watch', onError);
54
+ const { uri, clientId } = parseFlagOptions(extra, 'watch', OWN_FLAGS, onError);
55
+ return {
56
+ serverCommand,
57
+ serverArgs,
58
+ cwd,
59
+ logLevel,
60
+ uri: assertRequired(uri, '--uri', 'watch', onError),
61
+ clientId
62
+ };
63
+ }
64
+
65
+ export function runWatchCommand(args: string[]): Promise<void> {
66
+ if (helpRequested(args, WATCH_VALUE_FLAGS)) {
67
+ printHelp(WATCH_HELP);
68
+ return Promise.resolve();
69
+ }
70
+ const options = parseWatchArgs(args);
71
+ const controller = new AbortController();
72
+ const unwire = wireSigintAbort(controller);
73
+ return runWatch({ ...options, signal: controller.signal }).finally(() => unwire());
74
+ }
@@ -0,0 +1,185 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import type { LogThreshold, TransferElement } from '@hydranium/protocol';
11
+ import type { DataClientProtocol, DataServerProtocol, TransferDocumentUpdatedEvent } from '@hydranium/protocol/data';
12
+ import { logLevelEnv } from '../log-level.js';
13
+ import { spawnDataServer } from '../spawn-data-server.js';
14
+
15
+ /**
16
+ * Options for the {@link runWatch} subcommand. Subscribes to document
17
+ * updates at `uri` and writes each event as one JSON line. Long-running
18
+ * — exits on SIGINT (Ctrl-C) or when `signal` aborts. Each event is
19
+ * line-flushable for `jq` / `tee` pipelines.
20
+ *
21
+ * `clientId` defaults to `'hydranium-cli'`. `signal` is the test seam
22
+ * — production CLI wires SIGINT directly. `__handleForTest` bypasses
23
+ * the spawn step for unit tests.
24
+ */
25
+ export interface WatchCommandOptions {
26
+ readonly serverCommand: string;
27
+ readonly serverArgs?: readonly string[];
28
+ readonly cwd?: string;
29
+ /** Log threshold for the spawned server, set on its `HYDRANIUM_LOG_LEVEL` env. */
30
+ readonly logLevel?: LogThreshold;
31
+ readonly uri: string;
32
+ readonly clientId?: string;
33
+ readonly write?: (line: string) => void;
34
+ /**
35
+ * AbortSignal that cancels the subscription loop. Production wires
36
+ * SIGINT via {@link wireSigintAbort}; tests pass a controlled signal
37
+ * to assert deterministic teardown.
38
+ */
39
+ readonly signal?: AbortSignal;
40
+ /**
41
+ * Test-only injection — supplies the typed server proxy along with a
42
+ * "fire event" hook that runWatch calls during construction to wire
43
+ * its local client. Production CLI never passes this; subcommand
44
+ * unit tests do.
45
+ */
46
+ readonly __handleForTest?: WatchTestHandle;
47
+ }
48
+
49
+ /**
50
+ * Test seam for {@link runWatch}. Wraps the bits of `DataServerHandle`
51
+ * the watch loop actually uses — the typed server proxy plus a
52
+ * `bindClient` hook the test fixture uses to capture the local
53
+ * `DataClientProtocol` so it can fire events at it.
54
+ */
55
+ export interface WatchTestHandle {
56
+ readonly server: Pick<DataServerProtocol<TransferElement>, 'watchModelDocument' | 'unwatchModelDocument'>;
57
+ /**
58
+ * Called once during {@link runWatch} startup with the watch loop's
59
+ * local {@link DataClientProtocol}. Test fixtures stash the reference
60
+ * and invoke `client.onDocumentUpdated(event)` to simulate the
61
+ * data-server's notification.
62
+ */
63
+ bindClient(client: DataClientProtocol<TransferElement>): void;
64
+ /** Mirror of `DataServerHandle.shutdown` — tests can stub as a no-op. */
65
+ shutdown(timeoutMs?: number): Promise<void>;
66
+ /**
67
+ * Mirror of `DataServerHandle.whenTerminated`. Omit it for a fixture with no
68
+ * child to lose; supply it to drive the premature-death race, which is the
69
+ * only way a test reaches {@link runWatch}'s shutdown filtering.
70
+ */
71
+ readonly whenTerminated?: Promise<never>;
72
+ }
73
+
74
+ /**
75
+ * Wait for the handle's premature-death rejection, and swallow it once the watch
76
+ * is being torn down.
77
+ *
78
+ * `watch` ends on SIGINT, which a shell delivers to the whole process group, so
79
+ * the child dies of the same keystroke. Its `unwatchModelDocument` round trip can
80
+ * then never be answered, leaving the loop pending and the child's exit the only
81
+ * settled outcome — so unfiltered, every clean Ctrl-C is reported as
82
+ * `exited before the request completed (… signal=SIGINT)`. Resolving instead
83
+ * also settles the race the loop can no longer settle.
84
+ *
85
+ * The check waits a turn of the event loop rather than reading `aborted` in the
86
+ * rejection's own turn: the child's death and this process's own SIGINT are
87
+ * delivered together and their order is the kernel's to choose. A genuine
88
+ * mid-watch crash pays that one turn before it is reported.
89
+ */
90
+ async function resolveOnceShuttingDown(whenTerminated: Promise<never> | undefined, signal: AbortSignal | undefined): Promise<void> {
91
+ if (!whenTerminated) {
92
+ return new Promise<void>(() => undefined);
93
+ }
94
+ try {
95
+ await whenTerminated;
96
+ } catch (err: unknown) {
97
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
98
+ if (signal?.aborted) {
99
+ return;
100
+ }
101
+ throw err;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Wire SIGINT to abort the given controller. Returns a function that
107
+ * un-wires the listener — call it once the subcommand exits so future
108
+ * SIGINTs don't trigger an already-aborted controller.
109
+ */
110
+ export function wireSigintAbort(controller: AbortController): () => void {
111
+ const handler = (): void => controller.abort();
112
+ process.once('SIGINT', handler);
113
+ return () => process.removeListener('SIGINT', handler);
114
+ }
115
+
116
+ export async function runWatch(options: WatchCommandOptions): Promise<void> {
117
+ const write = options.write ?? ((line: string) => process.stdout.write(line));
118
+ const clientId = options.clientId ?? 'hydranium-cli';
119
+
120
+ // Local client: filters by URI (so multiple watchers on the same wire
121
+ // don't cross-pollinate) and writes each event as one JSON line.
122
+ // `onProjectsChanged` and `onDocumentSaved` are no-ops here — `watch`
123
+ // streams build-phase update events. Project lifecycle and persistence
124
+ // events are out of band for the CLI's per-URI update view.
125
+ const localClient: DataClientProtocol<TransferElement> = {
126
+ onDocumentUpdated(event: TransferDocumentUpdatedEvent<TransferElement>): void {
127
+ if (event.document.uri !== options.uri) {
128
+ return;
129
+ }
130
+ write(`${JSON.stringify(event)}\n`);
131
+ },
132
+ onDocumentSaved(): void {
133
+ // Persistence is out of band for the per-URI update view.
134
+ },
135
+ onProjectsChanged(): void {
136
+ // Project lifecycle is out of band for the per-URI update view.
137
+ }
138
+ };
139
+
140
+ // One body for both paths: a test handle that returned before the race would
141
+ // leave the premature-death branch it stands in for uncoverable.
142
+ const handle =
143
+ options.__handleForTest ??
144
+ spawnDataServer<TransferElement>(
145
+ {
146
+ command: options.serverCommand,
147
+ args: options.serverArgs,
148
+ cwd: options.cwd,
149
+ env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
150
+ },
151
+ localClient
152
+ );
153
+ options.__handleForTest?.bindClient(localClient);
154
+ try {
155
+ // Race the subscription loop against the child's premature death so a mid-watch
156
+ // server crash exits with a clear message instead of hanging until Ctrl-C. Guard
157
+ // the loop's late rejection (an unwatch write to a dead stream) so it does not
158
+ // surface as an unhandled rejection when the death wins the race.
159
+ const loop = runWatchOnce(handle.server, options.uri, clientId, options.signal);
160
+ void loop.catch(() => undefined);
161
+ await Promise.race([loop, resolveOnceShuttingDown(handle.whenTerminated, options.signal)]);
162
+ } finally {
163
+ await handle.shutdown();
164
+ }
165
+ }
166
+
167
+ async function runWatchOnce(
168
+ server: Pick<DataServerProtocol<TransferElement>, 'watchModelDocument' | 'unwatchModelDocument'>,
169
+ uri: string,
170
+ clientId: string,
171
+ signal: AbortSignal | undefined
172
+ ): Promise<void> {
173
+ await server.watchModelDocument({ uri, clientId });
174
+ try {
175
+ await new Promise<void>(resolve => {
176
+ if (signal?.aborted) {
177
+ resolve();
178
+ return;
179
+ }
180
+ signal?.addEventListener('abort', () => resolve(), { once: true });
181
+ });
182
+ } finally {
183
+ await server.unwatchModelDocument({ uri, clientId });
184
+ }
185
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
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
+ export * from './commands/generate-transfer-model.js';
11
+ export * from './commands/projects.js';
12
+ export * from './commands/query.js';
13
+ export * from './commands/save.js';
14
+ export * from './commands/watch.js';
15
+ export * from './package-name.js';
16
+ export * from './spawn-data-server.js';
@@ -0,0 +1,49 @@
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 { DEFAULT_LOG_LEVEL_ENV, LEVEL_ORDER, parseLogLevel, type LogThreshold } from '@hydranium/protocol';
11
+
12
+ /**
13
+ * Validate a raw `--log-level` argument into a {@link LogThreshold}. Throws on
14
+ * an unrecognised value so the CLI's top-level `catch` surfaces a clean error
15
+ * and exits non-zero (mirroring how the other option parsers fail fast). Use
16
+ * for the argv-supplied value; the resolved threshold is handed to
17
+ * {@link logLevelEnv} to target the spawned server.
18
+ */
19
+ export function parseLogLevelOption(raw: string): LogThreshold {
20
+ const level = parseLogLevel(raw);
21
+ if (!level) {
22
+ throw new Error(`Invalid --log-level: ${raw} (expected one of: ${Object.keys(LEVEL_ORDER).join(', ')})`);
23
+ }
24
+ return level;
25
+ }
26
+
27
+ /**
28
+ * Build the environment override that targets *only the spawned server's* log
29
+ * threshold. Passed to `spawnDataServer` as `env`, it sets the same
30
+ * `HYDRANIUM_LOG_LEVEL` variable the server reads at startup — the CLI's own
31
+ * logging is unaffected, unlike an ambient `HYDRANIUM_LOG_LEVEL=…` that the
32
+ * child would inherit alongside the parent. The server owns *how* log level
33
+ * works; this only routes the CLI's request onto the channel it already reads.
34
+ *
35
+ * **The variable is read by `LspLogger`'s constructor**, which is bound on the
36
+ * `Logger` slot by `createLspServerSharedModule`. A server that overrides that
37
+ * slot with a logger of its own therefore decides for itself whether this flag
38
+ * means anything — which is the intended seam, not a gap. A server composing no
39
+ * head module at all binds `@hydranium/core`'s `NoopLogger` and stays silent
40
+ * regardless, so the flag is inert against it.
41
+ *
42
+ * Output lands on the spawned server's **stderr**, which
43
+ * `spawnDataServer` inherits to the user by default. It deliberately does
44
+ * not go to stdout: that is the JSON-RPC channel, so a log line there would
45
+ * corrupt the very request this flag was set to diagnose.
46
+ */
47
+ export function logLevelEnv(level: LogThreshold): Record<string, string> {
48
+ return { [DEFAULT_LOG_LEVEL_ENV]: level };
49
+ }
@@ -0,0 +1,39 @@
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 { toPascal } from '@hydranium/protocol';
11
+
12
+ /**
13
+ * Validates against the npm package-name grammar (scoped or unscoped,
14
+ * lowercase, etc.).
15
+ *
16
+ * Published on this barrel with no caller inside the framework: it is
17
+ * scaffolding vocabulary for a generator that composes package names of its
18
+ * own, which is a job an adopter does in its own repository. It lives on the
19
+ * CLI rather than on the wire-contract package because a package name has no
20
+ * wire meaning and a client bundle should not carry the pattern.
21
+ */
22
+ export const NPM_PACKAGE_NAME_REGEX = /^(?:(?:@(?:[a-z0-9-*~][a-z0-9-*._~]*)?\/[a-z0-9-._~])|[a-z0-9-~])[a-z0-9-._~]*$/;
23
+
24
+ /**
25
+ * Convert an npm package name to a Pascal-case identifier. The optional scope
26
+ * prefix is dropped; remaining segments separated by `-`, `.`, or `~` are
27
+ * Pascal-cased and concatenated.
28
+ *
29
+ * `'@my-org/foo-bar'` → `'FooBar'`
30
+ *
31
+ * The inverse direction of the `kebab` derivation the scaffolder itself runs:
32
+ * that one starts from a PascalCase project name and produces the package id,
33
+ * this one recovers an identifier from a package name that already exists.
34
+ * Same outside-reader constraint as {@link NPM_PACKAGE_NAME_REGEX}.
35
+ */
36
+ export function packageNameToId(input: string): string {
37
+ const unscoped = input.split('/').at(-1)!;
38
+ return unscoped.split(/[~.-]/).map(toPascal).join('');
39
+ }
@@ -0,0 +1,247 @@
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 { createRpcProxy, type TransferDiagnostic, type TransferElement } from '@hydranium/protocol';
11
+ import {
12
+ DATA_CLIENT_PROTOCOL_METHODS,
13
+ DATA_SERVER_WIRE_PREFIX,
14
+ type DataClientProtocol,
15
+ type DataServerProtocol
16
+ } from '@hydranium/protocol/data';
17
+ import { type ChildProcess, spawn } from 'node:child_process';
18
+ import { StreamMessageReader, StreamMessageWriter, createMessageConnection, type MessageConnection } from 'vscode-jsonrpc/node';
19
+
20
+ /**
21
+ * Options for {@link spawnDataServer}. `command` is the binary or script
22
+ * path; `args` are the command-line arguments passed to it. The adopter's
23
+ * server is expected to bootstrap a `DataServer` against the supplied
24
+ * MessageConnection and call `connection.listen()` so its stdin/stdout
25
+ * carry the JSON-RPC stream.
26
+ *
27
+ * `shutdown()` disposes the JSON-RPC connection and waits for the child
28
+ * to exit with a configurable timeout. Subcommands that take a SIGINT
29
+ * (e.g. `watch`) call shutdown on the cleanup path so the child doesn't
30
+ * stay orphaned.
31
+ */
32
+ export interface SpawnDataServerOptions {
33
+ readonly command: string;
34
+ readonly args?: readonly string[];
35
+ /**
36
+ * Working directory for the spawned child. Default: current cwd.
37
+ * Useful when the child resolves model files relative to a workspace
38
+ * root that differs from where the CLI was invoked.
39
+ */
40
+ readonly cwd?: string;
41
+ /**
42
+ * Additional environment variables for the child. Merged on top of
43
+ * `process.env`. Default: no overrides.
44
+ */
45
+ readonly env?: Readonly<Record<string, string | undefined>>;
46
+ /**
47
+ * Where to pipe the child's stderr. Default: inherited from the
48
+ * parent (visible to the user). Set to `'ignore'` for quiet
49
+ * scripted use; set to `'pipe'` to capture for assertion in tests.
50
+ */
51
+ readonly stderr?: 'inherit' | 'ignore' | 'pipe';
52
+ }
53
+
54
+ /**
55
+ * Lifecycle handle for a spawned data-server. Owns the child process,
56
+ * the JSON-RPC connection, and the typed protocol proxy. Always pair
57
+ * {@link spawnDataServer} with {@link DataServerHandle.shutdown} (in a
58
+ * `try`/`finally` or equivalent) so the child doesn't outlive the CLI
59
+ * invocation.
60
+ */
61
+ export interface DataServerHandle<TTransfer extends TransferElement, TDiagnostic extends TransferDiagnostic = TransferDiagnostic> {
62
+ /** Typed server proxy — the consumer-facing surface for protocol calls. */
63
+ readonly server: DataServerProtocol<TTransfer, TDiagnostic>;
64
+ /** Raw child process — for stderr piping in tests; do not write to stdin/stdout directly. */
65
+ readonly child: ChildProcess;
66
+ /** vscode-jsonrpc connection — for callers wanting fine-grained lifecycle control. */
67
+ readonly connection: MessageConnection;
68
+ /**
69
+ * Rejects when the child fails to start (e.g. `ENOENT` for a missing
70
+ * command) or exits before {@link DataServerHandle.shutdown} is
71
+ * requested — i.e. a premature death that would otherwise leave an
72
+ * in-flight RPC hanging forever. Never resolves. Race a protocol call
73
+ * against it (see {@link withDataServer}) to fail fast with a clear
74
+ * message instead of hanging or crashing on an unhandled `'error'`
75
+ * event. Always internally handled, so ignoring it never triggers an
76
+ * unhandled-rejection crash.
77
+ */
78
+ readonly whenTerminated: Promise<never>;
79
+ /**
80
+ * Tear down the connection + wait for the child to exit. Resolves
81
+ * once the child has terminated (gracefully or via SIGTERM). Safe
82
+ * to call multiple times.
83
+ */
84
+ shutdown(timeoutMs?: number): Promise<void>;
85
+ }
86
+
87
+ /**
88
+ * Spawn a data-server subprocess and wire a typed proxy over its
89
+ * stdio JSON-RPC. The function returns once the connection is
90
+ * listening; protocol calls are queued behind the underlying
91
+ * MessageConnection's ordering guarantees.
92
+ *
93
+ * Supplying `localClient` registers its `DataClientProtocol` methods as
94
+ * inbound-notification handlers on the connection — the data-server's
95
+ * `data-server/onDocumentUpdated` notifications dispatch to
96
+ * `localClient.onDocumentUpdated`. Subcommands like `watch` use this to
97
+ * stream events; subcommands like `projects` / `query` / `save` pass
98
+ * no localClient.
99
+ */
100
+ export function spawnDataServer<TTransfer extends TransferElement, TDiagnostic extends TransferDiagnostic = TransferDiagnostic>(
101
+ options: SpawnDataServerOptions,
102
+ localClient?: DataClientProtocol<TTransfer, TDiagnostic>
103
+ ): DataServerHandle<TTransfer, TDiagnostic> {
104
+ const child = spawn(options.command, [...(options.args ?? [])], {
105
+ cwd: options.cwd,
106
+ env: options.env ? { ...process.env, ...options.env } : process.env,
107
+ stdio: ['pipe', 'pipe', options.stderr ?? 'inherit']
108
+ });
109
+
110
+ if (!child.stdin || !child.stdout) {
111
+ // Mirrors the synchronous failure-to-create-pipes case Node surfaces
112
+ // when stdio isn't 'pipe' on either end. We always request 'pipe'
113
+ // for stdin/stdout, so this is defensive only.
114
+ //
115
+ // Worded for whoever passed the command, not for whoever wrote this
116
+ // function: the reachable way to arrive here is a `--server` value that
117
+ // does not start, and naming the internal spawner told that reader
118
+ // nothing they could act on.
119
+ child.kill();
120
+ throw new Error(
121
+ `Could not start the data server: the command '${options.command}' provided no stdin/stdout pipe. ` +
122
+ 'Check that --server names a runnable command, and that its own entry path resolves from --cwd.'
123
+ );
124
+ }
125
+
126
+ // Guard stdin against writes that flush after the child has died (spawn failure
127
+ // or a premature exit). vscode-jsonrpc queues a message write on `setImmediate`;
128
+ // if the child's stdin is destroyed by the time it flushes, Node's `write` errors
129
+ // and vscode-jsonrpc orphans that write-queue rejection (it surfaces transport
130
+ // errors only via `onError`, which we consume separately) — an unhandled
131
+ // rejection. Reporting the destroyed write as a silent success drops the doomed
132
+ // message; the child's death is already reported cleanly via `whenTerminated`.
133
+ const stdin = child.stdin;
134
+ const originalWrite = stdin.write.bind(stdin) as (...writeArgs: unknown[]) => boolean;
135
+ stdin.write = ((...writeArgs: unknown[]): boolean => {
136
+ if (!stdin.destroyed) {
137
+ return originalWrite(...writeArgs);
138
+ }
139
+ const callback = writeArgs.find((arg): arg is (error?: Error | null) => void => typeof arg === 'function');
140
+ callback?.(null);
141
+ return true;
142
+ }) as typeof stdin.write;
143
+
144
+ const reader = new StreamMessageReader(child.stdout);
145
+ const writer = new StreamMessageWriter(child.stdin);
146
+ const connection = createMessageConnection(reader, writer);
147
+ // Consume transport error / close so a child that dies mid-stream (its stdout
148
+ // closing, a write landing on a torn-down stdin) is observed here rather than
149
+ // escaping as an unhandled error — the death is reported via `whenTerminated`.
150
+ connection.onError(() => undefined);
151
+ connection.onClose(() => undefined);
152
+ connection.listen();
153
+
154
+ // One typed proxy over the data-server protocol; when a `localClient` is
155
+ // supplied its `on*` notification handlers are bound inbound on the same
156
+ // connection (skipped when absent — subcommands like `projects`/`query`).
157
+ const server = createRpcProxy<DataServerProtocol<TTransfer, TDiagnostic>, DataClientProtocol<TTransfer, TDiagnostic>>(connection, {
158
+ methodNamespace: DATA_SERVER_WIRE_PREFIX,
159
+ localTarget: localClient,
160
+ localMethods: DATA_CLIENT_PROTOCOL_METHODS
161
+ });
162
+
163
+ // A premature child death (spawn `ENOENT`, a crash, or an exit before we asked
164
+ // for one) rejects `whenTerminated` so a racing RPC fails fast instead of
165
+ // hanging on a request that can never be answered. `terminating` gates it so an
166
+ // expected `shutdown()` exit does NOT reject. The `void .catch` guarantees the
167
+ // promise is always handled — an ignored failure can never crash the process
168
+ // with an unhandled 'error' event / rejection.
169
+ let terminating = false;
170
+ const whenTerminated = new Promise<never>((_, reject) => {
171
+ child.once('error', error => {
172
+ if (!terminating) {
173
+ reject(new Error(`data-server process failed to start (command='${options.command}'): ${error.message}`));
174
+ }
175
+ });
176
+ child.once('exit', (code, signal) => {
177
+ if (!terminating) {
178
+ reject(
179
+ new Error(
180
+ `data-server process exited before the request completed ` +
181
+ `(command='${options.command}', code=${code ?? 'null'}, signal=${signal ?? 'null'}).`
182
+ )
183
+ );
184
+ }
185
+ });
186
+ });
187
+ void whenTerminated.catch(() => undefined);
188
+
189
+ let shutdownPromise: Promise<void> | undefined;
190
+ const shutdown = (timeoutMs = 5_000): Promise<void> => {
191
+ if (shutdownPromise) {
192
+ return shutdownPromise;
193
+ }
194
+ terminating = true;
195
+ shutdownPromise = new Promise<void>(resolve => {
196
+ const finalize = (): void => {
197
+ connection.dispose();
198
+ resolve();
199
+ };
200
+ if (child.exitCode !== null || child.signalCode !== null) {
201
+ finalize();
202
+ return;
203
+ }
204
+ const onExit = (): void => {
205
+ clearTimeout(killTimer);
206
+ finalize();
207
+ };
208
+ child.once('exit', onExit);
209
+ // Polite exit first — most servers handle SIGTERM by closing
210
+ // their JSON-RPC connection and returning from `connection.listen()`.
211
+ child.kill('SIGTERM');
212
+ const killTimer = setTimeout(() => {
213
+ child.kill('SIGKILL');
214
+ }, timeoutMs);
215
+ });
216
+ return shutdownPromise;
217
+ };
218
+
219
+ return { server, child, connection, whenTerminated, shutdown };
220
+ }
221
+
222
+ /**
223
+ * Spawn a data-server, run `use` against its typed proxy, and always shut the
224
+ * child down afterwards. The `use` call is raced against
225
+ * {@link DataServerHandle.whenTerminated}, so a spawn failure or a premature child
226
+ * exit rejects with a clear message instead of hanging on an RPC that can never be
227
+ * answered. This is the one-shot counterpart the `projects` / `query` / `save`
228
+ * subcommands compose; `watch` races its own long-running loop against
229
+ * `whenTerminated` directly.
230
+ */
231
+ export async function withDataServer<TTransfer extends TransferElement, TDiagnostic extends TransferDiagnostic, R>(
232
+ options: SpawnDataServerOptions,
233
+ use: (server: DataServerProtocol<TTransfer, TDiagnostic>, handle: DataServerHandle<TTransfer, TDiagnostic>) => Promise<R>,
234
+ localClient?: DataClientProtocol<TTransfer, TDiagnostic>
235
+ ): Promise<R> {
236
+ const handle = spawnDataServer<TTransfer, TDiagnostic>(options, localClient);
237
+ const usePromise = use(handle.server, handle);
238
+ // Defensively handle `usePromise` so that if `whenTerminated` wins the race
239
+ // (the child died), the losing in-flight RPC's later rejection — e.g. a write to
240
+ // the now-destroyed stdin — does not surface as an unhandled rejection.
241
+ void usePromise.catch(() => undefined);
242
+ try {
243
+ return await Promise.race([usePromise, handle.whenTerminated]);
244
+ } finally {
245
+ await handle.shutdown();
246
+ }
247
+ }