@uipath/maestro-builder-sdk 6.9.1 → 6.9.3

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.
@@ -130,8 +130,8 @@ export function registerRegistryCommands(program) {
130
130
  .option('--connection-id <id>', 'connection to read the schema through')
131
131
  .option('--connection <name>', 'pick the connection by name when several match')
132
132
  .option('--bindings <file>', 'bindings file to update (default: ./bindings.json)')
133
- .option('--bind-connection <name>', 'symbolic connection name (default: the connector suffix)')
134
- .option('--bind-folder <name>', 'symbolic folder name (default: shared)')
133
+ .option('--bind-connection <name>', 'symbolic connection name (default: the one label the source uses for this connector, else the connector suffix)')
134
+ .option('--bind-folder <name>', 'symbolic folder name (default: the one folder label the source uses, else shared)')
135
135
  // A SEPARATE key, not commander's negation of `--bindings`: sharing one key
136
136
  // made the result order-dependent, so `--no-bindings --bindings b.json` and
137
137
  // the reverse disagreed. argparse had two independent options.
package/dist/decompile.js CHANGED
@@ -1283,10 +1283,13 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1283
1283
  // only what a pre-1.6 file carries. Reading strings alone made every
1284
1284
  // modern file decompile to a conversational agent with no turn wiring.
1285
1285
  const raw = unwrapSource(st[k]);
1286
- if (raw === undefined || raw === '')
1286
+ if (raw === undefined || raw === '' || raw === '=js:')
1287
1287
  return undefined;
1288
- const js = raw.startsWith('=js:') ? raw.slice(4) : raw;
1289
- return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : str(js);
1288
+ // Only a `=js:` value is an expression: `emitRef` lowers a single path to
1289
+ // `out(...)` and anything composite to a `js` template. Any other string
1290
+ // is literal text, as the product reads it (flow-schema
1291
+ // `coerceToExpressionValue`), even when it looks like `$vars.…`.
1292
+ return raw.startsWith('=js:') ? emitRef(raw.slice(4), imp, inputNames) : str(raw);
1290
1293
  };
1291
1294
  // `'simple'` mode DERIVES the four runtime fields from `context`, so only
1292
1295
  // the context binding is the author's; recovering the derived ones would
