@pipefy/pipefy-process-coder 0.1.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 (92) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
  19. package/AGENTS.md +456 -0
  20. package/README.md +808 -0
  21. package/bin/pipe.js +13 -0
  22. package/package.json +35 -0
  23. package/src/apply/adopt.ts +150 -0
  24. package/src/apply/agentops.ts +71 -0
  25. package/src/apply/compile.ts +875 -0
  26. package/src/apply/execute.ts +336 -0
  27. package/src/apply/flowops.ts +399 -0
  28. package/src/apply/idmap.ts +241 -0
  29. package/src/apply/mutations.ts +955 -0
  30. package/src/apply/registry.ts +430 -0
  31. package/src/apply/types.ts +134 -0
  32. package/src/cli/args.ts +88 -0
  33. package/src/cli.ts +211 -0
  34. package/src/codec/flow.ts +199 -0
  35. package/src/codec/pack.ts +103 -0
  36. package/src/codec/roundtrip.ts +94 -0
  37. package/src/codec/unpack.ts +198 -0
  38. package/src/commands/agents.ts +184 -0
  39. package/src/commands/apply.ts +1144 -0
  40. package/src/commands/context.ts +119 -0
  41. package/src/commands/create.ts +79 -0
  42. package/src/commands/diff.ts +314 -0
  43. package/src/commands/flows.ts +414 -0
  44. package/src/commands/misc.ts +644 -0
  45. package/src/commands/plan.ts +331 -0
  46. package/src/commands/pull.ts +567 -0
  47. package/src/commands/runs.ts +83 -0
  48. package/src/commands/skills.ts +137 -0
  49. package/src/commands/verify.ts +253 -0
  50. package/src/config.ts +168 -0
  51. package/src/diff/agents.ts +122 -0
  52. package/src/diff/diff.ts +1130 -0
  53. package/src/diff/flow.ts +318 -0
  54. package/src/diff/html.ts +322 -0
  55. package/src/diff/render.ts +101 -0
  56. package/src/model/payload.ts +154 -0
  57. package/src/model/tree.ts +99 -0
  58. package/src/model/volatile.ts +55 -0
  59. package/src/pipefy/agents.ts +165 -0
  60. package/src/pipefy/automations.ts +219 -0
  61. package/src/pipefy/capability.ts +119 -0
  62. package/src/pipefy/client.ts +267 -0
  63. package/src/pipefy/discovery.ts +209 -0
  64. package/src/pipefy/internal.ts +380 -0
  65. package/src/pipefy/ipaas.ts +365 -0
  66. package/src/pipefy/reconstruct.ts +775 -0
  67. package/src/pipefy/reference.ts +251 -0
  68. package/src/pipefy/snapshot.ts +245 -0
  69. package/src/pipefy/toolkit.ts +200 -0
  70. package/src/pipefy/toolkit_bearer.py +137 -0
  71. package/src/report/integrations.ts +231 -0
  72. package/src/report/run.ts +475 -0
  73. package/src/util/fsx.ts +45 -0
  74. package/src/util/git.ts +32 -0
  75. package/src/util/json.ts +55 -0
  76. package/src/util/log.ts +76 -0
  77. package/src/util/pool.ts +48 -0
  78. package/src/util/slug.ts +26 -0
  79. package/src/util/tui.ts +335 -0
  80. package/src/validate/index.ts +123 -0
  81. package/src/validate/integrity.ts +387 -0
  82. package/src/validate/reference.ts +136 -0
  83. package/src/validate/schema.ts +328 -0
  84. package/src/workspace/agents.ts +290 -0
  85. package/src/workspace/docs.ts +407 -0
  86. package/src/workspace/flows.ts +191 -0
  87. package/src/workspace/layout.ts +165 -0
  88. package/src/workspace/lock.ts +148 -0
  89. package/src/workspace/read.ts +165 -0
  90. package/src/workspace/reference.ts +24 -0
  91. package/src/workspace/stamp.ts +301 -0
  92. package/src/workspace/write.ts +225 -0
