@uipath/maestro-builder-sdk 6.27.0 → 6.28.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.
@@ -1,4 +1,4 @@
1
- import { addValue, inferCheckMode, invoke, loadTool, namedOrLegacySecond, retiredCompiledOption, refuse } from '../common.js';
1
+ import { addValue, inferCheckMode, invoke, namedOrLegacySecond, retiredCompiledOption, refuse } from '../common.js';
2
2
  export function registerBpmnCommands(program) {
3
3
  const family = program.command('bpmn').description('Build, inspect, and merge BPMN artifacts.');
4
4
  family.command('compile')
@@ -18,7 +18,7 @@ export function registerBpmnCommands(program) {
18
18
  addValue(argv, '--library', options.library);
19
19
  addValue(argv, '--bindings', options.bindings);
20
20
  addValue(argv, '--connectors-local', options.connectorsLocal);
21
- await invoke('bpmn compile', () => loadTool(import.meta.url, '../../bpmn/compile-cli.js'), argv);
21
+ await invoke('bpmn compile', () => import('../../bpmn/compile-cli.js'), argv);
22
22
  });
23
23
  family.command('check')
24
24
  .description('Check authored TypeScript. Check a .bpmn with `uip maestro bpmn validate`.')
@@ -51,7 +51,7 @@ export function registerBpmnCommands(program) {
51
51
  addValue(argv, '--library', options.library);
52
52
  addValue(argv, '--connectors-local', options.connectorsLocal);
53
53
  addValue(argv, '--bindings', options.bindings);
54
- await invoke('bpmn check', () => loadTool(import.meta.url, '../../bpmn/check-cli.js'), argv);
54
+ await invoke('bpmn check', () => import('../../bpmn/check-cli.js'), argv);
55
55
  });
56
56
  family.command('tidy')
57
57
  .description('Add diagram layout to a compiled .bpmn artifact.')
@@ -60,7 +60,7 @@ export function registerBpmnCommands(program) {
60
60
  .action(async (input, options) => {
61
61
  const argv = [input];
62
62
  addValue(argv, '-o', options.output);
63
- await invoke('bpmn tidy', () => loadTool(import.meta.url, '../../bpmn/tidy-cli.js'), argv);
63
+ await invoke('bpmn tidy', () => import('../../bpmn/tidy-cli.js'), argv);
64
64
  });
65
65
  family
66
66
  .command('decompile')
@@ -74,7 +74,7 @@ export function registerBpmnCommands(program) {
74
74
  addValue(argv, '-o', options.output);
75
75
  addValue(argv, '--import', options.import);
76
76
  addValue(argv, '--style', options.style);
77
- await invoke('bpmn decompile', () => loadTool(import.meta.url, '../../bpmn/decompile-cli.js'), argv);
77
+ await invoke('bpmn decompile', () => import('../../bpmn/decompile-cli.js'), argv);
78
78
  });
79
79
  family
80
80
  .command('merge')
@@ -95,6 +95,6 @@ export function registerBpmnCommands(program) {
95
95
  const argv = [original, edited];
96
96
  addValue(argv, '-o', options.output);
97
97
  addValue(argv, '--baseline', options.baseline);
98
- await invoke('bpmn merge', () => loadTool(import.meta.url, '../../bpmn/merge-cli.js'), argv);
98
+ await invoke('bpmn merge', () => import('../../bpmn/merge-cli.js'), argv);
99
99
  });
100
100
  }
@@ -1,4 +1,4 @@
1
- import { addValue, inferCheckMode, invoke, loadTool, namedOrLegacySecond, retiredCompiledOption, refuse } from '../common.js';
1
+ import { addValue, inferCheckMode, invoke, namedOrLegacySecond, retiredCompiledOption, refuse } from '../common.js';
2
2
  export function registerCaseCommands(program) {
3
3
  const family = program.command('case').description('Build, check, and merge Case artifacts.');
4
4
  family.command('compile')
@@ -14,7 +14,7 @@ export function registerCaseCommands(program) {
14
14
  addValue(argv, '--library', options.library);
15
15
  addValue(argv, '--bindings', options.bindings);
16
16
  addValue(argv, '--connectors-local', options.connectorsLocal);
17
- await invoke('case compile', () => loadTool(import.meta.url, '../../case/compile-cli.js'), argv);
17
+ await invoke('case compile', () => import('../../case/compile-cli.js'), argv);
18
18
  });
19
19
  family.command('check')
20
20
  .description('Check authored TypeScript. Check a caseplan.json with `uip maestro case validate`.')
@@ -45,7 +45,7 @@ export function registerCaseCommands(program) {
45
45
  addValue(argv, '--library', options.library);
46
46
  addValue(argv, '--connectors-local', options.connectorsLocal);
47
47
  addValue(argv, '--bindings', options.bindings);
48
- await invoke('case check', () => loadTool(import.meta.url, '../../case/check-cli.js'), argv);
48
+ await invoke('case check', () => import('../../case/check-cli.js'), argv);
49
49
  });
50
50
  family.command('decompile')
51
51
  .description('Convert a caseplan.json to authored TypeScript.')
@@ -64,7 +64,7 @@ export function registerCaseCommands(program) {
64
64
  argv.push('--best-effort');
65
65
  if (options.pipeline === false)
66
66
  argv.push('--no-pipeline');
67
- await invoke('case decompile', () => loadTool(import.meta.url, '../../case/decompile-cli.js'), argv);
67
+ await invoke('case decompile', () => import('../../case/decompile-cli.js'), argv);
68
68
  });
69
69
  family.command('merge')
70
70
  .description('Merge an edited case plan with its original caseplan.json.')
@@ -82,6 +82,6 @@ export function registerCaseCommands(program) {
82
82
  const [edited, original] = pair;
83
83
  const argv = [original, edited];
84
84
  addValue(argv, '-o', options.output);
85
- await invoke('case merge', () => loadTool(import.meta.url, '../../case/merge-cli.js'), argv);
85
+ await invoke('case merge', () => import('../../case/merge-cli.js'), argv);
86
86
  });
87
87
  }
@@ -1,5 +1,5 @@
1
1
  import { Option } from 'commander';
2
- import { addValue, emitOnlyEnabled, inferCheckMode, invoke, loadTool, namedOrLegacySecond, retiredCompiledOption, refuse } from '../common.js';
2
+ import { addValue, emitOnlyEnabled, inferCheckMode, invoke, namedOrLegacySecond, retiredCompiledOption, refuse } from '../common.js';
3
3
  /** The retired compiled check's flags: `[commander flags, option key, description]`. One table, so hiding and warning cannot drift. */
4
4
  const RETIRED_COMPILED_CHECK_FLAGS = [
5
5
  ['--uip', 'uip', 'retired: resolve missing schemas with the UiPath CLI'],
@@ -32,7 +32,7 @@ export function registerFlowCommands(program) {
32
32
  addValue(argv, '--connectors-local', options.connectorsLocal);
33
33
  if (options.check === false || emitOnlyEnabled())
34
34
  argv.push('--no-check');
35
- await invoke('flow compile', () => loadTool(import.meta.url, '../../compile-cli.js'), argv);
35
+ await invoke('flow compile', () => import('../../compile-cli.js'), argv);
36
36
  });
37
37
  const check = flow.command('check')
38
38
  .description('Check authored TypeScript. Check a compiled .flow with `uip maestro flow validate`.')