@@ -1304,7 +1307,9 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1304
1307
  return `${imp.use('conversationalAgent')}(${opts([
1305
1308
  ['model', str(plain(i.model) ?? '')],
1306
1309
  ['systemPrompt', promptSrc(i.systemPrompt) ?? str('')],
1307
- ['settings', settingsSrc],
1310
+ // `settings` is required on ConversationalAgentInputs, so an empty one
1311
+ // stays as `{}`, which check then reports as CONV_AGENT_NO_TURN.
1312
+ ['settings', settingsSrc || '{}'],
1308
1313
  ['endExchange', unwrapPlain(i.endExchange) === false ? 'false' : undefined],
1309
1314
  ['temperature', num(i.temperature)],
1310
1315
  ['maxTokenPerResponse', num(i.maxTokenPerResponse)],
@@ -44,6 +44,27 @@ export declare function exitText(error: unknown): string;
44
44
  export declare function connectionLine(row: Record<string, unknown>, connectionId: string): string;
45
45
  /** ` bindings: <symbolic name> -> <bindings.json>`, once per row this run changed. */
46
46
  export declare function bindingsLine(symbolicName: string, bindingsPath: string): string;
47
+ /** ` (the label Demo.flow.ts uses)` — appended to a bindings line whose name came from the source. */
48
+ export declare function sourceLabelSuffix(files: readonly string[]): string;
49
+ /**
50
+ * The source uses several labels for this one connector, and one prepare binds one
51
+ * connection: refused before any tenant call or write, with the command for each.
52
+ */
53
+ export declare function ambiguousLabelsMessage(attribute: "connection" | "folder", connectorKey: string, action: string, labels: ReadonlyMap<string, readonly string[]>, command: string): string;
54
+ /**
55
+ * The source gives this connector's steps a connection label that steps of another
56
+ * connector use too. One label binds one connection id, so one of them would dial the
57
+ * other's connection — a mismatch compile cannot see, because the label IS declared.
58
+ */
59
+ export declare function sharedLabelMessage(label: string, connectorKey: string, others: readonly string[], files: readonly string[]): string;
60
+ /** bindings.json already binds the label to a connection of ANOTHER connector: refused, the row is left as it is. */
61
+ export declare function labelTakenMessage(label: string, connectorKey: string, existingId: string, existingConnector: string, bindingsShown: string): string;
62
+ /** `--bind-connection`/`--bind-folder` names a label the source does not use for this connector. */
63
+ export declare function flagDiffersFromSourceLine(attribute: "connection" | "folder", flag: string, labels: readonly string[], files: readonly string[]): string;
64
+ /** One label was read and bound, but another call's could not be: the line names that call. */
65
+ export declare function partlyUnreadableLabelLine(attribute: "connection" | "folder", label: string, reasons: readonly string[]): string;
66
+ /** A label that could not be read statically: the default is bound, and the line says why. */
67
+ export declare function unreadableLabelLine(attribute: "connection" | "folder", fallback: string, reasons: readonly string[]): string;
47
68
  /** `_missing_generic_schema_message`: a Generic action that expanded to nothing. */
48
69
  export declare function missingGenericSchemaMessage(operation: string, objectName?: string | null): string;
49
70
  /** One `registry get` probe that missed, as a refusal quotes it. */
@@ -62,6 +62,68 @@ export function connectionLine(row, connectionId) {
62
62
  export function bindingsLine(symbolicName, bindingsPath) {
63
63
  return ` bindings: ${symbolicName} -> ${bindingsPath}`;
64
64
  }
65
+ /** ` (the label Demo.flow.ts uses)` — appended to a bindings line whose name came from the source. */
66
+ export function sourceLabelSuffix(files) {
67
+ return ` (the label ${files.join(", ")} ${files.length === 1 ? "uses" : "use"})`;
68
+ }
69
+ /**
70
+ * The source uses several labels for this one connector, and one prepare binds one
71
+ * connection: refused before any tenant call or write, with the command for each.
72
+ */
73
+ export function ambiguousLabelsMessage(attribute, connectorKey, action, labels, command) {
74
+ const flag = attribute === "connection" ? "--bind-connection" : "--bind-folder";
75
+ const files = [...new Set([...labels.values()].flat())];
76
+ const lines = [...labels.keys()].map((label) => ` ${command} ${connectorKey} ${action} ${flag} ${label} --connection-id <id>`);
77
+ return prefixed(`${files.join(", ")} ${files.length === 1 ? "uses" : "use"} ${labels.size} ${attribute} labels for ${connectorKey} `
78
+ + `(${[...labels.keys()].join(", ")}), and one prepare binds one ${attribute === "connection" ? "connection" : "folder key"}. `
79
+ + `Bind each label to the connection it means, one run per label:\n${lines.join("\n")}\n`
80
+ + (files.length > 1
81
+ ? `The labels come from different files; if only one of them is the flow you are preparing, `
82
+ + `pass --source <file> to read just that one.\n`
83
+ : "")
84
+ + "Nothing was written.");
85
+ }
86
+ /**
87
+ * The source gives this connector's steps a connection label that steps of another
88
+ * connector use too. One label binds one connection id, so one of them would dial the
89
+ * other's connection — a mismatch compile cannot see, because the label IS declared.
90
+ */
91
+ export function sharedLabelMessage(label, connectorKey, others, files) {
92
+ return prefixed(`${files.join(", ")} ${files.length === 1 ? "uses" : "use"} connection label "${label}" for ${connectorKey} and for `
93
+ + `${others.join(", ")}. One bindings.json label binds one connection, so one of those steps would run on the `
94
+ + `other connector's connection. Give each connector its own label in the source (for example `
95
+ + `connection: '${afterLastDash(connectorKey)}') and re-run, or pass --bind-connection <label> to choose the name `
96
+ + "yourself. Nothing was written.");
97
+ }
98
+ /** bindings.json already binds the label to a connection of ANOTHER connector: refused, the row is left as it is. */
99
+ export function labelTakenMessage(label, connectorKey, existingId, existingConnector, bindingsShown) {
100
+ return prefixed(`${bindingsShown} already binds "${label}" to ${existingId}, a ${existingConnector} connection. Writing this `
101
+ + `${connectorKey} connection under the same label would repoint every ${existingConnector} step that uses it. `
102
+ + `Give the ${connectorKey} steps their own label in the source, or pass --bind-connection <label>. Nothing was written.`);
103
+ }
104
+ /** `--bind-connection`/`--bind-folder` names a label the source does not use for this connector. */
105
+ export function flagDiffersFromSourceLine(attribute, flag, labels, files) {
106
+ const option = attribute === "connection" ? "--bind-connection" : "--bind-folder";
107
+ return ` WARN: ${option} ${flag}, but ${files.join(", ")} ${files.length === 1 ? "uses" : "use"} ${attribute} `
108
+ + `${labels.map((l) => `"${l}"`).join(", ")} for this connector; compile refuses a label bindings.json does not `
109
+ + `declare (BINDING_UNDECLARED), so make the two match`;
110
+ }
111
+ /** One label was read and bound, but another call's could not be: the line names that call. */
112
+ export function partlyUnreadableLabelLine(attribute, label, reasons) {
113
+ return ` bindings: bound under "${label}", the ${attribute} label the source uses; could not read one more `
114
+ + `(${reasons[0]}${reasons.length > 1 ? `, and ${reasons.length - 1} more` : ""}) — if that step is this `
115
+ + `connector's under another label, compile refuses it (BINDING_UNDECLARED)`;
116
+ }
117
+ /** A label that could not be read statically: the default is bound, and the line says why. */
118
+ export function unreadableLabelLine(attribute, fallback, reasons) {
119
+ const option = attribute === "connection" ? "--bind-connection" : "--bind-folder";
120
+ return ` bindings: could not read the ${attribute} label the source uses (${reasons[0]}${reasons.length > 1 ? `, and ${reasons.length - 1} more` : ""}); `
121
+ + `bound under "${fallback}" — pass ${option} <label> if the source uses another`;
122
+ }
123
+ function afterLastDash(key) {
124
+ const i = key.lastIndexOf("-");
125
+ return i >= 0 ? key.slice(i + 1) : key;
126
+ }
65
127
  // ---------------------------------------------------------------------------
