@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.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- 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';
|
package/src/log-level.ts
ADDED
|
@@ -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
|
+
}
|