@waron97/prbot 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -18
- package/agrippa-pb.md +141 -68
- package/package.json +5 -1
- package/src/agrippa/commands/clone.js +17 -10
- package/src/agrippa/commands/cloneLrp.js +114 -0
- package/src/agrippa/commands/clonePb.js +10 -10
- package/src/agrippa/commands/diff.js +9 -8
- package/src/agrippa/commands/init.js +14 -13
- package/src/agrippa/commands/initPhase.js +51 -42
- package/src/agrippa/commands/pb.js +71 -39
- package/src/agrippa/commands/pull.js +137 -42
- package/src/agrippa/commands/pullLrp.js +54 -0
- package/src/agrippa/commands/pullPb.js +2 -3
- package/src/agrippa/commands/push.js +112 -33
- package/src/agrippa/commands/pushLrp.js +56 -0
- package/src/agrippa/commands/repair.js +4 -3
- package/src/agrippa/index.js +48 -30
- package/src/agrippa/lib/lrpApi.js +153 -0
- package/src/agrippa/lib/pbEdit.js +31 -10
- package/src/agrippa/lib/pbLayout.js +8 -1
- package/src/agrippa/lib/pbModel.js +318 -86
- package/src/agrippa/lib/pbPreview.js +6 -2
- package/src/agrippa/lib/pbProject.js +93 -7
- package/src/agrippa/lib/pbScriptTemplate.js +29 -0
- package/src/commands/autopr.js +21 -22
- package/src/commands/changelog.js +4 -3
- package/src/commands/commit.js +11 -10
- package/src/commands/export.js +2 -1
- package/src/commands/exportPb.js +19 -2
- package/src/commands/init.js +2 -1
- package/src/commands/routine.js +19 -8
- package/src/index.js +137 -112
- package/src/lib/auth.js +19 -1
- package/src/lib/logger.js +12 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Symphony long-running-process (LRP) API client. Base = protocol+host of
|
|
2
|
+
// IMPORTEXPORT_URL (the tabulator/deployBpmn endpoints live on the Symphony
|
|
3
|
+
// host itself, not under the import/export path). Auth uses the same
|
|
4
|
+
// Keycloak bearer as everything else (getToken()).
|
|
5
|
+
//
|
|
6
|
+
// Unlike PBs, LRPs have no stable guid: the tabulator `id` changes on every
|
|
7
|
+
// save/version bump (confirmed live — a re-fetched id differs from a
|
|
8
|
+
// previously captured one for the same process). The process `name` is the
|
|
9
|
+
// only stable identifier, so every write re-resolves the current id/tenantId
|
|
10
|
+
// by name immediately before acting on it.
|
|
11
|
+
|
|
12
|
+
import fetch from 'node-fetch';
|
|
13
|
+
|
|
14
|
+
function getSymphonyBase() {
|
|
15
|
+
const url = process.env.IMPORTEXPORT_URL;
|
|
16
|
+
if (!url) throw new Error('IMPORTEXPORT_URL is not configured. Run `prbot init`.');
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// List/search processes by name (server-side `like` filter when nameFilter is
|
|
22
|
+
// set; unfiltered page otherwise). Returns the richer per-row fields the save
|
|
23
|
+
// body needs (tenantId, svg), not just id/name.
|
|
24
|
+
async function listLrps(token, nameFilter, signal) {
|
|
25
|
+
const base = getSymphonyBase();
|
|
26
|
+
const size = nameFilter ? 20 : 12;
|
|
27
|
+
const params = encodeURIComponent(JSON.stringify({ page: 1, size, sorters: [], filters: [] }));
|
|
28
|
+
const otherfilters = encodeURIComponent(
|
|
29
|
+
JSON.stringify([
|
|
30
|
+
{ field: 'id', type: '=', value: null },
|
|
31
|
+
{ field: 'name', type: 'like', value: nameFilter ?? null },
|
|
32
|
+
{ field: 'tenantId', type: '=', value: null },
|
|
33
|
+
{ field: 'latestVersion', type: '=', value: true },
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
const othersort = encodeURIComponent(
|
|
37
|
+
JSON.stringify({ field: 'lastModifiedDate', dir: 'desc' })
|
|
38
|
+
);
|
|
39
|
+
const url = `${base}/symphony/restInfo/ajax/tabulator?params=${params}&connector=SymphBpmnFileTabCon&otherfilters=${otherfilters}&card=true&othersort=${othersort}`;
|
|
40
|
+
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
headers: { accept: 'application/json', Authorization: `Bearer ${token}` },
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) throw new Error(`LRP list failed with ${res.status}: ${await res.text()}`);
|
|
46
|
+
const json = await res.json();
|
|
47
|
+
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const row of json.data || []) {
|
|
50
|
+
for (let i = 1; i <= 4; i++) {
|
|
51
|
+
const cell = row[`cellContent${i}`];
|
|
52
|
+
if (cell && cell.id && cell.name) {
|
|
53
|
+
rows.push({
|
|
54
|
+
id: String(cell.id),
|
|
55
|
+
name: cell.name,
|
|
56
|
+
tenantId: cell.tenantId,
|
|
57
|
+
version: cell.version,
|
|
58
|
+
status: cell.status,
|
|
59
|
+
bpmnFileSvg: cell.bpmnFileSvg,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Resolve the current row for a process by its stable name (exact match
|
|
68
|
+
// against the server-side `like` search results) — the id-by-name lookup
|
|
69
|
+
// that replaces PB's guid-based addressing everywhere.
|
|
70
|
+
async function resolveLrpByName(token, name) {
|
|
71
|
+
const rows = await listLrps(token, name);
|
|
72
|
+
const row = rows.find((r) => r.name === name);
|
|
73
|
+
if (!row) throw new Error(`No long-running process found with name "${name}"`);
|
|
74
|
+
return row;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fetch the full BPMN XML + description for one process by id. The endpoint
|
|
78
|
+
// returns an HTML/JS fragment (not JSON) — the real payload is a base64
|
|
79
|
+
// `doc.value` assignment plus a set of `bpmn_*` hidden inputs.
|
|
80
|
+
async function getLrpXml(token, id) {
|
|
81
|
+
const base = getSymphonyBase();
|
|
82
|
+
const url = `${base}/symphony/restInfo/ajax/tabulator/id/${id}?connector=SymphBpmnFileTabCon&modelroot=/management/development/edit`;
|
|
83
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
84
|
+
if (!res.ok) throw new Error(`LRP detail fetch failed with ${res.status}: ${await res.text()}`);
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
|
|
87
|
+
const docMatch = text.match(/doc\.value\s*=\s*'([^']+)'/);
|
|
88
|
+
if (!docMatch) throw new Error('Could not find doc.value in LRP detail response');
|
|
89
|
+
const filenameMatch = text.match(/filename\.value\s*=\s*'([^']+)'/);
|
|
90
|
+
if (!filenameMatch) throw new Error('Could not find filename.value in LRP detail response');
|
|
91
|
+
const descMatch = text.match(/name="bpmn_description"\s+value="([^"]*)"/);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
xml: Buffer.from(docMatch[1], 'base64').toString('utf-8'),
|
|
95
|
+
filename: filenameMatch[1],
|
|
96
|
+
description: descMatch ? descMatch[1] : '',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Save (PATCH) the wizard's BPMN back to Symphony. `row` must carry the
|
|
101
|
+
// CURRENT id/tenantId (re-resolved by name immediately before this call —
|
|
102
|
+
// see resolveLrpByName) and `bpmnFileSvg` (the last-known diagram thumbnail,
|
|
103
|
+
// echoed back unchanged per the agreed approach — it's display-only, the
|
|
104
|
+
// server deploys from the XML, not the SVG). `newVersion:false` saves the
|
|
105
|
+
// process in place rather than creating a new version.
|
|
106
|
+
async function saveLrp(token, row, xml, description = '') {
|
|
107
|
+
const base = getSymphonyBase();
|
|
108
|
+
const url = `${base}/symphony/restInfo/ajax/tabulator/${row.id}?connector=SymphBpmnFileTabCon`;
|
|
109
|
+
const body = {
|
|
110
|
+
id: row.id,
|
|
111
|
+
tenantId: row.tenantId,
|
|
112
|
+
newVersion: false,
|
|
113
|
+
description,
|
|
114
|
+
name: row.name,
|
|
115
|
+
bpmnFile: Buffer.from(xml, 'utf-8').toString('base64'),
|
|
116
|
+
bpmnFileSvg: row.bpmnFileSvg ?? '',
|
|
117
|
+
oldTenantId: row.tenantId,
|
|
118
|
+
};
|
|
119
|
+
const res = await fetch(url, {
|
|
120
|
+
method: 'PATCH',
|
|
121
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) throw new Error(`LRP save failed with ${res.status}: ${await res.text()}`);
|
|
125
|
+
const responseText = await res.text();
|
|
126
|
+
return responseText ? JSON.parse(responseText) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Resolve-by-name + fetch detail in one call — the shape both pull's
|
|
130
|
+
// classification step and pullLrpEntry/pushLrpEntry need: the current row
|
|
131
|
+
// (id/tenantId/svg, re-resolved fresh — never trust a stale one) plus a
|
|
132
|
+
// decompose-ready payload.
|
|
133
|
+
async function fetchUpstream(token, name) {
|
|
134
|
+
const row = await resolveLrpByName(token, name);
|
|
135
|
+
const { xml, description } = await getLrpXml(token, row.id);
|
|
136
|
+
return { row: { ...row, description }, payload: { built_page: xml } };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Deploy a saved process so live consumers see the latest version.
|
|
140
|
+
async function deployLrp(token, id) {
|
|
141
|
+
const base = getSymphonyBase();
|
|
142
|
+
const url = `${base}/symphony/restInfo/ajax/deployBpmn`;
|
|
143
|
+
const res = await fetch(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
146
|
+
body: JSON.stringify({ id }),
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) throw new Error(`LRP deploy failed with ${res.status}: ${await res.text()}`);
|
|
149
|
+
const responseText = await res.text();
|
|
150
|
+
return responseText ? JSON.parse(responseText) : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { listLrps, resolveLrpByName, getLrpXml, saveLrp, deployLrp, fetchUpstream };
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { stringify as yamlStringify } from 'yaml';
|
|
16
16
|
import { pad, toSlug } from './pbProject.js';
|
|
17
|
+
import { SCRIPT_TEMPLATE } from './pbScriptTemplate.js';
|
|
17
18
|
|
|
18
19
|
// Fixed element sizes (measured across all fixtures). Containers are sized by
|
|
19
20
|
// the layout engine; we seed a small default so a stubbed clone stays valid.
|
|
@@ -27,6 +28,12 @@ const SIZE = {
|
|
|
27
28
|
userTask: [84, 84],
|
|
28
29
|
subProcess: [350, 200],
|
|
29
30
|
transaction: [350, 200],
|
|
31
|
+
// LRP-only (measured across the real LRP corpus):
|
|
32
|
+
intermediateCatchEvent: [36, 36],
|
|
33
|
+
intermediateThrowEvent: [36, 36],
|
|
34
|
+
callActivity: [84, 84],
|
|
35
|
+
parallelGateway: [50, 50],
|
|
36
|
+
eventBasedGateway: [50, 50],
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
// BPMN id prefixes by node type (mirrors the upstream naming convention).
|
|
@@ -40,6 +47,11 @@ const PREFIX = {
|
|
|
40
47
|
userTask: 'UserTask',
|
|
41
48
|
subProcess: 'SubProcess',
|
|
42
49
|
transaction: 'Transaction',
|
|
50
|
+
intermediateCatchEvent: 'IntermediateCatchEvent',
|
|
51
|
+
intermediateThrowEvent: 'IntermediateThrowEvent',
|
|
52
|
+
callActivity: 'CallActivity',
|
|
53
|
+
parallelGateway: 'ParallelGateway',
|
|
54
|
+
eventBasedGateway: 'EventBasedGateway',
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
const CONTAINER = new Set(['subProcess', 'transaction']);
|
|
@@ -126,7 +138,7 @@ function nextScriptSeq(existingScriptFiles) {
|
|
|
126
138
|
|
|
127
139
|
// ---------- add ----------
|
|
128
140
|
|
|
129
|
-
// Add a node. For scriptTask, scaffolds
|
|
141
|
+
// Add a node. For scriptTask, scaffolds a templated script file and refs it. For
|
|
130
142
|
// userTask, scaffolds a minimal page file + a manifest page entry (guid=null →
|
|
131
143
|
// push will create it upstream). Container nodes start empty + expanded.
|
|
132
144
|
function addNode(structure, manifest, opts, ctx = {}) {
|
|
@@ -144,7 +156,7 @@ function addNode(structure, manifest, opts, ctx = {}) {
|
|
|
144
156
|
if (type === 'scriptTask') {
|
|
145
157
|
const seq = nextScriptSeq(ctx.existingScripts);
|
|
146
158
|
const file = `scripts/${pad(seq)}_${toSlug(name || id)}.js`;
|
|
147
|
-
writes[file] =
|
|
159
|
+
writes[file] = SCRIPT_TEMPLATE;
|
|
148
160
|
node.script = file;
|
|
149
161
|
} else if (type === 'userTask') {
|
|
150
162
|
const formKey = toSlug(name || id);
|
|
@@ -434,19 +446,24 @@ function lintEdgeNames(structure) {
|
|
|
434
446
|
for (const e of outs) {
|
|
435
447
|
if (e.id === def) continue;
|
|
436
448
|
if (!e.name) {
|
|
437
|
-
issues.push(
|
|
438
|
-
`${n.id} → ${e.target} (${e.id}): non-default flow has no name.`
|
|
439
|
-
);
|
|
449
|
+
issues.push(`${n.id} → ${e.target} (${e.id}): non-default flow has no name.`);
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
});
|
|
443
453
|
return issues;
|
|
444
454
|
}
|
|
445
455
|
|
|
456
|
+
// Gateway types that legitimately fan multiple flows into one node (a "join").
|
|
457
|
+
// parallelGateway/eventBasedGateway joins are common in the real LRP corpus
|
|
458
|
+
// (61 occurrences) — only exclusiveGateway's merge semantics need a lint rule.
|
|
459
|
+
const JOIN_CAPABLE_GATEWAYS = new Set(['exclusiveGateway', 'parallelGateway', 'eventBasedGateway']);
|
|
460
|
+
|
|
446
461
|
// Flag incoming-edge violations:
|
|
447
|
-
// - Only
|
|
462
|
+
// - Only a gateway may have >1 incoming flows (plain tasks/events are 1-in).
|
|
448
463
|
// - An exclusiveGateway may not have both >1 incoming AND >1 outgoing (pick one
|
|
449
|
-
// direction for merging or splitting, not both at once)
|
|
464
|
+
// direction for merging or splitting, not both at once) — parallel/event-based
|
|
465
|
+
// gateways may legitimately fork-then-join across the diagram, so this
|
|
466
|
+
// specific rule stays scoped to exclusiveGateway.
|
|
450
467
|
function lintIncomingEdges(structure) {
|
|
451
468
|
const inCount = {};
|
|
452
469
|
const outCount = {};
|
|
@@ -461,9 +478,9 @@ function lintIncomingEdges(structure) {
|
|
|
461
478
|
eachNode(structure.nodes, null, (n) => {
|
|
462
479
|
const inc = inCount[n.id] || 0;
|
|
463
480
|
const out = outCount[n.id] || 0;
|
|
464
|
-
if (inc > 1 && n.type
|
|
481
|
+
if (inc > 1 && !JOIN_CAPABLE_GATEWAYS.has(n.type)) {
|
|
465
482
|
issues.push(
|
|
466
|
-
`${n.id} (${n.name || n.type}): only
|
|
483
|
+
`${n.id} (${n.name || n.type}): only a gateway may have multiple incoming flows (has ${inc}).`
|
|
467
484
|
);
|
|
468
485
|
}
|
|
469
486
|
if (n.type === 'exclusiveGateway' && inc > 1 && out > 1) {
|
|
@@ -477,7 +494,11 @@ function lintIncomingEdges(structure) {
|
|
|
477
494
|
|
|
478
495
|
// Run all lint rules and return combined issues.
|
|
479
496
|
function lintAll(structure) {
|
|
480
|
-
return [
|
|
497
|
+
return [
|
|
498
|
+
...lintGateways(structure),
|
|
499
|
+
...lintEdgeNames(structure),
|
|
500
|
+
...lintIncomingEdges(structure),
|
|
501
|
+
];
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
// Remove an edge by id, or by --from/--to pair.
|
|
@@ -154,7 +154,14 @@ async function autoLayout(structure) {
|
|
|
154
154
|
targets: [e.target],
|
|
155
155
|
sourcePort,
|
|
156
156
|
labels: e.name
|
|
157
|
-
? [
|
|
157
|
+
? [
|
|
158
|
+
{
|
|
159
|
+
id: `${e.id}__lbl`,
|
|
160
|
+
text: e.name,
|
|
161
|
+
width: Math.min(e.name.length * 6, 140),
|
|
162
|
+
height: 14,
|
|
163
|
+
},
|
|
164
|
+
]
|
|
158
165
|
: [],
|
|
159
166
|
layoutOptions: {
|
|
160
167
|
'elk.layered.priority.straightness': isHappy ? '10' : 1,
|