@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.
@@ -18,7 +18,15 @@
18
18
  import slugify from 'slugify';
19
19
  import { Document, visit, parse as yamlParse, stringify as yamlStringify } from 'yaml';
20
20
  import { computeChecksum } from './checksum.js';
21
- import { buildProcess, compareDiagram, compareProcess, diff, parseProcess } from './pbModel.js';
21
+ import {
22
+ buildProcess,
23
+ compareDiagram,
24
+ compareProcess,
25
+ diagramGeometry,
26
+ diff,
27
+ normalizeProcessTree,
28
+ parseProcess,
29
+ } from './pbModel.js';
22
30
 
23
31
  const MANIFEST_FILE = '.agrippa-pb.json';
24
32
  const STRUCTURE_FILE = 'structure.yaml';
@@ -52,7 +60,9 @@ const NODE_FIELDS = [
52
60
  'fields',
53
61
  'formKey',
54
62
  'attachedToRef',
55
- 'errorEventDefinition',
63
+ 'eventDefinitions',
64
+ 'calledElement',
65
+ 'dataIO',
56
66
  'multiInstance',
57
67
  'documentation',
58
68
  ];
@@ -107,9 +117,12 @@ function geometryMaps(diagram) {
107
117
  if (s.attrs.isExpanded !== undefined)
108
118
  expanded[s.attrs.bpmnElement] = s.attrs.isExpanded;
109
119
  }
120
+ // Always record the edge, even with zero waypoints (rare, but real —
121
+ // some exporters emit an empty <...:BPMNEdge>). Skipping on `.length`
122
+ // would drop the entry, so buildDiagram doesn't recreate the edge shape
123
+ // at all, losing a real (if degenerate) diagram element.
110
124
  for (const e of diagram.edges)
111
- if (e.waypoints?.length)
112
- waypoints[e.attrs.bpmnElement] = e.waypoints.map((w) => [w.x, w.y]);
125
+ waypoints[e.attrs.bpmnElement] = (e.waypoints || []).map((w) => [w.x, w.y]);
113
126
  }
114
127
  return { bounds, waypoints, expanded };
115
128
  }
