@uipath/maestro-builder-sdk 6.18.7 → 6.18.9

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.
@@ -20,4 +20,25 @@ export declare const CONNECTORS_LIBRARY_VERSION_ENV = "CONNECTORS_LIBRARY_VERSIO
20
20
  * rather than guessed at, so a typo cannot fall back to "latest" silently.
21
21
  */
22
22
  export declare function connectorsLibraryUrl(argv: string[], env?: Record<string, string | undefined>): string;
23
+ /**
24
+ * Point `current` at an entry, replacing whatever it named.
25
+ *
26
+ * Nothing to do when `current` already resolves to the entry — the unchanged
27
+ * pull, which is most of them.
28
+ *
29
+ * Otherwise the new link is made under a temp name and renamed over `current`.
30
+ * On POSIX that rename is atomic over an existing symlink, so a concurrent
31
+ * compile never finds the link missing. On Windows `current` is a junction, and
32
+ * a rename onto an existing junction fails with `EPERM` (flow-builder-sdk#945),
33
+ * so there the old link is parked under `current.old-<pid>` first, the new one
34
+ * renamed in, and the parked one removed; a failed rename puts the parked link
35
+ * back. That leaves a moment where `current` does not exist, which Windows
36
+ * gives no way to avoid with a junction.
37
+ *
38
+ * Links are removed with `unlink`, never a recursive `rm`: the link's target is
39
+ * the extracted library, and removing the link must not reach into it.
40
+ *
41
+ * @param platform - Which branch to take; a parameter so both run in tests.
42
+ */
43
+ export declare function pointCurrentAt(root: string, sha256: string, platform?: NodeJS.Platform): Promise<void>;
23
44
  export declare function run(argv: string[]): Promise<number>;
@@ -1,6 +1,6 @@
1
1
  import { reportResult } from '../cli-result.js';
2
2
  import { createHash } from "node:crypto";
3
- import { mkdir, readFile, rename, rm, stat, symlink, writeFile, } from "node:fs/promises";
3
+ import { mkdir, readFile, realpath, rename, rm, stat, symlink, unlink, writeFile, } from "node:fs/promises";
4
4
  import { join, resolve } from "node:path";
5
5
  import { writeJsonFile } from "../core/json-file.js";
6
6
  import { CURRENT_LINK, LIBRARY_JSON_DIR, LIBRARY_MARKDOWN_DIR, cacheEntry, libraryCacheRoot, referencedConnectorKeys, } from "../core/library-cache.js";
