@waron97/prbot 3.1.4 → 3.2.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 (39) hide show
  1. package/README.md +50 -11
  2. package/agrippa-pb.md +350 -0
  3. package/package.json +7 -3
  4. package/src/agrippa/commands/clone.js +29 -13
  5. package/src/agrippa/commands/clonePb.js +107 -0
  6. package/src/agrippa/commands/diff.js +94 -18
  7. package/src/agrippa/commands/init.js +66 -12
  8. package/src/agrippa/commands/initPhase.js +16 -17
  9. package/src/agrippa/commands/pb.js +279 -0
  10. package/src/agrippa/commands/pull.js +119 -13
  11. package/src/agrippa/commands/pullPb.js +54 -0
  12. package/src/agrippa/commands/push.js +112 -47
  13. package/src/agrippa/commands/pushPb.js +87 -0
  14. package/src/agrippa/commands/repair.js +3 -1
  15. package/src/agrippa/index.js +138 -14
  16. package/src/agrippa/lib/api.js +17 -3
  17. package/src/agrippa/lib/checksum.js +3 -1
  18. package/src/agrippa/lib/config.js +2 -2
  19. package/src/agrippa/lib/pbApi.js +71 -0
  20. package/src/agrippa/lib/pbEdit.js +467 -0
  21. package/src/agrippa/lib/pbLayout.js +283 -0
  22. package/src/agrippa/lib/pbModel.js +698 -0
  23. package/src/agrippa/lib/pbPreview.js +151 -0
  24. package/src/agrippa/lib/pbProject.js +390 -0
  25. package/src/agrippa/lib/pbWorkspace.js +30 -0
  26. package/src/agrippa/lib/workspace.js +23 -3
  27. package/src/commands/autopr.js +5 -2
  28. package/src/commands/changelog.js +4 -1
  29. package/src/commands/export.js +3 -3
  30. package/src/commands/exportEmailTemplates.js +25 -15
  31. package/src/commands/exportImperex.js +4 -4
  32. package/src/commands/exportLrp.js +10 -7
  33. package/src/commands/exportPb.js +4 -5
  34. package/src/commands/exportWorkflow.js +27 -14
  35. package/src/commands/init.js +7 -0
  36. package/src/commands/routine.js +7 -5
  37. package/src/index.js +24 -7
  38. package/src/lib/premigrate.js +3 -3
  39. package/src/lib/updateCheck.js +5 -2
