@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.
- package/README.md +53 -37
- package/package.json +5 -1
- package/src/agrippa/commands/clone.js +10 -9
- package/src/agrippa/commands/cloneLrp.js +9 -9
- package/src/agrippa/commands/clonePb.js +10 -10
- package/src/agrippa/commands/diff.js +64 -25
- package/src/agrippa/commands/init.js +14 -13
- package/src/agrippa/commands/initPhase.js +10 -11
- package/src/agrippa/commands/pb.js +29 -26
- package/src/agrippa/commands/pull.js +67 -48
- package/src/agrippa/commands/pullLrp.js +2 -3
- package/src/agrippa/commands/pullPb.js +2 -3
- package/src/agrippa/commands/push.js +33 -24
- package/src/agrippa/commands/repair.js +4 -3
- package/src/agrippa/index.js +25 -13
- package/src/agrippa/lib/pbEdit.js +49 -2
- package/src/agrippa/lib/pbLayout.js +8 -1
- package/src/agrippa/lib/pbModel.js +1 -0
- package/src/agrippa/lib/pbPreview.js +6 -2
- package/src/agrippa/lib/pbProject.js +91 -3
- package/src/commands/autopr.js +20 -21
- package/src/commands/changelog.js +4 -3
- package/src/commands/commit.js +11 -10
- package/src/commands/export.js +2 -1
- package/src/commands/exportPb.js +19 -2
- package/src/commands/init.js +2 -1
- package/src/commands/routine.js +19 -8
- package/src/index.js +137 -112
- package/src/lib/auth.js +19 -1
- package/src/lib/logger.js +12 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ CLI tool for managing PRs, changelogs, and Odoo workflow XML files in the addons
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
Requires Node >= 20.
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
npm install -g @waron97/prbot
|
|
9
11
|
```
|
|
@@ -129,9 +131,11 @@ prbot export workflow --no-commit
|
|
|
129
131
|
|
|
130
132
|
Options:
|
|
131
133
|
|
|
132
|
-
| Flag
|
|
133
|
-
|
|
|
134
|
-
| `--no-commit`
|
|
134
|
+
| Flag | Description |
|
|
135
|
+
| -------------- | ------------------------------------------------------------- |
|
|
136
|
+
| `--no-commit` | Skip the git commit step |
|
|
137
|
+
| `-q, --quiet` | Suppress informational output — errors still fail the command |
|
|
138
|
+
| `-s, --silent` | Deprecated alias of `--quiet`; no longer swallows errors |
|
|
135
139
|
|
|
136
140
|
### `prbot export pb`
|
|
137
141
|
|
|
@@ -144,9 +148,11 @@ prbot export pb --no-commit
|
|
|
144
148
|
|
|
145
149
|
Options:
|
|
146
150
|
|
|
147
|
-
| Flag
|
|
148
|
-
|
|
|
149
|
-
| `--no-commit`
|
|
151
|
+
| Flag | Description |
|
|
152
|
+
| -------------- | ------------------------------------------------------------- |
|
|
153
|
+
| `--no-commit` | Skip the git commit step |
|
|
154
|
+
| `-q, --quiet` | Suppress informational output — errors still fail the command |
|
|
155
|
+
| `-s, --silent` | Deprecated alias of `--quiet`; no longer swallows errors |
|
|
150
156
|
|
|
151
157
|
### `prbot export imperex`
|
|
152
158
|
|
|
@@ -159,9 +165,11 @@ prbot export imperex --no-commit
|
|
|
159
165
|
|
|
160
166
|
Options:
|
|
161
167
|
|
|
162
|
-
| Flag
|
|
163
|
-
|
|
|
164
|
-
| `--no-commit`
|
|
168
|
+
| Flag | Description |
|
|
169
|
+
| -------------- | ------------------------------------------------------------- |
|
|
170
|
+
| `--no-commit` | Skip the git commit step |
|
|
171
|
+
| `-q, --quiet` | Suppress informational output — errors still fail the command |
|
|
172
|
+
| `-s, --silent` | Deprecated alias of `--quiet`; no longer swallows errors |
|
|
165
173
|
|
|
166
174
|
### `prbot export email-templates`
|
|
167
175
|
|
|
@@ -174,9 +182,11 @@ prbot export email-templates --no-commit
|
|
|
174
182
|
|
|
175
183
|
Options:
|
|
176
184
|
|
|
177
|
-
| Flag
|
|
178
|
-
|
|
|
179
|
-
| `--no-commit`
|
|
185
|
+
| Flag | Description |
|
|
186
|
+
| -------------- | ------------------------------------------------------------- |
|
|
187
|
+
| `--no-commit` | Skip the git commit step |
|
|
188
|
+
| `-q, --quiet` | Suppress informational output — errors still fail the command |
|
|
189
|
+
| `-s, --silent` | Deprecated alias of `--quiet`; no longer swallows errors |
|
|
180
190
|
|
|
181
191
|
### `prbot init`
|
|
182
192
|
|
|
@@ -184,10 +194,11 @@ Interactive setup: writes `~/.config/prbot/config`.
|
|
|
184
194
|
|
|
185
195
|
### `prbot update`
|
|
186
196
|
|
|
187
|
-
Reinstalls
|
|
197
|
+
Reinstalls prbot from npm — latest by default, or a pinned version.
|
|
188
198
|
|
|
189
199
|
```bash
|
|
190
|
-
prbot update
|
|
200
|
+
prbot update # latest
|
|
201
|
+
prbot update 3.4.0 # pin an exact version
|
|
191
202
|
```
|
|
192
203
|
|
|
193
204
|
---
|
|
@@ -225,26 +236,27 @@ agrippa clone --lrp --name B2WA_ml_IFS_passive_trigger --path my-lrp/
|
|
|
225
236
|
|
|
226
237
|
Options:
|
|
227
238
|
|
|
228
|
-
| Flag
|
|
229
|
-
|
|
|
230
|
-
| `--phase`
|
|
231
|
-
| `--mfa`
|
|
232
|
-
| `--pb`
|
|
233
|
-
| `--lrp`
|
|
234
|
-
| `--id <id>`
|
|
235
|
-
| `--name <name>`
|
|
236
|
-
| `--path <path>`
|
|
239
|
+
| Flag | Description |
|
|
240
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
241
|
+
| `--phase` | Clone a phase (select a workflow) |
|
|
242
|
+
| `--mfa` | Clone an MFA record |
|
|
243
|
+
| `--pb` | Clone a process-builder wizard |
|
|
244
|
+
| `--lrp` | Clone a long-running process |
|
|
245
|
+
| `--id <id>` | Skip selection, clone by ID (phase/mfa) |
|
|
246
|
+
| `--name <name>` | Skip selection: `document_id` (with `--pb`) or process name (with `--lrp`) |
|
|
247
|
+
| `--path <path>` | Destination path (base dir for phases/wizard/lrp, file for MFA) |
|
|
237
248
|
|
|
238
249
|
### `agrippa pull`
|
|
239
250
|
|
|
240
|
-
Fetches remote code for all tracked entries and shows what changed. Classifies each as `fast-forward` (safe overwrite) or `conflict` (local edits would be lost). Lets you select which to pull.
|
|
251
|
+
Fetches remote code for all tracked entries and shows what changed. Classifies each as `fast-forward` (safe overwrite) or `conflict` (local edits would be lost). `conflict` entries are shown but **not** preselected — you opt in explicitly. Lets you select which to pull.
|
|
241
252
|
|
|
242
253
|
After pulling, also checks tracked workflows for newly added `from_code` phases and auto-clones any not yet present locally.
|
|
243
254
|
|
|
244
|
-
Tracked **process-builder wizards** and **long-running processes** are also refreshed from upstream: the local project is re-decomposed from the latest payload (orphan script files pruned; PB wizards also prune orphan pages), with the same `fast-forward`/`conflict` classification (
|
|
255
|
+
Tracked **process-builder wizards** and **long-running processes** are also refreshed from upstream: the local project is re-decomposed from the latest payload (orphan script files pruned; PB wizards also prune orphan pages), with the same `fast-forward`/`conflict` classification — a semantic checksum of the recomposed payload (not raw `updated_date`), canonicalized so cosmetic noise never triggers a false conflict: whitespace-only BPMN text nodes, `format`-only `labelPos`, page ordering, and the `updated_date`/`modified_by` audit fields Odoo/Symple bump on any server touch are all excluded from the comparison. The current local state is backed up to `.backup/<timestamp>/<path>/local.json` first.
|
|
245
256
|
|
|
246
257
|
```bash
|
|
247
258
|
agrippa pull
|
|
259
|
+
agrippa pull --non-interactive # no prompts: auto-select fast-forward, fail if any conflict
|
|
248
260
|
```
|
|
249
261
|
|
|
250
262
|
### `agrippa push`
|
|
@@ -257,18 +269,21 @@ Pushing a wizard saves it as a **draft**; publish it so live consumers see the c
|
|
|
257
269
|
agrippa push # prompts whether to publish/deploy each pushed wizard/LRP
|
|
258
270
|
agrippa push --publish # auto-publish wizards, auto-deploy LRPs
|
|
259
271
|
agrippa push --skip-publish # never publish/deploy (no prompt)
|
|
272
|
+
agrippa push --non-interactive # no prompts: auto-select fast-forward, fail if any conflict;
|
|
273
|
+
# publish/deploy defaults to skip unless --publish is also passed
|
|
260
274
|
```
|
|
261
275
|
|
|
262
|
-
### `agrippa diff [
|
|
276
|
+
### `agrippa diff [target]`
|
|
263
277
|
|
|
264
|
-
Shows a diff between local files and remote code.
|
|
278
|
+
Shows a diff between local files and remote code. `[target]` optionally narrows it to a single file, a workflow folder, a cloned project directory, or a `document_id`/name; omit it for the whole workspace.
|
|
265
279
|
|
|
266
|
-
For process-builder wizards, diffs the whole project tree against what the workspace would look like if the
|
|
280
|
+
For process-builder wizards and long-running processes, diffs the whole project tree against what the workspace would look like if the project were decomposed fresh from upstream right now (`.backup/` and `preview.svg` are excluded as local-only artifacts).
|
|
267
281
|
|
|
268
282
|
```bash
|
|
269
283
|
agrippa diff
|
|
270
284
|
agrippa diff my-workflow/some-phase.py
|
|
271
285
|
agrippa diff ml_review_billing # a cloned wizard's directory
|
|
286
|
+
agrippa diff B2WA_M2C_passthrough # a cloned LRP, by directory or name
|
|
272
287
|
```
|
|
273
288
|
|
|
274
289
|
### `agrippa init-phase`
|
|
@@ -295,15 +310,16 @@ These commands exist mainly so an **AI agent** can add/remove/connect blocks wit
|
|
|
295
310
|
|
|
296
311
|
Newly added blocks get placeholder geometry. `pb format` re-lays-out the **whole** diagram (discarding any hand-tuned layout), so running it is a deliberate **human decision** — the alternative is positioning the new blocks by hand in the UI. Agents are expected to make structural edits but **not** to run `format` or to `pull`/`push`; a human reviews and syncs.
|
|
297
312
|
|
|
298
|
-
| Command | Purpose
|
|
299
|
-
| --------------- |
|
|
300
|
-
| `pb format` | Re-lay-out the **entire** diagram (elkjs, left→right); overwrites existing layout
|
|
301
|
-
| `pb add` | Add a node (`--type`, `--name`, `--parent`); scaffolds script/page files
|
|
302
|
-
| `pb rm` | Remove a node (`--id`), its edges, and its script/page files
|
|
303
|
-
| `pb connect` | Add a flow (`--from`, `--to`, `--condition`, `--default`); enforces the gateway rule
|
|
304
|
-
| `pb disconnect` | Remove a flow (`--id`, or `--from`/`--to`)
|
|
305
|
-
| `pb
|
|
306
|
-
| `pb
|
|
313
|
+
| Command | Purpose |
|
|
314
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
315
|
+
| `pb format` | Re-lay-out the **entire** diagram (elkjs, left→right); overwrites existing layout |
|
|
316
|
+
| `pb add` | Add a node (`--type`, `--name`, `--parent`); scaffolds script/page files |
|
|
317
|
+
| `pb rm` | Remove a node (`--id`), its edges, and its script/page files |
|
|
318
|
+
| `pb connect` | Add a flow (`--from`, `--to`, `--condition`, `--default`); enforces the gateway rule |
|
|
319
|
+
| `pb disconnect` | Remove a flow (`--id`, or `--from`/`--to`); clears the source gateway's `default` if it pointed at that flow |
|
|
320
|
+
| `pb lint` | Check the structural rules: gateway defaults/conditions, branch names, incoming-flow limits, and `default`s pointing at flows that no longer exist |
|
|
321
|
+
| `pb ls` | List nodes and edges with their ids (discover targets without reading the YAML) |
|
|
322
|
+
| `pb preview` | Render the diagram to an SVG (`--out`) for a quick visual check |
|
|
307
323
|
|
|
308
324
|
```bash
|
|
309
325
|
agrippa pb ls --pb ml_review_billing
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waron97/prbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.0.0"
|
|
8
|
+
},
|
|
9
|
+
"packageManager": "npm@10.9.8",
|
|
6
10
|
"scripts": {
|
|
7
11
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
12
|
},
|
|
@@ -3,6 +3,7 @@ import select from '@inquirer/select';
|
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { getToken } from '../../lib/auth.js';
|
|
5
5
|
import { fuzzyMatch } from '../../lib/fuzzy.js';
|
|
6
|
+
import { log, warn } from '../../lib/logger.js';
|
|
6
7
|
import { describeWorkflow, getPhasesByWorkflow, listMfas, listWorkflows } from '../lib/api.js';
|
|
7
8
|
import { computeChecksum } from '../lib/checksum.js';
|
|
8
9
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
@@ -42,7 +43,7 @@ async function clone(opts) {
|
|
|
42
43
|
if (!ripUrl)
|
|
43
44
|
throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
log('Fetching records...');
|
|
46
47
|
const token = await getToken();
|
|
47
48
|
|
|
48
49
|
let records;
|
|
@@ -53,7 +54,7 @@ async function clone(opts) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
if (!records.length) {
|
|
56
|
-
|
|
57
|
+
log(`No ${objectType} records found.`);
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -92,11 +93,11 @@ async function clone(opts) {
|
|
|
92
93
|
basePath = inputPath;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
log(`Fetching phases for "${record.name}"...`);
|
|
96
97
|
const phases = await getPhasesByWorkflow(token, ripUrl, record.id, { fromCode: true });
|
|
97
98
|
|
|
98
99
|
if (!phases.length) {
|
|
99
|
-
|
|
100
|
+
log('No phases found.');
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -112,19 +113,19 @@ async function clone(opts) {
|
|
|
112
113
|
checksum_at_pull: computeChecksum(phase.code),
|
|
113
114
|
name: `${record.name} / ${phase.name}`,
|
|
114
115
|
});
|
|
115
|
-
|
|
116
|
+
log(` wrote ${filePath}`);
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
// Drop the workflow graph alongside the phase files as read-only context.
|
|
119
120
|
try {
|
|
120
121
|
const structure = await describeWorkflow(token, ripUrl, record.id);
|
|
121
122
|
const docPath = writeWorkflowDoc(basePath, structure);
|
|
122
|
-
|
|
123
|
+
log(` wrote ${docPath}`);
|
|
123
124
|
} catch (err) {
|
|
124
|
-
|
|
125
|
+
warn(` could not fetch workflow structure: ${err.message}`);
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
log(`Cloned ${phases.length} phase(s) to ${basePath}/`);
|
|
128
129
|
} else {
|
|
129
130
|
const defaultPath = defaultMfaPath(record.model_name, record.name);
|
|
130
131
|
if (!basePath) {
|
|
@@ -147,7 +148,7 @@ async function clone(opts) {
|
|
|
147
148
|
checksum_at_pull: computeChecksum(record.code),
|
|
148
149
|
name: `${record.model_name} / ${record.name}`,
|
|
149
150
|
});
|
|
150
|
-
|
|
151
|
+
log(`Cloned MFA to ${basePath}`);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
writeConfig(config);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import search from '@inquirer/search';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { getToken } from '../../lib/auth.js';
|
|
4
|
-
import {
|
|
4
|
+
import { log, warn } from '../../lib/logger.js';
|
|
5
5
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
6
6
|
import { getLrpXml, listLrps, resolveLrpByName } from '../lib/lrpApi.js';
|
|
7
|
-
import { comparePayload, decompose, recompose
|
|
7
|
+
import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
|
|
8
8
|
import { projectReader, writeProject } from '../lib/pbWorkspace.js';
|
|
9
9
|
|
|
10
10
|
// Clone a long-running process (LRP). Structurally identical to a PB clone
|
|
@@ -32,7 +32,7 @@ async function cloneLrp(opts) {
|
|
|
32
32
|
if (opts.name) {
|
|
33
33
|
chosen = await resolveLrpByName(token, opts.name);
|
|
34
34
|
} else {
|
|
35
|
-
|
|
35
|
+
log('Fetching long-running process list...');
|
|
36
36
|
const initial = await listLrps(token, null);
|
|
37
37
|
let controller = null;
|
|
38
38
|
chosen = await search({
|
|
@@ -65,7 +65,7 @@ async function cloneLrp(opts) {
|
|
|
65
65
|
dest = inputPath;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
log(`Fetching "${chosen.name}"...`);
|
|
69
69
|
const { xml, description } = await getLrpXml(token, chosen.id);
|
|
70
70
|
const payload = { built_page: xml };
|
|
71
71
|
|
|
@@ -73,7 +73,7 @@ async function cloneLrp(opts) {
|
|
|
73
73
|
writeProject(dest, files);
|
|
74
74
|
|
|
75
75
|
const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
|
|
76
|
-
|
|
76
|
+
log(`Cloned to ${dest}/ (${scriptCount} script(s)).`);
|
|
77
77
|
|
|
78
78
|
// Prove the clone reconstructs the original XML (0-loss bar A). LRPs have
|
|
79
79
|
// no `pages` at all — recompose always synthesizes `pages: []`, which
|
|
@@ -82,10 +82,10 @@ async function cloneLrp(opts) {
|
|
|
82
82
|
const rebuilt = recompose(projectReader(dest));
|
|
83
83
|
const diffs = comparePayload(payload, rebuilt).filter((d) => !d.startsWith('pages:'));
|
|
84
84
|
if (diffs.length) {
|
|
85
|
-
|
|
86
|
-
diffs.forEach((d) =>
|
|
85
|
+
warn('WARNING: round-trip verification found differences:');
|
|
86
|
+
diffs.forEach((d) => warn(' - ' + d));
|
|
87
87
|
} else {
|
|
88
|
-
|
|
88
|
+
log('Round-trip verified: recomposed payload is identical (0 information loss).');
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Register in the workspace for later pull/push — keyed by NAME, not id.
|
|
@@ -102,7 +102,7 @@ async function cloneLrp(opts) {
|
|
|
102
102
|
description,
|
|
103
103
|
// Baseline for push classification (see pull.js/push.js): checksum of
|
|
104
104
|
// the *recomposed* payload, changes only when local files change.
|
|
105
|
-
checksum_at_pull:
|
|
105
|
+
checksum_at_pull: checksumOfPayload(rebuilt),
|
|
106
106
|
version: chosen.version,
|
|
107
107
|
status: chosen.status,
|
|
108
108
|
};
|
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
19
|
+
log('Fetching process list...');
|
|
20
20
|
const token = await getToken();
|
|
21
21
|
const processes = await listProcesses(token);
|
|
22
22
|
if (!processes.length) {
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
diffs.forEach((d) =>
|
|
79
|
+
warn('WARNING: round-trip verification found differences:');
|
|
80
|
+
diffs.forEach((d) => warn(' - ' + d));
|
|
81
81
|
} else {
|
|
82
|
-
|
|
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:
|
|
98
|
+
checksum_at_pull: checksumOfPayload(rebuilt),
|
|
99
99
|
updated_date: payload.updated_date,
|
|
100
100
|
status: payload.status,
|
|
101
101
|
};
|
|
@@ -12,8 +12,10 @@ 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';
|
|
18
|
+
import { fetchUpstream } from '../lib/lrpApi.js';
|
|
17
19
|
import { getProcess } from '../lib/pbApi.js';
|
|
18
20
|
import { decompose, localChecksum } from '../lib/pbProject.js';
|
|
19
21
|
import { projectReader, writeProject } from '../lib/pbWorkspace.js';
|
|
@@ -25,25 +27,36 @@ async function diff(targetArg) {
|
|
|
25
27
|
loadEffectiveEnv(config);
|
|
26
28
|
|
|
27
29
|
if (!config.workspace.length) {
|
|
28
|
-
|
|
30
|
+
log('No tracked resources. Run `agrippa clone` first.');
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const entries = filterEntries(config.workspace, targetArg);
|
|
33
35
|
if (!entries.length) {
|
|
34
|
-
|
|
36
|
+
log('No tracked files match the given path.');
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// Decomposed projects (pb + lrp) diff as whole directory trees; phases and
|
|
41
|
+
// MFAs are single code files. An LRP left in the code bucket used to reach
|
|
42
|
+
// readCodeFile() with a directory path and blow up with EISDIR, so the
|
|
43
|
+
// split is by "is this a project" rather than "is this a wizard".
|
|
44
|
+
const projectEntries = entries.filter((e) => PROJECT_TYPES.has(e.object_type));
|
|
45
|
+
const codeEntries = entries.filter((e) => !PROJECT_TYPES.has(e.object_type));
|
|
40
46
|
|
|
41
47
|
if (codeEntries.length && !process.env.RIP_URL)
|
|
42
48
|
throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
43
|
-
if (
|
|
49
|
+
if (projectEntries.some((e) => e.object_type === 'process_builder') && !process.env.PB_URL)
|
|
44
50
|
throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
51
|
+
if (
|
|
52
|
+
projectEntries.some((e) => e.object_type === 'long_running_process') &&
|
|
53
|
+
!process.env.IMPORTEXPORT_URL
|
|
54
|
+
)
|
|
55
|
+
throw new Error(
|
|
56
|
+
'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
|
|
57
|
+
);
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
log('Fetching remote code...');
|
|
47
60
|
const token = await getToken();
|
|
48
61
|
|
|
49
62
|
let diffCount = 0;
|
|
@@ -56,8 +69,8 @@ async function diff(targetArg) {
|
|
|
56
69
|
chunks.push(...cs);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
for (const entry of
|
|
60
|
-
const { hasDiff, chunk } = await
|
|
72
|
+
for (const entry of projectEntries) {
|
|
73
|
+
const { hasDiff, chunk } = await diffProjectEntry(token, entry);
|
|
61
74
|
if (hasDiff) {
|
|
62
75
|
diffCount++;
|
|
63
76
|
chunks.push(chunk);
|
|
@@ -65,13 +78,13 @@ async function diff(targetArg) {
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
if (diffCount === 0) {
|
|
68
|
-
|
|
81
|
+
log('No differences found — all tracked files match the remote.');
|
|
69
82
|
} else {
|
|
70
83
|
const combined = Buffer.concat(chunks);
|
|
71
84
|
const pager = process.env.PAGER || 'less';
|
|
72
85
|
const pagerArgs = pager === 'less' ? ['-R', '-F'] : [];
|
|
73
86
|
spawnSync(pager, pagerArgs, { input: combined, stdio: ['pipe', 'inherit', 'inherit'] });
|
|
74
|
-
|
|
87
|
+
log(`\n${diffCount} file(s) differ from remote.`);
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
|
@@ -90,7 +103,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
|
|
|
90
103
|
if (computeChecksum(localCode) === computeChecksum(remoteCode)) continue;
|
|
91
104
|
|
|
92
105
|
if (!fileExists(entry.path)) {
|
|
93
|
-
|
|
106
|
+
log(`\n--- ${entry.path} (local file missing)`);
|
|
94
107
|
continue;
|
|
95
108
|
}
|
|
96
109
|
|
|
@@ -105,7 +118,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
|
|
|
105
118
|
);
|
|
106
119
|
// exit code 1 means differences found (normal), 0 means identical, >1 means error
|
|
107
120
|
if (result.status !== null && result.status > 1) {
|
|
108
|
-
|
|
121
|
+
error(`git diff failed for ${entry.path}`);
|
|
109
122
|
}
|
|
110
123
|
const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] ===\n`);
|
|
111
124
|
chunks.push(Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]));
|
|
@@ -123,14 +136,33 @@ function diffCodeEntries(entries, remoteCodeMap) {
|
|
|
123
136
|
return { diffCount, chunks };
|
|
124
137
|
}
|
|
125
138
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
139
|
+
// Decomposed project (process-builder wizard or long-running process): fetch
|
|
140
|
+
// the upstream payload, decompose it into a throwaway project tree (exactly
|
|
141
|
+
// what re-cloning now would produce) and diff it against a copy of the local
|
|
142
|
+
// project, recursively. `.backup/` and `preview.svg` are local-only artifacts
|
|
143
|
+
// (push backups, dev preview render) with no upstream counterpart, so they're
|
|
144
|
+
// stripped from the local copy before diffing.
|
|
145
|
+
//
|
|
146
|
+
// The two types differ only in how upstream is reached: a wizard by guid, an
|
|
147
|
+
// LRP by name (its tabulator id changes on every save — never a stable key,
|
|
148
|
+
// see pullLrpEntry). Everything downstream is the shared decompose pipeline.
|
|
149
|
+
const PROJECT_TYPES = new Set(['process_builder', 'long_running_process']);
|
|
150
|
+
|
|
151
|
+
async function fetchProjectUpstream(token, entry) {
|
|
152
|
+
if (entry.object_type === 'long_running_process') {
|
|
153
|
+
const res = await fetchUpstream(token, entry.name);
|
|
154
|
+
if (!res?.payload) throw new Error(`could not fetch upstream LRP "${entry.name}"`);
|
|
155
|
+
return res.payload;
|
|
156
|
+
}
|
|
132
157
|
const upstream = await getProcess(token, entry.guid);
|
|
133
158
|
if (!upstream) throw new Error(`could not fetch upstream wizard ${entry.guid}`);
|
|
159
|
+
return upstream;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function diffProjectEntry(token, entry) {
|
|
163
|
+
const upstream = await fetchProjectUpstream(token, entry);
|
|
164
|
+
const label =
|
|
165
|
+
entry.object_type === 'long_running_process' ? 'long-running process' : 'process-builder';
|
|
134
166
|
|
|
135
167
|
const tmpRoot = mkdtempSync(join(tmpdir(), 'agrippa-pb-diff-'));
|
|
136
168
|
try {
|
|
@@ -156,9 +188,9 @@ async function diffPbEntry(token, entry) {
|
|
|
156
188
|
{ cwd: tmpRoot, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
157
189
|
);
|
|
158
190
|
if (result.status !== null && result.status > 1) {
|
|
159
|
-
|
|
191
|
+
error(`git diff failed for ${entry.path}`);
|
|
160
192
|
}
|
|
161
|
-
const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] (
|
|
193
|
+
const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] (${label}) ===\n`);
|
|
162
194
|
const chunk = Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]);
|
|
163
195
|
return { hasDiff: true, chunk };
|
|
164
196
|
} finally {
|
|
@@ -172,14 +204,21 @@ function filterEntries(workspace, targetArg) {
|
|
|
172
204
|
// Normalise: strip trailing slash
|
|
173
205
|
const target = targetArg.replace(/\/$/, '');
|
|
174
206
|
|
|
175
|
-
// If it exists on disk and is a directory,
|
|
207
|
+
// If it exists on disk and is a directory, take both the entries *under* it
|
|
208
|
+
// (a workflow folder holding one file per phase) and any entry that *is* it
|
|
209
|
+
// — a pb/lrp project is tracked as the directory itself, so prefix matching
|
|
210
|
+
// alone silently returned nothing for the most common target.
|
|
176
211
|
if (existsSync(target) && statSync(target).isDirectory()) {
|
|
177
|
-
const prefix = target
|
|
178
|
-
|
|
212
|
+
const prefix = target + '/';
|
|
213
|
+
const matches = workspace.filter((e) => e.path === target || e.path.startsWith(prefix));
|
|
214
|
+
if (matches.length) return matches;
|
|
179
215
|
}
|
|
180
216
|
|
|
181
|
-
//
|
|
182
|
-
|
|
217
|
+
// Exact file path (whether it exists yet or not), else fall back to the
|
|
218
|
+
// identifiers the other commands accept — document_id (`--pb`) and name.
|
|
219
|
+
const byPath = workspace.filter((e) => e.path === target);
|
|
220
|
+
if (byPath.length) return byPath;
|
|
221
|
+
return workspace.filter((e) => e.document_id === target || e.name === target);
|
|
183
222
|
}
|
|
184
223
|
|
|
185
224
|
export { diff };
|
|
@@ -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
|
-
|
|
97
|
+
log('Skipped workspace file.');
|
|
97
98
|
} else {
|
|
98
99
|
writeFileSync(WORKSPACE_FILE, TEMPLATE, 'utf-8');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
log('Refreshed agrippa-pb guidance in CLAUDE.md');
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
}
|