@@ -69,7 +69,7 @@ export function registerFlowCommands(program) {
69
69
  const argv = [input];
70
70
  addValue(argv, '--library', options.library);
71
71
  addValue(argv, '--connectors-local', options.connectorsLocal);
72
- await invoke('flow check', () => loadTool(import.meta.url, '../../check-cli.js'), argv);
72
+ await invoke('flow check', () => import('../../check-cli.js'), argv);
73
73
  return;
74
74
  }
75
75
  // The COMPILED-artifact analyser is gone: it was a standalone tool bundled into this
@@ -100,15 +100,18 @@ export function registerFlowCommands(program) {
100
100
  argv.push('--strict');
101
101
  if (options.pipeline === false)
102
102
  argv.push('--no-pipeline');
103
- await invoke('flow decompile', () => loadTool(import.meta.url, '../../decompile-cli.js'), argv);
103
+ await invoke('flow decompile', () => import('../../decompile-cli.js'), argv);
104
104
  });
105
105
  flow.command('merge')
106
106
  .description('Merge an edited flow with its original artifact.')
107
- .argument('<edited>', 'edited .flow path')
107
+ .argument('<edited>', 'edited .flow path, or the edited .flow.ts source (merge compiles it, tolerating untouched draft nodes)')
108
108
  .option('--original <file>', '(required) original .flow path, the artifact to merge into')
109
109
  .option('-o, --output <file>', 'merged output path')
110
110
  .option('--baseline <file>', 'compiled pristine decompile')
111
- .option('--strict', 'refuse when the no-edit round trip (--baseline) differs from the original')
111
+ .option('--baseline-from <file>', 'build the baseline from this original .flow (decompile + compile, tolerating draft nodes) instead of --baseline')
112
+ .option('--library <dir>', 'connector library, for compiling with --baseline-from or a .flow.ts edited source')
113
+ .option('--bindings <file>', 'bindings.json, for the same compiles')
114
+ .option('--strict', 'refuse when the no-edit round trip (the baseline) differs from the original')
112
115
  .allowExcessArguments()
113
116
  .action(async (_edited, options, command) => {
114
117
  const pair = namedOrLegacySecond(command, {
@@ -121,8 +124,11 @@ export function registerFlowCommands(program) {
121
124
  const argv = [original, edited];
122
125
  addValue(argv, '-o', options.output);
123
126
  addValue(argv, '--baseline', options.baseline);
127
+ addValue(argv, '--baseline-from', options.baselineFrom);
128
+ addValue(argv, '--library', options.library);
129
+ addValue(argv, '--bindings', options.bindings);
124
130
  if (options.strict)
125
131
  argv.push('--strict');
126
- await invoke('flow merge', () => loadTool(import.meta.url, '../../merge-cli.js'), argv);
132
+ await invoke('flow merge', () => import('../../merge-cli.js'), argv);
127
133
  });
128
134
  }
@@ -1,4 +1,4 @@
1
- import { addValue, invoke, loadTool, namedOrLegacySecond } from '../common.js';
1
+ import { addValue, invoke, namedOrLegacySecond } from '../common.js';
2
2
  /** Commander hands a repeatable option one value at a time. */
3
3
  function collect(value, previous) {
4
4
  return [...previous, value];
@@ -49,7 +49,7 @@ export function registerRegistryCommands(program) {
49
49
  addValue(argv, '--root', options.root);
50
50
  addValue(argv, '--extractor', options.extractor);
51
51
  addValue(argv, '--generator', options.generator);
52
- await invoke('registry pull', () => loadTool(import.meta.url, '../../registry/pull-cli.js'), argv);
52
+ await invoke('registry pull', () => import('../../registry/pull-cli.js'), argv);
53
53
  });
54
54
  family.command('materialize')
55
55
  .description('Generate library-md and connectors/ from an extracted library-json.')
@@ -80,7 +80,7 @@ export function registerRegistryCommands(program) {
80
80
  argv.push('--check-orphans');
81
81
  addValue(argv, '--python', options.python);
82
82
  addValue(argv, '--generator', options.generator);
83
- await invoke('registry materialize', () => loadTool(import.meta.url, '../../registry/materialize-cli.js'), argv);
83
+ await invoke('registry materialize', () => import('../../registry/materialize-cli.js'), argv);
84
84
  });
85
85
  // The pipeline's generator, `generate_library_json.py`, flag for flag: the
86
86
  // Azure YAML that calls it should only have to swap the executable.
@@ -119,7 +119,7 @@ export function registerRegistryCommands(program) {
119
119
  addValue(argv, '--connectors', options.connectors);
120
120
  addValue(argv, '--catalog-scope', options.catalogScope);
121
121
  addValue(argv, '--concurrency', options.concurrency);
122
- await invoke('registry build-library', () => loadTool(import.meta.url, '../../registry/library-build/cli.js'), argv);
122
+ await invoke('registry build-library', () => import('../../registry/library-build/cli.js'), argv);
123
123
  });
124
124
  family.command('prepare')
125
125
  .description("Resolve one operation's schema against a live connection into ./.flow-sdk/connectors-local.")
@@ -200,7 +200,7 @@ export function registerRegistryCommands(program) {
200
200
  argv.push('--no-source-rewrite');
201
201
  addValue(argv, '--python', options.python);
202
202
  addValue(argv, '--generator', options.generator);
203
- await invoke('registry prepare', () => loadTool(import.meta.url, '../../registry/prepare-cli.js'), argv);
203
+ await invoke('registry prepare', () => import('../../registry/prepare-cli.js'), argv);
204
204
  });
205
205
  family.command('path')
206
206
  .description('Print where the cached connector library is.')
@@ -216,7 +216,7 @@ export function registerRegistryCommands(program) {
216
216
  if (options[key])
217
217
  argv.push(`--${flag}`);
218
218
  }
219
- await invoke('registry path', () => loadTool(import.meta.url, '../../registry/path-cli.js'), argv);
219
+ await invoke('registry path', () => import('../../registry/path-cli.js'), argv);
220
220
  });
221
221
  family.command('search')
222
222
  .description('Find connector operations in an extracted library.')
@@ -231,6 +231,6 @@ export function registerRegistryCommands(program) {
231
231
  addValue(argv, '--library-md', options.libraryMd);
232
232
  addValue(argv, '--connector', options.connector);
233
233
  addValue(argv, '--limit', options.limit);
234
- await invoke('registry search', () => loadTool(import.meta.url, '../../registry/search-cli.js'), argv);
234
+ await invoke('registry search', () => import('../../registry/search-cli.js'), argv);
235
235
  });
236
236
  }
@@ -1,8 +1,5 @@
1
1
  import { type Command, Option } from 'commander';
2
2
  export type ToolRun = (argv: string[]) => Promise<number> | number;
3
- export declare function loadTool(baseUrl: string, relativePath: string): Promise<{
4
- run: ToolRun;
5
- }>;
6
3
  export declare function invoke(label: string, load: () => Promise<{
7
4
  run: ToolRun;
8
5
  }>, argv: string[]): Promise<void>;