@@ -121,6 +134,7 @@ function structEdge(e, geo) {
121
134
  if (e.name !== undefined) o.name = e.name;
122
135
  if (e.condition !== undefined) o.condition = e.condition;
123
136
  if (e.conditionType !== undefined) o.conditionType = e.conditionType;
137
+ if (e.conditionAttrs !== undefined) o.conditionAttrs = e.conditionAttrs;
124
138
  if (e.documentation !== undefined) o.documentation = e.documentation;
125
139
  if (geo.waypoints[e.id]) o.waypoints = geo.waypoints[e.id];
126
140
  return o;
@@ -166,6 +180,7 @@ function flattenNodes(structNodes, parentId, model, read, geo) {
166
180
  if (se.name !== undefined) e.name = se.name;
167
181
  if (se.condition !== undefined) e.condition = se.condition;
168
182
  if (se.conditionType !== undefined) e.conditionType = se.conditionType;
183
+ if (se.conditionAttrs !== undefined) e.conditionAttrs = se.conditionAttrs;
169
184
  if (se.documentation !== undefined) e.documentation = se.documentation;
170
185
  if (parentId) e.parent = parentId;
171
186
  if (se.waypoints) geo.waypoints[se.id] = se.waypoints;
@@ -219,7 +234,10 @@ function decompose(payload) {
219
234
  // back for the diagram).
220
235
  const structure = {
221
236
  process: model.process,
237
+ messages: model.messages,
222
238
  errors: model.errors,
239
+ signals: model.signals,
240
+ extraDefs: model.extraDefs,
223
241
  nodes: nestNodes(model, null, scriptsMap, geo),
224
242
  annotations: model.annotations.map((a) =>
225
243
  geo.bounds[a.id] ? { ...a, layout: geo.bounds[a.id] } : a
@@ -227,6 +245,9 @@ function decompose(payload) {
227
245
  associations: model.associations.map((a) =>
228
246
  geo.waypoints[a.id] ? { ...a, waypoints: geo.waypoints[a.id] } : a
229
247
  ),
248
+ groups: (model.groups || []).map((g) =>
249
+ geo.bounds[g.id] ? { ...g, layout: geo.bounds[g.id] } : g
250
+ ),
230
251
  };
231
252
  files[STRUCTURE_FILE] = stringifyStructure(structure);
232
253
 
@@ -259,17 +280,22 @@ function recompose(read) {
259
280
 
260
281
  const model = {
261
282
  process: structure.process,
283
+ messages: structure.messages || [],
262
284
  errors: structure.errors || [],
285
+ signals: structure.signals || [],
286
+ extraDefs: structure.extraDefs || [],
263
287
  nodes: [],
264
288
  edges: [],
265
289
  annotations: structure.annotations || [],
266
290
  associations: structure.associations || [],
291
+ groups: structure.groups || [],
267
292
  };
268
293
  const geo = { bounds: {}, waypoints: {}, expanded: {}, labelPos: {} };
269
294
  flattenNodes(structure.nodes, null, model, read, geo);
270
- // annotation/association geometry (top-level lists carry it inline)
295
+ // annotation/association/group geometry (top-level lists carry it inline)
271
296
  for (const a of model.annotations) if (a.layout) geo.bounds[a.id] = a.layout;
272
297
  for (const a of model.associations) if (a.waypoints) geo.waypoints[a.id] = a.waypoints;
298
+ for (const g of model.groups) if (g.layout) geo.bounds[g.id] = g.layout;
273
299
 
274
300
  const built_page = buildProcess({ ns: manifest.ns, model, geo });
275
301
 
@@ -337,10 +363,69 @@ function stableStringify(value) {
337
363
  return JSON.stringify(value ?? null);
338
364
  }
339
365
 
366
+ // Canonical form of a recomposed payload used ONLY for change-detection
367
+ // checksums — never for the actual push payload (recompose() itself, and
368
+ // what gets sent on push, are untouched). Routes built_page through the same
369
+ // formatting/order-insensitive canonicalization already used by
370
+ // comparePayload()'s 0-loss round-trip verification:
371
+ // - normalizeProcessTree drops insignificant whitespace text nodes (incl.
372
+ // the extraDefs `#text` padding between <definitions>-root siblings) and
373
+ // sorts declarations by id, so cosmetic reformatting doesn't count;
374
+ // - diagramGeometry keeps only x/y/width/height/isExpanded/waypoints, so
375
+ // `format`-only artifacts like labelPos never enter the comparison.
376
+ // Without this, a push followed by an unmodified re-fetch could classify as
377
+ // a phantom `conflict` instead of `unchanged` — see
378
+ // ai_tasks/2026-07-16-lrp-clone/deferred_work.md items 1, 3, 4.
379
+ //
380
+ // `updated_date`/`modified_by` are server-managed bookkeeping, not process
381
+ // content — Odoo/Symple bump them on any touch of the record, including ones
382
+ // with no semantic effect (observed live: an integration service account
383
+ // re-saving the wizard moved `updated_date`/`modified_by` on both the
384
+ // top-level payload and a page wrapper with zero content diff otherwise, via
385
+ // comparePayload). They exist at two levels: the top-level payload scalars,
386
+ // and per-entry inside `payload.pages` (each page's audit wrapper, distinct
387
+ // from the page *content* under `.page`, which is left untouched). Left in,
388
+ // they permanently pin the object to `conflict` the moment anything server-
389
+ // side touches it, regardless of actual content — worse than the cosmetic
390
+ // built_page diffs above, since nothing local or an agrippa push causes them
391
+ // to resync.
392
+ const VOLATILE_AUDIT_FIELDS = ['updated_date', 'modified_by'];
393
+
394
+ function canonicalForChecksum(payload) {
395
+ const { process, decls } = normalizeProcessTree(payload.built_page);
396
+ const rest = omit(payload, ['built_page', 'pages', ...VOLATILE_AUDIT_FIELDS]);
397
+ // Page order is not a stable identity — locally it's whatever order the
398
+ // manifest happened to record at last decompose, while a live upstream
399
+ // fetch can return the same set of pages in a different order (observed
400
+ // live: a fresh fetch came back with pages 1-3 cyclically rotated versus
401
+ // the local manifest, same guids, zero content difference). Sort by the
402
+ // page's own guid — stable, unlike array position — before hashing, the
403
+ // same trick normalizeProcessTree already uses for extraDefs decls.
404
+ const pages = [...(payload.pages || [])]
405
+ .sort((a, b) => (a.guid ?? '').localeCompare(b.guid ?? ''))
406
+ .map((p) => omit(p, VOLATILE_AUDIT_FIELDS));
407
+ return {
408
+ ...rest,
409
+ pages,
410
+ built_page_process: process,
411
+ built_page_decls: decls,
412
+ built_page_diagram: diagramGeometry(payload.built_page),
413
+ };
414
+ }
415
+
416
+ // Checksum of a recomposed payload, canonicalized so cosmetic round-trip
417
+ // differences don't register as a change. Used for checksum_at_pull baselines
418
+ // (clone/pull) and for the push/pull classifier (localChecksum/
419
+ // remoteChecksumPb below) — keep every PB/LRP checksum call site on this one
420
+ // function so baselines and comparisons stay on the same normalization.
421
+ function checksumOfPayload(payload) {
422
+ return computeChecksum(stableStringify(canonicalForChecksum(payload)));
423
+ }
424
+
340
425
  // Checksum of the recomposed payload — stable across runs, changes only when the
341
426
  // local files change. clonePb stores this as checksum_at_pull; push recomputes it.
342
427
  function localChecksum(read) {
343
- return computeChecksum(stableStringify(recompose(read)));
428
+ return checksumOfPayload(recompose(read));
344
429
  }
345
430
 
346
431
  // Semantic checksum of a remote payload. Decompose → recompose normalises field
@@ -349,7 +434,7 @@ function localChecksum(read) {
349
434
  function remoteChecksumPb(payload) {
350
435
  const { files } = decompose(payload);
351
436
  const read = (p) => files[p] ?? '';
352
- return computeChecksum(stableStringify(recompose(read)));
437
+ return checksumOfPayload(recompose(read));
353
438
  }
354
439
 
355
440
  // Enumerate the wizard's pages from the pages/*.yml files (the authoritative
@@ -388,6 +473,7 @@ export {
388
473
  comparePayload,
389
474
  verifyRoundTrip,
390
475
  stableStringify,
476
+ checksumOfPayload,
391
477
  localChecksum,
392
478
  remoteChecksumPb,
393
479
  enumeratePages,
@@ -0,0 +1,29 @@
1
+ // Initial body scaffolded into a new scriptTask's .js file (mirrors the
2
+ // `pbtemplate` editor snippet) so created scripts aren't empty.
3
+ export const SCRIPT_TEMPLATE = `try {
4
+ // ----------------------------
5
+ // Input gathering
6
+ // ----------------------------
7
+
8
+ // ----------------------------
9
+ // Output variable initialization
10
+ // ----------------------------
11
+
12
+ // ----------------------------
13
+ // Logical Helpers
14
+ // ----------------------------
15
+
16
+ // ----------------------------
17
+ // Main Execution
18
+ // ----------------------------
19
+
20
+ // ----------------------------
21
+ // Output
22
+ // ----------------------------
23
+
24
+ } catch (err) {
25
+ execution.setVariable('isAlive', false);
26
+ execution.setVariable('errorCode', '');
27
+ execution.setVariable('errorMessage', err.message);
28
+ }
29
+ `;
@@ -6,6 +6,7 @@ import fetch from 'node-fetch';
6
6
  import { resolveAddonsPath } from '../lib/addons.js';
7
7
  import { fuzzyMatch } from '../lib/fuzzy.js';
8
8
  import { execGit } from '../lib/git.js';
9
+ import { log } from '../lib/logger.js';
9
10
  import {
10
11
  appendPrToLine,
11
12
  appendRefsToLine,
@@ -120,7 +121,7 @@ async function createDevopsPR(branch, title, description) {
120
121
  return { id: data.pullRequestId, url: prUrl };
121
122
  }
122
123
 
123
- async function appendChecklistPrLink(taskId, currentChecklist, prUrl, prNumber) {
124
+ async function appendChecklistPrLink(taskId, currentChecklist, prUrl) {
124
125
  const link = `<a href="${prUrl}">${prUrl}</a><br/>`;
125
126
  const updated = currentChecklist ? `${currentChecklist}\n${link}` : link;
126
127
  const url = `${process.env.TRIDENT_URL}/jsonrpc`;
@@ -191,8 +192,8 @@ async function selectSection(sections, candidates) {
191
192
  const scored = candidates.length > 0 ? scoreSections(sections, candidates) : [];
192
193
 
193
194
  if (candidates.length > 0) {
194
- console.log(`\nTask fields: ${candidates.join(' | ')}`);
195
- if (scored.length === 0) console.log('No matching sections found.');
195
+ log(`\nTask fields: ${candidates.join(' | ')}`);
196
+ if (scored.length === 0) log('No matching sections found.');
196
197
  }
197
198
 
198
199
  const scoredHeadings = new Set(scored.map((s) => s.heading));
@@ -224,13 +225,13 @@ async function autoprAmend(options) {
224
225
  const pr = await fetchActivePr(branch);
225
226
  const prNumber = pr.pullRequestId;
226
227
  const prUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_git/${DEVOPS_REPO}/pullrequest/${prNumber}`;
227
- console.log(`Found PR #${prNumber}: ${prUrl}`);
228
+ log(`Found PR #${prNumber}: ${prUrl}`);
228
229
 
229
230
  const ids = options.trident ?? [];
230
231
  const jiras = options.jira ?? [];
231
232
 
232
233
  const tasks = ids.length > 0 ? await Promise.all(ids.map(fetchTask)) : [];
233
- tasks.forEach((t) => console.log(`Task: ${t.name}`));
234
+ tasks.forEach((t) => log(`Task: ${t.name}`));
234
235
 
235
236
  const newLinks = [
236
237
  ...ids.map((id) => `${process.env.TRIDENT_URL}/odoo/my-tasks/${id}`),
@@ -241,20 +242,18 @@ async function autoprAmend(options) {
241
242
  ? `${pr.description}\n${newLinks.join('\n')}`
242
243
  : newLinks.join('\n');
243
244
  await patchDevopsPrDescription(prNumber, updatedDescription);
244
- console.log('PR description updated');
245
+ log('PR description updated');
245
246
  }
246
247
 
247
248
  for (let i = 0; i < ids.length; i++) {
248
- await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl, prNumber);
249
+ await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl);
249
250
  }
250
- if (ids.length > 0) console.log('Checklist updated');
251
+ if (ids.length > 0) log('Checklist updated');
251
252
 
252
253
  const content = readFileSync(changelogPath, 'utf-8');
253
254
  const existing = findLineByPrNumber(content, prNumber);
254
255
  if (!existing) {
255
- console.log(
256
- `Warning: no changelog line found for PR #${prNumber} — skipping changelog update`
257
- );
256
+ log(`Warning: no changelog line found for PR #${prNumber} — skipping changelog update`);
258
257
  return;
259
258
  }
260
259
  const lines = content.split('\n');
@@ -264,8 +263,8 @@ async function autoprAmend(options) {
264
263
  await execGit(['add', 'CHANGELOG.md'], ADDONS_PATH);
265
264
  await execGit(['commit', '-m', '[DOC][CHANGELOG] Changelog'], ADDONS_PATH);
266
265
  await execGit(['push'], ADDONS_PATH);
267
- console.log('Changelog updated and pushed');
268
- console.log('\nReminder: squash the two changelog commits before merging the PR.');
266
+ log('Changelog updated and pushed');
267
+ log('\nReminder: squash the two changelog commits before merging the PR.');
269
268
  }
270
269
 
271
270
  async function autopr(options) {
@@ -278,15 +277,15 @@ async function autopr(options) {
278
277
  const hasTridents = ids.length > 0;
279
278
 
280
279
  const tasks = hasTridents ? await Promise.all(ids.map((id) => fetchTask(id))) : [];
281
- tasks.forEach((t) => console.log(`Task: ${t.name}`));
280
+ tasks.forEach((t) => log(`Task: ${t.name}`));
282
281
 
283
282
  const content = readFileSync(changelogPath, 'utf-8');
284
283
 
285
- const duplicate = hasTridents ? findDuplicateLine(content, ids, []) : null;
284
+ const duplicate = findDuplicateLine(content, ids, options.jira ?? []);
286
285
 
287
286
  let appendMode = false;
288
287
  if (duplicate) {
289
- console.log(`\nExisting entry (line ${duplicate.lineNumber + 1}):\n ${duplicate.line}`);
288
+ log(`\nExisting entry (line ${duplicate.lineNumber + 1}):\n ${duplicate.line}`);
290
289
  const { confirm } = await inquirer.prompt([
291
290
  {
292
291
  type: 'confirm',
@@ -315,19 +314,19 @@ async function autopr(options) {
315
314
  }
316
315
 
317
316
  await execGit(['checkout', '-b', branch], ADDONS_PATH);
318
- console.log(`Branch created: ${branch}`);
317
+ log(`Branch created: ${branch}`);
319
318
  await execGit(['push', '-u', 'origin', branch], ADDONS_PATH);
320
319
 
321
320
  const prTitle = options.name ?? tasks[0]?.name ?? branch;
322
321
  const prDescription = buildPrDescription(ids, options.jira ?? []);
323
322
  const { id: prNumber, url: prUrl } = await createDevopsPR(branch, prTitle, prDescription);
324
- console.log(`PR opened: #${prNumber} — ${prUrl}`);
323
+ log(`PR opened: #${prNumber} — ${prUrl}`);
325
324
 
326
325
  if (hasTridents) {
327
326
  for (let i = 0; i < ids.length; i++) {
328
- await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl, prNumber);
327
+ await appendChecklistPrLink(ids[i], tasks[i].x_release_checklist, prUrl);
329
328
  }
330
- console.log('Checklist updated');
329
+ log('Checklist updated');
331
330
  }
332
331
 
333
332
  const lines = content.split('\n');
@@ -365,12 +364,12 @@ async function autopr(options) {
365
364
  }
366
365
 
367
366
  await fs.writeFile(changelogPath, lines.join('\n'));
368
- console.log('Changelog entry written');
367
+ log('Changelog entry written');
369
368
 
370
369
  await execGit(['add', 'CHANGELOG.md'], ADDONS_PATH);
371
370
  await execGit(['commit', '-m', '[DOC][CHANGELOG] Changelog'], ADDONS_PATH);
372
371
  await execGit(['push'], ADDONS_PATH);
373
- console.log('Changelog committed and pushed');
372
+ log('Changelog committed and pushed');
374
373
  }
375
374
 
376
375
  export { autopr };
@@ -4,6 +4,7 @@ import search from '@inquirer/search';
4
4
  import inquirer from 'inquirer';
5
5
  import { resolveAddonsPath } from '../lib/addons.js';
6
6
  import { fuzzyMatch } from '../lib/fuzzy.js';
7
+ import { log } from '../lib/logger.js';
7
8
 
8
9
  function buildRefString(tridents, jiras, prNumber) {
9
10
  const refs = [];
@@ -116,7 +117,7 @@ async function changelog(prNumber, options) {
116
117
  const duplicate = findDuplicateLine(content, tridents, jiras);
117
118
  let appendMode = false;
118
119
  if (duplicate) {
119
- console.log(`\nExisting entry (line ${duplicate.lineNumber + 1}):\n ${duplicate.line}`);
120
+ log(`\nExisting entry (line ${duplicate.lineNumber + 1}):\n ${duplicate.line}`);
120
121
  const { confirm } = await inquirer.prompt([
121
122
  {
122
123
  type: 'confirm',
@@ -132,7 +133,7 @@ async function changelog(prNumber, options) {
132
133
  const lines = content.split('\n');
133
134
  lines[duplicate.lineNumber] = appendPrToLine(lines[duplicate.lineNumber], prNumber);
134
135
  await fs.writeFile(changelogPath, lines.join('\n'));
135
- console.log('Updated existing line');
136
+ log('Updated existing line');
136
137
  return;
137
138
  }
138
139
 
@@ -176,7 +177,7 @@ async function changelog(prNumber, options) {
176
177
  lines.splice(endLine + 1, 0, newEntry);
177
178
 
178
179
  await fs.writeFile(changelogPath, lines.join('\n'));
179
- console.log('Changelog entry added');
180
+ log('Changelog entry added');
180
181
  }
181
182
 
182
183
  function findLineByPrNumber(content, prNumber) {
@@ -3,6 +3,7 @@ import inquirer from 'inquirer';
3
3
  import searchList from 'inquirer-search-list';
4
4
  import { resolveAddonsPath } from '../lib/addons.js';
5
5
  import { execGit } from '../lib/git.js';
6
+ import { log } from '../lib/logger.js';
6
7
 
7
8
  inquirer.registerPrompt('search-list', searchList);
8
9
 
@@ -50,7 +51,7 @@ function validateSameModule(files) {
50
51
  const allSameModule = modules.every((module) => `[${module}]` === currentModule);
51
52
 
52
53
  if (!allSameModule) {
53
- console.log(chalk.red('Selected files are not of the same module'));
54
+ log(chalk.red('Selected files are not of the same module'));
54
55
  return null;
55
56
  }
56
57
 
@@ -59,8 +60,8 @@ function validateSameModule(files) {
59
60
 
60
61
  async function getFilesToCommit(stagedChanges, unstagedChanges) {
61
62
  if (stagedChanges.trim()) {
62
- console.log(chalk.green('Staged changes:'));
63
- console.log(stagedChanges);
63
+ log(chalk.green('Staged changes:'));
64
+ log(stagedChanges);
64
65
 
65
66
  return {
66
67
  filesToCheck: stagedChanges.trim().split('\n'),
@@ -71,9 +72,9 @@ async function getFilesToCommit(stagedChanges, unstagedChanges) {
71
72
  const unstagedFiles = unstagedChanges.trim().split('\n');
72
73
 
73
74
  while (true) {
74
- console.log(chalk.yellow('No staged changes found.'));
75
- console.log(chalk.yellow('Unstaged changes:'));
76
- console.log(unstagedChanges);
75
+ log(chalk.yellow('No staged changes found.'));
76
+ log(chalk.yellow('Unstaged changes:'));
77
+ log(unstagedChanges);
77
78
 
78
79
  const answers = await inquirer.prompt([
79
80
  {
@@ -104,7 +105,7 @@ async function commit() {
104
105
  const stagedChanges = await execGit(['diff', '--cached', '--name-only'], ADDONS_PATH);
105
106
 
106
107
  if (!unstagedChanges.trim() && !stagedChanges.trim()) {
107
- console.log(chalk.red('No changes found to commit.'));
108
+ log(chalk.red('No changes found to commit.'));
108
109
  return;
109
110
  }
110
111
 
@@ -115,7 +116,7 @@ async function commit() {
115
116
  } = await getFilesToCommit(stagedChanges, unstagedChanges);
116
117
 
117
118
  if (filesToCheck.length === 0) {
118
- console.log(chalk.red('No files selected.'));
119
+ log(chalk.red('No files selected.'));
119
120
  return;
120
121
  }
121
122
 
@@ -167,13 +168,13 @@ async function commit() {
167
168
  }
168
169
 
169
170
  await execGit(['commit', '-m', commitMessage], ADDONS_PATH);
170
- console.log(chalk.green('Commit created successfully!'));
171
+ log(chalk.green('Commit created successfully!'));
171
172
 
172
173
  const remainingUnstaged = await execGit(['diff', '--name-only'], ADDONS_PATH);
173
174
  const remainingStaged = await execGit(['diff', '--cached', '--name-only'], ADDONS_PATH);
174
175
 
175
176
  if (!remainingUnstaged.trim() && !remainingStaged.trim()) {
176
- console.log(chalk.green('No more changes to commit.'));
177
+ log(chalk.green('No more changes to commit.'));
177
178
  break;
178
179
  }
179
180
 
@@ -1,3 +1,4 @@
1
+ import { log } from '../lib/logger.js';
1
2
  import { exportEmailTemplates } from './exportEmailTemplates.js';
2
3
  import { exportImperex } from './exportImperex.js';
3
4
  import { exportLrp } from './exportLrp.js';
@@ -5,7 +6,7 @@ import { exportPb } from './exportPb.js';
5
6
  import { exportWorkflow } from './exportWorkflow.js';
6
7
 
7
8
  function exportRip() {
8
- console.log('Not implemented yet.');
9
+ log('Not implemented yet.');
9
10
  }
10
11
 
11
12
  export { exportPb, exportRip, exportImperex, exportEmailTemplates, exportWorkflow, exportLrp };
@@ -36,13 +36,30 @@ async function initiateExport(guid, token) {
36
36
  if (!response.ok) throw new Error(await response.text());
37
37
  }
38
38
 
39
+ // No job/request id is returned by initiateExport, so the poll still
40
+ // correlates by GUID + a corrected date window over the last few results —
41
+ // a heuristic, not a stable identifier (see REL-ADP-PB-001; a real fix
42
+ // depends on the ImportExport API exposing a request id, EXT-002). What we
43
+ // can and do own locally is not hanging forever: an overall deadline.
44
+ const PB_EXPORT_POLL_INTERVAL_MS = 3_000;
45
+ const PB_EXPORT_POLL_TIMEOUT_MS = 120_000;
46
+
39
47
  async function pollExportResult(guid, requestTime, token) {
40
48
  const url = `${process.env.IMPORTEXPORT_URL}/export/info/processKey=ExportElement&subProcess=true&status=FAILED,COMPLETED&referenceId=process_builder`;
41
49
  // Server createDate is offset -1hr from system time; subtract 1hr+5s buffer
42
50
  const cutoff = requestTime - 3_605_000;
51
+ const deadline = requestTime + PB_EXPORT_POLL_TIMEOUT_MS;
43
52
 
44
53
  while (true) {
45
- await new Promise((r) => setTimeout(r, 3000));
54
+ if (Date.now() > deadline) {
55
+ throw new Error(
56
+ `REMOTE_TIMEOUT: Process Builder export for guid ${guid} did not complete within ${
57
+ PB_EXPORT_POLL_TIMEOUT_MS / 1000
58
+ }s`
59
+ );
60
+ }
61
+
62
+ await new Promise((r) => setTimeout(r, PB_EXPORT_POLL_INTERVAL_MS));
46
63
 
47
64
  const response = await fetch(url, {
48
65
  method: 'POST',
@@ -153,4 +170,4 @@ async function exportPb(opts) {
153
170
  }
154
171
  }
155
172
 
156
- export { exportPb };
173
+ export { exportPb, pollExportResult };
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import inquirer from 'inquirer';
3
3
  import { CONFIG_DIR, CONFIG_FILE } from '../config.js';
4
+ import { log } from '../lib/logger.js';
4
5
 
5
6
  async function init() {
6
7
  if (!existsSync(CONFIG_DIR)) {
@@ -142,7 +143,7 @@ async function init() {
142
143
  .map(([k, v]) => `${k}=${v}`)
143
144
  .join('\n') + '\n'
144
145
  );
145
- console.log(`Config written to ${CONFIG_FILE}`);
146
+ log(`Config written to ${CONFIG_FILE}`);
146
147
  }
147
148
 
148
149
  export { init };
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { select } from '@inquirer/prompts';
4
4
  import { parse } from 'yaml';
5
5
  import { CONFIG_DIR } from '../config.js';
6
- import { setSilent } from '../lib/logger.js';
6
+ import { log, setSilent } from '../lib/logger.js';
7
7
  import { exportEmailTemplates } from './exportEmailTemplates.js';
8
8
  import { exportImperex } from './exportImperex.js';
9
9
  import { exportLrp } from './exportLrp.js';
@@ -52,39 +52,50 @@ function isNothingToCommit(err) {
52
52
  }
53
53
 
54
54
  async function runRoutine(routine) {
55
- console.log(`Running Routine: ${routine.name}`);
55
+ log(`Running Routine: ${routine.name}`);
56
+
57
+ const failures = [];
56
58
 
57
59
  for (const step of routine.steps) {
58
60
  const label = `[${step.name}]`;
59
61
  const fn = COMMAND_MAP[step.command];
60
62
  if (!fn) {
61
- console.log(`${label} Unknown command: ${step.command}`);
63
+ log(`${label} Unknown command: ${step.command}`);
64
+ failures.push(`${step.name}: unknown command "${step.command}"`);
62
65
  continue;
63
66
  }
64
67
 
65
- console.log(`${label} Job started`);
68
+ log(`${label} Job started`);
66
69
  setSilent(true);
67
70
 
68
71
  try {
69
72
  await fn(stepOpts(step));
70
- console.log(`${label} Done (committed)`);
73
+ log(`${label} Done (committed)`);
71
74
  } catch (err) {
72
75
  if (isNothingToCommit(err)) {
73
- console.log(`${label} Done (nothing to commit)`);
76
+ log(`${label} Done (nothing to commit)`);
74
77
  } else {
75
- console.log(`${label} Failed: ${err.message}`);
78
+ log(`${label} Failed: ${err.message}`);
79
+ failures.push(`${step.name}: ${err.message}`);
76
80
  }
77
81
  } finally {
78
82
  setSilent(false);
79
83
  }
80
84
  }
85
+
86
+ if (failures.length) {
87
+ throw new Error(
88
+ `Routine "${routine.name}" completed with ${failures.length} failed step(s):\n` +
89
+ failures.map((f) => ` - ${f}`).join('\n')
90
+ );
91
+ }
81
92
  }
82
93
 
83
94
  async function routine() {
84
95
  const routines = loadRoutines();
85
96
 
86
97
  if (!routines.length) {
87
- console.log(
98
+ log(
88
99
  'No routines defined. Create ~/.config/prbot/routines.yaml or add routines: to agrippa.yaml.'
89
100
  );
90
101
  return;