@@ -215,16 +215,81 @@ async function writeSource(libraryPath, archive, sha256, archiveSha256, overlayS
215
215
  /**
216
216
  * Point `current` at an entry, replacing whatever it named.
217
217
  *
218
- * Written to a temp name and renamed, so a reader never sees the link missing —
219
- * `rename` over an existing symlink is atomic, while unlink-then-create leaves a
220
- * window where a concurrent compile finds nothing.
218
+ * Nothing to do when `current` already resolves to the entry — the unchanged
219
+ * pull, which is most of them.
220
+ *
221
+ * Otherwise the new link is made under a temp name and renamed over `current`.
222
+ * On POSIX that rename is atomic over an existing symlink, so a concurrent
223
+ * compile never finds the link missing. On Windows `current` is a junction, and
224
+ * a rename onto an existing junction fails with `EPERM` (flow-builder-sdk#945),
225
+ * so there the old link is parked under `current.old-<pid>` first, the new one
226
+ * renamed in, and the parked one removed; a failed rename puts the parked link
227
+ * back. That leaves a moment where `current` does not exist, which Windows
228
+ * gives no way to avoid with a junction.
229
+ *
230
+ * Links are removed with `unlink`, never a recursive `rm`: the link's target is
231
+ * the extracted library, and removing the link must not reach into it.
232
+ *
233
+ * @param platform - Which branch to take; a parameter so both run in tests.
221
234
  */
222
- async function pointCurrentAt(root, sha256) {
235
+ export async function pointCurrentAt(root, sha256, platform = process.platform) {
223
236
  const link = join(root, CURRENT_LINK);
237
+ if (await resolvesTo(link, join(root, sha256)))
238
+ return;
224
239
  const temporary = `${link}.tmp-${process.pid}`;
225
- await rm(temporary, { recursive: true, force: true });
226
- await symlink(sha256, temporary, "junction");
227
- await rename(temporary, link);
240
+ await removeLink(temporary);
241
+ try {
242
+ await symlink(sha256, temporary, "junction");
243
+ if (platform !== "win32") {
244
+ await rename(temporary, link);
245
+ return;
246
+ }
247
+ const parked = `${link}.old-${process.pid}`;
248
+ await removeLink(parked);
249
+ let wasParked = false;
250
+ try {
251
+ await rename(link, parked);
252
+ wasParked = true;
253
+ }
254
+ catch (error) {
255
+ if (error.code !== "ENOENT")
256
+ throw error;
257
+ }
258
+ try {
259
+ await rename(temporary, link);
260
+ }
261
+ catch (error) {
262
+ if (wasParked)
263
+ await rename(parked, link).catch(() => undefined);
264
+ throw error;
265
+ }
266
+ if (wasParked)
267
+ await removeLink(parked);
268
+ }
269
+ finally {
270
+ // Gone already after a successful rename; this is for every way out that
271
+ // is not one, so a failed pull leaves no `current.tmp-<pid>` behind.
272
+ await removeLink(temporary);
273
+ }
274
+ }
275
+ /** Whether `link` and `entry` name the same directory; false when either is missing. */
276
+ async function resolvesTo(link, entry) {
277
+ try {
278
+ return (await realpath(link)) === (await realpath(entry));
279
+ }
280
+ catch {
281
+ return false;
282
+ }
283
+ }
284
+ /** Remove a symlink or junction without following it; a missing one is fine. */
285
+ async function removeLink(path) {
286
+ try {
287
+ await unlink(path);
288
+ }
289
+ catch (error) {
290
+ if (error.code !== "ENOENT")
291
+ throw error;
292
+ }
228
293
  }
229
294
  export async function run(argv) {
230
295
  if (has(argv, "--help") || has(argv, "-h")) {
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { join } from "node:path";
5
5
  import { writeJsonFile } from "../../core/json-file.js";
6
6
  import { HostUipClient } from "./host.js";
7
+ import { launchFor } from "./launch.js";
7
8
  /**
8
9
  * Room for the largest `registry search` a tenant has answered, with margin.
9
10
  * `execFile`'s 1 MiB default cut that stream off, so the port raised it; a
@@ -35,6 +36,9 @@ export class SpawnUipClient {
35
36
  const [executable, ...args] = argv;
36
37
  const timeoutMs = options.timeoutMs ?? this.defaults.timeoutMs;
37
38
  return new Promise((resolve, reject) => {
39
+ // `uip` stays the logical name in argv (recordings are keyed by it);
40
+ // only here is it turned into what Windows can start (launch.ts, #940).
41
+ const launch = launchFor(executable, args);
38
42
  // stdin is `ignore`, not the default pipe. A pipe this process never
39
43
  // writes to stays open, so a `uip` that decides to prompt — an expired
40
44
  // login, a consent question — would block on it for as long as the
@@ -42,7 +46,7 @@ export class SpawnUipClient {
42
46
  // reads EOF at once and the CLI fails like any other call, which is
43
47
  // what Python's `subprocess.run` gave it: the parent's stdin, already at
44
48
  // EOF on an agent.
45
- const child = childProcess().spawn(executable, args, {
49
+ const child = childProcess().spawn(launch.file, launch.args, {
46
50
  cwd: options.cwd ?? this.defaults.cwd,
47
51
  stdio: ["ignore", "pipe", "pipe"],
48
52
  });
@@ -265,7 +269,8 @@ export function runUipSync(argv, options = {}) {
265
269
  if (replay)
266
270
  return { ...readRecording(replay, argv).result };
267
271
  const [executable, ...args] = argv;
268
- const r = childProcess().spawnSync(executable, args, {
272
+ const launch = launchFor(executable, args);
273
+ const r = childProcess().spawnSync(launch.file, launch.args, {
269
274
  cwd: options.cwd,
270
275
  stdio: ["ignore", "pipe", "pipe"],
271
276
  encoding: "utf8",
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
+ import { launchFor } from "./launch.js";
2
3
  /** The MCP tool this client calls; a server without it is treated as absent. */
3
4
  export const HOST_TOOL = "run_argv";
4
5
  /** How long the host may take to start and list its tools before the client gives up on it. */
@@ -241,7 +242,8 @@ export class HostUipClient {
241
242
  // entries reach `client.ts`, so a static import would load a process API on
242
243
  // every builder import (FB009).
243
244
  const { spawn } = createRequire(import.meta.url)("node:child_process");
244
- child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
245
+ const launch = launchFor(command[0], command.slice(1));
246
+ child = spawn(launch.file, launch.args, { stdio: ["pipe", "pipe", "pipe"] });
245
247
  }
246
248
  catch (error) {
247
249
  this.giveUp(`could not start ${command.join(" ")}: ${error instanceof Error ? error.message : String(error)}`);
@@ -0,0 +1,24 @@
1
+ export interface Launch {
2
+ file: string;
3
+ args: string[];
4
+ }
5
+ /** The parts of the host the lookup reads; injected so the Windows branch is testable anywhere. */
6
+ export interface LaunchHost {
7
+ platform: NodeJS.Platform;
8
+ /** `PATH`, `;`-separated on Windows. */
9
+ path: string | undefined;
10
+ /** `PATHEXT`; cmd.exe's default when unset. */
11
+ pathExt: string | undefined;
12
+ /** The Node that runs a shim's script — this process's own. */
13
+ execPath: string;
14
+ isFile(path: string): boolean;
15
+ readFile(path: string): string;
16
+ }
17
+ /**
18
+ * The file and arguments to spawn for `executable args`, without a shell.
19
+ *
20
+ * @throws when, on Windows, the name resolves to a batch shim this cannot read
21
+ * a script out of — naming the shim, so the remedy is not "put uip on PATH"
22
+ * for a `uip` that is on PATH.
23
+ */
24
+ export declare function launchFor(executable: string, args: readonly string[], host?: LaunchHost): Launch;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * What to actually spawn for a logical `uip` command line.
3
+ *
4
+ * Every argv in the registry starts with the bare name `uip` (`commands.ts`),
5
+ * and that name is what recordings are keyed by, so it stays bare everywhere
6
+ * above the spawn. On POSIX the bare name is also what `spawn` wants. On
7
+ * Windows it is not (flow-builder-sdk#940): npm installs the CLI as a `uip.cmd`
8
+ * shim (plus a POSIX `uip` script Windows cannot run), and Node starts neither
9
+ * without a shell — `uip` is `ENOENT`, and `uip.cmd` is `EINVAL` since the
10
+ * CVE-2024-27980 fix refused `.cmd`/`.bat` without `shell`.
11
+ *
12
+ * `shell: true` would start it, but every argument (`-f name=value`, a JSON
13
+ * value, a `|` in a JMESPath filter) would then go through cmd.exe quoting, and
14
+ * a timeout's `kill` would end cmd.exe and leave the CLI running. So on Windows
15
+ * a bare name is looked up on `PATH` the way cmd.exe would (`PATHEXT` order),
16
+ * and:
17
+ * - an `.exe`/`.com` is spawned by its full path;
18
+ * - a `.cmd`/`.bat` npm shim is read for the script it runs
19
+ * (`"%dp0%\node_modules\@uipath\cli\dist\index.js" %*`), and that script is
20
+ * run with this process's own Node, which is what the shim would do;
21
+ * - a shim that names no script is an error that says so, not a guess.
22
+ * A name that is not bare (a path, or one with an extension) is spawned as
23
+ * given, and so is anything on another platform.
24
+ *
25
+ * Example, on Windows with npm's global bin on PATH:
26
+ * launchFor("uip", ["--version"])
27
+ * // -> { file: "C:\\Program Files\\nodejs\\node.exe",
28
+ * // args: ["C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@uipath\\cli\\dist\\index.js", "--version"] }
29
+ */
30
+ import { readFileSync, statSync } from "node:fs";
31
+ import { win32 } from "node:path";
32
+ const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
33
+ /**
34
+ * The script an npm (`cmd-shim`) or pnpm shim hands its arguments to: the last
35
+ * quoted `%dp0%\…` / `%~dp0\…` path followed by `%*`. Only a JavaScript target
36
+ * counts; a shim around something else is not one this module can run directly.
37
+ */
38
+ const SHIM_TARGET = /"%~?dp0%?\\([^"]+\.[cm]?js)"\s+%\*/g;
39
+ function ambientHost() {
40
+ return {
41
+ platform: process.platform,
42
+ path: process.env.PATH,
43
+ pathExt: process.env.PATHEXT,
44
+ execPath: process.execPath,
45
+ isFile: (path) => {
46
+ try {
47
+ return statSync(path).isFile();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ },
53
+ readFile: (path) => readFileSync(path, "utf8"),
54
+ };
55
+ }
56
+ /** True for a name `PATH` lookup applies to: no directory part and no extension. */
57
+ function isBareName(executable) {
58
+ return executable !== "" && !/[\\/]/.test(executable) && win32.extname(executable) === "";
59
+ }
60
+ /** The first `<dir>\<executable><ext>` on PATH, in cmd.exe's order, or undefined. */
61
+ function findOnPath(executable, host) {
62
+ const extensions = (host.pathExt || DEFAULT_PATHEXT).split(";").filter(Boolean);
63
+ for (const dir of (host.path ?? "").split(";")) {
64
+ const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
65
+ if (trimmed === "")
66
+ continue;
67
+ for (const ext of extensions) {
68
+ const candidate = win32.join(trimmed, executable + ext.toLowerCase());
69
+ if (host.isFile(candidate))
70
+ return candidate;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ /** The script a `.cmd`/`.bat` shim runs, as an absolute path, or undefined. */
76
+ function shimTarget(shim, host) {
77
+ const matches = [...host.readFile(shim).matchAll(SHIM_TARGET)];
78
+ const last = matches.at(-1);
79
+ return last ? win32.join(win32.dirname(shim), last[1]) : undefined;
80
+ }
81
+ /**
82
+ * The file and arguments to spawn for `executable args`, without a shell.
83
+ *
84
+ * @throws when, on Windows, the name resolves to a batch shim this cannot read
85
+ * a script out of — naming the shim, so the remedy is not "put uip on PATH"
86
+ * for a `uip` that is on PATH.
87
+ */
88
+ export function launchFor(executable, args, host = ambientHost()) {
89
+ if (host.platform !== "win32" || !isBareName(executable))
90
+ return { file: executable, args: [...args] };
91
+ const found = findOnPath(executable, host);
92
+ // Not on PATH: spawn the bare name and let the ENOENT say so.
93
+ if (!found)
94
+ return { file: executable, args: [...args] };
95
+ const ext = win32.extname(found).toLowerCase();
96
+ if (ext !== ".cmd" && ext !== ".bat")
97
+ return { file: found, args: [...args] };
98
+ const script = shimTarget(found, host);
99
+ if (!script || !host.isFile(script)) {
100
+ throw new Error(`Could not start ${executable}: ${found} is a batch file, which Node cannot run without a shell, ` +
101
+ (script ? `and the script it names (${script}) does not exist.` : "and it names no JavaScript entry point to run instead.") +
102
+ " Reinstall the UiPath CLI with npm (`npm install -g @uipath/cli`).");
103
+ }
104
+ return { file: host.execPath, args: [script, ...args] };
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.18.7",
3
+ "version": "6.18.9",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "7dffe3f9de6568ee2673c38faa5046eb7c86eb54"
95
+ "gitref": "6f8e3c88230704c5b5bc0e8983be45ebedd135dc"
96
96
  }