@uipath/maestro-builder-sdk 6.45.1 → 6.46.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.
@@ -233,6 +233,11 @@
233
233
  "name": "value",
234
234
  "isAttr": true,
235
235
  "type": "String"
236
+ },
237
+ {
238
+ "name": "isDefault",
239
+ "isAttr": true,
240
+ "type": "Boolean"
236
241
  }
237
242
  ]
238
243
  },
package/dist/check.js CHANGED
@@ -651,7 +651,7 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
651
651
  level: 'error',
652
652
  code: 'SWITCH_CASE_ID',
653
653
  message: typeof caseId !== 'string' || caseId === ''
654
- ? `Switch "${s.name}" case ${JSON.stringify(c.value)} has an id that is not a non-empty string. Leave \`id\` out for the positional case_<n>.`
654
+ ? `Switch "${s.name}" case ${i + 1} has an id that is not a non-empty string. Leave \`id\` out for the positional case_<n>.`
655
655
  : `Switch "${s.name}" has two cases with the id "${caseId}", so both would leave from one 'case-${caseId}' port. Give one a different \`id\`.`,
656
656
  step: s.name,
657
657
  });
@@ -20,7 +20,7 @@
20
20
  import { reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
21
21
  import { writeJsonFile } from './core/json-file.js';
22
22
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
23
- import { basename, dirname, join, resolve } from 'node:path';
23
+ import { basename, dirname, join, relative, resolve } from 'node:path';
24
24
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
25
25
  import { serializeWithSidecars } from './serialize.js';
26
26
  import { compileOwnsResource } from './core/agent-tool-resources.js';
@@ -272,8 +272,18 @@ export async function run(argv) {
272
272
  }
273
273
  if (!compileOwnsResource(body))
274
274
  continue;
275
+ try {
276
+ rmSync(join(resourcesDir, entry), { recursive: true, force: true });
277
+ }
278
+ catch (error) {
279
+ // Studio Web's project file system refuses deletes. Only a surviving file keeps its row: a later compile skips a folder with no resource.json.
280
+ if (existsSync(file)) {
281
+ console.error(`compile: could not remove stale ${relative(outDir, file)} (${error instanceof Error ? error.message : String(error)}); `
282
+ + 'it is still declared as a resource of this agent until it is deleted.');
283
+ continue;
284
+ }
285
+ }
275
286
  removedResources.push(body);
276
- rmSync(join(resourcesDir, entry), { recursive: true, force: true });
277
287
  console.log(`compile: removed stale ${join('resources', entry, 'resource.json')}`);
278
288
  }
279
289
  }
@@ -21,6 +21,10 @@
21
21
  * `process.getBuiltinModule`, not an `import`, so the library loads no `node:` builtin
22
22
  * (FB015 exempts this fallback), and `process` is read through `globalThis`, as in
23
23
  * `core/typescript-module.ts`.
