@waron97/prbot 3.1.4 → 3.2.1

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 +375 -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 +109 -20
  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 +291 -0
  10. package/src/agrippa/commands/pull.js +122 -16
  11. package/src/agrippa/commands/pullPb.js +54 -0
  12. package/src/agrippa/commands/push.js +112 -48
  13. package/src/agrippa/commands/pushPb.js +87 -0
  14. package/src/agrippa/commands/repair.js +3 -1
  15. package/src/agrippa/index.js +144 -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 +543 -0
  21. package/src/agrippa/lib/pbLayout.js +304 -0
  22. package/src/agrippa/lib/pbModel.js +701 -0
  23. package/src/agrippa/lib/pbPreview.js +153 -0
  24. package/src/agrippa/lib/pbProject.js +401 -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, remoteChecksumPb } 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,149 @@ 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 localSemantic = localChecksum(projectReader(entry.path));
75
+ const remoteSemantic = upstream ? remoteChecksumPb(upstream) : null;
76
+ const pullChecksum = entry.checksum_at_pull;
77
+ let status;
78
+ if (localSemantic === remoteSemantic) status = 'unchanged';
79
+ else if (pullChecksum === remoteSemantic) 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) {
61
- 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
-
91
+ if (localChecksumVal === remoteChecksum) status = 'unchanged';
92
+ else if (pullChecksum === remoteChecksum) status = 'fast-forward';
93
+ else status = 'conflict';
70
94
  return { ...entry, remoteCode, localCode, status };
71
95
  });
72
96
 
73
97
  const changed = classified.filter((e) => e.status !== 'unchanged');
74
-
75
98
  if (!changed.length) {
76
- console.log('Nothing to push — local files match the last-pulled state.');
99
+ console.log('Nothing to push — everything matches the last-pulled state.');
77
100
  return;
78
101
  }
79
102
 
80
- const selected = await selectEntries(changed, 'push (overwrites remote code)', 'push');
81
-
103
+ const selected = await selectEntries(changed, 'push (overwrites remote)', 'push');
82
104
  if (!selected.length) {
83
105
  console.log('Nothing selected. No changes made.');
84
106
  return;
85
107
  }
86
108
 
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);
109
+ const backupTs = new Date().toISOString().replace(/:/g, '-').replace('T', '_').slice(0, 19);
93
110
 
111
+ // Back up remote code for phase/mfa before overwriting (pb backs up its own
112
+ // full upstream payload inside pushPbEntry).
94
113
  for (const entry of selected) {
95
- if (entry.remoteCode !== null) {
114
+ if (entry.object_type !== 'process_builder' && entry.remoteCode != null) {
96
115
  const backupPath = join(BACKUP_DIR, backupTs, entry.path);
97
116
  mkdirSync(dirname(backupPath), { recursive: true });
98
117
  writeFileSync(backupPath, (entry.remoteCode ?? '').trim() + '\n', 'utf-8');
99
118
  }
100
119
  }
101
- console.log(`Remote backups written to ${BACKUP_DIR}/${backupTs}/`);
102
120
 
103
- // Push local code to API
121
+ const pushedPb = [];
104
122
  let pushed = 0;
105
123
  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
124
  const idx = config.workspace.findIndex(
114
- (e) => e.id === entry.id && e.object_type === entry.object_type,
125
+ (e) =>
126
+ e.object_type === entry.object_type &&
127
+ (entry.object_type === 'process_builder'
128
+ ? e.guid === entry.guid
129
+ : e.id === entry.id)
115
130
  );
116
- if (idx !== -1) {
117
- config.workspace[idx].checksum_at_pull = computeChecksum(code);
131
+
132
+ if (entry.object_type === 'process_builder') {
133
+ const res = await pushPbEntry(token, entry, BACKUP_DIR, backupTs);
134
+ const note = [
135
+ res.created && `${res.created} new page(s)`,
136
+ res.updated && `${res.updated} page(s) updated`,
137
+ ]
138
+ .filter(Boolean)
139
+ .join(', ');
140
+ console.log(` ${entry.name} → saved (draft)${note ? ` [${note}]` : ''}`);
141
+ if (idx !== -1) {
142
+ config.workspace[idx].checksum_at_pull = res.newChecksum;
143
+ if (res.newUpdatedDate) config.workspace[idx].updated_date = res.newUpdatedDate;
144
+ config.workspace[idx].status = res.newStatus || 'draft';
145
+ }
146
+ pushedPb.push({ entry, idx });
147
+ } else {
148
+ const code = entry.localCode ?? '';
149
+ if (entry.object_type === 'phase') await updatePhase(token, ripUrl, entry.id, code);
150
+ else await updateMfa(token, ripUrl, entry.id, code);
151
+ if (idx !== -1) config.workspace[idx].checksum_at_pull = computeChecksum(code);
118
152
  }
119
153
  pushed++;
120
154
  }
121
155
 
156
+ console.log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
157
+
158
+ // Publish step for pushed wizards (now in "draft").
159
+ if (pushedPb.length) {
160
+ await handlePublish(token, pushedPb, config, opts);
161
+ }
162
+
122
163
  writeConfig(config);
123
164
  console.log(`\nPushed ${pushed} record(s).`);
124
165
  }
