@waron97/prbot 3.1.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +50 -11
  2. package/agrippa-pb.md +350 -0
  3. package/package.json +7 -3
  4. package/src/agrippa/commands/clone.js +29 -13
  5. package/src/agrippa/commands/clonePb.js +107 -0
  6. package/src/agrippa/commands/diff.js +94 -18
  7. package/src/agrippa/commands/init.js +66 -12
  8. package/src/agrippa/commands/initPhase.js +16 -17
  9. package/src/agrippa/commands/pb.js +279 -0
  10. package/src/agrippa/commands/pull.js +119 -13
  11. package/src/agrippa/commands/pullPb.js +54 -0
  12. package/src/agrippa/commands/push.js +112 -47
  13. package/src/agrippa/commands/pushPb.js +87 -0
  14. package/src/agrippa/commands/repair.js +3 -1
  15. package/src/agrippa/index.js +138 -14
  16. package/src/agrippa/lib/api.js +17 -3
  17. package/src/agrippa/lib/checksum.js +3 -1
  18. package/src/agrippa/lib/config.js +2 -2
  19. package/src/agrippa/lib/pbApi.js +71 -0
  20. package/src/agrippa/lib/pbEdit.js +467 -0
  21. package/src/agrippa/lib/pbLayout.js +283 -0
  22. package/src/agrippa/lib/pbModel.js +698 -0
  23. package/src/agrippa/lib/pbPreview.js +151 -0
  24. package/src/agrippa/lib/pbProject.js +390 -0
  25. package/src/agrippa/lib/pbWorkspace.js +30 -0
  26. package/src/agrippa/lib/workspace.js +23 -3
  27. package/src/commands/autopr.js +5 -2
  28. package/src/commands/changelog.js +4 -1
  29. package/src/commands/export.js +3 -3
  30. package/src/commands/exportEmailTemplates.js +25 -15
  31. package/src/commands/exportImperex.js +4 -4
  32. package/src/commands/exportLrp.js +10 -7
  33. package/src/commands/exportPb.js +4 -5
  34. package/src/commands/exportWorkflow.js +27 -14
  35. package/src/commands/init.js +7 -0
  36. package/src/commands/routine.js +7 -5
  37. package/src/index.js +24 -7
  38. package/src/lib/premigrate.js +3 -3
  39. package/src/lib/updateCheck.js +5 -2