24
+ *
25
+ * The tenant calls (`registry prepare`'s `uip` runs) have a sibling slot,
26
+ * `globalThis[Symbol.for('uipath.maestro-builder-sdk.uip')]`, documented on
27
+ * `defaultUipClient` in `registry/uip/client.ts`.
24
28
  */
25
29
  /** The file's text, or `undefined` when there is none. */
26
30
  export type ReadText = (path: string) => string | undefined;
@@ -21,6 +21,10 @@
21
21
  * `process.getBuiltinModule`, not an `import`, so the library loads no `node:` builtin
22
22
  * (FB015 exempts this fallback), and `process` is read through `globalThis`, as in
23
23
  * `core/typescript-module.ts`.
24
+ *
25
+ * The tenant calls (`registry prepare`'s `uip` runs) have a sibling slot,
26
+ * `globalThis[Symbol.for('uipath.maestro-builder-sdk.uip')]`, documented on
27
+ * `defaultUipClient` in `registry/uip/client.ts`.
24
28
  */
25
29
  const HOST_READ_TEXT = Symbol.for('uipath.maestro-builder-sdk.readText');
26
30
  /** The reader a default read uses: the host's, when one is in the slot, else the disk. */
@@ -13,7 +13,7 @@
13
13
  * a `--keep-temp` cache the Python wrote drives this code to the same bytes,
14
14
  * which is how the port is proven.
15
15
  */
16
- import { createHash } from "node:crypto";
16
+ import { bytesToHex, sha256 } from "../../core/sha.js";
17
17
  import { pyJsonStringify } from "../codecs/pyjson.js";
18
18
  import { sortedByKeys } from "../compat/compare.js";
19
19
  import { isActivitiesList as isActivitiesListArgv, isConnectorsMetadata, isResourcesDescribeObject, isResourcesDescribeOperation, isResourcesList as isResourcesListArgv, registryGet as registryGetArgv, } from "../uip/commands.js";
@@ -47,7 +47,7 @@ export function fieldScope(parentFields) {
47
47
  return "";
48
48
  const pairs = sortedByKeys([...parentFields.entries()], ([k]) => k, ([, v]) => v);
49
49
  const flat = pairs.map(([k, v]) => `${k}=${v}`).join("\0");
50
- return `f-${createHash("sha256").update(flat, "utf8").digest("hex").slice(0, 12)}`;
50
+ return `f-${bytesToHex(sha256(flat)).slice(0, 12)}`;
51
51
  }
52
52
  /** Cache file for one `flow registry get`: the node type with `.` → `_`. */
53
53
  export function registryCacheName(nodeType) {
@@ -21,12 +21,13 @@
21
21
  * a {@link UipClient} and hand the rows to them. Every refusal's text is kept
22
22
  * verbatim — agents read these messages and follow them.
23
23
  */
24
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
24
+ import { readdirSync } from "node:fs";
25
25
  import { dirname, join, resolve as resolvePath } from "node:path";
26
26
  import { pyJsonStringify } from "../codecs/pyjson.js";
27
27
  import { Refusal, messageOf } from "../errors.js";
28
28
  import { isConnectionsList } from "../uip/commands.js";
29
29
  import { dataItems, failureReason, parseEnvelope } from "../uip/envelope.js";
30
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "./staged-files.js";
30
31
  import { isPlainObject, notJsonReason, pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
31
32
  /** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
32
33
  export class ConnectionRefusal extends Refusal {
@@ -13,12 +13,12 @@
13
13
  * `_unwrap_value` in `prepare_connector.py`. The text lives in `messages.ts`;
14
14
  * this module reads the generated module and parses it.
15
15
  */
16
- import { readFileSync } from "node:fs";
17
16
  import { join, resolve } from "node:path";
18
17
  import { relativePosix, toPosix } from "../compat/paths.js";
19
18
  import { pyStrip } from "../compat/pystr.js";
20
19
  import { isRelativeSpecifier } from "./source-import.js";
21
20
  import { descriptorUsageFallbackLines, descriptorUsageLines, descriptorUsageUnparsedLines } from "./messages.js";
21
+ import { readFileSync } from "./staged-files.js";
22
22
  /**
23
23
  * `ConnectorValue<string>` → `string`, for the handoff skeleton. The generated
24
24
  * property type says what the field ACCEPTS — the literal or an expression —
@@ -30,6 +30,15 @@ export declare const PREPARE_COMMAND = "npx flow-sdk registry prepare";
30
30
  export declare function prefixed(text: string): string;
31
31
  /** `prepare-connector: next: <text>` — the last lines of a run, each one a call the reader can paste. */
32
32
  export declare function nextLine(text: string): string;
33
+ /**
34
+ * The run's last line: the `registry describe` that prints the page just
35
+ * prepared. `overlay` is `--out` when it is not the default describe reads.
36
+ */
37
+ export declare function describeNextLine(cli: string, nodeType: string, objectName?: string, overlay?: string): string;
38
+ /** The line a refusal gains when the run had already staged files, which its progress lines may have named. */
39
+ export declare function discardedNote(count: number): string;
40
+ /** A write failed after the run succeeded, so the files written before it remain. */
41
+ export declare function partialWriteMessage(path: string, reason: string): string;
33
42
  /**
34
43
  * A refusal whose message is the COMPLETE text the Python passed to
35
44
  * `sys.exit`, prefix included. Every other {@link Refusal} reaching the exit
@@ -36,6 +36,24 @@ export function prefixed(text) {
36
36
  export function nextLine(text) {
37
37
  return prefixed(`next: ${text}`);
38
38
  }
39
+ /**
40
+ * The run's last line: the `registry describe` that prints the page just
41
+ * prepared. `overlay` is `--out` when it is not the default describe reads.
42
+ */
43
+ export function describeNextLine(cli, nodeType, objectName, overlay) {
44
+ const object = objectName ? ` --object ${objectName}` : "";
45
+ const local = overlay ? ` --connectors-local ${overlay}` : "";
46
+ return nextLine(`read the prepared page: ${cli} registry describe ${nodeType}${object}${local}`);
47
+ }
48
+ /** The line a refusal gains when the run had already staged files, which its progress lines may have named. */
49
+ export function discardedNote(count) {
50
+ return `Nothing was written: the ${count} file(s) this run staged, including any reported above, were discarded.`;
51
+ }
52
+ /** A write failed after the run succeeded, so the files written before it remain. */
53
+ export function partialWriteMessage(path, reason) {
54
+ return prefixed(`could not write ${path} (${reason}). The files written before it remain, so the overlay or bindings.json `
55
+ + "may be partly updated; fix the cause and run the same prepare again.");
56
+ }
39
57
  /**
40
58
  * A refusal whose message is the COMPLETE text the Python passed to
41
59
  * `sys.exit`, prefix included. Every other {@link Refusal} reaching the exit
@@ -31,11 +31,11 @@
31
31
  *
32
32
  * Ported from `integrations/scripts/_objects.py`; the refusal text is verbatim.
33
33
  */
34
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
35
34
  import { dirname, join } from "node:path";
36
35
  import { pyJsonStringify } from "../codecs/pyjson.js";
37
36
  import { pyStrip } from "../compat/pystr.js";
38
37
  import { Refusal, messageOf } from "../errors.js";
38
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "./staged-files.js";
39
39
  import { isPlainObject, pyOr, pyRepr, pySplitWhitespace, pyStrAny, pyTruthy, pyTypeName } from "./support.js";
40
40
  /**
41
41
  * Enough rows to choose from without dumping a catalog into the error. The
@@ -10,8 +10,14 @@ export declare function variantKey(entry: Record<string, unknown>): string;
10
10
  export declare function descriptorOverlayPath(path: string): string;
11
11
  /**
12
12
  * `_migrate_legacy_descriptor_roots`: move `<dst>/<key>/**` written by an
13
- * earlier `prepare` under `<dst>/descriptors/<key>/**`, bottom-up, leaving no
14
- * empty directory behind. Only roots the index still names are touched.
13
+ * earlier `prepare` under `<dst>/descriptors/<key>/**`, leaving no empty
14
+ * directory behind. Only roots the index still names are touched.
15
+ *
16
+ * The files are copied with the run's other writes, and only once those are
17
+ * written is each old file renamed over its copy and its emptied folder
18
+ * removed, so a refused run moves nothing. Where the file system refuses
19
+ * renames or deletes (Studio Web), the old files stay beside their copies;
20
+ * once the index names `descriptors/`, nothing reads them.
15
21
  */
16
22
  export declare function migrateLegacyDescriptorRoots(dst: string, entries: readonly unknown[]): void;
17
23
  /**
@@ -20,8 +20,8 @@
20
20
  * `_keep_prepared_schema` and `_lookup_specs_from_overlay` in
21
21
  * `prepare_connector.py`.
22
22
  */
23
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
24
- import { basename, dirname, join } from "node:path";
23
+ import { lstatSync, readdirSync, renameSync, rmdirSync } from "node:fs";
24
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
25
25
  import { pyJsonDocument } from "../codecs/pyjson.js";
26
26
  import { sortedByKeys, sortedStrings } from "../compat/compare.js";
27
27
  import { toPosix } from "../compat/paths.js";
@@ -30,6 +30,7 @@ import { SCHEMA_VERSION } from "../library-build/canonical.js";
30
30
  import { sidecarPath, unwrapV1Def } from "../library-reader.js";
31
31
  import { catalogReferences, loadInvariantCollections, specsFromFields } from "../lookup-specs.js";
32
32
  import { isEventNodeType } from "../model/library.js";
33
+ import { afterWrites, existsSync, mkdirSync, readFileSync, stagedReadText, writeFileSync } from "./staged-files.js";
33
34
  import { isPlainObject, pyGet, pyOr, pyStrAny, pyTruthy, pyTypeName } from "./support.js";
34
35
  export const INDEX_FILENAME = "index.json";
35
36
  export const DESCRIPTORS_ROOT = "descriptors";
@@ -55,8 +56,14 @@ export function descriptorOverlayPath(path) {
55
56
  }
56
57
  /**
57
58
  * `_migrate_legacy_descriptor_roots`: move `<dst>/<key>/**` written by an
58
- * earlier `prepare` under `<dst>/descriptors/<key>/**`, bottom-up, leaving no
59
- * empty directory behind. Only roots the index still names are touched.
59
+ * earlier `prepare` under `<dst>/descriptors/<key>/**`, leaving no empty
60
+ * directory behind. Only roots the index still names are touched.
61
+ *
62
+ * The files are copied with the run's other writes, and only once those are
63
+ * written is each old file renamed over its copy and its emptied folder
64
+ * removed, so a refused run moves nothing. Where the file system refuses
65
+ * renames or deletes (Studio Web), the old files stay beside their copies;
66
+ * once the index names `descriptors/`, nothing reads them.
60
67
  */
61
68
  export function migrateLegacyDescriptorRoots(dst, entries) {
62
69
  const legacyRoots = new Set();
@@ -71,27 +78,74 @@ export function migrateLegacyDescriptorRoots(dst, entries) {
71
78
  legacyRoots.add(parts[0]);
72
79
  }
73
80
  for (const legacyRoot of sortedStrings(legacyRoots)) {
74
- const sourceRoot = join(dst, legacyRoot);
75
- if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory())
81
+ const sourceRoot = legacyRootDir(dst, legacyRoot);
82
+ if (sourceRoot === undefined)
76
83
  continue;
77
- const destinationRoot = join(dst, DESCRIPTORS_ROOT, legacyRoot);
78
- const walk = (dir) => {
79
- const children = readdirSync(dir, { withFileTypes: true });
80
- const subdirs = children.filter((c) => c.isDirectory()).map((c) => c.name);
81
- // `os.walk(topdown=False)`: the children are handled before their parent.
82
- for (const name of subdirs)
83
- walk(join(dir, name));
84
- const destination = join(destinationRoot, toPosix(dir.slice(sourceRoot.length)).replace(/^\//, ""));
84
+ const moved = [];
85
+ // Dirent types do not follow symlinks, so a linked file or folder is neither and is skipped.
86
+ const walk = (dir, destination) => {
85
87
  mkdirSync(destination, { recursive: true });
86
- for (const child of children) {
87
- if (child.isFile())
88
- renameSync(join(dir, child.name), join(destination, child.name));
88
+ for (const child of readdirSync(dir, { withFileTypes: true })) {
89
+ if (child.isDirectory())
90
+ walk(join(dir, child.name), join(destination, child.name));
91
+ else if (child.isFile()) {
92
+ writeFileSync(join(destination, child.name), readFileSync(join(dir, child.name)));
93
+ moved.push([join(dir, child.name), join(destination, child.name)]);
94
+ }
89
95
  }
90
- for (const name of subdirs)
91
- rmdirSync(join(dir, name));
92
96
  };
93
- walk(sourceRoot);
94
- rmdirSync(sourceRoot);
97
+ walk(sourceRoot, join(dst, DESCRIPTORS_ROOT, legacyRoot));
98
+ afterWrites(() => moveOver(sourceRoot, moved));
99
+ }
100
+ }
101
+ /** A connector key as the legacy layout named its folder: one plain path segment. */
102
+ const LEGACY_ROOT = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
103
+ /**
104
+ * `<dst>/<legacyRoot>`, or `undefined` unless it is a real folder strictly
105
+ * inside `dst`. The name comes from `index.json`, a project file anyone with
106
+ * the project can edit, so `..` or a link must not reach the files beside the
107
+ * overlay.
108
+ */
109
+ function legacyRootDir(dst, legacyRoot) {
110
+ if (!LEGACY_ROOT.test(legacyRoot))
111
+ return undefined;
112
+ const root = resolve(dst, legacyRoot);
113
+ const inside = relative(resolve(dst), root);
114
+ if (inside === "" || inside.startsWith("..") || isAbsolute(inside))
115
+ return undefined;
116
+ try {
117
+ const stat = lstatSync(root);
118
+ return stat.isDirectory() && !stat.isSymbolicLink() ? root : undefined;
119
+ }
120
+ catch {
121
+ return undefined;
122
+ }
123
+ }
124
+ /**
125
+ * Rename each copied file over its copy, then remove the folders between it
126
+ * and `root` once empty, deepest first. Only these files and folders: anything
127
+ * added to the old tree since the copy stays where it is.
128
+ */
129
+ function moveOver(root, moved) {
130
+ try {
131
+ for (const [source, destination] of moved)
132
+ renameSync(source, destination);
133
+ }
134
+ catch {
135
+ return; // refused (Studio Web): the old files stay beside their copies
136
+ }
137
+ const folders = new Set();
138
+ for (const [source] of moved) {
139
+ for (let folder = dirname(source); folder.startsWith(root); folder = dirname(folder))
140
+ folders.add(folder);
141
+ }
142
+ for (const folder of [...folders].sort((a, b) => b.length - a.length)) {
143
+ try {
144
+ rmdirSync(folder);
145
+ }
146
+ catch {
147
+ // not empty, or refused: it stays
148
+ }
95
149
  }
96
150
  }
97
151
  function sortKey(entry, key) {
@@ -463,7 +517,7 @@ export function lookupSpecsFromOverlay(out, nodeType, objectName) {
463
517
  const entries = isPlainObject(index) ? pyGet(index, "entries", []) : [];
464
518
  if (!Array.isArray(entries))
465
519
  return new Map();
466
- const invariant = loadInvariantCollections(out);
520
+ const invariant = loadInvariantCollections(out, stagedReadText());
467
521
  for (const row of entries) {
468
522
  if (!isPlainObject(row) || row.nodeType !== nodeType)
469
523
  continue;
@@ -53,8 +53,8 @@ export interface PreparePorts {
53
53
  /** The project root the Python read as `Path.cwd()`. Defaults to the process's. */
54
54
  cwd?: string;
55
55
  timeoutMs?: number;
56
- /** How the pasteable `registry prepare` is spelled in the notes. */
57
- prepareCommand?: string;
56
+ /** How the CLI is spelled in the pasteable commands; defaults to {@link cliSpelling}. */
57
+ cli?: string;
58
58
  }
59
59
  /**
60
60
  * Where prepare writes its overlay when `--out` is not given, as shown to the
@@ -102,10 +102,13 @@ export declare function matchCatalogObject(ctx: BuildContext, connectorKey: stri
102
102
  * command PICKED and finds nothing, the other enabled connections for the
103
103
  * connector are probed, and the run starts over pinned to the one that
104
104
  * resolves everything (flow-builder-sdk#744). The Python re-exec'd itself for
105
- * that; here the bindings snapshot is restored and `prepareConnector` is
106
- * re-entered with `--connection-id <winner> --refresh-lookups`. The refresh is
105
+ * that; here the bindings snapshot is restored and the run starts over, inside
106
+ * the same held writes, with `--connection-id <winner> --refresh-lookups`. The refresh is
107
107
  * NOT optional: the ledger keys a resolution by `[nodeType, field, by, value]`
108
108
  * and not by connection, so an id recorded against the connection just
109
109
  * abandoned would come back as "already resolved" on the re-run.
110
+ *
111
+ * Nothing reaches the disk until the whole command, re-run included, has
112
+ * succeeded; a refusal writes nothing (`staged-files.ts`).
110
113
  */
111
114
  export declare function prepareConnector(options: PrepareOptions, ports: PreparePorts): Promise<PrepareReport>;
@@ -24,9 +24,9 @@
24
24
  * into the lines verbatim, as `args.out` and `args.bindings` did — while the
25
25
  * file system is reached through paths resolved against `cwd`.
26
26
  */
27
- import { existsSync, readFileSync } from "node:fs";
28
27
  import { relative, resolve } from "node:path";
29
28
  import { GUID_RE } from "../../core/binding-messages.js";
29
+ import { cliSpelling } from "../../core/cli-spelling.js";
30
30
  import { PACKAGE_NAME } from "../../package-name.js";
31
31
  import { afterLast } from "../compat/pystr.js";
32
32
  import { extractCanonical, isHttpPassthrough, nodeOf } from "../library-build/canonical.js";
@@ -41,7 +41,7 @@ import { prepareEvent } from "./event.js";
41
41
  import { integrationServiceEntry } from "./integration-service.js";
42
42
  import { descriptorUsage } from "./handoff.js";
43
43
  import { ResolutionsExhausted, allParentsResolved, resolvedParentValues, writeResolutions } from "./ledger.js";
44
- import { PREPARE_COMMAND, PrepareRefusal, ambiguousLabelsMessage, connectionChoiceLine, bindingsLine, bpmnConnectorUsageLines, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, flagDiffersFromSourceLine, keptSchemaMessage, labelTakenMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedFromIntegrationServiceLine, actionCanonicalizedLine, catalogRefreshFailedLine, refreshingCatalogLine, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, sharedLabelMessage, sourceLabelSuffix, tsNote, partlyUnreadableLabelLine, unreadableLabelLine, unresolvedOperationMessage, } from "./messages.js";
44
+ import { PrepareRefusal, ambiguousLabelsMessage, connectionChoiceLine, bindingsLine, bpmnConnectorUsageLines, candidateLabel, connectionLine, describeNextLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, flagDiffersFromSourceLine, keptSchemaMessage, labelTakenMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedFromIntegrationServiceLine, actionCanonicalizedLine, catalogRefreshFailedLine, refreshingCatalogLine, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, sharedLabelMessage, sourceLabelSuffix, tsNote, partlyUnreadableLabelLine, unreadableLabelLine, unresolvedOperationMessage, } from "./messages.js";
45
45
  import { ObjectMatchRefusal, RESOLUTIONS_FILENAME, matchObject, recordObjectResolution } from "./objects.js";
46
46
  import { countInputFields, keepPreparedSchema, lookupSpecsFromOverlay, mergeFilesInto, parentFields as overlayParentFields, schemaActionParents, } from "./overlay.js";
47
47
  import { declaredParents, objectActionRequest, objectActionSpecs, parseFields } from "./parents.js";
@@ -50,6 +50,7 @@ import { retryOtherConnections } from "./retry.js";
50
50
  import { repointSource } from "./source-import.js";
51
51
  import { sourceBindingLabels } from "./source-labels.js";
52
52
  import { isPlainObject, pyGet, pyOr, pyPathJoin, pyPathlibStr, pyStrAny, pyTruthy } from "./support.js";
53
+ import { existsSync, readFileSync, stagedWrites } from "./staged-files.js";
53
54
  import { workPath } from "../../workdir.js";
54
55
  /**
55
56
  * Where prepare writes its overlay when `--out` is not given, as shown to the
@@ -119,7 +120,7 @@ export async function matchCatalogObject(ctx, connectorKey, wanted, verb) {
119
120
  function chooseLabels(options, ports) {
120
121
  const info = (line) => ports.reporter.info(line);
121
122
  const warn = (line) => ports.reporter.warn(line);
122
- const command = ports.prepareCommand ?? PREPARE_COMMAND;
123
+ const command = `${ports.cli ?? cliSpelling()} registry prepare`;
123
124
  const labels = sourceBindingLabels({
124
125
  cwd: ports.cwd ?? process.cwd(),
125
126
  out: options.out ?? defaultOverlayOut(ports.cwd ?? process.cwd()),
@@ -263,13 +264,19 @@ async function connect(options, ports) {
263
264
  * command PICKED and finds nothing, the other enabled connections for the
264
265
  * connector are probed, and the run starts over pinned to the one that
265
266
  * resolves everything (flow-builder-sdk#744). The Python re-exec'd itself for
266
- * that; here the bindings snapshot is restored and `prepareConnector` is
267
- * re-entered with `--connection-id <winner> --refresh-lookups`. The refresh is
267
+ * that; here the bindings snapshot is restored and the run starts over, inside
268
+ * the same held writes, with `--connection-id <winner> --refresh-lookups`. The refresh is
268
269
  * NOT optional: the ledger keys a resolution by `[nodeType, field, by, value]`
269
270
  * and not by connection, so an id recorded against the connection just
270
271
  * abandoned would come back as "already resolved" on the re-run.
272
+ *
273
+ * Nothing reaches the disk until the whole command, re-run included, has
274
+ * succeeded; a refusal writes nothing (`staged-files.ts`).
271
275
  */
272
- export async function prepareConnector(options, ports) {
276
+ export function prepareConnector(options, ports) {
277
+ return stagedWrites(() => prepareOrRetry(options, ports));
278
+ }
279
+ async function prepareOrRetry(options, ports) {
273
280
  const info = (line) => ports.reporter.info(line);
274
281
  const warn = (line) => ports.reporter.warn(line);
275
282
  // Whether the CALLER chose the connection, recorded before discovery answers
@@ -324,7 +331,7 @@ export async function prepareConnector(options, ports) {
324
331
  // The labels this run chose ride along as flags, so the re-entry binds under
325
332
  // the same names instead of deriving them again.
326
333
  const labels = connected.bound ? { bindConnection: connected.bound.connection, bindFolder: connected.bound.folder } : {};
327
- return prepareConnector({ ...options, ...labels, connectionId: chosenId, refreshLookups: true }, ports);
334
+ return prepareOrRetry({ ...options, ...labels, connectionId: chosenId, refreshLookups: true }, ports);
328
335
  }
329
336
  }
330
337
  /**
@@ -336,7 +343,8 @@ async function prepareOnConnection(options, ports, connected, registryRefresh) {
336
343
  const cwd = ports.cwd ?? process.cwd();
337
344
  const info = (line) => ports.reporter.info(line);
338
345
  const warn = (line) => ports.reporter.warn(line);
339
- const command = ports.prepareCommand ?? PREPARE_COMMAND;
346
+ const cli = ports.cli ?? cliSpelling();
347
+ const command = `${cli} registry prepare`;
340
348
  const { connectorKey } = options;
341
349
  // Rebound to the activity's own action id when Integration Service supplies
342
350
  // the entry, so the overlay path, the summary and the handoff all spell the
@@ -349,6 +357,7 @@ async function prepareOnConnection(options, ports, connected, registryRefresh) {
349
357
  const importSpecifier = options.importSpecifier ?? PACKAGE_NAME;
350
358
  const jsonOnly = options.jsonOnly ?? false;
351
359
  const { connectionId, bound, rollback } = connected;
360
+ const describeOverlay = out === resolve(cwd, defaultOverlayOut(cwd)) ? undefined : outShown;
352
361
  const ctx = createBuildContext({ client: ports.client, caches: ports.caches ?? memoryCaches(), reporter: ports.reporter, concurrency: 1, timeoutMs: ports.timeoutMs }, { connectionId, parentFields });
353
362
  let nodeType = `uipath.connector.${connectorKey}.${action}`;
354
363
  let fromIntegrationService;
@@ -376,6 +385,7 @@ async function prepareOnConnection(options, ports, connected, registryRefresh) {
376
385
  prepareCommand: command,
377
386
  });
378
387
  if (!("missed" in event)) {
388
+ info(describeNextLine(cli, event.eventNodeType, event.objectName, describeOverlay));
379
389
  return { kind: "event", nodeType: event.eventNodeType, connectionId, bound, objectName: event.objectName };
380
390
  }
381
391
  const probes = [actionProbe.miss, event.missed];
@@ -574,5 +584,6 @@ async function prepareOnConnection(options, ports, connected, registryRefresh) {
574
584
  for (const line of usage)
575
585
  info(nextLine(line));
576
586
  }
587
+ info(describeNextLine(cli, nodeType, options.allObjects ? "<object>" : objectName, describeOverlay));
577
588
  return { kind: "action", nodeType, connectionId, bound, variants, inputFields, kept, objectName };
578
589
  }
@@ -18,12 +18,12 @@
18
18
  * `generate_connectors_ts.py --library <out> --output <out>`; the generator is
19
19
  * now `renderDescriptorModules`, called in-process.
20
20
  */
21
- import { mkdirSync, writeFileSync } from "node:fs";
22
21
  import { join } from "node:path";
23
22
  import { Refusal } from "../errors.js";
24
23
  import { formatRequiredInputGaps, renderDescriptorModules } from "../generators/descriptors.js";
25
24
  import { loadLibrary } from "../library-reader.js";
26
25
  import { overlayHasGenericExpanded } from "./overlay.js";
26
+ import { mkdirSync, stagedReadText, writeFileSync } from "./staged-files.js";
27
27
  /**
28
28
  * Write `<out>/<connectorKey>.ts` for every connector in the overlay and
29
29
  * return the module names written; nothing at all under `--json-only`.
@@ -35,7 +35,7 @@ import { overlayHasGenericExpanded } from "./overlay.js";
35
35
  export function regenerateDescriptors(out, importSpecifier, jsonOnly = false) {
36
36
  if (jsonOnly)
37
37
  return [];
38
- const library = loadLibrary(out);
38
+ const library = loadLibrary(out, stagedReadText());
39
39
  const output = renderDescriptorModules(library, {
40
40
  importSpecifier,
41
41
  includeGenericExpanded: overlayHasGenericExpanded(out),
@@ -19,13 +19,14 @@
19
19
  * Ported from `_IMPORT_RE`, `_resolves_to`, `_sources_importing`,
20
20
  * `_repoint_import` and `_repoint_source` in `prepare_connector.py`.
21
21
  */
22
- import { readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
22
+ import { readdirSync, realpathSync } from "node:fs";
23
23
  import { basename, dirname, join, resolve } from "node:path";
24
24
  import { compareCodePoints } from "../compat/compare.js";
25
25
  import { relativePosix } from "../compat/paths.js";
26
26
  import { workPath } from "../../workdir.js";
27
27
  import { isFile } from "../files.js";
28
28
  import { repointedLine, severalSourcesLine, sourceNotAFileLine } from "./messages.js";
29
+ import { existsSync, readFileSync, writeFileSync } from "./staged-files.js";
29
30
  /**
30
31
  * `import … from "<specifier>"` — the specifier is group 3, quotes preserved.
31
32
  * Anchored to a real import statement on purpose (see the module comment).
@@ -180,9 +181,10 @@ export function repointImport(source, baked, prepared) {
180
181
  */
181
182
  export function repointSource(options) {
182
183
  const cwd = options.cwd ?? process.cwd();
183
- const prepared = canonicalPath(resolve(cwd, options.out, `${options.connectorKey}.ts`));
184
- if (options.noSourceRewrite || !isFile(prepared))
184
+ const module = resolve(cwd, options.out, `${options.connectorKey}.ts`);
185
+ if (options.noSourceRewrite || !existsSync(module))
185
186
  return [];
187
+ const prepared = canonicalPath(module);
186
188
  const root = canonicalPath(cwd);
187
189
  // Where `registry pull` put the module: `.flow-sdk/connectors/`, or a legacy `connectors/`.
188
190
  const baked = canonicalPath(join(workPath("connectors", { cwd: root }), `${options.connectorKey}.ts`));
@@ -0,0 +1,21 @@
1
+ import { type ReadText } from "../../core/read-text.js";
2
+ /**
3
+ * Run `work` with its writes held, then write them all, or none when it throws.
4
+ * One run at a time per process: the held files are process-wide, so a second
5
+ * run started before the first ends is refused rather than mixed into it.
6
+ */
7
+ export declare function stagedWrites<T>(work: () => Promise<T>): Promise<T>;
8
+ /** Run `step` once the held files are written: at once outside a run, never for a run that throws. */
9
+ export declare function afterWrites(step: () => void): void;
10
+ export declare function writeFileSync(path: string, content: string | Uint8Array, encoding?: "utf8"): void;
11
+ /** A held file's parent is made when it is written, so there is nothing to make yet. */
12
+ export declare function mkdirSync(path: string, options: {
13
+ recursive: true;
14
+ }): void;
15
+ export declare function existsSync(path: string): boolean;
16
+ export declare function readFileSync(path: string): Uint8Array;
17
+ export declare function readFileSync(path: string, encoding: "utf8"): string;
18
+ /** Drops a held file; only outside a run does it reach the disk. */
19
+ export declare function rmSync(path: string): void;
20
+ /** The library reader `loadLibrary` takes, seeing held files first. */
21
+ export declare function stagedReadText(): ReadText;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `prepare`'s file writes, held in memory until the run has succeeded.
3
+ *
4
+ * Studio Web's project file system refuses deletes, and so renames, which
5
+ * rules out both ways a failed run used to be cleaned up: writing a scratch
6
+ * copy and swapping it in, and removing what a refusal left behind. Instead
7
+ * the prepare modules read and write through these functions. While
8
+ * {@link stagedWrites} runs, a write lands in memory and a later read in the
9
+ * same run sees it; when the run resolves, each file is written once, with a
10
+ * plain write. A run that throws writes nothing.
11
+ *
12
+ * Outside {@link stagedWrites} every function is the `node:fs` one, so a
13
+ * helper called on its own (the tests, `mergeInto`) writes at once as before.
14
+ */
15
+ import * as fs from "node:fs";
16
+ import { basename, dirname, resolve } from "node:path";
17
+ import { defaultReadText } from "../../core/read-text.js";
18
+ import { Refusal, messageOf } from "../errors.js";
19
+ import { PrepareRefusal, discardedNote, partialWriteMessage } from "./messages.js";
20
+ let pending;
21
+ let afterwards = [];
22
+ /**
23
+ * Run `work` with its writes held, then write them all, or none when it throws.
24
+ * One run at a time per process: the held files are process-wide, so a second
25
+ * run started before the first ends is refused rather than mixed into it.
26
+ */
27
+ export async function stagedWrites(work) {
28
+ if (pending !== undefined) {
29
+ throw new Error("registry prepare is already running in this process; run one prepare at a time, and wait for it to end before the next.");
30
+ }
31
+ const files = new Map();
32
+ pending = files;
33
+ const then = [];
34
+ afterwards = then;
35
+ let result;
36
+ try {
37
+ result = await work();
38
+ }
39
+ catch (error) {
40
+ if (error instanceof Refusal && files.size > 0)
41
+ error.message += `\n${discardedNote(files.size)}`;
42
+ throw error;
43
+ }
44
+ finally {
45
+ pending = undefined;
46
+ }
47
+ // An index written before a failed write would name descriptors that are not there.
48
+ const indexLast = [...files].sort(([a], [b]) => Number(basename(a) === "index.json") - Number(basename(b) === "index.json"));
49
+ for (const [path, content] of indexLast) {
50
+ try {
51
+ fs.mkdirSync(dirname(path), { recursive: true });
52
+ fs.writeFileSync(path, content);
53
+ }
54
+ catch (error) {
55
+ throw new PrepareRefusal(partialWriteMessage(path, messageOf(error)), "PREPARE_WRITE_FAILED");
56
+ }
57
+ }
58
+ for (const step of then)
59
+ step();
60
+ return result;
61
+ }
62
+ /** Run `step` once the held files are written: at once outside a run, never for a run that throws. */
63
+ export function afterWrites(step) {
64
+ if (pending !== undefined)
65
+ afterwards.push(step);
66
+ else
67
+ step();
68
+ }
69
+ function held(path) {
70
+ return pending?.get(resolve(path));
71
+ }
72
+ export function writeFileSync(path, content, encoding) {
73
+ if (pending !== undefined)
74
+ pending.set(resolve(path), content);
75
+ else
76
+ fs.writeFileSync(path, content, encoding);
77
+ }
78
+ /** A held file's parent is made when it is written, so there is nothing to make yet. */
79
+ export function mkdirSync(path, options) {
80
+ if (pending === undefined)
81
+ fs.mkdirSync(path, options);
82
+ }
83
+ export function existsSync(path) {
84
+ return held(path) !== undefined || fs.existsSync(path);
85
+ }
86
+ export function readFileSync(path, encoding) {
87
+ const content = held(path);
88
+ if (content === undefined)
89
+ return encoding === undefined ? fs.readFileSync(path) : fs.readFileSync(path, encoding);
90
+ if (encoding === undefined)
91
+ return typeof content === "string" ? new TextEncoder().encode(content) : content;
92
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
93
+ }
94
+ /** Drops a held file; only outside a run does it reach the disk. */
95
+ export function rmSync(path) {
96
+ if (pending !== undefined)
97
+ pending.delete(resolve(path));
98
+ else
99
+ fs.rmSync(path);
100
+ }
101
+ /** The library reader `loadLibrary` takes, seeing held files first. */
102
+ export function stagedReadText() {
103
+ const read = defaultReadText();
104
+ return (path) => {
105
+ const content = held(path);
106
+ if (content === undefined)
107
+ return read(path);
108
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
109
+ };
110
+ }
@@ -109,6 +109,24 @@ export declare class OfflineUipClient implements UipClient {
109
109
  * ask for with `host` (see `host.ts`): every call then spawns its own `uip`,
110
110
  * which is how the registry ran before flow-builder-sdk#965 and the shape to
111
111
  * compare against when a prepare misbehaves.
112
+ *
113
+ * A host that cannot start a process (Studio Web runs the SDK's CLI in a
114
+ * sandbox with no `child_process`), and so cannot pass a client in, hands one
115
+ * over before the run: `globalThis[Symbol.for('uipath.maestro-builder-sdk.uip')]
116
+ * = client`, an object with the {@link UipClient} shape. Its `run(argv, options)`
117
+ * gets the argv `uip` would (`argv[0]` is `"uip"`) and resolves a {@link UipResult}
118
+ * — `code` 0 and the JSON envelope on `stdout` for a success, as the CLI prints
119
+ * it. The argv carries the author's values (`-f`, `--resolve`), so a host
120
+ * hands it to the CLI as an array and never through a shell. A rejection or a
121
+ * malformed answer reads as a failed run (`code` 1, the reason on `stderr`),
122
+ * and a call past its `timeoutMs` as a timed-out one. It answers every call in
123
+ * place of a spawned `uip`, `host` and `FLOW_SDK_UIP_HOST` included, and no
124
+ * `node:child_process` is loaded. Calls can
125
+ * overlap: the library builder runs several at once. `registry prepare` calls
126
+ * `close()`, if it has one, when it ends, and nothing else does, so a client
127
+ * that outlives the command leaves `close` out. `UIP_REPLAY_DIR` still wins over it, so a replay
128
+ * never reaches the tenant, and `UIP_RECORD_DIR` records what it answers. This is
129
+ * the tenant's counterpart of the library's `readText` slot (`core/read-text.ts`).
112
130
  */
113
131
  export declare function defaultUipClient(options?: UipRunOptions & {
114
132
  host?: HostUipClientOptions | boolean;
@@ -210,6 +210,59 @@ export class OfflineUipClient {
210
210
  };
211
211
  }
212
212
  }
213
+ const HOST_UIP = Symbol.for("uipath.maestro-builder-sdk.uip");
214
+ /** A call the slot's client could not answer, read like a failed run. */
215
+ function slotFailure(reason) {
216
+ return { code: 1, signal: null, stdout: "", stderr: `uip slot: ${reason}`, timedOut: false };
217
+ }
218
+ /** Why `answer` is not a {@link UipResult}, or `undefined` when it is one. */
219
+ function notAResult(answer) {
220
+ if (answer === null || typeof answer !== "object")
221
+ return `answered ${answer === null ? "null" : typeof answer}, not a result object`;
222
+ const { code, stdout, stderr } = answer;
223
+ if (typeof code !== "number")
224
+ return `answered a result whose code is ${typeof code}, not a number`;
225
+ if (typeof stdout !== "string" || typeof stderr !== "string")
226
+ return "answered a result whose stdout or stderr is not a string";
227
+ return undefined;
228
+ }
229
+ /**
230
+ * The host's client in the slot, held to the contract the spawning client
231
+ * keeps: a timeout is enforced, and a rejection or a malformed answer is a
232
+ * failed run, never a throw.
233
+ */
234
+ class SlotUipClient {
235
+ hosted;
236
+ defaults;
237
+ constructor(hosted, defaults) {
238
+ this.hosted = hosted;
239
+ this.defaults = defaults;
240
+ }
241
+ async run(argv, options = {}) {
242
+ const answer = Promise.resolve()
243
+ .then(() => this.hosted.run(argv, options))
244
+ .then((result) => {
245
+ const reason = notAResult(result);
246
+ return reason === undefined ? result : slotFailure(reason);
247
+ }, (error) => slotFailure(`the call failed: ${error instanceof Error ? error.message : String(error)}`));
248
+ const timeoutMs = options.timeoutMs ?? this.defaults.timeoutMs;
249
+ if (timeoutMs === undefined || timeoutMs <= 0)
250
+ return answer;
251
+ let timer;
252
+ const expired = new Promise((resolve) => {
253
+ timer = setTimeout(() => resolve({ code: null, signal: null, stdout: "", stderr: `uip slot: no answer within ${timeoutMs} ms`, timedOut: true }), timeoutMs);
254
+ });
255
+ try {
256
+ return await Promise.race([answer, expired]);
257
+ }
258
+ finally {
259
+ clearTimeout(timer);
260
+ }
261
+ }
262
+ close() {
263
+ this.hosted.close?.();
264
+ }
265
+ }
213
266
  /**
214
267
  * The client a verb should use, honouring the recording and replay switches.
215
268
  *
@@ -237,12 +290,39 @@ export class OfflineUipClient {
237
290
  * ask for with `host` (see `host.ts`): every call then spawns its own `uip`,
238
291
  * which is how the registry ran before flow-builder-sdk#965 and the shape to
239
292
  * compare against when a prepare misbehaves.
293
+ *
294
+ * A host that cannot start a process (Studio Web runs the SDK's CLI in a
295
+ * sandbox with no `child_process`), and so cannot pass a client in, hands one
296
+ * over before the run: `globalThis[Symbol.for('uipath.maestro-builder-sdk.uip')]
297
+ * = client`, an object with the {@link UipClient} shape. Its `run(argv, options)`
298
+ * gets the argv `uip` would (`argv[0]` is `"uip"`) and resolves a {@link UipResult}
299
+ * — `code` 0 and the JSON envelope on `stdout` for a success, as the CLI prints
300
+ * it. The argv carries the author's values (`-f`, `--resolve`), so a host
301
+ * hands it to the CLI as an array and never through a shell. A rejection or a
302
+ * malformed answer reads as a failed run (`code` 1, the reason on `stderr`),
303
+ * and a call past its `timeoutMs` as a timed-out one. It answers every call in
304
+ * place of a spawned `uip`, `host` and `FLOW_SDK_UIP_HOST` included, and no
305
+ * `node:child_process` is loaded. Calls can
306
+ * overlap: the library builder runs several at once. `registry prepare` calls
307
+ * `close()`, if it has one, when it ends, and nothing else does, so a client
308
+ * that outlives the command leaves `close` out. `UIP_REPLAY_DIR` still wins over it, so a replay
309
+ * never reaches the tenant, and `UIP_RECORD_DIR` records what it answers. This is
310
+ * the tenant's counterpart of the library's `readText` slot (`core/read-text.ts`).
240
311
  */
241
312
  export function defaultUipClient(options = {}) {
242
313
  const { replay, record } = uipSwitches();
243
314
  if (replay)
244
315
  return new ReplayUipClient(replay);
245
316
  const { host, ...runOptions } = options;
317
+ const hosted = globalThis[HOST_UIP];
318
+ if (hosted !== undefined && hosted !== null) {
319
+ if (typeof hosted.run !== "function") {
320
+ throw new Error("globalThis[Symbol.for('uipath.maestro-builder-sdk.uip')] holds no uip client: it needs a " +
321
+ "run(argv, options) method that resolves { code, stdout, stderr }.");
322
+ }
323
+ const slot = new SlotUipClient(hosted, runOptions);
324
+ return record ? new RecordingUipClient(slot, record) : slot;
325
+ }
246
326
  const spawning = new SpawnUipClient(runOptions);
247
327
  // One `uip mcp serve` for the command's whole chain of calls, where the
248
328
  // caller asked for it and the operator has not switched it off. Serial, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.45.1",
3
+ "version": "6.46.0",
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",
@@ -93,5 +93,5 @@
93
93
  "@types/node": "^22.7.0",
94
94
  "esbuild": "^0.28.1"
95
95
  },
96
- "gitref": "f8fe049e27996a1862ea9ce5c48507b15b6178d0"
96
+ "gitref": "b61f819d0271c38d3231a0183daeb2a5f880f9a4"
97
97
  }