@waron97/prbot 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -18
- package/agrippa-pb.md +141 -68
- package/package.json +5 -1
- package/src/agrippa/commands/clone.js +17 -10
- package/src/agrippa/commands/cloneLrp.js +114 -0
- package/src/agrippa/commands/clonePb.js +10 -10
- package/src/agrippa/commands/diff.js +9 -8
- package/src/agrippa/commands/init.js +14 -13
- package/src/agrippa/commands/initPhase.js +51 -42
- package/src/agrippa/commands/pb.js +71 -39
- package/src/agrippa/commands/pull.js +137 -42
- package/src/agrippa/commands/pullLrp.js +54 -0
- package/src/agrippa/commands/pullPb.js +2 -3
- package/src/agrippa/commands/push.js +112 -33
- package/src/agrippa/commands/pushLrp.js +56 -0
- package/src/agrippa/commands/repair.js +4 -3
- package/src/agrippa/index.js +48 -30
- package/src/agrippa/lib/lrpApi.js +153 -0
- package/src/agrippa/lib/pbEdit.js +31 -10
- package/src/agrippa/lib/pbLayout.js +8 -1
- package/src/agrippa/lib/pbModel.js +318 -86
- package/src/agrippa/lib/pbPreview.js +6 -2
- package/src/agrippa/lib/pbProject.js +93 -7
- package/src/agrippa/lib/pbScriptTemplate.js +29 -0
- package/src/commands/autopr.js +21 -22
- package/src/commands/changelog.js +4 -3
- package/src/commands/commit.js +11 -10
- package/src/commands/export.js +2 -1
- package/src/commands/exportPb.js +19 -2
- package/src/commands/init.js +2 -1
- package/src/commands/routine.js +19 -8
- package/src/index.js +137 -112
- package/src/lib/auth.js +19 -1
- package/src/lib/logger.js +12 -1
|
@@ -18,6 +18,7 @@ import { dirname, join } from 'path';
|
|
|
18
18
|
import search from '@inquirer/search';
|
|
19
19
|
import { parse as yamlParse } from 'yaml';
|
|
20
20
|
import { fuzzyMatch } from '../../lib/fuzzy.js';
|
|
21
|
+
import { log, warn } from '../../lib/logger.js';
|
|
21
22
|
import { readConfig } from '../lib/config.js';
|
|
22
23
|
import {
|
|
23
24
|
addNode,
|
|
@@ -37,32 +38,49 @@ import { projectReader } from '../lib/pbWorkspace.js';
|
|
|
37
38
|
|
|
38
39
|
// ---------- project resolution ----------
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// Resolves against both process-builder wizards and long-running processes —
|
|
42
|
+
// `structure.yaml`-level editing is identical for both (see pbEdit.js). PBs
|
|
43
|
+
// select by --pb/--name matched against document_id; LRPs have no document_id
|
|
44
|
+
// so the same flag matches against name instead.
|
|
45
|
+
async function resolveProjectEntry(opts) {
|
|
41
46
|
const config = readConfig();
|
|
42
|
-
const entries = (config.workspace || []).filter(
|
|
47
|
+
const entries = (config.workspace || []).filter(
|
|
48
|
+
(e) => e.object_type === 'process_builder' || e.object_type === 'long_running_process'
|
|
49
|
+
);
|
|
43
50
|
if (!entries.length) {
|
|
44
51
|
throw new Error(
|
|
45
|
-
'No process-builder wizards in this workspace.
|
|
52
|
+
'No process-builder wizards or long-running processes in this workspace. ' +
|
|
53
|
+
'Clone one with `agrippa clone --pb` or `agrippa clone --lrp`.'
|
|
46
54
|
);
|
|
47
55
|
}
|
|
48
56
|
const sel = opts.pb || opts.name;
|
|
49
57
|
if (sel) {
|
|
50
|
-
const entry = entries.find((e) => e.document_id === sel);
|
|
51
|
-
if (!entry) throw new Error(`No cloned
|
|
52
|
-
return entry
|
|
58
|
+
const entry = entries.find((e) => e.document_id === sel || e.name === sel);
|
|
59
|
+
if (!entry) throw new Error(`No cloned project with document_id/name "${sel}"`);
|
|
60
|
+
return entry;
|
|
53
61
|
}
|
|
54
|
-
if (entries.length === 1) return entries[0]
|
|
62
|
+
if (entries.length === 1) return entries[0];
|
|
55
63
|
const entry = await search({
|
|
56
|
-
message: 'Select a cloned
|
|
64
|
+
message: 'Select a cloned project:',
|
|
57
65
|
source: (input) => {
|
|
58
66
|
const list = input
|
|
59
67
|
? entries.filter(
|
|
60
|
-
(e) =>
|
|
68
|
+
(e) =>
|
|
69
|
+
fuzzyMatch(e.name, input) ||
|
|
70
|
+
(e.document_id && fuzzyMatch(e.document_id, input))
|
|
61
71
|
)
|
|
62
72
|
: entries;
|
|
63
|
-
return list.map((e) => ({
|
|
73
|
+
return list.map((e) => ({
|
|
74
|
+
name: e.document_id ? `${e.name} (${e.document_id})` : e.name,
|
|
75
|
+
value: e,
|
|
76
|
+
}));
|
|
64
77
|
},
|
|
65
78
|
});
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveProjectPath(opts) {
|
|
83
|
+
const entry = await resolveProjectEntry(opts);
|
|
66
84
|
return entry.path;
|
|
67
85
|
}
|
|
68
86
|
|
|
@@ -103,7 +121,7 @@ function validate(dir) {
|
|
|
103
121
|
try {
|
|
104
122
|
recompose(projectReader(dir));
|
|
105
123
|
} catch (e) {
|
|
106
|
-
|
|
124
|
+
warn(`WARNING: project no longer recomposes cleanly: ${e.message}`);
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -122,27 +140,33 @@ async function pbFormat(opts) {
|
|
|
122
140
|
if (!n.layout) missing++;
|
|
123
141
|
});
|
|
124
142
|
validate(dir);
|
|
125
|
-
|
|
143
|
+
log(
|
|
126
144
|
`Formatted ${dir} (${nodes} node(s) laid out${missing ? `, ${missing} without layout` : ''}).`
|
|
127
145
|
);
|
|
128
146
|
const issues = lintAll(structure);
|
|
129
147
|
if (issues.length) {
|
|
130
|
-
|
|
131
|
-
for (const w of issues)
|
|
148
|
+
warn('Diagram issues:');
|
|
149
|
+
for (const w of issues) warn(` ! ${w}`);
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
async function pbAdd(opts) {
|
|
136
154
|
if (!opts.type)
|
|
137
155
|
throw new Error(
|
|
138
|
-
'--type is required (e.g. scriptTask, serviceTask, userTask, exclusiveGateway, subProcess, endEvent...)'
|
|
156
|
+
'--type is required (e.g. scriptTask, serviceTask, userTask, exclusiveGateway, subProcess, endEvent, callActivity...)'
|
|
139
157
|
);
|
|
140
158
|
if ((opts.from || opts.to) && !(opts.from && opts.to))
|
|
141
159
|
throw new Error('--from and --to must be used together');
|
|
142
160
|
if (opts.from && opts.parent)
|
|
143
161
|
throw new Error('--parent is implied by --from/--to; pass only one');
|
|
144
162
|
|
|
145
|
-
const
|
|
163
|
+
const projectEntry = await resolveProjectEntry(opts);
|
|
164
|
+
if (opts.type === 'userTask' && projectEntry.object_type === 'long_running_process') {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'userTask is not valid on a long-running process — LRPs have no pages (user tasks are a process-builder-only concept).'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const dir = projectEntry.path;
|
|
146
170
|
const { structure, manifest } = loadProject(dir);
|
|
147
171
|
const ctx = { existingScripts: listScriptFiles(dir), documentId: manifest.document_id };
|
|
148
172
|
|
|
@@ -157,13 +181,13 @@ async function pbAdd(opts) {
|
|
|
157
181
|
saveStructure(dir, structure);
|
|
158
182
|
saveManifest(dir, manifest);
|
|
159
183
|
validate(dir);
|
|
160
|
-
|
|
184
|
+
log(
|
|
161
185
|
`Added ${result.type} ${result.id}${result.file ? ` (${result.file})` : ''} between ${opts.from} → ${opts.to}.`
|
|
162
186
|
);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
for (const w of result.warnings || [])
|
|
166
|
-
|
|
187
|
+
log(` ${opts.from} → ${result.id} (${result.edgeId}, retargeted)`);
|
|
188
|
+
log(` ${result.id} → ${opts.to} (${result.newEdgeId})`);
|
|
189
|
+
for (const w of result.warnings || []) warn(` ! ${w}`);
|
|
190
|
+
log('Run `agrippa pb format` to lay it out.');
|
|
167
191
|
return;
|
|
168
192
|
}
|
|
169
193
|
|
|
@@ -177,10 +201,8 @@ async function pbAdd(opts) {
|
|
|
177
201
|
saveStructure(dir, structure);
|
|
178
202
|
saveManifest(dir, manifest);
|
|
179
203
|
validate(dir);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
'Connect it with `agrippa pb connect`, then run `agrippa pb format` to lay it out.'
|
|
183
|
-
);
|
|
204
|
+
log(`Added ${result.type} ${result.id}${result.file ? ` (${result.file})` : ''}.`);
|
|
205
|
+
log('Connect it with `agrippa pb connect`, then run `agrippa pb format` to lay it out.');
|
|
184
206
|
}
|
|
185
207
|
|
|
186
208
|
async function pbRemove(opts) {
|
|
@@ -192,7 +214,7 @@ async function pbRemove(opts) {
|
|
|
192
214
|
saveStructure(dir, structure);
|
|
193
215
|
saveManifest(dir, manifest);
|
|
194
216
|
validate(dir);
|
|
195
|
-
|
|
217
|
+
log(
|
|
196
218
|
`Removed ${result.removed.length} node(s) [${result.removed.join(', ')}], ` +
|
|
197
219
|
`${result.removedEdges} dangling edge(s), ${deletes.length} file(s).`
|
|
198
220
|
);
|
|
@@ -212,11 +234,11 @@ async function pbConnect(opts) {
|
|
|
212
234
|
});
|
|
213
235
|
saveStructure(dir, structure);
|
|
214
236
|
validate(dir);
|
|
215
|
-
|
|
237
|
+
log(
|
|
216
238
|
`Connected ${result.from} → ${result.to} (${result.id})${opts.default ? ' [default]' : ''}.`
|
|
217
239
|
);
|
|
218
|
-
for (const w of result.warnings || [])
|
|
219
|
-
|
|
240
|
+
for (const w of result.warnings || []) warn(` ! ${w}`);
|
|
241
|
+
log('Run `agrippa pb format` to route it.');
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
async function pbDisconnect(opts) {
|
|
@@ -227,7 +249,7 @@ async function pbDisconnect(opts) {
|
|
|
227
249
|
const { result } = disconnect(structure, { id: opts.id, from: opts.from, to: opts.to });
|
|
228
250
|
saveStructure(dir, structure);
|
|
229
251
|
validate(dir);
|
|
230
|
-
|
|
252
|
+
log(`Removed ${result.removed} edge(s)${result.id ? ` (${result.id})` : ''}.`);
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
async function pbSetDefault(opts) {
|
|
@@ -238,11 +260,11 @@ async function pbSetDefault(opts) {
|
|
|
238
260
|
const { result } = setDefault(structure, { id: opts.id, from: opts.from, to: opts.to });
|
|
239
261
|
saveStructure(dir, structure);
|
|
240
262
|
validate(dir);
|
|
241
|
-
|
|
263
|
+
log(
|
|
242
264
|
`Default flow on ${result.from} is now ${result.id} (→ ${result.to})` +
|
|
243
265
|
`${result.prev && result.prev !== result.id ? `, was ${result.prev}` : ''}.`
|
|
244
266
|
);
|
|
245
|
-
for (const w of result.warnings || [])
|
|
267
|
+
for (const w of result.warnings || []) warn(` ! ${w}`);
|
|
246
268
|
}
|
|
247
269
|
|
|
248
270
|
async function pbList(opts) {
|
|
@@ -252,7 +274,7 @@ async function pbList(opts) {
|
|
|
252
274
|
for (const r of rows) {
|
|
253
275
|
const where = r.parent ? ` [in ${r.parent}]` : '';
|
|
254
276
|
const label = r.name ? ` "${r.name}"` : '';
|
|
255
|
-
|
|
277
|
+
log(`${r.id} (${r.type})${label}${where}`);
|
|
256
278
|
for (const e of r.edges) {
|
|
257
279
|
const tag = [
|
|
258
280
|
e.isDefault && '[default]',
|
|
@@ -261,10 +283,10 @@ async function pbList(opts) {
|
|
|
261
283
|
]
|
|
262
284
|
.filter(Boolean)
|
|
263
285
|
.join(' ');
|
|
264
|
-
|
|
286
|
+
log(` → ${e.target} (${e.id})${tag ? ` ${tag}` : ''}`);
|
|
265
287
|
}
|
|
266
288
|
}
|
|
267
|
-
|
|
289
|
+
log(`\n${rows.length} node(s).`);
|
|
268
290
|
}
|
|
269
291
|
|
|
270
292
|
async function pbPreview(opts) {
|
|
@@ -273,7 +295,7 @@ async function pbPreview(opts) {
|
|
|
273
295
|
const svg = toSvg(structure);
|
|
274
296
|
const out = opts.out || join(dir, 'preview.svg');
|
|
275
297
|
writeFileSync(out, svg, 'utf-8');
|
|
276
|
-
|
|
298
|
+
log(`Wrote ${out} (${svg.length} bytes).`);
|
|
277
299
|
}
|
|
278
300
|
|
|
279
301
|
async function pbLint(opts) {
|
|
@@ -281,11 +303,21 @@ async function pbLint(opts) {
|
|
|
281
303
|
const { structure } = loadProject(dir);
|
|
282
304
|
const issues = lintAll(structure);
|
|
283
305
|
if (!issues.length) {
|
|
284
|
-
|
|
306
|
+
log('No issues.');
|
|
285
307
|
} else {
|
|
286
|
-
for (const w of issues)
|
|
308
|
+
for (const w of issues) warn(` ! ${w}`);
|
|
287
309
|
process.exitCode = 1;
|
|
288
310
|
}
|
|
289
311
|
}
|
|
290
312
|
|
|
291
|
-
export {
|
|
313
|
+
export {
|
|
314
|
+
pbFormat,
|
|
315
|
+
pbAdd,
|
|
316
|
+
pbRemove,
|
|
317
|
+
pbConnect,
|
|
318
|
+
pbDisconnect,
|
|
319
|
+
pbSetDefault,
|
|
320
|
+
pbList,
|
|
321
|
+
pbPreview,
|
|
322
|
+
pbLint,
|
|
323
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { dirname } from 'path';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { getToken } from '../../lib/auth.js';
|
|
4
|
+
import { log, warn } from '../../lib/logger.js';
|
|
4
5
|
import { describeWorkflow, getPhasesByIds, getPhasesByWorkflow, listMfas } from '../lib/api.js';
|
|
5
6
|
import { computeChecksum } from '../lib/checksum.js';
|
|
6
7
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
8
|
+
import { fetchUpstream } from '../lib/lrpApi.js';
|
|
7
9
|
import { getProcess } from '../lib/pbApi.js';
|
|
8
10
|
import { localChecksum, remoteChecksumPb } from '../lib/pbProject.js';
|
|
9
11
|
import { projectReader } from '../lib/pbWorkspace.js';
|
|
@@ -14,9 +16,10 @@ import {
|
|
|
14
16
|
writeCodeFile,
|
|
15
17
|
writeWorkflowDoc,
|
|
16
18
|
} from '../lib/workspace.js';
|
|
19
|
+
import { pullLrpEntry } from './pullLrp.js';
|
|
17
20
|
import { pullPbEntry } from './pullPb.js';
|
|
18
21
|
|
|
19
|
-
async function pull() {
|
|
22
|
+
async function pull(opts = {}) {
|
|
20
23
|
const config = readConfig();
|
|
21
24
|
loadEffectiveEnv(config);
|
|
22
25
|
|
|
@@ -27,19 +30,23 @@ async function pull() {
|
|
|
27
30
|
// Stale-file check
|
|
28
31
|
const stale = config.workspace.filter((e) => !fileExists(e.path));
|
|
29
32
|
if (stale.length) {
|
|
30
|
-
|
|
31
|
-
stale.forEach((e) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
log('\nThe following tracked files no longer exist on disk:');
|
|
34
|
+
stale.forEach((e) => log(` - ${e.path} (${e.name})`));
|
|
35
|
+
if (opts.nonInteractive) {
|
|
36
|
+
log(' (non-interactive: leaving them tracked; run interactively to clean up)');
|
|
37
|
+
} else {
|
|
38
|
+
const { cleanup } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'confirm',
|
|
41
|
+
name: 'cleanup',
|
|
42
|
+
message: 'Remove these entries from the workspace config?',
|
|
43
|
+
default: true,
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
if (cleanup) {
|
|
47
|
+
config.workspace = config.workspace.filter((e) => fileExists(e.path));
|
|
48
|
+
writeConfig(config);
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -50,11 +57,13 @@ async function pull() {
|
|
|
50
57
|
const changedWorkflowIds = new Set();
|
|
51
58
|
|
|
52
59
|
// ── pull existing tracked entries ─────────────────────────────────────────
|
|
53
|
-
// process_builder wizards are refreshed separately (different
|
|
54
|
-
// see pullPbEntries below.
|
|
55
|
-
const pullable = config.workspace.filter(
|
|
60
|
+
// process_builder wizards and LRPs are refreshed separately (different
|
|
61
|
+
// checksum/identity model); see pullPbEntries/pullLrpEntries below.
|
|
62
|
+
const pullable = config.workspace.filter(
|
|
63
|
+
(e) => e.object_type !== 'process_builder' && e.object_type !== 'long_running_process'
|
|
64
|
+
);
|
|
56
65
|
if (pullable.length) {
|
|
57
|
-
|
|
66
|
+
log('Fetching remote code...');
|
|
58
67
|
const remoteCodeMap = await fetchRemoteCode(token, ripUrl, pullable);
|
|
59
68
|
|
|
60
69
|
const classified = pullable.map((entry) => {
|
|
@@ -81,12 +90,12 @@ async function pull() {
|
|
|
81
90
|
const changed = classified.filter((e) => e.status !== 'unchanged');
|
|
82
91
|
|
|
83
92
|
if (!changed.length) {
|
|
84
|
-
|
|
93
|
+
log('Everything is up to date.');
|
|
85
94
|
} else {
|
|
86
|
-
const selected = await selectEntries(changed, 'pull (overwrites local files)');
|
|
95
|
+
const selected = await selectEntries(changed, 'pull (overwrites local files)', opts);
|
|
87
96
|
|
|
88
97
|
if (!selected.length) {
|
|
89
|
-
|
|
98
|
+
log('Nothing selected. No changes made.');
|
|
90
99
|
} else {
|
|
91
100
|
for (const entry of selected) {
|
|
92
101
|
writeCodeFile(entry.path, entry.remoteCode);
|
|
@@ -101,15 +110,18 @@ async function pull() {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
writeConfig(config);
|
|
104
|
-
|
|
113
|
+
log(`\nPulled ${selected.length} record(s).`);
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
} else {
|
|
108
|
-
|
|
117
|
+
log('No tracked resources. Run `agrippa clone` first.');
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
// ── refresh tracked process-builder wizards ───────────────────────────────
|
|
112
|
-
await pullPbEntries(token, config);
|
|
121
|
+
await pullPbEntries(token, config, opts);
|
|
122
|
+
|
|
123
|
+
// ── refresh tracked long-running processes ────────────────────────────────
|
|
124
|
+
await pullLrpEntries(token, config, opts);
|
|
113
125
|
|
|
114
126
|
// ── discover new phases on tracked workflows ──────────────────────────────
|
|
115
127
|
await discoverNewPhases(token, ripUrl, config, changedWorkflowIds);
|
|
@@ -120,13 +132,13 @@ async function pull() {
|
|
|
120
132
|
// unchanged local === remote semantically → nothing to do
|
|
121
133
|
// fast-forward remote changed, local untouched since pull → safe overwrite
|
|
122
134
|
// conflict local diverged from checksum_at_pull → overwrite loses local work
|
|
123
|
-
async function pullPbEntries(token, config) {
|
|
135
|
+
async function pullPbEntries(token, config, opts = {}) {
|
|
124
136
|
const entries = config.workspace.filter((e) => e.object_type === 'process_builder');
|
|
125
137
|
if (!entries.length) return;
|
|
126
138
|
if (!process.env.PB_URL)
|
|
127
139
|
throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
128
140
|
|
|
129
|
-
|
|
141
|
+
log('Checking process-builder wizards...');
|
|
130
142
|
const classified = [];
|
|
131
143
|
for (const entry of entries) {
|
|
132
144
|
let upstream = null;
|
|
@@ -136,7 +148,7 @@ async function pullPbEntries(token, config) {
|
|
|
136
148
|
upstream = null;
|
|
137
149
|
}
|
|
138
150
|
if (!upstream) {
|
|
139
|
-
|
|
151
|
+
warn(` ${entry.name}: could not fetch upstream, skipping`);
|
|
140
152
|
continue;
|
|
141
153
|
}
|
|
142
154
|
const localSemantic = localChecksum(projectReader(entry.path));
|
|
@@ -151,13 +163,13 @@ async function pullPbEntries(token, config) {
|
|
|
151
163
|
|
|
152
164
|
const changed = classified.filter((e) => e.status !== 'unchanged');
|
|
153
165
|
if (!changed.length) {
|
|
154
|
-
|
|
166
|
+
log('Wizards are up to date.');
|
|
155
167
|
return;
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
const selected = await selectEntries(changed, 'pull (overwrites local wizard files)');
|
|
170
|
+
const selected = await selectEntries(changed, 'pull (overwrites local wizard files)', opts);
|
|
159
171
|
if (!selected.length) {
|
|
160
|
-
|
|
172
|
+
log('No wizards selected.');
|
|
161
173
|
return;
|
|
162
174
|
}
|
|
163
175
|
|
|
@@ -173,10 +185,79 @@ async function pullPbEntries(token, config) {
|
|
|
173
185
|
if (res.newStatus) config.workspace[idx].status = res.newStatus;
|
|
174
186
|
}
|
|
175
187
|
const note = res.diffs.length ? ` (WARNING: ${res.diffs.length} round-trip diff(s))` : '';
|
|
176
|
-
|
|
188
|
+
log(` ${entry.name} → refreshed${note}`);
|
|
177
189
|
}
|
|
178
190
|
writeConfig(config);
|
|
179
|
-
|
|
191
|
+
log(`\nPulled ${selected.length} wizard(s). Local backups in .backup/${backupTs}/`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Refresh tracked long-running processes from upstream. Same classification
|
|
195
|
+
// concern as pullPbEntries, but entries are located by NAME (the tabulator id
|
|
196
|
+
// changes on every save/version — never a stable key).
|
|
197
|
+
async function pullLrpEntries(token, config, opts = {}) {
|
|
198
|
+
const entries = config.workspace.filter((e) => e.object_type === 'long_running_process');
|
|
199
|
+
if (!entries.length) return;
|
|
200
|
+
if (!process.env.IMPORTEXPORT_URL)
|
|
201
|
+
throw new Error(
|
|
202
|
+
'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
log('Checking long-running processes...');
|
|
206
|
+
const classified = [];
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
let upstream = null;
|
|
209
|
+
try {
|
|
210
|
+
upstream = await fetchUpstream(token, entry.name);
|
|
211
|
+
} catch {
|
|
212
|
+
upstream = null;
|
|
213
|
+
}
|
|
214
|
+
if (!upstream) {
|
|
215
|
+
warn(` ${entry.name}: could not fetch upstream, skipping`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const localSemantic = localChecksum(projectReader(entry.path));
|
|
219
|
+
const remoteSemantic = remoteChecksumPb(upstream.payload);
|
|
220
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
221
|
+
let status;
|
|
222
|
+
if (localSemantic === remoteSemantic) status = 'unchanged';
|
|
223
|
+
else if (pullChecksum === localSemantic) status = 'fast-forward';
|
|
224
|
+
else status = 'conflict';
|
|
225
|
+
classified.push({ ...entry, upstream, status });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const changed = classified.filter((e) => e.status !== 'unchanged');
|
|
229
|
+
if (!changed.length) {
|
|
230
|
+
log('Long-running processes are up to date.');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const selected = await selectEntries(changed, 'pull (overwrites local LRP files)', opts);
|
|
235
|
+
if (!selected.length) {
|
|
236
|
+
log('No long-running processes selected.');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const backupTs = new Date().toISOString().replace(/:/g, '-').replace('T', '_').slice(0, 19);
|
|
241
|
+
for (const entry of selected) {
|
|
242
|
+
const res = await pullLrpEntry(token, entry, '.backup', backupTs);
|
|
243
|
+
const idx = config.workspace.findIndex(
|
|
244
|
+
(e) => e.object_type === 'long_running_process' && e.name === entry.name
|
|
245
|
+
);
|
|
246
|
+
if (idx !== -1) {
|
|
247
|
+
config.workspace[idx].checksum_at_pull = res.newChecksum;
|
|
248
|
+
config.workspace[idx].tenant_id = res.newRow.tenantId;
|
|
249
|
+
config.workspace[idx].svg = res.newRow.bpmnFileSvg;
|
|
250
|
+
config.workspace[idx].description = res.newRow.description;
|
|
251
|
+
config.workspace[idx].version = res.newRow.version;
|
|
252
|
+
config.workspace[idx].status = res.newRow.status;
|
|
253
|
+
}
|
|
254
|
+
const note = res.diffs.length ? ` (WARNING: ${res.diffs.length} round-trip diff(s))` : '';
|
|
255
|
+
log(` ${entry.name} → refreshed${note}`);
|
|
256
|
+
}
|
|
257
|
+
writeConfig(config);
|
|
258
|
+
log(
|
|
259
|
+
`\nPulled ${selected.length} long-running process(es). Local backups in .backup/${backupTs}/`
|
|
260
|
+
);
|
|
180
261
|
}
|
|
181
262
|
|
|
182
263
|
async function discoverNewPhases(token, ripUrl, config, changedWorkflowIds = new Set()) {
|
|
@@ -218,7 +299,7 @@ async function discoverNewPhases(token, ripUrl, config, changedWorkflowIds = new
|
|
|
218
299
|
checksum_at_pull: computeChecksum(phase.code),
|
|
219
300
|
name: `${wfName} / ${phase.name}`,
|
|
220
301
|
});
|
|
221
|
-
|
|
302
|
+
log(` new phase: ${filePath}`);
|
|
222
303
|
newCount++;
|
|
223
304
|
wfChanged = true;
|
|
224
305
|
}
|
|
@@ -231,14 +312,14 @@ async function discoverNewPhases(token, ripUrl, config, changedWorkflowIds = new
|
|
|
231
312
|
const structure = await describeWorkflow(token, ripUrl, wfId);
|
|
232
313
|
writeWorkflowDoc(basePath, structure);
|
|
233
314
|
} catch (err) {
|
|
234
|
-
|
|
315
|
+
warn(` could not refresh workflow.yml for ${wfName}: ${err.message}`);
|
|
235
316
|
}
|
|
236
317
|
}
|
|
237
318
|
}
|
|
238
319
|
|
|
239
320
|
if (newCount) {
|
|
240
321
|
writeConfig(config);
|
|
241
|
-
|
|
322
|
+
log(`Added ${newCount} new phase(s) from tracked workflows.`);
|
|
242
323
|
}
|
|
243
324
|
}
|
|
244
325
|
|
|
@@ -267,18 +348,32 @@ async function fetchRemoteCode(token, ripUrl, workspace) {
|
|
|
267
348
|
return map;
|
|
268
349
|
}
|
|
269
350
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
351
|
+
// `changed` only ever contains `fast-forward` and `conflict` entries (the
|
|
352
|
+
// caller already filtered out `unchanged`). `conflict` means local and remote
|
|
353
|
+
// have both diverged from the last-known-good baseline — applying it blindly
|
|
354
|
+
// can silently lose either side's work, so it is never preselected and is
|
|
355
|
+
// refused outright in `--non-interactive` mode instead of being silently
|
|
356
|
+
// skipped or applied.
|
|
357
|
+
async function selectEntries(changed, verb, opts = {}) {
|
|
358
|
+
const conflicts = changed.filter((e) => e.status === 'conflict');
|
|
359
|
+
|
|
360
|
+
if (opts.nonInteractive) {
|
|
361
|
+
if (conflicts.length) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`CONFLICT: refusing to ${verb} non-interactively — ${conflicts.length} resource(s) ` +
|
|
364
|
+
`in conflict: ${conflicts.map((e) => e.name).join(', ')}. Resolve interactively ` +
|
|
365
|
+
`or re-run once the conflicting side is reconciled.`
|
|
366
|
+
);
|
|
274
367
|
}
|
|
275
|
-
return
|
|
276
|
-
}
|
|
368
|
+
return changed;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const badgeFor = (status) => (status === 'fast-forward' ? '↑ safe' : '⚠ conflict');
|
|
277
372
|
|
|
278
373
|
const choices = changed.map((e) => ({
|
|
279
374
|
name: `${e.name} [${badgeFor(e.status)}]`,
|
|
280
375
|
value: e,
|
|
281
|
-
checked:
|
|
376
|
+
checked: e.status !== 'conflict',
|
|
282
377
|
}));
|
|
283
378
|
|
|
284
379
|
const { selected } = await inquirer.prompt([
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fetchUpstream } from '../lib/lrpApi.js';
|
|
4
|
+
import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
|
|
5
|
+
import { projectReader, writeProject } from '../lib/pbWorkspace.js';
|
|
6
|
+
|
|
7
|
+
// Delete files under <baseDir>/scripts that are not in the fresh decompose
|
|
8
|
+
// map, so a refresh that renames/removes scripts doesn't leave orphans behind
|
|
9
|
+
// (LRPs have no pages/ dir — no user tasks).
|
|
10
|
+
function pruneOrphans(baseDir, files) {
|
|
11
|
+
const dir = join(baseDir, 'scripts');
|
|
12
|
+
if (!existsSync(dir)) return;
|
|
13
|
+
for (const f of readdirSync(dir)) {
|
|
14
|
+
if (!(`scripts/${f}` in files)) rmSync(join(dir, f));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Refresh one cloned LRP from upstream (the pull counterpart of
|
|
19
|
+
// pushLrpEntry): back up the current local state, re-decompose the upstream
|
|
20
|
+
// XML, overwrite local files (pruning orphans), and verify the round-trip.
|
|
21
|
+
// `entry.upstream` is the {row, payload} fetched during classification;
|
|
22
|
+
// re-resolved by name if absent (id may have changed since the last pull).
|
|
23
|
+
async function pullLrpEntry(token, entry, backupDir, backupTs) {
|
|
24
|
+
const upstream = entry.upstream ?? (await fetchUpstream(token, entry.name));
|
|
25
|
+
if (!upstream) throw new Error(`could not fetch upstream LRP "${entry.name}"`);
|
|
26
|
+
|
|
27
|
+
// 1. Backup the current local state (recomposed payload) before overwriting.
|
|
28
|
+
try {
|
|
29
|
+
const localPayload = recompose(projectReader(entry.path));
|
|
30
|
+
const backupPath = join(backupDir, backupTs, entry.path, 'local.json');
|
|
31
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
32
|
+
writeFileSync(backupPath, JSON.stringify(localPayload, null, 2), 'utf-8');
|
|
33
|
+
} catch {
|
|
34
|
+
// local files unreadable (e.g. mid-edit) — nothing safe to back up, proceed.
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Decompose upstream → fresh file map; prune orphans; write.
|
|
38
|
+
const { files } = decompose(upstream.payload);
|
|
39
|
+
pruneOrphans(entry.path, files);
|
|
40
|
+
writeProject(entry.path, files);
|
|
41
|
+
|
|
42
|
+
// 3. Verify the freshly-written project reconstructs the upstream XML.
|
|
43
|
+
// (pages diff is expected noise for LRPs — see cloneLrp.js.)
|
|
44
|
+
const rebuilt = recompose(projectReader(entry.path));
|
|
45
|
+
const diffs = comparePayload(upstream.payload, rebuilt).filter((d) => !d.startsWith('pages:'));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
newChecksum: checksumOfPayload(rebuilt),
|
|
49
|
+
newRow: upstream.row,
|
|
50
|
+
diffs,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { pullLrpEntry };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
|
-
import { computeChecksum } from '../lib/checksum.js';
|
|
4
3
|
import { getProcess } from '../lib/pbApi.js';
|
|
5
|
-
import { comparePayload, decompose, recompose
|
|
4
|
+
import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
|
|
6
5
|
import { projectReader, writeProject } from '../lib/pbWorkspace.js';
|
|
7
6
|
|
|
8
7
|
// Delete files under <baseDir>/<sub> that are not in the fresh decompose map, so
|
|
@@ -44,7 +43,7 @@ async function pullPbEntry(token, entry, backupDir, backupTs) {
|
|
|
44
43
|
const diffs = comparePayload(upstream, rebuilt);
|
|
45
44
|
|
|
46
45
|
return {
|
|
47
|
-
newChecksum:
|
|
46
|
+
newChecksum: checksumOfPayload(rebuilt),
|
|
48
47
|
newUpdatedDate: upstream.updated_date,
|
|
49
48
|
newStatus: upstream.status,
|
|
50
49
|
diffs,
|