@uipath/maestro-builder-sdk 6.16.13 → 6.17.0
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/dist/api-index.md +108 -107
- package/dist/api-members.md +295 -291
- package/dist/bindings-v2.d.ts +39 -1
- package/dist/bindings-v2.js +289 -9
- package/dist/bpmn/compile-cli.js +11 -2
- package/dist/bpmn/decompile.js +2 -1
- package/dist/bpmn/registry-fallback.js +9 -7
- package/dist/case/compile-cli.js +16 -6
- package/dist/case/decompile-cli.js +2 -1
- package/dist/case/decompile.js +3 -2
- package/dist/case/merge-cli.js +3 -2
- package/dist/check.js +219 -37
- package/dist/cli-result.d.ts +9 -0
- package/dist/cli-result.js +12 -0
- package/dist/compile-cli.js +78 -53
- package/dist/core/actions.d.ts +70 -16
- package/dist/core/agentic-process-forms.d.ts +51 -0
- package/dist/core/agentic-process-forms.js +49 -0
- package/dist/core/binding-messages.d.ts +5 -0
- package/dist/core/comment-text.d.ts +29 -0
- package/dist/core/comment-text.js +40 -0
- package/dist/core/contribution-bindings.d.ts +73 -0
- package/dist/core/contribution-bindings.js +105 -0
- package/dist/core/expr-check.js +7 -2
- package/dist/core/json-file.d.ts +1 -0
- package/dist/core/json-file.js +12 -0
- package/dist/core/script-return.js +4 -1
- package/dist/core/typed-raw-node.js +1 -0
- package/dist/core/typescript-module.d.ts +2 -0
- package/dist/core/typescript-module.js +18 -0
- package/dist/core-definitions.json +210 -0
- package/dist/decompile-cli.js +2 -1
- package/dist/decompile.js +64 -24
- package/dist/emit.js +4 -3
- package/dist/flow-sdk.d.ts +14 -9
- package/dist/flow-sdk.js +14 -4
- package/dist/flow-step-tree.d.ts +2 -3
- package/dist/flow-step-tree.js +2 -3
- package/dist/load.js +1 -1
- package/dist/merge-cli.js +3 -2
- package/dist/merge.d.ts +2 -1
- package/dist/merge.js +0 -0
- package/dist/path-within.d.ts +33 -0
- package/dist/path-within.js +41 -0
- package/dist/registry/archive/extract.js +2 -5
- package/dist/registry/curated-overlay.js +3 -2
- package/dist/registry/generators/descriptors.js +20 -18
- package/dist/registry/generators/triggers.js +4 -4
- package/dist/registry/prepare/messages.js +2 -1
- package/dist/registry/prepare/source-labels.js +60 -1
- package/dist/registry/prepare-cli.js +14 -4
- package/dist/registry/pull-cli.js +3 -2
- package/dist/registry/uip/client.d.ts +28 -3
- package/dist/registry/uip/client.js +95 -34
- package/dist/registry/uip/host.d.ts +41 -0
- package/dist/registry/uip/host.js +312 -0
- package/dist/serialize.d.ts +7 -0
- package/dist/serialize.js +409 -275
- package/dist/workdir.js +1 -1
- package/package.json +3 -3
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Running `uip`, and standing in for it.
|
|
3
|
-
*
|
|
4
|
-
* Everything in the registry that talks to the tenant goes through a
|
|
5
|
-
* {@link UipClient}. The real one spawns the CLI with a timeout and UTF-8
|
|
6
|
-
* decoding. The replay one serves envelopes captured earlier, keyed by the
|
|
7
|
-
* exact argv, and fails loudly on a call nobody recorded — which is how prepare
|
|
8
|
-
* and the library builder are tested without a tenant, and how the same
|
|
9
|
-
* recording drives a byte-for-byte comparison against the Python they replace.
|
|
10
|
-
*
|
|
11
|
-
* Nothing here judges the response. Whether a non-zero exit is fatal, or a
|
|
12
|
-
* `Result: Failure` envelope should be cached, is the caller's rule — the
|
|
13
|
-
* Python callers disagreed with each other on purpose, and those rules stay
|
|
14
|
-
* beside the call sites that own them.
|
|
15
|
-
*/
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
17
1
|
import { createHash } from "node:crypto";
|
|
18
|
-
import { mkdirSync, readFileSync,
|
|
2
|
+
import { mkdirSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
19
4
|
import { join } from "node:path";
|
|
5
|
+
import { writeJsonFile } from "../../core/json-file.js";
|
|
6
|
+
import { HostUipClient } from "./host.js";
|
|
20
7
|
/**
|
|
21
8
|
* Room for the largest `registry search` a tenant has answered, with margin.
|
|
22
9
|
* `execFile`'s 1 MiB default cut that stream off, so the port raised it; a
|
|
23
10
|
* child that exceeds even this is killed, which is what `execFile` did too.
|
|
24
11
|
*/
|
|
25
12
|
const MAX_OUTPUT_BYTES = 256 * 1024 * 1024;
|
|
13
|
+
let childProcessModule;
|
|
14
|
+
/**
|
|
15
|
+
* `node:child_process`, loaded on first use. `bpmn/registry-fallback.ts` imports this
|
|
16
|
+
* module, so the `/bpmn` and root builder entries reach it; a builder import must not
|
|
17
|
+
* load a process API it may never use (FB009). The specifier stays a literal so the
|
|
18
|
+
* architecture lint sees the edge.
|
|
19
|
+
*/
|
|
20
|
+
function childProcess() {
|
|
21
|
+
return (childProcessModule ??= createRequire(import.meta.url)("node:child_process"));
|
|
22
|
+
}
|
|
23
|
+
/** The error for a `uip` that is not there: what to install, not just ENOENT. */
|
|
24
|
+
function cannotStart(executable, error) {
|
|
25
|
+
return new Error(`Could not start ${executable}: ${error.message}. The registry verbs call the ` +
|
|
26
|
+
"UiPath CLI; install it and log in, or put it on PATH.");
|
|
27
|
+
}
|
|
26
28
|
/** The real thing: spawn the CLI, capture both streams, never throw on a non-zero exit. */
|
|
27
29
|
export class SpawnUipClient {
|
|
28
30
|
defaults;
|
|
@@ -40,7 +42,7 @@ export class SpawnUipClient {
|
|
|
40
42
|
// reads EOF at once and the CLI fails like any other call, which is
|
|
41
43
|
// what Python's `subprocess.run` gave it: the parent's stdin, already at
|
|
42
44
|
// EOF on an agent.
|
|
43
|
-
const child = spawn(executable, args, {
|
|
45
|
+
const child = childProcess().spawn(executable, args, {
|
|
44
46
|
cwd: options.cwd ?? this.defaults.cwd,
|
|
45
47
|
stdio: ["ignore", "pipe", "pipe"],
|
|
46
48
|
});
|
|
@@ -70,8 +72,7 @@ export class SpawnUipClient {
|
|
|
70
72
|
if (timer !== undefined)
|
|
71
73
|
clearTimeout(timer);
|
|
72
74
|
if (error.code === "ENOENT") {
|
|
73
|
-
reject(
|
|
74
|
-
"UiPath CLI; install it and log in, or put it on PATH."));
|
|
75
|
+
reject(cannotStart(executable, error));
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
// Every other spawn failure — EACCES on the binary, EAGAIN with no
|
|
@@ -118,6 +119,21 @@ export function recordingName(argv) {
|
|
|
118
119
|
.slice(0, 60);
|
|
119
120
|
return `${slug}-${digest}.json`;
|
|
120
121
|
}
|
|
122
|
+
/** The recording for `argv` in `directory`; throws, naming the argv and the file, when there is none. */
|
|
123
|
+
function readRecording(directory, argv) {
|
|
124
|
+
const path = join(directory, recordingName(argv));
|
|
125
|
+
if (!existsSync(path)) {
|
|
126
|
+
throw new Error(`ReplayUipClient: no recording for\n ${argv.join(" ")}\n(expected ${path}). ` +
|
|
127
|
+
"Record it with RecordingUipClient, or the call is one the code under test should not make. " +
|
|
128
|
+
"A build replays only when UIP_REPLAY_DIR is set; if that was not intended, unset it.");
|
|
129
|
+
}
|
|
130
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
131
|
+
}
|
|
132
|
+
/** Write `result` down as the recording for `argv` in `directory`. */
|
|
133
|
+
function writeRecording(directory, argv, result) {
|
|
134
|
+
mkdirSync(directory, { recursive: true });
|
|
135
|
+
writeJsonFile(join(directory, recordingName(argv)), { argv: [...argv], result });
|
|
136
|
+
}
|
|
121
137
|
/**
|
|
122
138
|
* Serve recorded results; refuse anything unrecorded.
|
|
123
139
|
*
|
|
@@ -134,13 +150,7 @@ export class ReplayUipClient {
|
|
|
134
150
|
const name = recordingName(argv);
|
|
135
151
|
let recording = this.byName.get(name);
|
|
136
152
|
if (!recording) {
|
|
137
|
-
|
|
138
|
-
if (!existsSync(path)) {
|
|
139
|
-
throw new Error(`ReplayUipClient: no recording for\n ${argv.join(" ")}\n(expected ${path}). ` +
|
|
140
|
-
"Record it with RecordingUipClient, or the call is one the code under test should not make. " +
|
|
141
|
-
"A build replays only when UIP_REPLAY_DIR is set; if that was not intended, unset it.");
|
|
142
|
-
}
|
|
143
|
-
recording = JSON.parse(readFileSync(path, "utf8"));
|
|
153
|
+
recording = readRecording(this.directory, argv);
|
|
144
154
|
this.byName.set(name, recording);
|
|
145
155
|
}
|
|
146
156
|
return { ...recording.result };
|
|
@@ -157,10 +167,12 @@ export class RecordingUipClient {
|
|
|
157
167
|
}
|
|
158
168
|
async run(argv, options) {
|
|
159
169
|
const result = await this.inner.run(argv, options);
|
|
160
|
-
|
|
161
|
-
writeFileSync(join(this.directory, recordingName(argv)), `${JSON.stringify(recording, null, 2)}\n`);
|
|
170
|
+
writeRecording(this.directory, argv, result);
|
|
162
171
|
return result;
|
|
163
172
|
}
|
|
173
|
+
close() {
|
|
174
|
+
this.inner.close?.();
|
|
175
|
+
}
|
|
164
176
|
}
|
|
165
177
|
/**
|
|
166
178
|
* A client that runs nothing and reports every call as refused.
|
|
@@ -213,14 +225,63 @@ export class OfflineUipClient {
|
|
|
213
225
|
*
|
|
214
226
|
* Both exist to capture a fixture from a real tenant and to replay one — that
|
|
215
227
|
* is how `fixtures/library-build/` was taken — and neither is meant to be set
|
|
216
|
-
* in a pipeline. They are read
|
|
217
|
-
*
|
|
228
|
+
* in a pipeline. They are read by `uipSwitches()` here, for this and
|
|
229
|
+
* {@link runUipSync}, and nowhere else, so this is the only place an operator can
|
|
230
|
+
* be told what they do.
|
|
231
|
+
*
|
|
232
|
+
* A third, `FLOW_SDK_UIP_HOST=0`, turns off the one-process host a caller may
|
|
233
|
+
* ask for with `host` (see `host.ts`): every call then spawns its own `uip`,
|
|
234
|
+
* which is how the registry ran before flow-builder-sdk#965 and the shape to
|
|
235
|
+
* compare against when a prepare misbehaves.
|
|
218
236
|
*/
|
|
219
237
|
export function defaultUipClient(options = {}) {
|
|
220
|
-
const replay =
|
|
238
|
+
const { replay, record } = uipSwitches();
|
|
221
239
|
if (replay)
|
|
222
240
|
return new ReplayUipClient(replay);
|
|
223
|
-
const
|
|
224
|
-
const
|
|
241
|
+
const { host, ...runOptions } = options;
|
|
242
|
+
const spawning = new SpawnUipClient(runOptions);
|
|
243
|
+
// One `uip mcp serve` for the command's whole chain of calls, where the
|
|
244
|
+
// caller asked for it and the operator has not switched it off. Serial, so
|
|
245
|
+
// not for the library builder, whose `concurrency` runs calls side by side.
|
|
246
|
+
const client = host && process.env.FLOW_SDK_UIP_HOST !== "0"
|
|
247
|
+
? new HostUipClient(spawning, typeof host === "object" ? host : {})
|
|
248
|
+
: spawning;
|
|
225
249
|
return record ? new RecordingUipClient(client, record) : client;
|
|
226
250
|
}
|
|
251
|
+
/** The replay and record switches (see {@link defaultUipClient}); an empty value is unset. */
|
|
252
|
+
function uipSwitches() {
|
|
253
|
+
return { replay: process.env.UIP_REPLAY_DIR || undefined, record: process.env.UIP_RECORD_DIR || undefined };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Run `uip` synchronously, for the one caller that cannot await:
|
|
257
|
+
* `bpmn/registry-fallback.ts` resolves an extension type while a builder method runs.
|
|
258
|
+
* Same switches as {@link defaultUipClient} (`UIP_REPLAY_DIR` answers from recordings and
|
|
259
|
+
* throws on an unrecorded call; `UIP_RECORD_DIR` writes each result down) and the same
|
|
260
|
+
* contract as {@link SpawnUipClient}: no stdin, a non-zero exit or a timeout is a result,
|
|
261
|
+
* a missing executable throws the install instruction.
|
|
262
|
+
*/
|
|
263
|
+
export function runUipSync(argv, options = {}) {
|
|
264
|
+
const { replay, record } = uipSwitches();
|
|
265
|
+
if (replay)
|
|
266
|
+
return { ...readRecording(replay, argv).result };
|
|
267
|
+
const [executable, ...args] = argv;
|
|
268
|
+
const r = childProcess().spawnSync(executable, args, {
|
|
269
|
+
cwd: options.cwd,
|
|
270
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
271
|
+
encoding: "utf8",
|
|
272
|
+
timeout: options.timeoutMs,
|
|
273
|
+
killSignal: "SIGKILL",
|
|
274
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
275
|
+
});
|
|
276
|
+
const errno = r.error?.code;
|
|
277
|
+
if (errno === "ENOENT")
|
|
278
|
+
throw cannotStart(executable, r.error);
|
|
279
|
+
if (r.error && errno !== "ETIMEDOUT" && errno !== "ENOBUFS")
|
|
280
|
+
throw r.error;
|
|
281
|
+
const result = {
|
|
282
|
+
code: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "", timedOut: errno === "ETIMEDOUT",
|
|
283
|
+
};
|
|
284
|
+
if (record)
|
|
285
|
+
writeRecording(record, argv, result);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UipClient, UipResult, UipRunOptions } from "./client.js";
|
|
2
|
+
/** The MCP tool this client calls; a server without it is treated as absent. */
|
|
3
|
+
export declare const HOST_TOOL = "run_argv";
|
|
4
|
+
export interface HostUipClientOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The command that starts the host for `executable` (`argv[0]`, normally
|
|
7
|
+
* `uip`). Defaults to `[executable, "mcp", "serve"]`; tests point it at a
|
|
8
|
+
* fake server.
|
|
9
|
+
*/
|
|
10
|
+
hostCommand?: (executable: string) => string[];
|
|
11
|
+
startTimeoutMs?: number;
|
|
12
|
+
/** Where to say why the host was given up on; silent by default. */
|
|
13
|
+
onFallback?: (reason: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Run `uip` calls through one `uip mcp serve` process, falling back to
|
|
17
|
+
* `fallback` (the spawning client) whenever the host cannot serve a call.
|
|
18
|
+
*/
|
|
19
|
+
export declare class HostUipClient implements UipClient {
|
|
20
|
+
private readonly fallback;
|
|
21
|
+
private readonly options;
|
|
22
|
+
private host;
|
|
23
|
+
private starting;
|
|
24
|
+
private disabled;
|
|
25
|
+
private queue;
|
|
26
|
+
constructor(fallback: UipClient, options?: HostUipClientOptions);
|
|
27
|
+
run(argv: readonly string[], options?: UipRunOptions): Promise<UipResult>;
|
|
28
|
+
/** Shut the host down. Calls still in flight finish first; later calls start a new one. */
|
|
29
|
+
close(): void;
|
|
30
|
+
private runSerially;
|
|
31
|
+
private ensureHost;
|
|
32
|
+
private start;
|
|
33
|
+
/**
|
|
34
|
+
* Spawn per call from here on, silently. This is the path for a CLI whose
|
|
35
|
+
* server predates `run_argv`: a prepare on it works exactly as it did
|
|
36
|
+
* before this client existed, so there is nothing to tell the user.
|
|
37
|
+
*/
|
|
38
|
+
private spawnFromNowOn;
|
|
39
|
+
/** Spawn per call from here on, and say why: the host failed. */
|
|
40
|
+
private giveUp;
|
|
41
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
/** The MCP tool this client calls; a server without it is treated as absent. */
|
|
3
|
+
export const HOST_TOOL = "run_argv";
|
|
4
|
+
/** How long the host may take to start and list its tools before the client gives up on it. */
|
|
5
|
+
const DEFAULT_START_TIMEOUT_MS = 20_000;
|
|
6
|
+
/** Stderr the host printed, kept for the message when it dies. */
|
|
7
|
+
const STDERR_KEEP = 4_000;
|
|
8
|
+
/** Thrown inside the client when a call's timeout elapsed; never escapes `run`. */
|
|
9
|
+
class HostTimeout extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** Thrown inside the client when the host or its protocol failed; never escapes `run`. */
|
|
12
|
+
class HostFailure extends Error {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `ref`/`unref` a child's stdio pipe. The streams are typed as plain
|
|
16
|
+
* `Readable`/`Writable`, but a piped stdio stream is a `net.Socket`, which
|
|
17
|
+
* has both; a stream without them (a test's fake) is simply left alone.
|
|
18
|
+
*/
|
|
19
|
+
function refStream(stream, on) {
|
|
20
|
+
const socket = stream;
|
|
21
|
+
if (!socket)
|
|
22
|
+
return;
|
|
23
|
+
if (on)
|
|
24
|
+
socket.ref?.();
|
|
25
|
+
else
|
|
26
|
+
socket.unref?.();
|
|
27
|
+
}
|
|
28
|
+
/** A live `uip mcp serve` process and the JSON-RPC conversation with it. */
|
|
29
|
+
class Host {
|
|
30
|
+
child;
|
|
31
|
+
pending = new Map();
|
|
32
|
+
nextId = 1;
|
|
33
|
+
buffer = "";
|
|
34
|
+
stderr = "";
|
|
35
|
+
closed = false;
|
|
36
|
+
/** Set when the client killed the host on purpose, so `close` is not a failure. */
|
|
37
|
+
killedForTimeout = false;
|
|
38
|
+
exitHook;
|
|
39
|
+
constructor(child) {
|
|
40
|
+
this.child = child;
|
|
41
|
+
child.stdout.setEncoding("utf8");
|
|
42
|
+
child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
43
|
+
child.stderr.setEncoding("utf8");
|
|
44
|
+
child.stderr.on("data", (chunk) => {
|
|
45
|
+
this.stderr = (this.stderr + chunk).slice(-STDERR_KEEP);
|
|
46
|
+
});
|
|
47
|
+
child.on("error", (error) => this.fail(new HostFailure(`uip mcp serve: ${error.message}`)));
|
|
48
|
+
child.on("close", (code, signal) => {
|
|
49
|
+
this.closed = true;
|
|
50
|
+
process.removeListener("exit", this.exitHook);
|
|
51
|
+
const why = this.killedForTimeout
|
|
52
|
+
? new HostTimeout("killed after a call's timeout")
|
|
53
|
+
: new HostFailure(`uip mcp serve exited (code ${code}, signal ${signal})` + (this.stderr.trim() ? `: ${this.stderr.trim()}` : ""));
|
|
54
|
+
this.fail(why);
|
|
55
|
+
});
|
|
56
|
+
// stdin is written only by us; an error there (EPIPE after the server
|
|
57
|
+
// died) is reported through `close`, not as an unhandled event.
|
|
58
|
+
child.stdin.on("error", () => undefined);
|
|
59
|
+
this.exitHook = () => {
|
|
60
|
+
if (!this.closed)
|
|
61
|
+
child.kill("SIGKILL");
|
|
62
|
+
};
|
|
63
|
+
process.once("exit", this.exitHook);
|
|
64
|
+
this.idle();
|
|
65
|
+
}
|
|
66
|
+
get alive() {
|
|
67
|
+
return !this.closed;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Let the SDK process exit while the host sits idle: with nothing in
|
|
71
|
+
* flight nothing here holds the event loop, and when the parent exits the
|
|
72
|
+
* server reads EOF on its stdin and shuts down. A call in flight `ref`s the
|
|
73
|
+
* pipes again, so the loop waits for its answer.
|
|
74
|
+
*/
|
|
75
|
+
idle() {
|
|
76
|
+
this.child.unref();
|
|
77
|
+
for (const stream of [this.child.stdout, this.child.stderr, this.child.stdin])
|
|
78
|
+
refStream(stream, false);
|
|
79
|
+
}
|
|
80
|
+
busy() {
|
|
81
|
+
this.child.ref();
|
|
82
|
+
for (const stream of [this.child.stdout, this.child.stderr, this.child.stdin])
|
|
83
|
+
refStream(stream, true);
|
|
84
|
+
}
|
|
85
|
+
onData(chunk) {
|
|
86
|
+
this.buffer += chunk;
|
|
87
|
+
let newline = this.buffer.indexOf("\n");
|
|
88
|
+
while (newline >= 0) {
|
|
89
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
90
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
91
|
+
if (line !== "")
|
|
92
|
+
this.onLine(line);
|
|
93
|
+
newline = this.buffer.indexOf("\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
onLine(line) {
|
|
97
|
+
let message;
|
|
98
|
+
try {
|
|
99
|
+
message = JSON.parse(line);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Something other than JSON-RPC on the server's stdout — a command that
|
|
103
|
+
// wrote to the real stream instead of its sink. The framing is broken
|
|
104
|
+
// from here on, so every call in flight fails and the client falls back.
|
|
105
|
+
this.fail(new HostFailure(`uip mcp serve wrote something other than JSON-RPC: ${line.slice(0, 200)}`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// A notification has no id; a server-to-client REQUEST (`ping`) has an id
|
|
109
|
+
// of its own numbering and a `method`. Neither is an answer to us, and a
|
|
110
|
+
// request whose id happened to equal a pending call's would otherwise be
|
|
111
|
+
// read as that call's reply and fail it.
|
|
112
|
+
if (typeof message.id !== "number" || "method" in message)
|
|
113
|
+
return;
|
|
114
|
+
const waiting = this.pending.get(message.id);
|
|
115
|
+
if (!waiting)
|
|
116
|
+
return;
|
|
117
|
+
this.pending.delete(message.id);
|
|
118
|
+
if (this.pending.size === 0)
|
|
119
|
+
this.idle();
|
|
120
|
+
waiting.resolve(message);
|
|
121
|
+
}
|
|
122
|
+
/** Reject every call in flight; later calls find `alive` false. */
|
|
123
|
+
fail(error) {
|
|
124
|
+
const waiting = [...this.pending.values()];
|
|
125
|
+
this.pending.clear();
|
|
126
|
+
this.idle();
|
|
127
|
+
for (const entry of waiting)
|
|
128
|
+
entry.reject(error);
|
|
129
|
+
}
|
|
130
|
+
notify(method, params) {
|
|
131
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
132
|
+
}
|
|
133
|
+
request(method, params, timeoutMs) {
|
|
134
|
+
if (this.closed)
|
|
135
|
+
return Promise.reject(new HostFailure("uip mcp serve is not running"));
|
|
136
|
+
const id = this.nextId++;
|
|
137
|
+
this.busy();
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
let timer;
|
|
140
|
+
const settle = (fn) => (value) => {
|
|
141
|
+
if (timer !== undefined)
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
fn(value);
|
|
144
|
+
};
|
|
145
|
+
this.pending.set(id, { resolve: settle(resolve), reject: settle(reject) });
|
|
146
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
147
|
+
timer = setTimeout(() => {
|
|
148
|
+
// A command cannot be cancelled inside the host, so the host goes.
|
|
149
|
+
// `close` then fails this call with the timeout the flag records.
|
|
150
|
+
this.killedForTimeout = true;
|
|
151
|
+
this.child.kill("SIGKILL");
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
}
|
|
154
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** End the conversation: the server treats stdin EOF as shutdown. */
|
|
158
|
+
close() {
|
|
159
|
+
process.removeListener("exit", this.exitHook);
|
|
160
|
+
if (this.closed)
|
|
161
|
+
return;
|
|
162
|
+
this.child.stdin.end();
|
|
163
|
+
// If it ignores EOF, it does not get to outlive the command.
|
|
164
|
+
const grace = setTimeout(() => {
|
|
165
|
+
if (!this.closed)
|
|
166
|
+
this.child.kill("SIGKILL");
|
|
167
|
+
}, 2_000);
|
|
168
|
+
grace.unref();
|
|
169
|
+
}
|
|
170
|
+
kill() {
|
|
171
|
+
process.removeListener("exit", this.exitHook);
|
|
172
|
+
if (!this.closed)
|
|
173
|
+
this.child.kill("SIGKILL");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Run `uip` calls through one `uip mcp serve` process, falling back to
|
|
178
|
+
* `fallback` (the spawning client) whenever the host cannot serve a call.
|
|
179
|
+
*/
|
|
180
|
+
export class HostUipClient {
|
|
181
|
+
fallback;
|
|
182
|
+
options;
|
|
183
|
+
host;
|
|
184
|
+
starting;
|
|
185
|
+
disabled = false;
|
|
186
|
+
queue = Promise.resolve();
|
|
187
|
+
constructor(fallback, options = {}) {
|
|
188
|
+
this.fallback = fallback;
|
|
189
|
+
this.options = options;
|
|
190
|
+
}
|
|
191
|
+
run(argv, options = {}) {
|
|
192
|
+
if (this.disabled || options.cwd !== undefined)
|
|
193
|
+
return this.fallback.run(argv, options);
|
|
194
|
+
const task = this.queue.then(() => this.runSerially(argv, options));
|
|
195
|
+
this.queue = task.then(() => undefined, () => undefined);
|
|
196
|
+
return task;
|
|
197
|
+
}
|
|
198
|
+
/** Shut the host down. Calls still in flight finish first; later calls start a new one. */
|
|
199
|
+
close() {
|
|
200
|
+
const host = this.host;
|
|
201
|
+
this.host = undefined;
|
|
202
|
+
if (host)
|
|
203
|
+
this.queue.then(() => host.close(), () => host.close());
|
|
204
|
+
}
|
|
205
|
+
async runSerially(argv, options) {
|
|
206
|
+
if (this.disabled)
|
|
207
|
+
return this.fallback.run(argv, options);
|
|
208
|
+
const host = await this.ensureHost(argv[0]);
|
|
209
|
+
if (!host)
|
|
210
|
+
return this.fallback.run(argv, options);
|
|
211
|
+
try {
|
|
212
|
+
const response = await host.request("tools/call", { name: HOST_TOOL, arguments: { args: argv.slice(1) } }, options.timeoutMs);
|
|
213
|
+
return toResult(response);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error instanceof HostTimeout) {
|
|
217
|
+
this.host = undefined;
|
|
218
|
+
return { code: null, signal: "SIGKILL", stdout: "", stderr: "", timedOut: true };
|
|
219
|
+
}
|
|
220
|
+
this.giveUp(error instanceof Error ? error.message : String(error));
|
|
221
|
+
return this.fallback.run(argv, options);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async ensureHost(executable) {
|
|
225
|
+
if (this.host?.alive)
|
|
226
|
+
return this.host;
|
|
227
|
+
this.host = undefined;
|
|
228
|
+
if (!this.starting) {
|
|
229
|
+
this.starting = this.start(executable).finally(() => {
|
|
230
|
+
this.starting = undefined;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return this.starting;
|
|
234
|
+
}
|
|
235
|
+
async start(executable) {
|
|
236
|
+
const command = (this.options.hostCommand ?? ((exe) => [exe, "mcp", "serve"]))(executable);
|
|
237
|
+
const startTimeoutMs = this.options.startTimeoutMs ?? DEFAULT_START_TIMEOUT_MS;
|
|
238
|
+
let child;
|
|
239
|
+
try {
|
|
240
|
+
// Loaded here, not at import: `client.ts` imports this module and the builder
|
|
241
|
+
// entries reach `client.ts`, so a static import would load a process API on
|
|
242
|
+
// every builder import (FB009).
|
|
243
|
+
const { spawn } = createRequire(import.meta.url)("node:child_process");
|
|
244
|
+
child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.giveUp(`could not start ${command.join(" ")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const host = new Host(child);
|
|
251
|
+
try {
|
|
252
|
+
const initialized = await host.request("initialize", {
|
|
253
|
+
protocolVersion: "2024-11-05",
|
|
254
|
+
capabilities: {},
|
|
255
|
+
clientInfo: { name: "maestro-builder-sdk", version: "0" },
|
|
256
|
+
}, startTimeoutMs);
|
|
257
|
+
if (initialized.error)
|
|
258
|
+
throw new HostFailure(`initialize failed: ${initialized.error.message ?? "unknown error"}`);
|
|
259
|
+
host.notify("notifications/initialized");
|
|
260
|
+
const listed = await host.request("tools/list", {}, startTimeoutMs);
|
|
261
|
+
const tools = listed.result?.tools ?? [];
|
|
262
|
+
if (!tools.some((tool) => tool.name === HOST_TOOL)) {
|
|
263
|
+
// An older CLI, not a fault: spawn per call, and say nothing.
|
|
264
|
+
host.close();
|
|
265
|
+
this.spawnFromNowOn();
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
host.kill();
|
|
271
|
+
this.giveUp(error instanceof HostTimeout
|
|
272
|
+
? `uip mcp serve did not answer within ${startTimeoutMs} ms`
|
|
273
|
+
: error instanceof Error
|
|
274
|
+
? error.message
|
|
275
|
+
: String(error));
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
this.host = host;
|
|
279
|
+
return host;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Spawn per call from here on, silently. This is the path for a CLI whose
|
|
283
|
+
* server predates `run_argv`: a prepare on it works exactly as it did
|
|
284
|
+
* before this client existed, so there is nothing to tell the user.
|
|
285
|
+
*/
|
|
286
|
+
spawnFromNowOn() {
|
|
287
|
+
this.disabled = true;
|
|
288
|
+
this.host?.kill();
|
|
289
|
+
this.host = undefined;
|
|
290
|
+
}
|
|
291
|
+
/** Spawn per call from here on, and say why: the host failed. */
|
|
292
|
+
giveUp(reason) {
|
|
293
|
+
if (this.disabled)
|
|
294
|
+
return;
|
|
295
|
+
this.spawnFromNowOn();
|
|
296
|
+
this.options.onFallback?.(`uip host unavailable, spawning each call instead: ${reason}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** The tool's structured answer as the result every caller already reads. */
|
|
300
|
+
function toResult(response) {
|
|
301
|
+
if (response.error)
|
|
302
|
+
throw new HostFailure(`${HOST_TOOL} failed: ${response.error.message ?? "unknown error"}`);
|
|
303
|
+
const result = response.result;
|
|
304
|
+
const structured = result?.structuredContent;
|
|
305
|
+
if (structured === undefined ||
|
|
306
|
+
typeof structured.stdout !== "string" ||
|
|
307
|
+
typeof structured.stderr !== "string" ||
|
|
308
|
+
typeof structured.exitCode !== "number") {
|
|
309
|
+
throw new HostFailure(`${HOST_TOOL} answered without stdout/stderr/exitCode`);
|
|
310
|
+
}
|
|
311
|
+
return { code: structured.exitCode, signal: null, stdout: structured.stdout, stderr: structured.stderr, timedOut: false };
|
|
312
|
+
}
|
package/dist/serialize.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DefinitionReference } from './core/node-classes.js';
|
|
1
2
|
import type { BuiltFlow } from './flow-sdk.js';
|
|
2
3
|
import type { Library } from './library.js';
|
|
3
4
|
import type { Bindings } from './bindings.js';
|
|
@@ -87,6 +88,12 @@ export declare function serializeWithSidecars(built: BuiltFlow, opts?: Serialize
|
|
|
87
88
|
json: unknown;
|
|
88
89
|
}>;
|
|
89
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a contribution's {@link DefinitionReference} to a concrete manifest —
|
|
93
|
+
* exactly (design §2.6/§4.2): the named version resolves or compilation fails,
|
|
94
|
+
* and a contributor that ignored the author's requested version is rejected.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveContributionDefinition(ref: DefinitionReference, requestedVersion: string | undefined, where: string): any;
|
|
90
97
|
export declare function serialize(built: BuiltFlow, opts?: SerializeOpts): any;
|
|
91
98
|
/**
|
|
92
99
|
* Every node `configuration` that carries Studio Web's split envelope must be
|