@@ -1,14 +1,12 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { Option } from 'commander';
4
- export function loadTool(baseUrl, relativePath) {
5
- return import(new URL(relativePath, baseUrl).href);
6
- }
7
4
  export async function invoke(label, load, argv) {
8
5
  // In `--json` mode stdout carries exactly one JSON object, printed by
9
6
  // `emitJsonResult` once the verb is done. The tool's own human lines go to
10
7
  // stderr meanwhile, so they are still seen but never mixed into it.
11
8
  const restore = jsonResultRequested() ? routeStdoutToStderr() : () => { };
9
+ globalThis[HOSTED_BY_CLI] = true;
12
10
  try {
13
11
  const code = await (await load()).run(argv);
14
12
  if (code !== 0)
@@ -24,6 +22,8 @@ export async function invoke(label, load, argv) {
24
22
  restore();
25
23
  }
26
24
  }
25
+ /** Tells `runWhenInvokedDirectly` (`src/cli-run.ts`) the tool is run by this CLI. */
26
+ const HOSTED_BY_CLI = Symbol.for('uipath.maestro-builder-sdk.cli-hosted');
27
27
  /**
28
28
  * The slot `src/cli-result.ts` writes (`reportResult`). Read here through the
29
29
  * same `Symbol.for` key, since `dist/cli/` and `dist/` are separate builds.
package/dist/cli-run.js CHANGED
@@ -14,7 +14,17 @@ function realOrAbsolute(path) {
14
14
  return resolve(path);
15
15
  }
16
16
  }
17
+ /**
18
+ * Set by the `flow-sdk` CLI before it imports a tool (`cli/common.ts`, read
19
+ * through the same `Symbol.for` key since `dist/cli/` is a separate build). A
20
+ * bundle gives every module the bundle's own `import.meta.url`, which matches
21
+ * `process.argv[1]` for whichever tool the CLI loads; without this the tool
22
+ * would run a second time, on the CLI's argv.
23
+ */
24
+ const HOSTED_BY_CLI = Symbol.for('uipath.maestro-builder-sdk.cli-hosted');
17
25
  export function runWhenInvokedDirectly(moduleUrl, label, run) {
26
+ if (globalThis[HOSTED_BY_CLI])
27
+ return;
18
28
  const entry = process.argv[1];
19
29
  if (!entry)
20
30
  return;
@@ -37,6 +37,12 @@ export interface ConnectorInputOpts {
37
37
  lookups?: Readonly<Record<string, LookupSpec>>;
38
38
  /** `ignore` skips an unresolved lookup instead of refusing it — what `check` wants. */
39
39
  unresolvedLookups?: 'error' | 'ignore';
40
+ /**
41
+ * Receives a missing REQUIRED input instead of refusing it — the draft
42
+ * tolerance of a merge-owned compile (#1134). Every other input error still
43
+ * refuses.
44
+ */
45
+ onMissingRequired?: (message: string) => void;
40
46
  }
41
47
  /** Flow's dialect, re-exported so `serialize.ts` need not reach past this module. */
42
48
  export { FLOW_EXPR_DIALECT } from './connector-checks.js';
@@ -116,8 +116,13 @@ export function buildConnectorInputs(rc, inputs, opts) {
116
116
  // not know what to put in it than because they forgot it exists: name the
117
117
  // id it sends and the helper that finds one, so "(string)" is not read as
118
118
  // "any string" — an e-mail into `fields.reporter.id` is exactly that misreading.
119
- if (f.required)
120
- errs.push(`missing required input "${f.name}" (${f.type ?? 'value'})${missingLookupHint(f.name, lookups[f.name], rc, dialect.descriptors)}`);
119
+ if (f.required) {
120
+ const message = `missing required input "${f.name}" (${f.type ?? 'value'})${missingLookupHint(f.name, lookups[f.name], rc, dialect.descriptors)}`;
121
+ if (opts.onMissingRequired)
122
+ opts.onMissingRequired(message);
123
+ else
124
+ errs.push(message);
125
+ }
121
126
  continue;
122
127
  }
123
128
  // An unresolved lookup is swapped for its recorded id, or reported. It is
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SHA-1 and SHA-256 of a string's UTF-8 bytes, in plain JavaScript.
3
+ *
4
+ * The ids built from them (`schemaId` in `serialize.ts`, `sha256Uuid` in
5
+ * `stable-id.ts`) are computed synchronously while a flow serializes, and must be
6
+ * byte-for-byte what `node:crypto` gives, since they are already in shipped
7
+ * artifacts. Owning the digest here keeps the authoring path free of `node:`
8
+ * imports, so a host without Node (a browser sandbox) runs it unshimmed; the
9
+ * browser's own `crypto.subtle.digest` is async-only. `tests/sha.test.ts` checks
10
+ * both against `node:crypto`. They derive ids, nothing secret: not for passwords,
11
+ * signatures or integrity checks.
12
+ */
13
+ export declare function sha1(text: string): Uint8Array;
14
+ export declare function sha256(text: string): Uint8Array;
15
+ /** Lowercase hex of `bytes`, as `digest('hex')` writes it. */
16
+ export declare function bytesToHex(bytes: Uint8Array): string;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * SHA-1 and SHA-256 of a string's UTF-8 bytes, in plain JavaScript.
3
+ *
4
+ * The ids built from them (`schemaId` in `serialize.ts`, `sha256Uuid` in
5
+ * `stable-id.ts`) are computed synchronously while a flow serializes, and must be
6
+ * byte-for-byte what `node:crypto` gives, since they are already in shipped
7
+ * artifacts. Owning the digest here keeps the authoring path free of `node:`
8
+ * imports, so a host without Node (a browser sandbox) runs it unshimmed; the
9
+ * browser's own `crypto.subtle.digest` is async-only. `tests/sha.test.ts` checks
10
+ * both against `node:crypto`. They derive ids, nothing secret: not for passwords,
11
+ * signatures or integrity checks.
12
+ */
13
+ function utf8(text) {
14
+ return new TextEncoder().encode(text);
15
+ }
16
+ /** The message padded to a multiple of 64 bytes, with its bit length in the last 8, big-endian. */
17
+ function padded(message) {
18
+ const length = Math.ceil((message.length + 9) / 64) * 64;
19
+ const bytes = new Uint8Array(length);
20
+ bytes.set(message);
21
+ bytes[message.length] = 0x80;
22
+ const view = new DataView(bytes.buffer);
23
+ const bits = message.length * 8;
24
+ view.setUint32(length - 8, Math.floor(bits / 0x100000000));
25
+ view.setUint32(length - 4, bits >>> 0);
26
+ return view;
27
+ }
28
+ function digest(words) {
29
+ const out = new Uint8Array(words.length * 4);
30
+ const view = new DataView(out.buffer);
31
+ words.forEach((word, i) => view.setUint32(i * 4, word));
32
+ return out;
33
+ }
34
+ const rotl = (x, n) => (x << n) | (x >>> (32 - n));
35
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
36
+ export function sha1(text) {
37
+ const view = padded(utf8(text));
38
+ const h = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
39
+ const w = new Array(80);
40
+ for (let block = 0; block < view.byteLength; block += 64) {
41
+ for (let i = 0; i < 16; i++)
42
+ w[i] = view.getUint32(block + i * 4);
43
+ for (let i = 16; i < 80; i++)
44
+ w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
45
+ let [a, b, c, d, e] = h;
46
+ for (let i = 0; i < 80; i++) {
47
+ const [f, k] = i < 20 ? [(b & c) | (~b & d), 0x5a827999]
48
+ : i < 40 ? [b ^ c ^ d, 0x6ed9eba1]
49
+ : i < 60 ? [(b & c) | (b & d) | (c & d), 0x8f1bbcdc]
50
+ : [b ^ c ^ d, 0xca62c1d6];
51
+ const t = (rotl(a, 5) + f + e + k + w[i]) | 0;
52
+ e = d;
53
+ d = c;
54
+ c = rotl(b, 30);
55
+ b = a;
56
+ a = t;
57
+ }
58
+ h[0] = (h[0] + a) | 0;
59
+ h[1] = (h[1] + b) | 0;
60
+ h[2] = (h[2] + c) | 0;
61
+ h[3] = (h[3] + d) | 0;
62
+ h[4] = (h[4] + e) | 0;
63
+ }
64
+ return digest(h);
65
+ }
66
+ const K256 = [
67
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
68
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
69
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
70
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
71
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
72
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
73
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
74
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
75
+ ];
76
+ export function sha256(text) {
77
+ const view = padded(utf8(text));
78
+ const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
79
+ const w = new Array(64);
80
+ for (let block = 0; block < view.byteLength; block += 64) {
81
+ for (let i = 0; i < 16; i++)
82
+ w[i] = view.getUint32(block + i * 4);
83
+ for (let i = 16; i < 64; i++) {
84
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
85
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
86
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
87
+ }
88
+ let [a, b, c, d, e, f, g, hh] = h;
89
+ for (let i = 0; i < 64; i++) {
90
+ const t1 = (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K256[i] + w[i]) | 0;
91
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) | 0;
92
+ hh = g;
93
+ g = f;
94
+ f = e;
95
+ e = (d + t1) | 0;
96
+ d = c;
97
+ c = b;
98
+ b = a;
99
+ a = (t1 + t2) | 0;
100
+ }
101
+ h[0] = (h[0] + a) | 0;
102
+ h[1] = (h[1] + b) | 0;
103
+ h[2] = (h[2] + c) | 0;
104
+ h[3] = (h[3] + d) | 0;
105
+ h[4] = (h[4] + e) | 0;
106
+ h[5] = (h[5] + f) | 0;
107
+ h[6] = (h[6] + g) | 0;
108
+ h[7] = (h[7] + hh) | 0;
109
+ }
110
+ return digest(h);
111
+ }
112
+ /** Lowercase hex of `bytes`, as `digest('hex')` writes it. */
113
+ export function bytesToHex(bytes) {
114
+ let out = '';
115
+ for (const byte of bytes)
116
+ out += byte.toString(16).padStart(2, '0');
117
+ return out;
118
+ }
@@ -36,7 +36,7 @@
36
36
  * site uses; `tests/cli.test.ts` pins the Case entry point's `uniqueId`. Change one
37
37
  * only deliberately, updating that table and any byte-lock fixture it moves.
38
38
  */
39
- import { createHash } from 'node:crypto';
39
+ import { bytesToHex, sha1, sha256 } from './sha.js';
40
40
  /** A uuid-shaped id derived from `seed`. Every segment is unsigned, including the `>>> 0` fix explained above. */
41
41
  export function stableId(seed) {
42
42
  let h1 = 0x811c9dc5;
@@ -88,7 +88,7 @@ export function fnv1aX4Uuid(seed) {
88
88
  * Consumer: a Case entry point's `uniqueId` in `entry-points.json` (`case/compile-cli.ts`).
89
89
  */
90
90
  export function sha256Uuid(seed) {
91
- const h = createHash('sha256').update(seed).digest('hex');
91
+ const h = bytesToHex(sha256(seed));
92
92
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
93
93
  }
94
94
  /**
@@ -100,10 +100,9 @@ export function sha256Uuid(seed) {
100
100
  * (SHA-1 based, version + variant bits set) because that is what the field holds.
101
101
  */
102
102
  export function schemaId(flowId, nodeId) {
103
- const h = createHash('sha1').update(`uipath.human-in-the-loop:${flowId}:${nodeId}`).digest();
104
- const b = Buffer.from(h.subarray(0, 16));
103
+ const b = sha1(`uipath.human-in-the-loop:${flowId}:${nodeId}`).slice(0, 16);
105
104
  b[6] = (b[6] & 0x0f) | 0x50;
106
105
  b[8] = (b[8] & 0x3f) | 0x80;
107
- const hex = b.toString('hex');
108
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
106
+ const h = bytesToHex(b);
107
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
109
108
  }
@@ -6,15 +6,13 @@
6
6
  * Kept apart from `serialize.ts` so the registry can depend on the bundle without
7
7
  * importing the serializer that depends on the registry.
8
8
  */
9
- import { readFileSync } from 'node:fs';
10
- import { dirname, join } from 'node:path';
11
- import { fileURLToPath } from 'node:url';
9
+ import coreDefinitions from './core-definitions.json' with { type: 'json' };
12
10
  /**
13
11
  * The bundled definitions, keyed `<nodeType>@<version>`. Deep-frozen: every reader
14
12
  * shares this one copy, so a write meant for one node must clone first, and one
15
13
  * that does not throws at its own site instead of leaking into another reader.
16
14
  */
17
- export const BUNDLED_DEFINITIONS = deepFreeze(JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'core-definitions.json'), 'utf8')));
15
+ export const BUNDLED_DEFINITIONS = deepFreeze(coreDefinitions);
18
16
  function deepFreeze(value) {
19
17
  if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
20
18
  Object.freeze(value);
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import { type CliExitCode } from './cli-run.js';
3
- export declare function run(argv: string[]): CliExitCode;
3
+ export declare function run(argv: string[]): Promise<CliExitCode>;
package/dist/merge-cli.js CHANGED
@@ -3,7 +3,13 @@
3
3
  * merge-cli — merge an edited flow back into the original `.flow` (bin `flow-merge`)
4
4
  *
5
5
  * Usage:
6
- * flow-merge <original.flow> <edited.flow> [-o merged.flow] [--baseline <baseline.flow>] [--strict]
6
+ * flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow]
7
+ * [--baseline <baseline.flow> | --baseline-from <original.flow>] [--strict]
8
+ *
9
+ * `--baseline-from <original.flow>` makes merge build the baseline itself
10
+ * (decompile + compile, nothing written), and an edited `.flow.ts` makes merge
11
+ * compile it: both compiles tolerate DRAFT nodes, and merge refuses a draft
12
+ * only where it would take the node from the edited flow. See merge-compile.ts.
7
13
  *