@@ -0,0 +1,336 @@
1
+ import { writeJson } from '../util/fsx.ts';
2
+ import { debug, dim, green, red, yellow } from '../util/log.ts';
3
+ import { Progress } from '../util/tui.ts';
4
+ import type { PipefyClient } from '../pipefy/client.ts';
5
+ import type { InternalApi } from '../pipefy/internal.ts';
6
+ import { IdMap } from './idmap.ts';
7
+ import type { Plan, Step } from './types.ts';
8
+
9
+ /**
10
+ * The executor. Non-atomic forward by necessity, so:
11
+ *
12
+ * - every step's status is persisted to `.ppc/plans/<id>.json` as it runs, which
13
+ * is what makes a failed apply resumable rather than a mystery
14
+ * - ids created by a step are substituted into later steps at send time, not at
15
+ * compile time (PLAN.md §9)
16
+ * - a step that still depends on an unresolved placeholder is skipped with a
17
+ * reason, never sent with a literal `%{_new:...}` in it
18
+ */
19
+
20
+ export type ExecuteOpts = {
21
+ client: PipefyClient;
22
+ internal: InternalApi;
23
+ /** Where to persist step status. */
24
+ planPath: string;
25
+ dryRun: boolean;
26
+ /** Stop at the first failure (default) or keep going. */
27
+ continueOnError?: boolean;
28
+ onProgress?: (step: Step, plan: Plan) => void;
29
+ };
30
+
31
+ export type ExecuteResult = {
32
+ plan: Plan;
33
+ done: number;
34
+ failed: number;
35
+ skipped: number;
36
+ /** Steps never attempted because an earlier one failed. */
37
+ pending: number;
38
+ /**
39
+ * What each placeholder became. Verification needs it: the intent says
40
+ * `%{_new:<uuid>}` and the pipe holds the id the server chose, so without this
41
+ * the only options are to skip those values or to report them as differences.
42
+ */
43
+ idMap: IdMap;
44
+ };
45
+
46
+ const pluck = (obj: unknown, path: string[]): unknown => {
47
+ let cur: unknown = obj;
48
+ for (const key of path) {
49
+ if (cur && typeof cur === 'object' && key in (cur as Record<string, unknown>)) {
50
+ cur = (cur as Record<string, unknown>)[key];
51
+ } else {
52
+ return undefined;
53
+ }
54
+ }
55
+ return cur;
56
+ };
57
+
58
+ export const execute = async (plan: Plan, opts: ExecuteOpts): Promise<ExecuteResult> => {
59
+ const idMap = new IdMap(plan.idMap);
60
+ plan.status = 'running';
61
+
62
+ /**
63
+ * The step list, live. A plan is a queue of network calls whose count and
64
+ * order the user has already seen in `pipe plan`, so showing that same list
65
+ * fill in is the honest progress indicator — and in a dry run it shows exactly
66
+ * what would be sent, in order, without sending it.
67
+ */
68
+ const ui = new Progress({
69
+ title: opts.dryRun ? `dry run · ${plan.steps.length} steps` : `applying ${plan.steps.length} steps`,
70
+ });
71
+ for (const s of plan.steps) {
72
+ ui.add({
73
+ id: s.id,
74
+ label: s.description,
75
+ state: s.status === 'done' ? 'done' : 'pending',
76
+ tags: [
77
+ ...(s.destructive ? ['destructive'] : []),
78
+ ...(s.unverified ? ['unverified'] : []),
79
+ ...(s.kind.startsWith('internal') ? ['internal-api'] : []),
80
+ ],
81
+ });
82
+ }
83
+ ui.start();
84
+
85
+ const persist = async () => {
86
+ if (!opts.dryRun) await writeJson(opts.planPath, plan);
87
+ };
88
+
89
+ for (const s of plan.steps) {
90
+ if (s.status === 'done') {
91
+ // Resume: a completed create still has to re-register its placeholder so
92
+ // later steps can resolve against it.
93
+ if (s.produces && s.result !== undefined) {
94
+ const id = Number(pluck(s.result, s.produces.path));
95
+ if (Number.isFinite(id)) idMap.resolvePlaceholder(s.produces.placeholder, id, s.produces.aliases ?? []);
96
+ }
97
+ continue;
98
+ }
99
+
100
+ const unresolved = idMap.unresolved(s.request.variables ?? s.request.form ?? s.request.path ?? null);
101
+ if (unresolved.length) {
102
+ s.status = 'skipped';
103
+ s.error = `depends on ids that were never created: ${unresolved.join(', ')}`;
104
+ ui.update(s.id, { state: 'skipped', detail: s.error });
105
+ await persist();
106
+ continue;
107
+ }
108
+
109
+ const request = {
110
+ ...s.request,
111
+ variables: s.request.variables ? idMap.rewrite(s.request.variables) : undefined,
112
+ form: s.request.form ? idMap.rewrite(s.request.form) : undefined,
113
+ path: s.request.path ? (idMap.rewrite({ p: s.request.path }) as { p: string }).p : undefined,
114
+ };
115
+
116
+ /**
117
+ * Nothing leaves here still carrying a placeholder.
118
+ *
119
+ * A *registered* placeholder passes the unresolved check above, but is only
120
+ * substituted if its key is known to hold an id — so a new id-bearing key
121
+ * that nobody added to ID_BEARING_KEYS sends the marker to the API verbatim.
122
+ * That happened with `fieldConditionIds`, and the resulting error named the
123
+ * field but said nothing about the cause. This refuses to send, and says
124
+ * which key.
125
+ */
126
+ const leftovers: string[] = [];
127
+ const findMarkers = (v: unknown, path: string) => {
128
+ if (typeof v === 'string') {
129
+ if (v.includes('_new:')) leftovers.push(`${path} = ${v}`);
130
+ return;
131
+ }
132
+ if (Array.isArray(v)) {
133
+ v.forEach((x, i) => findMarkers(x, `${path}[${i}]`));
134
+ return;
135
+ }
136
+ if (v && typeof v === 'object') {
137
+ for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
138
+ findMarkers(val, path ? `${path}.${k}` : k);
139
+ }
140
+ }
141
+ };
142
+ findMarkers(request.variables ?? null, '');
143
+ findMarkers(request.form ?? null, '');
144
+
145
+ if (leftovers.length) {
146
+ s.status = 'failed';
147
+ s.error =
148
+ `unsubstituted placeholder at ${leftovers.join(', ')} — that key is not in ID_BEARING_KEYS, ` +
149
+ 'so nothing replaced it. Not sent.';
150
+ s.finishedAt = new Date().toISOString();
151
+ ui.update(s.id, { state: 'failed', detail: s.error });
152
+ await persist();
153
+ if (!opts.continueOnError) break;
154
+ continue;
155
+ }
156
+
157
+ if (opts.dryRun) {
158
+ s.status = 'skipped';
159
+ /**
160
+ * A dry run marks each step done in the display rather than skipped: the
161
+ * step is complete as far as this run goes, and painting a whole plan
162
+ * yellow suggests something went wrong when nothing did. The title says
163
+ * "dry run", and nothing was sent.
164
+ */
165
+ ui.update(s.id, { state: 'done', detail: 'not sent' });
166
+ debug(JSON.stringify(request.variables ?? request.form ?? {}).slice(0, 400));
167
+ opts.onProgress?.(s, plan);
168
+ continue;
169
+ }
170
+
171
+ s.status = 'running';
172
+ s.startedAt = new Date().toISOString();
173
+ await persist();
174
+ ui.update(s.id, { state: 'running' });
175
+
176
+ try {
177
+ let result: unknown;
178
+ if (s.kind === 'graphql') {
179
+ try {
180
+ result = await opts.client.raw(request.query as string, request.variables, s.request.mutation);
181
+ } catch (e) {
182
+ /**
183
+ * `phaseSettings` is known to answer "Something went wrong" for a change
184
+ * it has just made — observed twice, on two different runs, with the
185
+ * `phase_team_memberships` row present in the read-back both times and
186
+ * timestamped to the failing call. Its failures are therefore
187
+ * inconclusive, not negative, and saying so is the difference between
188
+ * an operator re-running a harmless write and an operator hunting a
189
+ * change that already exists.
190
+ */
191
+ if (s.request.mutation === 'phaseSettings') {
192
+ throw new Error(
193
+ `${(e as Error).message} — note that phaseSettings has been observed reporting this ` +
194
+ 'for a change it did apply. Re-read the phase before assuming nothing happened.',
195
+ );
196
+ }
197
+ throw e;
198
+ }
199
+ } else if (s.kind === 'internal-graphql') {
200
+ result = await runInternalAutomationStep(opts.internal, s, request.variables ?? {});
201
+ } else if (s.kind === 'internal-rest') {
202
+ result = await runInternalRestStep(opts.internal, s, request);
203
+ } else {
204
+ result = { noop: true };
205
+ }
206
+
207
+ s.result = result;
208
+ s.status = 'done';
209
+ s.finishedAt = new Date().toISOString();
210
+ ui.update(s.id, { state: 'done' });
211
+
212
+ if (s.produces) {
213
+ const raw = pluck(result, s.produces.path);
214
+ const id = Number(raw);
215
+ if (Number.isFinite(id)) {
216
+ idMap.resolvePlaceholder(s.produces.placeholder, id, s.produces.aliases ?? []);
217
+ } else {
218
+ ui.note(yellow('! ') + `${s.id} succeeded but produced no id at ${s.produces.path.join('.')} — later steps may skip`);
219
+ }
220
+ }
221
+ opts.onProgress?.(s, plan);
222
+ } catch (e) {
223
+ s.status = 'failed';
224
+ s.error = (e as Error).message;
225
+ s.finishedAt = new Date().toISOString();
226
+ ui.update(s.id, { state: 'failed', detail: s.error });
227
+ await persist();
228
+ opts.onProgress?.(s, plan);
229
+ if (!opts.continueOnError) break;
230
+ }
231
+ await persist();
232
+ }
233
+
234
+ const counts = plan.steps.reduce(
235
+ (acc, s) => {
236
+ acc[s.status] = (acc[s.status] ?? 0) + 1;
237
+ return acc;
238
+ },
239
+ {} as Record<string, number>,
240
+ );
241
+
242
+ plan.status =
243
+ (counts['failed'] ?? 0) > 0
244
+ ? (counts['done'] ?? 0) > 0
245
+ ? 'partially-applied'
246
+ : 'failed'
247
+ : opts.dryRun
248
+ ? 'planned'
249
+ : 'applied';
250
+ await persist();
251
+
252
+ const done = counts['done'] ?? 0;
253
+ const failed = counts['failed'] ?? 0;
254
+ const skipped = counts['skipped'] ?? 0;
255
+
256
+ /**
257
+ * A dry run's steps are recorded as skipped, which is right for the plan file
258
+ * and wrong as a headline: "0 done, 2 skipped" reads like something went
259
+ * wrong. Nothing did — nothing was sent.
260
+ */
261
+ ui.stop(
262
+ opts.dryRun
263
+ ? ' ' + green(`${plan.steps.length} step${plan.steps.length === 1 ? '' : 's'} simulated`) + dim(', nothing sent')
264
+ : ' ' +
265
+ [
266
+ done ? green(`${done} done`) : dim('0 done'),
267
+ failed ? red(`${failed} failed`) : dim('0 failed'),
268
+ skipped ? yellow(`${skipped} skipped`) : dim('0 skipped'),
269
+ ].join(dim(' · ')) +
270
+ dim(` · ${(ui.elapsed / 1000).toFixed(1)}s`),
271
+ );
272
+
273
+ return {
274
+ plan,
275
+ done: counts['done'] ?? 0,
276
+ failed: counts['failed'] ?? 0,
277
+ skipped: counts['skipped'] ?? 0,
278
+ pending: counts['pending'] ?? 0,
279
+ idMap,
280
+ };
281
+ };
282
+
283
+ const runInternalAutomationStep = async (internal: InternalApi, s: Step, variables: Record<string, unknown>) => {
284
+ if (s.request.mutation === 'createAutomation') {
285
+ const created = await internal.createAutomation(variables);
286
+ if (!created) throw new Error('createAutomation returned no automation (internal API unavailable or rejected)');
287
+ return { createAutomation: { automation: created } };
288
+ }
289
+ if (s.request.mutation === 'deleteAutomation') {
290
+ const success = await internal.deleteAutomation(String(variables['id']));
291
+ if (!success) throw new Error(`deleteAutomation(${String(variables['id'])}) did not report success`);
292
+ return { deleteAutomation: { success } };
293
+ }
294
+ if (s.request.mutation === 'updateAutomation') {
295
+ const res = await internal.gql<Record<string, unknown>>(
296
+ `mutation updateAutomation($input: UpdateAutomationInput!) {
297
+ updateAutomation(input: $input) { automation { id } error_details { object_key messages } }
298
+ }`,
299
+ { input: variables },
300
+ 'updateAutomation',
301
+ );
302
+ if (!res) throw new Error('updateAutomation failed on the internal endpoint');
303
+ return res;
304
+ }
305
+ throw new Error(`unknown internal mutation ${s.request.mutation}`);
306
+ };
307
+
308
+ const runInternalRestStep = async (internal: InternalApi, s: Step, request: Step['request']) => {
309
+ const phaseId = /\/phases\/([^/?]+)/.exec(request.path ?? '')?.[1];
310
+ if (!phaseId) throw new Error(`could not read a phase id out of ${request.path}`);
311
+ const targets = (request.form?.['phase[jump_target_ids][]'] ?? []).filter((t) => t !== '');
312
+
313
+ /**
314
+ * A jump target is an id and nothing else.
315
+ *
316
+ * The endpoint answers a malformed target with a bare 404, which reads as
317
+ * "that phase does not exist" and sends you looking in the wrong place. Every
318
+ * such 404 so far has been a substitution that did not happen — a placeholder
319
+ * rewritten to `%{344123894}` rather than to `344123894`. Checking the shape
320
+ * here names the real cause.
321
+ */
322
+ const malformed = targets.filter((t) => !/^[0-9]+$/.test(t));
323
+ if (malformed.length) {
324
+ throw new Error(
325
+ `jump target(s) ${malformed.join(', ')} are not ids — an id substitution did not happen. Not sent.`,
326
+ );
327
+ }
328
+
329
+ const res = await internal.setPhaseJumpTargets(phaseId, targets);
330
+ /**
331
+ * Assert on the echoed next_phase_ids, not on the HTTP status — PLAN.md §2.1.
332
+ * A 200 from an unversioned endpoint is not evidence that anything changed.
333
+ */
334
+ if (!res.ok) throw new Error(res.error ?? 'phase jump write could not be confirmed from the response');
335
+ return res;
336
+ };