@@ -0,0 +1,107 @@
1
+ import search from '@inquirer/search';
2
+ import inquirer from 'inquirer';
3
+ import { getToken } from '../../lib/auth.js';
4
+ import { fuzzyMatch } from '../../lib/fuzzy.js';
5
+ import { computeChecksum } from '../lib/checksum.js';
6
+ import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
7
+ import { getProcess, listProcesses } from '../lib/pbApi.js';
8
+ import { comparePayload, decompose, recompose, stableStringify } from '../lib/pbProject.js';
9
+ import { projectReader, writeProject } from '../lib/pbWorkspace.js';
10
+
11
+ async function clonePb(opts) {
12
+ const config = readConfig();
13
+ loadEffectiveEnv(config);
14
+
15
+ if (!process.env.PB_URL) {
16
+ throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
17
+ }
18
+
19
+ console.log('Fetching process list...');
20
+ const token = await getToken();
21
+ const processes = await listProcesses(token);
22
+ if (!processes.length) {
23
+ console.log('No process-builder wizards found.');
24
+ return;
25
+ }
26
+
27
+ // Select the process: by --name (document_id) or interactive fuzzy search.
28
+ let chosen;
29
+ if (opts.name) {
30
+ chosen = processes.find((p) => p.document_id === opts.name);
31
+ if (!chosen) throw new Error(`No process found with document_id "${opts.name}"`);
32
+ } else {
33
+ chosen = await search({
34
+ message: 'Select a process-builder wizard:',
35
+ source: (input) => {
36
+ const list = input
37
+ ? processes.filter(
38
+ (p) =>
39
+ fuzzyMatch(p.process_name, input) || fuzzyMatch(p.document_id, input)
40
+ )
41
+ : processes;
42
+ return list.map((p) => ({
43
+ name: `${p.process_name} (${p.document_id})`,
44
+ value: p,
45
+ }));
46
+ },
47
+ });
48
+ }
49
+
50
+ // Destination directory.
51
+ let dest = opts.path ?? null;
52
+ if (!dest) {
53
+ const { inputPath } = await inquirer.prompt([
54
+ {
55
+ type: 'input',
56
+ name: 'inputPath',
57
+ message: 'Destination directory:',
58
+ default: chosen.document_id,
59
+ },
60
+ ]);
61
+ dest = inputPath;
62
+ }
63
+
64
+ console.log(`Fetching "${chosen.process_name}"...`);
65
+ const payload = await getProcess(token, chosen.guid);
66
+
67
+ const { files } = decompose(payload);
68
+ writeProject(dest, files);
69
+
70
+ const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
71
+ const pageCount = Object.keys(files).filter((p) => p.startsWith('pages/')).length;
72
+ console.log(`Cloned to ${dest}/ (${scriptCount} script(s), ${pageCount} page(s))`);
73
+
74
+ // Prove the clone reconstructs the original payload (0-loss bar A) by
75
+ // reading the files back from disk and recomposing.
76
+ const rebuilt = recompose(projectReader(dest));
77
+ const diffs = comparePayload(payload, rebuilt);
78
+ if (diffs.length) {
79
+ console.warn('WARNING: round-trip verification found differences:');
80
+ diffs.forEach((d) => console.warn(' - ' + d));
81
+ } else {
82
+ console.log('Round-trip verified: recomposed payload is identical (0 information loss).');
83
+ }
84
+
85
+ // Register in the workspace for later pull/push.
86
+ config.workspace = config.workspace || [];
87
+ const existing = config.workspace.findIndex(
88
+ (e) => e.object_type === 'process_builder' && e.guid === chosen.guid
89
+ );
90
+ const entry = {
91
+ path: dest,
92
+ object_type: 'process_builder',
93
+ guid: chosen.guid,
94
+ document_id: chosen.document_id,
95
+ name: chosen.process_name,
96
+ // Baselines for `push` classification: checksum of the *recomposed* payload
97
+ // (changes only when local files change) + upstream updated_date/status.
98
+ checksum_at_pull: computeChecksum(stableStringify(rebuilt)),
99
+ updated_date: payload.updated_date,
100
+ status: payload.status,
101
+ };
102
+ if (existing >= 0) config.workspace[existing] = entry;
103
+ else config.workspace.push(entry);
104
+ writeConfig(config);
105
+ }
106
+
107
+ export { clonePb };
@@ -1,20 +1,29 @@
1
- import { existsSync, statSync, writeFileSync, unlinkSync } from 'fs';
2
- import { join } from 'path';
3
- import { tmpdir } from 'os';
4
1
  import { spawnSync } from 'child_process';
5
- import { readConfig, loadEffectiveEnv } from '../lib/config.js';
6
- import { fetchRemoteCode } from './pull.js';
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ rmSync,
8
+ statSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from 'fs';
12
+ import { tmpdir } from 'os';
13
+ import { join } from 'path';
7
14
  import { getToken } from '../../lib/auth.js';
8
- import { readCodeFile, fileExists } from '../lib/workspace.js';
9
15
  import { computeChecksum } from '../lib/checksum.js';
16
+ import { loadEffectiveEnv, readConfig } from '../lib/config.js';
17
+ import { getProcess } from '../lib/pbApi.js';
18
+ import { decompose, localChecksum } from '../lib/pbProject.js';
19
+ import { projectReader, writeProject } from '../lib/pbWorkspace.js';
20
+ import { fileExists, readCodeFile } from '../lib/workspace.js';
21
+ import { fetchRemoteCode } from './pull.js';
10
22
 