66
128
  // Refusals of the action path
67
129
  /** `_missing_generic_schema_message`: a Generic action that expanded to nothing. */
@@ -12,9 +12,12 @@ export interface PrepareOptions {
12
12
  connection?: string;
13
13
  /** `--bindings` (default `./bindings.json`). */
14
14
  bindings?: string;
15
- /** `--bind-connection` (default: the connector's last dash-segment). */
15
+ /**
16
+ * `--bind-connection`. Default: the one `connection:` label the source already
17
+ * uses for this connector, else the connector's last dash-segment.
18
+ */
16
19
  bindConnection?: string;
17
- /** `--bind-folder` (default `shared`). */
20
+ /** `--bind-folder`. Default: the one `folder:` label the source uses for this connector, else `shared`. */
18
21
  bindFolder?: string;
19
22
  noBindings?: boolean;
20
23
  /** `--object`; mutually exclusive with `allObjects`. */
@@ -24,7 +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";
27
28
  import { resolve } from "node:path";
29
+ import { GUID_RE } from "../../core/binding-messages.js";
28
30
  import { PACKAGE_NAME } from "../../package-name.js";
29
31
  import { afterLast } from "../compat/pystr.js";
30
32
  import { extractCanonical, isHttpPassthrough, nodeOf } from "../library-build/canonical.js";
@@ -34,17 +36,18 @@ import { expandGenericForConnector, genericTemplatesByOp } from "../library-buil
34
36
  import { planLibrary } from "../library-build/plan.js";
35
37
  import { MemoryResponseCache } from "../uip/response-cache.js";
36
38
  import { registryCatalog } from "./actions.js";
37
- import { discoverConnection, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, } from "./connections.js";
39
+ import { discoverConnection, listConnections, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, } from "./connections.js";
38
40
  import { prepareEvent } from "./event.js";
39
41
  import { descriptorUsage } from "./handoff.js";
40
42
  import { ResolutionsExhausted, allParentsResolved, resolvedParentValues, writeResolutions } from "./ledger.js";
41
- import { PREPARE_COMMAND, PrepareRefusal, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, keptSchemaMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, tsNote, unresolvedOperationMessage, } from "./messages.js";
43
+ import { PREPARE_COMMAND, PrepareRefusal, ambiguousLabelsMessage, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, flagDiffersFromSourceLine, keptSchemaMessage, labelTakenMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, sharedLabelMessage, sourceLabelSuffix, tsNote, partlyUnreadableLabelLine, unreadableLabelLine, unresolvedOperationMessage, } from "./messages.js";
42
44
  import { ObjectMatchRefusal, RESOLUTIONS_FILENAME, matchObject, recordObjectResolution } from "./objects.js";
43
45
  import { countInputFields, keepPreparedSchema, lookupSpecsFromOverlay, mergeFilesInto, parentFields as overlayParentFields, schemaActionParents, } from "./overlay.js";
44
46
  import { declaredParents, objectActionRequest, objectActionSpecs, parseFields } from "./parents.js";
45
47
  import { regenerateDescriptors } from "./regenerate.js";
46
48
  import { retryOtherConnections } from "./retry.js";
47
49
  import { repointSource } from "./source-import.js";
50
+ import { sourceBindingLabels } from "./source-labels.js";
48
51
  import { isPlainObject, pyGet, pyOr, pyPathJoin, pyPathlibStr, pyStrAny, pyTruthy } from "./support.js";
49
52
  function memoryCaches() {
50
53
  return { registry: new MemoryResponseCache(), is: new MemoryResponseCache(), connectors: new MemoryResponseCache() };
@@ -83,9 +86,111 @@ export async function matchCatalogObject(ctx, connectorKey, wanted, verb) {
83
86
  }
84
87
  return undefined;
85
88
  }
89
+ /**
90
+ * The names to bind under, settled BEFORE any tenant call or write, so a refusal
91
+ * here leaves the tenant unasked and bindings.json as it was.
92
+ *
93
+ * Per attribute: the flag; else the ONE label the source already uses for this
94
+ * connector; else the old default (the key's last dash-segment, `shared`). The
95
+ * source is parsed, never run (source-labels.ts). Several labels for one
96
+ * connector are refused rather than picked — binding one would leave the others
97
+ * to compile's BINDING_UNDECLARED — and so is a connection label the source also
98
+ * gives another connector's steps, since one label binds one connection id. A
99
+ * label that cannot be read statically keeps the default and says why — unless
100
+ * one label WAS read, which is then bound, with a line naming the unread call.
101
+ */
102
+ function chooseLabels(options, ports) {
103
+ const info = (line) => ports.reporter.info(line);
104
+ const warn = (line) => ports.reporter.warn(line);
105
+ const command = ports.prepareCommand ?? PREPARE_COMMAND;
106
+ const labels = sourceBindingLabels({
107
+ cwd: ports.cwd ?? process.cwd(),
108
+ out: options.out ?? "./connectors-local",
109
+ connectorKey: options.connectorKey,
110
+ source: options.source,
111
+ });
112
+ // `args.bind_connection or …`: an empty --bind-connection falls back, as Python's
113
+ // `or` did. --bind-folder was an argparse default, so an empty one STANDS.
114
+ const flags = {
115
+ connection: pyTruthy(options.bindConnection) ? options.bindConnection : undefined,
116
+ folder: options.bindFolder,
117
+ };
118
+ const defaults = { connection: afterLast(options.connectorKey, "-"), folder: "shared" };
119
+ const chosen = {};
120
+ for (const attribute of ["connection", "folder"]) {
121
+ const found = labels[attribute];
122
+ const files = [...new Set([...found.values()].flat())];
123
+ const flag = flags[attribute];
124
+ if (flag !== undefined) {
125
+ if (found.size > 0 && !found.has(flag))
126
+ warn(flagDiffersFromSourceLine(attribute, flag, [...found.keys()], files));
127
+ chosen[attribute] = flag;
128
+ continue;
129
+ }
130
+ if (found.size > 1) {
131
+ throw new PrepareRefusal(ambiguousLabelsMessage(attribute, options.connectorKey, options.action, found, command), "BINDING_LABEL_AMBIGUOUS");
132
+ }
133
+ const unreadable = labels.unreadable[attribute];
134
+ if (found.size === 1) {
135
+ const [label] = found.keys();
136
+ const others = attribute === "connection" ? labels.otherConnectors.get(label) : undefined;
137
+ if (others && others.size > 0) {
138
+ throw new PrepareRefusal(sharedLabelMessage(label, options.connectorKey, [...others], files), "BINDING_LABEL_SHARED");
139
+ }
140
+ // One readable label wins over the default even when another call could not
141
+ // be read: the default is certain to miss the step that WAS read, while the
142
+ // unread call, if it is this connector's under another label, is compile's
143
+ // BINDING_UNDECLARED. The line says which call was not read.
144
+ if (unreadable.length > 0)
145
+ info(partlyUnreadableLabelLine(attribute, label, unreadable));
146
+ chosen[attribute] = label;
147
+ chosen[attribute === "connection" ? "connectionFrom" : "folderFrom"] = files;
148
+ continue;
149
+ }
150
+ if (unreadable.length > 0) {
151
+ info(unreadableLabelLine(attribute, defaults[attribute], unreadable));
152
+ chosen[attribute] = defaults[attribute];
153
+ continue;
154
+ }
155
+ chosen[attribute] = defaults[attribute];
156
+ }
157
+ return chosen;
158
+ }
159
+ /**
160
+ * Refuse to overwrite a label that bindings.json already binds to ANOTHER
161
+ * connector's connection. Only for a label read from the source: that is the
162
+ * route that can collide by accident (two connectors named after one display
163
+ * name, `is-sandboxes`), and compile cannot see the result, because the label IS
164
+ * declared. The same connector's other connection under the same label is a
165
+ * reconfigure and is written as before; so is a stub or an unlisted id. The one
166
+ * extra listing happens only when the row exists with a different real id.
167
+ */
168
+ async function refuseLabelOfAnotherConnector(client, connectorKey, label, connectionId, bindingsPath, bindingsShown) {
169
+ if (!existsSync(bindingsPath))
170
+ return;
171
+ let entries;
172
+ try {
173
+ entries = JSON.parse(readFileSync(bindingsPath, "utf8"))?.bindings;
174
+ }
175
+ catch {
176
+ return; // mergeBindings refuses an unreadable file with its own message
177
+ }
178
+ if (!Array.isArray(entries))
179
+ return;
180
+ const row = entries.find((e) => isPlainObject(e) && (e.name === label || e.id === label));
181
+ const existing = typeof row?.resourceKey === "string" ? row.resourceKey : undefined;
182
+ if (!existing || existing === connectionId || !GUID_RE.test(existing))
183
+ return;
184
+ const owner = (await listConnections(client, { allFolders: true })).find((r) => pyStrAny(r.Id) === existing);
185
+ const ownerKey = owner ? pyStrAny(owner.ConnectorKey) : "";
186
+ if (ownerKey && ownerKey !== connectorKey) {
187
+ throw new PrepareRefusal(labelTakenMessage(label, connectorKey, existing, ownerKey, bindingsShown), "BINDING_LABEL_TAKEN");
188
+ }
189
+ }
86
190
  /** Stage 0: the connection, discovered or checked, and its row in `bindings.json`. */
87
191
  async function connect(options, ports) {
88
192
  const info = (line) => ports.reporter.info(line);
193
+ const labels = options.noBindings ? undefined : chooseLabels(options, ports);
89
194
  let row;
90
195
  let connectionId;
91
196
  if (!options.connectionId) {
@@ -102,25 +207,25 @@ async function connect(options, ports) {
102
207
  row = await lookupConnection(ports.client, options.connectorKey, connectionId);
103
208
  }
104
209
  info(connectionLine(row, connectionId));
105
- if (options.noBindings)
210
+ if (!labels)
106
211
  return { connectionId, row };
107
212
  // Recorded on BOTH routes. Until 3.30.5 only discovery wrote it, and the
108
213
  // explicit-id route left the author hand-writing the id and folder key.
109
214
  const bindingsShown = options.bindings ?? "./bindings.json";
110
215
  const bindingsPath = resolve(ports.cwd ?? process.cwd(), bindingsShown);
216
+ if (labels.connectionFrom) {
217
+ await refuseLabelOfAnotherConnector(ports.client, options.connectorKey, labels.connection, connectionId, bindingsPath, bindingsShown);
218
+ }
111
219
  // Taken before the first write so a later refusal can undo exactly this
112
220
  // command's changes, and only them.
113
221
  const rollback = { path: bindingsPath, snapshot: snapshotBindings(bindingsPath), shown: bindingsShown };
114
- // `args.bind_connection or args.key.rsplit("-", 1)[-1]`: an empty
115
- // --bind-connection falls back, as Python's `or` did. --bind-folder is an
116
- // argparse default, so an empty one STANDS, as it did there.
117
- const symbolic = pyOr(options.bindConnection, afterLast(options.connectorKey, "-"));
118
- const folder = options.bindFolder ?? "shared";
222
+ const symbolic = labels.connection;
223
+ const folder = labels.folder;
119
224
  const [wroteConnection, wroteFolder] = mergeBindings(bindingsPath, symbolic, folder, row);
120
225
  if (wroteConnection)
121
- info(bindingsLine(symbolic, bindingsShown));
226
+ info(bindingsLine(symbolic, bindingsShown) + (labels.connectionFrom ? sourceLabelSuffix(labels.connectionFrom) : ""));
122
227
  if (wroteFolder)
123
- info(bindingsLine(folder, bindingsShown));
228
+ info(bindingsLine(folder, bindingsShown) + (labels.folderFrom ? sourceLabelSuffix(labels.folderFrom) : ""));
124
229
  return { connectionId, row, bound: { connection: symbolic, folder }, rollback };
125
230
  }
126
231
  /**
@@ -188,7 +293,10 @@ export async function prepareConnector(options, ports) {
188
293
  // per command is the whole contract: the probe has already proved this
189
294
  // connection resolves every lookup, so a re-run that still cannot is a
190
295
  // changed tenant, not another connection to go looking through.
191
- return prepareConnector({ ...options, connectionId: chosenId, refreshLookups: true }, ports);
296
+ // The labels this run chose ride along as flags, so the re-entry binds under
297
+ // the same names instead of deriving them again.
298
+ const labels = connected.bound ? { bindConnection: connected.bound.connection, bindFolder: connected.bound.folder } : {};
299
+ return prepareConnector({ ...options, ...labels, connectionId: chosenId, refreshLookups: true }, ports);
192
300
  }
193
301
  }
194
302
  /** Everything after stage 0, against one settled connection. */
@@ -16,6 +16,8 @@ export declare function canonicalPath(p: string): string;
16
16
  * A bare package specifier never does — it is not our overlay.
17
17
  */
18
18
  export declare function resolvesTo(specifier: string, source: string, target: string): boolean;
19
+ /** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
20
+ export declare function flowSources(root: string): string[];
19
21
  /**
20
22
  * `_sources_importing`: every `*.flow.ts` under `root` whose import resolves
21
23
  * to `baked`. Selection is by CONTENT rather than "the single flow in this
@@ -67,7 +67,7 @@ function comparePaths(a, b) {
67
67
  return a.length - b.length;
68
68
  }
69
69
  /** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
70
- function flowSources(root) {
70
+ export function flowSources(root) {
71
71
  const found = [];
72
72
  const walk = (dir, parts) => {
73
73
  let children;
@@ -0,0 +1,24 @@
1
+ export interface SourceLabels {
2
+ /** Distinct connection labels this connector's steps use → the files (relative to cwd) that use each. */
3
+ connection: Map<string, string[]>;
4
+ /** Distinct folder labels this connector's steps use → the files that use each. */
5
+ folder: Map<string, string[]>;
6
+ /** Connection labels that steps of OTHER connectors use in the same sources → those connector keys. */
7
+ otherConnectors: Map<string, Set<string>>;
8
+ /** Why a label for this connector could not be read, one sentence each, per attribute. */
9
+ unreadable: {
10
+ connection: string[];
11
+ folder: string[];
12
+ };
13
+ }
14
+ export interface SourceLabelOptions {
15
+ /** The project root (`prepare`'s cwd). */
16
+ cwd: string;
17
+ /** The overlay directory (`--out`), relative to `cwd` or absolute. */
18
+ out: string;
19
+ connectorKey: string;
20
+ /** `--source`: read only this file. Otherwise every `*.flow.ts` under `cwd`. */
21
+ source?: string | null;
22
+ }
23
+ /** Every label the sources under `cwd` (or `--source`) use for `connectorKey`. Reads files, writes nothing. */
24
+ export declare function sourceBindingLabels(options: SourceLabelOptions): SourceLabels;
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Which `connection:` / `folder:` labels the author's `.flow.ts` already uses for
3
+ * ONE connector — read statically, so `prepare` can write its bindings.json
4
+ * entries under those names instead of inventing its own.
5
+ *
6
+ * Why this exists: `prepare` named the entries after the connector key alone
7
+ * (`slack`, `shared`), while the source it then repointed said
8
+ * `connection: 'is-sandboxes'`. Two authorities chose one name, and the mismatch
9
+ * reached the run as `'Connection' has an invalid GUID value: 'is-sandboxes'`
10
+ * (skill-flow-e2e-escalation-orchestrator-paths, 2026-09-23).
11
+ *
12
+ * The source is PARSED with the TypeScript compiler, never executed: at prepare
13
+ * time its other connector imports may not resolve yet, and running an author's
14
+ * module to learn a string is not this command's business. A label is attributed
15
+ * to a connector only through what the call names:
16
+ *
17
+ * - `connector('<key>', '<action>', inputs, opts)` — the stringly form, also
18
+ * written `connector(...KEY, inputs, opts)` over a same-file const tuple
19
+ * (`const KEY = ['<key>', '<action>'] as const`);
20
+ * - `connector(Descriptor, inputs, opts)` — `Descriptor` imported from
21
+ * `./connectors/<key>.ts` or the prepared `<out>/<key>.ts` (named or namespace
22
+ * import), so a source that a first `prepare` already repointed is still read;
23
+ * - `onEvent(...)` / `waitForEvent(...)` — a subscription object whose
24
+ * `connector` is `'<key>'`, or a descriptor imported as above with its options.
25
+ *
26
+ * `opts` and the labels in it may be literals or same-file `const`s, through
27
+ * `as const`, `satisfies`, parentheses, spreads of such consts, and property
28
+ * reads (`OUTLOOK.connection`). Anything else — a computed value, a spread of
29
+ * something that is not a same-file const, a call whose connector cannot be
30
+ * named (a descriptor from a module that is not a generated connector module, a
31
+ * spread of an imported tuple), a file that does not parse — is reported as
32
+ * UNREADABLE, never guessed, so the caller can fall back and say why. An
33
+ * unplaceable call is reported for every connector key, because it may be this
34
+ * one's step.
35
+ *
36
+ * Managed HTTP steps (`http({ managed: true, connection, folder })`) are not
37
+ * read: they name no connector key to attribute a label to. `check` and
38
+ * `compile` still refuse an undeclared label there (BINDING_UNDECLARED).
39
+ * GUID-shaped labels are dropped: they are resource keys written directly, not
40
+ * names to bind.
41
+ */
42
+ import { readFileSync, statSync } from "node:fs";
43
+ import { basename, dirname, extname, resolve } from "node:path";
44
+ import * as ts from "typescript";
45
+ import { GUID_RE } from "../../core/binding-messages.js";
46
+ import { relativePosix } from "../compat/paths.js";
47
+ import { canonicalPath, flowSources } from "./source-import.js";
48
+ const CALLEES = new Set(["connector", "onEvent", "waitForEvent"]);
49
+ function isFile(path) {
50
+ try {
51
+ return statSync(path).isFile();
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /** Every label the sources under `cwd` (or `--source`) use for `connectorKey`. Reads files, writes nothing. */
58
+ export function sourceBindingLabels(options) {
59
+ const result = { connection: new Map(), folder: new Map(), otherConnectors: new Map(), unreadable: { connection: [], folder: [] } };
60
+ const root = canonicalPath(options.cwd);
61
+ const moduleDirs = [canonicalPath(resolve(root, "connectors")), canonicalPath(resolve(options.cwd, options.out))];
62
+ let files;
63
+ if (options.source) {
64
+ const one = canonicalPath(resolve(options.cwd, options.source));
65
+ files = isFile(one) ? [one] : [];
66
+ }
67
+ else {
68
+ files = flowSources(root);
69
+ }
70
+ for (const file of files) {
71
+ let text;
72
+ try {
73
+ text = readFileSync(file, "utf8");
74
+ }
75
+ catch {
76
+ continue; // unreadable: it cannot be the author's working source
77
+ }
78
+ readFile(file, relativePosix(root, file), text, options.connectorKey, moduleDirs, result);
79
+ }
80
+ return result;
81
+ }
82
+ /** The connector key a module specifier names, when it is one of the generated connector modules. */
83
+ function connectorModuleKey(specifier, from, moduleDirs) {
84
+ if (!specifier.startsWith("."))
85
+ return undefined;
86
+ let target = canonicalPath(resolve(dirname(from), specifier));
87
+ if (extname(target) === "")
88
+ target = `${target}.ts`;
89
+ if (extname(target) !== ".ts" && extname(target) !== ".js")
90
+ return undefined;
91
+ if (!moduleDirs.includes(dirname(target)))
92
+ return undefined;
93
+ return basename(target).replace(/\.(ts|js)$/, "");
94
+ }
95
+ function unwrap(expr) {
96
+ let e = expr;
97
+ for (;;) {
98
+ if (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e)
99
+ || ts.isNonNullExpression(e) || ts.isTypeAssertionExpression(e)) {
100
+ e = e.expression;
101
+ }
102
+ else {
103
+ return e;
104
+ }
105
+ }
106
+ }
107
+ function propertyName(name) {
108
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name))
109
+ return name.text;
110
+ return undefined;
111
+ }
112
+ function readFile(file, shown, text, connectorKey, moduleDirs, result) {
113
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
114
+ // `parseDiagnostics` is where the scanner records syntax problems (see core/expr-check.ts).
115
+ const problems = sf.parseDiagnostics;
116
+ if (problems && problems.length > 0) {
117
+ if (text.includes(connectorKey)) {
118
+ const why = `${shown} does not parse, so its labels could not be read`;
119
+ result.unreadable.connection.push(why);
120
+ result.unreadable.folder.push(why);
121
+ }
122
+ return;
123
+ }
124
+ // Imports of generated connector modules: local name → key, and namespace name → key.
125
+ const named = new Map();
126
+ const namespaces = new Map();
127
+ // Same-file consts: name → initializer, or `null` when declared more than once.
128
+ const consts = new Map();
129
+ const visitDecls = (node) => {
130
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
131
+ const key = connectorModuleKey(node.moduleSpecifier.text, file, moduleDirs);
132
+ const clause = node.importClause;
133
+ if (key && clause?.namedBindings) {
134
+ if (ts.isNamespaceImport(clause.namedBindings))
135
+ namespaces.set(clause.namedBindings.name.text, key);
136
+ else
137
+ for (const el of clause.namedBindings.elements)
138
+ named.set(el.name.text, key);
139
+ }
140
+ }
141
+ if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.Const) !== 0) {
142
+ for (const d of node.declarations) {
143
+ if (!ts.isIdentifier(d.name) || !d.initializer)
144
+ continue;
145
+ consts.set(d.name.text, consts.has(d.name.text) ? null : d.initializer);
146
+ }
147
+ }
148
+ ts.forEachChild(node, visitDecls);
149
+ };
150
+ visitDecls(sf);
151
+ const constObject = (expr, depth = 0) => {
152
+ const e = unwrap(expr);
153
+ if (ts.isObjectLiteralExpression(e))
154
+ return e;
155
+ if (depth < 8 && ts.isIdentifier(e)) {
156
+ const init = consts.get(e.text);
157
+ if (init)
158
+ return constObject(init, depth + 1);
159
+ }
160
+ return undefined;
161
+ };
162
+ const prop = (obj, name, depth = 0) => {
163
+ let found = { kind: "absent" };
164
+ for (const p of obj.properties) {
165
+ if (ts.isPropertyAssignment(p) && propertyName(p.name) === name)
166
+ found = { kind: "found", expr: p.initializer };
167
+ else if (ts.isShorthandPropertyAssignment(p) && p.name.text === name)
168
+ found = { kind: "found", expr: p.name };
169
+ else if (ts.isSpreadAssignment(p)) {
170
+ const inner = depth < 8 ? constObject(p.expression) : undefined;
171
+ if (!inner)
172
+ found = { kind: "unknown", why: `a spread of \`${p.expression.getText(sf).slice(0, 60)}\`` };
173
+ else {
174
+ const got = prop(inner, name, depth + 1);
175
+ if (got.kind !== "absent")
176
+ found = got;
177
+ }
178
+ }
179
+ }
180
+ return found;
181
+ };
182
+ const constString = (expr, depth = 0) => {
183
+ const e = unwrap(expr);
184
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))
185
+ return e.text;
186
+ if (depth >= 8)
187
+ return undefined;
188
+ if (ts.isIdentifier(e)) {
189
+ const init = consts.get(e.text);
190
+ return init ? constString(init, depth + 1) : undefined;
191
+ }
192
+ if (ts.isPropertyAccessExpression(e)) {
193
+ const obj = constObject(e.expression);
194
+ if (!obj)
195
+ return undefined;
196
+ const got = prop(obj, e.name.text);
197
+ return got.kind === "found" ? constString(got.expr, depth + 1) : undefined;
198
+ }
199
+ return undefined;
200
+ };
201
+ /** The connector key a descriptor expression was imported for. */
202
+ const descriptorKey = (expr) => {
203
+ const e = unwrap(expr);
204
+ if (ts.isIdentifier(e))
205
+ return named.get(e.text);
206
+ if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression))
207
+ return namespaces.get(e.expression.text);
208
+ return undefined;
209
+ };
210
+ const add = (map, label) => {
211
+ const files = map.get(label) ?? [];
212
+ if (!files.includes(shown))
213
+ files.push(shown);
214
+ map.set(label, files);
215
+ };
216
+ const snippet = (node) => {
217
+ const t = node.getText(sf).replace(/\s+/g, " ");
218
+ return t.length > 60 ? `${t.slice(0, 57)}...` : t;
219
+ };
220
+ const where = (node, callee) => `${shown}:${sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1} ${callee}(…)`;
221
+ /** Record one call's labels, attributed to `key`. `opts` undefined = the call passes none. */
222
+ const record = (key, opts, at) => {
223
+ if (opts === undefined)
224
+ return;
225
+ const obj = constObject(opts);
226
+ if (key !== connectorKey) {
227
+ // Only the connection label matters across connectors: a folder is shared by design.
228
+ if (!obj)
229
+ return;
230
+ const got = prop(obj, "connection");
231
+ const label = got.kind === "found" ? constString(got.expr) : undefined;
232
+ if (label !== undefined && !GUID_RE.test(label)) {
233
+ const keys = result.otherConnectors.get(label) ?? new Set();
234
+ keys.add(key);
235
+ result.otherConnectors.set(label, keys);
236
+ }
237
+ return;
238
+ }
239
+ if (!obj) {
240
+ const why = `${at} passes its options as \`${snippet(unwrap(opts))}\`, which is not a same-file object`;
241
+ result.unreadable.connection.push(why);
242
+ result.unreadable.folder.push(why);
243
+ return;
244
+ }
245
+ for (const attribute of ["connection", "folder"]) {
246
+ const got = prop(obj, attribute);
247
+ if (got.kind === "absent")
248
+ continue;
249
+ if (got.kind === "unknown") {
250
+ result.unreadable[attribute].push(`${at} takes its ${attribute} from ${got.why}`);
251
+ continue;
252
+ }
253
+ const label = constString(got.expr);
254
+ if (label === undefined) {
255
+ result.unreadable[attribute].push(`${at} computes its ${attribute} (\`${snippet(unwrap(got.expr))}\`)`);
256
+ continue;
257
+ }
258
+ if (GUID_RE.test(label))
259
+ continue;
260
+ add(result[attribute], label);
261
+ }
262
+ };
263
+ /**
264
+ * The call's arguments with spreads of same-file const arrays expanded
265
+ * (`connector(...SLACK, inputs, opts)`), or why a spread could not be.
266
+ */
267
+ const expandArgs = (args, depth = 0) => {
268
+ const out = [];
269
+ for (const a of args) {
270
+ if (!ts.isSpreadElement(a)) {
271
+ out.push(a);
272
+ continue;
273
+ }
274
+ const arr = depth < 8 ? constArray(a.expression) : undefined;
275
+ if (!arr)
276
+ return { why: `spreads \`${snippet(a.expression)}\`, which is not a same-file const array` };
277
+ const inner = expandArgs(arr.elements, depth + 1);
278
+ if (!Array.isArray(inner))
279
+ return inner;
280
+ out.push(...inner);
281
+ }
282
+ return out;
283
+ };
284
+ const constArray = (expr, depth = 0) => {
285
+ const e = unwrap(expr);
286
+ if (ts.isArrayLiteralExpression(e))
287
+ return e;
288
+ if (depth < 8 && ts.isIdentifier(e)) {
289
+ const init = consts.get(e.text);
290
+ if (init)
291
+ return constArray(init, depth + 1);
292
+ }
293
+ return undefined;
294
+ };
295
+ /** A call whose connector cannot be named may be this connector's: it makes every label unreadable. */
296
+ const unplaceable = (at, why) => {
297
+ const line = `${at} ${why}, so which connector it binds cannot be read`;
298
+ result.unreadable.connection.push(line);
299
+ result.unreadable.folder.push(line);
300
+ };
301
+ const visitCalls = (node) => {
302
+ if (ts.isCallExpression(node)) {
303
+ const callee = ts.isIdentifier(node.expression)
304
+ ? node.expression.text
305
+ : ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : undefined;
306
+ if (callee && CALLEES.has(callee) && node.arguments.length > 0) {
307
+ const at = where(node, callee);
308
+ const args = expandArgs(node.arguments);
309
+ if (!Array.isArray(args))
310
+ unplaceable(at, args.why);
311
+ else if (args.length === 0)
312
+ unplaceable(at, "spreads an empty array");
313
+ else if (callee === "connector") {
314
+ const [first] = args;
315
+ const literalKey = constString(first);
316
+ const key = literalKey ?? descriptorKey(first);
317
+ if (key === undefined)
318
+ unplaceable(at, `names its connector as \`${snippet(unwrap(first))}\`, which is neither a string nor a descriptor imported from a generated connector module`);
319
+ else
320
+ record(key, args[literalKey !== undefined ? 3 : 2], at);
321
+ }
322
+ else {
323
+ const [first] = args;
324
+ const subscription = constObject(first);
325
+ if (subscription) {
326
+ const got = prop(subscription, "connector");
327
+ const key = got.kind === "found" ? constString(got.expr) : undefined;
328
+ if (key)
329
+ record(key, first, at);
330
+ else
331
+ unplaceable(at, got.kind === "absent" ? "passes a subscription with no `connector`" : "computes its subscription's `connector`");
332
+ }
333
+ else {
334
+ const key = descriptorKey(first);
335
+ if (key)
336
+ record(key, args[1], at);
337
+ else
338
+ unplaceable(at, `takes \`${snippet(unwrap(first))}\`, which is neither a same-file subscription object nor a descriptor imported from a generated connector module`);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ ts.forEachChild(node, visitCalls);
344
+ };
345
+ visitCalls(sf);
346
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.9.1",
3
+ "version": "6.9.3",
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",
@@ -87,5 +87,5 @@
87
87
  "@types/node": "^22.7.0",
88
88
  "esbuild": "^0.28.1"
89
89
  },
90
- "gitref": "f637225ed0a30014831ffab8733374625126f6fb"
90
+ "gitref": "ac3e6768fb9c4f51d37eb6f51c588cbf7b7912c7"
91
91
  }