@@ -1,26 +1,27 @@
1
1
  import { mkdirSync, writeFileSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import inquirer from 'inquirer';
4
- import { readConfig, writeConfig, loadEffectiveEnv } from '../lib/config.js';
5
- import { updatePhase, updateMfa } from '../lib/api.js';
6
4
  import { getToken } from '../../lib/auth.js';
5
+ import { updateMfa, updatePhase } from '../lib/api.js';
7
6
  import { computeChecksum } from '../lib/checksum.js';
8
- import { readCodeFile, fileExists } from '../lib/workspace.js';
7
+ import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
8
+ import { getProcess } from '../lib/pbApi.js';
9
+ import { localChecksum } from '../lib/pbProject.js';
10
+ import { projectReader } from '../lib/pbWorkspace.js';
11
+ import { fileExists, readCodeFile } from '../lib/workspace.js';
9
12
  import { fetchRemoteCode, selectEntries } from './pull.js';
13
+ import { publish, pushPbEntry } from './pushPb.js';
10
14
 
11
15
  const BACKUP_DIR = '.backup';
12
16
 
13
- async function push() {
17
+ async function push(opts = {}) {
14
18
  const config = readConfig();
15
19
  loadEffectiveEnv(config);
16
20
 
17
- const ripUrl = process.env.RIP_URL;
18
- if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
19
-
20
- // Stale-file check
21
+ // Stale-entry check (a path may be a file for phase/mfa or a dir for pb).
21
22
  const stale = config.workspace.filter((e) => !fileExists(e.path));
22
23
  if (stale.length) {
23
- console.log('\nThe following tracked files no longer exist on disk:');
24
+ console.log('\nThe following tracked resources no longer exist on disk:');
24
25
  stale.forEach((e) => console.log(` - ${e.path} (${e.name})`));
25
26
  const { cleanup } = await inquirer.prompt([
26
27
  {
@@ -41,86 +42,150 @@ async function push() {
41
42
  return;
42
43
  }
43
44
 
44
- console.log('Fetching remote code...');
45
+ const hasCode = config.workspace.some(
46
+ (e) => e.object_type === 'phase' || e.object_type === 'mfa'
47
+ );
48
+ const hasPb = config.workspace.some((e) => e.object_type === 'process_builder');
49
+ const ripUrl = process.env.RIP_URL;
50
+ if (hasCode && !ripUrl)
51
+ throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
52
+ if (hasPb && !process.env.PB_URL)
53
+ throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
54
+
55
+ console.log('Fetching remote state...');
45
56
  const token = await getToken();
46
57
 
47
- const remoteCodeMap = await fetchRemoteCode(token, ripUrl, config.workspace);
58
+ const remoteCodeMap = hasCode
59
+ ? await fetchRemoteCode(token, ripUrl, config.workspace)
60
+ : new Map();
61
+ const upstreamMap = new Map();
62
+ for (const e of config.workspace.filter((x) => x.object_type === 'process_builder')) {
63
+ try {
64
+ upstreamMap.set(e.guid, await getProcess(token, e.guid));
65
+ } catch {
66
+ upstreamMap.set(e.guid, null);
67
+ }
68
+ }
48
69
 
49
- // Classify — concern here is remote work being overwritten
70
+ // Classify every entry to a status badge — concern: overwriting remote work.
50
71
  const classified = config.workspace.map((entry) => {
72
+ if (entry.object_type === 'process_builder') {
73
+ const upstream = upstreamMap.get(entry.guid) ?? null;
74
+ const local = localChecksum(projectReader(entry.path));
75
+ const localChanged = entry.checksum_at_pull !== local;
76
+ const remoteChanged = !upstream || upstream.updated_date !== entry.updated_date;
77
+ let status;
78
+ if (!localChanged && !remoteChanged) status = 'unchanged';
79
+ else if (!remoteChanged) status = 'fast-forward';
80
+ else status = 'conflict';
81
+ return { ...entry, upstream, status };
82
+ }
83
+
51
84
  const key = `${entry.object_type}:${entry.id}`;
52
85
  const remoteCode = remoteCodeMap.get(key) ?? null;
53
86
  const localCode = readCodeFile(entry.path);
54
-
55
87
  const remoteChecksum = computeChecksum(remoteCode ?? '');
56
- const localChecksum = computeChecksum(localCode ?? '');
88
+ const localChecksumVal = computeChecksum(localCode ?? '');
57
89
  const pullChecksum = entry.checksum_at_pull;
58
-
59
90
  let status;
60
- if (pullChecksum === localChecksum && pullChecksum === remoteChecksum) {
91
+ if (pullChecksum === localChecksumVal && pullChecksum === remoteChecksum)
61
92
  status = 'unchanged';
62
- } else if (pullChecksum === remoteChecksum) {
63
- // remote untouched since last pull, local has new changes
64
- status = 'fast-forward';
65
- } else {
66
- // remote changed since last pull — pushing will overwrite remote work
67
- status = 'conflict';
68
- }
69
-
93
+ else if (pullChecksum === remoteChecksum) status = 'fast-forward';
94
+ else status = 'conflict';
70
95
  return { ...entry, remoteCode, localCode, status };
71
96
  });
72
97
 
73
98
  const changed = classified.filter((e) => e.status !== 'unchanged');
74
-
75
99
  if (!changed.length) {
76
- console.log('Nothing to push — local files match the last-pulled state.');
100
+ console.log('Nothing to push — everything matches the last-pulled state.');
77
101
  return;
78
102
  }
79
103
 
80
- const selected = await selectEntries(changed, 'push (overwrites remote code)', 'push');
81
-
104
+ const selected = await selectEntries(changed, 'push (overwrites remote)', 'push');
82
105
  if (!selected.length) {
83
106
  console.log('Nothing selected. No changes made.');
84
107
  return;
85
108
  }
86
109
 
87
- // Write backups of remote code before overwriting
88
- const backupTs = new Date()
89
- .toISOString()
90
- .replace(/:/g, '-')
91
- .replace('T', '_')
92
- .slice(0, 19);
110
+ const backupTs = new Date().toISOString().replace(/:/g, '-').replace('T', '_').slice(0, 19);
93
111
 
112
+ // Back up remote code for phase/mfa before overwriting (pb backs up its own
113
+ // full upstream payload inside pushPbEntry).
94
114
  for (const entry of selected) {
95
- if (entry.remoteCode !== null) {
115
+ if (entry.object_type !== 'process_builder' && entry.remoteCode != null) {
96
116
  const backupPath = join(BACKUP_DIR, backupTs, entry.path);
97
117
  mkdirSync(dirname(backupPath), { recursive: true });
98
118
  writeFileSync(backupPath, (entry.remoteCode ?? '').trim() + '\n', 'utf-8');
99
119
  }
100
120
  }
101
- console.log(`Remote backups written to ${BACKUP_DIR}/${backupTs}/`);
102
121
 
103
- // Push local code to API
122
+ const pushedPb = [];
104
123
  let pushed = 0;
105
124
  for (const entry of selected) {
106
- const code = entry.localCode ?? '';
107
- if (entry.object_type === 'phase') {
108
- await updatePhase(token, ripUrl, entry.id, code);
109
- } else {
110
- await updateMfa(token, ripUrl, entry.id, code);
111
- }
112
-
113
125
  const idx = config.workspace.findIndex(
114
- (e) => e.id === entry.id && e.object_type === entry.object_type,
126
+ (e) =>
127
+ e.object_type === entry.object_type &&
128
+ (entry.object_type === 'process_builder'
129
+ ? e.guid === entry.guid
130
+ : e.id === entry.id)
115
131
  );
116
- if (idx !== -1) {
117
- config.workspace[idx].checksum_at_pull = computeChecksum(code);
132
+
133
+ if (entry.object_type === 'process_builder') {
134
+ const res = await pushPbEntry(token, entry, BACKUP_DIR, backupTs);
135
+ const note = [
136
+ res.created && `${res.created} new page(s)`,
137
+ res.updated && `${res.updated} page(s) updated`,
138
+ ]
139
+ .filter(Boolean)
140
+ .join(', ');
141
+ console.log(` ${entry.name} → saved (draft)${note ? ` [${note}]` : ''}`);
142
+ if (idx !== -1) {
143
+ config.workspace[idx].checksum_at_pull = res.newChecksum;
144
+ if (res.newUpdatedDate) config.workspace[idx].updated_date = res.newUpdatedDate;
145
+ config.workspace[idx].status = res.newStatus || 'draft';
146
+ }
147
+ pushedPb.push({ entry, idx });
148
+ } else {
149
+ const code = entry.localCode ?? '';
150
+ if (entry.object_type === 'phase') await updatePhase(token, ripUrl, entry.id, code);
151
+ else await updateMfa(token, ripUrl, entry.id, code);
152
+ if (idx !== -1) config.workspace[idx].checksum_at_pull = computeChecksum(code);
118
153
  }
119
154
  pushed++;
120
155
  }
121
156
 
157
+ console.log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
158
+
159
+ // Publish step for pushed wizards (now in "draft").
160
+ if (pushedPb.length) {
161
+ await handlePublish(token, pushedPb, config, opts);
162
+ }
163
+
122
164
  writeConfig(config);
123
165
  console.log(`\nPushed ${pushed} record(s).`);
124
166
  }
125
167
 
168
+ async function handlePublish(token, pushedPb, config, opts) {
169
+ for (const { entry, idx } of pushedPb) {
170
+ let doPublish;
171
+ if (opts.skipPublish) doPublish = false;
172
+ else if (opts.publish) doPublish = true;
173
+ else {
174
+ ({ doPublish } = await inquirer.prompt([
175
+ {
176
+ type: 'confirm',
177
+ name: 'doPublish',
178
+ message: `Publish "${entry.name}" now?`,
179
+ default: false,
180
+ },
181
+ ]));
182
+ }
183
+ if (doPublish) {
184
+ await publish(token, entry.guid);
185
+ if (idx !== -1) config.workspace[idx].status = 'published';
186
+ console.log(` published ${entry.name}`);
187
+ }
188
+ }
189
+ }
190
+
126
191
  export { push };
@@ -0,0 +1,87 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { createPage, publishProcess, updatePage, updateProcess } from '../lib/pbApi.js';
4
+ import {
5
+ enumeratePages,
6
+ localChecksum,
7
+ MANIFEST_FILE,
8
+ recompose,
9
+ stableStringify,
10
+ } from '../lib/pbProject.js';
11
+ import { listPageFiles, projectReader } from '../lib/pbWorkspace.js';
12
+
13
+ function stepkeyOf(wrapper) {
14
+ return wrapper?.page?._id?.stepkey ?? wrapper?.name;
15
+ }
16
+
17
+ // Persist a newly-created page's guid into the local manifest so future pushes
18
+ // treat it as an existing page (PATCH, not POST).
19
+ function recordPageGuid(projectPath, stepkey, file, createdWrapper) {
20
+ const manifestPath = join(projectPath, MANIFEST_FILE);
21
+ const read = projectReader(projectPath);
22
+ const manifest = JSON.parse(read(MANIFEST_FILE));
23
+ manifest.pages = manifest.pages || {};
24
+ const wrapper = { ...createdWrapper };
25
+ delete wrapper.page;
26
+ manifest.pages[stepkey] = { file, wrapper };
27
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
28
+ }
29
+
30
+ // Push one process_builder entry: backup upstream, sync pages, PATCH the wizard.
31
+ // `entry.upstream` is the upstream payload fetched during classification.
32
+ // Returns a summary; the wizard is left in "draft".
33
+ async function pushPbEntry(token, entry, backupDir, backupTs) {
34
+ const read = projectReader(entry.path);
35
+ const localPayload = recompose(read);
36
+ const upstream = entry.upstream;
37
+
38
+ // 1. Backup the full upstream payload before applying edits.
39
+ if (upstream) {
40
+ const backupPath = join(backupDir, backupTs, entry.path, 'upstream.json');
41
+ mkdirSync(dirname(backupPath), { recursive: true });
42
+ writeFileSync(backupPath, JSON.stringify(upstream, null, 2), 'utf-8');
43
+ }
44
+
45
+ // 2. Page sync (independent of the wizard save, mirroring the UI page popup).
46
+ const localPages = enumeratePages(read, listPageFiles(entry.path));
47
+ const upstreamByStep = new Map((upstream?.pages || []).map((w) => [stepkeyOf(w), w]));
48
+
49
+ let created = 0;
50
+ let updated = 0;
51
+ const pagesForPatch = [];
52
+ for (const lp of localPages) {
53
+ const body = { name: lp.stepkey, page: lp.page };
54
+ if (lp.guid) {
55
+ const up = upstreamByStep.get(lp.stepkey);
56
+ if (!up || stableStringify(up.page) !== stableStringify(lp.page)) {
57
+ await updatePage(token, entry.guid, lp.guid, body);
58
+ updated++;
59
+ }
60
+ pagesForPatch.push({ ...(lp.wrapper || {}), name: lp.stepkey, page: lp.page });
61
+ } else {
62
+ const createdWrapper = (await createPage(token, entry.guid, body)) || {};
63
+ recordPageGuid(entry.path, lp.stepkey, lp.file, createdWrapper);
64
+ pagesForPatch.push({ ...createdWrapper, name: lp.stepkey, page: lp.page });
65
+ created++;
66
+ }
67
+ }
68
+ // Ensure the wizard PATCH body carries every current page (incl. new ones).
69
+ localPayload.pages = pagesForPatch;
70
+
71
+ // 3. Save the whole wizard (blocks/structure/scalars). Wizard -> draft.
72
+ const saved = await updateProcess(token, entry.guid, localPayload);
73
+
74
+ return {
75
+ created,
76
+ updated,
77
+ newChecksum: localChecksum(read),
78
+ newUpdatedDate: saved?.updated_date,
79
+ newStatus: saved?.status,
80
+ };
81
+ }
82
+
83
+ function publish(token, guid) {
84
+ return publishProcess(token, guid);
85
+ }
86
+
87
+ export { pushPbEntry, publish };
@@ -18,7 +18,9 @@ async function repair() {
18
18
  }
19
19
 
20
20
  writeConfig(config);
21
- console.log(`Removed ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'} (${before} → ${config.workspace.length}).`);
21
+ console.log(
22
+ `Removed ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'} (${before} → ${config.workspace.length}).`
23
+ );
22
24
  }
23
25
 
24
26
  export { repair };
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
3
  import { program } from 'commander';
4
- import { init } from './commands/init.js';
5
4
  import { clone } from './commands/clone.js';
6
- import { pull } from './commands/pull.js';
7
- import { push } from './commands/push.js';
8
5
  import { diff } from './commands/diff.js';
6
+ import { init } from './commands/init.js';
9
7
  import { initPhase } from './commands/initPhase.js';
8
+ import {
9
+ pbAdd,
10
+ pbConnect,
11
+ pbDisconnect,
12
+ pbFormat,
13
+ pbList,
14
+ pbPreview,
15
+ pbRemove,
16
+ pbSetDefault,
17
+ } from './commands/pb.js';
18
+ import { pull } from './commands/pull.js';
19
+ import { push } from './commands/push.js';
10
20
  import { repair } from './commands/repair.js';
11
21
 
12
22
  const require = createRequire(import.meta.url);
@@ -22,40 +32,154 @@ program.name('agrippa').version(version);
22
32
  program
23
33
  .command('init')
24
34
  .description('Create agrippa.yaml workspace config in the current directory')
25
- .action(() => init().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
35
+ .action(() =>
36
+ init().catch((err) => {
37
+ console.error(`Error: ${err.message}`);
38
+ process.exit(1);
39
+ })
40
+ );
26
41
 
27
42
  program
28
43
  .command('clone')
29
- .description('Clone a phase or MFA from RIP into this workspace')
44
+ .description('Clone a phase, MFA, or process-builder wizard into this workspace')
30
45
  .option('--phase', 'Clone a phase (select a workflow)')
31
46
  .option('--mfa', 'Clone a Model Function Access record')
32
- .option('--id <id>', 'Record ID to clone')
33
- .option('--path <path>', 'Destination path (file for MFA, base dir for workflow)')
34
- .action((opts) => clone(opts).catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
47
+ .option('--pb', 'Clone a process-builder wizard')
48
+ .option('--id <id>', 'Record ID to clone (phase/mfa)')
49
+ .option('--name <document_id>', 'Process-builder document_id to clone (with --pb)')
50
+ .option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb)')
51
+ .action((opts) =>
52
+ clone(opts).catch((err) => {
53
+ console.error(`Error: ${err.message}`);
54
+ process.exit(1);
55
+ })
56
+ );
35
57
 
36
58
  program
37
59
  .command('pull')
38
60
  .description('Pull remote changes into local files')
39
- .action(() => pull().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
61
+ .action(() =>
62
+ pull().catch((err) => {
63
+ console.error(`Error: ${err.message}`);
64
+ process.exit(1);
65
+ })
66
+ );
40
67
 
41
68
  program
42
69
  .command('push')
43
- .description('Push local changes to RIP (backs up remote code first)')
44
- .action(() => push().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
70
+ .description('Push local changes to RIP / Process Builder (backs up remote first)')
71
+ .option('--publish', 'Auto-publish pushed process-builder wizards')
72
+ .option('--skip-publish', 'Skip publishing pushed wizards (no prompt)')
73
+ .action((opts) =>
74
+ push(opts).catch((err) => {
75
+ console.error(`Error: ${err.message}`);
76
+ process.exit(1);
77
+ })
78
+ );
45
79
 
46
80
  program
47
81
  .command('diff [path]')
48
82
  .description('Show differences between local files and remote code')
49
- .action((path) => diff(path).catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
83
+ .action((path) =>
84
+ diff(path).catch((err) => {
85
+ console.error(`Error: ${err.message}`);
86
+ process.exit(1);
87
+ })
88
+ );
50
89
 
51
90
  program
52
91
  .command('init-phase')
53
92
  .description('Initialize a phase with default code template and result vars')
54
- .action(() => initPhase().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
93
+ .action(() =>
94
+ initPhase().catch((err) => {
95
+ console.error(`Error: ${err.message}`);
96
+ process.exit(1);
97
+ })
98
+ );
55
99
 
56
100
  program
57
101
  .command('repair')
58
102
  .description('Remove stale workspace entries where local file no longer exists')
59
- .action(() => repair().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
103
+ .action(() =>
104
+ repair().catch((err) => {
105
+ console.error(`Error: ${err.message}`);
106
+ process.exit(1);
107
+ })
108
+ );
109
+
110
+ // ---- pb: local editing helpers for a cloned process-builder wizard ----
111
+ const die = (err) => {
112
+ console.error(`Error: ${err.message}`);
113
+ process.exit(1);
114
+ };
115
+ const pb = program
116
+ .command('pb')
117
+ .description('Edit a cloned process-builder wizard (local; run `pb format` after edits)');
118
+
119
+ pb.command('format')
120
+ .description('Auto-lay-out the diagram (left→right) and rewrite geometry')
121
+ .option('--pb <document_id>', 'Target wizard (else single-entry / fuzzy prompt)')
122
+ .action((opts) => pbFormat(opts).catch(die));
123
+
124
+ pb.command('add')
125
+ .description('Add a node (scaffolds script/page); stub geometry, run format after')
126
+ .requiredOption(
127
+ '--type <type>',
128
+ 'Node type: scriptTask|serviceTask|userTask|exclusiveGateway|subProcess|transaction|startEvent|endEvent|boundaryEvent'
129
+ )
130
+ .option('--name <name>', 'Node name')
131
+ .option('--parent <id>', 'Place inside this subProcess/transaction')
132
+ .option(
133
+ '--from <id>',
134
+ 'Insert between two already-connected nodes: source id (requires --to; ' +
135
+ 'exactly one edge must already run --from → --to)'
136
+ )
137
+ .option('--to <id>', 'Insert between two already-connected nodes: target id (requires --from)')
138
+ .option('--pb <document_id>', 'Target wizard')
139
+ .action((opts) => pbAdd(opts).catch(die));
140
+
141
+ pb.command('rm')
142
+ .description('Remove a node, its edges, and its script/page files')
143
+ .requiredOption('--id <id>', 'Node id to remove')
144
+ .option('--pb <document_id>', 'Target wizard')
145
+ .action((opts) => pbRemove(opts).catch(die));
146
+
147
+ pb.command('connect')
148
+ .description('Add a sequenceFlow between two nodes')
149
+ .requiredOption('--from <id>', 'Source node id')
150
+ .requiredOption('--to <id>', 'Target node id')
151
+ .option('--name <name>', 'Flow name (label)')
152
+ .option('--condition <expr>', 'Condition expression, e.g. ${isAlive}')
153
+ .option('--condition-type <type>', 'xsi:type for the condition (default tFormalExpression)')
154
+ .option('--default', 'Mark this as the source gateway default flow')
155
+ .option('--pb <document_id>', 'Target wizard')
156
+ .action((opts) => pbConnect(opts).catch(die));
157
+
158
+ pb.command('disconnect')
159
+ .description('Remove a sequenceFlow by id, or by --from/--to')
160
+ .option('--id <id>', 'Edge id to remove')
161
+ .option('--from <id>', 'Source node id')
162
+ .option('--to <id>', 'Target node id')
163
+ .option('--pb <document_id>', 'Target wizard')
164
+ .action((opts) => pbDisconnect(opts).catch(die));
165
+
166
+ pb.command('set-default')
167
+ .description("Mark an existing flow as the source gateway's default (by --id or --from/--to)")
168
+ .option('--id <id>', 'Edge id to mark default')
169
+ .option('--from <id>', 'Source gateway id')
170
+ .option('--to <id>', 'Target node id')
171
+ .option('--pb <document_id>', 'Target wizard')
172
+ .action((opts) => pbSetDefault(opts).catch(die));
173
+
174
+ pb.command('ls')
175
+ .description('List nodes and edges (discover ids without reading the YAML)')
176
+ .option('--pb <document_id>', 'Target wizard')
177
+ .action((opts) => pbList(opts).catch(die));
178
+
179
+ pb.command('preview')
180
+ .description('Render the diagram to an SVG (dev check of format output)')
181
+ .option('--out <file>', 'Output path (default <project>/preview.svg)')
182
+ .option('--pb <document_id>', 'Target wizard')
183
+ .action((opts) => pbPreview(opts).catch(die));
60
184
 
61
185
  program.parse();
@@ -32,7 +32,7 @@ function getPhasesByIds(token, ripUrl, ids) {
32
32
  'GET',
33
33
  `/symple.triplet.phase/*?_filter_=[('id', 'in', [${ids.join(',')}])]`,
34
34
  token,
35
- ripUrl,
35
+ ripUrl
36
36
  );
37
37
  }
38
38
 
@@ -44,6 +44,19 @@ function listMfas(token, ripUrl) {
44
44
  return makeRequest('GET', '/symple.workflow/get_mfas', token, ripUrl);
45
45
  }
46
46
 
47
+ // Structure (graph) of a workflow: phases, results/edges, processes, triplets.
48
+ // Backed by the `agrippa_describe_workflow` MFA on helpdesk.ticket. Returns no
49
+ // phase code -- the editable phase code already lives on disk as .py files.
50
+ // Registered as a GET MFA, so workflow_id rides the query string.
51
+ function describeWorkflow(token, ripUrl, workflowId) {
52
+ return makeRequest(
53
+ 'GET',
54
+ `/helpdesk.ticket/agrippa_describe_workflow?workflow_id=${workflowId}`,
55
+ token,
56
+ ripUrl
57
+ );
58
+ }
59
+
47
60
  function updateMfa(token, ripUrl, mfaId, code) {
48
61
  return makeRequest('POST', '/symple.workflow/update_mfa', token, ripUrl, { id: mfaId, code });
49
62
  }
@@ -55,7 +68,7 @@ async function getPhaseResults(token, ripUrl, ids) {
55
68
  'GET',
56
69
  `/symple.triplet.phase.result/*?_filter_=[('id', 'in', [${ids.join(',')}])]`,
57
70
  token,
58
- ripUrl,
71
+ ripUrl
59
72
  );
60
73
  } catch (err) {
61
74
  if (err.message.includes('404')) return [];
@@ -76,7 +89,7 @@ async function getPhaseConfigurators(token, ripUrl, phaseId) {
76
89
  'GET',
77
90
  `/result.code.configurator/*?_filter_=[('code_phase_id', '=', ${phaseId})]`,
78
91
  token,
79
- ripUrl,
92
+ ripUrl
80
93
  );
81
94
  } catch (err) {
82
95
  if (err.message.includes('404')) return [];
@@ -98,6 +111,7 @@ export {
98
111
  getPhasesByIds,
99
112
  updatePhase,
100
113
  listMfas,
114
+ describeWorkflow,
101
115
  updateMfa,
102
116
  getPhaseResults,
103
117
  initPhaseRemote,
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'crypto';
2
2
 
3
3
  function computeChecksum(code) {
4
- return createHash('md5').update((code ?? '').trim()).digest('hex');
4
+ return createHash('md5')
5
+ .update((code ?? '').trim())
6
+ .digest('hex');
5
7
  }
6
8
 
7
9
  export { computeChecksum };
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'fs';
2
- import { parse, stringify } from 'yaml';
3
2
  import { configDotenv } from 'dotenv';
3
+ import { parse, stringify } from 'yaml';
4
4
  import { CONFIG_FILE } from '../../config.js';
5
5
 
6
6
  const WORKSPACE_FILE = 'agrippa.yaml';
@@ -24,7 +24,7 @@ function loadEffectiveEnv(localConfig) {
24
24
 
25
25
  // Overlay workspace-level overrides from agrippa.yaml's agrippa: section
26
26
  const overrides = localConfig?.agrippa ?? {};
27
- const keys = ['KC_URL', 'KC_USER', 'KC_PASSWORD', 'KC_ID', 'KC_SECRET', 'RIP_URL'];
27
+ const keys = ['KC_URL', 'KC_USER', 'KC_PASSWORD', 'KC_ID', 'KC_SECRET', 'RIP_URL', 'PB_URL'];
28
28
  for (const key of keys) {
29
29
  if (overrides[key]) {
30
30
  process.env[key] = overrides[key];
@@ -0,0 +1,71 @@
1
+ // Process-builder API client. Base URL = PB_URL, e.g.
2
+ // https://sorgenia-test-02.symple.cloud/api/processbuilder/v1
3
+ // Auth uses the same Keycloak bearer as import-export (getToken()).
4
+
5
+ import fetch from 'node-fetch';
6
+
7
+ function pbBase() {
8
+ const base = process.env.PB_URL;
9
+ if (!base) {
10
+ throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
11
+ }
12
+ return base.replace(/\/+$/, '');
13
+ }
14
+
15
+ async function pbRequest(method, path, token, body) {
16
+ const hasBody = body !== undefined;
17
+ const res = await fetch(`${pbBase()}${path}`, {
18
+ method,
19
+ headers: {
20
+ Authorization: `Bearer ${token}`,
21
+ ...(hasBody ? { 'Content-Type': 'application/json' } : {}),
22
+ },
23
+ body: hasBody ? JSON.stringify(body) : undefined,
24
+ });
25
+ if (!res.ok) {
26
+ throw new Error(`PB API ${method} ${path} failed with ${res.status}: ${await res.text()}`);
27
+ }
28
+ const text = await res.text();
29
+ return text ? JSON.parse(text) : null;
30
+ }
31
+
32
+ // List processes. The endpoint returns full objects; we keep only the
33
+ // identity fields needed for selection.
34
+ async function listProcesses(token) {
35
+ const data = await pbRequest('GET', '/builder/process', token);
36
+ const arr = Array.isArray(data) ? data : (data.data ?? data.results ?? []);
37
+ return arr.map((p) => ({
38
+ guid: p.guid,
39
+ document_id: p.document_id,
40
+ process_name: p.process_name,
41
+ status: p.status,
42
+ version: p.version,
43
+ }));
44
+ }
45
+
46
+ // Fetch the full payload for one process by guid.
47
+ function getProcess(token, guid) {
48
+ return pbRequest('GET', `/builder/process/${guid}`, token);
49
+ }
50
+
51
+ // Save the whole wizard (blocks/structure/scalars). Body = full payload. Wizard → draft.
52
+ function updateProcess(token, guid, payload) {
53
+ return pbRequest('PATCH', `/builder/process/${guid}`, token, payload);
54
+ }
55
+
56
+ // Create a new user-task page. Body = { name, page }. Returns the created page (with guid).
57
+ function createPage(token, guid, body) {
58
+ return pbRequest('POST', `/builder/process/${guid}/page`, token, body);
59
+ }
60
+
61
+ // Update an existing user-task page. Body = { name, page }.
62
+ function updatePage(token, guid, pageGuid, body) {
63
+ return pbRequest('PATCH', `/builder/process/${guid}/page/${pageGuid}`, token, body);
64
+ }
65
+
66
+ // Publish the wizard so live consumers see the latest saved state.
67
+ function publishProcess(token, guid) {
68
+ return pbRequest('POST', `/builder/process/publish/${guid}`, token, null);
69
+ }
70
+
71
+ export { listProcesses, getProcess, updateProcess, createPage, updatePage, publishProcess };