@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.
@@ -0,0 +1,114 @@
1
+ import search from '@inquirer/search';
2
+ import inquirer from 'inquirer';
3
+ import { getToken } from '../../lib/auth.js';
4
+ import { log, warn } from '../../lib/logger.js';
5
+ import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
6
+ import { getLrpXml, listLrps, resolveLrpByName } from '../lib/lrpApi.js';
7
+ import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
8
+ import { projectReader, writeProject } from '../lib/pbWorkspace.js';
9
+
10
+ // Clone a long-running process (LRP). Structurally identical to a PB clone
11
+ // (same decompose/recompose/checksum machinery — LRPs are plain Activiti BPMN
12
+ // too), but selection and identity are name-based: the tabulator `id` changes
13
+ // on every save/version bump (verified live — a re-fetched id differs from a
14
+ // previously captured one for the same process), so it can never be used as
15
+ // a stable workspace key the way PB's guid is.
16
+ async function cloneLrp(opts) {
17
+ const config = readConfig();
18
+ loadEffectiveEnv(config);
19
+
20
+ if (!process.env.IMPORTEXPORT_URL) {
21
+ throw new Error(
22
+ 'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
23
+ );
24
+ }
25
+
26
+ const token = await getToken();
27
+
28
+ // Selection: by --name (exact) or interactive server-side search (LRPs
29
+ // are searched server-side by name — see listLrps — not fetched-all-then-
30
+ // fuzzy-filtered like PBs).
31
+ let chosen;
32
+ if (opts.name) {
33
+ chosen = await resolveLrpByName(token, opts.name);
34
+ } else {
35
+ log('Fetching long-running process list...');
36
+ const initial = await listLrps(token, null);
37
+ let controller = null;
38
+ chosen = await search({
39
+ message: 'Select a long-running process:',
40
+ source: async (input) => {
41
+ if (!input) return initial.map((p) => ({ name: p.name, value: p }));
42
+ if (controller) controller.abort();
43
+ controller = new AbortController();
44
+ try {
45
+ const list = await listLrps(token, input, controller.signal);
46
+ return list.map((p) => ({ name: p.name, value: p }));
47
+ } catch {
48
+ return initial.map((p) => ({ name: p.name, value: p }));
49
+ }
50
+ },
51
+ });
52
+ }
53
+
54
+ // Destination directory.
55
+ let dest = opts.path ?? null;
56
+ if (!dest) {
57
+ const { inputPath } = await inquirer.prompt([
58
+ {
59
+ type: 'input',
60
+ name: 'inputPath',
61
+ message: 'Destination directory:',
62
+ default: chosen.name.replace(/^B2WA_/, ''),
63
+ },
64
+ ]);
65
+ dest = inputPath;
66
+ }
67
+
68
+ log(`Fetching "${chosen.name}"...`);
69
+ const { xml, description } = await getLrpXml(token, chosen.id);
70
+ const payload = { built_page: xml };
71
+
72
+ const { files } = decompose(payload);
73
+ writeProject(dest, files);
74
+
75
+ const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
76
+ log(`Cloned to ${dest}/ (${scriptCount} script(s)).`);
77
+
78
+ // Prove the clone reconstructs the original XML (0-loss bar A). LRPs have
79
+ // no `pages` at all — recompose always synthesizes `pages: []`, which
80
+ // comparePayload (shared with PB, where it's meaningful) would otherwise
81
+ // flag as noise on every single LRP clone.
82
+ const rebuilt = recompose(projectReader(dest));
83
+ const diffs = comparePayload(payload, rebuilt).filter((d) => !d.startsWith('pages:'));
84
+ if (diffs.length) {
85
+ warn('WARNING: round-trip verification found differences:');
86
+ diffs.forEach((d) => warn(' - ' + d));
87
+ } else {
88
+ log('Round-trip verified: recomposed payload is identical (0 information loss).');
89
+ }
90
+
91
+ // Register in the workspace for later pull/push — keyed by NAME, not id.
92
+ config.workspace = config.workspace || [];
93
+ const existing = config.workspace.findIndex(
94
+ (e) => e.object_type === 'long_running_process' && e.name === chosen.name
95
+ );
96
+ const entry = {
97
+ path: dest,
98
+ object_type: 'long_running_process',
99
+ name: chosen.name,
100
+ tenant_id: chosen.tenantId,
101
+ svg: chosen.bpmnFileSvg,
102
+ description,
103
+ // Baseline for push classification (see pull.js/push.js): checksum of
104
+ // the *recomposed* payload, changes only when local files change.
105
+ checksum_at_pull: checksumOfPayload(rebuilt),
106
+ version: chosen.version,
107
+ status: chosen.status,
108
+ };
109
+ if (existing >= 0) config.workspace[existing] = entry;
110
+ else config.workspace.push(entry);
111
+ writeConfig(config);
112
+ }
113
+
114
+ export { cloneLrp };
@@ -2,10 +2,10 @@ import search from '@inquirer/search';
2
2
  import inquirer from 'inquirer';
