@bpmnkit/cli 0.0.28 → 0.0.30

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.
@@ -0,0 +1,679 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { Bpmn, compactify, expand } from "@bpmnkit/core";
3
+ // ── JSON schema reference ─────────────────────────────────────────────────────
4
+ const SCHEMA_HELP = `CompactDiagram JSON schema — for --definition and stdin input
5
+ ==============================================================
6
+
7
+ TOP-LEVEL
8
+ { "id": "Definitions_<unique>", "processes": [CompactProcess] }
9
+
10
+ CompactProcess
11
+ { "id": "proc-id", "name"?: "Human Name",
12
+ "elements": [CompactElement], "flows": [CompactFlow] }
13
+
14
+ CompactElement — all fields except id+type are optional
15
+ {
16
+ "id": unique element ID,
17
+ "type": BpmnElementType (see below),
18
+ "name": display label,
19
+
20
+ serviceTask/sendTask:
21
+ "jobType": Zeebe job type string (e.g. "my-worker"),
22
+ "taskHeaders": { key: value } — Zeebe task headers,
23
+ "resultVariable": variable to store the response in,
24
+
25
+ callActivity:
26
+ "calledProcess": process ID of the called process,
27
+
28
+ userTask:
29
+ "formId": Camunda form ID,
30
+
31
+ businessRuleTask:
32
+ "decisionId": DMN decision ID,
33
+ "resultVariable": variable to store decision result,
34
+
35
+ events (startEvent, endEvent, boundaryEvent, intermediateCatchEvent, etc.):
36
+ "eventType": timer | message | signal | error | escalation
37
+ | terminate | cancel | conditional | link | compensate,
38
+
39
+ boundaryEvent:
40
+ "attachedTo": ID of the host task,
41
+ "interrupting": false for non-interrupting (default: true),
42
+
43
+ subProcess/eventSubProcess/transaction:
44
+ "children": { "elements": [...], "flows": [...] }
45
+ }
46
+
47
+ CompactFlow
48
+ {
49
+ "id": unique flow ID,
50
+ "from": source element ID,
51
+ "to": target element ID,
52
+ "name"?: flow label,
53
+ "condition"?: FEEL condition expression (e.g. "= status = \\"approved\\"")
54
+ }
55
+
56
+ ELEMENT TYPES
57
+ Tasks: serviceTask userTask scriptTask businessRuleTask
58
+ callActivity sendTask receiveTask manualTask task
59
+ Events: startEvent endEvent intermediateCatchEvent intermediateThrowEvent
60
+ boundaryEvent
61
+ Gateways: exclusiveGateway parallelGateway inclusiveGateway
62
+ eventBasedGateway complexGateway
63
+ Containers: subProcess adHocSubProcess eventSubProcess transaction
64
+
65
+ HTTP CONNECTOR (serviceTask)
66
+ { "type": "serviceTask", "jobType": "io.camunda:http-json:1",
67
+ "taskHeaders": { "url": "https://...", "method": "POST" },
68
+ "resultVariable": "response" }
69
+
70
+ PATCH FORMAT (for --patch and stdin when --input is set)
71
+ Adds elements and flows to an existing process. IDs must not collide.
72
+ {
73
+ "elements": [CompactElement, ...], // new elements to add
74
+ "flows": [CompactFlow, ...] // new flows to add (can reference existing IDs)
75
+ }
76
+
77
+ Example — add a rejection path to an existing gateway named "gw":
78
+ {
79
+ "elements": [
80
+ { "id": "rejected", "type": "serviceTask", "name": "Notify Rejection", "jobType": "notify" },
81
+ { "id": "end-reject","type": "endEvent", "name": "Rejected" }
82
+ ],
83
+ "flows": [
84
+ { "id": "f-rej1", "from": "gw", "to": "rejected", "condition": "= not approved", "name": "No" },
85
+ { "id": "f-rej2", "from": "rejected", "to": "end-reject" }
86
+ ]
87
+ }
88
+
89
+ FULL EXAMPLE — order approval
90
+ {
91
+ "id": "Definitions_order",
92
+ "processes": [{
93
+ "id": "order-approval",
94
+ "name": "Order Approval",
95
+ "elements": [
96
+ { "id": "start", "type": "startEvent", "name": "Order Received" },
97
+ { "id": "validate", "type": "serviceTask", "name": "Validate", "jobType": "validate-order" },
98
+ { "id": "gw", "type": "exclusiveGateway","name": "Valid?" },
99
+ { "id": "review", "type": "userTask", "name": "Manual Review", "formId": "review-form" },
100
+ { "id": "process", "type": "serviceTask", "name": "Process Order", "jobType": "process-order" },
101
+ { "id": "end-ok", "type": "endEvent", "name": "Processed" },
102
+ { "id": "end-rej", "type": "endEvent", "name": "Rejected" },
103
+ { "id": "err", "type": "boundaryEvent", "attachedTo": "validate", "eventType": "error" },
104
+ { "id": "end-err", "type": "endEvent", "name": "Validation Error", "eventType": "error" }
105
+ ],
106
+ "flows": [
107
+ { "id": "f1", "from": "start", "to": "validate" },
108
+ { "id": "f2", "from": "validate", "to": "gw" },
109
+ { "id": "f3", "from": "gw", "to": "review", "condition": "= not valid", "name": "Invalid" },
110
+ { "id": "f4", "from": "gw", "to": "process", "condition": "= valid", "name": "Valid" },
111
+ { "id": "f5", "from": "review", "to": "end-rej", "name": "Rejected" },
112
+ { "id": "f6", "from": "review", "to": "process", "name": "Approved" },
113
+ { "id": "f7", "from": "process", "to": "end-ok" },
114
+ { "id": "f8", "from": "err", "to": "end-err" }
115
+ ]
116
+ }]
117
+ }`;
118
+ const TEMPLATES = {
119
+ empty: (id, name) => ({
120
+ id: `Definitions_${id}`,
121
+ processes: [
122
+ {
123
+ id,
124
+ name: name ?? "New Process",
125
+ elements: [{ id: "start", type: "startEvent", name: "Start" }],
126
+ flows: [],
127
+ },
128
+ ],
129
+ }),
130
+ minimal: (id, name) => ({
131
+ id: `Definitions_${id}`,
132
+ processes: [
133
+ {
134
+ id,
135
+ name: name ?? "New Process",
136
+ elements: [
137
+ { id: "start", type: "startEvent", name: "Start" },
138
+ {
139
+ id: "task1",
140
+ type: "serviceTask",
141
+ name: name ? `${name} Task` : "Process Request",
142
+ jobType: "my-worker",
143
+ },
144
+ { id: "end", type: "endEvent", name: "End" },
145
+ ],
146
+ flows: [
147
+ { id: "f1", from: "start", to: "task1" },
148
+ { id: "f2", from: "task1", to: "end" },
149
+ ],
150
+ },
151
+ ],
152
+ }),
153
+ "user-task": (id, name) => ({
154
+ id: `Definitions_${id}`,
155
+ processes: [
156
+ {
157
+ id,
158
+ name: name ?? "Human Task Process",
159
+ elements: [
160
+ { id: "start", type: "startEvent", name: "Start" },
161
+ { id: "ut1", type: "userTask", name: name ?? "Review", formId: "my-form" },
162
+ { id: "end", type: "endEvent", name: "Done" },
163
+ ],
164
+ flows: [
165
+ { id: "f1", from: "start", to: "ut1" },
166
+ { id: "f2", from: "ut1", to: "end" },
167
+ ],
168
+ },
169
+ ],
170
+ }),
171
+ "call-activity": (id, name) => ({
172
+ id: `Definitions_${id}`,
173
+ processes: [
174
+ {
175
+ id,
176
+ name: name ?? "Orchestration Process",
177
+ elements: [
178
+ { id: "start", type: "startEvent", name: "Start" },
179
+ {
180
+ id: "ca1",
181
+ type: "callActivity",
182
+ name: name ?? "Run Sub-Process",
183
+ calledProcess: "child-process",
184
+ },
185
+ { id: "end", type: "endEvent", name: "Done" },
186
+ ],
187
+ flows: [
188
+ { id: "f1", from: "start", to: "ca1" },
189
+ { id: "f2", from: "ca1", to: "end" },
190
+ ],
191
+ },
192
+ ],
193
+ }),
194
+ "business-rule": (id, name) => ({
195
+ id: `Definitions_${id}`,
196
+ processes: [
197
+ {
198
+ id,
199
+ name: name ?? "Decision Process",
200
+ elements: [
201
+ { id: "start", type: "startEvent", name: "Start" },
202
+ {
203
+ id: "brt1",
204
+ type: "businessRuleTask",
205
+ name: name ?? "Evaluate Decision",
206
+ decisionId: "my-decision",
207
+ resultVariable: "decisionResult",
208
+ },
209
+ { id: "end", type: "endEvent", name: "Done" },
210
+ ],
211
+ flows: [
212
+ { id: "f1", from: "start", to: "brt1" },
213
+ { id: "f2", from: "brt1", to: "end" },
214
+ ],
215
+ },
216
+ ],
217
+ }),
218
+ approval: (id, name) => ({
219
+ id: `Definitions_${id}`,
220
+ processes: [
221
+ {
222
+ id,
223
+ name: name ?? "Approval Process",
224
+ elements: [
225
+ { id: "start", type: "startEvent", name: "Request Received" },
226
+ { id: "review", type: "userTask", name: "Review Request", formId: "review-form" },
227
+ { id: "gw", type: "exclusiveGateway", name: "Approved?" },
228
+ {
229
+ id: "process",
230
+ type: "serviceTask",
231
+ name: "Process Approval",
232
+ jobType: "process-approval",
233
+ },
234
+ { id: "end-ok", type: "endEvent", name: "Approved" },
235
+ { id: "end-rej", type: "endEvent", name: "Rejected" },
236
+ ],
237
+ flows: [
238
+ { id: "f1", from: "start", to: "review" },
239
+ { id: "f2", from: "review", to: "gw" },
240
+ { id: "f3", from: "gw", to: "process", condition: "= approved", name: "Yes" },
241
+ { id: "f4", from: "gw", to: "end-rej", condition: "= not approved", name: "No" },
242
+ { id: "f5", from: "process", to: "end-ok" },
243
+ ],
244
+ },
245
+ ],
246
+ }),
247
+ parallel: (id, name) => ({
248
+ id: `Definitions_${id}`,
249
+ processes: [
250
+ {
251
+ id,
252
+ name: name ?? "Parallel Process",
253
+ elements: [
254
+ { id: "start", type: "startEvent", name: "Start" },
255
+ { id: "fork", type: "parallelGateway", name: "Fork" },
256
+ { id: "task1", type: "serviceTask", name: "Task A", jobType: "task-a" },
257
+ { id: "task2", type: "serviceTask", name: "Task B", jobType: "task-b" },
258
+ { id: "join", type: "parallelGateway", name: "Join" },
259
+ { id: "end", type: "endEvent", name: "End" },
260
+ ],
261
+ flows: [
262
+ { id: "f1", from: "start", to: "fork" },
263
+ { id: "f2", from: "fork", to: "task1" },
264
+ { id: "f3", from: "fork", to: "task2" },
265
+ { id: "f4", from: "task1", to: "join" },
266
+ { id: "f5", from: "task2", to: "join" },
267
+ { id: "f6", from: "join", to: "end" },
268
+ ],
269
+ },
270
+ ],
271
+ }),
272
+ inclusive: (id, name) => ({
273
+ id: `Definitions_${id}`,
274
+ processes: [
275
+ {
276
+ id,
277
+ name: name ?? "Inclusive Gateway Process",
278
+ elements: [
279
+ { id: "start", type: "startEvent", name: "Start" },
280
+ { id: "split", type: "inclusiveGateway", name: "Which?" },
281
+ { id: "task1", type: "serviceTask", name: "Option A", jobType: "option-a" },
282
+ { id: "task2", type: "serviceTask", name: "Option B", jobType: "option-b" },
283
+ { id: "merge", type: "inclusiveGateway", name: "Merge" },
284
+ { id: "end", type: "endEvent", name: "End" },
285
+ ],
286
+ flows: [
287
+ { id: "f1", from: "start", to: "split" },
288
+ { id: "f2", from: "split", to: "task1", condition: "= needsA", name: "A" },
289
+ { id: "f3", from: "split", to: "task2", condition: "= needsB", name: "B" },
290
+ { id: "f4", from: "task1", to: "merge" },
291
+ { id: "f5", from: "task2", to: "merge" },
292
+ { id: "f6", from: "merge", to: "end" },
293
+ ],
294
+ },
295
+ ],
296
+ }),
297
+ "timer-start": (id, name) => ({
298
+ id: `Definitions_${id}`,
299
+ processes: [
300
+ {
301
+ id,
302
+ name: name ?? "Scheduled Process",
303
+ elements: [
304
+ { id: "start", type: "startEvent", name: "Timer", eventType: "timer" },
305
+ {
306
+ id: "task1",
307
+ type: "serviceTask",
308
+ name: name ?? "Scheduled Job",
309
+ jobType: "scheduled-worker",
310
+ },
311
+ { id: "end", type: "endEvent", name: "Done" },
312
+ ],
313
+ flows: [
314
+ { id: "f1", from: "start", to: "task1" },
315
+ { id: "f2", from: "task1", to: "end" },
316
+ ],
317
+ },
318
+ ],
319
+ }),
320
+ "message-start": (id, name) => ({
321
+ id: `Definitions_${id}`,
322
+ processes: [
323
+ {
324
+ id,
325
+ name: name ?? "Message-Triggered Process",
326
+ elements: [
327
+ { id: "start", type: "startEvent", name: "Message Received", eventType: "message" },
328
+ {
329
+ id: "task1",
330
+ type: "serviceTask",
331
+ name: name ?? "Handle Message",
332
+ jobType: "message-handler",
333
+ },
334
+ { id: "end", type: "endEvent", name: "Done" },
335
+ ],
336
+ flows: [
337
+ { id: "f1", from: "start", to: "task1" },
338
+ { id: "f2", from: "task1", to: "end" },
339
+ ],
340
+ },
341
+ ],
342
+ }),
343
+ "error-boundary": (id, name) => ({
344
+ id: `Definitions_${id}`,
345
+ processes: [
346
+ {
347
+ id,
348
+ name: name ?? "Error Handling Process",
349
+ elements: [
350
+ { id: "start", type: "startEvent", name: "Start" },
351
+ { id: "task1", type: "serviceTask", name: name ?? "Risky Task", jobType: "risky-worker" },
352
+ {
353
+ id: "err",
354
+ type: "boundaryEvent",
355
+ attachedTo: "task1",
356
+ eventType: "error",
357
+ name: "Error",
358
+ },
359
+ { id: "end-ok", type: "endEvent", name: "Success" },
360
+ { id: "end-err", type: "endEvent", name: "Failed", eventType: "error" },
361
+ ],
362
+ flows: [
363
+ { id: "f1", from: "start", to: "task1" },
364
+ { id: "f2", from: "task1", to: "end-ok" },
365
+ { id: "f3", from: "err", to: "end-err" },
366
+ ],
367
+ },
368
+ ],
369
+ }),
370
+ subprocess: (id, name) => ({
371
+ id: `Definitions_${id}`,
372
+ processes: [
373
+ {
374
+ id,
375
+ name: name ?? "Sub-Process Demo",
376
+ elements: [
377
+ { id: "start", type: "startEvent", name: "Start" },
378
+ {
379
+ id: "sp1",
380
+ type: "subProcess",
381
+ name: name ?? "Sub-Process",
382
+ children: {
383
+ elements: [
384
+ { id: "sp_start", type: "startEvent", name: "Begin" },
385
+ { id: "sp_task", type: "serviceTask", name: "Inner Task", jobType: "inner-worker" },
386
+ { id: "sp_end", type: "endEvent", name: "Finish" },
387
+ ],
388
+ flows: [
389
+ { id: "sf1", from: "sp_start", to: "sp_task" },
390
+ { id: "sf2", from: "sp_task", to: "sp_end" },
391
+ ],
392
+ },
393
+ },
394
+ { id: "end", type: "endEvent", name: "End" },
395
+ ],
396
+ flows: [
397
+ { id: "f1", from: "start", to: "sp1" },
398
+ { id: "f2", from: "sp1", to: "end" },
399
+ ],
400
+ },
401
+ ],
402
+ }),
403
+ "event-subprocess": (id, name) => ({
404
+ id: `Definitions_${id}`,
405
+ processes: [
406
+ {
407
+ id,
408
+ name: name ?? "Process with Error Handler",
409
+ elements: [
410
+ { id: "start", type: "startEvent", name: "Start" },
411
+ { id: "task1", type: "serviceTask", name: name ?? "Main Task", jobType: "main-worker" },
412
+ { id: "end", type: "endEvent", name: "End" },
413
+ {
414
+ id: "evtsp",
415
+ type: "eventSubProcess",
416
+ name: "Error Handler",
417
+ children: {
418
+ elements: [
419
+ {
420
+ id: "evtsp_start",
421
+ type: "startEvent",
422
+ name: "Error",
423
+ eventType: "error",
424
+ interrupting: false,
425
+ },
426
+ {
427
+ id: "evtsp_task",
428
+ type: "serviceTask",
429
+ name: "Compensate",
430
+ jobType: "compensate-worker",
431
+ },
432
+ { id: "evtsp_end", type: "endEvent", name: "Handled" },
433
+ ],
434
+ flows: [
435
+ { id: "sf1", from: "evtsp_start", to: "evtsp_task" },
436
+ { id: "sf2", from: "evtsp_task", to: "evtsp_end" },
437
+ ],
438
+ },
439
+ },
440
+ ],
441
+ flows: [
442
+ { id: "f1", from: "start", to: "task1" },
443
+ { id: "f2", from: "task1", to: "end" },
444
+ ],
445
+ },
446
+ ],
447
+ }),
448
+ };
449
+ const TEMPLATE_NAMES = Object.keys(TEMPLATES).sort();
450
+ // ── Stdin reader ──────────────────────────────────────────────────────────────
451
+ async function readStdin() {
452
+ const chunks = [];
453
+ for await (const chunk of process.stdin) {
454
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
455
+ }
456
+ return Buffer.concat(chunks).toString("utf-8").trim();
457
+ }
458
+ // ── Command ───────────────────────────────────────────────────────────────────
459
+ const generateBpmnCmd = {
460
+ name: "bpmn",
461
+ description: "Generate a BPMN file from parameters, a template, or a CompactDiagram JSON definition",
462
+ args: [],
463
+ flags: [
464
+ {
465
+ name: "process-id",
466
+ short: "i",
467
+ description: "Process ID (used in templates)",
468
+ type: "string",
469
+ },
470
+ {
471
+ name: "name",
472
+ short: "n",
473
+ description: "Process display name (used in templates)",
474
+ type: "string",
475
+ },
476
+ {
477
+ name: "output",
478
+ short: "o",
479
+ description: "Output file path. Default: <process-id>.bpmn. Use - for stdout.",
480
+ type: "string",
481
+ },
482
+ {
483
+ name: "template",
484
+ description: `Template name. Options: ${TEMPLATE_NAMES.join(", ")}`,
485
+ type: "string",
486
+ default: "minimal",
487
+ enum: TEMPLATE_NAMES,
488
+ },
489
+ {
490
+ name: "definition",
491
+ short: "d",
492
+ description: "CompactDiagram JSON string (overrides --template). See --help-schema.",
493
+ type: "string",
494
+ },
495
+ {
496
+ name: "help-schema",
497
+ description: "Print the CompactDiagram JSON schema and exit",
498
+ type: "boolean",
499
+ },
500
+ {
501
+ name: "input",
502
+ short: "f",
503
+ description: "Existing .bpmn file to load and modify",
504
+ type: "string",
505
+ },
506
+ {
507
+ name: "patch",
508
+ description: 'JSON patch to apply to --input: {"elements":[...],"flows":[...]}. See --help-schema.',
509
+ type: "string",
510
+ },
511
+ {
512
+ name: "dump-compact",
513
+ description: "Print the CompactDiagram JSON of --input and exit (for AI inspection of existing files)",
514
+ type: "boolean",
515
+ },
516
+ ],
517
+ examples: [
518
+ {
519
+ description: "Minimal service-task process",
520
+ command: "casen generate bpmn --process-id order --name 'Order Processing'",
521
+ },
522
+ {
523
+ description: "Approval workflow template",
524
+ command: "casen generate bpmn --template approval --process-id approve",
525
+ },
526
+ {
527
+ description: "Timer-triggered scheduled job",
528
+ command: "casen generate bpmn --template timer-start --process-id nightly-sync",
529
+ },
530
+ {
531
+ description: "Parallel branch pattern",
532
+ command: "casen generate bpmn --template parallel --process-id enrich --name Enrichment",
533
+ },
534
+ {
535
+ description: "Error boundary with error end event",
536
+ command: "casen generate bpmn --template error-boundary --process-id resilient",
537
+ },
538
+ {
539
+ description: "Full custom definition (AI/scripting path)",
540
+ command: 'casen generate bpmn --definition \'{"id":"Defs","processes":[{"id":"p","elements":[...],"flows":[...]}]}\'',
541
+ },
542
+ {
543
+ description: "Pipe CompactDiagram JSON from AI output",
544
+ command: "echo '{...}' | casen generate bpmn --output my-process.bpmn",
545
+ },
546
+ {
547
+ description: "Print full JSON schema reference",
548
+ command: "casen generate bpmn --help-schema",
549
+ },
550
+ {
551
+ description: "Inspect an existing file as compact JSON (AI planning step)",
552
+ command: "casen generate bpmn --input existing.bpmn --dump-compact",
553
+ },
554
+ {
555
+ description: "Add a new gateway path to an existing file",
556
+ command: 'casen generate bpmn --input order.bpmn --patch \'{"elements":[{"id":"notify","type":"serviceTask","name":"Notify","jobType":"notify-worker"},{"id":"end2","type":"endEvent","name":"Notified"}],"flows":[{"id":"fn1","from":"gw","to":"notify","condition":"= urgent"},{"id":"fn2","from":"notify","to":"end2"}]}\'',
557
+ },
558
+ {
559
+ description: "Pipe a patch from AI output",
560
+ command: 'echo \'{"elements":[...],"flows":[...]}\' | casen generate bpmn --input order.bpmn',
561
+ },
562
+ {
563
+ description: "Re-apply auto-layout to an existing file",
564
+ command: "casen generate bpmn --input messy.bpmn --output clean.bpmn",
565
+ },
566
+ ],
567
+ async run(ctx) {
568
+ // --help-schema: print schema and exit
569
+ if (ctx.flags["help-schema"]) {
570
+ process.stdout.write(`${SCHEMA_HELP}\n`);
571
+ return;
572
+ }
573
+ const processId = ctx.flags["process-id"]?.trim() || "process";
574
+ const name = ctx.flags.name?.trim();
575
+ const outputFlag = ctx.flags.output;
576
+ const defFlag = ctx.flags.definition;
577
+ const inputFile = ctx.flags.input;
578
+ const patchFlag = ctx.flags.patch;
579
+ // ── Modify-existing mode (--input) ─────────────────────────────────────
580
+ if (inputFile) {
581
+ const xml = await readFile(inputFile, "utf-8");
582
+ const defs = Bpmn.parse(xml);
583
+ const compact = compactify(defs);
584
+ // --dump-compact: print JSON for AI inspection and exit
585
+ if (ctx.flags["dump-compact"]) {
586
+ process.stdout.write(`${JSON.stringify(compact, null, 2)}\n`);
587
+ return;
588
+ }
589
+ // Resolve patch from --patch flag or stdin
590
+ let patch = null;
591
+ if (patchFlag) {
592
+ try {
593
+ patch = JSON.parse(patchFlag);
594
+ }
595
+ catch {
596
+ throw new Error("--patch is not valid JSON. Run --help-schema to see the format.");
597
+ }
598
+ }
599
+ else if (!process.stdin.isTTY) {
600
+ const raw = await readStdin();
601
+ if (raw) {
602
+ try {
603
+ patch = JSON.parse(raw);
604
+ }
605
+ catch {
606
+ throw new Error("stdin is not valid patch JSON. Expected {elements:[...],flows:[...]}. Run --help-schema.");
607
+ }
608
+ }
609
+ }
610
+ // Apply patch to first process (covers all single-process cases)
611
+ if (patch) {
612
+ const proc = compact.processes[0];
613
+ if (!proc)
614
+ throw new Error("Input BPMN has no processes");
615
+ if (patch.elements?.length)
616
+ proc.elements.push(...patch.elements);
617
+ if (patch.flows?.length)
618
+ proc.flows.push(...patch.flows);
619
+ }
620
+ const patched = Bpmn.export(expand(compact));
621
+ if (outputFlag === "-") {
622
+ process.stdout.write(patched);
623
+ return;
624
+ }
625
+ const outputPath = typeof outputFlag === "string" && outputFlag.length > 0 ? outputFlag : inputFile;
626
+ await writeFile(outputPath, patched, "utf-8");
627
+ ctx.output.ok(patch ? `Patched and written to ${outputPath}` : `Re-laid-out and written to ${outputPath}`);
628
+ return;
629
+ }
630
+ // ── Generate mode (template / definition / stdin) ──────────────────────
631
+ let compact = null;
632
+ if (defFlag) {
633
+ try {
634
+ compact = JSON.parse(defFlag);
635
+ }
636
+ catch {
637
+ throw new Error("--definition is not valid JSON. Run --help-schema to see the format.");
638
+ }
639
+ }
640
+ else if (!process.stdin.isTTY) {
641
+ const raw = await readStdin();
642
+ if (raw) {
643
+ try {
644
+ compact = JSON.parse(raw);
645
+ }
646
+ catch {
647
+ throw new Error("stdin is not valid CompactDiagram JSON. Run --help-schema to see the format.");
648
+ }
649
+ }
650
+ }
651
+ let xml;
652
+ if (compact) {
653
+ xml = Bpmn.export(expand(compact));
654
+ }
655
+ else {
656
+ const templateName = ctx.flags.template ?? "minimal";
657
+ const templateFn = TEMPLATES[templateName];
658
+ if (!templateFn) {
659
+ throw new Error(`Unknown template "${templateName}". Available: ${TEMPLATE_NAMES.join(", ")}`);
660
+ }
661
+ xml = Bpmn.export(expand(templateFn(processId, name)));
662
+ }
663
+ if (outputFlag === "-") {
664
+ process.stdout.write(xml);
665
+ return;
666
+ }
667
+ const effectiveId = compact?.processes[0]?.id ?? ctx.flags["process-id"] ?? "process";
668
+ const outputPath = typeof outputFlag === "string" && outputFlag.length > 0 ? outputFlag : `${effectiveId}.bpmn`;
669
+ await writeFile(outputPath, xml, "utf-8");
670
+ ctx.output.ok(`BPMN written to ${outputPath}`);
671
+ },
672
+ };
673
+ export const generateGroup = {
674
+ name: "generate",
675
+ aliases: ["gen"],
676
+ description: "Generate BPMN, DMN, and form files from parameters",
677
+ commands: [generateBpmnCmd],
678
+ };
679
+ //# sourceMappingURL=generate.js.map
@@ -4,6 +4,7 @@ import { askGroup } from "./ask.js";
4
4
  import { getDmnReqsXmlCmd, getDmnXmlCmd, getStartFormCmd, getUserTaskFormCmd, getXmlCmd, renderBpmnCmd, } from "./bpmn.js";
