@waron97/prbot 3.2.1 → 3.3.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.
@@ -100,27 +100,26 @@ async function initPhase() {
100
100
  return;
101
101
  }
102
102
 
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 }));
103
+ const { selectedPhases } = await inquirer.prompt([
104
+ {
105
+ type: 'checkbox',
106
+ name: 'selectedPhases',
107
+ message: 'Select phases to initialize:',
108
+ choices: phases.map((p) => ({ name: p.name, value: p })),
109
+ loop: false,
108
110
  },
109
- });
111
+ ]);
110
112
 
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);
113
+ if (!selectedPhases.length) {
114
+ console.log('No phases selected. Aborted.');
115
+ return;
115
116
  }
116
117
 
117
- const code = generateCode(results);
118
-
119
118
  const { confirm } = await inquirer.prompt([
120
119
  {
121
120
  type: 'confirm',
122
121
  name: 'confirm',
123
- message: `Initialize phase "${phase.name}"? This will overwrite existing code.`,
122
+ message: `Initialize ${selectedPhases.length} phase(s)? This will overwrite existing code.`,
124
123
  default: true,
125
124
  },
126
125
  ]);
@@ -130,29 +129,40 @@ async function initPhase() {
130
129
  return;
131
130
  }
132
131
 
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);
132
+ for (const phase of selectedPhases) {
133
+ // Resolve result objects API may return IDs or full objects
134
+ let results = phase.allowed_phase_result_ids || [];
135
+ if (results.length > 0 && typeof results[0] === 'number') {
136
+ results = await getPhaseResults(token, ripUrl, results);
137
+ }
138
+
139
+ const code = generateCode(results);
140
+
141
+ // Delete existing configurator records for this phase
142
+ const existing = await getPhaseConfigurators(token, ripUrl, phase.id);
143
+ for (const cfg of existing) {
144
+ await deleteConfigurator(token, ripUrl, cfg.id);
145
+ }
146
+
147
+ // Update phase code + set_result_automatically
148
+ await initPhaseRemote(token, ripUrl, phase.id, code);
149
+
150
+ // Create new configurator records for each result
151
+ const vars = results.map((_, i) => `RES${i + 1}`);
152
+ for (let i = 0; i < results.length; i++) {
153
+ await createConfigurator(token, ripUrl, {
154
+ result_value: vars[i],
155
+ code_phase_id: phase.id,
156
+ triplet_phase_result_id: results[i].id,
157
+ });
158
+ }
159
+
160
+ console.log(`Initialized phase "${phase.name}".`);
161
+ if (results.length > 0) {
162
+ console.log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
163
+ }
137
164
  }
138
165
 
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
166
  console.log(
157
167
  `Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`
158
168
  );
@@ -37,32 +37,49 @@ import { projectReader } from '../lib/pbWorkspace.js';
37
37
 
38
38
  // ---------- project resolution ----------
39
39
 