125
166
 
167
+ async function handlePublish(token, pushedPb, config, opts) {
168
+ for (const { entry, idx } of pushedPb) {
169
+ let doPublish;
170
+ if (opts.skipPublish) doPublish = false;
171
+ else if (opts.publish) doPublish = true;
172
+ else {
173
+ ({ doPublish } = await inquirer.prompt([
174
+ {
175
+ type: 'confirm',
176
+ name: 'doPublish',
177
+ message: `Publish "${entry.name}" now?`,
178
+ default: false,
179
+ },
180
+ ]));
181
+ }
182
+ if (doPublish) {
183
+ await publish(token, entry.guid);
184
+ if (idx !== -1) config.workspace[idx].status = 'published';
185
+ console.log(` published ${entry.name}`);
186
+ }
187
+ }
188
+ }
189
+
126
190
  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,23 @@
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
+ pbLint,
14
+ pbList,
15
+ pbPreview,
16
+ pbRemove,
17
+ pbSetDefault,
18
+ } from './commands/pb.js';
19
+ import { pull } from './commands/pull.js';
20
+ import { push } from './commands/push.js';
10
21
  import { repair } from './commands/repair.js';
11
22
 
12
23
  const require = createRequire(import.meta.url);
@@ -22,40 +33,159 @@ program.name('agrippa').version(version);
22
33
  program
23
34
  .command('init')
24
35
  .description('Create agrippa.yaml workspace config in the current directory')
25
- .action(() => init().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
36
+ .action(() =>
37
+ init().catch((err) => {
38
+ console.error(`Error: ${err.message}`);
39
+ process.exit(1);
40
+ })
41
+ );
26
42
 
27
43
  program
28
44
  .command('clone')
29
- .description('Clone a phase or MFA from RIP into this workspace')
45
+ .description('Clone a phase, MFA, or process-builder wizard into this workspace')
30
46
  .option('--phase', 'Clone a phase (select a workflow)')
31
47
  .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); }));
48
+ .option('--pb', 'Clone a process-builder wizard')
49
+ .option('--id <id>', 'Record ID to clone (phase/mfa)')
50
+ .option('--name <document_id>', 'Process-builder document_id to clone (with --pb)')
51
+ .option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb)')
52
+ .action((opts) =>
53
+ clone(opts).catch((err) => {
54
+ console.error(`Error: ${err.message}`);
55
+ process.exit(1);
56
+ })
57
+ );
35
58
 
36
59
  program
37
60
  .command('pull')
38
61
  .description('Pull remote changes into local files')
39
- .action(() => pull().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
62
+ .action(() =>
63
+ pull().catch((err) => {
64
+ console.error(`Error: ${err.message}`);
65
+ process.exit(1);
66
+ })
67
+ );
40
68
 
41
69
  program
42
70
  .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); }));
71
+ .description('Push local changes to RIP / Process Builder (backs up remote first)')
72
+ .option('--publish', 'Auto-publish pushed process-builder wizards')
73
+ .option('--skip-publish', 'Skip publishing pushed wizards (no prompt)')
74
+ .action((opts) =>
75
+ push(opts).catch((err) => {
76
+ console.error(`Error: ${err.message}`);
77
+ process.exit(1);
78
+ })
79
+ );
45
80
 
46
81
  program
47
82
  .command('diff [path]')
48
83
  .description('Show differences between local files and remote code')