5
5
  import { completionGroup } from "./completion.js";
6
6
  import { connectorGroup } from "./connector.js";
7
+ import { generateGroup } from "./generate.js";
7
8
  import { lintGroup } from "./lint.js";
8
9
  import { pluginGroup } from "./plugin.js";
9
10
  import { profileGroup } from "./profile.js";
@@ -14,6 +15,7 @@ import { settingsGroup } from "./settings.js";
14
15
  import { skillsGroup } from "./skills.js";
15
16
  import { storyGroup } from "./story.js";
16
17
  import { testGroup } from "./test.js";
18
+ import { viewGroup } from "./view.js";
17
19
  import { workerStartCmd } from "./worker-start.js";
18
20
  import { workerCmd } from "./worker.js";
19
21
  // Inject custom commands into generated groups without modifying generated files.
@@ -55,6 +57,7 @@ const workerGroup = {
55
57
  /** Pinned groups shown above the separator in the main TUI menu. */
56
58
  export const pinnedGroups = [
57
59
  askGroup,
60
+ generateGroup,
58
61
  lintGroup,
59
62
  proxyGroup,
60
63
  reebeGroup,
@@ -62,6 +65,7 @@ export const pinnedGroups = [
62
65
  storyGroup,
63
66
  settingsGroup,
64
67
  testGroup,
68
+ viewGroup,
65
69
  workerGroup,
66
70
  ];
67
71
  /** API command groups — shown below the plugin section in the main TUI menu. */
@@ -0,0 +1,373 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile, readdir, stat } from "node:fs/promises";
3
+ import { createServer } from "node:http";
4
+ import { basename, extname, join } from "node:path";
5
+ import { renderDmnAscii, renderFormAscii } from "@bpmnkit/ascii";
6
+ import { Bpmn, exportSvg } from "@bpmnkit/core";
7
+ // ── Utilities ─────────────────────────────────────────────────────────────────
8
+ function openBrowser(url) {
9
+ const [cmd, args] = process.platform === "darwin"
10
+ ? ["open", [url]]
11
+ : process.platform === "win32"
12
+ ? ["cmd", ["/c", "start", "", url]]
13
+ : ["xdg-open", [url]];
14
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
15
+ }
16
+ function escapeHtml(s) {
17
+ return s
18
+ .replace(/&/g, "&amp;")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;")
21
+ .replace(/"/g, "&quot;");
22
+ }
23
+ /** Expand paths: directories are scanned (top-level) for matching extensions. */
24
+ async function resolvePaths(paths, extensions) {
25
+ const result = [];
26
+ for (const p of paths) {
27
+ const s = await stat(p).catch(() => null);
28
+ if (!s)
29
+ throw new Error(`Path not found: ${p}`);
30
+ if (s.isDirectory()) {
31
+ const entries = await readdir(p, { withFileTypes: true });
32
+ const matched = entries
33
+ .filter((e) => e.isFile() && extensions.includes(extname(e.name).toLowerCase()))
34
+ .sort((a, b) => a.name.localeCompare(b.name))
35
+ .map((e) => join(p, e.name));
36
+ if (matched.length === 0) {
37
+ throw new Error(`No ${extensions.join("/")} files found in directory: ${p}`);
38
+ }
39
+ result.push(...matched);
40
+ }
41
+ else {
42
+ result.push(p);
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ // ── Panel builders ────────────────────────────────────────────────────────────
48
+ async function buildBpmnPanel(file, theme) {
49
+ const xml = await readFile(file, "utf-8");
50
+ const defs = Bpmn.parse(xml);
51
+ return { label: basename(file), type: "bpmn", content: exportSvg(defs, { theme }) };
52
+ }
53
+ async function buildDmnPanel(file) {
54
+ const xml = await readFile(file, "utf-8");
55
+ return { label: basename(file), type: "dmn", content: renderDmnAscii(xml) };
56
+ }
57
+ async function buildFormPanel(file) {
58
+ const json = await readFile(file, "utf-8");
59
+ return { label: basename(file), type: "form", content: renderFormAscii(json) };
60
+ }
61
+ async function panelFromFile(file, theme) {
62
+ const ext = extname(file).toLowerCase();
63
+ if (ext === ".bpmn")
64
+ return buildBpmnPanel(file, theme);
65
+ if (ext === ".dmn")
66
+ return buildDmnPanel(file);
67
+ if (ext === ".form")
68
+ return buildFormPanel(file);
69
+ throw new Error(`Unsupported file extension: ${file}. Use .bpmn, .dmn, or .form`);
70
+ }
71
+ // ── HTML builder ──────────────────────────────────────────────────────────────
72
+ function buildHtml(panels, theme) {
73
+ const tabs = panels
74
+ .map((p, i) => `<button class="tab${i === 0 ? " active" : ""}" data-idx="${i}" data-type="${p.type}">${escapeHtml(p.label)}</button>`)
75
+ .join("\n ");
76
+ const panelHtml = panels
77
+ .map((p, i) => {
78
+ const cls = `panel${i === 0 ? " active" : ""}`;
79
+ if (p.type === "bpmn") {
80
+ return `<div class="${cls}" data-idx="${i}" data-type="bpmn">${p.content}</div>`;
81
+ }
82
+ return `<div class="${cls}" data-idx="${i}" data-type="${p.type}"><pre>${escapeHtml(p.content)}</pre></div>`;
83
+ })
84
+ .join("\n ");
85
+ return `<!DOCTYPE html>
86
+ <html lang="en" data-theme="${theme}">
87
+ <head>
88
+ <meta charset="UTF-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
90
+ <title>BPMN Kit Viewer</title>
91
+ <style>
92
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
93
+ :root {
94
+ --bg: #f4f4f8; --surface: #ffffff; --border: #d0d0e8;
95
+ --fg: #1a1a2e; --fg-muted: #6666a0; --accent: #1a56db;
96
+ --mono: ui-monospace, "Cascadia Code", "JetBrains Mono", monospace;
97
+ }
98
+ [data-theme="dark"] {
99
+ --bg: #0d0d16; --surface: #161626; --border: #2a2a42;
100
+ --fg: #cdd6f4; --fg-muted: #8888a8; --accent: #6b9df7;
101
+ }
102
+ body {
103
+ font-family: system-ui, -apple-system, sans-serif;
104
+ background: var(--bg);
105
+ color: var(--fg);
106
+ height: 100vh;
107
+ display: flex;
108
+ flex-direction: column;
109
+ }
110
+ header {
111
+ background: var(--surface);
112
+ border-bottom: 1px solid var(--border);
113
+ padding: 0 16px;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 2px;
117
+ overflow-x: auto;
118
+ flex-shrink: 0;
119
+ height: 44px;
120
+ }
121
+ .brand {
122
+ font-size: 11px;
123
+ font-weight: 700;
124
+ letter-spacing: 0.08em;
125
+ text-transform: uppercase;
126
+ color: var(--fg-muted);
127
+ margin-right: 12px;
128
+ white-space: nowrap;
129
+ }
130
+ .tab {
131
+ padding: 6px 14px;
132
+ border: none;
133
+ border-bottom: 2px solid transparent;
134
+ background: none;
135
+ color: var(--fg-muted);
136
+ font: inherit;
137
+ font-size: 13px;
138
+ cursor: pointer;
139
+ border-radius: 4px 4px 0 0;
140
+ white-space: nowrap;
141
+ transition: color 0.1s;
142
+ }
143
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 500; }
144
+ .tab:hover:not(.active) { color: var(--fg); }
145
+ .tab-type {
146
+ font-size: 9px;
147
+ font-weight: 700;
148
+ letter-spacing: 0.06em;
149
+ text-transform: uppercase;
150
+ opacity: 0.6;
151
+ margin-left: 5px;
152
+ vertical-align: middle;
153
+ }
154
+ main {
155
+ flex: 1;
156
+ overflow: auto;
157
+ padding: 24px;
158
+ display: flex;
159
+ justify-content: center;
160
+ align-items: flex-start;
161
+ }
162
+ .panel { display: none; width: 100%; }
163
+ .panel.active { display: block; }
164
+ .panel[data-type="bpmn"] svg {
165
+ max-width: 100%;
166
+ height: auto;
167
+ border-radius: 8px;
168
+ box-shadow: 0 2px 16px rgba(0,0,0,0.1);
169
+ }
170
+ .panel[data-type="dmn"] pre,
171
+ .panel[data-type="form"] pre {
172
+ font-family: var(--mono);
173
+ font-size: 13px;
174
+ line-height: 1.5;
175
+ background: var(--surface);
176
+ border: 1px solid var(--border);
177
+ border-radius: 8px;
178
+ padding: 20px 24px;
179
+ overflow-x: auto;
180
+ white-space: pre;
181
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
182
+ }
183
+ footer {
184
+ background: var(--surface);
185
+ border-top: 1px solid var(--border);
186
+ padding: 6px 16px;
187
+ font-size: 11px;
188
+ color: var(--fg-muted);
189
+ flex-shrink: 0;
190
+ }
191
+ kbd {
192
+ background: var(--border);
193
+ border-radius: 3px;
194
+ padding: 1px 4px;
195
+ font-family: inherit;
196
+ }
197
+ </style>
198
+ </head>
199
+ <body>
200
+ <header>
201
+ <span class="brand">BPMN Kit</span>
202
+ ${tabs}
203
+ </header>
204
+ <main>
205
+ ${panelHtml}
206
+ </main>
207
+ <footer>
208
+ Use <kbd>Ctrl+Scroll</kbd> or browser zoom to zoom &mdash;
209
+ Press <kbd>Ctrl+C</kbd> in terminal to stop the server
210
+ </footer>
211
+ <script>
212
+ const tabs = document.querySelectorAll('.tab')
213
+ const panels = document.querySelectorAll('.panel')
214
+ tabs.forEach(tab => {
215
+ tab.addEventListener('click', () => {
216
+ const idx = tab.dataset.idx
217
+ tabs.forEach(t => t.classList.toggle('active', t.dataset.idx === idx))
218
+ panels.forEach(p => p.classList.toggle('active', p.dataset.idx === idx))
219
+ })
220
+ })
221
+ </script>
222
+ </body>
223
+ </html>`;
224
+ }
225
+ // ── Server ────────────────────────────────────────────────────────────────────
226
+ async function serve(panels, port, theme, noOpen, ctx) {
227
+ const html = buildHtml(panels, theme);
228
+ const server = createServer((_req, res) => {
229
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
230
+ res.end(html);
231
+ });
232
+ await new Promise((resolve, reject) => {
233
+ server.listen(port, "127.0.0.1", () => resolve());
234
+ server.on("error", reject);
235
+ });
236
+ const url = `http://localhost:${port}`;
237
+ ctx.output.info(`Serving ${panels.length} file${panels.length === 1 ? "" : "s"} at ${url}`);
238
+ if (!noOpen)
239
+ openBrowser(url);
240
+ ctx.output.info("Press Ctrl+C to stop");
241
+ await new Promise((resolve) => {
242
+ process.once("SIGINT", resolve);
243
+ process.once("SIGTERM", resolve);
244
+ });
245
+ server.close();
246
+ }
247
+ // ── Shared flags ──────────────────────────────────────────────────────────────
248
+ const PORT_FLAG = {
249
+ name: "port",
250
+ description: "Port for the local server (default: 3044)",
251
+ type: "number",
252
+ default: 3044,
253
+ };
254
+ const THEME_FLAG = {
255
+ name: "theme",
256
+ description: "Color theme: light (default) or dark",
257
+ type: "string",
258
+ default: "light",
259
+ enum: ["light", "dark"],
260
+ };
261
+ const NO_OPEN_FLAG = {
262
+ name: "no-open",
263
+ description: "Do not open the browser automatically",
264
+ type: "boolean",
265
+ };
266
+ // ── Commands ──────────────────────────────────────────────────────────────────
267
+ const viewBpmnCmd = {
268
+ name: "bpmn",
269
+ description: "View BPMN files or a folder of .bpmn files in the browser",
270
+ args: [
271
+ { name: "path", description: "File(s) or folder(s) containing .bpmn files", required: true },
272
+ ],
273
+ flags: [PORT_FLAG, THEME_FLAG, NO_OPEN_FLAG],
274
+ examples: [
275
+ { description: "View a single file", command: "casen view bpmn process.bpmn" },
276
+ { description: "View all files in a folder", command: "casen view bpmn ./processes/" },
277
+ { description: "View multiple files", command: "casen view bpmn order.bpmn payment.bpmn" },
278
+ {
279
+ description: "Dark theme on custom port",
280
+ command: "casen view bpmn process.bpmn --theme dark --port 8080",
281
+ },
282
+ ],
283
+ async run(ctx) {
284
+ if (ctx.positional.length === 0)
285
+ throw new Error("Provide at least one file or folder path");
286
+ const port = ctx.flags.port ?? 3044;
287
+ const theme = ctx.flags.theme === "dark" ? "dark" : "light";
288
+ const files = await resolvePaths(ctx.positional, [".bpmn"]);
289
+ const panels = await Promise.all(files.map((f) => buildBpmnPanel(f, theme)));
290
+ await serve(panels, port, theme, ctx.flags["no-open"] === true, ctx);
291
+ },
292
+ };
293
+ const viewDmnCmd = {
294
+ name: "dmn",
295
+ description: "View DMN files or a folder of .dmn files in the browser",
296
+ args: [
297
+ { name: "path", description: "File(s) or folder(s) containing .dmn files", required: true },
298
+ ],
299
+ flags: [PORT_FLAG, THEME_FLAG, NO_OPEN_FLAG],
300
+ examples: [
301
+ { description: "View a single DMN file", command: "casen view dmn decision.dmn" },
302
+ { description: "View all DMN files in a folder", command: "casen view dmn ./decisions/" },
303
+ ],
304
+ async run(ctx) {
305
+ if (ctx.positional.length === 0)
306
+ throw new Error("Provide at least one file or folder path");
307
+ const port = ctx.flags.port ?? 3044;
308
+ const theme = ctx.flags.theme === "dark" ? "dark" : "light";
309
+ const files = await resolvePaths(ctx.positional, [".dmn"]);
310
+ const panels = await Promise.all(files.map((f) => buildDmnPanel(f)));
311
+ await serve(panels, port, theme, ctx.flags["no-open"] === true, ctx);
312
+ },
313
+ };
314
+ const viewFormCmd = {
315
+ name: "form",
316
+ description: "View Camunda form files or a folder of .form files in the browser",
317
+ args: [
318
+ { name: "path", description: "File(s) or folder(s) containing .form files", required: true },
319
+ ],
320
+ flags: [PORT_FLAG, THEME_FLAG, NO_OPEN_FLAG],
321
+ examples: [
322
+ { description: "View a single form file", command: "casen view form my-form.form" },
323
+ { description: "View all forms in a folder", command: "casen view form ./forms/" },
324
+ ],
325
+ async run(ctx) {
326
+ if (ctx.positional.length === 0)
327
+ throw new Error("Provide at least one file or folder path");
328
+ const port = ctx.flags.port ?? 3044;
329
+ const theme = ctx.flags.theme === "dark" ? "dark" : "light";
330
+ const files = await resolvePaths(ctx.positional, [".form"]);
331
+ const panels = await Promise.all(files.map((f) => buildFormPanel(f)));
332
+ await serve(panels, port, theme, ctx.flags["no-open"] === true, ctx);
333
+ },
334
+ };
335
+ const viewOpenCmd = {
336
+ name: "open",
337
+ description: "View any mix of .bpmn, .dmn, and .form files or folders in the browser",
338
+ args: [
339
+ {
340
+ name: "path",
341
+ description: "File(s) or folder(s) — .bpmn, .dmn, and .form auto-detected",
342
+ required: true,
343
+ },
344
+ ],
345
+ flags: [PORT_FLAG, THEME_FLAG, NO_OPEN_FLAG],
346
+ examples: [
347
+ {
348
+ description: "Open any supported file",
349
+ command: "casen view open process.bpmn decision.dmn",
350
+ },
351
+ { description: "Open a folder (all supported types)", command: "casen view open ./project/" },
352
+ {
353
+ description: "Mix files and folders",
354
+ command: "casen view open ./processes/ extra.dmn review.form",
355
+ },
356
+ ],
357
+ async run(ctx) {
358
+ if (ctx.positional.length === 0)
359
+ throw new Error("Provide at least one file or folder path");
360
+ const port = ctx.flags.port ?? 3044;
361
+ const theme = ctx.flags.theme === "dark" ? "dark" : "light";
362
+ const files = await resolvePaths(ctx.positional, [".bpmn", ".dmn", ".form"]);
363
+ const panels = await Promise.all(files.map((f) => panelFromFile(f, theme)));
364
+ await serve(panels, port, theme, ctx.flags["no-open"] === true, ctx);
365
+ },
366
+ };
367
+ // ── Group ─────────────────────────────────────────────────────────────────────
368
+ export const viewGroup = {
369
+ name: "view",
370
+ description: "View BPMN, DMN, and form files in the browser via a local server",
371
+ commands: [viewOpenCmd, viewBpmnCmd, viewDmnCmd, viewFormCmd],
372
+ };
373
+ //# sourceMappingURL=view.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/cli",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "Command-line interface for Camunda 8 — deploy, manage, and monitor processes from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,12 +17,12 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@bpmnkit/api": "0.0.18",
20
+ "@bpmnkit/ascii": "0.0.23",
20
21
  "@bpmnkit/connector-gen": "0.0.13",
21
- "@bpmnkit/core": "0.0.22",
22
- "@bpmnkit/ascii": "0.0.22",
23
- "@bpmnkit/engine": "0.1.22",
22
+ "@bpmnkit/core": "0.0.23",
23
+ "@bpmnkit/engine": "0.1.23",
24
24
  "@bpmnkit/profiles": "0.0.16",
25
- "@bpmnkit/proxy": "0.0.25"
25
+ "@bpmnkit/proxy": "0.0.26"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"