@uipath/maestro-builder-sdk 6.16.14 → 6.17.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.
Files changed (60) hide show
  1. package/dist/api-index.md +108 -107
  2. package/dist/api-members.md +295 -291
  3. package/dist/bindings-v2.d.ts +39 -1
  4. package/dist/bindings-v2.js +289 -9
  5. package/dist/bpmn/compile-cli.js +11 -2
  6. package/dist/bpmn/decompile.js +2 -1
  7. package/dist/bpmn/registry-fallback.js +9 -7
  8. package/dist/case/compile-cli.js +16 -6
  9. package/dist/case/decompile-cli.js +2 -1
  10. package/dist/case/decompile.js +3 -2
  11. package/dist/case/merge-cli.js +3 -2
  12. package/dist/check.js +219 -37
  13. package/dist/cli-result.d.ts +9 -0
  14. package/dist/cli-result.js +12 -0
  15. package/dist/compile-cli.js +78 -53
  16. package/dist/core/actions.d.ts +70 -16
  17. package/dist/core/agentic-process-forms.d.ts +51 -0
  18. package/dist/core/agentic-process-forms.js +49 -0
  19. package/dist/core/binding-messages.d.ts +5 -0
  20. package/dist/core/comment-text.d.ts +29 -0
  21. package/dist/core/comment-text.js +40 -0
  22. package/dist/core/contribution-bindings.d.ts +73 -0
  23. package/dist/core/contribution-bindings.js +105 -0
  24. package/dist/core/expr-check.js +7 -2
  25. package/dist/core/json-file.d.ts +1 -0
  26. package/dist/core/json-file.js +12 -0
  27. package/dist/core/script-return.js +4 -1
  28. package/dist/core/typed-raw-node.js +1 -0
  29. package/dist/core/typescript-module.d.ts +2 -0
  30. package/dist/core/typescript-module.js +18 -0
  31. package/dist/core-definitions.json +210 -0
  32. package/dist/decompile-cli.js +2 -1
  33. package/dist/decompile.js +64 -24
  34. package/dist/emit.js +4 -3
  35. package/dist/flow-sdk.d.ts +14 -9
  36. package/dist/flow-sdk.js +14 -4
  37. package/dist/flow-step-tree.d.ts +2 -3
  38. package/dist/flow-step-tree.js +2 -3
  39. package/dist/load.js +1 -1
  40. package/dist/merge-cli.js +3 -2
  41. package/dist/merge.d.ts +2 -1
  42. package/dist/merge.js +0 -0
  43. package/dist/path-within.d.ts +33 -0
  44. package/dist/path-within.js +41 -0
  45. package/dist/registry/archive/extract.js +2 -5
  46. package/dist/registry/curated-overlay.js +3 -2
  47. package/dist/registry/generators/descriptors.js +20 -18
  48. package/dist/registry/generators/triggers.js +4 -4
  49. package/dist/registry/prepare/messages.js +2 -1
  50. package/dist/registry/prepare/source-labels.js +60 -1
  51. package/dist/registry/prepare-cli.js +14 -4
  52. package/dist/registry/pull-cli.js +3 -2
  53. package/dist/registry/uip/client.d.ts +28 -3
  54. package/dist/registry/uip/client.js +95 -34
  55. package/dist/registry/uip/host.d.ts +41 -0
  56. package/dist/registry/uip/host.js +312 -0
  57. package/dist/serialize.d.ts +7 -0
  58. package/dist/serialize.js +390 -270
  59. package/dist/workdir.js +1 -1
  60. package/package.json +3 -3