49
- .action((path) => diff(path).catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
84
+ .action((path) =>
85
+ diff(path).catch((err) => {
86
+ console.error(`Error: ${err.message}`);
87
+ process.exit(1);
88
+ })
89
+ );
50
90
 
51
91
  program
52
92
  .command('init-phase')
53
93
  .description('Initialize a phase with default code template and result vars')
54
- .action(() => initPhase().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
94
+ .action(() =>
95
+ initPhase().catch((err) => {
96
+ console.error(`Error: ${err.message}`);
97
+ process.exit(1);
98
+ })
99
+ );
55
100
 
56
101
  program
57
102
  .command('repair')
58
103
  .description('Remove stale workspace entries where local file no longer exists')
59
- .action(() => repair().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
104
+ .action(() =>
105
+ repair().catch((err) => {
106
+ console.error(`Error: ${err.message}`);
107
+ process.exit(1);
108
+ })
109
+ );
110
+
111
+ // ---- pb: local editing helpers for a cloned process-builder wizard ----
112
+ const die = (err) => {
113
+ console.error(`Error: ${err.message}`);
114
+ process.exit(1);
115
+ };
116
+ const pb = program
117
+ .command('pb')
118
+ .description('Edit a cloned process-builder wizard (local; run `pb format` after edits)');
119
+
120
+ pb.command('format')
121
+ .description('Auto-lay-out the diagram (left→right) and rewrite geometry')
122
+ .option('--pb <document_id>', 'Target wizard (else single-entry / fuzzy prompt)')
123
+ .action((opts) => pbFormat(opts).catch(die));
124
+
125
+ pb.command('add')
126
+ .description('Add a node (scaffolds script/page); stub geometry, run format after')
127
+ .requiredOption(
128
+ '--type <type>',
129
+ 'Node type: scriptTask|serviceTask|userTask|exclusiveGateway|subProcess|transaction|startEvent|endEvent|boundaryEvent'
130
+ )
131
+ .option('--name <name>', 'Node name')
132
+ .option('--parent <id>', 'Place inside this subProcess/transaction')
133
+ .option(
134
+ '--from <id>',
135
+ 'Insert between two already-connected nodes: source id (requires --to; ' +
136
+ 'exactly one edge must already run --from → --to)'
137
+ )
138
+ .option('--to <id>', 'Insert between two already-connected nodes: target id (requires --from)')
139
+ .option('--pb <document_id>', 'Target wizard')
140
+ .action((opts) => pbAdd(opts).catch(die));
141
+
142
+ pb.command('rm')
143
+ .description('Remove a node, its edges, and its script/page files')
144
+ .requiredOption('--id <id>', 'Node id to remove')
145
+ .option('--pb <document_id>', 'Target wizard')
146
+ .action((opts) => pbRemove(opts).catch(die));
147
+
148
+ pb.command('connect')
149
+ .description('Add a sequenceFlow between two nodes')
150
+ .requiredOption('--from <id>', 'Source node id')
151
+ .requiredOption('--to <id>', 'Target node id')
152
+ .option('--name <name>', 'Flow name (label)')
153
+ .option('--condition <expr>', 'Condition expression, e.g. ${isAlive}')
154
+ .option('--condition-type <type>', 'xsi:type for the condition (default tFormalExpression)')
155
+ .option('--default', 'Mark this as the source gateway default flow')
156
+ .option('--pb <document_id>', 'Target wizard')
157
+ .action((opts) => pbConnect(opts).catch(die));
158
+
159
+ pb.command('disconnect')
160
+ .description('Remove a sequenceFlow by id, or by --from/--to')
161
+ .option('--id <id>', 'Edge id to remove')
162
+ .option('--from <id>', 'Source node id')
163
+ .option('--to <id>', 'Target node id')
164
+ .option('--pb <document_id>', 'Target wizard')
165
+ .action((opts) => pbDisconnect(opts).catch(die));
166
+
167
+ pb.command('set-default')
168
+ .description("Mark an existing flow as the source gateway's default (by --id or --from/--to)")
169
+ .option('--id <id>', 'Edge id to mark default')
170
+ .option('--from <id>', 'Source gateway id')
171
+ .option('--to <id>', 'Target node id')
172
+ .option('--pb <document_id>', 'Target wizard')
173
+ .action((opts) => pbSetDefault(opts).catch(die));
174
+
175
+ pb.command('lint')
176
+ .description('Check diagram for structural issues (edge names, incoming-edge rules, gateway rules)')
177
+ .option('--pb <document_id>', 'Target wizard')
178
+ .action((opts) => pbLint(opts).catch(die));
179
+
180
+ pb.command('ls')
181
+ .description('List nodes and edges (discover ids without reading the YAML)')
182
+ .option('--pb <document_id>', 'Target wizard')
183
+ .action((opts) => pbList(opts).catch(die));
184
+
185
+ pb.command('preview')
186
+ .description('Render the diagram to an SVG (dev check of format output)')
187
+ .option('--out <file>', 'Output path (default <project>/preview.svg)')
188
+ .option('--pb <document_id>', 'Target wizard')
189
+ .action((opts) => pbPreview(opts).catch(die));
60
190
 
61
191
  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];