11
23
  async function diff(targetArg) {
12
24
  const config = readConfig();
13
25
  loadEffectiveEnv(config);
14
26
 
15
- const ripUrl = process.env.RIP_URL;
16
- if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
17
-
18
27
  if (!config.workspace.length) {
19
28
  console.log('No tracked resources. Run `agrippa clone` first.');
20
29
  return;
@@ -26,13 +35,40 @@ async function diff(targetArg) {
26
35
  return;
27
36
  }
28
37
 
38
+ const pbEntries = entries.filter((e) => e.object_type === 'process_builder');
39
+ const codeEntries = entries.filter((e) => e.object_type !== 'process_builder');
40
+
41
+ if (codeEntries.length && !process.env.RIP_URL)
42
+ throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
43
+ if (pbEntries.length && !process.env.PB_URL)
44
+ throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
45
+
29
46
  console.log('Fetching remote code...');
30
47
  const token = await getToken();
31
- const remoteCodeMap = await fetchRemoteCode(token, ripUrl, entries);
32
48
 
33
49
  let diffCount = 0;
34
- const tmpFiles = [];
35
50
 
51
+ if (codeEntries.length) {
52
+ const remoteCodeMap = await fetchRemoteCode(token, process.env.RIP_URL, codeEntries);
53
+ diffCount += diffCodeEntries(codeEntries, remoteCodeMap);
54
+ }
55
+
56
+ for (const entry of pbEntries) {
57
+ if (await diffPbEntry(token, entry)) diffCount++;
58
+ }
59
+
60
+ if (diffCount === 0) {
61
+ console.log('No differences found — all tracked files match the remote.');
62
+ } else {
63
+ console.log(`\n${diffCount} file(s) differ from remote.`);
64
+ }
65
+ }
66
+
67
+ // Single-file phase/mfa code entries: write the remote body to a tmp file and
68
+ // hand both sides to `git diff --no-index`.
69
+ function diffCodeEntries(entries, remoteCodeMap) {
70
+ let diffCount = 0;
71
+ const tmpFiles = [];
36
72
  try {
37
73
  for (const entry of entries) {
38
74
  const key = `${entry.object_type}:${entry.id}`;
@@ -46,7 +82,6 @@ async function diff(targetArg) {
46
82
  continue;
47
83
  }
48
84
 
49
- // Write remote content to a temp file so git diff --no-index can compare
50
85
  const tmpPath = join(tmpdir(), `agrippa-remote-${entry.object_type}-${entry.id}.py`);
51
86
  writeFileSync(tmpPath, (remoteCode ?? '').trim() + '\n', 'utf-8');
52
87
  tmpFiles.push(tmpPath);
@@ -56,7 +91,7 @@ async function diff(targetArg) {
56
91
  const result = spawnSync(
57
92
  'git',
58
93
  ['diff', '--no-index', '--color=always', tmpPath, entry.path],
59
- { stdio: ['ignore', 'inherit', 'inherit'] },
94
+ { stdio: ['ignore', 'inherit', 'inherit'] }
60
95
  );
61
96
  // exit code 1 means differences found (normal), 0 means identical, >1 means error
62
97
  if (result.status !== null && result.status > 1) {
@@ -66,14 +101,55 @@ async function diff(targetArg) {
66
101
  }
67
102
  } finally {
68
103
  for (const f of tmpFiles) {
69
- try { unlinkSync(f); } catch { /* ignore */ }
104
+ try {
105
+ unlinkSync(f);
106
+ } catch {
107
+ /* ignore */
108
+ }
70
109
  }
71
110
  }
111
+ return diffCount;
112
+ }
72
113
 
73
- if (diffCount === 0) {
74
- console.log('No differences found all tracked files match the remote.');
75
- } else {
76
- console.log(`\n${diffCount} file(s) differ from remote.`);
114
+ // Process-builder wizard: decompose the upstream payload into a throwaway
115
+ // project tree (exactly what re-cloning now would produce) and diff it against
116
+ // a copy of the local project, recursively. `.backup/` and `preview.svg` are
117
+ // local-only artifacts (push backups, dev preview render) with no upstream
118
+ // counterpart, so they're stripped from the local copy before diffing.
119
+ async function diffPbEntry(token, entry) {
120
+ const upstream = await getProcess(token, entry.guid);
121
+ if (!upstream) throw new Error(`could not fetch upstream wizard ${entry.guid}`);
122
+
123
+ const tmpRoot = mkdtempSync(join(tmpdir(), 'agrippa-pb-diff-'));
124
+ try {
125
+ const upstreamDir = join(tmpRoot, 'upstream');
126
+ const localDir = join(tmpRoot, 'local');
127
+ mkdirSync(upstreamDir, { recursive: true });
128
+ writeProject(upstreamDir, decompose(upstream).files);
129
+
130
+ cpSync(entry.path, localDir, { recursive: true });
131
+ rmSync(join(localDir, '.backup'), { recursive: true, force: true });
132
+ rmSync(join(localDir, 'preview.svg'), { force: true });
133
+
134
+ // Compare via the recompose pipeline on both sides (not raw upstream JSON
135
+ // vs. local): the BPMN xml is regenerated from structure.yaml on rebuild
136
+ // and never matches the raw upstream xml byte-for-byte even when nothing
137
+ // structural changed, so it'd otherwise look like a permanent false diff.
138
+ if (localChecksum(projectReader(upstreamDir)) === localChecksum(projectReader(localDir)))
139
+ return false;
140
+
141
+ console.log(`\n=== ${entry.path} [${entry.name}] (process-builder) ===`);
142
+ const result = spawnSync(
143
+ 'git',
144
+ ['diff', '--no-index', '--color=always', 'upstream', 'local'],
145
+ { cwd: tmpRoot, stdio: ['ignore', 'inherit', 'inherit'] }
146
+ );
147
+ if (result.status !== null && result.status > 1) {
148
+ console.error(`git diff failed for ${entry.path}`);
149
+ }
150
+ return true;
151
+ } finally {
152
+ rmSync(tmpRoot, { recursive: true, force: true });
77
153
  }
78
154
  }
79
155
 
@@ -1,6 +1,13 @@
1
- import { existsSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
2
- import { fileURLToPath } from 'url';
1
+ import {
2
+ appendFileSync,
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ } from 'fs';
3
9
  import { join } from 'path';
10
+ import { fileURLToPath } from 'url';
4
11
  import inquirer from 'inquirer';
5
12
  import { WORKSPACE_FILE } from '../lib/config.js';
6
13
 
@@ -44,16 +51,17 @@ builtins = [
44
51
  ]
45
52
  `;
46
53
 
47
- const PYRIGHTCONFIG = JSON.stringify(
48
- {
49
- pythonVersion: '3.10',
50
- stubPath: 'typings',
51
- typeCheckingMode: 'standard',
52
- reportArgumentType: 'none',
53
- },
54
- null,
55
- 2,
56
- ) + '\n';
54
+ const PYRIGHTCONFIG =
55
+ JSON.stringify(
56
+ {
57
+ pythonVersion: '3.10',
58
+ stubPath: 'typings',
59
+ typeCheckingMode: 'standard',
60
+ reportArgumentType: 'none',
61
+ },
62
+ null,
63
+ 2
64
+ ) + '\n';
57
65
 
58
66
  const TYPING_FILES = [
59
67
  '__builtins__.pyi',
@@ -63,6 +71,17 @@ const TYPING_FILES = [
63
71
  'b2w_entities.pyi',
64
72
  ];
65
73
 
74
+ // The copied guidance is wrapped in these sentinels so a re-run can find the
75
+ // managed block and refresh it in place (when agrippa-pb.md changes) without
76
+ // touching the human's own CLAUDE.md content outside the markers.
77
+ const PB_BEGIN = '<!-- BEGIN agrippa-pb guidance (managed by `agrippa init`) -->';
78
+ const PB_END = '<!-- END agrippa-pb guidance -->';
79
+
80
+ // Wrap the guide in the managed block.
81
+ function pbBlock(guide) {
82
+ return `${PB_BEGIN}\n\n${guide.trimEnd()}\n\n${PB_END}\n`;
83
+ }
84
+
66
85
  async function init() {
67
86
  if (existsSync(WORKSPACE_FILE)) {
68
87
  const { overwrite } = await inquirer.prompt([
@@ -115,6 +134,41 @@ async function init() {
115
134
  }
116
135
  console.log(`Copied ${TYPING_FILES.length} type stubs to typings/`);
117
136
  }
137
+
138
+ const { importInstructions } = await inquirer.prompt([
139
+ {
140
+ type: 'confirm',
141
+ name: 'importInstructions',
142
+ message: 'Copy agrippa-pb.md guidance into local CLAUDE.md?',
143
+ default: true,
144
+ },
145
+ ]);
146
+
147
+ if (importInstructions) {
148
+ const guidePath = fileURLToPath(new URL('../../../agrippa-pb.md', import.meta.url));
149
+ const block = pbBlock(readFileSync(guidePath, 'utf-8'));
150
+ if (!existsSync('CLAUDE.md')) {
151
+ writeFileSync('CLAUDE.md', block, 'utf-8');
152
+ console.log('Created CLAUDE.md with agrippa-pb guidance');
153
+ } else {
154
+ const current = readFileSync('CLAUDE.md', 'utf-8');
155
+ const begin = current.indexOf(PB_BEGIN);
156
+ if (begin === -1) {
157
+ appendFileSync('CLAUDE.md', `\n${block}`, 'utf-8');
158
+ console.log('Appended agrippa-pb guidance to CLAUDE.md');
159
+ } else {
160
+ // Replace the existing managed block in place; leave the rest as-is.
161
+ const endIdx = current.indexOf(PB_END, begin);
162
+ if (endIdx === -1)
163
+ throw new Error(
164
+ 'CLAUDE.md has a malformed agrippa-pb block (BEGIN without END) — fix it by hand.'
165
+ );
166
+ const after = current.slice(endIdx + PB_END.length).replace(/^\n/, '');
167
+ writeFileSync('CLAUDE.md', current.slice(0, begin) + block + after, 'utf-8');
168
+ console.log('Refreshed agrippa-pb guidance in CLAUDE.md');
169
+ }
170
+ }
171
+ }
118
172
  }
119
173
 
120
174
  export { init };
@@ -1,17 +1,17 @@
1
- import inquirer from 'inquirer';
2
1
  import search from '@inquirer/search';
3
- import { readConfig, loadEffectiveEnv } from '../lib/config.js';
2
+ import inquirer from 'inquirer';
3
+ import { getToken } from '../../lib/auth.js';
4
+ import { fuzzyMatch } from '../../lib/fuzzy.js';
4
5
  import {
5
- listWorkflows,
6
- getPhasesByWorkflow,
6
+ createConfigurator,
7
+ deleteConfigurator,
8
+ getPhaseConfigurators,
7
9
  getPhaseResults,
10
+ getPhasesByWorkflow,
8
11
  initPhaseRemote,
9
- getPhaseConfigurators,
10
- deleteConfigurator,
11
- createConfigurator,
12
+ listWorkflows,
12
13
  } from '../lib/api.js';
13
- import { getToken } from '../../lib/auth.js';
14
- import { fuzzyMatch } from '../../lib/fuzzy.js';
14
+ import { loadEffectiveEnv, readConfig } from '../lib/config.js';
15
15
 
16
16
  const CODE_TEMPLATE = `logs = []
17
17
  debug_logs = []
@@ -72,7 +72,8 @@ async function initPhase() {
72
72
  loadEffectiveEnv(config);
73
73
 
74
74
  const ripUrl = process.env.RIP_URL;
75
- if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
75
+ if (!ripUrl)
76
+ throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
76
77
 
77
78
  console.log('Fetching workflows...');
78
79
  const token = await getToken();
@@ -86,9 +87,7 @@ async function initPhase() {
86
87
  const workflow = await search({
87
88
  message: 'Select a workflow:',
88
89
  source: (input) => {
89
- const filtered = input
90
- ? workflows.filter((w) => fuzzyMatch(w.name, input))
91
- : workflows;
90
+ const filtered = input ? workflows.filter((w) => fuzzyMatch(w.name, input)) : workflows;
92
91
  return filtered.map((w) => ({ name: w.name, value: w }));
93
92
  },
94
93
  });
@@ -104,9 +103,7 @@ async function initPhase() {
104
103
  const phase = await search({
105
104
  message: 'Select a phase:',
106
105
  source: (input) => {
107
- const filtered = input
108
- ? phases.filter((p) => fuzzyMatch(p.name, input))
109
- : phases;
106
+ const filtered = input ? phases.filter((p) => fuzzyMatch(p.name, input)) : phases;
110
107
  return filtered.map((p) => ({ name: p.name, value: p }));
111
108
  },
112
109
  });
@@ -156,7 +153,9 @@ async function initPhase() {
156
153
  if (results.length > 0) {
157
154
  console.log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
158
155
  }
159
- console.log(`Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`);
156
+ console.log(
157
+ `Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`
158
+ );
160
159
  }
161
160
 
162
161
  export { initPhase };