3
3
  import { getToken } from '../../lib/auth.js';
4
4
  import { fuzzyMatch } from '../../lib/fuzzy.js';
5
- import { computeChecksum } from '../lib/checksum.js';
5
+ import { log, warn } from '../../lib/logger.js';
6
6
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
7
7
  import { getProcess, listProcesses } from '../lib/pbApi.js';
8
- import { comparePayload, decompose, recompose, stableStringify } from '../lib/pbProject.js';
8
+ import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
9
9
  import { projectReader, writeProject } from '../lib/pbWorkspace.js';
10
10
 
11
11
  async function clonePb(opts) {
@@ -16,11 +16,11 @@ async function clonePb(opts) {
16
16
  throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
17
17
  }
18
18
 
19
- console.log('Fetching process list...');
19
+ log('Fetching process list...');
20
20
  const token = await getToken();
21
21
  const processes = await listProcesses(token);
22
22
  if (!processes.length) {
23
- console.log('No process-builder wizards found.');
23
+ log('No process-builder wizards found.');
24
24
  return;
25
25
  }
26
26
 
@@ -61,7 +61,7 @@ async function clonePb(opts) {
61
61
  dest = inputPath;
62
62
  }
63
63
 
64
- console.log(`Fetching "${chosen.process_name}"...`);
64
+ log(`Fetching "${chosen.process_name}"...`);
65
65
  const payload = await getProcess(token, chosen.guid);
66
66
 
67
67
  const { files } = decompose(payload);
@@ -69,17 +69,17 @@ async function clonePb(opts) {
69
69
 
70
70
  const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
71
71
  const pageCount = Object.keys(files).filter((p) => p.startsWith('pages/')).length;
72
- console.log(`Cloned to ${dest}/ (${scriptCount} script(s), ${pageCount} page(s))`);
72
+ log(`Cloned to ${dest}/ (${scriptCount} script(s), ${pageCount} page(s))`);
73
73
 
74
74
  // Prove the clone reconstructs the original payload (0-loss bar A) by
75
75
  // reading the files back from disk and recomposing.
76
76
  const rebuilt = recompose(projectReader(dest));
77
77
  const diffs = comparePayload(payload, rebuilt);
78
78
  if (diffs.length) {
79
- console.warn('WARNING: round-trip verification found differences:');
80
- diffs.forEach((d) => console.warn(' - ' + d));
79
+ warn('WARNING: round-trip verification found differences:');
80
+ diffs.forEach((d) => warn(' - ' + d));
81
81
  } else {
82
- console.log('Round-trip verified: recomposed payload is identical (0 information loss).');
82
+ log('Round-trip verified: recomposed payload is identical (0 information loss).');
83
83
  }
84
84
 
85
85
  // Register in the workspace for later pull/push.
@@ -95,7 +95,7 @@ async function clonePb(opts) {
95
95
  name: chosen.process_name,
96
96
  // Baselines for `push` classification: checksum of the *recomposed* payload
97
97
  // (changes only when local files change) + upstream updated_date/status.
98
- checksum_at_pull: computeChecksum(stableStringify(rebuilt)),
98
+ checksum_at_pull: checksumOfPayload(rebuilt),
99
99
  updated_date: payload.updated_date,
100
100
  status: payload.status,
101
101
  };
@@ -12,6 +12,7 @@ import {
12
12
  import { tmpdir } from 'os';
13
13
  import { join } from 'path';
14
14
  import { getToken } from '../../lib/auth.js';
15
+ import { error, log } from '../../lib/logger.js';
15
16
  import { computeChecksum } from '../lib/checksum.js';
16
17
  import { loadEffectiveEnv, readConfig } from '../lib/config.js';
17
18
  import { getProcess } from '../lib/pbApi.js';
@@ -25,13 +26,13 @@ async function diff(targetArg) {
25
26
  loadEffectiveEnv(config);
26
27
 
27
28
  if (!config.workspace.length) {
28
- console.log('No tracked resources. Run `agrippa clone` first.');
29
+ log('No tracked resources. Run `agrippa clone` first.');
29
30
  return;
30
31
  }
31
32
 
32
33
  const entries = filterEntries(config.workspace, targetArg);
33
34
  if (!entries.length) {
34
- console.log('No tracked files match the given path.');
35
+ log('No tracked files match the given path.');
35
36
  return;
36
37
  }
37
38
 
@@ -43,7 +44,7 @@ async function diff(targetArg) {
43
44
  if (pbEntries.length && !process.env.PB_URL)
44
45
  throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
45
46
 
46
- console.log('Fetching remote code...');
47
+ log('Fetching remote code...');
47
48
  const token = await getToken();
48
49
 
49
50
  let diffCount = 0;
@@ -65,13 +66,13 @@ async function diff(targetArg) {
65
66
  }
66
67
 
67
68
  if (diffCount === 0) {
68
- console.log('No differences found — all tracked files match the remote.');
69
+ log('No differences found — all tracked files match the remote.');
69
70
  } else {
70
71
  const combined = Buffer.concat(chunks);
71
72
  const pager = process.env.PAGER || 'less';
72
73
  const pagerArgs = pager === 'less' ? ['-R', '-F'] : [];
73
74
  spawnSync(pager, pagerArgs, { input: combined, stdio: ['pipe', 'inherit', 'inherit'] });
74
- console.log(`\n${diffCount} file(s) differ from remote.`);
75
+ log(`\n${diffCount} file(s) differ from remote.`);
75
76
  }
76
77
  }
77
78
 
@@ -90,7 +91,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
90
91
  if (computeChecksum(localCode) === computeChecksum(remoteCode)) continue;
91
92
 
92
93
  if (!fileExists(entry.path)) {
93
- console.log(`\n--- ${entry.path} (local file missing)`);
94
+ log(`\n--- ${entry.path} (local file missing)`);
94
95
  continue;
95
96
  }
96
97
 
@@ -105,7 +106,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
105
106
  );
106
107
  // exit code 1 means differences found (normal), 0 means identical, >1 means error
107
108
  if (result.status !== null && result.status > 1) {
108
- console.error(`git diff failed for ${entry.path}`);
109
+ error(`git diff failed for ${entry.path}`);
109
110
  }
110
111
  const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] ===\n`);
111
112
  chunks.push(Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]));
@@ -156,7 +157,7 @@ async function diffPbEntry(token, entry) {
156
157
  { cwd: tmpRoot, stdio: ['ignore', 'pipe', 'pipe'] }
157
158
  );
158
159
  if (result.status !== null && result.status > 1) {
159
- console.error(`git diff failed for ${entry.path}`);
160
+ error(`git diff failed for ${entry.path}`);
160
161
  }
161
162
  const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] (process-builder) ===\n`);
162
163
  const chunk = Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]);