@@ -2,7 +2,7 @@
2
2
  export interface BindingsV2Resource {
3
3
  resource: string;
4
4
  key: string;
5
- id: string;
5
+ id?: string;
6
6
  value: Record<string, unknown>;
7
7
  metadata: Record<string, unknown>;
8
8
  [k: string]: unknown;
@@ -61,3 +61,41 @@ export declare function connectionResources(flow: unknown): BindingsV2Resource[]
61
61
  export declare function mergeBindingsV2(existing: unknown, connections: BindingsV2Resource[]): BindingsV2Document;
62
62
  /** Read a `bindings_v2.json`, or `undefined` when absent or unparseable. */
63
63
  export declare function readBindingsV2(path: string): BindingsV2Document | undefined;
64
+ /** The files of one inline agent, as `uip agent refresh` reads them. */
65
+ export interface InlineAgentFiles {
66
+ /** `<agent>/agent.json`. */
67
+ agent: unknown;
68
+ /** Every `<agent>/resources/<id>/resource.json` that has a `$resourceType`. */
69
+ resources: unknown[];
70
+ /** Every `<agent>/features/<id>/feature.json` that has a `$featureType`. */
71
+ features: unknown[];
72
+ }
73
+ /**
74
+ * The `bindings_v2.json` rows one inline agent declares — what `uip agent
75
+ * refresh --inline-in-flow` generates for it, and what `uip agent validate
76
+ * --inline-in-flow` requires to be on disk (`AgentValidationDrift`,
77
+ * flow-builder-sdk#962).
78
+ *
79
+ * A transcription of the agent tool's own generator, `generateBindings` in
80
+ * `@uipath/tool-agent/agent-bindings` (Studio.Common `packager/src/bindings/`;
81
+ * read from 2.0.0, the version uipath-cli's agent-tool locks, and identical in the
82
+ * bundle of uip 1.205.0-dev.9071). It is a COPY, so
83
+ * `tests/agent-bindings-contract.test.ts` runs the real generator over the
84
+ * resources compile writes and fails on any row that differs.
85
+ *
86
+ * Inputs are read the way refresh reads them: every resource file, then any
87
+ * resource in `agent.json` whose name no file already has; the same for features.
88
+ * Rows are deduplicated on `resource` + `key`, first one wins.
89
+ */
90
+ export declare function inlineAgentResources(files: InlineAgentFiles): BindingsV2Resource[];
91
+ /**
92
+ * Fold one inline agent's rows into the project's rows, the way `uip agent
93
+ * refresh --inline-in-flow` does (`mergeInlineFlowBindings`): drop the rows that
94
+ * exactly match a fresh one (this agent's own earlier output), then keep the
95
+ * FIRST row per identity with the existing rows first — so a row the flow
96
+ * already declares wins over the agent's copy of it — and spell every
97
+ * connection row's `resource` as `connection`.
98
+ */
99
+ export declare function mergeInlineAgentResources(existing: BindingsV2Resource[], fresh: BindingsV2Resource[]): BindingsV2Resource[];
100
+ /** Read one inline agent's directory the way refresh does; `undefined` when it has no readable `agent.json`. */
101
+ export declare function readInlineAgentFiles(agentDir: string): InlineAgentFiles | undefined;
@@ -32,16 +32,29 @@
32
32
  *
33
33
  * ## What this derives, and what it deliberately leaves alone
34
34
  *
35
- * Only `connection` resources, which is what refresh imports and what #689 is
36
- * about. Pack derives more from the BPMN projection — a `core.trigger.scheduled`
37
- * flow also yields a `TimeTrigger` resource whose `CronExpression` comes from
38
- * translating the trigger's ISO-8601 `timerPreset`. Re-deriving those here would
39
- * be a second implementation of someone else's generator, free to drift, for no
40
- * gain in the package. So non-connection resources already in the file are
41
- * PRESERVED untouched and never invented: {@link mergeBindingsV2} replaces only
42
- * the connection rows.
35
+ * Two kinds of row, each copied from the generator that owns it:
36
+ *
37
+ * - `connection` rows for the flow's own connector steps — what pack derives,
38
+ * and what #689 is about ({@link connectionResources}).
39
+ * - every inline agent's rows (`process`, `index`, `app`, `connection`, …) —
40
+ * what `uip agent refresh --inline-in-flow` derives from the agent's
41
+ * directory with the agent tool's `generateBindings`
42
+ * ({@link inlineAgentResources}). `uip agent validate --inline-in-flow`
43
+ * regenerates them and fails with `AgentValidationDrift` when one is
44
+ * missing, and the SDK route never runs refresh, so compile writes them
45
+ * (flow-builder-sdk#962). `tests/agent-bindings-contract.test.ts` runs the
46
+ * real generator against this copy.
47
+ *
48
+ * Pack derives more from the BPMN projection — a `core.trigger.scheduled` flow
49
+ * also yields a `TimeTrigger` resource whose `CronExpression` comes from
50
+ * translating the trigger's ISO-8601 `timerPreset`. Nothing in the SDK route
51
+ * checks those against the project file, and pack regenerates them in the
52
+ * package, so they are not copied here: other rows already in the file are
53
+ * PRESERVED untouched and never invented ({@link mergeBindingsV2},
54
+ * {@link mergeInlineAgentResources}).
43
55
  */
44
- import { existsSync, readFileSync } from 'node:fs';
56
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
57
+ import { join } from 'node:path';
45
58
  import { BindingRefusal, connectionConnectorConflictMessage } from './core/binding-messages.js';
46
59
  /** The document version pack emits. */
47
60
  const DOCUMENT_VERSION = '2.0';
@@ -257,3 +270,270 @@ export function readBindingsV2(path) {
257
270
  return undefined;
258
271
  }
259
272
  }
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+ // Inline-agent rows — the ones `uip agent refresh --inline-in-flow` derives
275
+ // ─────────────────────────────────────────────────────────────────────────────
276
+ /** The folder sentinel the agent tool treats the same as an empty folder. */
277
+ const SOLUTION_FOLDER = 'solution_folder';
278
+ /** Escalation recipient types that name an asset (`assetName` + `folderPath`). */
279
+ const ASSET_RECIPIENT_TYPES = new Set([4, 6]);
280
+ /** An escalation of this `escalationType` stores to a bucket, not an Action Center app. */
281
+ const VS_ESCALATION_TYPE = 1;
282
+ /** Tool `type` → the `subType` of the `process` row it declares. */
283
+ const PROCESS_TOOL_SUBTYPES = {
284
+ process: 'process',
285
+ agent: 'agent',
286
+ api: 'api',
287
+ processOrchestration: 'processOrchestration',
288
+ flow: 'flow',
289
+ function: 'function',
290
+ };
291
+ function bindingValue(defaultValue, displayName) {
292
+ return { defaultValue, isExpression: false, displayName };
293
+ }
294
+ function agentRowMetadata() {
295
+ return { bindingsVersion: BINDINGS_VERSION, solutionsSupport: 'true' };
296
+ }
297
+ /** Adds `folderPath` unless the folder is empty or `solution_folder` — both mean "the solution's own folder". */
298
+ function withFolderPath(value, raw) {
299
+ if (raw && raw !== SOLUTION_FOLDER)
300
+ value.folderPath = bindingValue(raw, 'Folder path');
301
+ return value;
302
+ }
303
+ /** A row whose value is one `name` (plus the folder) and whose metadata is the default pair. */
304
+ function namedRow(resource, name, displayName, folder) {
305
+ return {
306
+ resource,
307
+ key: name,
308
+ value: withFolderPath({ name: bindingValue(name, displayName) }, folder),
309
+ metadata: agentRowMetadata(),
310
+ };
311
+ }
312
+ function processRow(name, subType, folder) {
313
+ return {
314
+ resource: 'process',
315
+ key: name,
316
+ value: withFolderPath({ name: bindingValue(name, 'Process name') }, folder),
317
+ metadata: { subType, ...agentRowMetadata() },
318
+ };
319
+ }
320
+ function agentConnectionRow(id, connector, displayName) {
321
+ return {
322
+ resource: 'connection',
323
+ key: id,
324
+ value: { connectionId: bindingValue(id, displayName) },
325
+ metadata: { connector, useConnectionService: 'true', ...agentRowMetadata() },
326
+ };
327
+ }
328
+ function toolRows(tool) {
329
+ const props = isRecord(tool.properties) ? tool.properties : {};
330
+ const rows = [];
331
+ const subType = typeof tool.type === 'string' && Object.hasOwn(PROCESS_TOOL_SUBTYPES, tool.type)
332
+ ? PROCESS_TOOL_SUBTYPES[tool.type]
333
+ : undefined;
334
+ if (subType !== undefined) {
335
+ rows.push(processRow(props.processName, subType, props.folderPath));
336
+ }
337
+ else if (tool.type === 'integration') {
338
+ rows.push(agentConnectionRow(props.connection?.id, props.connection?.connector?.key, 'Connection ID'));
339
+ }
340
+ else if (tool.type === 'ixp') {
341
+ rows.push({
342
+ resource: 'ixp',
343
+ key: props.projectName,
344
+ value: {
345
+ name: bindingValue(props.projectName, 'IXP project name'),
346
+ version: bindingValue(props.versionTag, 'IXP project version'),
347
+ },
348
+ // The generator leaves `solutionsSupport` off an IXP row on purpose.
349
+ metadata: { bindingsVersion: BINDINGS_VERSION },
350
+ });
351
+ }
352
+ else if (tool.type === 'internal' && props.toolType === 'deep-rag') {
353
+ // A DeepRAG built-in grounds on an index; every other `internal` tool, and
354
+ // every unknown type, declares nothing.
355
+ const indexName = isRecord(props.settings) ? props.settings.indexName : undefined;
356
+ if (typeof indexName === 'string' && indexName.length > 0)
357
+ rows.push(namedRow('index', indexName, 'Index Name'));
358
+ }
359
+ for (const policy of Array.isArray(tool.guardrail?.policies) ? tool.guardrail.policies : []) {
360
+ if (policy?.action?.$actionType === 'escalate' && policy.action.app?.name) {
361
+ rows.push(namedRow('app', policy.action.app.name, 'App name', policy.action.app.folderName));
362
+ }
363
+ }
364
+ return rows;
365
+ }
366
+ function assetRecipientRows(recipients) {
367
+ const rows = [];
368
+ for (const r of Array.isArray(recipients) ? recipients : []) {
369
+ if (isRecord(r) && ASSET_RECIPIENT_TYPES.has(r.type) && r.assetName) {
370
+ rows.push(namedRow('asset', r.assetName, 'Asset Name', r.folderPath));
371
+ }
372
+ }
373
+ return rows;
374
+ }
375
+ function resourceRows(resource) {
376
+ switch (resource.$resourceType) {
377
+ case 'tool':
378
+ return toolRows(resource);
379
+ case 'context':
380
+ if (resource.contextType === 'datafabricentityset') {
381
+ return (Array.isArray(resource.entitySet) ? resource.entitySet : []).map((entity) => ({
382
+ resource: 'entity',
383
+ key: entity.referenceKey ?? entity.id,
384
+ value: withFolderPath({ name: bindingValue(entity.name, 'Entity name') }, entity.folderId),
385
+ metadata: { ...agentRowMetadata(), scope: 'folder' },
386
+ }));
387
+ }
388
+ return [namedRow('index', resource.indexName, 'Index Name', resource.folderPath)];
389
+ case 'escalation': {
390
+ if (resource.escalationType === VS_ESCALATION_TYPE) {
391
+ const bucket = resource.vsEscalationProperties?.storageBucketName;
392
+ return bucket
393
+ ? [namedRow('bucket', bucket, 'Storage Bucket Name', resource.vsEscalationProperties?.storageBucketFolderPath)]
394
+ : [];
395
+ }
396
+ const rows = [];
397
+ for (const channel of Array.isArray(resource.channels) ? resource.channels : []) {
398
+ if (channel?.type === 'actionCenter' && channel.properties?.appName) {
399
+ rows.push(namedRow('app', channel.properties.appName, 'App name', channel.properties.folderName));
400
+ }
401
+ rows.push(...assetRecipientRows(channel?.recipients));
402
+ }
403
+ return rows;
404
+ }
405
+ case 'mcp':
406
+ return [namedRow('mcpServer', resource.slug, 'Slug', resource.folderPath)];
407
+ case 'a2a':
408
+ return [namedRow('remoteA2aAgent', resource.slug, 'Slug', resource.folderPath)];
409
+ default:
410
+ return [];
411
+ }
412
+ }
413
+ function agentDefinitionRows(agent) {
414
+ if (agent.type !== 'lowCode')
415
+ return [];
416
+ const rows = [];
417
+ const byom = agent.settings?.byomProperties;
418
+ if (byom)
419
+ rows.push(agentConnectionRow(byom.connectionId, byom.connectorKey, 'Model connection ID'));
420
+ for (const guardrail of Array.isArray(agent.guardrails) ? agent.guardrails : []) {
421
+ if (guardrail?.action?.$actionType !== 'escalate')
422
+ continue;
423
+ if (guardrail.action.app?.name) {
424
+ rows.push(namedRow('app', guardrail.action.app.name, 'App name', guardrail.action.app.folderName));
425
+ }
426
+ rows.push(...assetRecipientRows([guardrail.action.recipient]));
427
+ }
428
+ return rows;
429
+ }
430
+ /**
431
+ * The `bindings_v2.json` rows one inline agent declares — what `uip agent
432
+ * refresh --inline-in-flow` generates for it, and what `uip agent validate
433
+ * --inline-in-flow` requires to be on disk (`AgentValidationDrift`,
434
+ * flow-builder-sdk#962).
435
+ *
436
+ * A transcription of the agent tool's own generator, `generateBindings` in
437
+ * `@uipath/tool-agent/agent-bindings` (Studio.Common `packager/src/bindings/`;
438
+ * read from 2.0.0, the version uipath-cli's agent-tool locks, and identical in the
439
+ * bundle of uip 1.205.0-dev.9071). It is a COPY, so
440
+ * `tests/agent-bindings-contract.test.ts` runs the real generator over the
441
+ * resources compile writes and fails on any row that differs.
442
+ *
443
+ * Inputs are read the way refresh reads them: every resource file, then any
444
+ * resource in `agent.json` whose name no file already has; the same for features.
445
+ * Rows are deduplicated on `resource` + `key`, first one wins.
446
+ */
447
+ export function inlineAgentResources(files) {
448
+ const agent = isRecord(files.agent) ? files.agent : {};
449
+ const fileResources = files.resources.filter(isRecord);
450
+ const fileNames = new Set(fileResources.map((r) => r.name));
451
+ const resources = [
452
+ ...fileResources,
453
+ ...(Array.isArray(agent.resources) ? agent.resources.filter(isRecord) : []).filter((r) => !fileNames.has(r.name)),
454
+ ];
455
+ const fileFeatures = files.features.filter(isRecord);
456
+ const featureNames = new Set(fileFeatures.map((f) => f.name));
457
+ const features = [
458
+ ...fileFeatures,
459
+ ...(Array.isArray(agent.features) ? agent.features.filter(isRecord) : []).filter((f) => !featureNames.has(f.name)),
460
+ ];
461
+ const rows = [];
462
+ for (const resource of resources)
463
+ rows.push(...resourceRows(resource));
464
+ for (const feature of features) {
465
+ if (feature.$featureType === 'memorySpace') {
466
+ rows.push(namedRow('memorySpace', feature.memorySpaceName, 'Memory name', feature.folderPath));
467
+ }
468
+ }
469
+ rows.push(...agentDefinitionRows(agent));
470
+ const seen = new Set();
471
+ return rows.filter((row) => {
472
+ const id = `${row.resource}_${row.key}`;
473
+ if (seen.has(id))
474
+ return false;
475
+ seen.add(id);
476
+ return true;
477
+ });
478
+ }
479
+ /** Refresh's identity for "this agent's own earlier row": exact casing, plus the folder. */
480
+ function agentRowDedupeKey(row) {
481
+ const value = isRecord(row.value) ? row.value : {};
482
+ const folderPath = isRecord(value.folderPath) ? value.folderPath.defaultValue ?? '' : '';
483
+ return `${row.resource}:${row.key}:${folderPath}`;
484
+ }
485
+ /** flow-schema's `mergeBindingResources` identity: resource case-insensitive, a connection's key too. */
486
+ function mergeIdentity(row) {
487
+ const resource = String(row.resource ?? '').toLowerCase();
488
+ const key = String(row.key ?? row.id ?? '');
489
+ return `${resource}\u0000${resource === 'connection' ? key.toLowerCase() : key}`;
490
+ }
491
+ /**
492
+ * Fold one inline agent's rows into the project's rows, the way `uip agent
493
+ * refresh --inline-in-flow` does (`mergeInlineFlowBindings`): drop the rows that
494
+ * exactly match a fresh one (this agent's own earlier output), then keep the
495
+ * FIRST row per identity with the existing rows first — so a row the flow
496
+ * already declares wins over the agent's copy of it — and spell every
497
+ * connection row's `resource` as `connection`.
498
+ */
499
+ export function mergeInlineAgentResources(existing, fresh) {
500
+ const freshKeys = new Set(fresh.map(agentRowDedupeKey));
501
+ const seen = new Set();
502
+ const out = [];
503
+ for (const row of [...existing.filter((r) => !freshKeys.has(agentRowDedupeKey(r))), ...fresh]) {
504
+ const id = mergeIdentity(row);
505
+ if (seen.has(id))
506
+ continue;
507
+ seen.add(id);
508
+ out.push(isConnection(row.resource) && row.resource !== 'connection' ? { ...row, resource: 'connection' } : row);
509
+ }
510
+ return out;
511
+ }
512
+ /** Read one inline agent's directory the way refresh does; `undefined` when it has no readable `agent.json`. */
513
+ export function readInlineAgentFiles(agentDir) {
514
+ let agent;
515
+ try {
516
+ agent = JSON.parse(readFileSync(join(agentDir, 'agent.json'), 'utf8'));
517
+ }
518
+ catch {
519
+ return undefined;
520
+ }
521
+ const read = (sub, file, tag) => {
522
+ const dir = join(agentDir, sub);
523
+ if (!existsSync(dir))
524
+ return [];
525
+ const out = [];
526
+ for (const entry of readdirSync(dir).sort()) {
527
+ try {
528
+ const body = JSON.parse(readFileSync(join(dir, entry, file), 'utf8'));
529
+ if (isRecord(body) && body[tag])
530
+ out.push(body);
531
+ }
532
+ catch {
533
+ // Refresh skips a malformed or missing file the same way.
534
+ }
535
+ }
536
+ return out;
537
+ };
538
+ return { agent, resources: read('resources', 'resource.json', '$resourceType'), features: read('features', 'feature.json', '$featureType') };
539
+ }
@@ -15,7 +15,7 @@
15
15
  * XML (semantic-only — no diagram). The authoritative deploy gate is
16
16
  * `uip maestro bpmn validate`; a separate "tidy" step lays the diagram out.
17
17
  */
18
- import { reportResult } from '../cli-result.js';
18
+ import { reportDiagnostics, reportResult, thrownDiagnostic } from '../cli-result.js';
19
19
  import { writeFileSync } from 'node:fs';
20
20
  import { basename, resolve } from 'node:path';
21
21
  import { resolveBpmnFile, loadBuiltBpmn } from './load.js';
@@ -54,11 +54,20 @@ export async function run(argv) {
54
54
  const diags = check(built, connectorCheckOpts(library, bindFile ? bindings : undefined));
55
55
  for (const d of diags)
56
56
  console.error(` ${d.level === 'error' ? '✗' : '⚠'} [${d.code}] ${d.message}`);
57
+ // Reported before serialize, so every exit path carries what stderr printed.
58
+ reportDiagnostics(diags);
57
59
  if (diags.some((d) => d.level === 'error')) {
58
60
  console.error(`compile: ${diags.filter((d) => d.level === 'error').length} error(s); not emitting.`);
59
61
  return 1;
60
62
  }
61
- const xml = await serialize(built, { library, bindings });
63
+ let xml;
64
+ try {
65
+ xml = await serialize(built, { library, bindings });
66
+ }
67
+ catch (error) {
68
+ reportDiagnostics([...diags, thrownDiagnostic(error)]);
69
+ throw error;
70
+ }
62
71
  const base = basename(file).replace(/\.bpmn\.ts$|\.ts$/, '');
63
72
  const out = flagValue(argv, '-o') ?? `${base}.bpmn`;
64
73
  writeFileSync(out, xml);
@@ -59,6 +59,7 @@ import { errorSchema, errorSchemaWithResponse } from './bpmn-sdk.js';
59
59
  import { SCRIPT_ARGS_ENVELOPE, defaultScriptInputSchema } from './serialize.js';
60
60
  import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
61
61
  import { preserveSchemaVersion } from '../schema-version.js';
62
+ import { commentText } from '../core/comment-text.js';
62
63
  const require = createRequire(import.meta.url);
63
64
  const uipathDescriptor = require('./uipath-moddle.v1.json');
64
65
  /**
@@ -1828,7 +1829,7 @@ function variablesHost(defs, process) {
1828
1829
  `// collaboration, and the SDK models a bare bpmn:process — so recompiling this\n` +
1829
1830
  `// file will NOT reproduce the pool or its lanes, and the canvas will look\n` +
1830
1831
  `// different. Dropped:\n` +
1831
- dropped.map((d) => `// - ${d}\n`).join('') +
1832
+ dropped.map((d) => `// - ${commentText(d)}\n`).join('') +
1832
1833
  `// The variables below were declared on the collaboration and are carried over;\n` +
1833
1834
  `// those scoped to it are rehomed onto the process, which is a legal scope.\n\n`;
1834
1835
  return { container: collab, rehome: { from: collab.id, to: process.id }, note };
@@ -32,8 +32,12 @@
32
32
  * `registry pull --force` and one more `get`. Both outcomes are remembered
33
33
  * for the process, so a file with ten unknown nodes costs at most two CLI
34
34
  * calls, not twenty.
35
+ *
36
+ * The call goes through `registry/uip/client.ts` (`runUipSync`), so
37
+ * `UIP_REPLAY_DIR` and `UIP_RECORD_DIR` apply here as they do to every other
38
+ * registry call.
35
39
  */
36
- import { execFileSync } from 'node:child_process';
40
+ import { runUipSync } from '../registry/uip/client.js';
37
41
  /** The `uip` binary. Matches `scripts/materialize-bpmn-registry.mjs`. */
38
42
  const UIP = () => process.env.UIP ?? 'uip';
39
43
  /**
@@ -66,12 +70,10 @@ let warnedLive = false;
66
70
  function uip(args, timeoutMs) {
67
71
  let out;
68
72
  try {
69
- out = execFileSync(UIP(), args, {
70
- encoding: 'utf8',
71
- stdio: ['ignore', 'pipe', 'ignore'],
72
- timeout: timeoutMs,
73
- maxBuffer: 64 * 1024 * 1024,
74
- });
73
+ const result = runUipSync([UIP(), ...args], { timeoutMs });
74
+ if (result.code !== 0)
75
+ return undefined; // a non-zero exit or a timeout means no answer
76
+ out = result.stdout;
75
77
  }
76
78
  catch {
77
79
  return undefined;
@@ -9,8 +9,9 @@
9
9
  * caseplan.json. The authoritative deploy gate is `uip maestro case validate`.
10
10
  * Output defaults to `caseplan.json` (the platform's canonical file name).
11
11
  */
12
- import { reportResult } from '../cli-result.js';
13
- import { writeFileSync, readFileSync, existsSync } from 'node:fs';
12
+ import { reportDiagnostics, reportResult, thrownDiagnostic } from '../cli-result.js';
13
+ import { writeJsonFile } from '../core/json-file.js';
14
+ import { readFileSync, existsSync } from 'node:fs';
14
15
  import { join, dirname, basename, resolve } from 'node:path';
15
16
  import { resolveCaseFile, loadBuiltCase } from './load.js';
16
17
  import { serialize } from './serialize.js';
@@ -40,6 +41,8 @@ export async function run(argv) {
40
41
  const diags = check(built);
41
42
  for (const d of diags)
42
43
  console.error(` ${d.level === 'error' ? '✗' : '⚠'} [${d.code}] ${d.message}`);
44
+ // Reported before serialize, so every exit path carries what stderr printed.
45
+ reportDiagnostics(diags);
43
46
  const errors = diags.filter((d) => d.level === 'error');
44
47
  if (errors.length) {
45
48
  console.error(`compile: ${errors.length} error(s); not emitting.`);
@@ -55,9 +58,16 @@ export async function run(argv) {
55
58
  const library = openCliLibrary(argv);
56
59
  const bindFile = flagValue(argv, '--bindings') ?? defaultBindingsFile();
57
60
  const bindings = new Bindings(bindFile);
58
- const caseplan = serialize(built, { library, bindings });
61
+ let caseplan;
62
+ try {
63
+ caseplan = serialize(built, { library, bindings });
64
+ }
65
+ catch (error) {
66
+ reportDiagnostics([...diags, thrownDiagnostic(error)]);
67
+ throw error;
68
+ }
59
69
  const out = flagValue(argv, '-o') ?? 'caseplan.json';
60
- writeFileSync(out, JSON.stringify(caseplan, null, 2) + '\n');
70
+ writeJsonFile(out, caseplan);
61
71
  const stages = caseplan.nodes.filter((n) => String(n.type).endsWith('Stage')).length;
62
72
  const triggers = syncEntryPoints(caseplan, out);
63
73
  const epNote = triggers >= 0 ? `, synced entry-points.json (${triggers} trigger(s))` : '';
@@ -135,7 +145,7 @@ function syncEntryPoints(caseplan, outPath) {
135
145
  displayName: (n.data && n.data.label) || n.id,
136
146
  };
137
147
  });
138
- writeFileSync(epPath, JSON.stringify(ep, null, 2) + '\n');
148
+ writeJsonFile(epPath, ep);
139
149
  return triggerNodes.length;
140
150
  }
141
151
  /**
@@ -243,7 +253,7 @@ function syncBindingsV2(caseplan, outPath) {
243
253
  });
244
254
  }
245
255
  resources.push(...connectionResources);
246
- writeFileSync(sidecarPath, JSON.stringify({ version: '2.0', resources }, null, 2) + '\n');
256
+ writeJsonFile(sidecarPath, { version: '2.0', resources });
247
257
  return resources.length;
248
258
  }
249
259
  function referencedBindingValue(value, bindings) {
@@ -24,6 +24,7 @@ import { flagValue, runWhenInvokedDirectly } from '../cli-run.js';
24
24
  import { Library } from '../core/library.js';
25
25
  import { decompileWithDiagnostics } from './decompile.js';
26
26
  import { CASE_IMPORT } from '../package-name.js';
27
+ import { commentText } from '../core/comment-text.js';
27
28
  /** Derive a PascalCase base name from the case name for the default output file. */
28
29
  function baseName(plan) {
29
30
  const src = plan.name ?? plan.id ?? 'Case';
@@ -88,7 +89,7 @@ export function run(argv) {
88
89
  function pipelineScript(originalPath, tsPath, pipelinePath, importSpecifier, bestEffort, libraryDir) {
89
90
  const base = pipelinePath.replace(/\.pipeline\.mjs$/, '');
90
91
  return `#!/usr/bin/env node
91
- // Generated by case decompile — edit ${basename(tsPath)}, then run this file.
92
+ // Generated by case decompile — edit ${commentText(basename(tsPath))}, then run this file.
92
93
  import { execFileSync, spawnSync } from 'node:child_process';
93
94
  import { existsSync } from 'node:fs';
94
95
 
@@ -1,3 +1,4 @@
1
+ import { commentText } from '../core/comment-text.js';
1
2
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
2
3
  import { preserveSchemaVersion } from '../schema-version.js';
3
4
  const TASK_KIND_METHOD = {
@@ -1164,7 +1165,7 @@ export function decompileWithDiagnostics(plan, options = {}) {
1164
1165
  if (schema.call)
1165
1166
  out.line(depth, schema.call);
1166
1167
  if (schema.problem)
1167
- out.line(depth, `// TODO: ${schema.problem}`);
1168
+ out.line(depth, `// TODO: ${commentText(schema.problem)}`);
1168
1169
  if (plan.name !== undefined)
1169
1170
  out.line(depth, `.name(${str(plan.name)})`);
1170
1171
  if (plan.description !== undefined)
@@ -1205,7 +1206,7 @@ export function decompileWithDiagnostics(plan, options = {}) {
1205
1206
  const diagnosticHeader = diagnostics.length
1206
1207
  ? `${[
1207
1208
  '// BEST-EFFORT DECOMPILE: this source is intentionally not buildable until every marker below is repaired.',
1208
- ...diagnostics.map((diagnostic) => `// [${diagnostic.code}] ${diagnostic.location}: ${diagnostic.message}`
1209
+ ...diagnostics.map((diagnostic) => `// [${commentText(diagnostic.code)}] ${commentText(diagnostic.location)}: ${commentText(diagnostic.message)}`
1209
1210
  .replace(/[\r\n]+/g, ' ')),
1210
1211
  ].join('\n')}\n\n`
1211
1212
  : '';
@@ -10,7 +10,8 @@
10
10
  * for what each side decides and why the join is by label rather than by id.
11
11
  */
12
12
  import { reportResult } from '../cli-result.js';
13
- import { readFileSync, writeFileSync } from 'node:fs';
13
+ import { writeJsonFile } from '../core/json-file.js';
14
+ import { readFileSync } from 'node:fs';
14
15
  import { basename, resolve } from 'node:path';
15
16
  import { mergeCase } from './merge.js';
16
17
  import { flagValue, runWhenInvokedDirectly } from '../cli-run.js';
@@ -32,7 +33,7 @@ export function run(argv) {
32
33
  warn: (message) => console.error(`case-merge: warning: ${message}`),
33
34
  });
34
35
  const out = flagValue(argv, '-o') ?? 'merged.caseplan.json';
35
- writeFileSync(out, JSON.stringify(merged, null, 2) + '\n');
36
+ writeJsonFile(out, merged);
36
37
  const stages = (merged.nodes ?? [])
37
38
  .filter((node) => String(node.type ?? '').endsWith('Stage')).length;
38
39
  console.log(`case-merge: wrote ${basename(out)} (${stages} stage(s))`);