8
14
  * `edited.flow` is compile(decompile(original) after your edits). `--baseline`
9
15
  * is compile(decompile(original)) BEFORE any edit — supplying it enables the
@@ -19,27 +25,88 @@ import { readFileSync } from 'node:fs';
19
25
  import { basename, resolve } from 'node:path';
20
26
  import { MergeRefusedError, mergeFlow } from './merge.js';
21
27
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
22
- export function run(argv) {
28
+ import { ensureTypeScriptRuntime } from './node-runtime.js';
29
+ import { flowCheckOpts, openCliLibrary } from './cli-library.js';
30
+ import { Bindings } from './bindings.js';
31
+ import { defaultBindingsFile } from './workdir.js';
32
+ import { baselineFromOriginal, checkEdited, classifyDrafts, compileSourceDraftTolerant } from './merge-compile.js';
33
+ const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
34
+ + '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
35
+ export async function run(argv) {
23
36
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
24
- console.error('usage: flow-merge <original.flow> <edited.flow> [-o merged.flow] [--baseline <baseline.flow>] [--strict]');
37
+ console.error(USAGE);
25
38
  return 2;
26
39
  }
27
- const flags = new Set(['-o', '--baseline']);
40
+ const flags = new Set(['-o', '--baseline', '--baseline-from', '--library', '--bindings', '--connectors-local']);
28
41
  const positionals = argv.filter((a, i) => !flags.has(a) && !flags.has(argv[i - 1]) && !a.startsWith('-'));
29
42
  const [originalPath, editedPath] = positionals;
30
43
  if (!originalPath || !editedPath) {
31
44
  console.error('flow-merge: need <original.flow> and <edited.flow>');
32
45
  return 2;
33
46
  }
34
- const original = JSON.parse(readFileSync(originalPath, 'utf8'));
35
- const edited = JSON.parse(readFileSync(editedPath, 'utf8'));
36
47
  const baselinePath = flagValue(argv, '--baseline');
37
- const baseline = baselinePath ? JSON.parse(readFileSync(baselinePath, 'utf8')) : undefined;
48
+ const baselineFrom = flagValue(argv, '--baseline-from');
49
+ if (baselinePath && baselineFrom) {
50
+ console.error('flow-merge: pass --baseline or --baseline-from, not both');
51
+ return 2;
52
+ }
53
+ const editedIsSource = /\.ts$/.test(editedPath);
54
+ // Merge compiles a side itself only for --baseline-from or a .flow.ts edited
55
+ // source; both load TypeScript, so both need the type-stripping runtime.
56
+ if (baselineFrom || editedIsSource)
57
+ ensureTypeScriptRuntime();
58
+ const library = baselineFrom || editedIsSource ? openCliLibrary(argv) : undefined;
59
+ const bindings = baselineFrom || editedIsSource ? new Bindings(flagValue(argv, '--bindings') ?? defaultBindingsFile()) : undefined;
60
+ const compileOpts = { library, bindings };
61
+ const original = JSON.parse(readFileSync(originalPath, 'utf8'));
62
+ let drafts = [];
63
+ let diagnostics = [];
64
+ let edited;
65
+ if (editedIsSource) {
66
+ // The edited source gets what `compile` gives it: check() with the same
67
+ // options, and the serializer's warnings (CONNECTION_STUB, ...) printed.
68
+ const compiled = await compileSourceDraftTolerant(resolve(editedPath), {
69
+ ...compileOpts,
70
+ warn: (w) => console.error(` ⚠ [${w.code}] ${w.message}`),
71
+ });
72
+ edited = compiled.flow;
73
+ drafts = compiled.drafts;
74
+ diagnostics = checkEdited(compiled.built, edited, flowCheckOpts(library, bindings));
75
+ }
76
+ else {
77
+ edited = JSON.parse(readFileSync(editedPath, 'utf8'));
78
+ }
79
+ const baseline = baselineFrom
80
+ ? (await baselineFromOriginal(baselineFrom, compileOpts, (m) => console.error(`flow-merge: decompile warning: ${m}`))).flow
81
+ : baselinePath ? JSON.parse(readFileSync(baselinePath, 'utf8')) : undefined;
38
82
  const strict = argv.includes('--strict');
39
83
  if (strict && !baseline) {
40
- console.error('flow-merge: --strict compares the no-edit round trip with the original; pass --baseline <baseline.flow>');
84
+ console.error('flow-merge: --strict compares the no-edit round trip with the original; pass --baseline <baseline.flow> or --baseline-from <original.flow>');
41
85
  return 2;
42
86
  }
87
+ // A draft node (no connection, an empty required input, no object) is
88
+ // acceptable only where merge restores the ORIGINAL node; one the author
89
+ // changed or added gets the strict compile's refusal (#1134).
90
+ // check()'s errors follow the same rule; its warnings are printed as compile prints them.
91
+ const { kept, refused } = classifyDrafts(original, edited, baseline, drafts);
92
+ const checked = classifyDrafts(original, edited, baseline, diagnostics.filter((d) => d.level === 'error'));
93
+ for (const d of diagnostics)
94
+ if (d.level === 'warning')
95
+ console.error(` ⚠ [${d.code}] ${d.message}`);
96
+ for (const d of [...kept, ...checked.kept]) {
97
+ console.error(` ⚠ [DRAFT_NODE_KEPT] "${d.nodeId}" is unfinished in the original (${d.code}); it is untouched, so the original node is kept as is.`);
98
+ }
99
+ const errors = [...refused, ...checked.refused];
100
+ if (errors.length) {
101
+ for (const d of errors) {
102
+ const where = d.nodeId === '' ? 'the edited flow'
103
+ : d.reason === 'subflow' ? `"${d.nodeId}" is inside a subflow, which merge takes whole; finish it`
104
+ : `"${d.nodeId}" is changed or new in the edited flow`;
105
+ console.error(` ✗ [${d.code}] ${where}: ${d.message}`);
106
+ }
107
+ console.error(`flow-merge: ${errors.length} error(s) in nodes merge takes from the edited flow; finish them or revert the edit. Nothing written.`);
108
+ return 1;
109
+ }
43
110
  let merged;
44
111
  try {
45
112
  merged = mergeFlow(original, edited, {
@@ -0,0 +1,55 @@
1
+ import { type DraftFinding, type SerializeOpts } from './serialize.js';
2
+ import { type CheckOpts, type Diagnostic } from './check.js';
3
+ import type { BuiltFlow } from './flow-sdk.js';
4
+ /** A compile's flow and the draft refusals it reported instead of throwing. */
5
+ export interface DraftTolerantCompile {
6
+ flow: any;
7
+ drafts: DraftFinding[];
8
+ }
9
+ /** Compile `built` with draft tolerance. Every other refusal still throws. */
10
+ export declare function compileDraftTolerant(built: BuiltFlow, opts?: Omit<SerializeOpts, 'draft'>): DraftTolerantCompile;
11
+ /**
12
+ * Load a `.flow.ts` source and compile it with draft tolerance.
13
+ * The caller has already called `ensureTypeScriptRuntime()`.
14
+ */
15
+ export declare function compileSourceDraftTolerant(file: string, opts?: Omit<SerializeOpts, 'draft'>): Promise<DraftTolerantCompile & {
16
+ built: BuiltFlow;
17
+ }>;
18
+ /**
19
+ * The baseline of `originalPath`: decompile it (reading agent sidecars beside
20
+ * it, as `flow decompile` does), then compile the source with draft tolerance.
21
+ * The source goes to a temporary directory that is removed afterwards.
22
+ */
23
+ export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft'>, warn?: (message: string) => void): Promise<DraftTolerantCompile>;
24
+ /** Why merge takes a node from the edited flow, so a finding on it is an error. */
25
+ export type TakenReason = 'edited' | 'subflow';
26
+ /** A finding on a node merge takes from the edited flow, with the reason it takes it. */
27
+ export type Refused<T> = T & {
28
+ reason: TakenReason;
29
+ };
30
+ /**
31
+ * Sort each finding by what merge does with its node: `refused` when the node
32
+ * comes from the edited flow (reason `edited`: the author changed or added it;
33
+ * reason `subflow`: it sits in a subflow, which merge takes from `edited`
34
+ * whole), `kept` when merge restores the original node.
35
+ *
36
+ * For instance a Slack node with no connection that the author did not touch
37
+ * is `kept`; the same node with a new message text is `refused` as `edited`.
38
+ */
39
+ export declare function classifyDrafts<T extends {
40
+ nodeId: string;
41
+ }>(original: any, edited: any, baseline: any, findings: readonly T[]): {
42
+ kept: T[];
43
+ refused: Refused<T>[];
44
+ };
45
+ /** A `check()` diagnostic, placed on the root node its step emitted (`''` when it names no root step). */
46
+ export type PlacedDiagnostic = Diagnostic & {
47
+ nodeId: string;
48
+ };
49
+ /**
50
+ * Run `check()` on the edited source, as `compile` does, and place each
51
+ * diagnostic on the node its step emitted in `editedFlow`, so
52
+ * {@link classifyDrafts} can sort it. A diagnostic with no step, or a step
53
+ * with no root node, gets nodeId `''`, which is always refused.
54
+ */
55
+ export declare function checkEdited(built: BuiltFlow, editedFlow: any, opts: CheckOpts): PlacedDiagnostic[];
@@ -0,0 +1,104 @@
1
+ /**
2
+ * merge-compile — the compiles `flow merge --baseline-from` runs itself (#1134).
3
+ *
4
+ * Both compiles tolerate draft nodes (`SerializeOpts.draft`). A draft on a node
5
+ * merge keeps from the original is a warning; a draft or `check()` error on a
6
+ * node merge takes from the edited flow is an error. Nothing draft-tolerant is
7
+ * written to disk.
8
+ */
9
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { decompile } from './decompile.js';
13
+ import { loadBuiltFlow } from './load.js';
14
+ import { serialize } from './serialize.js';
15
+ import { check } from './check.js';
16
+ import { nodesTakenFromEdited } from './merge.js';
17
+ import { PACKAGE_NAME } from './package-name.js';
18
+ /** Compile `built` with draft tolerance. Every other refusal still throws. */
19
+ export function compileDraftTolerant(built, opts = {}) {
20
+ const drafts = [];
21
+ const flow = serialize(built, { ...opts, draft: (f) => drafts.push(f) });
22
+ return { flow, drafts };
23
+ }
24
+ /**
25
+ * Load a `.flow.ts` source and compile it with draft tolerance.
26
+ * The caller has already called `ensureTypeScriptRuntime()`.
27
+ */
28
+ export async function compileSourceDraftTolerant(file, opts = {}) {
29
+ const built = await loadBuiltFlow(file);
30
+ return { ...compileDraftTolerant(built, opts), built };
31
+ }
32
+ /**
33
+ * The baseline of `originalPath`: decompile it (reading agent sidecars beside
34
+ * it, as `flow decompile` does), then compile the source with draft tolerance.
35
+ * The source goes to a temporary directory that is removed afterwards.
36
+ */
37
+ export async function baselineFromOriginal(originalPath, opts = {}, warn) {
38
+ const original = JSON.parse(readFileSync(originalPath, 'utf8'));
39
+ const source = decompile(original, {
40
+ importSpecifier: PACKAGE_NAME,
41
+ warn,
42
+ readSidecar: (relativePath) => {
43
+ try {
44
+ return readFileSync(join(dirname(resolve(originalPath)), relativePath), 'utf8');
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ },
50
+ });
51
+ const dir = mkdtempSync(join(tmpdir(), 'flow-merge-baseline-'));
52
+ try {
53
+ writeFileSync(join(dir, 'package.json'), '{"type":"module"}\n');
54
+ const file = join(dir, 'Baseline.flow.ts');
55
+ writeFileSync(file, source);
56
+ return await compileSourceDraftTolerant(file, opts);
57
+ }
58
+ finally {
59
+ rmSync(dir, { recursive: true, force: true });
60
+ }
61
+ }
62
+ /**
63
+ * Sort each finding by what merge does with its node: `refused` when the node
64
+ * comes from the edited flow (reason `edited`: the author changed or added it;
65
+ * reason `subflow`: it sits in a subflow, which merge takes from `edited`
66
+ * whole), `kept` when merge restores the original node.
67
+ *
68
+ * For instance a Slack node with no connection that the author did not touch
69
+ * is `kept`; the same node with a new message text is `refused` as `edited`.
70
+ */
71
+ export function classifyDrafts(original, edited, baseline, findings) {
72
+ const fromEdited = new Set(nodesTakenFromEdited(original, edited, baseline));
73
+ const rootIds = new Set((edited?.nodes ?? []).map((n) => n.id));
74
+ const kept = [];
75
+ const refused = [];
76
+ for (const f of findings) {
77
+ if (!rootIds.has(f.nodeId))
78
+ refused.push({ ...f, reason: 'subflow' });
79
+ else if (fromEdited.has(f.nodeId))
80
+ refused.push({ ...f, reason: 'edited' });
81
+ else
82
+ kept.push(f);
83
+ }
84
+ return { kept, refused };
85
+ }
86
+ /**
87
+ * Run `check()` on the edited source, as `compile` does, and place each
88
+ * diagnostic on the node its step emitted in `editedFlow`, so
89
+ * {@link classifyDrafts} can sort it. A diagnostic with no step, or a step
90
+ * with no root node, gets nodeId `''`, which is always refused.
91
+ */
92
+ export function checkEdited(built, editedFlow, opts) {
93
+ const byStep = new Map();
94
+ for (const n of editedFlow?.nodes ?? []) {
95
+ const label = n?.display?.label;
96
+ if (typeof label === 'string' && !byStep.has(label))
97
+ byStep.set(label, n.id);
98
+ }
99
+ const ids = new Set((editedFlow?.nodes ?? []).map((n) => n.id));
100
+ return check(built, opts).map((d) => ({
101
+ ...d,
102
+ nodeId: d.step === undefined ? '' : byStep.get(d.step) ?? (ids.has(d.step) ? d.step : ''),
103
+ }));
104
+ }
package/dist/merge.d.ts CHANGED
@@ -152,4 +152,13 @@ export interface MergeOptions {
152
152
  */
153
153
  strict?: boolean;
154
154
  }
155
+ /**
156
+ * The ids of the root nodes {@link mergeFlow} takes from `edited` rather than
157
+ * restoring from `original`: every node that is new, or that differs from its
158
+ * `baseline` counterpart. With no baseline, that is every node.
159
+ *
160
+ * For instance, with a baseline equal to `edited` and every node present in
161
+ * `original`, the answer is `[]` — a no-edit merge keeps them all.
162
+ */
163
+ export declare function nodesTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
155
164
  export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?: MergeOptions): AnyFlow;
package/dist/merge.js CHANGED
@@ -97,6 +97,27 @@ function nodeUnchanged(a, b) {
97
97
  return false;
98
98
  return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {});
99
99
  }
100
+ /**
101
+ * The ids of the root nodes {@link mergeFlow} takes from `edited` rather than
102
+ * restoring from `original`: every node that is new, or that differs from its
103
+ * `baseline` counterpart. With no baseline, that is every node.
104
+ *
105
+ * For instance, with a baseline equal to `edited` and every node present in
106
+ * `original`, the answer is `[]` — a no-edit merge keeps them all.
107
+ */
108
+ export function nodesTakenFromEdited(original, edited, baseline) {
109
+ const origNodes = byId(original.nodes);
110
+ const baseNodes = byId(baseline?.nodes);
111
+ return (edited.nodes ?? []).filter((e) => !isKept(e, origNodes, baseNodes, baseline)).map((e) => e.id);
112
+ }
113
+ /**
114
+ * Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
115
+ * baseline, the original has the node, and `e` equals its baseline counterpart.
116
+ * {@link mergeFlow}'s node loop and {@link nodesTakenFromEdited} both ask this.
117
+ */
118
+ function isKept(e, origNodes, baseNodes, baseline) {
119
+ return !!baseline && origNodes.has(e.id) && nodeUnchanged(e, baseNodes.get(e.id));
120
+ }
100
121
  /**
101
122
  * Give an edited node the original's position, by id.
102
123
  *
@@ -132,7 +153,7 @@ export function mergeFlow(original, edited, options = {}) {
132
153
  const finalNodes = [];
133
154
  for (const e of edited.nodes ?? []) {
134
155
  const orig = origNodes.get(e.id);
135
- if (options.baseline && orig && nodeUnchanged(e, baseNodes.get(e.id))) {
156
+ if (isKept(e, origNodes, baseNodes, options.baseline) && orig) {
136
157
  // Untouched by the user (equal to the faithful baseline) → keep the
137
158
  // original node exactly, placeholder-safe.
138
159
  finalNodes.push(jsonClone(orig));
@@ -4,8 +4,10 @@
4
4
  * `sdk-resolve-hook.ts` for why. Idempotent; call it before importing authored
5
5
  * source.
6
6
  *
7
- * Does nothing when the compiled hook is absent, which is the case only when
8
- * the SDK runs from its TypeScript source under the test runner. The runner
9
- * resolves imports itself, so no hook is needed there.
7
+ * Does nothing when the SDK runs from its TypeScript source under the test
8
+ * runner, which resolves imports itself. Anywhere else a missing hook means the
9
+ * SDK was bundled: authored source would then load a second copy of the
10
+ * package, its expressions would fail this copy's `instanceof` checks, and the
11
+ * `.flow` would come out wrong with no error. That throws instead.
10
12
  */
11
13
  export declare function useThisSdkForAuthoredSource(): void;
@@ -2,6 +2,7 @@
2
2
  import { existsSync } from 'node:fs';
3
3
  import { register } from 'node:module';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { PACKAGE_NAME } from './package-name.js';
5
6
  let registered = false;
6
7
  /**
7
8
  * Make `import … from '@uipath/maestro-builder-sdk'` in authored source resolve
@@ -9,16 +10,24 @@ let registered = false;
9
10
  * `sdk-resolve-hook.ts` for why. Idempotent; call it before importing authored
10
11
  * source.
11
12
  *
12
- * Does nothing when the compiled hook is absent, which is the case only when
13
- * the SDK runs from its TypeScript source under the test runner. The runner
14
- * resolves imports itself, so no hook is needed there.
13
+ * Does nothing when the SDK runs from its TypeScript source under the test
14
+ * runner, which resolves imports itself. Anywhere else a missing hook means the
15
+ * SDK was bundled: authored source would then load a second copy of the
16
+ * package, its expressions would fail this copy's `instanceof` checks, and the
17
+ * `.flow` would come out wrong with no error. That throws instead.
15
18
  */
16
19
  export function useThisSdkForAuthoredSource() {
17
20
  if (registered)
18
21
  return;
19
- registered = true;
20
22
  const hook = new URL('./sdk-resolve-hook.js', import.meta.url);
21
- if (!existsSync(fileURLToPath(hook)))
22
- return;
23
+ if (!existsSync(fileURLToPath(hook))) {
24
+ if (import.meta.url.endsWith('.ts')) {
25
+ registered = true;
26
+ return;
27
+ }
28
+ throw new Error(`cannot point authored source at this copy of ${PACKAGE_NAME}: ${fileURLToPath(hook)} is missing, as in a bundle, `
29
+ + 'so its imports would load a second copy and serialize wrong. Run the installed package, or load authored source yourself.');
30
+ }
23
31
  register(hook);
32
+ registered = true;
24
33
  }
@@ -1,6 +1,7 @@
1
1
  import type { DefinitionReference } from './core/node-classes.js';
2
2
  import type { BuiltFlow } from './flow-sdk.js';
3
3
  import type { Library } from './library.js';
4
+ import { type BindingRefusalCode } from './core/binding-messages.js';
4
5
  import type { Bindings } from './bindings.js';
5
6
  import { connectorInputErrors as coreConnectorInputErrors } from './core/connector-inputs.js';
6
7
  /**
@@ -47,6 +48,18 @@ export interface SerializeWarning {
47
48
  code: string;
48
49
  message: string;
49
50
  }
51
+ /** A required connector input left empty — the one draft code with no error class of its own. */
52
+ export declare const CONNECTOR_INPUT_MISSING = "CONNECTOR_INPUT_MISSING";
53
+ /** The refusals a draft-tolerant compile reports instead of throwing. */
54
+ export type DraftCode = BindingRefusalCode | typeof CONNECTOR_INPUT_MISSING | 'CONNECTOR_OBJECT_REQUIRED';
55
+ /** A refusal a draft-tolerant compile reported instead of throwing (see {@link SerializeOpts.draft}). */
56
+ export interface DraftFinding {
57
+ /** The emitted node's id. */
58
+ nodeId: string;
59
+ code: DraftCode;
60
+ /** The sentence the strict compile refuses with. */
61
+ message: string;
62
+ }
50
63
  export interface SerializeOpts {
51
64
  library?: Library;
52
65
  bindings?: Bindings;
@@ -56,6 +69,19 @@ export interface SerializeOpts {
56
69
  * hears nothing, which keeps `serialize()` silent in tests and libraries.
57
70
  */
58
71
  warn?: (warning: SerializeWarning) => void;
72
+ /**
73
+ * Draft tolerance, for a compile whose output only `merge` reads (#1134).
74
+ *
75
+ * A canvas flow often holds DRAFT nodes — a connector with no connection, an
76
+ * empty required input, a generic operation with no object — which the
77
+ * canvas saves and compile refuses. In a brownfield merge an untouched node
78
+ * is restored from the original anyway, so refusing it only blocks the edit.
79
+ * When this callback is given, those three refusals are reported here, with
80
+ * the node's id, and the node is emitted with what it has; `merge` then
81
+ * refuses any such node it would take from the edited flow. Absent (every
82
+ * ordinary compile), behaviour is unchanged.
83
+ */
84
+ draft?: (finding: DraftFinding) => void;
59
85
  /**
60
86
  * Filled in by `serialize` with the extra files the flow needs beside the
61
87
  * `.flow`, relative to it: one `<source>/agent.json` per inline agent, plus a
package/dist/serialize.js CHANGED
@@ -6,9 +6,7 @@
6
6
  * resolved from the connector library (entry + v1def sidecar), matching what
7
7
  * the platform emits, so the result passes `uip maestro flow validate`.
8
8
  */
9
- import { readFileSync } from 'node:fs';
10
- import { fileURLToPath } from 'node:url';
11
- import { dirname, join } from 'node:path';
9
+ import ixpDefinition from './ixp-definition.json' with { type: 'json' };
12
10
  import { BUNDLED_DEFINITIONS, bundledVersions } from './flow-definitions.js';
13
11
  import { Expr, toExpr, SCHEDULE_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf } from './flow-sdk.js';
14
12
  import { FlowAction, FlowTrigger } from './core/node-classes.js';
@@ -22,6 +20,7 @@ import { stepExits } from './flow-step-exits.js';
22
20
  import { resolveVersionCarried } from './core/nodes/versioned.js';
23
21
  import { indexCarriedDefinitions } from './core/carried-definitions.js';
24
22
  import { FORM_FIELD_JSON_TYPE } from './core/form-field-type.js';
23
+ import { ConnectorResolveError } from './core/library.js';
25
24
  import { connectionConnectors } from './bindings-v2.js';
26
25
  import { BindingRefusal, GUID_RE, bindingLabelRefusal, bindingMissingMessage, missingBindingLabels } from './core/binding-messages.js';
27
26
  import { ByoaRefusal, byoaUnverified } from './core/byoa.js';
@@ -65,7 +64,6 @@ import { types } from './core/expr.js';
65
64
  import { SUBFLOW_START_LABEL } from './core/subflow-start.js';
66
65
  import { ownEntry } from './core/own-entry.js';
67
66
  import { jsonClone } from './core/json-file.js';
68
- const HERE = dirname(fileURLToPath(import.meta.url));
69
67
  // core-definitions.json is an object keyed by "<nodeType>@<version>".
70
68
  const DEFS = BUNDLED_DEFINITIONS;
71
69
  // FIRST occurrence wins: with multiple bundled versions of one family (HITL
@@ -2592,7 +2590,7 @@ function promptTokens(content) {
2592
2590
  * ["fileRef"]` (the pre-pull answer) instead of the three the platform's
2593
2591
  * `ixp-node` validator actually enforces.
2594
2592
  */
2595
- const IXP_DEF = JSON.parse(readFileSync(join(HERE, 'ixp-definition.json'), 'utf8'));
2593
+ const IXP_DEF = ixpDefinition;
2596
2594
  /**
2597
2595
  * Build the definition for one published IxP project.
2598
2596
  *
@@ -3002,6 +3000,8 @@ function scheduleInputs(inputs, nodeId) {
3002
3000
  function outcomeId(name, index) {
3003
3001
  return outcomeSlug(name, index);
3004
3002
  }
3003
+ /** A required connector input left empty — the one draft code with no error class of its own. */
3004
+ export const CONNECTOR_INPUT_MISSING = 'CONNECTOR_INPUT_MISSING';
3005
3005
  /**
3006
3006
  * `serialize`, plus the extra files the flow needs on disk beside it.
3007
3007
  *
@@ -4961,20 +4961,67 @@ export function serialize(built, opts = {}) {
4961
4961
  if (spec.kind === 'connector') {
4962
4962
  if (!opts.library)
4963
4963
  throw new Error(`connector "${step.name}" needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
4964
- const rc = opts.library.resolve(spec.key, spec.action, spec.version, spec.object);
4964
+ let rc;
4965
+ try {
4966
+ rc = opts.library.resolve(spec.key, spec.action, spec.version, spec.object);
4967
+ }
4968
+ catch (error) {
4969
+ if (!opts.draft || !(error instanceof ConnectorResolveError) || error.code !== 'CONNECTOR_OBJECT_REQUIRED')
4970
+ throw error;
4971
+ // A generic operation saved before its object was picked has no
4972
+ // definition to emit against. Draft tolerance emits a stable stand-in
4973
+ // under the node's id; merge restores the original when it is untouched
4974
+ // and refuses it otherwise.
4975
+ const id = stepUid(scope, step.name);
4976
+ opts.draft({ nodeId: id, code: error.code, message: error.message });
4977
+ // It leaves on the port every connector action leaves on, so the
4978
+ // no-edit round trip rewires nothing. Its inputs carry what the author
4979
+ // wrote, so an edit to the node (an input, a connection) shows up as a
4980
+ // change and merge refuses it instead of restoring the original.
4981
+ const type = `uipath.connector.${spec.key}.${spec.action}`;
4982
+ scope.nodes.push({
4983
+ id, type, typeVersion: spec.version ?? '1.0.0',
4984
+ ...(parentId ? { parentId } : {}), display: { label: step.name },
4985
+ inputs: {
4986
+ draft: true,
4987
+ spec: renderDeep(spec.inputs ?? {}, scope.rename),
4988
+ ...(spec.connection !== undefined ? { connection: spec.connection } : {}),
4989
+ ...(spec.folder !== undefined ? { folder: spec.folder } : {}),
4990
+ },
4991
+ });
4992
+ for (const t of tails)
4993
+ addEdge(scope, t, id);
4994
+ return [{ node: id, port: successPort(type) }];
4995
+ }
4965
4996
  const id = stepUid(scope, step.name);
4966
4997
  const node = makeNode(rc.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def: rc.v1def });
4967
4998
  declareListOutputType(node, rc);
4968
- requireBindingPair(`connector "${step.name}"`, spec, { key: spec.key, action: spec.action });
4999
+ // A draft that names no connection gets no binding rows either: there
5000
+ // is nothing to bind, and rows with an empty resourceKey would leak into
5001
+ // the merged file through merge's bindings union.
5002
+ let unbound = false;
5003
+ try {
5004
+ requireBindingPair(`connector "${step.name}"`, spec, { key: spec.key, action: spec.action });
5005
+ }
5006
+ catch (error) {
5007
+ if (!opts.draft || !(error instanceof BindingRefusal))
5008
+ throw error;
5009
+ opts.draft({ nodeId: id, code: error.code, message: error.message });
5010
+ unbound = true;
5011
+ }
4969
5012
  const conn = resolveBinding(opts, spec.connection, 'connection', `connector "${step.name}"`, { key: spec.key, action: spec.action });
4970
5013
  const folder = resolveBinding(opts, spec.folder, 'folder', `connector "${step.name}"`, { key: spec.key, action: spec.action });
4971
5014
  // The platform reaches the connection through the flow's `bindings[]`,
4972
5015
  // never through `inputs.detail` — without this pair the run faults with
4973
5016
  // `[102010]` (board G-19; see connectionResourceBindings).
4974
5017
  const { entries: connBindings } = connectionResourceBindings({ def: rc.v1def, connectorKey: rc.connectorKey, connectionId: conn, folderKey: folder }, resourceBindings);
4975
- for (const b of connBindings)
4976
- resourceBindings.set(b.id, b);
5018
+ if (!unbound)
5019
+ for (const b of connBindings)
5020
+ resourceBindings.set(b.id, b);
4977
5021
  const connectorInputs = buildConnectorInputs(rc, spec.inputs, {
5022
+ ...(opts.draft
5023
+ ? { onMissingRequired: (m) => opts.draft({ nodeId: id, code: CONNECTOR_INPUT_MISSING, message: `connector ${rc.nodeType}: ${m}` }) }
5024
+ : {}),
4978
5025
  dialect: FLOW_EXPR_DIALECT,
4979
5026
  render: (value) => renderDeep(value, scope.rename),
4980
5027
  resolutions: opts.library.lookupResolutions ?? {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.27.0",
3
+ "version": "6.28.0",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "69352e3f5c7c379e0e70b3c7b6fb2fba91255f84"
95
+ "gitref": "2129535cf7f13d90345db00a5d72d096c69fce04"
96
96
  }