40
- async function resolveProjectPath(opts) {
40
+ // Resolves against both process-builder wizards and long-running processes —
41
+ // `structure.yaml`-level editing is identical for both (see pbEdit.js). PBs
42
+ // select by --pb/--name matched against document_id; LRPs have no document_id
43
+ // so the same flag matches against name instead.
44
+ async function resolveProjectEntry(opts) {
41
45
  const config = readConfig();
42
- const entries = (config.workspace || []).filter((e) => e.object_type === 'process_builder');
46
+ const entries = (config.workspace || []).filter(
47
+ (e) => e.object_type === 'process_builder' || e.object_type === 'long_running_process'
48
+ );
43
49
  if (!entries.length) {
44
50
  throw new Error(
45
- 'No process-builder wizards in this workspace. Clone one with `agrippa clone --pb`.'
51
+ 'No process-builder wizards or long-running processes in this workspace. ' +
52
+ 'Clone one with `agrippa clone --pb` or `agrippa clone --lrp`.'
46
53
  );
47
54
  }
48
55
  const sel = opts.pb || opts.name;
49
56
  if (sel) {
50
- const entry = entries.find((e) => e.document_id === sel);
51
- if (!entry) throw new Error(`No cloned wizard with document_id "${sel}"`);
52
- return entry.path;
57
+ const entry = entries.find((e) => e.document_id === sel || e.name === sel);
58
+ if (!entry) throw new Error(`No cloned project with document_id/name "${sel}"`);
59
+ return entry;
53
60
  }
54
- if (entries.length === 1) return entries[0].path;
61
+ if (entries.length === 1) return entries[0];
55
62
  const entry = await search({
56
- message: 'Select a cloned wizard:',
63
+ message: 'Select a cloned project:',
57
64
  source: (input) => {
58
65
  const list = input
59
66
  ? entries.filter(
60
- (e) => fuzzyMatch(e.name, input) || fuzzyMatch(e.document_id, input)
67
+ (e) =>
68
+ fuzzyMatch(e.name, input) ||
69
+ (e.document_id && fuzzyMatch(e.document_id, input))
61
70
  )
62
71
  : entries;
63
- return list.map((e) => ({ name: `${e.name} (${e.document_id})`, value: e }));
72
+ return list.map((e) => ({
73
+ name: e.document_id ? `${e.name} (${e.document_id})` : e.name,
74
+ value: e,
75
+ }));
64
76
  },
65
77
  });
78
+ return entry;
79
+ }
80
+
81
+ async function resolveProjectPath(opts) {
82
+ const entry = await resolveProjectEntry(opts);
66
83
  return entry.path;
67
84
  }
68
85
 
@@ -135,14 +152,20 @@ async function pbFormat(opts) {
135
152
  async function pbAdd(opts) {
136
153
  if (!opts.type)
137
154
  throw new Error(
138
- '--type is required (e.g. scriptTask, serviceTask, userTask, exclusiveGateway, subProcess, endEvent...)'
155
+ '--type is required (e.g. scriptTask, serviceTask, userTask, exclusiveGateway, subProcess, endEvent, callActivity...)'
139
156
  );
140
157
  if ((opts.from || opts.to) && !(opts.from && opts.to))
141
158
  throw new Error('--from and --to must be used together');
142
159
  if (opts.from && opts.parent)
143
160
  throw new Error('--parent is implied by --from/--to; pass only one');
144
161
 
145
- const dir = await resolveProjectPath(opts);
162
+ const projectEntry = await resolveProjectEntry(opts);
163
+ if (opts.type === 'userTask' && projectEntry.object_type === 'long_running_process') {
164
+ throw new Error(
165
+ 'userTask is not valid on a long-running process — LRPs have no pages (user tasks are a process-builder-only concept).'
166
+ );
167
+ }
168
+ const dir = projectEntry.path;
146
169
  const { structure, manifest } = loadProject(dir);
147
170
  const ctx = { existingScripts: listScriptFiles(dir), documentId: manifest.document_id };
148
171
 
@@ -288,4 +311,14 @@ async function pbLint(opts) {
288
311
  }
289
312
  }
290
313
 
291
- export { pbFormat, pbAdd, pbRemove, pbConnect, pbDisconnect, pbSetDefault, pbList, pbPreview, pbLint };
314
+ export {
315
+ pbFormat,
316
+ pbAdd,
317
+ pbRemove,
318
+ pbConnect,
319
+ pbDisconnect,
320
+ pbSetDefault,
321
+ pbList,
322
+ pbPreview,
323
+ pbLint,
324
+ };
@@ -4,6 +4,7 @@ import { getToken } from '../../lib/auth.js';
4
4
  import { describeWorkflow, getPhasesByIds, getPhasesByWorkflow, listMfas } from '../lib/api.js';
5
5
  import { computeChecksum } from '../lib/checksum.js';
6
6
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
7
+ import { fetchUpstream } from '../lib/lrpApi.js';
7
8
  import { getProcess } from '../lib/pbApi.js';
8
9
  import { localChecksum, remoteChecksumPb } from '../lib/pbProject.js';
9
10
  import { projectReader } from '../lib/pbWorkspace.js';
@@ -14,6 +15,7 @@ import {
14
15
  writeCodeFile,
15
16
  writeWorkflowDoc,
16
17
  } from '../lib/workspace.js';
18
+ import { pullLrpEntry } from './pullLrp.js';
17
19
  import { pullPbEntry } from './pullPb.js';
18
20
 
19
21
  async function pull() {
@@ -50,9 +52,11 @@ async function pull() {
50
52
  const changedWorkflowIds = new Set();
51
53
 
52
54
  // ── pull existing tracked entries ─────────────────────────────────────────
53
- // process_builder wizards are refreshed separately (different checksum model);
54
- // see pullPbEntries below.
55
- const pullable = config.workspace.filter((e) => e.object_type !== 'process_builder');
55
+ // process_builder wizards and LRPs are refreshed separately (different
56
+ // checksum/identity model); see pullPbEntries/pullLrpEntries below.
57
+ const pullable = config.workspace.filter(
58
+ (e) => e.object_type !== 'process_builder' && e.object_type !== 'long_running_process'
59
+ );
56
60
  if (pullable.length) {
57
61
  console.log('Fetching remote code...');
58
62
  const remoteCodeMap = await fetchRemoteCode(token, ripUrl, pullable);
@@ -111,6 +115,9 @@ async function pull() {
111
115
  // ── refresh tracked process-builder wizards ───────────────────────────────
112
116
  await pullPbEntries(token, config);
113
117
 
118
+ // ── refresh tracked long-running processes ────────────────────────────────
119
+ await pullLrpEntries(token, config);
120
+
114
121
  // ── discover new phases on tracked workflows ──────────────────────────────
115
122
  await discoverNewPhases(token, ripUrl, config, changedWorkflowIds);
116
123
  }
@@ -179,6 +186,75 @@ async function pullPbEntries(token, config) {
179
186
  console.log(`\nPulled ${selected.length} wizard(s). Local backups in .backup/${backupTs}/`);
180
187
  }
181
188
 
189
+ // Refresh tracked long-running processes from upstream. Same classification
190
+ // concern as pullPbEntries, but entries are located by NAME (the tabulator id
191
+ // changes on every save/version — never a stable key).
192
+ async function pullLrpEntries(token, config) {
193
+ const entries = config.workspace.filter((e) => e.object_type === 'long_running_process');
194
+ if (!entries.length) return;
195
+ if (!process.env.IMPORTEXPORT_URL)
196
+ throw new Error(
197
+ 'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
198
+ );
199
+
200
+ console.log('Checking long-running processes...');
201
+ const classified = [];
202
+ for (const entry of entries) {
203
+ let upstream = null;
204
+ try {
205
+ upstream = await fetchUpstream(token, entry.name);
206
+ } catch {
207
+ upstream = null;
208
+ }
209
+ if (!upstream) {
210
+ console.warn(` ${entry.name}: could not fetch upstream, skipping`);
211
+ continue;
212
+ }
213
+ const localSemantic = localChecksum(projectReader(entry.path));
214
+ const remoteSemantic = remoteChecksumPb(upstream.payload);
215
+ const pullChecksum = entry.checksum_at_pull;
216
+ let status;
217
+ if (localSemantic === remoteSemantic) status = 'unchanged';
218
+ else if (pullChecksum === localSemantic) status = 'fast-forward';
219
+ else status = 'conflict';
220
+ classified.push({ ...entry, upstream, status });
221
+ }
222
+
223
+ const changed = classified.filter((e) => e.status !== 'unchanged');
224
+ if (!changed.length) {
225
+ console.log('Long-running processes are up to date.');
226
+ return;
227
+ }
228
+
229
+ const selected = await selectEntries(changed, 'pull (overwrites local LRP files)');
230
+ if (!selected.length) {
231
+ console.log('No long-running processes selected.');
232
+ return;
233
+ }
234
+
235
+ const backupTs = new Date().toISOString().replace(/:/g, '-').replace('T', '_').slice(0, 19);
236
+ for (const entry of selected) {
237
+ const res = await pullLrpEntry(token, entry, '.backup', backupTs);
238
+ const idx = config.workspace.findIndex(
239
+ (e) => e.object_type === 'long_running_process' && e.name === entry.name
240
+ );
241
+ if (idx !== -1) {
242
+ config.workspace[idx].checksum_at_pull = res.newChecksum;
243
+ config.workspace[idx].tenant_id = res.newRow.tenantId;
244
+ config.workspace[idx].svg = res.newRow.bpmnFileSvg;
245
+ config.workspace[idx].description = res.newRow.description;
246
+ config.workspace[idx].version = res.newRow.version;
247
+ config.workspace[idx].status = res.newRow.status;
248
+ }
249
+ const note = res.diffs.length ? ` (WARNING: ${res.diffs.length} round-trip diff(s))` : '';
250
+ console.log(` ${entry.name} → refreshed${note}`);
251
+ }
252
+ writeConfig(config);
253
+ console.log(
254
+ `\nPulled ${selected.length} long-running process(es). Local backups in .backup/${backupTs}/`
255
+ );
256
+ }
257
+
182
258
  async function discoverNewPhases(token, ripUrl, config, changedWorkflowIds = new Set()) {
183
259
  // Build map of workflow_id → { workflow_name, basePath } from existing phase entries
184
260
  const workflows = new Map();
@@ -0,0 +1,55 @@
1
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { computeChecksum } from '../lib/checksum.js';
4
+ import { fetchUpstream } from '../lib/lrpApi.js';
5
+ import { comparePayload, decompose, recompose, stableStringify } from '../lib/pbProject.js';
6
+ import { projectReader, writeProject } from '../lib/pbWorkspace.js';
7
+
8
+ // Delete files under <baseDir>/scripts that are not in the fresh decompose
9
+ // map, so a refresh that renames/removes scripts doesn't leave orphans behind
10
+ // (LRPs have no pages/ dir — no user tasks).
11
+ function pruneOrphans(baseDir, files) {
12
+ const dir = join(baseDir, 'scripts');
13
+ if (!existsSync(dir)) return;
14
+ for (const f of readdirSync(dir)) {
15
+ if (!(`scripts/${f}` in files)) rmSync(join(dir, f));
16
+ }
17
+ }
18
+
19
+ // Refresh one cloned LRP from upstream (the pull counterpart of
20
+ // pushLrpEntry): back up the current local state, re-decompose the upstream
21
+ // XML, overwrite local files (pruning orphans), and verify the round-trip.
22
+ // `entry.upstream` is the {row, payload} fetched during classification;
23
+ // re-resolved by name if absent (id may have changed since the last pull).
24
+ async function pullLrpEntry(token, entry, backupDir, backupTs) {
25
+ const upstream = entry.upstream ?? (await fetchUpstream(token, entry.name));
26
+ if (!upstream) throw new Error(`could not fetch upstream LRP "${entry.name}"`);
27
+
28
+ // 1. Backup the current local state (recomposed payload) before overwriting.
29
+ try {
30
+ const localPayload = recompose(projectReader(entry.path));
31
+ const backupPath = join(backupDir, backupTs, entry.path, 'local.json');
32
+ mkdirSync(dirname(backupPath), { recursive: true });
33
+ writeFileSync(backupPath, JSON.stringify(localPayload, null, 2), 'utf-8');
34
+ } catch {
35
+ // local files unreadable (e.g. mid-edit) — nothing safe to back up, proceed.
36
+ }
37
+
38
+ // 2. Decompose upstream → fresh file map; prune orphans; write.
39
+ const { files } = decompose(upstream.payload);
40
+ pruneOrphans(entry.path, files);
41
+ writeProject(entry.path, files);
42
+
43
+ // 3. Verify the freshly-written project reconstructs the upstream XML.
44
+ // (pages diff is expected noise for LRPs — see cloneLrp.js.)
45
+ const rebuilt = recompose(projectReader(entry.path));
46
+ const diffs = comparePayload(upstream.payload, rebuilt).filter((d) => !d.startsWith('pages:'));
47
+
48
+ return {
49
+ newChecksum: computeChecksum(stableStringify(rebuilt)),
50
+ newRow: upstream.row,
51
+ diffs,
52
+ };
53
+ }
54
+
55
+ export { pullLrpEntry };
@@ -5,11 +5,13 @@ import { getToken } from '../../lib/auth.js';
5
5
  import { updateMfa, updatePhase } from '../lib/api.js';
6
6
  import { computeChecksum } from '../lib/checksum.js';
7
7
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
8
+ import { fetchUpstream } from '../lib/lrpApi.js';
8
9
  import { getProcess } from '../lib/pbApi.js';
9
10
  import { localChecksum, remoteChecksumPb } from '../lib/pbProject.js';
10
11
  import { projectReader } from '../lib/pbWorkspace.js';
11
12
  import { fileExists, readCodeFile } from '../lib/workspace.js';
12
13
  import { fetchRemoteCode, selectEntries } from './pull.js';
14
+ import { deploy, pushLrpEntry } from './pushLrp.js';
13
15
  import { publish, pushPbEntry } from './pushPb.js';
14
16
 
15
17
  const BACKUP_DIR = '.backup';
@@ -46,11 +48,16 @@ async function push(opts = {}) {
46
48
  (e) => e.object_type === 'phase' || e.object_type === 'mfa'
47
49
  );
48
50
  const hasPb = config.workspace.some((e) => e.object_type === 'process_builder');
51
+ const hasLrp = config.workspace.some((e) => e.object_type === 'long_running_process');
49
52
  const ripUrl = process.env.RIP_URL;
50
53
  if (hasCode && !ripUrl)
51
54
  throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
52
55
  if (hasPb && !process.env.PB_URL)
53
56
  throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
57
+ if (hasLrp && !process.env.IMPORTEXPORT_URL)
58
+ throw new Error(
59
+ 'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
60
+ );
54
61
 
55
62
  console.log('Fetching remote state...');
56
63
  const token = await getToken();
@@ -66,6 +73,14 @@ async function push(opts = {}) {
66
73
  upstreamMap.set(e.guid, null);
67
74
  }
68
75
  }
76
+ const lrpUpstreamMap = new Map();
77
+ for (const e of config.workspace.filter((x) => x.object_type === 'long_running_process')) {
78
+ try {
79
+ lrpUpstreamMap.set(e.name, await fetchUpstream(token, e.name));
80
+ } catch {
81
+ lrpUpstreamMap.set(e.name, null);
82
+ }
83
+ }
69
84
 
70
85
  // Classify every entry to a status badge — concern: overwriting remote work.
71
86
  const classified = config.workspace.map((entry) => {
@@ -81,6 +96,18 @@ async function push(opts = {}) {
81
96
  return { ...entry, upstream, status };
82
97
  }
83
98
 
99
+ if (entry.object_type === 'long_running_process') {
100
+ const upstream = lrpUpstreamMap.get(entry.name) ?? null;
101
+ const localSemantic = localChecksum(projectReader(entry.path));
102
+ const remoteSemantic = upstream ? remoteChecksumPb(upstream.payload) : null;
103
+ const pullChecksum = entry.checksum_at_pull;
104
+ let status;
105
+ if (localSemantic === remoteSemantic) status = 'unchanged';
106
+ else if (pullChecksum === remoteSemantic) status = 'fast-forward';
107
+ else status = 'conflict';
108
+ return { ...entry, upstream, status };
109
+ }
110
+
84
111
  const key = `${entry.object_type}:${entry.id}`;
85
112
  const remoteCode = remoteCodeMap.get(key) ?? null;
86
113
  const localCode = readCodeFile(entry.path);
@@ -108,10 +135,14 @@ async function push(opts = {}) {
108
135
 
109
136
  const backupTs = new Date().toISOString().replace(/:/g, '-').replace('T', '_').slice(0, 19);
110
137
 
111
- // Back up remote code for phase/mfa before overwriting (pb backs up its own
112
- // full upstream payload inside pushPbEntry).
138
+ // Back up remote code for phase/mfa before overwriting (pb/lrp back up
139
+ // their own full upstream payload inside pushPbEntry/pushLrpEntry).
113
140
  for (const entry of selected) {
114
- if (entry.object_type !== 'process_builder' && entry.remoteCode != null) {
141
+ if (
142
+ entry.object_type !== 'process_builder' &&
143
+ entry.object_type !== 'long_running_process' &&
144
+ entry.remoteCode != null
145
+ ) {
115
146
  const backupPath = join(BACKUP_DIR, backupTs, entry.path);
116
147
  mkdirSync(dirname(backupPath), { recursive: true });
117
148
  writeFileSync(backupPath, (entry.remoteCode ?? '').trim() + '\n', 'utf-8');
@@ -119,15 +150,15 @@ async function push(opts = {}) {
119
150
  }
120
151
 
121
152
  const pushedPb = [];
153
+ const pushedLrp = [];
122
154
  let pushed = 0;
123
155
  for (const entry of selected) {
124
- const idx = config.workspace.findIndex(
125
- (e) =>
126
- e.object_type === entry.object_type &&
127
- (entry.object_type === 'process_builder'
128
- ? e.guid === entry.guid
129
- : e.id === entry.id)
130
- );
156
+ const idx = config.workspace.findIndex((e) => {
157
+ if (e.object_type !== entry.object_type) return false;
158
+ if (entry.object_type === 'process_builder') return e.guid === entry.guid;
159
+ if (entry.object_type === 'long_running_process') return e.name === entry.name;
160
+ return e.id === entry.id;
161
+ });
131
162
 
132
163
  if (entry.object_type === 'process_builder') {
133
164
  const res = await pushPbEntry(token, entry, BACKUP_DIR, backupTs);
@@ -144,6 +175,19 @@ async function push(opts = {}) {
144
175
  config.workspace[idx].status = res.newStatus || 'draft';
145
176
  }
146
177
  pushedPb.push({ entry, idx });
178
+ } else if (entry.object_type === 'long_running_process') {
179
+ const res = await pushLrpEntry(token, entry, BACKUP_DIR, backupTs);
180
+ console.log(` ${entry.name} → saved`);
181
+ if (idx !== -1) {
182
+ config.workspace[idx].checksum_at_pull = res.newChecksum;
183
+ config.workspace[idx].tenant_id = res.newRow.tenantId;
184
+ config.workspace[idx].description = res.newRow.description;
185
+ config.workspace[idx].version = res.newRow.version;
186
+ config.workspace[idx].status = res.newRow.status;
187
+ }
188
+ // deployId is the fresh post-save id (re-resolved by name in
189
+ // pushLrpEntry); the pre-save id is dead once the save returns.
190
+ pushedLrp.push({ entry, idx, deployId: res.deployId });
147
191
  } else {
148
192
  const code = entry.localCode ?? '';
149
193
  if (entry.object_type === 'phase') await updatePhase(token, ripUrl, entry.id, code);
@@ -155,10 +199,13 @@ async function push(opts = {}) {
155
199
 
156
200
  console.log(`\nRemote backups written to ${BACKUP_DIR}/${backupTs}/`);
157
201
 
158
- // Publish step for pushed wizards (now in "draft").
202
+ // Publish/deploy step for pushed wizards and LRPs.
159
203
  if (pushedPb.length) {
160
204
  await handlePublish(token, pushedPb, config, opts);
161
205
  }
206
+ if (pushedLrp.length) {
207
+ await handleDeploy(token, pushedLrp, config, opts);
208
+ }
162
209
 
163
210
  writeConfig(config);
164
211
  console.log(`\nPushed ${pushed} record(s).`);
@@ -187,4 +234,27 @@ async function handlePublish(token, pushedPb, config, opts) {
187
234
  }
188
235
  }
189
236
 
237
+ async function handleDeploy(token, pushedLrp, config, opts) {
238
+ for (const { entry, idx, deployId } of pushedLrp) {
239
+ let doDeploy;
240
+ if (opts.skipPublish) doDeploy = false;
241
+ else if (opts.publish) doDeploy = true;
242
+ else {
243
+ ({ doDeploy } = await inquirer.prompt([
244
+ {
245
+ type: 'confirm',
246
+ name: 'doDeploy',
247
+ message: `Deploy "${entry.name}" now?`,
248
+ default: false,
249
+ },
250
+ ]));
251
+ }
252
+ if (doDeploy) {
253
+ await deploy(token, deployId);
254
+ if (idx !== -1) config.workspace[idx].status = 'deployed';
255
+ console.log(` deployed ${entry.name}`);
256
+ }
257
+ }
258
+ }
259
+
190
260
  export { push };
@@ -0,0 +1,56 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { deployLrp, resolveLrpByName, saveLrp } from '../lib/lrpApi.js';
4
+ import { localChecksum, recompose } from '../lib/pbProject.js';
5
+ import { projectReader } from '../lib/pbWorkspace.js';
6
+
7
+ // Push one LRP entry: back up upstream, re-resolve the CURRENT id/tenantId by
8
+ // name (never trust a stored/stale id — it changes on every save/version),
9
+ // and PATCH the recomposed XML back. Returns a summary; deploy is a separate
10
+ // step (see deploy()), mirroring PB's push-then-publish split.
11
+ //
12
+ // The save bumps the version and mints a NEW id, so the pre-save id is stale
13
+ // the moment saveLrp returns. Both the deploy target and the post-save
14
+ // version/status must come from a fresh resolve-by-name AFTER the save — never
15
+ // from the pre-save row or the PATCH response (which does not echo the new id).
16
+ async function pushLrpEntry(token, entry, backupDir, backupTs) {
17
+ const read = projectReader(entry.path);
18
+ const localPayload = recompose(read);
19
+ const upstream = entry.upstream; // { row, payload } from push.js's classification
20
+
21
+ if (upstream) {
22
+ const backupPath = join(backupDir, backupTs, entry.path, 'upstream.xml');
23
+ mkdirSync(dirname(backupPath), { recursive: true });
24
+ writeFileSync(backupPath, upstream.payload.built_page, 'utf-8');
25
+ }
26
+
27
+ // Re-resolve immediately before saving — the row captured during
28
+ // classification may be stale if something else saved in between.
29
+ const row = await resolveLrpByName(token, entry.name);
30
+ const description = upstream?.row?.description ?? entry.description ?? '';
31
+ const saved = await saveLrp(
32
+ token,
33
+ { ...row, bpmnFileSvg: entry.svg ?? row.bpmnFileSvg },
34
+ localPayload.built_page,
35
+ description
36
+ );
37
+
38
+ // The save minted a new id and bumped the version — re-resolve by name to
39
+ // get the fresh row. deployBpmn must target this new id (the pre-save id is
40
+ // now dead: deploying it 200s but activates nothing), and the workspace
41
+ // entry's version/status must come from here, not the stale pre-save row.
42
+ const deployRow = await resolveLrpByName(token, entry.name);
43
+
44
+ return {
45
+ newChecksum: localChecksum(read),
46
+ newRow: { ...deployRow, description },
47
+ saved,
48
+ deployId: deployRow.id,
49
+ };
50
+ }
51
+
52
+ function deploy(token, id) {
53
+ return deployLrp(token, id);
54
+ }
55
+
56
+ export { pushLrpEntry, deploy };