@@ -9,6 +9,7 @@ import {
9
9
  import { join } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import inquirer from 'inquirer';
12
+ import { log } from '../../lib/logger.js';
12
13
  import { WORKSPACE_FILE } from '../lib/config.js';
13
14
 
14
15
  const TEMPLATE = `# agrippa workspace configuration
@@ -93,23 +94,23 @@ async function init() {
93
94
  },
94
95
  ]);
95
96
  if (!overwrite) {
96
- console.log('Skipped workspace file.');
97
+ log('Skipped workspace file.');
97
98
  } else {
98
99
  writeFileSync(WORKSPACE_FILE, TEMPLATE, 'utf-8');
99
- console.log(`Created ${WORKSPACE_FILE}`);
100
- console.log(`Run 'agrippa clone' to add resources to this workspace.`);
101
- console.log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
100
+ log(`Created ${WORKSPACE_FILE}`);
101
+ log(`Run 'agrippa clone' to add resources to this workspace.`);
102
+ log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
102
103
  }
103
104
  } else {
104
105
  writeFileSync(WORKSPACE_FILE, TEMPLATE, 'utf-8');
105
- console.log(`Created ${WORKSPACE_FILE}`);
106
- console.log(`Run 'agrippa clone' to add resources to this workspace.`);
107
- console.log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
106
+ log(`Created ${WORKSPACE_FILE}`);
107
+ log(`Run 'agrippa clone' to add resources to this workspace.`);
108
+ log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
108
109
  }
109
110
 
110
111
  if (!existsSync('pyproject.toml')) {
111
112
  writeFileSync('pyproject.toml', PYPROJECT_TOML, 'utf-8');
112
- console.log('Created pyproject.toml (ruff builtins)');
113
+ log('Created pyproject.toml (ruff builtins)');
113
114
  }
114
115
 
115
116
  const { importTypings } = await inquirer.prompt([
@@ -124,7 +125,7 @@ async function init() {
124
125
  if (importTypings) {
125
126
  if (!existsSync('pyrightconfig.json')) {
126
127
  writeFileSync('pyrightconfig.json', PYRIGHTCONFIG, 'utf-8');
127
- console.log('Created pyrightconfig.json');
128
+ log('Created pyrightconfig.json');
128
129
  }
129
130
 
130
131
  const typingsDir = fileURLToPath(new URL('../../../agrippa_typings', import.meta.url));
@@ -132,7 +133,7 @@ async function init() {
132
133
  for (const file of TYPING_FILES) {
133
134
  copyFileSync(join(typingsDir, file), join('typings', file));
134
135
  }
135
- console.log(`Copied ${TYPING_FILES.length} type stubs to typings/`);
136
+ log(`Copied ${TYPING_FILES.length} type stubs to typings/`);
136
137
  }
137
138
 
138
139
  const { importInstructions } = await inquirer.prompt([
@@ -149,13 +150,13 @@ async function init() {
149
150
  const block = pbBlock(readFileSync(guidePath, 'utf-8'));
150
151
  if (!existsSync('CLAUDE.md')) {
151
152
  writeFileSync('CLAUDE.md', block, 'utf-8');
152
- console.log('Created CLAUDE.md with agrippa-pb guidance');
153
+ log('Created CLAUDE.md with agrippa-pb guidance');
153
154
  } else {
154
155
  const current = readFileSync('CLAUDE.md', 'utf-8');
155
156
  const begin = current.indexOf(PB_BEGIN);
156
157
  if (begin === -1) {
157
158
  appendFileSync('CLAUDE.md', `\n${block}`, 'utf-8');
158
- console.log('Appended agrippa-pb guidance to CLAUDE.md');
159
+ log('Appended agrippa-pb guidance to CLAUDE.md');
159
160
  } else {
160
161
  // Replace the existing managed block in place; leave the rest as-is.
161
162
  const endIdx = current.indexOf(PB_END, begin);
@@ -165,7 +166,7 @@ async function init() {
165
166
  );
166
167
  const after = current.slice(endIdx + PB_END.length).replace(/^\n/, '');
167
168
  writeFileSync('CLAUDE.md', current.slice(0, begin) + block + after, 'utf-8');
168
- console.log('Refreshed agrippa-pb guidance in CLAUDE.md');
169
+ log('Refreshed agrippa-pb guidance in CLAUDE.md');
169
170
  }
170
171
  }
171
172
  }
@@ -2,6 +2,7 @@ import search from '@inquirer/search';
2
2
  import inquirer from 'inquirer';
3
3
  import { getToken } from '../../lib/auth.js';
4
4
  import { fuzzyMatch } from '../../lib/fuzzy.js';
5
+ import { log } from '../../lib/logger.js';
5
6
  import {
6
7
  createConfigurator,
7
8
  deleteConfigurator,
@@ -75,12 +76,12 @@ async function initPhase() {
75
76
  if (!ripUrl)
76
77
  throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
77
78
 
78
- console.log('Fetching workflows...');
79
+ log('Fetching workflows...');
79
80
  const token = await getToken();
80
81
  const workflows = await listWorkflows(token, ripUrl);
81
82
 
82
83
  if (!workflows.length) {
83
- console.log('No workflows found.');
84
+ log('No workflows found.');
84
85
  return;
85
86
  }
86
87
 
@@ -92,70 +93,78 @@ async function initPhase() {
92
93
  },
93
94
  });
94
95
 
95
- console.log(`Fetching phases for "${workflow.name}"...`);
96
+ log(`Fetching phases for "${workflow.name}"...`);
96
97
  const phases = await getPhasesByWorkflow(token, ripUrl, workflow.id);
97
98
 
98
99
  if (!phases.length) {
99
- console.log('No phases found for this workflow.');
100
+ log('No phases found for this workflow.');
100
101
  return;
101
102
  }
102
103
 
103
- const phase = await search({
104
- message: 'Select a phase:',
105
- source: (input) => {
106
- const filtered = input ? phases.filter((p) => fuzzyMatch(p.name, input)) : phases;
107
- return filtered.map((p) => ({ name: p.name, value: p }));
104
+ const { selectedPhases } = await inquirer.prompt([
105
+ {
106
+ type: 'checkbox',
107
+ name: 'selectedPhases',
108
+ message: 'Select phases to initialize:',
109
+ choices: phases.map((p) => ({ name: p.name, value: p })),
110
+ loop: false,
108
111
  },
109
- });
112
+ ]);
110
113
 
111
- // Resolve result objects — API may return IDs or full objects
112
- let results = phase.allowed_phase_result_ids || [];
113
- if (results.length > 0 && typeof results[0] === 'number') {
114
- results = await getPhaseResults(token, ripUrl, results);
114
+ if (!selectedPhases.length) {
115
+ log('No phases selected. Aborted.');
116
+ return;
115
117
  }
116
118
 
117
- const code = generateCode(results);
118
-
119
119
  const { confirm } = await inquirer.prompt([
120
120
  {
121
121
  type: 'confirm',
122
122
  name: 'confirm',
123
- message: `Initialize phase "${phase.name}"? This will overwrite existing code.`,
123
+ message: `Initialize ${selectedPhases.length} phase(s)? This will overwrite existing code.`,
124
124
  default: true,
125
125
  },
126
126
  ]);
127
127
 
128
128
  if (!confirm) {
129
- console.log('Aborted.');
129
+ log('Aborted.');
130
130
  return;
131
131
  }
132
132
 
133
- // Delete existing configurator records for this phase
134
- const existing = await getPhaseConfigurators(token, ripUrl, phase.id);
135
- for (const cfg of existing) {
136
- await deleteConfigurator(token, ripUrl, cfg.id);
133
+ for (const phase of selectedPhases) {
134
+ // Resolve result objects API may return IDs or full objects
135
+ let results = phase.allowed_phase_result_ids || [];
136
+ if (results.length > 0 && typeof results[0] === 'number') {
137
+ results = await getPhaseResults(token, ripUrl, results);
138
+ }
139
+
140
+ const code = generateCode(results);
141
+
142
+ // Delete existing configurator records for this phase
143
+ const existing = await getPhaseConfigurators(token, ripUrl, phase.id);
144
+ for (const cfg of existing) {
145
+ await deleteConfigurator(token, ripUrl, cfg.id);
146
+ }
147
+
148
+ // Update phase code + set_result_automatically
149
+ await initPhaseRemote(token, ripUrl, phase.id, code);
150
+
151
+ // Create new configurator records for each result
152
+ const vars = results.map((_, i) => `RES${i + 1}`);
153
+ for (let i = 0; i < results.length; i++) {
154
+ await createConfigurator(token, ripUrl, {
155
+ result_value: vars[i],
156
+ code_phase_id: phase.id,
157
+ triplet_phase_result_id: results[i].id,
158
+ });
159
+ }
160
+
161
+ log(`Initialized phase "${phase.name}".`);
162
+ if (results.length > 0) {
163
+ log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
164
+ }
137
165
  }
138
166
 
139
- // Update phase code + set_result_automatically
140
- await initPhaseRemote(token, ripUrl, phase.id, code);
141
-
142
- // Create new configurator records for each result
143
- const vars = results.map((_, i) => `RES${i + 1}`);
144
- for (let i = 0; i < results.length; i++) {
145
- await createConfigurator(token, ripUrl, {
146
- result_value: vars[i],
147
- code_phase_id: phase.id,
148
- triplet_phase_result_id: results[i].id,
149
- });
150
- }
151
-
152
- console.log(`Initialized phase "${phase.name}".`);
153
- if (results.length > 0) {
154
- console.log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
155
- }
156
- console.log(
157
- `Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`
158
- );
167
+ log(`Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`);
159
168
  }
160
169
 
161
170
  export { initPhase };