@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.
- package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
- package/AGENTS.md +456 -0
- package/README.md +808 -0
- package/bin/pipe.js +13 -0
- package/package.json +35 -0
- package/src/apply/adopt.ts +150 -0
- package/src/apply/agentops.ts +71 -0
- package/src/apply/compile.ts +875 -0
- package/src/apply/execute.ts +336 -0
- package/src/apply/flowops.ts +399 -0
- package/src/apply/idmap.ts +241 -0
- package/src/apply/mutations.ts +955 -0
- package/src/apply/registry.ts +430 -0
- package/src/apply/types.ts +134 -0
- package/src/cli/args.ts +88 -0
- package/src/cli.ts +211 -0
- package/src/codec/flow.ts +199 -0
- package/src/codec/pack.ts +103 -0
- package/src/codec/roundtrip.ts +94 -0
- package/src/codec/unpack.ts +198 -0
- package/src/commands/agents.ts +184 -0
- package/src/commands/apply.ts +1144 -0
- package/src/commands/context.ts +119 -0
- package/src/commands/create.ts +79 -0
- package/src/commands/diff.ts +314 -0
- package/src/commands/flows.ts +414 -0
- package/src/commands/misc.ts +644 -0
- package/src/commands/plan.ts +331 -0
- package/src/commands/pull.ts +567 -0
- package/src/commands/runs.ts +83 -0
- package/src/commands/skills.ts +137 -0
- package/src/commands/verify.ts +253 -0
- package/src/config.ts +168 -0
- package/src/diff/agents.ts +122 -0
- package/src/diff/diff.ts +1130 -0
- package/src/diff/flow.ts +318 -0
- package/src/diff/html.ts +322 -0
- package/src/diff/render.ts +101 -0
- package/src/model/payload.ts +154 -0
- package/src/model/tree.ts +99 -0
- package/src/model/volatile.ts +55 -0
- package/src/pipefy/agents.ts +165 -0
- package/src/pipefy/automations.ts +219 -0
- package/src/pipefy/capability.ts +119 -0
- package/src/pipefy/client.ts +267 -0
- package/src/pipefy/discovery.ts +209 -0
- package/src/pipefy/internal.ts +380 -0
- package/src/pipefy/ipaas.ts +365 -0
- package/src/pipefy/reconstruct.ts +775 -0
- package/src/pipefy/reference.ts +251 -0
- package/src/pipefy/snapshot.ts +245 -0
- package/src/pipefy/toolkit.ts +200 -0
- package/src/pipefy/toolkit_bearer.py +137 -0
- package/src/report/integrations.ts +231 -0
- package/src/report/run.ts +475 -0
- package/src/util/fsx.ts +45 -0
- package/src/util/git.ts +32 -0
- package/src/util/json.ts +55 -0
- package/src/util/log.ts +76 -0
- package/src/util/pool.ts +48 -0
- package/src/util/slug.ts +26 -0
- package/src/util/tui.ts +335 -0
- package/src/validate/index.ts +123 -0
- package/src/validate/integrity.ts +387 -0
- package/src/validate/reference.ts +136 -0
- package/src/validate/schema.ts +328 -0
- package/src/workspace/agents.ts +290 -0
- package/src/workspace/docs.ts +407 -0
- package/src/workspace/flows.ts +191 -0
- package/src/workspace/layout.ts +165 -0
- package/src/workspace/lock.ts +148 -0
- package/src/workspace/read.ts +165 -0
- package/src/workspace/reference.ts +24 -0
- package/src/workspace/stamp.ts +301 -0
- package/src/workspace/write.ts +225 -0
package/README.md
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
# Pipefy Process Coder
|
|
2
|
+
|
|
3
|
+
Pull a Pipefy pipe and everything it connects to down to disk as readable JSON,
|
|
4
|
+
edit it with Claude, diff it, push it back.
|
|
5
|
+
|
|
6
|
+
A CLI, and only a CLI. `PLAN.md` describes an Electron app with a CLI seam inside
|
|
7
|
+
it; this build inverts that — `pipe` **is** the product. The desktop shell is
|
|
8
|
+
gone rather than deferred: the one thing a GUI bought was *seeing* a diff, and
|
|
9
|
+
`pipe diff --open` renders the same page in the browser with nothing to install.
|
|
10
|
+
|
|
11
|
+
> **Working on this repo with an agent?** Read [`AGENTS.md`](AGENTS.md) first —
|
|
12
|
+
> what the tool is for, the doctrines that make it trustworthy, the specific ways
|
|
13
|
+
> this API lies, and what is still unbuilt.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipe login # browser sign-in via the Pipefy AI toolkit
|
|
17
|
+
pipe doctor # what can this endpoint actually do?
|
|
18
|
+
pipe pull <pipeId> --out ./my-pipe # snapshot if available, GraphQL if not
|
|
19
|
+
cd my-pipe
|
|
20
|
+
pipe validate # offline, instant
|
|
21
|
+
pipe diff --open # see it
|
|
22
|
+
pipe plan --dry-run # refuses what cannot be applied
|
|
23
|
+
pipe apply # snapshot, apply, verify
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The two workflows
|
|
27
|
+
|
|
28
|
+
An existing pipe starts with **`pipe pull`**. A new one starts with
|
|
29
|
+
**`pipe create`**, which creates the pipe and then pulls it, so both land in the
|
|
30
|
+
same place: a workspace on disk with a baseline to diff against.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
EXISTING PIPE NEW PIPE
|
|
34
|
+
───────────── ────────
|
|
35
|
+
|
|
36
|
+
pipe pull <pipeId> --out ./my-pipe pipe create "New pipe" --org <orgId>
|
|
37
|
+
│ │
|
|
38
|
+
│ snapshot, or GraphQL │ createPipe, then pulls it —
|
|
39
|
+
│ reconstruction if the org │ a new pipe is not empty, it
|
|
40
|
+
│ has no snapshots │ arrives with a start form
|
|
41
|
+
│ │ and three default phases
|
|
42
|
+
└──────────────┬───────────────────────┘
|
|
43
|
+
▼
|
|
44
|
+
./my-pipe/ pipes/<id>--<name>/… the tree you edit
|
|
45
|
+
.ppc/baseline/<id>.json what you diff against
|
|
46
|
+
AGENTS.md · MAPPING.md how to edit it, and what every id means
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
┌──── edit the JSON ─────────────────────────────────┐
|
|
50
|
+
│ by hand, or with an agent reading AGENTS.md │
|
|
51
|
+
│ │
|
|
52
|
+
│ pipe validate shape + referential integrity │ offline,
|
|
53
|
+
│ pipe diff against the baseline │ instant,
|
|
54
|
+
│ pipe plan --dry-run is it all applicable? │ no network
|
|
55
|
+
│ │
|
|
56
|
+
└──── both clean? ───────────────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
pipe apply
|
|
60
|
+
1. snapshot the pipe, labelled pre-apply/<ts> the rollback point
|
|
61
|
+
2. execute step by step, persisting each status resumable
|
|
62
|
+
3. adopt created ids from the responses no read-back needed
|
|
63
|
+
4. verify for free (GraphQL), or ask first before a 2nd snapshot
|
|
64
|
+
│
|
|
65
|
+
┌────────────┴────────────┐
|
|
66
|
+
▼ ▼
|
|
67
|
+
clean something failed
|
|
68
|
+
done pipe apply --resume <plan> replay as compiled
|
|
69
|
+
pipe apply recompile and retry
|
|
70
|
+
(offered) revert everything pipe, flows, agents —
|
|
71
|
+
whatever this run
|
|
72
|
+
actually changed
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `pull` vs `export`
|
|
76
|
+
|
|
77
|
+
**They perform the same read.** Snapshot if the organization has them, GraphQL
|
|
78
|
+
reconstruction if not. The difference is entirely in what they leave on disk.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pipe export <pipeId> --out pipe.json pipe pull <pipeId> --out ./my-pipe
|
|
82
|
+
─────────────────────────────────── ─────────────────────────────
|
|
83
|
+
|
|
84
|
+
pipe.json 76K ./my-pipe/
|
|
85
|
+
pipes/<pipeId>--<slug>/
|
|
86
|
+
ONE file. The structure exactly as pipe.json
|
|
87
|
+
Pipefy's snapshot format has it: phases/00--start-form.json
|
|
88
|
+
twenty flat arrays named after phases/01--new-request.json
|
|
89
|
+
database tables. …7 more phase files
|
|
90
|
+
labels.json webhooks.json
|
|
91
|
+
payload.relations.fields[0] public-form.json relations.json
|
|
92
|
+
.label = "Some field in a late phase" preferences.json
|
|
93
|
+
.phase_id = <a phase id> _readonly/ _non-snapshot/
|
|
94
|
+
.ppc/baseline/<pipeId>.json
|
|
95
|
+
All 48 fields in one array, in .ppc/lock.json
|
|
96
|
+
whatever order the database gave AGENTS.md MAPPING.md CAPABILITIES.md
|
|
97
|
+
REFERENCE.md
|
|
98
|
+
them, each pointing at its phase .gitignore workspace.json
|
|
99
|
+
by id. 29 files
|
|
100
|
+
|
|
101
|
+
The same 48 fields, but each one inside
|
|
102
|
+
the phase file it belongs to, ordered by
|
|
103
|
+
index:
|
|
104
|
+
|
|
105
|
+
phases/00--start-form.json
|
|
106
|
+
.name = "Start form"
|
|
107
|
+
.fields[0].label = "Request title"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
So:
|
|
111
|
+
|
|
112
|
+
| | `pipe export` | `pipe pull` |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| Writes | one JSON file | a workspace directory |
|
|
115
|
+
| Shape | the raw payload, 20 flat arrays | files organised the way you would edit them |
|
|
116
|
+
| Baseline to diff against | no | yes, `.ppc/baseline/<id>.json` |
|
|
117
|
+
| Docs for agents | no | `AGENTS.md`, `MAPPING.md`, `CAPABILITIES.md`, `REFERENCE.md` |
|
|
118
|
+
| Connected repos | no, one repo only | yes, `--depth` walks them |
|
|
119
|
+
| `validate` / `diff` / `plan` / `apply` work on it | **no** | yes |
|
|
120
|
+
|
|
121
|
+
That last row is the whole distinction. Run `pipe diff` in a directory holding
|
|
122
|
+
only an export and you get *"no pipe workspace found"* — because there is no
|
|
123
|
+
workspace, no baseline, and nothing organised to edit. **`export` gives you the
|
|
124
|
+
data; `pull` gives you somewhere to work.**
|
|
125
|
+
|
|
126
|
+
### So why does `export` exist?
|
|
127
|
+
|
|
128
|
+
To carry a read from a machine that can reach the pipe to one that cannot.
|
|
129
|
+
|
|
130
|
+
An FDE often gets one session on a client's laptop, or one token that expires, or
|
|
131
|
+
a VPN they will not have tomorrow. `export` captures the structure as a file they
|
|
132
|
+
can take away. On the other side, `pull --from-file` turns that file into a real
|
|
133
|
+
workspace — the same 29 files, no network, no token, no second snapshot:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# on the machine with access, once
|
|
137
|
+
pipe export <pipeId> --envelope --pretty --out pipe.json
|
|
138
|
+
|
|
139
|
+
# anywhere, afterwards
|
|
140
|
+
pipe pull --from-file pipe.json --out ./my-pipe
|
|
141
|
+
cd cs && pipe validate && pipe diff # the normal loop, offline
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It is a transport format, not a step in the workflow. If you can reach the pipe,
|
|
145
|
+
use `pipe pull` and skip it.
|
|
146
|
+
|
|
147
|
+
**When you do use it, pass `--envelope`.** Without it the file is a bare payload
|
|
148
|
+
with no coverage map, and `--from-file` then has to assume every entity group was
|
|
149
|
+
read in full. On an endpoint with snapshots that is true. On one without, it is
|
|
150
|
+
false in the dangerous direction: a group the reconstruction could not read looks
|
|
151
|
+
*empty*, and empty means delete. Both commands warn about it now, but the fix is
|
|
152
|
+
the flag.
|
|
153
|
+
|
|
154
|
+
## Does it save tokens?
|
|
155
|
+
|
|
156
|
+
Yes, and the saving grows with the size of the pipe. Measured against the live
|
|
157
|
+
FDE pipe — 8 phases, 58 fields, 14 automations — with
|
|
158
|
+
`research/measure-read-cost.ts`, which issues the same queries the Pipefy MCP
|
|
159
|
+
tools issue and weighs the responses.
|
|
160
|
+
|
|
161
|
+
**Learning the pipe through MCP tool calls:**
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
minified as the MCP sends it
|
|
165
|
+
get_pipe 1 call ~3,931 tok ~7,432 tok
|
|
166
|
+
get_automations 1 call ~405 tok ~786 tok
|
|
167
|
+
get_automation × each 14 calls ~2,488 tok ~4,218 tok
|
|
168
|
+
get_webhooks 1 call ~87 tok ~164 tok
|
|
169
|
+
get_field_conditions 1 call ~85 tok ~187 tok
|
|
170
|
+
get_pipe_relations 1 call ~75 tok ~142 tok
|
|
171
|
+
get_pipe_members 1 call ~64 tok ~131 tok
|
|
172
|
+
─────────────────────────────────────────────────────────────
|
|
173
|
+
TOTAL 20 calls ~7,135 tok ~13,060 tok
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The right-hand column is the one that matters. An MCP result is pretty-printed
|
|
177
|
+
with a 2-space indent and then embedded as an escaped string inside its own JSON
|
|
178
|
+
envelope, which is roughly **1.8× the minified data**. Both steps are
|
|
179
|
+
deterministic, so that number is measured, not estimated.
|
|
180
|
+
|
|
181
|
+
**Learning the same pipe through the CLI:**
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
pipe pull <pipeId> --out ./my-pipe ~138 tok its whole terminal output
|
|
185
|
+
|
|
186
|
+
then read only what the task needs:
|
|
187
|
+
MAPPING.md every id → label + phase ~2,179 tok
|
|
188
|
+
AGENTS.md the editing rules ~1,210 tok
|
|
189
|
+
one phase file, to edit its fields ~1,089 tok
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A pull moves 115 KB of pipe structure onto disk and puts **~138 tokens** in the
|
|
193
|
+
context — a fifteen-line summary. Nothing else is read until something needs it,
|
|
194
|
+
and then it is one file, not twenty API responses.
|
|
195
|
+
|
|
196
|
+
For a typical structural edit — orient with `MAPPING.md`, open the phase you are
|
|
197
|
+
changing, edit it — that is around **3,300 tokens against ~13,000**. About a
|
|
198
|
+
quarter. Add a second edit in the same session and the gap widens: the CLI
|
|
199
|
+
rereads one file, while the MCP path either re-queries or holds the whole
|
|
200
|
+
structure in context for the rest of the conversation.
|
|
201
|
+
|
|
202
|
+
### Where the rest of the saving comes from
|
|
203
|
+
|
|
204
|
+
| | |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| **Diffs instead of dumps** | `pipe diff` on a real 3-field change: **148 tokens**. `pipe plan --dry-run`: **73**. `pipe validate`: **7**. The model sees what changed, never the pipe. |
|
|
207
|
+
| **No tool schemas** | The Pipefy MCP exposes ~250 tools. Their JSON schemas sit in context for the whole session whether used or not. `pipe` is one Bash call. |
|
|
208
|
+
| **Refusals cost nothing** | `pipe plan` says "`start_form_title` has no input" offline, before a single request. The MCP path finds out by sending a mutation that succeeds and silently changes nothing. |
|
|
209
|
+
| **Failures explain themselves** | A failed apply writes its step statuses to the plan file. Resuming reads one line, not a re-derivation of what happened. |
|
|
210
|
+
|
|
211
|
+
The honest counter-case: for **one** small read — "what phases does this pipe
|
|
212
|
+
have?" — the MCP is cheaper, because a pull costs a snapshot (~60s) and writes 29
|
|
213
|
+
files you did not need. Use the MCP to look; use `pipe` to change.
|
|
214
|
+
|
|
215
|
+
## Does it handle integrations?
|
|
216
|
+
|
|
217
|
+
The ones with a public write path, yes — all of them, exercised live and
|
|
218
|
+
reverted. See `VALIDATION.md`.
|
|
219
|
+
|
|
220
|
+
| Integration | Coverage |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| **Webhooks** | create, update, delete. The full row: url, actions, headers, filters. |
|
|
223
|
+
| **Pipe relations** (connections) | create, update, delete, including all eight behaviour flags |
|
|
224
|
+
| **Cross-pipe automations** | create and delete, and update by delete+recreate. A `create_card` into another pipe with its field map is what wires the FDE pipe to the CS pipe. |
|
|
225
|
+
| **HTTP request automations** | the whole `send_http_request` surface: url, method, headers, body, and the four authentication params |
|
|
226
|
+
| **Connector fields** | read and diffed; edited inside their phase file |
|
|
227
|
+
| **Public form** | 14 of the 16 `PublicFormSettingsInput` fields; `html` and `css` have no input |
|
|
228
|
+
| **Email templates** | **read-only.** `EmailTemplate` has no Input type on this endpoint at all |
|
|
229
|
+
| **Email inboxes** | **read-only.** No create, update or delete exists |
|
|
230
|
+
| **iPaaS flows** | pulled, diffed, planned and applied by the same commands — see **iPaaS flows** below |
|
|
231
|
+
| **iPaaS connections** | read and referenced, never written. A missing one is created as an inert placeholder so a flow can still be built |
|
|
232
|
+
| **AI agents** | read separately (they are absent from the snapshot payload) and never written back |
|
|
233
|
+
|
|
234
|
+
Every run also writes the integration surface out on its own when `--report` is
|
|
235
|
+
passed — webhooks, relations, outbound automations, connector fields, inbound
|
|
236
|
+
email, and every repo the pipe reaches. See **Runs** below.
|
|
237
|
+
|
|
238
|
+
What makes this trustworthy rather than a claim: the tool refuses at plan time
|
|
239
|
+
when a write path does not exist, per column, rather than sending a mutation that
|
|
240
|
+
reports success and changes nothing. `pipe capabilities` prints the whole
|
|
241
|
+
registry — 29 supported paths, and the reason for each gap.
|
|
242
|
+
|
|
243
|
+
## iPaaS flows
|
|
244
|
+
|
|
245
|
+
Pipefy's iPaaS — "Advanced Automations" — is **Activepieces** underneath, and it
|
|
246
|
+
is where a process does what the automation engine cannot: reach external
|
|
247
|
+
systems, and read pipes other than the one that triggered.
|
|
248
|
+
|
|
249
|
+
**A flow is an entity of its pipe.** Every pipe has its own iPaaS workspace, and
|
|
250
|
+
a pipe's own webhook URLs carry the ids of its flows — so `pipe pull` reads them
|
|
251
|
+
without a flag, and they land under the pipe:
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
pipes/<pipeId>--<slug>/flows/
|
|
255
|
+
_meta.json coverage, projectId, counts, read time
|
|
256
|
+
connections.json externalId, pieceName, displayName, status
|
|
257
|
+
pieces.json pieceName -> version this instance offers
|
|
258
|
+
_readonly/flow-runs.json recent run outcomes; never written back
|
|
259
|
+
<flowId>--<slug>/
|
|
260
|
+
flow.json the flow row
|
|
261
|
+
version.json displayName, valid, schemaVersion, state
|
|
262
|
+
nodes/NN--<name>.json one node per file, in execution order
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A flow is a linked list on the wire — `nextAction`, `firstLoopAction`,
|
|
266
|
+
`children[]` — and one file per node on disk. That fold is a real
|
|
267
|
+
transformation rather than a move, so `link(unlink(F)) == F` is proven per flow
|
|
268
|
+
on every pull and in `test/flow-codec.test.ts`.
|
|
269
|
+
|
|
270
|
+
Authentication needs no new credential: the internal API mints a pipe-scoped
|
|
271
|
+
token and one exchange turns it into an iPaaS session, which also hands back the
|
|
272
|
+
project id. A pipe whose iPaaS is off gets `coverage: "unknown"` — never read as
|
|
273
|
+
"this pipe has no flows".
|
|
274
|
+
|
|
275
|
+
### The two rules the flow path enforces
|
|
276
|
+
|
|
277
|
+
**Enabling a flow is refused** unless `--allow-flow-enable` is passed, and
|
|
278
|
+
refused outright while the project holds a placeholder connection. Enabling
|
|
279
|
+
points a flow at live traffic; that is never incidental.
|
|
280
|
+
|
|
281
|
+
**Every datapill is compared against what was sent.** The whole-tree write the
|
|
282
|
+
API also offers, `IMPORT_FLOW`, runs a schema migration on whatever it is given
|
|
283
|
+
— so a flow already in live form comes back with every reference doubled,
|
|
284
|
+
`{{step_1['output'].x}}` becoming `{{step_1['output']['output'].x}}`. That is
|
|
285
|
+
structurally valid, passes Pipefy's own validator, and resolves to nothing at
|
|
286
|
+
runtime. `pipe` never sends it, compiles to the per-node operations instead, and
|
|
287
|
+
reads the stored bytes back to prove nothing was rewritten.
|
|
288
|
+
|
|
289
|
+
### Flags
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
pipe pull <pipeId> # flows included
|
|
293
|
+
pipe diff # flows diffed offline, against the recorded baseline
|
|
294
|
+
pipe diff --against live # re-read the pipe instead
|
|
295
|
+
pipe apply # flows applied after the pipe, then verified
|
|
296
|
+
pipe apply --allow-flow-enable # permit a status change to ENABLED
|
|
297
|
+
<any of the above> --no-flows # skip the flow half entirely
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`PLAN-IPAAS.md` has the design and the evidence behind it; the run logs under
|
|
301
|
+
`workspaces/*/flows/BUILD-LOG.md` record what each build found.
|
|
302
|
+
|
|
303
|
+
## Install
|
|
304
|
+
|
|
305
|
+
One requirement: **Node 22.18 or newer**. That version strips TypeScript types
|
|
306
|
+
natively, which is why there is no build step and no `dist/`.
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
git clone https://github.com/Guilherme-CA-Dias/pipefy-process-coder
|
|
310
|
+
cd pipefy-process-coder
|
|
311
|
+
npm link # puts `pipe` on PATH, pointing at this folder
|
|
312
|
+
pipe --version # confirms which clone you are running
|
|
313
|
+
pipe doctor # confirms it can reach Pipefy
|
|
314
|
+
pipe skills install # installs this repo's agent skills for every agent you use
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The skills under [`.agents/skills/`](.agents/skills) — named `ppc-pipefy-*`, not
|
|
318
|
+
just `pipefy-*`, to stay out of the namespace the official
|
|
319
|
+
[pipefy/ai-toolkit](https://github.com/pipefy/ai-toolkit) already ships skills
|
|
320
|
+
in — are what teaches an agent (Claude Code or otherwise) how to actually use
|
|
321
|
+
this tool: the pull/edit/diff/apply loop, the pipe-authoring rules, the iPaaS
|
|
322
|
+
flow traps, when to reach for which mechanism. They only trigger for a session
|
|
323
|
+
opened inside this repo until installed somewhere every session reads from,
|
|
324
|
+
because a pulled workspace (**Where `pipe` can run**, below) usually lives
|
|
325
|
+
somewhere else entirely.
|
|
326
|
+
|
|
327
|
+
`pipe skills install` doesn't copy them itself — it hands off to
|
|
328
|
+
[`npx skills add`](https://github.com/vercel-labs/skills), which already
|
|
329
|
+
solves "which agent's directory" for 75+ agents (Claude Code, Cursor, Codex,
|
|
330
|
+
…), picking each one's own path and symlinking by default so there is one
|
|
331
|
+
canonical copy. Every flag after `install` is forwarded to it verbatim —
|
|
332
|
+
`pipe skills install -g -a claude-code -y`, `--skill`, `--copy`, and the rest
|
|
333
|
+
of its README apply as-is. `pipe skills list` still shows what ships with this
|
|
334
|
+
install, locally, with no network call.
|
|
335
|
+
|
|
336
|
+
The source it hands `npx skills add` is a **local path** — this package's own
|
|
337
|
+
install location on disk, resolved the same way `pipe --version` locates
|
|
338
|
+
`package.json` — not a GitHub reference. That means `pipe skills install`
|
|
339
|
+
only works for someone who already has this CLI installed some other way
|
|
340
|
+
(the clone above, or `npm install -g` once published); it is not a way to get
|
|
341
|
+
the skills without it, and this repo's visibility (public or private) is
|
|
342
|
+
irrelevant to it. `pipefy/ai-toolkit`'s own `npx skills add pipefy/ai-toolkit`
|
|
343
|
+
is the other pattern — a GitHub reference, installable with no CLI at all —
|
|
344
|
+
which would need this repo to be public (or private with the installer's own
|
|
345
|
+
git credentials already granted access) if we ever want that decoupled path
|
|
346
|
+
too. Nothing here does that yet.
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
$ pipe --version
|
|
350
|
+
pipefy-process-coder 0.1.0
|
|
351
|
+
node v24.19.0 (needs >=22.18 for native type stripping)
|
|
352
|
+
running C:UsersyouCodepipefy-process-codersrccli.ts
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**No `npm install` needed to run it.** Every import in `src/` is a `node:`
|
|
356
|
+
builtin — child_process, crypto, fs, os, path, readline, url, util, zlib and
|
|
357
|
+
nothing else. Verified by deleting `node_modules` entirely and running `pipe
|
|
358
|
+
capabilities`, which worked. The two devDependencies (`typescript`,
|
|
359
|
+
`@types/node`) are only for `npm run typecheck`.
|
|
360
|
+
|
|
361
|
+
That matters for the actual use case: an FDE on a client-issued laptop, behind a
|
|
362
|
+
proxy that blocks the npm registry, can clone and run.
|
|
363
|
+
|
|
364
|
+
### Where `pipe` can run
|
|
365
|
+
|
|
366
|
+
Anywhere. `npm link` symlinks a launcher into the global npm bin directory, so
|
|
367
|
+
`pipe` is on PATH for every shell and every folder:
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
C:\Users\you\AppData\Roaming\npm\pipe → <the cloned repo>\bin\pipe.js
|
|
371
|
+
~/.npm-global/bin/pipe → <the cloned repo>/bin/pipe.js
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Two consequences of it being a symlink rather than a copy: moving or deleting the
|
|
375
|
+
clone breaks `pipe`, and `git pull` upgrades it instantly with nothing to rebuild.
|
|
376
|
+
If you would rather have a copy, `npm i -g .` installs one.
|
|
377
|
+
|
|
378
|
+
Commands that need no workspace — `login`, `doctor`, `pull`, `create`, `export`,
|
|
379
|
+
`capabilities` — run from any directory. The rest (`validate`, `diff`, `plan`,
|
|
380
|
+
`apply`, `verify`, `status`, `runs`) work on a workspace, and find it by walking
|
|
381
|
+
**up** from the current directory looking for `workspace.json`. So either `cd`
|
|
382
|
+
into the workspace, or pass its path:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
cd ./my-pipe && pipe diff # found by walking up
|
|
386
|
+
pipe diff ./my-pipe # or name it from anywhere
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Other machines, other platforms
|
|
390
|
+
|
|
391
|
+
The package is `private: true`, so it is not on npm — distribution is the git
|
|
392
|
+
clone above. Same three commands on any machine.
|
|
393
|
+
|
|
394
|
+
| | |
|
|
395
|
+
| --- | --- |
|
|
396
|
+
| Node | ≥ 22.18, for native type stripping. Older Node cannot run `.ts` at all. |
|
|
397
|
+
| npm install | not required to run; only for `npm run typecheck` |
|
|
398
|
+
| Auth, easy path | `uv tool install pipefy-cli`, then `pipe login` opens a browser |
|
|
399
|
+
| Auth, no-uv path | `pipe login --token <api token>` — a Pipefy personal token, stored in `~/.ppc/credentials.json` |
|
|
400
|
+
|
|
401
|
+
**Not Windows-only.** Nothing in the tool assumes a shell, and every
|
|
402
|
+
platform-specific branch is explicit: `.exe` suffixes and `Scripts/` vs `bin/`
|
|
403
|
+
when locating the toolkit, `cmd /c start` vs `open` vs `xdg-open` for
|
|
404
|
+
`pipe diff --open`, `USERPROFILE` vs `HOME`, and `~/.ppc` from `os.homedir()`.
|
|
405
|
+
macOS `~/Library/Application Support/uv/tools` is looked in too.
|
|
406
|
+
|
|
407
|
+
Being honest about the limit of that claim: it has been **run** only on Windows
|
|
408
|
+
11 with Node 24. The code is written for all three platforms and the tests are
|
|
409
|
+
pure Node, but macOS and Linux are untested rather than known-good.
|
|
410
|
+
|
|
411
|
+
## Authentication
|
|
412
|
+
|
|
413
|
+
`pipe` does not implement Pipefy login. It borrows the session from the official
|
|
414
|
+
[Pipefy AI toolkit](https://github.com/pipefy/ai-toolkit), so one browser
|
|
415
|
+
sign-in serves this tool, the `pipefy` CLI and the Pipefy MCP server.
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
uv tool install pipefy-cli # once
|
|
419
|
+
pipe login # runs `pipefy auth login` for you
|
|
420
|
+
pipe login --status # who am I, and via which source
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Resolution order:
|
|
424
|
+
|
|
425
|
+
| | Source | When |
|
|
426
|
+
| --- | --- | --- |
|
|
427
|
+
| 1 | `PIPEFY_TOKEN` / `PPC_TOKEN` | explicit override; CI |
|
|
428
|
+
| 2 | the toolkit | its own precedence: static token → service account → keychain session |
|
|
429
|
+
| 3 | `~/.ppc/credentials.json` | `pipe login --token <token>`, for machines without the toolkit |
|
|
430
|
+
|
|
431
|
+
The toolkit issues **five-minute** access tokens, so the credential is a
|
|
432
|
+
provider, not a string: it is fetched per process and re-fetched once when the
|
|
433
|
+
API answers 401, which is the difference between a long apply finishing and
|
|
434
|
+
failing at step 90. Nothing long-lived is ever written to disk by this tool.
|
|
435
|
+
|
|
436
|
+
[`src/pipefy/toolkit_bearer.py`](src/pipefy/toolkit_bearer.py) runs inside the
|
|
437
|
+
toolkit's own environment and prints one bearer as JSON, so the keychain
|
|
438
|
+
backend, the refresh grant and the credential precedence stay the toolkit's
|
|
439
|
+
rather than being reimplemented against a private store shape.
|
|
440
|
+
|
|
441
|
+
> **Windows note.** `pipefy auth login` fails with `WinError 1783 CredWrite` out
|
|
442
|
+
> of the box: Credential Manager caps a credential blob at ~2.5 KB and a Keycloak
|
|
443
|
+
> session is larger. The toolkit's documented unblock is
|
|
444
|
+
> `keychain_backend = "file"` in `%APPDATA%\pipefy\config.toml`, which stores the
|
|
445
|
+
> session in **plaintext** at `%APPDATA%\pipefy\keyring.cfg`. `pipefy auth logout`
|
|
446
|
+
> clears it.
|
|
447
|
+
|
|
448
|
+
No build step and no runtime dependencies: Node ≥ 22.18 strips the types itself,
|
|
449
|
+
so `node src/cli.ts` works on a clean checkout. That matters for a tool an FDE
|
|
450
|
+
runs on a client's laptop.
|
|
451
|
+
|
|
452
|
+
## Two read paths, one shape
|
|
453
|
+
|
|
454
|
+
`pipe pull` uses whichever is available, and both produce the identical
|
|
455
|
+
snapshot-shaped payload, so the codec, diff, plan and apply are written once.
|
|
456
|
+
|
|
457
|
+
| | Snapshot | Reconstruction |
|
|
458
|
+
| --- | --- | --- |
|
|
459
|
+
| When | the org has `createRepoSnapshot` | it does not, or `--no-snapshot`, or the repo is a table |
|
|
460
|
+
| Cost | ~63 s and a new S3 object per pull | a handful of queries, no snapshot rows |
|
|
461
|
+
| Coverage | all 20 entity arrays | 12 groups; the rest are marked **unknown** |
|
|
462
|
+
|
|
463
|
+
The reconstruction path is the Change Migrator recipes, generalised. Everything
|
|
464
|
+
it reads, it reads the way they do:
|
|
465
|
+
|
|
466
|
+
- structure from `pipe { phases { fields } start_form_fields }` — with
|
|
467
|
+
`internal_id` as the payload's `id` and the public `id` as the slug, which is
|
|
468
|
+
what every field reference in every automation depends on
|
|
469
|
+
- field conditions from `pipe.fieldConditions`, split back into the four payload
|
|
470
|
+
arrays a snapshot would have produced
|
|
471
|
+
- automations from the internal `getAutomations` plus `loadAutomationToEdit` per
|
|
472
|
+
automation — the only read path that returns `action_params.field_map`,
|
|
473
|
+
`searchFor`, `responseSchema` and `aiParams`
|
|
474
|
+
- phase jumps from `GET /internal_api/settings/phases/:id`
|
|
475
|
+
|
|
476
|
+
**Coverage is tracked per read, and absence is not emptiness.** A reconstructed
|
|
477
|
+
pull cannot tell "this pipe has no webhooks" from "I have no way to read
|
|
478
|
+
webhooks", so absence from an incomplete read is treated as no information at
|
|
479
|
+
three levels:
|
|
480
|
+
|
|
481
|
+
| Level | Measured on a real pipe (8 phases, 58 fields, 14 automations) | Without the rule |
|
|
482
|
+
| --- | --- | --- |
|
|
483
|
+
| group | `visibilities` has no public query | every reconstructed tree "empties" them |
|
|
484
|
+
| column | the public API has no `color_uuid`, `only_admin_can_move_to_previous` | ~10 phantom changes per phase, each proposing null |
|
|
485
|
+
| row | the internal automations list returns **13 of 15** | two live automations proposed for deletion |
|
|
486
|
+
| collection | no `field_map` for `update_card_field` automations | "delete every field mapping" |
|
|
487
|
+
|
|
488
|
+
Suppressions are reported, never silent: they appear under *not diffed* with the
|
|
489
|
+
row named. The rules are the subject of [test/coverage.test.ts](test/coverage.test.ts).
|
|
490
|
+
|
|
491
|
+
### Verified by reading the same pipe twice
|
|
492
|
+
|
|
493
|
+
The two paths are checked against each other — snapshot vs reconstruction of one
|
|
494
|
+
live pipe. They now agree on the repo row, all 9 phases, all 55 fields, field
|
|
495
|
+
conditions, labels and webhooks. The only remaining difference is real: the
|
|
496
|
+
snapshot payload carries **orphaned `phase_jumps`** pointing at phases that no
|
|
497
|
+
longer exist, and the live endpoint does not. The compiler drops those out of the
|
|
498
|
+
set-replacing write rather than resending them.
|
|
499
|
+
|
|
500
|
+
## Progress
|
|
501
|
+
|
|
502
|
+
`pull`, `apply` and `apply --dry-run` show a live task list: one line per repo
|
|
503
|
+
or plan step, with a spinner, elapsed time and the current stage. It exists
|
|
504
|
+
because a snapshot takes ~63 s and silence for a minute is indistinguishable from
|
|
505
|
+
a hang.
|
|
506
|
+
|
|
507
|
+
Two renderers, chosen by whether stderr is a terminal. The plain one is not a
|
|
508
|
+
fallback — it is what CI, `2>file` and a backgrounded run get:
|
|
509
|
+
|
|
510
|
+
- **live**: repaints a block in place, capped to a window so a 200-step plan
|
|
511
|
+
cannot scroll the terminal, and restores the cursor on Ctrl-C
|
|
512
|
+
- **plain**: one line per task outcome, plus a heartbeat every 15 s for work that
|
|
513
|
+
is still running, and no cursor control at all
|
|
514
|
+
|
|
515
|
+
While a display is up it owns the screen: `log.ts` routes ordinary output through
|
|
516
|
+
it, so an interleaved write cannot land inside a repaint. Set `PPC_NO_TUI=1` to
|
|
517
|
+
force plain. Everything goes to stderr, so `--json` on stdout stays clean.
|
|
518
|
+
|
|
519
|
+
## The diff
|
|
520
|
+
|
|
521
|
+
`pipe diff` is modelled on what the recipes do: one change list per entity type,
|
|
522
|
+
each row tagged create / update / delete, walked in dependency order.
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
pipe diff # against the last pull
|
|
526
|
+
pipe diff --against live # re-read the pipe now
|
|
527
|
+
pipe diff --against 302208097 # cross-repo: a dev→prod migration diff
|
|
528
|
+
pipe diff --open # the HTML view
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
`--against <another pipeId>` is the recipes' actual job. It runs in **cross-repo** mode,
|
|
532
|
+
where matching falls back to the natural key (phase name, phase + field label,
|
|
533
|
+
automation name) exactly as the recipes match, and the resulting id map is what
|
|
534
|
+
lets embedded field references be rewritten on apply — the work the recipes do
|
|
535
|
+
with a Pipefy table keyed `dev_pipe_field_internal_id` and one API call per id.
|
|
536
|
+
|
|
537
|
+
Same-repo mode matches on uuid, then id, which is exact and needs no lookups.
|
|
538
|
+
|
|
539
|
+
## What it refuses, and why that is the point
|
|
540
|
+
|
|
541
|
+
`pipe plan` classifies every change against a hand-maintained registry
|
|
542
|
+
(`src/apply/registry.ts`) and **blocks** on anything unsupported, naming the
|
|
543
|
+
entity and the reason. The worst failure mode available to this tool is an apply
|
|
544
|
+
that reports success having silently ignored something.
|
|
545
|
+
|
|
546
|
+
| Refused | Why |
|
|
547
|
+
| --- | --- |
|
|
548
|
+
| a field's `type_id` | `updatePhaseField` has no `type_id`; delete + create destroys every value |
|
|
549
|
+
| a field's `phase_id` | same, and the diff reconciles the delete/create pair into one refusal |
|
|
550
|
+
| reordering phases | `createPhase` takes an index, `updatePhase` does not |
|
|
551
|
+
| anything under `_readonly/` | no public write path — rejected on the path alone |
|
|
552
|
+
| deleting the start form | it is a real phase at index 0 |
|
|
553
|
+
| deleting a phase | needs `--allow-card-moves`: cards must move first |
|
|
554
|
+
|
|
555
|
+
`pipe capabilities` prints the whole registry. Each refusal names the platform ask
|
|
556
|
+
that would remove it (`ROUTE-B-V2.md` §8).
|
|
557
|
+
|
|
558
|
+
## Safety
|
|
559
|
+
|
|
560
|
+
- `pack(unpack(P)) ≡ P` under a canonical form, checked on every pull and
|
|
561
|
+
runnable as `pipe roundtrip`. A codec that loses data is caught before it can
|
|
562
|
+
corrupt a pipe, not after.
|
|
563
|
+
- Validation is two passes — shape, then referential integrity — and
|
|
564
|
+
distinguishes **pre-existing** problems from introduced ones. Pipe <pipeId>
|
|
565
|
+
ships with five `phase_jumps` pointing at phases that no longer exist; without
|
|
566
|
+
that distinction a freshly pulled, unedited workspace fails validation.
|
|
567
|
+
- `pipe apply` snapshots first (labelled `pre-apply/<ts>`), persists every step's
|
|
568
|
+
status to `.ppc/plans/`, so a failure is resumable with `--resume`.
|
|
569
|
+
- Ids that do not exist yet are placeholders resolved at send time, deep-walked
|
|
570
|
+
through the opaque JSON that `action_params`, `event_params`, `search_for` and
|
|
571
|
+
condition values really are.
|
|
572
|
+
- **Verification asks whether the pipe satisfies the intent**, not whether two
|
|
573
|
+
trees are identical. A newly authored entity is a partial specification: the
|
|
574
|
+
author writes what they care about and the server fills `slug`, `card_synced`,
|
|
575
|
+
`unique` and the connector flags. Comparing full rows failed a correct apply
|
|
576
|
+
with seven differences per created field.
|
|
577
|
+
- **The loop is idempotent, and no longer needs a read-back to stay that way.**
|
|
578
|
+
A create mutation's own response already carries the id — the same one
|
|
579
|
+
`%{_new:<uuid>}` substitution for later steps in the same run gets it from —
|
|
580
|
+
so `pipe apply` writes it straight into the file and the baseline, no network
|
|
581
|
+
read required. A created entity otherwise still reads `"id": null, "_new":
|
|
582
|
+
true"` where it was written, and the next apply would make a duplicate.
|
|
583
|
+
- **A second, live-read snapshot is never taken on its own initiative.** That
|
|
584
|
+
read-back is exactly what used to make the pre-apply rollback snapshot stop
|
|
585
|
+
being restorable the moment the apply that took it finished (`pipe rollback`
|
|
586
|
+
only restores the *latest* one). Verification instead defaults to a free
|
|
587
|
+
GraphQL reconstruction; a full snapshot-based verify-and-adopt is offered
|
|
588
|
+
as a yes/no prompt (default no), or `--post-snapshot` to skip asking.
|
|
589
|
+
- **On a failed or partial apply, `pipe apply` offers to roll back instead** —
|
|
590
|
+
to the pre-apply snapshot, which is normally still the latest one now that
|
|
591
|
+
nothing else has snapshotted the repo in between. Declining (the default)
|
|
592
|
+
leaves the record for `pipe apply --resume` or a manual `pipe rollback`.
|
|
593
|
+
The "is it still the latest" check this depends on was broken until it was
|
|
594
|
+
actually exercised against a real failure — see `Pipe.snapshots(last: N)`
|
|
595
|
+
above — and is now verified live: a real API rejection, a real restorable
|
|
596
|
+
check, a real `restoreRepoToSnapshot` that reaches `SUCCEEDED`.
|
|
597
|
+
- **One apply, one revert question — not three mechanisms scoped separately.**
|
|
598
|
+
On any failure partway — pipe, flow, or agent — `pipe apply` asks exactly
|
|
599
|
+
once whether to revert *everything* that run changed, not just whichever
|
|
600
|
+
half happened to error. Each part is reverted the way its platform actually
|
|
601
|
+
allows: pipe structure via `restoreRepoToSnapshot` (Pipefy's own mechanism,
|
|
602
|
+
only offered while that snapshot is still the latest); flows and agents —
|
|
603
|
+
neither ever had a Pipefy snapshot — by diffing the current live state
|
|
604
|
+
against the read taken right before this apply touched it and sending the
|
|
605
|
+
reverse plan through the same compiler every ordinary apply uses. Rendered
|
|
606
|
+
in full before the one question is asked; reverted, on confirmation, in the
|
|
607
|
+
reverse of the order they were applied. Still true regardless: no revert
|
|
608
|
+
un-sends an email or un-fires a webhook an automation already triggered —
|
|
609
|
+
that's the platform's own irreversibility, not something a bigger rollback
|
|
610
|
+
fixes. Verified live: a real API rejection, the correct reverse plan, and
|
|
611
|
+
the flow back to its exact original chain once applied.
|
|
612
|
+
- No draft rehearsal: `createRepoDraft` does not resolve on this endpoint, and
|
|
613
|
+
the draft's relation semantics could mutate production connected repos. It
|
|
614
|
+
slots in ahead of the pre-apply snapshot when it becomes available.
|
|
615
|
+
|
|
616
|
+
## What the live endpoint actually answers
|
|
617
|
+
|
|
618
|
+
Checked against `api.pipefy.com` and pipe <pipeId>, not assumed. Several of
|
|
619
|
+
these contradict `PLAN.md`, which is why the read paths are introspection-driven
|
|
620
|
+
rather than hard-coded.
|
|
621
|
+
|
|
622
|
+
| | Reality |
|
|
623
|
+
| --- | --- |
|
|
624
|
+
| `createRepoSnapshot`, `renameRepoSnapshot`, `createPresignedUrl` | present |
|
|
625
|
+
| `restoreRepoSnapshot`, `createRepoDraft`, `importRepoSnapshot` | **absent** — no atomic rollback, no rehearsal, no Route B |
|
|
626
|
+
| the 28 mutations the registry needs | all present |
|
|
627
|
+
| `Pipe.snapshots` | a Relay `RepoSnapshotConnection` (`nodes`), not a flat array |
|
|
628
|
+
| `Pipe.snapshots(last: N)` | **not the most recent N.** Measured on a pipe with 86 snapshots: `last: 1` returned `sequenceIndex: 1`, the *oldest* one; `first: 1` correctly returned `sequenceIndex: 86`. The connection is ordered newest-first internally, the opposite of the usual Relay convention — `SnapshotClient.list()` now asks with `first`. This silently broke `pipe rollback`'s and `pipe apply`'s "is this still the latest snapshot" check, which used `last` and compared against the oldest one instead |
|
|
629
|
+
| `Pipe.snapshot(versionId:)` | takes `ID`; declaring `String!` fails the query |
|
|
630
|
+
| `RepoSnapshotStatus` | `PENDING \| UPLOADED \| FAILED` — **UPLOADED** is success |
|
|
631
|
+
| `pipe(id:)` vs `table(id:)` | **both answer for every repo**; `Table.url` (`/pipes/` vs `/apollo_databases/`) is the only discriminator |
|
|
632
|
+
| phase fields | type `PhaseField`; `synced_with_card`, `canCreateNewConnected`, `canConnectExisting`, `canConnectMultiples`, no `unique` |
|
|
633
|
+
| `Field.connectedRepo` | a `PublicRepoUnion` of `PublicPipe \| PublicTable` |
|
|
634
|
+
| `Phase.next_phase_ids` | public, but **forward jumps only** — not the payload's `phase_jumps` |
|
|
635
|
+
| `GET settings/phases/:id` | answers `data.attributes.jump_targets`; the **PUT** answers `data.next_phase_ids` — different shapes |
|
|
636
|
+
| internal `AutomationActionParams` | has `taskParams`, `aiBehaviorParams`, `authenticationType` that the Change Migrator query omits |
|
|
637
|
+
| `CreatePhaseFieldInput` | has **no `uuid`** — the server mints it, so a client uuid is only a local handle |
|
|
638
|
+
| renaming a field | does **not** regenerate the slug. `triage_owner` survived a rename to "Triage owner (FDE)", so `PLAN.md` §9's worry about breaking slug-keyed integrations does not apply |
|
|
639
|
+
|
|
640
|
+
## Deviations from PLAN.md
|
|
641
|
+
|
|
642
|
+
Three, all deliberate:
|
|
643
|
+
|
|
644
|
+
1. **CLI only.** The Electron shell of `PLAN.md` phase 3 was dropped, not built:
|
|
645
|
+
`pipe diff --open` opens the HTML view in the browser.
|
|
646
|
+
2. **A phase's jump set lives in its phase file** as `jump_target_ids`, not in a
|
|
647
|
+
separate `phase-jumps.json`. The internal endpoint is per-phase and
|
|
648
|
+
set-replacing, so a set is the honest shape and there is no second file to
|
|
649
|
+
keep consistent.
|
|
650
|
+
3. **`webhooks.json` is one array file**, matching `labels.json`, rather than a
|
|
651
|
+
directory.
|
|
652
|
+
|
|
653
|
+
## Layout
|
|
654
|
+
|
|
655
|
+
```
|
|
656
|
+
src/
|
|
657
|
+
cli.ts command dispatch
|
|
658
|
+
config.ts endpoints, token resolution, limits
|
|
659
|
+
pipefy/ graphql client · internal API adapter · snapshots ·
|
|
660
|
+
reconstruction · discovery · capability detection
|
|
661
|
+
codec/ unpack · pack · round-trip comparator
|
|
662
|
+
workspace/ layout · write · read · lock · generated docs
|
|
663
|
+
validate/ shape · referential integrity
|
|
664
|
+
diff/ change set · text render · HTML view
|
|
665
|
+
apply/ registry · mutation builders · compiler · id map · executor ·
|
|
666
|
+
flowops (iPaaS operations)
|
|
667
|
+
report/ run records · the integration surface of a pipe
|
|
668
|
+
commands/ one file per command
|
|
669
|
+
test/ codec, workspace, diff/plan, id map
|
|
670
|
+
research/snap.json the pipe <pipeId> fixture every test runs against
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
## Commands
|
|
674
|
+
|
|
675
|
+
| | |
|
|
676
|
+
| --- | --- |
|
|
677
|
+
| `pipe --version` | version, Node version, and which clone is on PATH |
|
|
678
|
+
| `pipe login` | store a token in `~/.ppc/credentials.json` |
|
|
679
|
+
| `pipe doctor` | capabilities, missing mutations, internal-API status |
|
|
680
|
+
| `pipe skills` | the agent skills this install ships with; `install` hands off to `npx skills add` |
|
|
681
|
+
| `pipe create <name>` | create an empty pipe and pull it, ready to be built from files |
|
|
682
|
+
| `pipe pull <id>` | pull into a workspace; `--depth`, `--reuse-snapshot`, `--from-file` |
|
|
683
|
+
| `pipe export <id>` | one repo as a single payload, no workspace, no token needed to consume |
|
|
684
|
+
| `pipe validate` | offline shape + integrity |
|
|
685
|
+
| `pipe diff` | baseline, live, or cross-repo; `--html`, `--open`, `--json` |
|
|
686
|
+
| `pipe plan` | classify and order; works offline for classification |
|
|
687
|
+
| `pipe apply` | snapshot, execute, adopt ids, verify for free; `--resume`, `--dry-run`, `--post-snapshot` |
|
|
688
|
+
| `pipe verify` | read back and diff against the workspace |
|
|
689
|
+
| `pipe rollback` | restore a snapshot (needs `restoreRepoSnapshot`) |
|
|
690
|
+
| `pipe status` | what is in the workspace and how it was read |
|
|
691
|
+
| `pipe capabilities` | the write-coverage registry |
|
|
692
|
+
| `pipe runs` | every run recorded in the workspace; `--last`, `--show`, `--failed` |
|
|
693
|
+
| `pipe roundtrip` | codec self-check |
|
|
694
|
+
|
|
695
|
+
`pipe export` and `pipe pull --from-file` pair up — see **The two workflows**
|
|
696
|
+
above for when that is the right shape, and why `--envelope` matters.
|
|
697
|
+
|
|
698
|
+
## Runs
|
|
699
|
+
|
|
700
|
+
Off by default. Pass `--report` to a pull, apply, dry run or verify and it
|
|
701
|
+
writes a folder into the workspace recording what happened:
|
|
702
|
+
|
|
703
|
+
```
|
|
704
|
+
runs/index.md one line per run, oldest first
|
|
705
|
+
runs/<when>--<kind>/report.md what happened, readable, no credentials
|
|
706
|
+
runs/<when>--<kind>/run.json the same thing structured
|
|
707
|
+
runs/<when>--<kind>/pipe.json the payload as of the end of that run
|
|
708
|
+
runs/<when>--<kind>/integrations.json webhooks, relations, cross-repo automations
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
The reason this is a file and not terminal scrollback is that the write path is
|
|
712
|
+
non-atomic. When an apply half-succeeds, the plan file already says what the
|
|
713
|
+
tool meant to send; the run folder says which of those calls landed, in what
|
|
714
|
+
order, with what error, and what the pipe looked like on either side — an apply
|
|
715
|
+
also writes `pipe.before.json`, so both sides of the change sit in one place.
|
|
716
|
+
|
|
717
|
+
`integrations.json` is the outward-facing surface pulled out on its own:
|
|
718
|
+
webhooks, pipe relations, automations that reach another repo, connector fields,
|
|
719
|
+
inbound email. Those are the parts of a change whose blast radius leaves the
|
|
720
|
+
pipe, and the parts that carry credentials — so `report.md` names hosts and ids
|
|
721
|
+
and never a secret, while the two payload copies are covered by the generated
|
|
722
|
+
`.gitignore`.
|
|
723
|
+
|
|
724
|
+
It is opt-in because a run folder is a full copy of the pipe: one per command
|
|
725
|
+
would grow the workspace without bound and put webhook URLs and HTTP-request
|
|
726
|
+
credentials in one more place. The record an apply *needs* is the plan file
|
|
727
|
+
under `.ppc/plans/`, which is written either way and is what `--resume` reads.
|
|
728
|
+
|
|
729
|
+
A finished plan carries its outcome in the filename, so the directory reads as a
|
|
730
|
+
history without opening anything:
|
|
731
|
+
|
|
732
|
+
```
|
|
733
|
+
.ppc/plans/
|
|
734
|
+
2026-09-01T20-14-12Z--945e060b--applied.json
|
|
735
|
+
2026-09-01T20-04-15Z--29374430--partial.json
|
|
736
|
+
2026-09-01T20-00-46Z--9588c4ac--failed.json
|
|
737
|
+
2026-09-01T19-58-02Z--cdf84d72.json <- no suffix: never applied
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
The plan id does not change, so `pipe apply --resume <id>` keeps working whatever
|
|
741
|
+
the file is called.
|
|
742
|
+
`runs/` is for the times you want the human account kept — a migration someone
|
|
743
|
+
will ask about later, a change with an audit trail, a sequence of runs compared
|
|
744
|
+
side by side.
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
pipe apply --report # keep the account of this one
|
|
748
|
+
pipe runs # what has been recorded in this workspace
|
|
749
|
+
pipe runs --last # the newest report in full
|
|
750
|
+
pipe runs --failed # only the ones that did not finish
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
## Secrets
|
|
754
|
+
|
|
755
|
+
A pulled workspace contains webhook URLs — possession is authorization — and
|
|
756
|
+
HTTP-request automations can carry live credentials in `action_params`. A
|
|
757
|
+
`.gitignore` is generated on pull and the first pull warns. Do not commit a
|
|
758
|
+
workspace.
|
|
759
|
+
|
|
760
|
+
## Tests
|
|
761
|
+
|
|
762
|
+
```bash
|
|
763
|
+
npm test # node --test, no test framework to install
|
|
764
|
+
npm run typecheck # tsc --noEmit, strict
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
Every test runs against `research/snap.json`, the real pipe <pipeId> snapshot
|
|
768
|
+
captured during the research in `PLAN.md`.
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
## Internal
|
|
773
|
+
```bash
|
|
774
|
+
pipe pull <pipeId> --out ./my-pipe # ~63s per repo (snapshot generation)
|
|
775
|
+
cd my-pipe-request
|
|
776
|
+
# ...edit the JSON, with Claude or by hand...
|
|
777
|
+
pipe validate # offline, instant
|
|
778
|
+
pipe diff # or: pipe diff --open for the HTML view
|
|
779
|
+
pipe plan --dry-run # refuses what can't be applied, before anything is sent
|
|
780
|
+
pipe apply # snapshot → apply → verify → sync the files back
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Three things that differ from a plain git workflow
|
|
784
|
+
|
|
785
|
+
The diff has three modes, and only the first is "what did I change":
|
|
786
|
+
pipe diff # vs .ppc/baseline — your edits
|
|
787
|
+
pipe diff --against live # re-read the pipe now — did someone else touch it?
|
|
788
|
+
pipe diff --against 302208097 # cross-repo: a dev→prod migration diff
|
|
789
|
+
pipe diff --open # HTML view
|
|
790
|
+
|
|
791
|
+
pipe plan will refuse things, by design — a field's type_id, a field's phase_id, reordereadonly/. It names the entity and the reason rather than applying a partial change andreporting success.
|
|
792
|
+
|
|
793
|
+
A successful pipe apply rewrites your files. It adopts each created entity's real id straight from the create response — no read-back needed — because a created entity is written as "id": null, "_new": true and a second apply would otherwise duplicate it. Verification then runs for free against a GraphQL reconstruction unless you ask for a full snapshot-based one (pipe apply --post-snapshot), since a second snapshot is what used to make the pre-apply rollback point stop being restorable. A failed or partial apply offers one combined revert covering everything that run actually changed — pipe structure via the pre-apply snapshot, plus flows and agents via their own reverse plan, since neither ever had a Pipefy snapshot to begin with. Rendered in full before asking once; declining leaves the files untouched so --resume works. It cannot un-send an email or un-fire a webhook an automation already triggered — that's the platform's own irreversibility, not something the revert can reach.
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
## TODO
|
|
797
|
+
|
|
798
|
+
[] - Define dynamic content in the construction
|
|
799
|
+
[] - Index the workspace faster so it doesnt consume much tokens
|
|
800
|
+
[x] - After applying, update the baseline
|
|
801
|
+
[] - Add verbose config
|
|
802
|
+
[x] - add Databases
|
|
803
|
+
[] - add possibility to add pipe to a workspace
|
|
804
|
+
[] - Make the apply verbose on plan items
|
|
805
|
+
[] - Check the baseline is not stale before planning: compare the pulled version id against the live one, and refuse to propose deletions when the pipe has moved since the pull. A stale baseline turns someone else's changes into a diff — seen live, where a verification diff listed five changes made elsewhere as deletions the workspace wanted
|
|
806
|
+
[] - pipe pull inside a workspace pulls the pipes automatically
|
|
807
|
+
[] - One design point worth stating
|
|
808
|
+
Each repo still applies independently — own plan file, own pre-apply snapshot, own verify. That's deliberate: they're separate repos with separate baselines, and a failure in one must not roll back or abandon another that already succeeded. So it's a loop over the real thing, not a merged mega-plan. Exit code is the worst of them. --plan and --resume stay single-repo, since a saved plan belongs to exactly one.
|