@uipath/packager-tool-flow 0.0.17 → 0.0.20
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 +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +307 -0
- package/dist/inline-agent-utils.d.ts +8 -0
- package/package.json +19 -29
- package/src/ensure-process-bindings.ts +213 -0
- package/src/flow-io.ts +73 -0
- package/src/flow-tool.ts +264 -0
- 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,23 @@ 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;
|
|
22
|
+
/**
|
|
23
|
+
* Read the .flow file from the content folder, convert to BPMN, and
|
|
24
|
+
* generate entry-points.json, bindings_v2.json, and the BPMN file.
|
|
25
|
+
*
|
|
26
|
+
* Artifacts are always regenerated from the .flow source to ensure
|
|
27
|
+
* the package reflects the current state of the workflow, regardless
|
|
28
|
+
* of any stale artifacts that may exist on disk.
|
|
29
|
+
*/
|
|
30
|
+
private generateFlowPackagingArtifacts;
|
|
14
31
|
/**
|
|
15
32
|
* Copy project files into the correct nupkg structure.
|
|
16
33
|
*
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -7,6 +7,16 @@ import {
|
|
|
7
7
|
} from "@uipath/solutionpackager-tool-core";
|
|
8
8
|
|
|
9
9
|
// src/flow-tool.ts
|
|
10
|
+
import { convertFlowToBpmn } from "@uipath/flow-converter";
|
|
11
|
+
import {
|
|
12
|
+
generateBindingsJson,
|
|
13
|
+
generateEntryPointsJson,
|
|
14
|
+
generateOperateJson,
|
|
15
|
+
getBindingResources,
|
|
16
|
+
getEntryPoints,
|
|
17
|
+
inMemoryWorkflowToFileFormat,
|
|
18
|
+
ProjectType
|
|
19
|
+
} from "@uipath/flow-schema";
|
|
10
20
|
import {
|
|
11
21
|
NugetConstants,
|
|
12
22
|
NugetPackager,
|
|
@@ -83,6 +93,162 @@ var processAgents = async (fileSystem, logger, projectPath, contentFolder) => {
|
|
|
83
93
|
}
|
|
84
94
|
};
|
|
85
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
|
+
|
|
86
252
|
// src/flow-tool.ts
|
|
87
253
|
var FlowConstants = {
|
|
88
254
|
EntryPointsFileName: "entry-points.json",
|
|
@@ -112,6 +278,10 @@ class FlowTool extends ProjectTool {
|
|
|
112
278
|
await this.copyProjectFiles(options.projectPath, contentFolder);
|
|
113
279
|
this.logger.progress("Processing agents...");
|
|
114
280
|
await processAgents(this.fileSystem, this.logger, options.projectPath, contentFolder);
|
|
281
|
+
this.logger.progress("Resolving process bindings...");
|
|
282
|
+
await this.resolveProcessBindings(contentFolder);
|
|
283
|
+
this.logger.progress("Generating packaging artifacts from .flow...");
|
|
284
|
+
await this.generateFlowPackagingArtifacts(contentFolder, options.projectStorageId ?? "");
|
|
115
285
|
this.logger.progress("Creating operate.json file...");
|
|
116
286
|
await this.createOperateFile(options, contentFolder);
|
|
117
287
|
this.logger.progress("Creating package-descriptor.json file...");
|
|
@@ -153,6 +323,141 @@ class FlowTool extends ProjectTool {
|
|
|
153
323
|
await this._temporaryStorage.cleanup();
|
|
154
324
|
} catch {}
|
|
155
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
|
+
}
|
|
347
|
+
async generateFlowPackagingArtifacts(contentFolder, projectId) {
|
|
348
|
+
const entries = await this.fileSystem.readdir(contentFolder);
|
|
349
|
+
const flowFile = entries.find((e) => e.endsWith(".flow"));
|
|
350
|
+
if (!flowFile) {
|
|
351
|
+
this.logger.warn("No .flow file found in content folder — skipping artifact generation");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const flowFilePath = Path3.join(contentFolder, flowFile);
|
|
355
|
+
let workflow;
|
|
356
|
+
try {
|
|
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}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const bpmnFileName = flowFile.replace(/\.flow$/, ".bpmn");
|
|
367
|
+
const bpmnPath = Path3.join(contentFolder, bpmnFileName);
|
|
368
|
+
const fileFormat = inMemoryWorkflowToFileFormat(workflow);
|
|
369
|
+
setPublishIntentOnInlineAgents(fileFormat);
|
|
370
|
+
const resolvedFlowJson = JSON.stringify(fileFormat);
|
|
371
|
+
const { bpmn } = await convertFlowToBpmn(resolvedFlowJson);
|
|
372
|
+
await this.fileSystem.writeFile(bpmnPath, bpmn);
|
|
373
|
+
const packagingNodes = (workflow.nodes ?? []).map((node) => ({
|
|
374
|
+
id: node.id,
|
|
375
|
+
type: node.type,
|
|
376
|
+
typeVersion: node.typeVersion ?? "1.0",
|
|
377
|
+
display: node.display,
|
|
378
|
+
inputs: node.inputs,
|
|
379
|
+
outputs: node.outputs
|
|
380
|
+
}));
|
|
381
|
+
const variables = workflow.variables ?? {};
|
|
382
|
+
const bindings = workflow.bindings ?? [];
|
|
383
|
+
const definitions = workflow.definitions ?? [];
|
|
384
|
+
const entryPointsPath = Path3.join(contentFolder, FlowConstants.EntryPointsFileName);
|
|
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)}
|
|
449
|
+
`);
|
|
450
|
+
const bindingsPath = Path3.join(contentFolder, FlowConstants.BindingsV2FileName);
|
|
451
|
+
const bindingResources = getBindingResources(packagingNodes, bindings, definitions);
|
|
452
|
+
await this.fileSystem.writeFile(bindingsPath, `${JSON.stringify(generateBindingsJson(bindingResources), null, 2)}
|
|
453
|
+
`);
|
|
454
|
+
const operatePath = Path3.join(contentFolder, NugetConstants.OperateFileName);
|
|
455
|
+
const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
|
|
456
|
+
const startEventId = startEventMatch?.[1] ?? "start";
|
|
457
|
+
const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
|
|
458
|
+
await this.fileSystem.writeFile(operatePath, `${JSON.stringify(generateOperateJson(projectId || workflow.id || "", mainEntryPoint), null, 2)}
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
156
461
|
async copyProjectFiles(projectPath, contentFolder) {
|
|
157
462
|
await this.fileSystem.mkdir(contentFolder);
|
|
158
463
|
const entries = await this.fileSystem.readdir(projectPath);
|
|
@@ -258,6 +563,8 @@ class FlowToolFactory {
|
|
|
258
563
|
// src/index.ts
|
|
259
564
|
toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory);
|
|
260
565
|
export {
|
|
566
|
+
setPublishIntentOnInlineAgents,
|
|
567
|
+
readFlowWorkflow,
|
|
261
568
|
FlowToolFactory,
|
|
262
569
|
FlowTool
|
|
263
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/packager-tool-flow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "UiPath Flow tool implementation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,23 +18,6 @@
|
|
|
18
18
|
"registry": "https://registry.npmjs.org/"
|
|
19
19
|
},
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "bun build ./src/index.ts --outdir dist --format esm --target browser --external @uipath/solutionpackager-tool-core --external @uipath/tool-agent && tsc --emitDeclarationOnly --outDir dist",
|
|
23
|
-
"dev": "bun build ./src/index.ts --outdir dist --format esm --target browser --external @uipath/solutionpackager-tool-core --external @uipath/tool-agent --watch",
|
|
24
|
-
"test": "vitest run",
|
|
25
|
-
"test:browser": "vitest run --config=vitest.browser.config.ts",
|
|
26
|
-
"test:coverage": "vitest run --coverage",
|
|
27
|
-
"test:browser:coverage": "vitest run --config=vitest.browser.config.ts --coverage",
|
|
28
|
-
"test:all": "bun run test && bun run test:browser",
|
|
29
|
-
"test:all:coverage": "bun run test:coverage && bun run test:browser:coverage",
|
|
30
|
-
"prepack": "bun run build",
|
|
31
|
-
"publish:dry": "bun publish --dry-run",
|
|
32
|
-
"publish:gh": "bun publish",
|
|
33
|
-
"version:patch": "bun version patch --no-git-tag-version",
|
|
34
|
-
"version:minor": "bun version minor --no-git-tag-version",
|
|
35
|
-
"version:major": "bun version major --no-git-tag-version",
|
|
36
|
-
"lint": "biome check ."
|
|
37
|
-
},
|
|
38
21
|
"files": [
|
|
39
22
|
"dist",
|
|
40
23
|
"!dist/**/*.map",
|
|
@@ -43,18 +26,25 @@
|
|
|
43
26
|
"author": "",
|
|
44
27
|
"license": "ISC",
|
|
45
28
|
"peerDependencies": {
|
|
46
|
-
"@uipath/
|
|
47
|
-
"@uipath/
|
|
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": "0.0.33",
|
|
34
|
+
"@uipath/tool-agent": "^1.2.4"
|
|
48
35
|
},
|
|
49
36
|
"devDependencies": {
|
|
50
|
-
"@types/node": "^25.5.
|
|
51
|
-
"@uipath/
|
|
52
|
-
"@uipath/
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
55
|
-
"@vitest/
|
|
37
|
+
"@types/node": "^25.5.2",
|
|
38
|
+
"@uipath/filesystem": "1.195.0",
|
|
39
|
+
"@uipath/flow-core": "^0.2.376",
|
|
40
|
+
"@uipath/solutionpackager-tool-core": "0.0.33",
|
|
41
|
+
"@uipath/tool-agent": "^1.2.4",
|
|
42
|
+
"@vitest/browser": "^4.1.6",
|
|
43
|
+
"@vitest/browser-playwright": "^4.1.6",
|
|
44
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
56
45
|
"playwright": "^1.57.0",
|
|
57
|
-
"typescript": "^
|
|
58
|
-
"vitest": "^4.
|
|
59
|
-
}
|
|
46
|
+
"typescript": "^6.0.2",
|
|
47
|
+
"vitest": "^4.1.6"
|
|
48
|
+
},
|
|
49
|
+
"gitHead": "ca8e2977762f231364386b40d976c61ca9ce87ca"
|
|
60
50
|
}
|
|
@@ -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,3 +1,19 @@
|
|
|
1
|
+
import { convertFlowToBpmn } from "@uipath/flow-converter";
|
|
2
|
+
import {
|
|
3
|
+
type EntryPoint,
|
|
4
|
+
generateBindingsJson,
|
|
5
|
+
generateEntryPointsJson,
|
|
6
|
+
generateOperateJson,
|
|
7
|
+
getBindingResources,
|
|
8
|
+
getEntryPoints,
|
|
9
|
+
inMemoryWorkflowToFileFormat,
|
|
10
|
+
type PackagingBinding,
|
|
11
|
+
type PackagingNode,
|
|
12
|
+
type PackagingNodeManifest,
|
|
13
|
+
type PackagingWorkflowVariables,
|
|
14
|
+
ProjectType,
|
|
15
|
+
type Workflow,
|
|
16
|
+
} from "@uipath/flow-schema";
|
|
1
17
|
import type {
|
|
2
18
|
EntryPointsFileModel,
|
|
3
19
|
IProjectBuildOptions,
|
|
@@ -18,6 +34,9 @@ import {
|
|
|
18
34
|
ToolResult,
|
|
19
35
|
} from "@uipath/solutionpackager-tool-core";
|
|
20
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";
|
|
21
40
|
|
|
22
41
|
const FlowConstants = {
|
|
23
42
|
EntryPointsFileName: "entry-points.json",
|
|
@@ -79,6 +98,17 @@ export class FlowTool extends ProjectTool {
|
|
|
79
98
|
contentFolder,
|
|
80
99
|
);
|
|
81
100
|
|
|
101
|
+
this.logger.progress("Resolving process bindings...");
|
|
102
|
+
await this.resolveProcessBindings(contentFolder);
|
|
103
|
+
|
|
104
|
+
this.logger.progress(
|
|
105
|
+
"Generating packaging artifacts from .flow...",
|
|
106
|
+
);
|
|
107
|
+
await this.generateFlowPackagingArtifacts(
|
|
108
|
+
contentFolder,
|
|
109
|
+
options.projectStorageId ?? "",
|
|
110
|
+
);
|
|
111
|
+
|
|
82
112
|
this.logger.progress("Creating operate.json file...");
|
|
83
113
|
await this.createOperateFile(options, contentFolder);
|
|
84
114
|
|
|
@@ -149,6 +179,240 @@ export class FlowTool extends ProjectTool {
|
|
|
149
179
|
}
|
|
150
180
|
}
|
|
151
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
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Read the .flow file from the content folder, convert to BPMN, and
|
|
221
|
+
* generate entry-points.json, bindings_v2.json, and the BPMN file.
|
|
222
|
+
*
|
|
223
|
+
* Artifacts are always regenerated from the .flow source to ensure
|
|
224
|
+
* the package reflects the current state of the workflow, regardless
|
|
225
|
+
* of any stale artifacts that may exist on disk.
|
|
226
|
+
*/
|
|
227
|
+
private async generateFlowPackagingArtifacts(
|
|
228
|
+
contentFolder: string,
|
|
229
|
+
projectId: string,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
// Find the .flow file in content folder
|
|
232
|
+
const entries = await this.fileSystem.readdir(contentFolder);
|
|
233
|
+
const flowFile = entries.find((e: string) => e.endsWith(".flow"));
|
|
234
|
+
if (!flowFile) {
|
|
235
|
+
this.logger.warn(
|
|
236
|
+
"No .flow file found in content folder — skipping artifact generation",
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const flowFilePath = Path.join(contentFolder, flowFile);
|
|
242
|
+
let workflow: Workflow;
|
|
243
|
+
try {
|
|
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);
|
|
250
|
+
this.logger.warn(
|
|
251
|
+
`Could not load ${flowFile} as a workflow — skipping artifact generation: ${message}`,
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
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.
|
|
260
|
+
const bpmnFileName = flowFile.replace(/\.flow$/, ".bpmn");
|
|
261
|
+
const bpmnPath = Path.join(contentFolder, bpmnFileName);
|
|
262
|
+
const fileFormat = inMemoryWorkflowToFileFormat(workflow);
|
|
263
|
+
setPublishIntentOnInlineAgents(fileFormat);
|
|
264
|
+
const resolvedFlowJson = JSON.stringify(fileFormat);
|
|
265
|
+
const { bpmn } = await convertFlowToBpmn(resolvedFlowJson);
|
|
266
|
+
await this.fileSystem.writeFile(bpmnPath, bpmn);
|
|
267
|
+
|
|
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>,
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const variables =
|
|
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[];
|
|
287
|
+
|
|
288
|
+
// Generate entry-points.json (always regenerate from .flow source)
|
|
289
|
+
const entryPointsPath = Path.join(
|
|
290
|
+
contentFolder,
|
|
291
|
+
FlowConstants.EntryPointsFileName,
|
|
292
|
+
);
|
|
293
|
+
const entryPoints = getEntryPoints(
|
|
294
|
+
bpmnFileName,
|
|
295
|
+
packagingNodes,
|
|
296
|
+
variables,
|
|
297
|
+
definitions,
|
|
298
|
+
ProjectType.Flow,
|
|
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];
|
|
382
|
+
await this.fileSystem.writeFile(
|
|
383
|
+
entryPointsPath,
|
|
384
|
+
`${JSON.stringify(generateEntryPointsJson(allEntryPoints), null, 2)}\n`,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Generate bindings_v2.json (always regenerate from .flow source)
|
|
388
|
+
const bindingsPath = Path.join(
|
|
389
|
+
contentFolder,
|
|
390
|
+
FlowConstants.BindingsV2FileName,
|
|
391
|
+
);
|
|
392
|
+
const bindingResources = getBindingResources(
|
|
393
|
+
packagingNodes,
|
|
394
|
+
bindings,
|
|
395
|
+
definitions,
|
|
396
|
+
);
|
|
397
|
+
await this.fileSystem.writeFile(
|
|
398
|
+
bindingsPath,
|
|
399
|
+
`${JSON.stringify(generateBindingsJson(bindingResources), null, 2)}\n`,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Generate operate.json (always regenerate from .flow source)
|
|
403
|
+
const operatePath = Path.join(
|
|
404
|
+
contentFolder,
|
|
405
|
+
NugetConstants.OperateFileName,
|
|
406
|
+
);
|
|
407
|
+
const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
|
|
408
|
+
const startEventId = startEventMatch?.[1] ?? "start";
|
|
409
|
+
const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
|
|
410
|
+
await this.fileSystem.writeFile(
|
|
411
|
+
operatePath,
|
|
412
|
+
`${JSON.stringify(generateOperateJson(projectId || workflow.id || "", mainEntryPoint), null, 2)}\n`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
152
416
|
/**
|
|
153
417
|
* Copy project files into the correct nupkg structure.
|
|
154
418
|
*
|
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
|
+
}
|