@uipath/maestro-builder-sdk 6.31.4 → 6.31.6

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.
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
21
21
  import { writeJsonFile } from './core/json-file.js';
22
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
22
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
23
23
  import { basename, dirname, join, resolve } from 'node:path';
24
24
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
25
25
  import { serializeWithSidecars, compileOwnsResource } from './serialize.js';
@@ -32,9 +32,12 @@ import { readBindingsV2, readInlineAgentFiles } from './bindings-v2-files.js';
32
32
  import { ensureTypeScriptRuntime } from './node-runtime.js';
33
33
  import { defaultBindingsFile } from './workdir.js';
34
34
  import { isWithin } from './path-within.js';
35
- import { resourceSourcesOfOtherFlows } from './project-resource-users.js';
35
+ import { flowSources, resourceSourcesOfOtherFlows } from './project-resource-users.js';
36
+ import { fnv1aX4Uuid } from './core/stable-id.js';
36
37
  import { BindingRefusal } from './core/binding-messages.js';
37
38
  import { ByoaRefusal } from './core/byoa.js';
39
+ /** An agent folder name: a uuid, any case. */
40
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
38
41
  export async function run(argv) {
39
42
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
40
43
  console.error('usage: compile <flow.ts | BaseName> [-o out.flow] [--library <dir>] [--bindings <file>]');
@@ -93,10 +96,57 @@ export async function run(argv) {
93
96
  // (EVENT_BYOA_UNVERIFIED): the check pass above reports each first, and this is
94
97
  // the same refusal in the same `✗ [CODE]` shape when that pass was skipped
95
98
  // (`--no-check`, or an emit-only project). Nothing is written.
99
+ const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
100
+ const outDir = dirname(resolve(out));
101
+ // ONE FOLDER, ONE FLOW (#1184). An agent's `source` is the name of the folder
102
+ // beside the `.flow` that holds its `agent.json` and `resources/`. Decompile
103
+ // keeps a canvas agent's `source` (the sidecar must still resolve), so
104
+ // compiling that source to a SECOND `.flow` in the same project would write
105
+ // into the first flow's folder: its `agent.json` is overwritten with the new
106
+ // flow's agent, and `uip agent refresh` for the new flow declares every
107
+ // resource the first flow keeps there. The product never shares a folder
108
+ // either: Studio Web mints a fresh uuid for an imported flow whose agent the
109
+ // project already owns, copies the folder and restamps `projectId`
110
+ // (flow-workbench MST-14919). So does compile: an agent whose `source`
111
+ // another `.flow` in this Flow project claims is written to its own folder,
112
+ // derived from that `source` and this file's name, so a recompile lands in
113
+ // the same one. Outside a Flow project (`project.uiproj`) nothing is a
114
+ // deliverable: the brownfield loop writes its baseline/edited candidates
115
+ // beside the original on purpose, and they are left as they were.
116
+ //
117
+ // Two rules keep the choice stable across later edits to the project:
118
+ // - The OWNER keeps its folder. A source the existing `out` file already
119
+ // names stays put, even if another `.flow` names it too (one compiled
120
+ // into it before this rule existed): the flow being recompiled is the one
121
+ // that already lives there, so it is never the one moved.
122
+ // - A re-home is STICKY. Once the re-homed folder exists, the step keeps
123
+ // writing there even after the flow that claimed the old folder is
124
+ // deleted or renamed; otherwise the next compile would move back into the
125
+ // old folder (and its leftover resources) and orphan the new one.
126
+ const inProject = existsSync(join(outDir, 'project.uiproj'));
127
+ const claimed = inProject ? sourcesOfOtherFlows(outDir, resolve(out)) : new Map();
128
+ const ownSources = inProject ? new Set(flowSources(readJsonOrUndefined(resolve(out))).map((s) => s.toLowerCase())) : new Set();
129
+ const rehomed = new Map();
130
+ const agentSource = (source, step) => {
131
+ // Only a uuid names a folder (as in Studio Web); anything else is left to
132
+ // the sidecar-path check below rather than read from here.
133
+ if (!inProject || !UUID.test(source))
134
+ return source;
135
+ const key = source.toLowerCase();
136
+ if (ownSources.has(key))
137
+ return source;
138
+ const to = fnv1aX4Uuid(`${key}/${basename(out)}`);
139
+ const owner = claimed.get(key);
140
+ if (owner === undefined)
141
+ return existsSync(join(outDir, to, 'agent.json')) ? to : source;
142
+ if (!rehomed.has(source))
143
+ rehomed.set(source, { from: source, to, step, owner });
144
+ return to;
145
+ };
96
146
  const warnings = [];
97
147
  let emitted;
98
148
  try {
99
- emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w) });
149
+ emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w), agentSource });
100
150
  }
