@waron97/prbot 3.2.0 → 3.3.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 +172 -74
- package/package.json +1 -1
- package/src/agrippa/commands/clone.js +7 -1
- package/src/agrippa/commands/cloneLrp.js +114 -0
- package/src/agrippa/commands/diff.js +23 -10
- package/src/agrippa/commands/initPhase.js +44 -34
- package/src/agrippa/commands/pb.js +61 -16
- package/src/agrippa/commands/pull.js +93 -17
- package/src/agrippa/commands/pullLrp.js +55 -0
- package/src/agrippa/commands/push.js +88 -19
- package/src/agrippa/commands/pushLrp.js +56 -0
- package/src/agrippa/index.js +32 -17
- package/src/agrippa/lib/lrpApi.js +153 -0
- package/src/agrippa/lib/pbEdit.js +103 -6
- package/src/agrippa/lib/pbLayout.js +23 -2
- package/src/agrippa/lib/pbModel.js +324 -90
- package/src/agrippa/lib/pbPreview.js +4 -2
- package/src/agrippa/lib/pbProject.js +34 -5
- package/src/agrippa/lib/pbScriptTemplate.js +29 -0
- package/src/commands/autopr.js +1 -1
|
@@ -5,11 +5,13 @@ import { getToken } from '../../lib/auth.js';
|
|
|
5
5
|
import { updateMfa, updatePhase } from '../lib/api.js';
|
|
6
6
|
import { computeChecksum } from '../lib/checksum.js';
|
|
7
7
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
8
|
+
import { fetchUpstream } from '../lib/lrpApi.js';
|
|
8
9
|
import { getProcess } from '../lib/pbApi.js';
|
|
9
|
-
import { localChecksum } from '../lib/pbProject.js';
|
|
10
|
+
import { localChecksum, remoteChecksumPb } from '../lib/pbProject.js';
|
|
10
11
|
import { projectReader } from '../lib/pbWorkspace.js';
|
|
11
12
|
import { fileExists, readCodeFile } from '../lib/workspace.js';
|
|
12
13
|
import { fetchRemoteCode, selectEntries } from './pull.js';
|
|
14
|
+
import { deploy, pushLrpEntry } from './pushLrp.js';
|
|
13
15
|
import { publish, pushPbEntry } from './pushPb.js';
|
|
14
16
|
|
|
15
17
|
const BACKUP_DIR = '.backup';
|
|
@@ -46,11 +48,16 @@ async function push(opts = {}) {
|
|
|
46
48
|
(e) => e.object_type === 'phase' || e.object_type === 'mfa'
|
|
47
49
|
);
|
|
48
50
|
const hasPb = config.workspace.some((e) => e.object_type === 'process_builder');
|
|
51
|
+
const hasLrp = config.workspace.some((e) => e.object_type === 'long_running_process');
|
|
49
52
|
const ripUrl = process.env.RIP_URL;
|
|
50
53
|
if (hasCode && !ripUrl)
|
|
51
54
|
throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
52
55
|
if (hasPb && !process.env.PB_URL)
|
|
53
56
|
throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
57
|
+
if (hasLrp && !process.env.IMPORTEXPORT_URL)
|
|
58
|
+
throw new Error(
|
|
59
|
+
'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
|
|
60
|
+
);
|
|
54
61
|
|
|
55
62
|
console.log('Fetching remote state...');
|
|
56
63
|
const token = await getToken();
|
|
@@ -66,17 +73,37 @@ async function push(opts = {}) {
|
|
|
66
73
|
upstreamMap.set(e.guid, null);
|
|
67
74
|
}
|
|
68
75
|
}
|
|
76
|
+
const lrpUpstreamMap = new Map();
|
|
77
|
+
for (const e of config.workspace.filter((x) => x.object_type === 'long_running_process')) {
|
|
78
|
+
try {
|
|
79
|
+
lrpUpstreamMap.set(e.name, await fetchUpstream(token, e.name));
|
|
80
|
+
} catch {
|
|
81
|
+
lrpUpstreamMap.set(e.name, null);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
69
84
|
|
|
70
85
|
// Classify every entry to a status badge — concern: overwriting remote work.
|
|
71
86
|
const classified = config.workspace.map((entry) => {
|
|
72
87
|
if (entry.object_type === 'process_builder') {
|
|
73
88
|
const upstream = upstreamMap.get(entry.guid) ?? null;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
89
|
+
const localSemantic = localChecksum(projectReader(entry.path));
|
|
90
|
+
const remoteSemantic = upstream ? remoteChecksumPb(upstream) : null;
|
|
91
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
77
92
|
let status;
|
|
78
|
-
if (
|
|
79
|
-
else if (
|
|
93
|
+
if (localSemantic === remoteSemantic) status = 'unchanged';
|
|
94
|
+
else if (pullChecksum === remoteSemantic) status = 'fast-forward';
|
|
95
|
+
else status = 'conflict';
|
|
96
|
+
return { ...entry, upstream, status };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (entry.object_type === 'long_running_process') {
|
|
100
|
+
const upstream = lrpUpstreamMap.get(entry.name) ?? null;
|
|
101
|
+
const localSemantic = localChecksum(projectReader(entry.path));
|
|
102
|
+
const remoteSemantic = upstream ? remoteChecksumPb(upstream.payload) : null;
|
|
103
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
104
|
+
let status;
|
|
105
|
+
if (localSemantic === remoteSemantic) status = 'unchanged';
|
|
106
|
+
else if (pullChecksum === remoteSemantic) status = 'fast-forward';
|
|
80
107
|
else status = 'conflict';
|
|
81
108
|
return { ...entry, upstream, status };
|
|
82
109
|
}
|
|
@@ -88,8 +115,7 @@ async function push(opts = {}) {
|
|
|
88
115
|
const localChecksumVal = computeChecksum(localCode ?? '');
|
|
89
116
|
const pullChecksum = entry.checksum_at_pull;
|
|
90
117
|
let status;
|
|
91
|
-
if (
|
|
92
|
-
status = 'unchanged';
|
|
118
|
+
if (localChecksumVal === remoteChecksum) status = 'unchanged';
|
|
93
119
|
else if (pullChecksum === remoteChecksum) status = 'fast-forward';
|
|
94
120
|
else status = 'conflict';
|
|
95
121
|
return { ...entry, remoteCode, localCode, status };
|
|
@@ -109,10 +135,14 @@ async function push(opts = {}) {
|
|
|
109
135
|
|
|
110
136
|
const backupTs = new Date().toISOString().replace(/:/g, '-').replace('T', '_').slice(0, 19);
|
|
111
137
|
|
|
112
|
-
// Back up remote code for phase/mfa before overwriting (pb
|
|
113
|
-
// full upstream payload inside pushPbEntry).
|
|
138
|
+
// Back up remote code for phase/mfa before overwriting (pb/lrp back up
|
|
139
|
+
// their own full upstream payload inside pushPbEntry/pushLrpEntry).
|
|
114
140
|
for (const entry of selected) {
|
|
115
|
-
if (
|
|
141
|
+
if (
|
|
142
|
+
entry.object_type !== 'process_builder' &&
|
|
143
|
+
entry.object_type !== 'long_running_process' &&
|
|
144
|
+
entry.remoteCode != null
|
|
145
|
+
) {
|
|
116
146
|
const backupPath = join(BACKUP_DIR, backupTs, entry.path);
|
|
117
147
|
mkdirSync(dirname(backupPath), { recursive: true });
|
|
118
148
|
writeFileSync(backupPath, (entry.remoteCode ?? '').trim() + '\n', 'utf-8');
|
|
@@ -120,15 +150,15 @@ async function push(opts = {}) {
|
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
const pushedPb = [];
|
|
153
|
+
const pushedLrp = [];
|
|
123
154
|
let pushed = 0;
|
|
124
155
|
for (const entry of selected) {
|
|
125
|
-
const idx = config.workspace.findIndex(
|
|
126
|
-
(e)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
156
|
+
const idx = config.workspace.findIndex((e) => {
|
|
157
|
+
if (e.object_type !== entry.object_type) return false;
|
|
158
|
+
if (entry.object_type === 'process_builder') return e.guid === entry.guid;
|
|
159
|
+
if (entry.object_type === 'long_running_process') return e.name === entry.name;
|
|
160
|
+
return e.id === entry.id;
|
|
161
|
+
});
|
|
132
162
|
|
|
133
163
|
if (entry.object_type === 'process_builder') {
|
|
134
164
|
const res = await pushPbEntry(token, entry, BACKUP_DIR, backupTs);
|
|
@@ -145,6 +175,19 @@ async function push(opts = {}) {
|
|
|
145
175
|
config.workspace[idx].status = res.newStatus || 'draft';
|
|
146
176
|
}
|
|
147
177
|
pushedPb.push({ entry, idx });
|
|
178
|
+
} else if (entry.object_type === 'long_running_process') {
|
|
179
|
+
const res = await pushLrpEntry(token, entry, BACKUP_DIR, backupTs);
|
|
180
|
+
console.log(` ${entry.name} → saved`);
|
|
181
|
+
if (idx !== -1) {
|
|
182
|
+
config.workspace[idx].checksum_at_pull = res.newChecksum;
|
|
183
|
+
config.workspace[idx].tenant_id = res.newRow.tenantId;
|
|
184
|
+
config.workspace[idx].description = res.newRow.description;
|
|
185
|
+
config.workspace[idx].version = res.newRow.version;
|
|
186
|
+
config.workspace[idx].status = res.newRow.status;
|
|
187
|
+
}
|
|
188
|
+
// deployId is the fresh post-save id (re-resolved by name in
|
|
189
|
+
// pushLrpEntry); the pre-save id is dead once the save returns.
|
|
190
|
+
pushedLrp.push({ entry, idx, deployId: res.deployId });
|
|
148
191
|
} else {
|
|
149
192
|
const code = entry.localCode ?? '';
|
|
150
193
|
if (entry.object_type === 'phase') await updatePhase(token, ripUrl, entry.id, code);
|
|
@@ -156,10 +199,13 @@ async function push(opts = {}) {
|
|
|
156
199
|
|
|
157
200
|
console.log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
|
|
158
201
|
|
|
159
|
-
// Publish step for pushed wizards
|
|
202
|
+
// Publish/deploy step for pushed wizards and LRPs.
|
|
160
203
|
if (pushedPb.length) {
|
|
161
204
|
await handlePublish(token, pushedPb, config, opts);
|
|
162
205
|
}
|
|
206
|
+
if (pushedLrp.length) {
|
|
207
|
+
await handleDeploy(token, pushedLrp, config, opts);
|
|
208
|
+
}
|
|
163
209
|
|
|
164
210
|
writeConfig(config);
|
|
165
211
|
console.log(`\nPushed ${pushed} record(s).`);
|
|
@@ -188,4 +234,27 @@ async function handlePublish(token, pushedPb, config, opts) {
|
|
|
188
234
|
}
|
|
189
235
|
}
|
|
190
236
|
|
|
237
|
+
async function handleDeploy(token, pushedLrp, config, opts) {
|
|
238
|
+
for (const { entry, idx, deployId } of pushedLrp) {
|
|
239
|
+
let doDeploy;
|
|
240
|
+
if (opts.skipPublish) doDeploy = false;
|
|
241
|
+
else if (opts.publish) doDeploy = true;
|
|
242
|
+
else {
|
|
243
|
+
({ doDeploy } = await inquirer.prompt([
|
|
244
|
+
{
|
|
245
|
+
type: 'confirm',
|
|
246
|
+
name: 'doDeploy',
|
|
247
|
+
message: `Deploy "${entry.name}" now?`,
|
|
248
|
+
default: false,
|
|
249
|
+
},
|
|
250
|
+
]));
|
|
251
|
+
}
|
|
252
|
+
if (doDeploy) {
|
|
253
|
+
await deploy(token, deployId);
|
|
254
|
+
if (idx !== -1) config.workspace[idx].status = 'deployed';
|
|
255
|
+
console.log(` deployed ${entry.name}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
191
260
|
export { push };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { deployLrp, resolveLrpByName, saveLrp } from '../lib/lrpApi.js';
|
|
4
|
+
import { localChecksum, recompose } from '../lib/pbProject.js';
|
|
5
|
+
import { projectReader } from '../lib/pbWorkspace.js';
|
|
6
|
+
|
|
7
|
+
// Push one LRP entry: back up upstream, re-resolve the CURRENT id/tenantId by
|
|
8
|
+
// name (never trust a stored/stale id — it changes on every save/version),
|
|
9
|
+
// and PATCH the recomposed XML back. Returns a summary; deploy is a separate
|
|
10
|
+
// step (see deploy()), mirroring PB's push-then-publish split.
|
|
11
|
+
//
|
|
12
|
+
// The save bumps the version and mints a NEW id, so the pre-save id is stale
|
|
13
|
+
// the moment saveLrp returns. Both the deploy target and the post-save
|
|
14
|
+
// version/status must come from a fresh resolve-by-name AFTER the save — never
|
|
15
|
+
// from the pre-save row or the PATCH response (which does not echo the new id).
|
|
16
|
+
async function pushLrpEntry(token, entry, backupDir, backupTs) {
|
|
17
|
+
const read = projectReader(entry.path);
|
|
18
|
+
const localPayload = recompose(read);
|
|
19
|
+
const upstream = entry.upstream; // { row, payload } from push.js's classification
|
|
20
|
+
|
|
21
|
+
if (upstream) {
|
|
22
|
+
const backupPath = join(backupDir, backupTs, entry.path, 'upstream.xml');
|
|
23
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
24
|
+
writeFileSync(backupPath, upstream.payload.built_page, 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Re-resolve immediately before saving — the row captured during
|
|
28
|
+
// classification may be stale if something else saved in between.
|
|
29
|
+
const row = await resolveLrpByName(token, entry.name);
|
|
30
|
+
const description = upstream?.row?.description ?? entry.description ?? '';
|
|
31
|
+
const saved = await saveLrp(
|
|
32
|
+
token,
|
|
33
|
+
{ ...row, bpmnFileSvg: entry.svg ?? row.bpmnFileSvg },
|
|
34
|
+
localPayload.built_page,
|
|
35
|
+
description
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// The save minted a new id and bumped the version — re-resolve by name to
|
|
39
|
+
// get the fresh row. deployBpmn must target this new id (the pre-save id is
|
|
40
|
+
// now dead: deploying it 200s but activates nothing), and the workspace
|
|
41
|
+
// entry's version/status must come from here, not the stale pre-save row.
|
|
42
|
+
const deployRow = await resolveLrpByName(token, entry.name);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
newChecksum: localChecksum(read),
|
|
46
|
+
newRow: { ...deployRow, description },
|
|
47
|
+
saved,
|
|
48
|
+
deployId: deployRow.id,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function deploy(token, id) {
|
|
53
|
+
return deployLrp(token, id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { pushLrpEntry, deploy };
|
package/src/agrippa/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
pbConnect,
|
|
11
11
|
pbDisconnect,
|
|
12
12
|
pbFormat,
|
|
13
|
+
pbLint,
|
|
13
14
|
pbList,
|
|
14
15
|
pbPreview,
|
|
15
16
|
pbRemove,
|
|
@@ -41,13 +42,16 @@ program
|
|
|
41
42
|
|
|
42
43
|
program
|
|
43
44
|
.command('clone')
|
|
44
|
-
.description(
|
|
45
|
+
.description(
|
|
46
|
+
'Clone a phase, MFA, process-builder wizard, or long-running process into this workspace'
|
|
47
|
+
)
|
|
45
48
|
.option('--phase', 'Clone a phase (select a workflow)')
|
|
46
49
|
.option('--mfa', 'Clone a Model Function Access record')
|
|
47
50
|
.option('--pb', 'Clone a process-builder wizard')
|
|
51
|
+
.option('--lrp', 'Clone a long-running process')
|
|
48
52
|
.option('--id <id>', 'Record ID to clone (phase/mfa)')
|
|
49
|
-
.option('--name <
|
|
50
|
-
.option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb)')
|
|
53
|
+
.option('--name <name>', 'document_id (--pb) or process name (--lrp) to clone')
|
|
54
|
+
.option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb/lrp)')
|
|
51
55
|
.action((opts) =>
|
|
52
56
|
clone(opts).catch((err) => {
|
|
53
57
|
console.error(`Error: ${err.message}`);
|
|
@@ -67,9 +71,9 @@ program
|
|
|
67
71
|
|
|
68
72
|
program
|
|
69
73
|
.command('push')
|
|
70
|
-
.description('Push local changes to RIP / Process Builder (backs up remote first)')
|
|
71
|
-
.option('--publish', 'Auto-publish pushed
|
|
72
|
-
.option('--skip-publish', 'Skip publishing pushed wizards (no prompt)')
|
|
74
|
+
.description('Push local changes to RIP / Process Builder / LRP (backs up remote first)')
|
|
75
|
+
.option('--publish', 'Auto-publish pushed wizards and auto-deploy pushed LRPs')
|
|
76
|
+
.option('--skip-publish', 'Skip publishing/deploying pushed wizards and LRPs (no prompt)')
|
|
73
77
|
.action((opts) =>
|
|
74
78
|
push(opts).catch((err) => {
|
|
75
79
|
console.error(`Error: ${err.message}`);
|
|
@@ -107,25 +111,29 @@ program
|
|
|
107
111
|
})
|
|
108
112
|
);
|
|
109
113
|
|
|
110
|
-
// ---- pb: local editing helpers for a cloned process-builder wizard ----
|
|
114
|
+
// ---- pb: local editing helpers for a cloned process-builder wizard or LRP ----
|
|
111
115
|
const die = (err) => {
|
|
112
116
|
console.error(`Error: ${err.message}`);
|
|
113
117
|
process.exit(1);
|
|
114
118
|
};
|
|
115
119
|
const pb = program
|
|
116
120
|
.command('pb')
|
|
117
|
-
.description(
|
|
121
|
+
.description(
|
|
122
|
+
'Edit a cloned process-builder wizard or long-running process (local; run `pb format` after edits)'
|
|
123
|
+
);
|
|
118
124
|
|
|
119
125
|
pb.command('format')
|
|
120
126
|
.description('Auto-lay-out the diagram (left→right) and rewrite geometry')
|
|
121
|
-
.option('--pb <
|
|
127
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP (else single-entry / fuzzy prompt)')
|
|
122
128
|
.action((opts) => pbFormat(opts).catch(die));
|
|
123
129
|
|
|
124
130
|
pb.command('add')
|
|
125
131
|
.description('Add a node (scaffolds script/page); stub geometry, run format after')
|
|
126
132
|
.requiredOption(
|
|
127
133
|
'--type <type>',
|
|
128
|
-
'Node type: scriptTask|serviceTask|userTask|exclusiveGateway|subProcess|transaction|
|
|
134
|
+
'Node type: scriptTask|serviceTask|userTask|exclusiveGateway|subProcess|transaction|' +
|
|
135
|
+
'startEvent|endEvent|boundaryEvent|intermediateCatchEvent|intermediateThrowEvent|' +
|
|
136
|
+
'callActivity|parallelGateway|eventBasedGateway (userTask is process-builder only)'
|
|
129
137
|
)
|
|
130
138
|
.option('--name <name>', 'Node name')
|
|
131
139
|
.option('--parent <id>', 'Place inside this subProcess/transaction')
|
|
@@ -135,13 +143,13 @@ pb.command('add')
|
|
|
135
143
|
'exactly one edge must already run --from → --to)'
|
|
136
144
|
)
|
|
137
145
|
.option('--to <id>', 'Insert between two already-connected nodes: target id (requires --from)')
|
|
138
|
-
.option('--pb <
|
|
146
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
139
147
|
.action((opts) => pbAdd(opts).catch(die));
|
|
140
148
|
|
|
141
149
|
pb.command('rm')
|
|
142
150
|
.description('Remove a node, its edges, and its script/page files')
|
|
143
151
|
.requiredOption('--id <id>', 'Node id to remove')
|
|
144
|
-
.option('--pb <
|
|
152
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
145
153
|
.action((opts) => pbRemove(opts).catch(die));
|
|
146
154
|
|
|
147
155
|
pb.command('connect')
|
|
@@ -152,7 +160,7 @@ pb.command('connect')
|
|
|
152
160
|
.option('--condition <expr>', 'Condition expression, e.g. ${isAlive}')
|
|
153
161
|
.option('--condition-type <type>', 'xsi:type for the condition (default tFormalExpression)')
|
|
154
162
|
.option('--default', 'Mark this as the source gateway default flow')
|
|
155
|
-
.option('--pb <
|
|
163
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
156
164
|
.action((opts) => pbConnect(opts).catch(die));
|
|
157
165
|
|
|
158
166
|
pb.command('disconnect')
|
|
@@ -160,7 +168,7 @@ pb.command('disconnect')
|
|
|
160
168
|
.option('--id <id>', 'Edge id to remove')
|
|
161
169
|
.option('--from <id>', 'Source node id')
|
|
162
170
|
.option('--to <id>', 'Target node id')
|
|
163
|
-
.option('--pb <
|
|
171
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
164
172
|
.action((opts) => pbDisconnect(opts).catch(die));
|
|
165
173
|
|
|
166
174
|
pb.command('set-default')
|
|
@@ -168,18 +176,25 @@ pb.command('set-default')
|
|
|
168
176
|
.option('--id <id>', 'Edge id to mark default')
|
|
169
177
|
.option('--from <id>', 'Source gateway id')
|
|
170
178
|
.option('--to <id>', 'Target node id')
|
|
171
|
-
.option('--pb <
|
|
179
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
172
180
|
.action((opts) => pbSetDefault(opts).catch(die));
|
|
173
181
|
|
|
182
|
+
pb.command('lint')
|
|
183
|
+
.description(
|
|
184
|
+
'Check diagram for structural issues (edge names, incoming-edge rules, gateway rules)'
|
|
185
|
+
)
|
|
186
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
187
|
+
.action((opts) => pbLint(opts).catch(die));
|
|
188
|
+
|
|
174
189
|
pb.command('ls')
|
|
175
190
|
.description('List nodes and edges (discover ids without reading the YAML)')
|
|
176
|
-
.option('--pb <
|
|
191
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
177
192
|
.action((opts) => pbList(opts).catch(die));
|
|
178
193
|
|
|
179
194
|
pb.command('preview')
|
|
180
195
|
.description('Render the diagram to an SVG (dev check of format output)')
|
|
181
196
|
.option('--out <file>', 'Output path (default <project>/preview.svg)')
|
|
182
|
-
.option('--pb <
|
|
197
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
183
198
|
.action((opts) => pbPreview(opts).catch(die));
|
|
184
199
|
|
|
185
200
|
program.parse();
|
|
@@ -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 };
|