@waron97/prbot 3.3.0 → 3.5.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.
@@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import inquirer from 'inquirer';
4
4
  import { getToken } from '../../lib/auth.js';
5
+ import { log } from '../../lib/logger.js';
5
6
  import { updateMfa, updatePhase } from '../lib/api.js';
6
7
  import { computeChecksum } from '../lib/checksum.js';
7
8
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
@@ -23,24 +24,28 @@ async function push(opts = {}) {
23
24
  // Stale-entry check (a path may be a file for phase/mfa or a dir for pb).
24
25
  const stale = config.workspace.filter((e) => !fileExists(e.path));
25
26
  if (stale.length) {
26
- console.log('\nThe following tracked resources no longer exist on disk:');
27
- stale.forEach((e) => console.log(` - ${e.path} (${e.name})`));
28
- const { cleanup } = await inquirer.prompt([
29
- {
30
- type: 'confirm',
31
- name: 'cleanup',
32
- message: 'Remove these entries from the workspace config?',
33
- default: true,
34
- },
35
- ]);
36
- if (cleanup) {
37
- config.workspace = config.workspace.filter((e) => fileExists(e.path));
38
- writeConfig(config);
27
+ log('\nThe following tracked resources no longer exist on disk:');
28
+ stale.forEach((e) => log(` - ${e.path} (${e.name})`));
29
+ if (opts.nonInteractive) {
30
+ log(' (non-interactive: leaving them tracked; run interactively to clean up)');
31
+ } else {
32
+ const { cleanup } = await inquirer.prompt([
33
+ {
34
+ type: 'confirm',
35
+ name: 'cleanup',
36
+ message: 'Remove these entries from the workspace config?',
37
+ default: true,
38
+ },
39
+ ]);
40
+ if (cleanup) {
41
+ config.workspace = config.workspace.filter((e) => fileExists(e.path));
42
+ writeConfig(config);
43
+ }
39
44
  }
40
45
  }
41
46
 
42
47
  if (!config.workspace.length) {
43
- console.log('No tracked resources. Run `agrippa clone` first.');
48
+ log('No tracked resources. Run `agrippa clone` first.');
44
49
  return;
45
50
  }
46
51
 
@@ -59,7 +64,7 @@ async function push(opts = {}) {
59
64
  'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
60
65
  );
61
66
 
62
- console.log('Fetching remote state...');
67
+ log('Fetching remote state...');
63
68
  const token = await getToken();
64
69
 
65
70
  const remoteCodeMap = hasCode
@@ -123,13 +128,13 @@ async function push(opts = {}) {
123
128
 
124
129
  const changed = classified.filter((e) => e.status !== 'unchanged');
125
130
  if (!changed.length) {
126
- console.log('Nothing to push — everything matches the last-pulled state.');
131
+ log('Nothing to push — everything matches the last-pulled state.');
127
132
  return;
128
133
  }
129
134
 
130
- const selected = await selectEntries(changed, 'push (overwrites remote)', 'push');
135
+ const selected = await selectEntries(changed, 'push (overwrites remote)', opts);
131
136
  if (!selected.length) {
132
- console.log('Nothing selected. No changes made.');
137
+ log('Nothing selected. No changes made.');
133
138
  return;
134
139
  }
135
140
 
@@ -168,7 +173,7 @@ async function push(opts = {}) {
168
173
  ]
169
174
  .filter(Boolean)
170
175
  .join(', ');
171
- console.log(` ${entry.name} → saved (draft)${note ? ` [${note}]` : ''}`);
176
+ log(` ${entry.name} → saved (draft)${note ? ` [${note}]` : ''}`);
172
177
  if (idx !== -1) {
173
178
  config.workspace[idx].checksum_at_pull = res.newChecksum;
174
179
  if (res.newUpdatedDate) config.workspace[idx].updated_date = res.newUpdatedDate;
@@ -177,7 +182,7 @@ async function push(opts = {}) {
177
182
  pushedPb.push({ entry, idx });
178
183
  } else if (entry.object_type === 'long_running_process') {
179
184
  const res = await pushLrpEntry(token, entry, BACKUP_DIR, backupTs);
180
- console.log(` ${entry.name} → saved`);
185
+ log(` ${entry.name} → saved`);
181
186
  if (idx !== -1) {
182
187
  config.workspace[idx].checksum_at_pull = res.newChecksum;
183
188
  config.workspace[idx].tenant_id = res.newRow.tenantId;
@@ -197,7 +202,7 @@ async function push(opts = {}) {
197
202
  pushed++;
198
203
  }
199
204
 
200
- console.log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
205
+ log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
201
206
 
202
207
  // Publish/deploy step for pushed wizards and LRPs.
203
208
  if (pushedPb.length) {
@@ -208,7 +213,7 @@ async function push(opts = {}) {
208
213
  }
209
214
 
210
215
  writeConfig(config);
211
- console.log(`\nPushed ${pushed} record(s).`);
216
+ log(`\nPushed ${pushed} record(s).`);
212
217
  }
213
218
 
214
219
  async function handlePublish(token, pushedPb, config, opts) {
@@ -216,6 +221,8 @@ async function handlePublish(token, pushedPb, config, opts) {
216
221
  let doPublish;
217
222
  if (opts.skipPublish) doPublish = false;
218
223
  else if (opts.publish) doPublish = true;
224
+ else if (opts.nonInteractive)
225
+ doPublish = false; // no stdin to prompt on; default to safe
219
226
  else {
220
227
  ({ doPublish } = await inquirer.prompt([
221
228
  {
@@ -229,7 +236,7 @@ async function handlePublish(token, pushedPb, config, opts) {
229
236
  if (doPublish) {
230
237
  await publish(token, entry.guid);
231
238
  if (idx !== -1) config.workspace[idx].status = 'published';
232
- console.log(` published ${entry.name}`);
239
+ log(` published ${entry.name}`);
233
240
  }
234
241
  }
235
242
  }
@@ -239,6 +246,8 @@ async function handleDeploy(token, pushedLrp, config, opts) {
239
246
  let doDeploy;
240
247
  if (opts.skipPublish) doDeploy = false;
241
248
  else if (opts.publish) doDeploy = true;
249
+ else if (opts.nonInteractive)
250
+ doDeploy = false; // no stdin to prompt on; default to safe
242
251
  else {
243
252
  ({ doDeploy } = await inquirer.prompt([
244
253
  {
@@ -252,7 +261,7 @@ async function handleDeploy(token, pushedLrp, config, opts) {
252
261
  if (doDeploy) {
253
262
  await deploy(token, deployId);
254
263
  if (idx !== -1) config.workspace[idx].status = 'deployed';
255
- console.log(` deployed ${entry.name}`);
264
+ log(` deployed ${entry.name}`);
256
265
  }
257
266
  }
258
267
  }
@@ -1,3 +1,4 @@
1
+ import { log } from '../../lib/logger.js';
1
2
  import { readConfig, writeConfig } from '../lib/config.js';
2
3
  import { fileExists } from '../lib/workspace.js';
3
4
 
@@ -9,16 +10,16 @@ async function repair() {
9
10
  config.workspace = config.workspace.filter((entry) => fileExists(entry.path));
10
11
 
11
12
  if (!stale.length) {
12
- console.log('No stale entries found.');
13
+ log('No stale entries found.');
13
14
  return;
14
15
  }
15
16
 
16
17
  for (const entry of stale) {
17
- console.log(` removed: ${entry.path} (${entry.name})`);
18
+ log(` removed: ${entry.path} (${entry.name})`);
18
19
  }
19
20
 
20
21
  writeConfig(config);
21
- console.log(
22
+ log(
22
23
  `Removed ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'} (${before} → ${config.workspace.length}).`
23
24
  );
24
25
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
3
  import { program } from 'commander';
4
+ import { error } from '../lib/logger.js';
4
5
  import { clone } from './commands/clone.js';
5
6
  import { diff } from './commands/diff.js';
6
7
  import { init } from './commands/init.js';
@@ -24,7 +25,7 @@ const require = createRequire(import.meta.url);
24
25
  const { version } = require('../../package.json');
25
26
 
26
27
  process.on('unhandledRejection', (err) => {
27
- console.error(`Error: ${err.message}`);
28
+ error(`Error: ${err.message}`);
28
29
  process.exit(1);
29
30
  });
30
31
 
@@ -35,7 +36,7 @@ program
35
36
  .description('Create agrippa.yaml workspace config in the current directory')
36
37
  .action(() =>
37
38
  init().catch((err) => {
38
- console.error(`Error: ${err.message}`);
39
+ error(`Error: ${err.message}`);
39
40
  process.exit(1);
40
41
  })
41
42
  );
@@ -54,7 +55,7 @@ program
54
55
  .option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb/lrp)')
55
56
  .action((opts) =>
56
57
  clone(opts).catch((err) => {
57
- console.error(`Error: ${err.message}`);
58
+ error(`Error: ${err.message}`);
58
59
  process.exit(1);
59
60
  })
60
61
  );
@@ -62,9 +63,13 @@ program
62
63
  program
63
64
  .command('pull')
64
65
  .description('Pull remote changes into local files')
65
- .action(() =>
66
- pull().catch((err) => {
67
- console.error(`Error: ${err.message}`);
66
+ .option(
67
+ '--non-interactive',
68
+ 'No prompts; auto-select safe (fast-forward) entries and fail if any is in conflict'
69
+ )
70
+ .action((opts) =>
71
+ pull(opts).catch((err) => {
72
+ error(`Error: ${err.message}`);
68
73
  process.exit(1);
69
74
  })
70
75
  );
@@ -74,19 +79,26 @@ program
74
79
  .description('Push local changes to RIP / Process Builder / LRP (backs up remote first)')
75
80
  .option('--publish', 'Auto-publish pushed wizards and auto-deploy pushed LRPs')
76
81
  .option('--skip-publish', 'Skip publishing/deploying pushed wizards and LRPs (no prompt)')
82
+ .option(
83
+ '--non-interactive',
84
+ 'No prompts; auto-select safe (fast-forward) entries and fail if any is in conflict'
85
+ )
77
86
  .action((opts) =>
78
87
  push(opts).catch((err) => {
79
- console.error(`Error: ${err.message}`);
88
+ error(`Error: ${err.message}`);
80
89
  process.exit(1);
81
90
  })
82
91
  );
83
92
 
84
93
  program
85
- .command('diff [path]')
86
- .description('Show differences between local files and remote code')
94
+ .command('diff [target]')
95
+ .description(
96
+ 'Show differences between local files and remote code. [target] = file, folder, ' +
97
+ 'project dir, document_id or name; omit for the whole workspace'
98
+ )
87
99
  .action((path) =>
88
100
  diff(path).catch((err) => {
89
- console.error(`Error: ${err.message}`);
101
+ error(`Error: ${err.message}`);
90
102
  process.exit(1);
91
103
  })
92
104
  );
@@ -96,7 +108,7 @@ program
96
108
  .description('Initialize a phase with default code template and result vars')
97
109
  .action(() =>
98
110
  initPhase().catch((err) => {
99
- console.error(`Error: ${err.message}`);
111
+ error(`Error: ${err.message}`);
100
112
  process.exit(1);
101
113
  })
102
114
  );
@@ -106,14 +118,14 @@ program
106
118
  .description('Remove stale workspace entries where local file no longer exists')
107
119
  .action(() =>
108
120
  repair().catch((err) => {
109
- console.error(`Error: ${err.message}`);
121
+ error(`Error: ${err.message}`);
110
122
  process.exit(1);
111
123
  })
112
124
  );
113
125
 
114
126
  // ---- pb: local editing helpers for a cloned process-builder wizard or LRP ----
115
127
  const die = (err) => {
116
- console.error(`Error: ${err.message}`);
128
+ error(`Error: ${err.message}`);
117
129
  process.exit(1);
118
130
  };
119
131
  const pb = program
@@ -317,7 +317,13 @@ function removeNode(structure, manifest, { id }) {
317
317
  );
318
318
  }
319
319
 
320
- return { writes: {}, deletes, result: { removed: [...victimIds], removedEdges } };
320
+ const clearedDefaults = clearDanglingDefaults(structure);
321
+
322
+ return {
323
+ writes: {},
324
+ deletes,
325
+ result: { removed: [...victimIds], removedEdges, clearedDefaults },
326
+ };
321
327
  }
322
328
 
323
329
  // ---------- connect / disconnect ----------
@@ -492,15 +498,55 @@ function lintIncomingEdges(structure) {
492
498
  return issues;
493
499
  }
494
500
 
501
+ // Flag `attrs.default` values that don't name an outgoing edge of their own
502
+ // node. lintGateways only covers the inverse (a gateway that needs a default
503
+ // and hasn't got a valid one), so a node left with a single outgoing flow and a
504
+ // default pointing at a deleted one used to pass clean — and then fail at
505
+ // publish with Activiti's null-reference NPE. The edits that delete edges now
506
+ // clear these themselves (clearDanglingDefaults); this rule is the net for
507
+ // hand-edited structure.yaml and for projects predating that fix.
508
+ function lintDanglingDefaults(structure) {
509
+ const issues = [];
510
+ eachNode(structure.nodes, null, (n) => {
511
+ const def = n.attrs?.default;
512
+ if (!def) return;
513
+ if ((n.edges || []).some((e) => e.id === def)) return;
514
+ issues.push(
515
+ `${n.id} (${n.name || n.type}): default flow ${def} does not exist on this node — ` +
516
+ `Activiti fails the deploy on the dangling reference.`
517
+ );
518
+ });
519
+ return issues;
520
+ }
521
+
495
522
  // Run all lint rules and return combined issues.
496
523
  function lintAll(structure) {
497
524
  return [
498
525
  ...lintGateways(structure),
526
+ ...lintDanglingDefaults(structure),
499
527
  ...lintEdgeNames(structure),
500
528
  ...lintIncomingEdges(structure),
501
529
  ];
502
530
  }
503
531
 
532
+ // Drop every `attrs.default` that no longer names an outgoing edge of its own
533
+ // node. Must run after any edit that deletes edges: a default referencing a
534
+ // removed flow recomposes fine and passes the other lint rules, but Activiti
535
+ // resolves the reference at deploy time, gets null, and the publish fails with
536
+ // an unattributed `Could not deploy the XML: null. Error: null`. Returns the
537
+ // cleared [nodeId, edgeId] pairs so callers can report them.
538
+ function clearDanglingDefaults(structure) {
539
+ const cleared = [];
540
+ eachNode(structure.nodes, null, (n) => {
541
+ const def = n.attrs?.default;
542
+ if (!def) return;
543
+ if ((n.edges || []).some((e) => e.id === def)) return;
544
+ delete n.attrs.default;
545
+ cleared.push({ node: n.id, edge: def });
546
+ });
547
+ return cleared;
548
+ }
549
+
504
550
  // Remove an edge by id, or by --from/--to pair.
505
551
  function disconnect(structure, { id, from, to }) {
506
552
  let removed = 0;
@@ -516,7 +562,8 @@ function disconnect(structure, { id, from, to }) {
516
562
  removed += before - n.edges.length;
517
563
  });
518
564
  if (!removed) throw new Error(id ? `edge not found: ${id}` : `no edge from ${from} to ${to}`);
519
- return { writes: {}, deletes: [], result: { removed, id: removedId } };
565
+ const clearedDefaults = clearDanglingDefaults(structure);
566
+ return { writes: {}, deletes: [], result: { removed, id: removedId, clearedDefaults } };
520
567
  }
521
568
 
522
569
  // ---------- list ----------
@@ -154,7 +154,14 @@ async function autoLayout(structure) {
154
154
  targets: [e.target],
155
155
  sourcePort,
156
156
  labels: e.name
157
- ? [{ id: `${e.id}__lbl`, text: e.name, width: Math.min(e.name.length * 6, 140), height: 14 }]
157
+ ? [
158
+ {
159
+ id: `${e.id}__lbl`,
160
+ text: e.name,
161
+ width: Math.min(e.name.length * 6, 140),
162
+ height: 14,
163
+ },
164
+ ]
158
165
  : [],
159
166
  layoutOptions: {
160
167
  'elk.layered.priority.straightness': isHappy ? '10' : 1,
@@ -926,6 +926,7 @@ export {
926
926
  buildProcess,
927
927
  parseDiagram,
928
928
  normalizeProcessTree,
929
+ diagramGeometry,
929
930
  compareProcess,
930
931
  compareDiagram,
931
932
  diff,
@@ -92,8 +92,12 @@ function toSvg(structure) {
92
92
  );
93
93
  if (e.name) {
94
94
  const lp = e.labelPos;
95
- const lx = lp ? lp.x + lp.width / 2 : e.waypoints[Math.floor(e.waypoints.length / 2)][0];
96
- const ly = lp ? lp.y + lp.height / 2 : e.waypoints[Math.floor(e.waypoints.length / 2)][1] - 4;
95
+ const lx = lp
96
+ ? lp.x + lp.width / 2
97
+ : e.waypoints[Math.floor(e.waypoints.length / 2)][0];
98
+ const ly = lp
99
+ ? lp.y + lp.height / 2
100
+ : e.waypoints[Math.floor(e.waypoints.length / 2)][1] - 4;
97
101
  out.push(
98
102
  `<text x="${lx + ox}" y="${ly + oy}" text-anchor="middle" fill="#666" font-size="9">${esc(e.name)}</text>`
99
103
  );
@@ -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';
@@ -355,10 +363,89 @@ function stableStringify(value) {
355
363
  return JSON.stringify(value ?? null);
356
364
  }
357
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
+ // `status`/`version` are the server's publication lifecycle, not content —
395
+ // excluded for the same reason as the audit fields above, but only at the top
396
+ // level (page wrappers carry neither). They surface in process.yaml via
397
+ // EDITABLE_SCALARS and are still sent on push; they just don't count as a
398
+ // change. Every PATCH flips the wizard server-side to draft/modified while
399
+ // process.yaml keeps whatever status was recorded at the last clone/pull, and
400
+ // push stores the *local* checksum as the new baseline — so a push not
401
+ // followed by a publish guaranteed a phantom `conflict` on the next push,
402
+ // with zero content difference on either side (observed live 2026-07-21 on
403
+ // ml_voltura_fibra_data_input: upstream came back `modified` against a
404
+ // `published` process.yaml, shifting the remote checksum off the baseline on
405
+ // its own). `version` is worse still: a publish that bumps it server-side
406
+ // pins the object to `conflict` permanently, since nothing local resyncs it.
407
+ const VOLATILE_LIFECYCLE_FIELDS = ['status', 'version'];
408
+
409
+ function canonicalForChecksum(payload) {
410
+ const { process, decls } = normalizeProcessTree(payload.built_page);
411
+ const rest = omit(payload, [
412
+ 'built_page',
413
+ 'pages',
414
+ ...VOLATILE_AUDIT_FIELDS,
415
+ ...VOLATILE_LIFECYCLE_FIELDS,
416
+ ]);
417
+ // Page order is not a stable identity — locally it's whatever order the
418
+ // manifest happened to record at last decompose, while a live upstream
419
+ // fetch can return the same set of pages in a different order (observed
420
+ // live: a fresh fetch came back with pages 1-3 cyclically rotated versus
421
+ // the local manifest, same guids, zero content difference). Sort by the
422
+ // page's own guid — stable, unlike array position — before hashing, the
423
+ // same trick normalizeProcessTree already uses for extraDefs decls.
424
+ const pages = [...(payload.pages || [])]
425
+ .sort((a, b) => (a.guid ?? '').localeCompare(b.guid ?? ''))
426
+ .map((p) => omit(p, VOLATILE_AUDIT_FIELDS));
427
+ return {
428
+ ...rest,
429
+ pages,
430
+ built_page_process: process,
431
+ built_page_decls: decls,
432
+ built_page_diagram: diagramGeometry(payload.built_page),
433
+ };
434
+ }
435
+
436
+ // Checksum of a recomposed payload, canonicalized so cosmetic round-trip
437
+ // differences don't register as a change. Used for checksum_at_pull baselines
438
+ // (clone/pull) and for the push/pull classifier (localChecksum/
439
+ // remoteChecksumPb below) — keep every PB/LRP checksum call site on this one
440
+ // function so baselines and comparisons stay on the same normalization.
441
+ function checksumOfPayload(payload) {
442
+ return computeChecksum(stableStringify(canonicalForChecksum(payload)));
443
+ }
444
+
358
445
  // Checksum of the recomposed payload — stable across runs, changes only when the
359
446
  // local files change. clonePb stores this as checksum_at_pull; push recomputes it.
360
447
  function localChecksum(read) {
361
- return computeChecksum(stableStringify(recompose(read)));
448
+ return checksumOfPayload(recompose(read));
362
449
  }
363
450
 
364
451
  // Semantic checksum of a remote payload. Decompose → recompose normalises field
@@ -367,7 +454,7 @@ function localChecksum(read) {
367
454
  function remoteChecksumPb(payload) {
368
455
  const { files } = decompose(payload);
369
456
  const read = (p) => files[p] ?? '';
370
- return computeChecksum(stableStringify(recompose(read)));
457
+ return checksumOfPayload(recompose(read));
371
458
  }
372
459
 
373
460
  // Enumerate the wizard's pages from the pages/*.yml files (the authoritative
@@ -406,6 +493,7 @@ export {
406
493
  comparePayload,
407
494
  verifyRoundTrip,
408
495
  stableStringify,
496
+ checksumOfPayload,
409
497
  localChecksum,
410
498
  remoteChecksumPb,
411
499
  enumeratePages,
@@ -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,7 +277,7 @@ 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
 
@@ -286,7 +285,7 @@ async function autopr(options) {
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 };