101
151
  catch (error) {
102
152
  for (const w of warnings)
@@ -119,7 +169,6 @@ export async function run(argv) {
119
169
  for (const w of warnings)
120
170
  console.error(` ⚠ [${w.code}] ${w.message}`);
121
171
  diagnostics.push(...warnings.map(serializeWarningDiagnostic));
122
- const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
123
172
  // An inline agent is TWO artifacts: the node, and the agent's own configuration
124
173
  // at `<source>/agent.json` — which is where flow-v1's serializer points the
125
174
  // runtime (`entryPoint: "<source>/agent.json"`, resolved relative to the
@@ -131,7 +180,6 @@ export async function run(argv) {
131
180
  //
132
181
  // Every sidecar is checked before anything is written; success is reported
133
182
  // only once every file is on disk.
134
- const outDir = dirname(resolve(out));
135
183
  const targets = sidecars.map((s) => ({ sidecar: s, target: resolve(outDir, s.path) }));
136
184
  const escaping = targets.find(({ target }) => target === outDir || !isWithin(outDir, target));
137
185
  if (escaping) {
@@ -152,6 +200,20 @@ export async function run(argv) {
152
200
  console.log(`compile: wrote ${sidecar.path}`);
153
201
  written.push(target);
154
202
  }
203
+ // A re-homed agent's folder starts as a copy of the folder it came from, minus
204
+ // what this compile just wrote (its `agent.json`, the resources it derives)
205
+ // and minus every `resources/<id>/` this flow's nodes do not name — copying
206
+ // those would hand the new flow's agent the old flow's index again, which is
207
+ // the bug. A file already in the new folder is the new flow's own and is kept.
208
+ if (rehomed.size > 0) {
209
+ const referenced = new Set(flowSources(flowJson).map((s) => s.toLowerCase()));
210
+ for (const { from, to, step, owner } of rehomed.values()) {
211
+ const copied = seedFolder(join(outDir, from), join(outDir, to), referenced);
212
+ console.log(`compile: agent "${step}" uses its own folder ${to}: ${from} belongs to ${owner}`
213
+ + (copied > 0 ? ` (copied ${copied} file(s) from it)` : '')
214
+ + `. To make this explicit, set source: "${to}" on "${step}" in the .flow.ts.`);
215
+ }
216
+ }
155
217
  reportResult({ path: resolve(out), nodes: flowJson.nodes.length, edges: flowJson.edges.length, sidecars: written });
156
218
  // PRUNE the agent resources this compile owns but did not write this time.
157
219
  // Compile otherwise only ever ADDS files, and an agent resource is keyed by a
@@ -176,7 +238,11 @@ export async function run(argv) {
176
238
  // canvas resources there are still that flow's. A resource another
177
239
  // `.flow` beside this one references by `inputs.source` is never pruned.
178
240
  const removedResources = [];
179
- const referencedElsewhere = resourceSourcesOfOtherFlows(outDir, resolve(out));
241
+ const referencedElsewhere = sourcesOfOtherFlows(outDir, resolve(out));
242
+ // ... and neither is one THIS flow still names: a re-homed agent's folder is
243
+ // seeded with the resources its nodes reference (see `seedFolder`), and a
244
+ // designer resource a node here points at is in use, whatever its type.
245
+ const referencedHere = new Set(flowSources(flowJson).map((s) => s.toLowerCase()));
180
246
  const owned = new Set(sidecars
181
247
  .filter((s) => /\/resources\/[^/]+\/resource\.json$/.test(s.path))
182
248
  .map((s) => resolve(outDir, s.path)));
@@ -189,9 +255,9 @@ export async function run(argv) {
189
255
  continue;
190
256
  for (const entry of readdirSync(resourcesDir)) {
191
257
  const file = join(resourcesDir, entry, 'resource.json');
192
- if (!existsSync(file) || owned.has(file))
258
+ if (!existsSync(file) || owned.has(file) || referencedHere.has(entry.toLowerCase()))
193
259
  continue;
194
- const user = referencedElsewhere.get(entry);
260
+ const user = referencedElsewhere.get(entry.toLowerCase());
195
261
  if (user !== undefined) {
196
262
  console.log(`compile: kept ${join('resources', entry, 'resource.json')}: ${user} still references it`);
197
263
  continue;
@@ -251,3 +317,60 @@ function serializeWarningDiagnostic(w) {
251
317
  return { level: 'warning', code: w.code, message: w.message };
252
318
  }
253
319
  runWhenInvokedDirectly(import.meta.url, 'compile', run);
320
+ /** A JSON file's contents, or `undefined` when it is missing or unreadable. */
321
+ function readJsonOrUndefined(path) {
322
+ try {
323
+ return JSON.parse(readFileSync(path, 'utf8'));
324
+ }
325
+ catch {
326
+ return undefined;
327
+ }
328
+ }
329
+ /**
330
+ * `resourceSourcesOfOtherFlows`, keyed lower-case: folder names keep their
331
+ * case, claims must not. The first file to claim a source wins, as there.
332
+ */
333
+ function sourcesOfOtherFlows(dir, self) {
334
+ const users = new Map();
335
+ for (const [src, f] of resourceSourcesOfOtherFlows(dir, self)) {
336
+ if (!users.has(src.toLowerCase()))
337
+ users.set(src.toLowerCase(), f);
338
+ }
339
+ return users;
340
+ }
341
+ /**
342
+ * Copy agent folder `from` into `to`, for an agent re-homed off a folder another
343
+ * flow owns. Skipped: the top-level `agent.json` (compile writes its own, with
344
+ * the new identity), any `resources/<id>/` whose id is not in `referenced`, any
345
+ * file already present in `to`, and anything that is not a plain file or
346
+ * folder. Returns how many files were copied; a missing `from` copies nothing.
347
+ */
348
+ function seedFolder(from, to, referenced) {
349
+ let copied = 0;
350
+ const walk = (rel) => {
351
+ let entries;
352
+ try {
353
+ entries = readdirSync(join(from, rel), { withFileTypes: true });
354
+ }
355
+ catch {
356
+ return;
357
+ }
358
+ for (const e of entries) {
359
+ const r = rel === '' ? e.name : join(rel, e.name);
360
+ if (rel === '' && e.name === 'agent.json')
361
+ continue;
362
+ if (rel === 'resources' && !referenced.has(e.name.toLowerCase()))
363
+ continue;
364
+ if (e.isDirectory()) {
365
+ walk(r);
366
+ }
367
+ else if (e.isFile() && !existsSync(join(to, r))) {
368
+ mkdirSync(dirname(join(to, r)), { recursive: true });
369
+ copyFileSync(join(from, r), join(to, r));
370
+ copied++;
371
+ }
372
+ }
373
+ };
374
+ walk('');
375
+ return copied;
376
+ }
package/dist/merge.d.ts CHANGED
@@ -28,7 +28,9 @@
28
28
  * preserved from the original; `subflows` and edges (with their ids) come from
29
29
  * `edited`; `definitions` and `bindings` are unioned
30
30
  * (original wins on conflict, since the original's are the tenant-resolved
31
- * authority). A binding row's identity is what it binds, not its row id. Node ids are stable by construction (decompile names every step by
31
+ * authority). A binding row's identity is what it binds, not its row id, and a
32
+ * row the no-edit recompile minted only for nodes merge kept from the original
33
+ * is not carried (see withoutRowsMintedForKeptNodes). Node ids are stable by construction (decompile names every step by
32
34
  * its node id), which is what lets every re-attach key on id.
33
35
  *
34
36
  * ## Definitions are keyed by `nodeType@version`, not by `nodeType`
package/dist/merge.js CHANGED
@@ -28,7 +28,9 @@
28
28
  * preserved from the original; `subflows` and edges (with their ids) come from
29
29
  * `edited`; `definitions` and `bindings` are unioned
30
30
  * (original wins on conflict, since the original's are the tenant-resolved
31
- * authority). A binding row's identity is what it binds, not its row id. Node ids are stable by construction (decompile names every step by
31
+ * authority). A binding row's identity is what it binds, not its row id, and a
32
+ * row the no-edit recompile minted only for nodes merge kept from the original
33
+ * is not carried (see withoutRowsMintedForKeptNodes). Node ids are stable by construction (decompile names every step by
32
34
  * its node id), which is what lets every re-attach key on id.
33
35
  *
34
36
  * ## Definitions are keyed by `nodeType@version`, not by `nodeType`
@@ -212,12 +214,17 @@ function mergeFlowFindings(original, edited, options) {
212
214
  // ── Nodes: the edited graph decides which nodes exist and in what order; the
213
215
  // original decides their look (and, for untouched nodes, their content). ──
214
216
  const finalNodes = [];
217
+ // The edited nodes the merge replaced with the original's: their bindings
218
+ // references are the original's now, not the recompile's
219
+ // (see withoutRowsMintedForKeptNodes).
220
+ const keptFromOriginal = new Set();
215
221
  for (const e of edited.nodes ?? []) {
216
222
  const orig = origNodes.get(e.id);
217
223
  if (isKept(e, origNodes, baseNodes, options.baseline) && orig) {
218
224
  // Untouched by the user (equal to the faithful baseline) → keep the
219
225
  // original node exactly, placeholder-safe.
220
226
  finalNodes.push(jsonClone(orig));
227
+ keptFromOriginal.add(e.id);
221
228
  }
222
229
  else {
223
230
  // Added or edited → take the edited node, re-attaching original layout.
@@ -266,9 +273,11 @@ function mergeFlowFindings(original, edited, options) {
266
273
  ];
267
274
  result.definitions = mergeDefinitions(original.definitions, edited.definitions, allNodes, originalNodes, options.warn);
268
275
  // ── Bindings: union by what each row binds (original wins), so resources for
269
- // newly-added nodes come from `edited` while the original's stay intact. ──
276
+ // newly-added nodes come from `edited` while the original's stay intact.
277
+ // A row the no-edit recompile minted only for nodes the merge kept from the
278
+ // original is not carried (#1133). ──
270
279
  result.bindings = mergeBindings(original.bindings, edited.bindings);
271
- result.bindings = withoutRowsKeyedOnlyByKeptNodes(result.bindings, original, edited, options.baseline);
280
+ result.bindings = withoutRowsMintedForKeptNodes(result.bindings, original, edited, options.baseline, keptFromOriginal);
272
281
  // metadata / solutionId / projectId / layout / runtime were carried by the
273
282
  // initial clone of `original` and are intentionally left untouched.
274
283
  //
@@ -379,12 +388,14 @@ function mergeDefinitions(orig, edited, nodes, originalNodes, warn) {
379
388
  * holds it, in which case it gets a fresh one — ids are unreferenced labels, but
380
389
  * two rows with one id are not a valid file.
381
390
  *
382
- * Rows no surviving node uses are NOT pruned. The file has no uniform
391
+ * Rows of the ORIGINAL are never pruned. The canvas file has no uniform
383
392
  * node-to-row reference to decide "unused" by: an in-solution Flow is named by
384
393
  * the node TYPE's suffix, a connection by an id in the node's inputs, a published
385
394
  * process by a folder-qualified key that no node field spells. A wrong prune
386
395
  * drops a row a kept node needs, and the run fails to resolve its resource; an
387
- * orphan row is accepted by `validate` and ignored at run time.
396
+ * orphan row is accepted by `validate` and ignored at run time. Added rows the
397
+ * round trip minted only for kept nodes are dropped afterwards, by
398
+ * {@link withoutRowsMintedForKeptNodes}.
388
399
  */
389
400
  function mergeBindings(orig, edited) {
390
401
  const out = (orig ?? []).map((b) => jsonClone(b));
@@ -409,41 +420,70 @@ function mergeBindings(orig, edited) {
409
420
  return out;
410
421
  }
411
422
  /**
412
- * Drop each added row that the no-edit recompile minted for a resource only
413
- * nodes kept from the original spell. A row qualifies when the original lacks
414
- * it, the baseline has it, and its `resourceKey` appears in at least one edited
415
- * root node, every one of which the merge replaced with the original's: the kept
416
- * node is the original's, and it reaches the resource without that row.
423
+ * Drop each added row that the no-edit recompile minted only for nodes the merge
424
+ * kept from the original (#1133). A row qualifies when the original lacks it, the
425
+ * baseline has it, and at least one edited node references it, every one of
426
+ * which is in `keptFromOriginal`: the kept node is the original's, which reaches
427
+ * its resource without that row.
417
428
  *
418
- * Example: a classic-CLI IxP extract carries its model inline and has no
419
- * binding rows. It decompiles to `ixpExtract(...)`, whose compile mints
420
- * `ixpDeployment` `name` / `folderPath` rows keyed by the model id, and an
421
- * untouched node would carry them into the merged file.
429
+ * Examples:
430
+ * - A classic-CLI IxP extract carries its model inline and has no rows. It
431
+ * decompiles to `ixpExtract(...)`, whose compile mints `ixpDeployment` rows
432
+ * keyed by the model id.
433
+ * - A classic-CLI `core.action.http.v2` with no connection decompiles to
434
+ * `http({ managed: true })`, whose compile mints `""`-keyed rows for
435
+ * `<bindings.uipath-uipath-http connection>` and `<bindings.FolderKey>`.
422
436
  *
423
- * A row whose key no edited node spells is kept, as are rows a subflow node or
424
- * an edited node spells.
437
+ * "References" is read off the EDITED file, which the SDK wrote. A node
438
+ * references a row when it spells the row's `resourceKey`, or when it spells the
439
+ * row's `<bindings.NAME>` placeholder (in the node, or in its type's definition)
440
+ * and points at that row's resource. Names are not unique: a Slack step on
441
+ * connection X and a connection-less http step both use `<bindings.FolderKey>`,
442
+ * each with its own `FolderKey` row. So a placeholder counts for a keyed row
443
+ * only when no other row has that name, and for a `""`-keyed row only when the
444
+ * node spells none of the other same-named rows' keys.
445
+ *
446
+ * Kept: a row no edited node references (one a sidecar file uses, say), a row
447
+ * a subflow node or an edited node references, and every row without a baseline.
425
448
  */
426
- function withoutRowsKeyedOnlyByKeptNodes(rows, original, edited, baseline) {
427
- if (!baseline || !rows)
449
+ function withoutRowsMintedForKeptNodes(rows, original, edited, baseline, keptFromOriginal) {
450
+ if (!baseline || !rows || keptFromOriginal.size === 0)
428
451
  return rows;
429
- const origNodes = byId(original.nodes);
430
- const baseNodes = byId(baseline.nodes);
452
+ const contextByType = new Map();
453
+ for (const d of (edited.definitions ?? [])) {
454
+ if (typeof d?.nodeType !== 'string' || d.model?.context === undefined)
455
+ continue;
456
+ contextByType.set(d.nodeType, (contextByType.get(d.nodeType) ?? '') + JSON.stringify(d.model.context));
457
+ }
458
+ const describe = (n, kept) => ({ kept, text: JSON.stringify(n) + (contextByType.get(n.type) ?? '') });
431
459
  const nodes = [
432
- ...(edited.nodes ?? []).map((n) => ({ kept: origNodes.has(n.id) && nodeUnchanged(n, baseNodes.get(n.id)), text: JSON.stringify(n) })),
433
- ...Object.values(edited.subflows ?? {}).flatMap((sf) => (sf?.nodes ?? []).map((n) => ({ kept: false, text: JSON.stringify(n) }))),
460
+ ...(edited.nodes ?? []).map((n) => describe(n, keptFromOriginal.has(n.id))),
461
+ ...Object.values(edited.subflows ?? {}).flatMap((sf) => (sf?.nodes ?? []).map((n) => describe(n, false))),
434
462
  ];
435
- if (!nodes.some((n) => n.kept))
436
- return rows;
463
+ const spelled = (s) => JSON.stringify(s).slice(1, -1);
464
+ const keyOf = (b) => (typeof b.resourceKey === 'string' ? b.resourceKey : '');
465
+ const all = (edited.bindings ?? []);
437
466
  const fromOriginal = new Set((original.bindings ?? []).map(bindingIdentity));
438
467
  const inBaseline = new Set((baseline.bindings ?? []).map(bindingIdentity));
439
468
  return rows.filter((b) => {
440
469
  const identity = bindingIdentity(b);
441
470
  if (fromOriginal.has(identity) || !inBaseline.has(identity))
442
471
  return true;
443
- if (typeof b.resourceKey !== 'string' || b.resourceKey === '')
444
- return true;
445
- const spelled = JSON.stringify(b.resourceKey).slice(1, -1);
446
- const spellers = nodes.filter((n) => n.text.includes(spelled));
447
- return !(spellers.length > 0 && spellers.every((n) => n.kept));
472
+ const key = keyOf(b);
473
+ const others = typeof b.name === 'string'
474
+ ? all.filter((o) => o.name === b.name && keyOf(o) !== key)
475
+ : [];
476
+ const placeholder = typeof b.name === 'string' ? spelled(`<bindings.${b.name}>`) : undefined;
477
+ const references = (text) => {
478
+ if (key !== '' && text.includes(spelled(key)))
479
+ return true;
480
+ if (placeholder === undefined || !text.includes(placeholder))
481
+ return false;
482
+ return key === ''
483
+ ? !others.some((o) => text.includes(spelled(keyOf(o))))
484
+ : others.length === 0;
485
+ };
486
+ const referencers = nodes.filter((n) => references(n.text));
487
+ return !(referencers.length > 0 && referencers.every((n) => n.kept));
448
488
  });
449
489
  }
@@ -7,3 +7,11 @@
7
7
  * nothing.
8
8
  */
9
9
  export declare function resourceSourcesOfOtherFlows(dir: string, self: string): Map<string, string>;
10
+ /**
11
+ * Every `inputs.source` string a flow's nodes name — agent folders and agent
12
+ * resource ids alike — root scope and subflow bodies, in the case the file
13
+ * spells them. A malformed flow yields nothing. The one walk both
14
+ * {@link resourceSourcesOfOtherFlows} and compile's own-flow checks use, so the
15
+ * two cannot disagree on where a node keeps its source.
16
+ */
17
+ export declare function flowSources(flow: unknown): string[];
@@ -31,17 +31,31 @@ export function resourceSourcesOfOtherFlows(dir, self) {
31
31
  catch {
32
32
  continue;
33
33
  }
34
- const walk = (scope) => {
35
- const s = scope;
36
- for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
37
- const src = n?.inputs?.source;
38
- if (typeof src === 'string' && !users.has(src))
39
- users.set(src, f);
40
- }
41
- if (s?.subflows && typeof s.subflows === 'object')
42
- Object.values(s.subflows).forEach(walk);
43
- };
44
- walk(flow);
34
+ for (const src of flowSources(flow))
35
+ if (!users.has(src))
36
+ users.set(src, f);
45
37
  }
46
38
  return users;
47
39
  }
40
+ /**
41
+ * Every `inputs.source` string a flow's nodes name — agent folders and agent
42
+ * resource ids alike — root scope and subflow bodies, in the case the file
43
+ * spells them. A malformed flow yields nothing. The one walk both
44
+ * {@link resourceSourcesOfOtherFlows} and compile's own-flow checks use, so the
45
+ * two cannot disagree on where a node keeps its source.
46
+ */
47
+ export function flowSources(flow) {
48
+ const ids = [];
49
+ const walk = (scope) => {
50
+ const s = scope;
51
+ for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
52
+ const src = n?.inputs?.source;
53
+ if (typeof src === 'string')
54
+ ids.push(src);
55
+ }
56
+ if (s?.subflows && typeof s.subflows === 'object')
57
+ Object.values(s.subflows).forEach(walk);
58
+ };
59
+ walk(flow);
60
+ return ids;
61
+ }
@@ -99,6 +99,18 @@ export interface SerializeOpts {
99
99
  path: string;
100
100
  json: unknown;
101
101
  }>;
102
+ /**
103
+ * Maps an agent's folder uuid (its `source`) to the one this compile writes,
104
+ * given the step's name. `compile-cli` passes it when the `source` a step
105
+ * carries already belongs to ANOTHER `.flow` in the same Flow project (#1184):
106
+ * decompile keeps a canvas agent's `source`, so compiling that source to a
107
+ * second `.flow` would otherwise write into the first flow's folder. The uuid
108
+ * returned is spent everywhere the `source` is: the node's `inputs.source`,
109
+ * the `agent.json` identity, and every sidecar path.
110
+ *
111
+ * @internal
112
+ */
113
+ agentSource?: (source: string, step: string) => string;
102
114
  }
103
115
  /**
104
116
  * `serialize`, plus the extra files the flow needs on disk beside it.
package/dist/serialize.js CHANGED
@@ -4335,6 +4335,17 @@ export function serialize(built, opts = {}) {
4335
4335
  * gets them and one that does not is unaffected.
4336
4336
  */
4337
4337
  const sidecars = opts.sidecars ?? [];
4338
+ /**
4339
+ * An agent step's folder uuid (`source`): the one the step carries, else one
4340
+ * derived from the flow id + step name rather than random, so a recompile is
4341
+ * byte-identical — a v4-shaped uuid over a stable hash, because the platform
4342
+ * treats it as an opaque id and the designer shows it as a folder. Then
4343
+ * `opts.agentSource` may move it to a folder this flow owns (#1184).
4344
+ */
4345
+ const agentFolder = (carried, stepName) => {
4346
+ const source = carried ?? fnv1aX4Uuid(`${built.id}/${stepName}`);
4347
+ return opts.agentSource ? opts.agentSource(source, stepName) : source;
4348
+ };
4338
4349
  const subflows = {};
4339
4350
  const rootGlobals = emittableGlobals(built, `flow "${built.id}"`);
4340
4351
  const root = newScope(rootGlobals.outputs, rootGlobals.vars, rootGlobals.backing, rootGlobals.backingReads);
@@ -5146,11 +5157,8 @@ export function serialize(built, opts = {}) {
5146
5157
  : `Drop { version: '${version}' } to use ${INLINE_AGENT_DEFAULT_VERSION}, or drop \`memory\`.`));
5147
5158
  }
5148
5159
  const id = stepUid(scope, step.name);
5149
- // `source` is the agent's directory name. Derived from the flow id + step
5150
- // name rather than random so a recompile is byte-identical — a v4-shaped uuid
5151
- // over a stable hash, because the platform treats it as an opaque id and the
5152
- // designer shows it as a folder.
5153
- const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
5160
+ // `source` is the agent's directory name; see `agentFolder`.
5161
+ const source = agentFolder(s.source, step.name);
5154
5162
  const node = makeNode(def.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def });
5155
5163
  // Unlike the published families the inline agent KEEPS no node-level model:
5156
5164
  // 29 of 29 deployed instances carry none (the def's `model.source: true` is
@@ -5706,7 +5714,7 @@ export function serialize(built, opts = {}) {
5706
5714
  // lets the platform do that translation.
5707
5715
  const s = spec.inputs;
5708
5716
  const id = stepUid(scope, step.name);
5709
- const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
5717
+ const source = agentFolder(s.source, step.name);
5710
5718
  const node = makeNode(NODE_TYPE.conversationalAgent, id, step.name, { parentId, requestedVersion: step.options?.version });
5711
5719
  delete node.model;
5712
5720
  const st = s.settings ?? {};
@@ -5799,7 +5807,7 @@ export function serialize(built, opts = {}) {
5799
5807
  // definition declares what comes back.
5800
5808
  const s = spec.inputs;
5801
5809
  const id = stepUid(scope, step.name);
5802
- const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
5810
+ const source = agentFolder(s.source, step.name);
5803
5811
  const node = makeNode(NODE_TYPE.voiceAgent, id, step.name, { parentId, requestedVersion: step.options?.version });
5804
5812
  // Same rule as the inline agent's: the resource model lives on the
5805
5813
  // definition (its `model.source: true` is what makes `source` an input).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.31.4",
3
+ "version": "6.31.6",
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": "db2cd4696c73eec6402fdb616e678841a5ac3b06"
96
+ "gitref": "77c2e7b1f09268cc94ee0f4b8f9d4ae80ba625ee"
97
97
  }