@uipath/packager-tool-flow 0.0.18 → 1.195.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ensure-process-bindings.d.ts +15 -0
- package/dist/flow-io.d.ts +27 -0
- package/dist/flow-tool.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +269 -26
- package/dist/inline-agent-utils.d.ts +8 -0
- package/package.json +35 -61
- package/src/ensure-process-bindings.ts +213 -0
- package/src/flow-io.ts +73 -0
- package/src/flow-tool.ts +169 -53
- package/src/index.ts +2 -0
- package/src/inline-agent-utils.ts +23 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Workflow } from "@uipath/flow-core";
|
|
2
|
+
import type { IToolLogger } from "@uipath/solutionpackager-tool-core";
|
|
3
|
+
/**
|
|
4
|
+
* Safety net: ensure every process/agent node in the workflow has corresponding
|
|
5
|
+
* entries in the workflow `bindings` array and that `<bindings.*>` placeholders
|
|
6
|
+
* in model.context are resolved to `=bindings.<id>` references.
|
|
7
|
+
*
|
|
8
|
+
* This handles flows authored outside the CLI (e.g. direct file edits or
|
|
9
|
+
* skill-generated files) where `addNodeToFlow` was never called, so the
|
|
10
|
+
* binding creation / resolution step was skipped.
|
|
11
|
+
*
|
|
12
|
+
* Mutates the workflow in place. Safe to call multiple times — already-resolved
|
|
13
|
+
* nodes are skipped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureProcessBindings(workflow: Workflow, logger: IToolLogger): number;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type IFileSystem } from "@uipath/filesystem";
|
|
2
|
+
import { type Workflow } from "@uipath/flow-schema";
|
|
3
|
+
export type FlowIoLogger = {
|
|
4
|
+
warn: (message: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export interface ReadFlowWorkflowOptions {
|
|
7
|
+
/** Filesystem to read from. Defaults to `getFileSystem()`. */
|
|
8
|
+
fs?: IFileSystem;
|
|
9
|
+
/** Logger for non-fatal `$ref` resolution failures. */
|
|
10
|
+
logger?: FlowIoLogger;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read a `.flow` file, resolve any `$ref` chunk references, and convert
|
|
14
|
+
* to an in-memory Workflow.
|
|
15
|
+
*
|
|
16
|
+
* Flow files may be split across sibling chunk files (layout.json,
|
|
17
|
+
* shared-vars.json, subflows/) via JSON `$ref` objects. These must be
|
|
18
|
+
* resolved before calling the schema validator, since unknown keys
|
|
19
|
+
* (including `$ref`) are stripped at parse time. Missing ref targets
|
|
20
|
+
* log a warning (fail-soft) and resolve to `null`; the resulting
|
|
21
|
+
* workflow may be partial.
|
|
22
|
+
*
|
|
23
|
+
* Pass `options.fs` when calling from an environment that injects its
|
|
24
|
+
* own filesystem (e.g., a packaging tool bundled for the browser);
|
|
25
|
+
* otherwise the process-wide `getFileSystem()` is used.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readFlowWorkflow(filePath: string, options?: ReadFlowWorkflowOptions): Promise<Workflow>;
|
package/dist/flow-tool.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export declare class FlowTool extends ProjectTool {
|
|
|
11
11
|
buildAsync(options: IProjectBuildOptions, _cancellationToken?: AbortSignal): Promise<ToolResult>;
|
|
12
12
|
packAsync(options: IProjectPackOptions, cancellationToken?: AbortSignal): Promise<ToolResult>;
|
|
13
13
|
dispose(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Read the .flow file from the content folder, resolve any unresolved
|
|
16
|
+
* process bindings, and write the updated workflow back.
|
|
17
|
+
*
|
|
18
|
+
* This is the solution pack safety net — flows authored outside the CLI
|
|
19
|
+
* may have `<bindings.*>` placeholders that need resolving before packaging.
|
|
20
|
+
*/
|
|
21
|
+
private resolveProcessBindings;
|
|
14
22
|
/**
|
|
15
23
|
* Read the .flow file from the content folder, convert to BPMN, and
|
|
16
24
|
* generate entry-points.json, bindings_v2.json, and the BPMN file.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
generateOperateJson,
|
|
15
15
|
getBindingResources,
|
|
16
16
|
getEntryPoints,
|
|
17
|
+
inMemoryWorkflowToFileFormat,
|
|
17
18
|
ProjectType
|
|
18
19
|
} from "@uipath/flow-schema";
|
|
19
20
|
import {
|
|
@@ -92,6 +93,162 @@ var processAgents = async (fileSystem, logger, projectPath, contentFolder) => {
|
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
|
|
96
|
+
// src/ensure-process-bindings.ts
|
|
97
|
+
import { addBinding, createBinding } from "@uipath/flow-core";
|
|
98
|
+
var PROCESS_NODE_PREFIXES = [
|
|
99
|
+
"uipath.core.rpa-workflow.",
|
|
100
|
+
"uipath.agent.resource.tool.process.",
|
|
101
|
+
"uipath.core.agent.",
|
|
102
|
+
"uipath.core.api-workflow."
|
|
103
|
+
];
|
|
104
|
+
var UNRESOLVED_BINDING_PATTERN = /^<bindings\.\w+>$/;
|
|
105
|
+
function isProcessNode(nodeType) {
|
|
106
|
+
return PROCESS_NODE_PREFIXES.some((prefix) => nodeType?.startsWith(prefix));
|
|
107
|
+
}
|
|
108
|
+
function extractProcessGuid(nodeType) {
|
|
109
|
+
const match = nodeType.match(/^(?:uipath\.core\.rpa-workflow|uipath\.agent\.resource\.tool\.process|uipath\.core\.agent|uipath\.core\.api-workflow)\.([0-9a-f-]+)$/i);
|
|
110
|
+
if (!match) {
|
|
111
|
+
throw new Error(`Invalid process node type: "${nodeType}". ` + "Expected a known prefix followed by a process GUID.");
|
|
112
|
+
}
|
|
113
|
+
return match[1];
|
|
114
|
+
}
|
|
115
|
+
function createProcessBindings(manifest) {
|
|
116
|
+
const processGuid = extractProcessGuid(manifest.nodeType);
|
|
117
|
+
const model = manifest.model;
|
|
118
|
+
const processName = model?.bindings?.values?.name ?? "";
|
|
119
|
+
const folderPath = model?.bindings?.values?.folderPath;
|
|
120
|
+
if (folderPath == null) {
|
|
121
|
+
throw new Error("Manifest is missing model.bindings.values.folderPath. " + "Cannot create process bindings without a folder path.");
|
|
122
|
+
}
|
|
123
|
+
const subType = model?.bindings?.resourceSubType ?? "Process";
|
|
124
|
+
const resourceKey = model?.bindings?.resourceKey ?? processGuid;
|
|
125
|
+
const nameBinding = createBinding({
|
|
126
|
+
name: "name",
|
|
127
|
+
value: processName,
|
|
128
|
+
resource: "process",
|
|
129
|
+
resourceKey,
|
|
130
|
+
propertyAttribute: "name",
|
|
131
|
+
resourceSubType: subType
|
|
132
|
+
});
|
|
133
|
+
const folderBinding = createBinding({
|
|
134
|
+
name: "folderPath",
|
|
135
|
+
value: folderPath,
|
|
136
|
+
resource: "process",
|
|
137
|
+
resourceKey,
|
|
138
|
+
propertyAttribute: "folderPath",
|
|
139
|
+
resourceSubType: subType
|
|
140
|
+
});
|
|
141
|
+
if (folderPath === "" && folderBinding.default === undefined) {
|
|
142
|
+
folderBinding.default = "";
|
|
143
|
+
}
|
|
144
|
+
return { nameBinding, folderBinding };
|
|
145
|
+
}
|
|
146
|
+
function resolveContextPlaceholders(context, storedName, storedFolder) {
|
|
147
|
+
if (!context)
|
|
148
|
+
return;
|
|
149
|
+
for (const entry of context) {
|
|
150
|
+
if (entry.name === "name" && typeof entry.value === "string" && UNRESOLVED_BINDING_PATTERN.test(entry.value)) {
|
|
151
|
+
entry.default = storedName.default;
|
|
152
|
+
entry.value = `=bindings.${storedName.id}`;
|
|
153
|
+
} else if (entry.name === "folderPath" && typeof entry.value === "string" && UNRESOLVED_BINDING_PATTERN.test(entry.value)) {
|
|
154
|
+
entry.default = storedFolder.default;
|
|
155
|
+
entry.value = `=bindings.${storedFolder.id}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function ensureProcessBindings(workflow, logger) {
|
|
160
|
+
const nodes = workflow.nodes ?? [];
|
|
161
|
+
const definitions = workflow.definitions ?? [];
|
|
162
|
+
let bindingsCreated = 0;
|
|
163
|
+
for (const node of nodes) {
|
|
164
|
+
if (!isProcessNode(node.type))
|
|
165
|
+
continue;
|
|
166
|
+
const nodeModel = node.model;
|
|
167
|
+
const defModel = definitions.find((d) => d.nodeType === node.type)?.model;
|
|
168
|
+
const hasUnresolved = [nodeModel, defModel].some((m) => m?.context?.some((entry) => typeof entry.value === "string" && UNRESOLVED_BINDING_PATTERN.test(entry.value)));
|
|
169
|
+
if (!hasUnresolved)
|
|
170
|
+
continue;
|
|
171
|
+
const definition = definitions.find((d) => d.nodeType === node.type);
|
|
172
|
+
if (!definition)
|
|
173
|
+
continue;
|
|
174
|
+
let nameBinding;
|
|
175
|
+
let folderBinding;
|
|
176
|
+
try {
|
|
177
|
+
({ nameBinding, folderBinding } = createProcessBindings(definition));
|
|
178
|
+
} catch (err) {
|
|
179
|
+
logger.warn(`Skipping binding resolution for node "${node.id}": ${err instanceof Error ? err.message : String(err)}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const beforeCount = workflow.bindings?.length ?? 0;
|
|
183
|
+
addBinding(workflow, nameBinding);
|
|
184
|
+
addBinding(workflow, folderBinding);
|
|
185
|
+
const storedBindings = workflow.bindings;
|
|
186
|
+
const storedName = storedBindings.find((b) => b.resourceKey === nameBinding.resourceKey && b.propertyAttribute === nameBinding.propertyAttribute) ?? nameBinding;
|
|
187
|
+
const storedFolder = storedBindings.find((b) => b.resourceKey === folderBinding.resourceKey && b.propertyAttribute === folderBinding.propertyAttribute) ?? folderBinding;
|
|
188
|
+
resolveContextPlaceholders(nodeModel?.context, storedName, storedFolder);
|
|
189
|
+
resolveContextPlaceholders(defModel?.context, storedName, storedFolder);
|
|
190
|
+
const added = (workflow.bindings?.length ?? 0) - beforeCount;
|
|
191
|
+
bindingsCreated += added;
|
|
192
|
+
logger.info(`Resolved process bindings for node "${node.id}" (${node.type})`);
|
|
193
|
+
}
|
|
194
|
+
if (bindingsCreated > 0) {
|
|
195
|
+
logger.info(`ensureProcessBindings: created ${bindingsCreated} binding(s) for directly-authored nodes`);
|
|
196
|
+
}
|
|
197
|
+
return bindingsCreated;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/flow-io.ts
|
|
201
|
+
import { getFileSystem } from "@uipath/filesystem";
|
|
202
|
+
import { resolveAndConvertWorkflow } from "@uipath/flow-schema";
|
|
203
|
+
async function readFlowWorkflow(filePath, options = {}) {
|
|
204
|
+
const fs = options.fs ?? getFileSystem();
|
|
205
|
+
const logger = options.logger;
|
|
206
|
+
const raw = JSON.parse(await readUtf8(fs, filePath));
|
|
207
|
+
const resolver = async (refPath, baseUri) => {
|
|
208
|
+
const resolvedPath = fs.path.resolve(fs.path.dirname(baseUri), refPath);
|
|
209
|
+
const refContent = await readUtf8OrNull(fs, resolvedPath);
|
|
210
|
+
if (refContent === null) {
|
|
211
|
+
logger?.warn(`Failed to resolve $ref "${refPath}": file not found at ${resolvedPath}`);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return JSON.parse(refContent);
|
|
215
|
+
};
|
|
216
|
+
const { workflow } = await resolveAndConvertWorkflow(raw, resolver, {
|
|
217
|
+
baseUri: filePath
|
|
218
|
+
});
|
|
219
|
+
return workflow;
|
|
220
|
+
}
|
|
221
|
+
async function readUtf8(fs, filePath) {
|
|
222
|
+
const content = await readUtf8OrNull(fs, filePath);
|
|
223
|
+
if (content === null) {
|
|
224
|
+
throw new Error(`Could not read flow file: ${filePath}`);
|
|
225
|
+
}
|
|
226
|
+
return content;
|
|
227
|
+
}
|
|
228
|
+
async function readUtf8OrNull(fs, filePath) {
|
|
229
|
+
const buffer = await fs.readFile(filePath);
|
|
230
|
+
if (buffer === null)
|
|
231
|
+
return null;
|
|
232
|
+
return typeof buffer === "string" ? buffer : new TextDecoder().decode(buffer);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/inline-agent-utils.ts
|
|
236
|
+
var INLINE_AGENT_DEF_TYPES = [
|
|
237
|
+
"uipath.agent.autonomous",
|
|
238
|
+
"uipath.agent.conversational"
|
|
239
|
+
];
|
|
240
|
+
function setPublishIntentOnInlineAgents(fileFormat) {
|
|
241
|
+
const ff = fileFormat;
|
|
242
|
+
for (const def of ff.definitions ?? []) {
|
|
243
|
+
const d = def;
|
|
244
|
+
if (INLINE_AGENT_DEF_TYPES.includes(d.nodeType)) {
|
|
245
|
+
const model = d.model ?? {};
|
|
246
|
+
model.__packageIntent = "Publish";
|
|
247
|
+
d.model = model;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
95
252
|
// src/flow-tool.ts
|
|
96
253
|
var FlowConstants = {
|
|
97
254
|
EntryPointsFileName: "entry-points.json",
|
|
@@ -121,6 +278,8 @@ class FlowTool extends ProjectTool {
|
|
|
121
278
|
await this.copyProjectFiles(options.projectPath, contentFolder);
|
|
122
279
|
this.logger.progress("Processing agents...");
|
|
123
280
|
await processAgents(this.fileSystem, this.logger, options.projectPath, contentFolder);
|
|
281
|
+
this.logger.progress("Resolving process bindings...");
|
|
282
|
+
await this.resolveProcessBindings(contentFolder);
|
|
124
283
|
this.logger.progress("Generating packaging artifacts from .flow...");
|
|
125
284
|
await this.generateFlowPackagingArtifacts(contentFolder, options.projectStorageId ?? "");
|
|
126
285
|
this.logger.progress("Creating operate.json file...");
|
|
@@ -164,6 +323,27 @@ class FlowTool extends ProjectTool {
|
|
|
164
323
|
await this._temporaryStorage.cleanup();
|
|
165
324
|
} catch {}
|
|
166
325
|
}
|
|
326
|
+
async resolveProcessBindings(contentFolder) {
|
|
327
|
+
const entries = await this.fileSystem.readdir(contentFolder);
|
|
328
|
+
const flowFile = entries.find((e) => e.endsWith(".flow"));
|
|
329
|
+
if (!flowFile)
|
|
330
|
+
return;
|
|
331
|
+
const flowPath = Path3.join(contentFolder, flowFile);
|
|
332
|
+
const flowBuffer = await this.fileSystem.readFile(flowPath);
|
|
333
|
+
if (!flowBuffer)
|
|
334
|
+
return;
|
|
335
|
+
const flowJson = typeof flowBuffer === "string" ? flowBuffer : new TextDecoder().decode(flowBuffer);
|
|
336
|
+
let workflow;
|
|
337
|
+
try {
|
|
338
|
+
workflow = JSON.parse(flowJson);
|
|
339
|
+
} catch {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const count = ensureProcessBindings(workflow, this.logger);
|
|
343
|
+
if (count > 0) {
|
|
344
|
+
await this.fileSystem.writeFile(flowPath, JSON.stringify(workflow, null, 4));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
167
347
|
async generateFlowPackagingArtifacts(contentFolder, projectId) {
|
|
168
348
|
const entries = await this.fileSystem.readdir(contentFolder);
|
|
169
349
|
const flowFile = entries.find((e) => e.endsWith(".flow"));
|
|
@@ -171,50 +351,111 @@ class FlowTool extends ProjectTool {
|
|
|
171
351
|
this.logger.warn("No .flow file found in content folder — skipping artifact generation");
|
|
172
352
|
return;
|
|
173
353
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
this.logger.warn(`Could not read ${flowFile} — skipping artifact generation`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const flowJson = typeof flowJsonBuffer === "string" ? flowJsonBuffer : new TextDecoder().decode(flowJsonBuffer);
|
|
180
|
-
let flowData;
|
|
354
|
+
const flowFilePath = Path3.join(contentFolder, flowFile);
|
|
355
|
+
let workflow;
|
|
181
356
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
357
|
+
workflow = await readFlowWorkflow(flowFilePath, {
|
|
358
|
+
fs: this.fileSystem,
|
|
359
|
+
logger: this.logger
|
|
360
|
+
});
|
|
361
|
+
} catch (err) {
|
|
362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
+
this.logger.warn(`Could not load ${flowFile} as a workflow — skipping artifact generation: ${message}`);
|
|
185
364
|
return;
|
|
186
365
|
}
|
|
187
366
|
const bpmnFileName = flowFile.replace(/\.flow$/, ".bpmn");
|
|
188
367
|
const bpmnPath = Path3.join(contentFolder, bpmnFileName);
|
|
189
|
-
const
|
|
368
|
+
const fileFormat = inMemoryWorkflowToFileFormat(workflow);
|
|
369
|
+
setPublishIntentOnInlineAgents(fileFormat);
|
|
370
|
+
const resolvedFlowJson = JSON.stringify(fileFormat);
|
|
371
|
+
const { bpmn } = await convertFlowToBpmn(resolvedFlowJson);
|
|
190
372
|
await this.fileSystem.writeFile(bpmnPath, bpmn);
|
|
191
|
-
const packagingNodes = (
|
|
373
|
+
const packagingNodes = (workflow.nodes ?? []).map((node) => ({
|
|
192
374
|
id: node.id,
|
|
193
375
|
type: node.type,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
inputs: node.inputs,
|
|
199
|
-
outputs: node.outputs,
|
|
200
|
-
model: node.model
|
|
201
|
-
}
|
|
376
|
+
typeVersion: node.typeVersion ?? "1.0",
|
|
377
|
+
display: node.display,
|
|
378
|
+
inputs: node.inputs,
|
|
379
|
+
outputs: node.outputs
|
|
202
380
|
}));
|
|
203
|
-
const variables =
|
|
204
|
-
const bindings =
|
|
381
|
+
const variables = workflow.variables ?? {};
|
|
382
|
+
const bindings = workflow.bindings ?? [];
|
|
383
|
+
const definitions = workflow.definitions ?? [];
|
|
205
384
|
const entryPointsPath = Path3.join(contentFolder, FlowConstants.EntryPointsFileName);
|
|
206
|
-
const entryPoints = getEntryPoints(bpmnFileName, packagingNodes, variables, ProjectType.Flow);
|
|
207
|
-
|
|
385
|
+
const entryPoints = getEntryPoints(bpmnFileName, packagingNodes, variables, definitions, ProjectType.Flow);
|
|
386
|
+
const INLINE_AGENT_TYPES = [
|
|
387
|
+
"uipath.agent.autonomous",
|
|
388
|
+
"uipath.agent.conversational"
|
|
389
|
+
];
|
|
390
|
+
const readSource = (n) => {
|
|
391
|
+
const inputs = n.inputs;
|
|
392
|
+
const model = n.model;
|
|
393
|
+
const candidates = [
|
|
394
|
+
inputs?.source,
|
|
395
|
+
model?.source,
|
|
396
|
+
inputs?.agentProjectId,
|
|
397
|
+
model?.agentProjectId
|
|
398
|
+
];
|
|
399
|
+
for (const c of candidates) {
|
|
400
|
+
if (typeof c === "string" && c.length > 0)
|
|
401
|
+
return c;
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
};
|
|
405
|
+
const agentEntryPoints = [];
|
|
406
|
+
const seenSources = new Set;
|
|
407
|
+
for (const node of workflow.nodes ?? []) {
|
|
408
|
+
const n = node;
|
|
409
|
+
if (!INLINE_AGENT_TYPES.includes(n.type))
|
|
410
|
+
continue;
|
|
411
|
+
const source = readSource(n);
|
|
412
|
+
if (!source)
|
|
413
|
+
continue;
|
|
414
|
+
if (seenSources.has(source))
|
|
415
|
+
continue;
|
|
416
|
+
seenSources.add(source);
|
|
417
|
+
const agentJsonPath = Path3.join(contentFolder, source, "agent.json");
|
|
418
|
+
if (!await this.fileSystem.exists(agentJsonPath))
|
|
419
|
+
continue;
|
|
420
|
+
let agent = {};
|
|
421
|
+
try {
|
|
422
|
+
const raw = await this.fileSystem.readFile(agentJsonPath);
|
|
423
|
+
if (raw) {
|
|
424
|
+
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
425
|
+
agent = JSON.parse(text);
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const inputSchema = agent.inputSchema ?? {
|
|
431
|
+
type: "object",
|
|
432
|
+
properties: {}
|
|
433
|
+
};
|
|
434
|
+
const outputSchema = agent.outputSchema ?? {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {}
|
|
437
|
+
};
|
|
438
|
+
agentEntryPoints.push({
|
|
439
|
+
filePath: `content/${source}/agent.json`,
|
|
440
|
+
uniqueId: agent.projectId ?? source,
|
|
441
|
+
type: "agent",
|
|
442
|
+
input: inputSchema,
|
|
443
|
+
output: outputSchema,
|
|
444
|
+
displayName: agent.name ?? "Agent"
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
const allEntryPoints = [...entryPoints, ...agentEntryPoints];
|
|
448
|
+
await this.fileSystem.writeFile(entryPointsPath, `${JSON.stringify(generateEntryPointsJson(allEntryPoints), null, 2)}
|
|
208
449
|
`);
|
|
209
450
|
const bindingsPath = Path3.join(contentFolder, FlowConstants.BindingsV2FileName);
|
|
210
|
-
const bindingResources = getBindingResources(packagingNodes, bindings);
|
|
451
|
+
const bindingResources = getBindingResources(packagingNodes, bindings, definitions);
|
|
211
452
|
await this.fileSystem.writeFile(bindingsPath, `${JSON.stringify(generateBindingsJson(bindingResources), null, 2)}
|
|
212
453
|
`);
|
|
213
454
|
const operatePath = Path3.join(contentFolder, NugetConstants.OperateFileName);
|
|
214
455
|
const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
|
|
215
456
|
const startEventId = startEventMatch?.[1] ?? "start";
|
|
216
457
|
const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
|
|
217
|
-
await this.fileSystem.writeFile(operatePath, `${JSON.stringify(generateOperateJson(projectId ||
|
|
458
|
+
await this.fileSystem.writeFile(operatePath, `${JSON.stringify(generateOperateJson(projectId || workflow.id || "", mainEntryPoint), null, 2)}
|
|
218
459
|
`);
|
|
219
460
|
}
|
|
220
461
|
async copyProjectFiles(projectPath, contentFolder) {
|
|
@@ -322,6 +563,8 @@ class FlowToolFactory {
|
|
|
322
563
|
// src/index.ts
|
|
323
564
|
toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory);
|
|
324
565
|
export {
|
|
566
|
+
setPublishIntentOnInlineAgents,
|
|
567
|
+
readFlowWorkflow,
|
|
325
568
|
FlowToolFactory,
|
|
326
569
|
FlowTool
|
|
327
570
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set `__packageIntent = "Publish"` on inline agent definitions in a
|
|
3
|
+
* file-format workflow so the BPMN converter emits "content/" prefixed
|
|
4
|
+
* entry points.
|
|
5
|
+
*
|
|
6
|
+
* Mutates `fileFormat` in place.
|
|
7
|
+
*/
|
|
8
|
+
export declare function setPublishIntentOnInlineAgents(fileFormat: unknown): void;
|
package/package.json
CHANGED
|
@@ -1,63 +1,37 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
"files": [
|
|
39
|
-
"dist",
|
|
40
|
-
"!dist/**/*.map",
|
|
41
|
-
"src"
|
|
42
|
-
],
|
|
43
|
-
"author": "",
|
|
44
|
-
"license": "ISC",
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"@uipath/flow-converter": "^0.1.6",
|
|
47
|
-
"@uipath/flow-schema": "^0.2.141",
|
|
48
|
-
"@uipath/solutionpackager-tool-core": "0.0.33",
|
|
49
|
-
"@uipath/tool-agent": "^1.0.1"
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@types/node": "^25.5.0",
|
|
53
|
-
"@uipath/solutionpackager-tool-core": "0.0.33",
|
|
54
|
-
"@uipath/tool-agent": "^1.0.1",
|
|
55
|
-
"@vitest/browser": "^4.0.14",
|
|
56
|
-
"@vitest/browser-playwright": "^4.0.14",
|
|
57
|
-
"@vitest/coverage-v8": "^4.0.14",
|
|
58
|
-
"playwright": "^1.57.0",
|
|
59
|
-
"typescript": "^5.9.3",
|
|
60
|
-
"vitest": "^4.0.14"
|
|
61
|
-
},
|
|
62
|
-
"gitHead": "3f1b4d8e9f910be81e4cab956537f21dbd5d63ac"
|
|
2
|
+
"name": "@uipath/packager-tool-flow",
|
|
3
|
+
"version": "1.195.0",
|
|
4
|
+
"description": "UiPath Flow tool implementation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/UiPath/cli.git",
|
|
15
|
+
"directory": "packages/packager/packager-tool-flow"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"!dist/**/*.map",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@uipath/filesystem": "1.195.0",
|
|
30
|
+
"@uipath/flow-converter": "^0.1.273",
|
|
31
|
+
"@uipath/flow-core": "^0.2.376",
|
|
32
|
+
"@uipath/flow-schema": "^0.2.495",
|
|
33
|
+
"@uipath/solutionpackager-tool-core": "1.195.0",
|
|
34
|
+
"@uipath/tool-agent": "^1.2.4"
|
|
35
|
+
},
|
|
36
|
+
"gitHead": "eecf5713cd579b15783c770d1923e44e730271ea"
|
|
63
37
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Binding, NodeInstance, Workflow } from "@uipath/flow-core";
|
|
2
|
+
import { addBinding, createBinding } from "@uipath/flow-core";
|
|
3
|
+
import type { IToolLogger } from "@uipath/solutionpackager-tool-core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Node type prefixes that use process-style bindings (GUID as resourceKey).
|
|
7
|
+
* Covers RPA workflows, agent process tools, and agent nodes.
|
|
8
|
+
*/
|
|
9
|
+
const PROCESS_NODE_PREFIXES = [
|
|
10
|
+
"uipath.core.rpa-workflow.",
|
|
11
|
+
"uipath.agent.resource.tool.process.",
|
|
12
|
+
"uipath.core.agent.",
|
|
13
|
+
"uipath.core.api-workflow.",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const UNRESOLVED_BINDING_PATTERN = /^<bindings\.\w+>$/;
|
|
17
|
+
|
|
18
|
+
function isProcessNode(nodeType: string): boolean {
|
|
19
|
+
return PROCESS_NODE_PREFIXES.some((prefix) => nodeType?.startsWith(prefix));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractProcessGuid(nodeType: string): string {
|
|
23
|
+
const match = nodeType.match(
|
|
24
|
+
/^(?:uipath\.core\.rpa-workflow|uipath\.agent\.resource\.tool\.process|uipath\.core\.agent|uipath\.core\.api-workflow)\.([0-9a-f-]+)$/i,
|
|
25
|
+
);
|
|
26
|
+
if (!match) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Invalid process node type: "${nodeType}". ` +
|
|
29
|
+
"Expected a known prefix followed by a process GUID.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return match[1];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createProcessBindings(manifest: {
|
|
36
|
+
nodeType: string;
|
|
37
|
+
model?: Record<string, unknown>;
|
|
38
|
+
}): { nameBinding: Binding; folderBinding: Binding } {
|
|
39
|
+
const processGuid = extractProcessGuid(manifest.nodeType);
|
|
40
|
+
const model = manifest.model as
|
|
41
|
+
| {
|
|
42
|
+
bindings?: {
|
|
43
|
+
resourceSubType?: string;
|
|
44
|
+
resourceKey?: string;
|
|
45
|
+
values?: { name?: string; folderPath?: string };
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
| undefined;
|
|
49
|
+
const processName = model?.bindings?.values?.name ?? "";
|
|
50
|
+
const folderPath = model?.bindings?.values?.folderPath;
|
|
51
|
+
if (folderPath == null) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Manifest is missing model.bindings.values.folderPath. " +
|
|
54
|
+
"Cannot create process bindings without a folder path.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const subType = model?.bindings?.resourceSubType ?? "Process";
|
|
58
|
+
const resourceKey = model?.bindings?.resourceKey ?? processGuid;
|
|
59
|
+
|
|
60
|
+
const nameBinding = createBinding({
|
|
61
|
+
name: "name",
|
|
62
|
+
value: processName,
|
|
63
|
+
resource: "process",
|
|
64
|
+
resourceKey,
|
|
65
|
+
propertyAttribute: "name",
|
|
66
|
+
resourceSubType: subType,
|
|
67
|
+
});
|
|
68
|
+
const folderBinding = createBinding({
|
|
69
|
+
name: "folderPath",
|
|
70
|
+
value: folderPath,
|
|
71
|
+
resource: "process",
|
|
72
|
+
resourceKey,
|
|
73
|
+
propertyAttribute: "folderPath",
|
|
74
|
+
resourceSubType: subType,
|
|
75
|
+
});
|
|
76
|
+
if (folderPath === "" && folderBinding.default === undefined) {
|
|
77
|
+
folderBinding.default = "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { nameBinding, folderBinding };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveContextPlaceholders(
|
|
84
|
+
context: Array<Record<string, unknown>> | undefined,
|
|
85
|
+
storedName: Binding,
|
|
86
|
+
storedFolder: Binding,
|
|
87
|
+
): void {
|
|
88
|
+
if (!context) return;
|
|
89
|
+
for (const entry of context) {
|
|
90
|
+
if (
|
|
91
|
+
entry.name === "name" &&
|
|
92
|
+
typeof entry.value === "string" &&
|
|
93
|
+
UNRESOLVED_BINDING_PATTERN.test(entry.value)
|
|
94
|
+
) {
|
|
95
|
+
entry.default = storedName.default;
|
|
96
|
+
entry.value = `=bindings.${storedName.id}`;
|
|
97
|
+
} else if (
|
|
98
|
+
entry.name === "folderPath" &&
|
|
99
|
+
typeof entry.value === "string" &&
|
|
100
|
+
UNRESOLVED_BINDING_PATTERN.test(entry.value)
|
|
101
|
+
) {
|
|
102
|
+
entry.default = storedFolder.default;
|
|
103
|
+
entry.value = `=bindings.${storedFolder.id}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Safety net: ensure every process/agent node in the workflow has corresponding
|
|
110
|
+
* entries in the workflow `bindings` array and that `<bindings.*>` placeholders
|
|
111
|
+
* in model.context are resolved to `=bindings.<id>` references.
|
|
112
|
+
*
|
|
113
|
+
* This handles flows authored outside the CLI (e.g. direct file edits or
|
|
114
|
+
* skill-generated files) where `addNodeToFlow` was never called, so the
|
|
115
|
+
* binding creation / resolution step was skipped.
|
|
116
|
+
*
|
|
117
|
+
* Mutates the workflow in place. Safe to call multiple times — already-resolved
|
|
118
|
+
* nodes are skipped.
|
|
119
|
+
*/
|
|
120
|
+
export function ensureProcessBindings(
|
|
121
|
+
workflow: Workflow,
|
|
122
|
+
logger: IToolLogger,
|
|
123
|
+
): number {
|
|
124
|
+
const nodes = (workflow.nodes ?? []) as NodeInstance[];
|
|
125
|
+
const definitions = (workflow.definitions ?? []) as Array<{
|
|
126
|
+
nodeType: string;
|
|
127
|
+
model?: Record<string, unknown>;
|
|
128
|
+
}>;
|
|
129
|
+
|
|
130
|
+
let bindingsCreated = 0;
|
|
131
|
+
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
if (!isProcessNode(node.type)) continue;
|
|
134
|
+
|
|
135
|
+
const nodeModel = node.model as
|
|
136
|
+
| { context?: Array<Record<string, unknown>> }
|
|
137
|
+
| undefined;
|
|
138
|
+
const defModel = definitions.find((d) => d.nodeType === node.type)
|
|
139
|
+
?.model as { context?: Array<Record<string, unknown>> } | undefined;
|
|
140
|
+
|
|
141
|
+
// Check both node and definition contexts for unresolved placeholders
|
|
142
|
+
const hasUnresolved = [nodeModel, defModel].some((m) =>
|
|
143
|
+
m?.context?.some(
|
|
144
|
+
(entry) =>
|
|
145
|
+
typeof entry.value === "string" &&
|
|
146
|
+
UNRESOLVED_BINDING_PATTERN.test(entry.value),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
if (!hasUnresolved) continue;
|
|
150
|
+
|
|
151
|
+
const definition = definitions.find((d) => d.nodeType === node.type);
|
|
152
|
+
if (!definition) continue;
|
|
153
|
+
|
|
154
|
+
let nameBinding: Binding;
|
|
155
|
+
let folderBinding: Binding;
|
|
156
|
+
try {
|
|
157
|
+
({ nameBinding, folderBinding } =
|
|
158
|
+
createProcessBindings(definition));
|
|
159
|
+
} catch (err: unknown) {
|
|
160
|
+
logger.warn(
|
|
161
|
+
`Skipping binding resolution for node "${node.id}": ${err instanceof Error ? err.message : String(err)}`,
|
|
162
|
+
);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const beforeCount =
|
|
167
|
+
(workflow.bindings as Binding[] | undefined)?.length ?? 0;
|
|
168
|
+
addBinding(workflow, nameBinding);
|
|
169
|
+
addBinding(workflow, folderBinding);
|
|
170
|
+
|
|
171
|
+
// Resolve against stored bindings (addBinding deduplicates)
|
|
172
|
+
const storedBindings = workflow.bindings as Binding[];
|
|
173
|
+
const storedName =
|
|
174
|
+
storedBindings.find(
|
|
175
|
+
(b) =>
|
|
176
|
+
b.resourceKey === nameBinding.resourceKey &&
|
|
177
|
+
b.propertyAttribute === nameBinding.propertyAttribute,
|
|
178
|
+
) ?? nameBinding;
|
|
179
|
+
const storedFolder =
|
|
180
|
+
storedBindings.find(
|
|
181
|
+
(b) =>
|
|
182
|
+
b.resourceKey === folderBinding.resourceKey &&
|
|
183
|
+
b.propertyAttribute === folderBinding.propertyAttribute,
|
|
184
|
+
) ?? folderBinding;
|
|
185
|
+
|
|
186
|
+
// Resolve placeholders in node model.context
|
|
187
|
+
resolveContextPlaceholders(
|
|
188
|
+
nodeModel?.context,
|
|
189
|
+
storedName,
|
|
190
|
+
storedFolder,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Resolve in definition model.context for BPMN converter
|
|
194
|
+
resolveContextPlaceholders(defModel?.context, storedName, storedFolder);
|
|
195
|
+
|
|
196
|
+
const added =
|
|
197
|
+
((workflow.bindings as Binding[] | undefined)?.length ?? 0) -
|
|
198
|
+
beforeCount;
|
|
199
|
+
bindingsCreated += added;
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
`Resolved process bindings for node "${node.id}" (${node.type})`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (bindingsCreated > 0) {
|
|
207
|
+
logger.info(
|
|
208
|
+
`ensureProcessBindings: created ${bindingsCreated} binding(s) for directly-authored nodes`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return bindingsCreated;
|
|
213
|
+
}
|
package/src/flow-io.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getFileSystem, type IFileSystem } from "@uipath/filesystem";
|
|
2
|
+
import { resolveAndConvertWorkflow, type Workflow } from "@uipath/flow-schema";
|
|
3
|
+
|
|
4
|
+
export type FlowIoLogger = {
|
|
5
|
+
warn: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface ReadFlowWorkflowOptions {
|
|
9
|
+
/** Filesystem to read from. Defaults to `getFileSystem()`. */
|
|
10
|
+
fs?: IFileSystem;
|
|
11
|
+
/** Logger for non-fatal `$ref` resolution failures. */
|
|
12
|
+
logger?: FlowIoLogger;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read a `.flow` file, resolve any `$ref` chunk references, and convert
|
|
17
|
+
* to an in-memory Workflow.
|
|
18
|
+
*
|
|
19
|
+
* Flow files may be split across sibling chunk files (layout.json,
|
|
20
|
+
* shared-vars.json, subflows/) via JSON `$ref` objects. These must be
|
|
21
|
+
* resolved before calling the schema validator, since unknown keys
|
|
22
|
+
* (including `$ref`) are stripped at parse time. Missing ref targets
|
|
23
|
+
* log a warning (fail-soft) and resolve to `null`; the resulting
|
|
24
|
+
* workflow may be partial.
|
|
25
|
+
*
|
|
26
|
+
* Pass `options.fs` when calling from an environment that injects its
|
|
27
|
+
* own filesystem (e.g., a packaging tool bundled for the browser);
|
|
28
|
+
* otherwise the process-wide `getFileSystem()` is used.
|
|
29
|
+
*/
|
|
30
|
+
export async function readFlowWorkflow(
|
|
31
|
+
filePath: string,
|
|
32
|
+
options: ReadFlowWorkflowOptions = {},
|
|
33
|
+
): Promise<Workflow> {
|
|
34
|
+
const fs = options.fs ?? getFileSystem();
|
|
35
|
+
const logger = options.logger;
|
|
36
|
+
const raw = JSON.parse(await readUtf8(fs, filePath));
|
|
37
|
+
|
|
38
|
+
const resolver = async (refPath: string, baseUri: string) => {
|
|
39
|
+
const resolvedPath = fs.path.resolve(fs.path.dirname(baseUri), refPath);
|
|
40
|
+
const refContent = await readUtf8OrNull(fs, resolvedPath);
|
|
41
|
+
if (refContent === null) {
|
|
42
|
+
logger?.warn(
|
|
43
|
+
`Failed to resolve $ref "${refPath}": file not found at ${resolvedPath}`,
|
|
44
|
+
);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return JSON.parse(refContent);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const { workflow } = await resolveAndConvertWorkflow(raw, resolver, {
|
|
51
|
+
baseUri: filePath,
|
|
52
|
+
});
|
|
53
|
+
return workflow;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readUtf8(fs: IFileSystem, filePath: string): Promise<string> {
|
|
57
|
+
const content = await readUtf8OrNull(fs, filePath);
|
|
58
|
+
if (content === null) {
|
|
59
|
+
throw new Error(`Could not read flow file: ${filePath}`);
|
|
60
|
+
}
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readUtf8OrNull(
|
|
65
|
+
fs: IFileSystem,
|
|
66
|
+
filePath: string,
|
|
67
|
+
): Promise<string | null> {
|
|
68
|
+
const buffer = await fs.readFile(filePath);
|
|
69
|
+
if (buffer === null) return null;
|
|
70
|
+
return typeof buffer === "string"
|
|
71
|
+
? buffer
|
|
72
|
+
: new TextDecoder().decode(buffer);
|
|
73
|
+
}
|
package/src/flow-tool.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { convertFlowToBpmn } from "@uipath/flow-converter";
|
|
2
2
|
import {
|
|
3
|
+
type EntryPoint,
|
|
3
4
|
generateBindingsJson,
|
|
4
5
|
generateEntryPointsJson,
|
|
5
6
|
generateOperateJson,
|
|
6
7
|
getBindingResources,
|
|
7
8
|
getEntryPoints,
|
|
9
|
+
inMemoryWorkflowToFileFormat,
|
|
8
10
|
type PackagingBinding,
|
|
9
11
|
type PackagingNode,
|
|
12
|
+
type PackagingNodeManifest,
|
|
10
13
|
type PackagingWorkflowVariables,
|
|
11
14
|
ProjectType,
|
|
15
|
+
type Workflow,
|
|
12
16
|
} from "@uipath/flow-schema";
|
|
13
17
|
import type {
|
|
14
18
|
EntryPointsFileModel,
|
|
@@ -30,6 +34,9 @@ import {
|
|
|
30
34
|
ToolResult,
|
|
31
35
|
} from "@uipath/solutionpackager-tool-core";
|
|
32
36
|
import { processAgents } from "./agents.js";
|
|
37
|
+
import { ensureProcessBindings } from "./ensure-process-bindings.js";
|
|
38
|
+
import { readFlowWorkflow } from "./flow-io.js";
|
|
39
|
+
import { setPublishIntentOnInlineAgents } from "./inline-agent-utils.js";
|
|
33
40
|
|
|
34
41
|
const FlowConstants = {
|
|
35
42
|
EntryPointsFileName: "entry-points.json",
|
|
@@ -91,6 +98,9 @@ export class FlowTool extends ProjectTool {
|
|
|
91
98
|
contentFolder,
|
|
92
99
|
);
|
|
93
100
|
|
|
101
|
+
this.logger.progress("Resolving process bindings...");
|
|
102
|
+
await this.resolveProcessBindings(contentFolder);
|
|
103
|
+
|
|
94
104
|
this.logger.progress(
|
|
95
105
|
"Generating packaging artifacts from .flow...",
|
|
96
106
|
);
|
|
@@ -169,6 +179,43 @@ export class FlowTool extends ProjectTool {
|
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Read the .flow file from the content folder, resolve any unresolved
|
|
184
|
+
* process bindings, and write the updated workflow back.
|
|
185
|
+
*
|
|
186
|
+
* This is the solution pack safety net — flows authored outside the CLI
|
|
187
|
+
* may have `<bindings.*>` placeholders that need resolving before packaging.
|
|
188
|
+
*/
|
|
189
|
+
private async resolveProcessBindings(contentFolder: string): Promise<void> {
|
|
190
|
+
const entries = await this.fileSystem.readdir(contentFolder);
|
|
191
|
+
const flowFile = entries.find((e: string) => e.endsWith(".flow"));
|
|
192
|
+
if (!flowFile) return;
|
|
193
|
+
|
|
194
|
+
const flowPath = Path.join(contentFolder, flowFile);
|
|
195
|
+
const flowBuffer = await this.fileSystem.readFile(flowPath);
|
|
196
|
+
if (!flowBuffer) return;
|
|
197
|
+
|
|
198
|
+
const flowJson =
|
|
199
|
+
typeof flowBuffer === "string"
|
|
200
|
+
? flowBuffer
|
|
201
|
+
: new TextDecoder().decode(flowBuffer);
|
|
202
|
+
|
|
203
|
+
let workflow: Workflow;
|
|
204
|
+
try {
|
|
205
|
+
workflow = JSON.parse(flowJson) as Workflow;
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const count = ensureProcessBindings(workflow, this.logger);
|
|
211
|
+
if (count > 0) {
|
|
212
|
+
await this.fileSystem.writeFile(
|
|
213
|
+
flowPath,
|
|
214
|
+
JSON.stringify(workflow, null, 4),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
172
219
|
/**
|
|
173
220
|
* Read the .flow file from the content folder, convert to BPMN, and
|
|
174
221
|
* generate entry-points.json, bindings_v2.json, and the BPMN file.
|
|
@@ -191,70 +238,52 @@ export class FlowTool extends ProjectTool {
|
|
|
191
238
|
return;
|
|
192
239
|
}
|
|
193
240
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Path.join(contentFolder, flowFile),
|
|
197
|
-
);
|
|
198
|
-
if (!flowJsonBuffer) {
|
|
199
|
-
this.logger.warn(
|
|
200
|
-
`Could not read ${flowFile} — skipping artifact generation`,
|
|
201
|
-
);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
const flowJson =
|
|
205
|
-
typeof flowJsonBuffer === "string"
|
|
206
|
-
? flowJsonBuffer
|
|
207
|
-
: new TextDecoder().decode(flowJsonBuffer);
|
|
208
|
-
|
|
209
|
-
let flowData: {
|
|
210
|
-
id?: string;
|
|
211
|
-
nodes?: Array<{
|
|
212
|
-
id: string;
|
|
213
|
-
type: string;
|
|
214
|
-
typeVersion?: string;
|
|
215
|
-
display?: Record<string, unknown>;
|
|
216
|
-
inputs?: Record<string, unknown>;
|
|
217
|
-
outputs?: Record<string, unknown>;
|
|
218
|
-
model?: Record<string, unknown>;
|
|
219
|
-
}>;
|
|
220
|
-
bindings?: PackagingBinding[];
|
|
221
|
-
variables?: PackagingWorkflowVariables;
|
|
222
|
-
};
|
|
223
|
-
|
|
241
|
+
const flowFilePath = Path.join(contentFolder, flowFile);
|
|
242
|
+
let workflow: Workflow;
|
|
224
243
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
workflow = await readFlowWorkflow(flowFilePath, {
|
|
245
|
+
fs: this.fileSystem,
|
|
246
|
+
logger: this.logger,
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
250
|
this.logger.warn(
|
|
228
|
-
`Could not
|
|
251
|
+
`Could not load ${flowFile} as a workflow — skipping artifact generation: ${message}`,
|
|
229
252
|
);
|
|
230
253
|
return;
|
|
231
254
|
}
|
|
232
255
|
|
|
233
|
-
// Convert .flow to BPMN
|
|
256
|
+
// Convert .flow to BPMN from the resolved file format, so any $ref
|
|
257
|
+
// content is inlined before the converter sees it.
|
|
258
|
+
// For pack/publish, set __packageIntent = "Publish" on inline agent
|
|
259
|
+
// definitions so the converter emits "content/" prefixed entry points.
|
|
234
260
|
const bpmnFileName = flowFile.replace(/\.flow$/, ".bpmn");
|
|
235
261
|
const bpmnPath = Path.join(contentFolder, bpmnFileName);
|
|
236
|
-
const
|
|
262
|
+
const fileFormat = inMemoryWorkflowToFileFormat(workflow);
|
|
263
|
+
setPublishIntentOnInlineAgents(fileFormat);
|
|
264
|
+
const resolvedFlowJson = JSON.stringify(fileFormat);
|
|
265
|
+
const { bpmn } = await convertFlowToBpmn(resolvedFlowJson);
|
|
237
266
|
await this.fileSystem.writeFile(bpmnPath, bpmn);
|
|
238
267
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
outputs: node.outputs,
|
|
250
|
-
model: node.model,
|
|
251
|
-
},
|
|
268
|
+
// Map file-format nodes to flat PackagingNode shape so
|
|
269
|
+
// getEntryPoints can read inputs.entryPointId and display.label.
|
|
270
|
+
const packagingNodes: PackagingNode[] = (workflow.nodes ?? []).map(
|
|
271
|
+
(node: Record<string, unknown>) => ({
|
|
272
|
+
id: node.id as string,
|
|
273
|
+
type: node.type as string,
|
|
274
|
+
typeVersion: (node.typeVersion as string) ?? "1.0",
|
|
275
|
+
display: node.display as PackagingNode["display"],
|
|
276
|
+
inputs: node.inputs as Record<string, unknown>,
|
|
277
|
+
outputs: node.outputs as Record<string, unknown>,
|
|
252
278
|
}),
|
|
253
279
|
);
|
|
254
280
|
|
|
255
281
|
const variables =
|
|
256
|
-
(
|
|
257
|
-
const bindings = (
|
|
282
|
+
(workflow.variables as PackagingWorkflowVariables) ?? {};
|
|
283
|
+
const bindings = (workflow.bindings ?? []) as PackagingBinding[];
|
|
284
|
+
// definitions required: without it getBindingResources silently drops process/queue/app bindings from bindings_v2.json
|
|
285
|
+
const definitions = (workflow.definitions ??
|
|
286
|
+
[]) as PackagingNodeManifest[];
|
|
258
287
|
|
|
259
288
|
// Generate entry-points.json (always regenerate from .flow source)
|
|
260
289
|
const entryPointsPath = Path.join(
|
|
@@ -265,11 +294,94 @@ export class FlowTool extends ProjectTool {
|
|
|
265
294
|
bpmnFileName,
|
|
266
295
|
packagingNodes,
|
|
267
296
|
variables,
|
|
297
|
+
definitions,
|
|
268
298
|
ProjectType.Flow,
|
|
269
299
|
);
|
|
300
|
+
|
|
301
|
+
// Scan for inline agent nodes and add their entry points.
|
|
302
|
+
// The source UUID lives at `inputs.source` (canonical post
|
|
303
|
+
// flow-core 0.2.50) or `model.source` (legacy). Validation
|
|
304
|
+
// accepts both, so packaging must too — otherwise legacy flows
|
|
305
|
+
// produce a .nupkg whose entry-points.json silently omits the
|
|
306
|
+
// agent, and runtime returns 404 for `StartInlineAgentJob`.
|
|
307
|
+
//
|
|
308
|
+
// Canonical copy of this resolution rule lives at
|
|
309
|
+
// `flow-tool/src/services/packaging-utils.ts:readInlineAgentSource`.
|
|
310
|
+
// It's duplicated here (rather than imported) because `flow-tool`
|
|
311
|
+
// already depends on this package, so the reverse import would
|
|
312
|
+
// be a cycle. Keep both in sync; longer-term the helper should
|
|
313
|
+
// move to `@uipath/flow-schema` (an external dep both can share).
|
|
314
|
+
const INLINE_AGENT_TYPES = [
|
|
315
|
+
"uipath.agent.autonomous",
|
|
316
|
+
"uipath.agent.conversational",
|
|
317
|
+
];
|
|
318
|
+
const readSource = (n: Record<string, unknown>): string | undefined => {
|
|
319
|
+
const inputs = n.inputs as Record<string, unknown> | undefined;
|
|
320
|
+
const model = n.model as Record<string, unknown> | undefined;
|
|
321
|
+
const candidates: unknown[] = [
|
|
322
|
+
inputs?.source,
|
|
323
|
+
model?.source,
|
|
324
|
+
inputs?.agentProjectId,
|
|
325
|
+
model?.agentProjectId,
|
|
326
|
+
];
|
|
327
|
+
for (const c of candidates) {
|
|
328
|
+
if (typeof c === "string" && c.length > 0) return c;
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
};
|
|
332
|
+
const agentEntryPoints: EntryPoint[] = [];
|
|
333
|
+
const seenSources = new Set<string>();
|
|
334
|
+
for (const node of workflow.nodes ?? []) {
|
|
335
|
+
const n = node as Record<string, unknown>;
|
|
336
|
+
if (!INLINE_AGENT_TYPES.includes(n.type as string)) continue;
|
|
337
|
+
const source = readSource(n);
|
|
338
|
+
if (!source) continue;
|
|
339
|
+
if (seenSources.has(source)) continue;
|
|
340
|
+
seenSources.add(source);
|
|
341
|
+
|
|
342
|
+
const agentJsonPath = Path.join(
|
|
343
|
+
contentFolder,
|
|
344
|
+
source,
|
|
345
|
+
"agent.json",
|
|
346
|
+
);
|
|
347
|
+
if (!(await this.fileSystem.exists(agentJsonPath))) continue;
|
|
348
|
+
|
|
349
|
+
let agent: Record<string, unknown> = {};
|
|
350
|
+
try {
|
|
351
|
+
const raw = await this.fileSystem.readFile(agentJsonPath);
|
|
352
|
+
if (raw) {
|
|
353
|
+
const text =
|
|
354
|
+
typeof raw === "string"
|
|
355
|
+
? raw
|
|
356
|
+
: new TextDecoder().decode(raw);
|
|
357
|
+
agent = JSON.parse(text) as Record<string, unknown>;
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const inputSchema = (agent.inputSchema ?? {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {},
|
|
366
|
+
}) as EntryPoint["input"];
|
|
367
|
+
const outputSchema = (agent.outputSchema ?? {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {},
|
|
370
|
+
}) as EntryPoint["output"];
|
|
371
|
+
agentEntryPoints.push({
|
|
372
|
+
filePath: `content/${source}/agent.json`,
|
|
373
|
+
uniqueId: (agent.projectId as string) ?? source,
|
|
374
|
+
type: "agent" as EntryPoint["type"],
|
|
375
|
+
input: inputSchema,
|
|
376
|
+
output: outputSchema,
|
|
377
|
+
displayName: (agent.name as string) ?? "Agent",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const allEntryPoints = [...entryPoints, ...agentEntryPoints];
|
|
270
382
|
await this.fileSystem.writeFile(
|
|
271
383
|
entryPointsPath,
|
|
272
|
-
`${JSON.stringify(generateEntryPointsJson(
|
|
384
|
+
`${JSON.stringify(generateEntryPointsJson(allEntryPoints), null, 2)}\n`,
|
|
273
385
|
);
|
|
274
386
|
|
|
275
387
|
// Generate bindings_v2.json (always regenerate from .flow source)
|
|
@@ -277,7 +389,11 @@ export class FlowTool extends ProjectTool {
|
|
|
277
389
|
contentFolder,
|
|
278
390
|
FlowConstants.BindingsV2FileName,
|
|
279
391
|
);
|
|
280
|
-
const bindingResources = getBindingResources(
|
|
392
|
+
const bindingResources = getBindingResources(
|
|
393
|
+
packagingNodes,
|
|
394
|
+
bindings,
|
|
395
|
+
definitions,
|
|
396
|
+
);
|
|
281
397
|
await this.fileSystem.writeFile(
|
|
282
398
|
bindingsPath,
|
|
283
399
|
`${JSON.stringify(generateBindingsJson(bindingResources), null, 2)}\n`,
|
|
@@ -293,7 +409,7 @@ export class FlowTool extends ProjectTool {
|
|
|
293
409
|
const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
|
|
294
410
|
await this.fileSystem.writeFile(
|
|
295
411
|
operatePath,
|
|
296
|
-
`${JSON.stringify(generateOperateJson(projectId ||
|
|
412
|
+
`${JSON.stringify(generateOperateJson(projectId || workflow.id || "", mainEntryPoint), null, 2)}\n`,
|
|
297
413
|
);
|
|
298
414
|
}
|
|
299
415
|
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { toolsFactoryRepository } from "@uipath/solutionpackager-tool-core";
|
|
3
3
|
import { FlowToolFactory } from "./flow-tool-factory.js";
|
|
4
4
|
|
|
5
|
+
export { type FlowIoLogger, readFlowWorkflow } from "./flow-io.js";
|
|
5
6
|
export { FlowTool } from "./flow-tool.js";
|
|
6
7
|
export { FlowToolFactory } from "./flow-tool-factory.js";
|
|
8
|
+
export { setPublishIntentOnInlineAgents } from "./inline-agent-utils.js";
|
|
7
9
|
|
|
8
10
|
toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory());
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const INLINE_AGENT_DEF_TYPES = [
|
|
2
|
+
"uipath.agent.autonomous",
|
|
3
|
+
"uipath.agent.conversational",
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set `__packageIntent = "Publish"` on inline agent definitions in a
|
|
8
|
+
* file-format workflow so the BPMN converter emits "content/" prefixed
|
|
9
|
+
* entry points.
|
|
10
|
+
*
|
|
11
|
+
* Mutates `fileFormat` in place.
|
|
12
|
+
*/
|
|
13
|
+
export function setPublishIntentOnInlineAgents(fileFormat: unknown): void {
|
|
14
|
+
const ff = fileFormat as Record<string, unknown[]>;
|
|
15
|
+
for (const def of ff.definitions ?? []) {
|
|
16
|
+
const d = def as Record<string, unknown>;
|
|
17
|
+
if (INLINE_AGENT_DEF_TYPES.includes(d.nodeType as string)) {
|
|
18
|
+
const model = (d.model ?? {}) as Record<string, unknown>;
|
|
19
|
+
model.__packageIntent = "Publish";
|
|
20
|
+
d.model = model;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|