@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.
- package/README.md +50 -11
- package/agrippa-pb.md +350 -0
- package/package.json +7 -3
- package/src/agrippa/commands/clone.js +29 -13
- package/src/agrippa/commands/clonePb.js +107 -0
- package/src/agrippa/commands/diff.js +94 -18
- package/src/agrippa/commands/init.js +66 -12
- package/src/agrippa/commands/initPhase.js +16 -17
- package/src/agrippa/commands/pb.js +279 -0
- package/src/agrippa/commands/pull.js +119 -13
- package/src/agrippa/commands/pullPb.js +54 -0
- package/src/agrippa/commands/push.js +112 -47
- package/src/agrippa/commands/pushPb.js +87 -0
- package/src/agrippa/commands/repair.js +3 -1
- package/src/agrippa/index.js +138 -14
- package/src/agrippa/lib/api.js +17 -3
- package/src/agrippa/lib/checksum.js +3 -1
- package/src/agrippa/lib/config.js +2 -2
- package/src/agrippa/lib/pbApi.js +71 -0
- package/src/agrippa/lib/pbEdit.js +467 -0
- package/src/agrippa/lib/pbLayout.js +283 -0
- package/src/agrippa/lib/pbModel.js +698 -0
- package/src/agrippa/lib/pbPreview.js +151 -0
- package/src/agrippa/lib/pbProject.js +390 -0
- package/src/agrippa/lib/pbWorkspace.js +30 -0
- package/src/agrippa/lib/workspace.js +23 -3
- package/src/commands/autopr.js +5 -2
- package/src/commands/changelog.js +4 -1
- package/src/commands/export.js +3 -3
- package/src/commands/exportEmailTemplates.js +25 -15
- package/src/commands/exportImperex.js +4 -4
- package/src/commands/exportLrp.js +10 -7
- package/src/commands/exportPb.js +4 -5
- package/src/commands/exportWorkflow.js +27 -14
- package/src/commands/init.js +7 -0
- package/src/commands/routine.js +7 -5
- package/src/index.js +24 -7
- package/src/lib/premigrate.js +3 -3
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
88
|
+
const localChecksumVal = computeChecksum(localCode ?? '');
|
|
57
89
|
const pullChecksum = entry.checksum_at_pull;
|
|
58
|
-
|
|
59
90
|
let status;
|
|
60
|
-
if (pullChecksum ===
|
|
91
|
+
if (pullChecksum === localChecksumVal && pullChecksum === remoteChecksum)
|
|
61
92
|
status = 'unchanged';
|
|
62
|
-
|
|
63
|
-
|
|
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 —
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
117
|
-
|
|
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(
|
|
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 };
|
package/src/agrippa/index.js
CHANGED
|
@@ -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(() =>
|
|
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
|
|
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('--
|
|
33
|
-
.option('--
|
|
34
|
-
.
|
|
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(() =>
|
|
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
|
|
44
|
-
.
|
|
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) =>
|
|
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(() =>
|
|
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(() =>
|
|
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();
|
package/src/agrippa/lib/api.js
CHANGED
|
@@ -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,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 };
|