@pipefy/pipefy-process-coder 0.1.0 → 0.1.1

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 (159) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
  19. package/AGENTS.md +465 -456
  20. package/README.md +825 -807
  21. package/bin/pipe.js +25 -4
  22. package/dist/apply/adopt.js +128 -0
  23. package/dist/apply/agentops.js +42 -0
  24. package/dist/apply/compile.js +722 -0
  25. package/dist/apply/execute.js +277 -0
  26. package/dist/apply/flowops.js +293 -0
  27. package/dist/apply/idmap.js +233 -0
  28. package/dist/apply/mutations.js +704 -0
  29. package/dist/apply/registry.js +348 -0
  30. package/dist/apply/types.js +18 -0
  31. package/dist/cli/args.js +79 -0
  32. package/dist/cli.js +187 -0
  33. package/dist/codec/flow.js +164 -0
  34. package/dist/codec/pack.js +88 -0
  35. package/dist/codec/roundtrip.js +86 -0
  36. package/dist/codec/unpack.js +181 -0
  37. package/dist/commands/agents.js +145 -0
  38. package/dist/commands/apply.js +982 -0
  39. package/dist/commands/context.js +79 -0
  40. package/dist/commands/create.js +63 -0
  41. package/dist/commands/diff.js +294 -0
  42. package/dist/commands/flows.js +348 -0
  43. package/dist/commands/misc.js +528 -0
  44. package/dist/commands/plan.js +301 -0
  45. package/dist/commands/pull.js +511 -0
  46. package/dist/commands/runs.js +74 -0
  47. package/dist/commands/skills.js +131 -0
  48. package/dist/commands/verify.js +205 -0
  49. package/dist/config.js +125 -0
  50. package/dist/diff/agents.js +80 -0
  51. package/dist/diff/diff.js +983 -0
  52. package/dist/diff/flow.js +258 -0
  53. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  54. package/dist/diff/render.js +90 -0
  55. package/dist/model/payload.js +84 -0
  56. package/dist/model/tree.js +7 -0
  57. package/dist/model/volatile.js +54 -0
  58. package/dist/pipefy/agents.js +122 -0
  59. package/dist/pipefy/automations.js +155 -0
  60. package/dist/pipefy/capability.js +84 -0
  61. package/dist/pipefy/client.js +204 -0
  62. package/dist/pipefy/discovery.js +148 -0
  63. package/dist/pipefy/internal.js +315 -0
  64. package/dist/pipefy/ipaas.js +269 -0
  65. package/dist/pipefy/reconstruct.js +674 -0
  66. package/dist/pipefy/reference.js +172 -0
  67. package/dist/pipefy/snapshot.js +185 -0
  68. package/dist/pipefy/toolkit.js +158 -0
  69. package/dist/report/integrations.js +165 -0
  70. package/dist/report/run.js +355 -0
  71. package/dist/util/fsx.js +50 -0
  72. package/dist/util/git.js +25 -0
  73. package/dist/util/json.js +56 -0
  74. package/dist/util/log.js +69 -0
  75. package/dist/util/pool.js +42 -0
  76. package/dist/util/slug.js +23 -0
  77. package/dist/util/tui.js +287 -0
  78. package/dist/validate/index.js +86 -0
  79. package/dist/validate/integrity.js +370 -0
  80. package/dist/validate/reference.js +120 -0
  81. package/dist/validate/schema.js +278 -0
  82. package/dist/workspace/agents.js +245 -0
  83. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  84. package/dist/workspace/flows.js +149 -0
  85. package/dist/workspace/layout.js +146 -0
  86. package/dist/workspace/lock.js +83 -0
  87. package/dist/workspace/read.js +168 -0
  88. package/dist/workspace/reference.js +24 -0
  89. package/dist/workspace/stamp.js +234 -0
  90. package/dist/workspace/write.js +194 -0
  91. package/package.json +37 -35
  92. package/src/apply/adopt.ts +0 -150
  93. package/src/apply/agentops.ts +0 -71
  94. package/src/apply/compile.ts +0 -875
  95. package/src/apply/execute.ts +0 -336
  96. package/src/apply/flowops.ts +0 -399
  97. package/src/apply/idmap.ts +0 -241
  98. package/src/apply/mutations.ts +0 -955
  99. package/src/apply/registry.ts +0 -430
  100. package/src/apply/types.ts +0 -134
  101. package/src/cli/args.ts +0 -88
  102. package/src/cli.ts +0 -211
  103. package/src/codec/flow.ts +0 -199
  104. package/src/codec/pack.ts +0 -103
  105. package/src/codec/roundtrip.ts +0 -94
  106. package/src/codec/unpack.ts +0 -198
  107. package/src/commands/agents.ts +0 -184
  108. package/src/commands/apply.ts +0 -1144
  109. package/src/commands/context.ts +0 -119
  110. package/src/commands/create.ts +0 -79
  111. package/src/commands/diff.ts +0 -314
  112. package/src/commands/flows.ts +0 -414
  113. package/src/commands/misc.ts +0 -644
  114. package/src/commands/plan.ts +0 -331
  115. package/src/commands/pull.ts +0 -567
  116. package/src/commands/runs.ts +0 -83
  117. package/src/commands/skills.ts +0 -137
  118. package/src/commands/verify.ts +0 -253
  119. package/src/config.ts +0 -168
  120. package/src/diff/agents.ts +0 -122
  121. package/src/diff/diff.ts +0 -1130
  122. package/src/diff/flow.ts +0 -318
  123. package/src/diff/render.ts +0 -101
  124. package/src/model/payload.ts +0 -154
  125. package/src/model/tree.ts +0 -99
  126. package/src/model/volatile.ts +0 -55
  127. package/src/pipefy/agents.ts +0 -165
  128. package/src/pipefy/automations.ts +0 -219
  129. package/src/pipefy/capability.ts +0 -119
  130. package/src/pipefy/client.ts +0 -267
  131. package/src/pipefy/discovery.ts +0 -209
  132. package/src/pipefy/internal.ts +0 -380
  133. package/src/pipefy/ipaas.ts +0 -365
  134. package/src/pipefy/reconstruct.ts +0 -775
  135. package/src/pipefy/reference.ts +0 -251
  136. package/src/pipefy/snapshot.ts +0 -245
  137. package/src/pipefy/toolkit.ts +0 -200
  138. package/src/report/integrations.ts +0 -231
  139. package/src/report/run.ts +0 -475
  140. package/src/util/fsx.ts +0 -45
  141. package/src/util/git.ts +0 -32
  142. package/src/util/json.ts +0 -55
  143. package/src/util/log.ts +0 -76
  144. package/src/util/pool.ts +0 -48
  145. package/src/util/slug.ts +0 -26
  146. package/src/util/tui.ts +0 -335
  147. package/src/validate/index.ts +0 -123
  148. package/src/validate/integrity.ts +0 -387
  149. package/src/validate/reference.ts +0 -136
  150. package/src/validate/schema.ts +0 -328
  151. package/src/workspace/agents.ts +0 -290
  152. package/src/workspace/flows.ts +0 -191
  153. package/src/workspace/layout.ts +0 -165
  154. package/src/workspace/lock.ts +0 -148
  155. package/src/workspace/read.ts +0 -165
  156. package/src/workspace/reference.ts +0 -24
  157. package/src/workspace/stamp.ts +0 -301
  158. package/src/workspace/write.ts +0 -225
  159. /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
@@ -0,0 +1,348 @@
1
+ export const REGISTRY = {
2
+ 'repo.update': {
3
+ status: 'PARTIAL',
4
+ via: 'updatePipe',
5
+ transport: 'graphql',
6
+ reason: 'covers name, noun, icon, color, title_field_id, expiration_*, public and preferences; other repo columns have no input',
7
+ /**
8
+ * `UpdatePipeInput` has fifteen fields and the repos row has thirty-six, so
9
+ * most of what the payload shows about a pipe cannot be written back.
10
+ *
11
+ * `description` is the one that matters in practice: there is no input for
12
+ * it, so a workspace that sets it compiled a mutation that succeeded, changed
13
+ * nothing, and left the file permanently disagreeing with the pipe. Exactly
14
+ * the `start_form_title` failure, one entity up. Introspection filtering
15
+ * dropped the argument silently; naming the settable columns refuses it out
16
+ * loud instead.
17
+ */
18
+ settableColumns: [
19
+ 'name',
20
+ 'noun',
21
+ 'icon',
22
+ 'color',
23
+ 'public',
24
+ 'title_field_id',
25
+ 'expiration_time',
26
+ 'only_admin_can_remove_cards',
27
+ 'only_assignees_can_edit_cards',
28
+ ],
29
+ },
30
+ 'phases.create': { status: 'SUPPORTED', via: 'createPhase', transport: 'graphql' },
31
+ 'phases.update': { status: 'SUPPORTED', via: 'updatePhase + phaseSettings', transport: 'graphql' },
32
+ 'phases.delete': {
33
+ status: 'PARTIAL',
34
+ via: 'deletePhase',
35
+ transport: 'graphql',
36
+ reason: 'cards in the phase must be moved first. The Change Migrator recipes create an "Archive(Migration)" phase and move cards into it; pipe refuses unless --allow-card-moves is given',
37
+ },
38
+ 'phases.reorder': {
39
+ status: 'UNSUPPORTED',
40
+ reason: 'createPhase takes index: Float, updatePhase does not — there is no way to change an existing phase index',
41
+ ask: 4,
42
+ },
43
+ 'fields.create': { status: 'SUPPORTED', via: 'createPhaseField', transport: 'graphql' },
44
+ 'fields.update': {
45
+ status: 'SUPPORTED',
46
+ via: 'updatePhaseField',
47
+ transport: 'graphql',
48
+ immutableFields: ['type_id', 'phase_id', 'slug'],
49
+ reason: 'label is NON_NULL, so every update resends the full object rather than a sparse patch',
50
+ },
51
+ 'fields.delete': {
52
+ status: 'SUPPORTED',
53
+ via: 'deletePhaseField',
54
+ transport: 'graphql',
55
+ reason: 'requires pipeUuid alongside the field id, and destroys every existing value',
56
+ },
57
+ 'fields.archive': {
58
+ status: 'SUPPORTED',
59
+ via: 'archiveField / unarchiveField',
60
+ transport: 'graphql',
61
+ reason: "keyed on the field's uuid, not its id",
62
+ },
63
+ 'fields.reorder': { status: 'SUPPORTED', via: 'updatePhaseField (index: Float)', transport: 'graphql' },
64
+ 'fields.change_type': {
65
+ status: 'UNSUPPORTED',
66
+ reason: 'updatePhaseField has no type_id input. Changing a field type requires delete + create, which destroys every existing value — so pipe refuses instead of doing it silently',
67
+ ask: 2,
68
+ },
69
+ 'fields.move_phase': {
70
+ status: 'UNSUPPORTED',
71
+ reason: 'updatePhaseField has no phase_id input; moving a field between phases would destroy its data',
72
+ ask: 3,
73
+ },
74
+ 'labels.create': { status: 'SUPPORTED', via: 'createLabel', transport: 'graphql' },
75
+ 'labels.update': { status: 'SUPPORTED', via: 'updateLabel', transport: 'graphql' },
76
+ 'labels.delete': { status: 'SUPPORTED', via: 'deleteLabel', transport: 'graphql' },
77
+ /**
78
+ * All three are on the PUBLIC endpoint, and were being sent to the internal
79
+ * one — inherited from the Change Migrator recipes and never re-checked.
80
+ * `createAutomation`, `updateAutomation` and `deleteAutomation` are documented
81
+ * public mutations with introspectable inputs; the internal endpoint has no
82
+ * compatibility contract and answered a bad parameter with the single word
83
+ * "is invalid".
84
+ */
85
+ 'automations.create': {
86
+ status: 'SUPPORTED',
87
+ via: 'createAutomation',
88
+ transport: 'graphql',
89
+ reason: 'conditions, field_maps, distribute strategy and responseSchema all go in nested. Event and action parameters are checked against automationEvents/automationActions before sending, because both endpoints answer a wrong one with only "is invalid"',
90
+ },
91
+ 'automations.update': {
92
+ status: 'SUPPORTED',
93
+ via: 'updateAutomation',
94
+ transport: 'graphql',
95
+ reason: 'updateAutomation in place, keeping the id, uuid and run history. The recipes delete and recreate instead, on the grounds that action_params does not round-trip; measured against the live API and it does, across every field map dimension, action_id and event_id. UpdateAutomationInput accepts every field CreateAutomationInput does. --automation-recreate restores delete+create for the case where an update is refused',
96
+ },
97
+ 'automations.delete': { status: 'SUPPORTED', via: 'deleteAutomation', transport: 'graphql' },
98
+ /**
99
+ * Start form only. The mutation authorizes `phaseId` and then ignores it,
100
+ * placing every condition on the start form; the UI can target any phase, so
101
+ * there is no equivalent public write path for the rest.
102
+ */
103
+ 'field_conditions.create': {
104
+ status: 'PARTIAL',
105
+ via: 'createFieldCondition',
106
+ transport: 'graphql',
107
+ reason: 'input takes phaseId (camel) while update takes phase_id (snake) — recipe steps 85/109. ' +
108
+ 'And phaseId appears to be ignored: a create with phaseId set to a regular phase put the ' +
109
+ 'condition on the start form instead. Verification catches it; there is no known way to ' +
110
+ 'target a non-start-form phase on create',
111
+ },
112
+ 'field_conditions.update': { status: 'SUPPORTED', via: 'updateFieldCondition', transport: 'graphql' },
113
+ 'field_conditions.delete': { status: 'SUPPORTED', via: 'deleteFieldCondition', transport: 'graphql' },
114
+ 'field_conditions.reorder': { status: 'SUPPORTED', via: 'setFieldConditionOrder', transport: 'graphql' },
115
+ 'webhooks.create': { status: 'SUPPORTED', via: 'createWebhook', transport: 'graphql' },
116
+ 'webhooks.update': { status: 'SUPPORTED', via: 'updateWebhook', transport: 'graphql' },
117
+ 'webhooks.delete': { status: 'SUPPORTED', via: 'deleteWebhook', transport: 'graphql' },
118
+ 'pipe_relations.create': { status: 'SUPPORTED', via: 'createPipeRelation', transport: 'graphql' },
119
+ 'pipe_relations.update': { status: 'SUPPORTED', via: 'updatePipeRelation', transport: 'graphql' },
120
+ 'pipe_relations.delete': { status: 'SUPPORTED', via: 'deletePipeRelation', transport: 'graphql' },
121
+ 'phase_jumps.update': {
122
+ status: 'PARTIAL',
123
+ via: 'PUT /internal_api/settings/phases/:id',
124
+ transport: 'internal',
125
+ reason: 'no public mutation exists. Unversioned internal endpoint with no compatibility contract — isolated in one adapter, asserted on the echoed next_phase_ids, degrades to UNSUPPORTED rather than throwing',
126
+ ask: 1,
127
+ },
128
+ 'phase_team_memberships.update': { status: 'SUPPORTED', via: 'phaseSettings (teamMemberIds)', transport: 'graphql' },
129
+ 'repo_preferences.update': {
130
+ status: 'PARTIAL',
131
+ via: 'updateRepoPreferences (repoUuid) + updatePipe.preferences',
132
+ transport: 'graphql',
133
+ settableColumns: [
134
+ 'hidden_start_form_attributes',
135
+ 'hidden_top_buttons',
136
+ 'findable',
137
+ 'inbox_email_enabled',
138
+ 'main_tab_views',
139
+ ],
140
+ reason: 'the surface is small and split. updateRepoPreferences accepts only hiddenStartFormAttributes ' +
141
+ 'and hiddenTopButtons; updatePipe.preferences accepts findable, inboxEmailEnabled and ' +
142
+ 'mainTabViews. Every other column in the payload row — start_form_title, custom_sorting_*, ' +
143
+ 'the AI toggles, guests_can_follow_tickets, default_view — has no input on this endpoint',
144
+ },
145
+ 'public_forms.update': {
146
+ status: 'PARTIAL',
147
+ via: 'updatePipe.publicFormSettings',
148
+ transport: 'graphql',
149
+ settableColumns: [
150
+ 'title',
151
+ 'description',
152
+ 'submit_button_text',
153
+ 'after_submit_message',
154
+ 'brand_color',
155
+ 'background_color',
156
+ 'background_image',
157
+ 'logo',
158
+ 'display_pipefy_logo',
159
+ 'show_submit_another_response_button',
160
+ 'submitter_email_collection_enabled',
161
+ 'submitter_email_required',
162
+ 'submitter_email_field_id',
163
+ 'reuse_last_submission_response',
164
+ 'active',
165
+ ],
166
+ reason: 'PublicFormSettingsInput covers title, description, submitButtonText, afterSubmitMessage, ' +
167
+ 'brandColor, backgroundColor/Image, logo, displayPipefyLogo, showSubmitAnotherResponseButton, ' +
168
+ 'submitterEmail* and reuseLastSubmissionResponse. The html and css columns have no input',
169
+ },
170
+ 'visibilities.update': {
171
+ status: 'UNSUPPORTED',
172
+ reason: 'only reachable through updatePipe.public_form, and slug is not settable',
173
+ },
174
+ 'email_templates.create': { status: 'UNSUPPORTED', reason: 'EmailTemplate is read-only; no Input types exist', ask: 5 },
175
+ 'email_templates.update': { status: 'UNSUPPORTED', reason: 'EmailTemplate is read-only; no Input types exist', ask: 5 },
176
+ 'email_templates.delete': { status: 'UNSUPPORTED', reason: 'EmailTemplate is read-only; no Input types exist', ask: 5 },
177
+ 'email_inboxes.create': { status: 'UNSUPPORTED', reason: 'no mutation found' },
178
+ 'email_inboxes.update': { status: 'UNSUPPORTED', reason: 'no mutation found' },
179
+ 'email_inboxes.delete': { status: 'UNSUPPORTED', reason: 'no mutation found' },
180
+ };
181
+ /**
182
+ * What can be written on a **database**, which is a different question from the
183
+ * same entity on a pipe.
184
+ *
185
+ * A database's payload is a pipe's — its columns are `fields`, its record
186
+ * statuses are `phases` — so the diff produces the same entity names for both
187
+ * kinds. The *mutations* are not the same: `createTableField` rather than
188
+ * `createPhaseField`, `updateTable` rather than `updatePipe`, and a whole class
189
+ * of pipe entities that a database simply does not have.
190
+ *
191
+ * So this table is authoritative for a database, and anything absent from it is
192
+ * refused. Falling through to the pipe registry would be worse than useless: it
193
+ * would report `automations.create` as SUPPORTED for a repo with no workflow, and
194
+ * then send `createAutomation` at it.
195
+ *
196
+ * Identifier facts behind these rows are in VALIDATION.md §3g, all measured.
197
+ */
198
+ const TABLE_REGISTRY = {
199
+ 'repo.update': {
200
+ status: 'PARTIAL',
201
+ via: 'updateTable',
202
+ transport: 'graphql',
203
+ settableColumns: [
204
+ 'name',
205
+ 'noun',
206
+ 'icon',
207
+ 'color',
208
+ 'public',
209
+ 'description',
210
+ 'authorization',
211
+ 'create_card_label',
212
+ 'title_field_id',
213
+ ],
214
+ reason: 'updateTable covers those nine. labels and members can only be set when the database is created — UpdateTableInput has no input for either — and statuses, uuid, url and the record count have none at all. title_field_id takes the field slug or uuid, never the numeric id',
215
+ },
216
+ 'fields.create': {
217
+ status: 'SUPPORTED',
218
+ via: 'createTableField',
219
+ transport: 'graphql',
220
+ reason: 'CreateTableFieldInput has no index, so a new column lands at the end and its position is a separate setTableFieldOrder call',
221
+ },
222
+ 'fields.update': {
223
+ status: 'SUPPORTED',
224
+ via: 'updateTableField',
225
+ transport: 'graphql',
226
+ immutableFields: ['type_id', 'index', 'slug', 'uuid', 'phase_id', 'is_multiple'],
227
+ settableColumns: [
228
+ 'label',
229
+ 'options',
230
+ 'description',
231
+ 'help',
232
+ 'required',
233
+ 'unique',
234
+ 'minimal_view',
235
+ 'custom_validation',
236
+ 'can_search_connected_cards',
237
+ 'can_connect_multiple_cards',
238
+ 'can_create_connected_cards',
239
+ 'child_must_exist_to_finish_parent',
240
+ 'all_children_must_be_done_to_finish_parent',
241
+ ],
242
+ reason: 'addressed by the field slug, with table_id alongside it. type and index have no input on UpdateTableFieldInput',
243
+ },
244
+ 'fields.delete': {
245
+ status: 'SUPPORTED',
246
+ via: 'deleteTableField',
247
+ transport: 'graphql',
248
+ reason: 'addressed by the field slug. Deleting a column destroys its value in every record of the database',
249
+ },
250
+ 'fields.reorder': {
251
+ status: 'SUPPORTED',
252
+ via: 'setTableFieldOrder',
253
+ transport: 'graphql',
254
+ reason: 'set-replacing per database: the whole ordered list of field slugs is sent. It returns the applied order, so the step asserts its own effect rather than trusting a 200',
255
+ },
256
+ 'fields.change_type': {
257
+ status: 'UNSUPPORTED',
258
+ ask: 2,
259
+ reason: 'UpdateTableFieldInput has no type. Changing it would mean delete + create, which destroys that column in every record — the same wall as a pipe field, one repo kind over',
260
+ },
261
+ 'fields.archive': {
262
+ status: 'UNSUPPORTED',
263
+ reason: 'there is no archiveTableField. A database column can only be deleted, and that destroys data — so there is no non-destructive way to retire one',
264
+ },
265
+ 'fields.move_phase': {
266
+ status: 'UNSUPPORTED',
267
+ reason: 'a database has one form. There is nowhere to move a column to',
268
+ },
269
+ /**
270
+ * Labels work on a database exactly as they do on a pipe — measured as full
271
+ * CRUD. The catch is only in the create: `CreateLabelInput` has both `pipe_id`
272
+ * and `table_id`, and a database id sent as `pipe_id` is refused with `Pipe not
273
+ * found with id: <the database id>`, which reads like a missing capability and
274
+ * is not one.
275
+ */
276
+ 'labels.create': {
277
+ status: 'SUPPORTED',
278
+ via: 'createLabel',
279
+ transport: 'graphql',
280
+ reason: 'CreateLabelInput.table_id, not pipe_id — the latter is refused with "Pipe not found"',
281
+ },
282
+ 'labels.update': { status: 'SUPPORTED', via: 'updateLabel', transport: 'graphql' },
283
+ 'labels.delete': { status: 'SUPPORTED', via: 'deleteLabel', transport: 'graphql' },
284
+ /**
285
+ * Webhooks are refused on a database, and this one is a judgement rather than a
286
+ * missing path — so the measurement is recorded to stop someone "fixing" it.
287
+ *
288
+ * `createWebhook` with a database id **is accepted**, and the webhook really
289
+ * attaches: it reads back under `Table.webhooks`. But Pipefy does not offer
290
+ * webhooks on a database, no database in this org has one, and the actions on
291
+ * offer are card-shaped (`card.create` and friends) while a database holds
292
+ * records. A write path that is accepted and inert is worse than one that is
293
+ * refused, so it is refused until somebody needs it and can say what it should
294
+ * fire on.
295
+ */
296
+ 'webhooks.create': {
297
+ status: 'UNSUPPORTED',
298
+ reason: 'createWebhook accepts a database id and the webhook does attach — measured — but Pipefy does not offer webhooks on a database and the available actions are card-shaped, so one would sit there doing nothing. Refused rather than left as a trap',
299
+ },
300
+ 'webhooks.update': { status: 'UNSUPPORTED', reason: 'see webhooks.create — not offered on a database' },
301
+ 'webhooks.delete': { status: 'UNSUPPORTED', reason: 'see webhooks.create — not offered on a database' },
302
+ };
303
+ /** Entities a database does not have, refused with the reason rather than a shrug. */
304
+ const NO_WORKFLOW = new Set([
305
+ 'phases',
306
+ 'phase_jumps',
307
+ 'phase_team_memberships',
308
+ 'automations',
309
+ 'conditions',
310
+ 'condition_expressions',
311
+ 'field_maps',
312
+ 'distribute_assignments',
313
+ 'response_schemas',
314
+ 'field_conditions',
315
+ 'field_condition_actions',
316
+ ]);
317
+ export const support = (entity, op, kind = 'pipe') => {
318
+ if (kind === 'table') {
319
+ const hit = TABLE_REGISTRY[`${entity}.${op}`];
320
+ if (hit)
321
+ return hit;
322
+ if (entity !== 'repo' && NO_WORKFLOW.has(entity)) {
323
+ return {
324
+ status: 'UNSUPPORTED',
325
+ reason: `a database has no workflow, so ${entity} cannot be changed on one. Its phases are record statuses ` +
326
+ 'and its only form holds the columns. If this change is meant to automate something, it belongs on a pipe',
327
+ };
328
+ }
329
+ return {
330
+ status: 'UNSUPPORTED',
331
+ reason: `${entity}.${op} is not proven on a database. The mutation may exist and accept a table id, but it has ` +
332
+ 'not been measured, and this tool refuses rather than finding out on your data',
333
+ };
334
+ }
335
+ const hit = REGISTRY[`${entity}.${op}`];
336
+ if (hit)
337
+ return hit;
338
+ return {
339
+ status: 'UNSUPPORTED',
340
+ reason: `no write path is registered for ${entity}.${op} — if one exists, add it to src/apply/registry.ts`,
341
+ };
342
+ };
343
+ export const registryRows = () => Object.entries(REGISTRY).map(([key, v]) => {
344
+ const [entity, op] = key.split('.');
345
+ return { key, entity, op, ...v };
346
+ });
347
+ /** Distinct asks referenced by the registry — the evidence-backed ask list. */
348
+ export const registryAsks = () => [...new Set(registryRows().map((r) => r.ask).filter((a) => typeof a === 'number'))].sort((a, b) => a - b);
@@ -0,0 +1,18 @@
1
+ export const stepSummary = (plan) => {
2
+ const byStatus = plan.steps.reduce((acc, s) => {
3
+ acc[s.status]++;
4
+ return acc;
5
+ }, { pending: 0, running: 0, done: 0, failed: 0, skipped: 0 });
6
+ return byStatus;
7
+ };
8
+ export const changeByKey = (cs, key) => cs.changes.find((c) => c.key === key);
9
+ export const supportToClassification = (c, s, op) => ({
10
+ changeKey: c.key,
11
+ entity: c.entity,
12
+ op,
13
+ status: s.status,
14
+ via: s.via,
15
+ reason: s.reason,
16
+ ask: s.ask,
17
+ label: c.label,
18
+ });
@@ -0,0 +1,79 @@
1
+ import { UserError } from "../util/log.js";
2
+ export const parse = (argv) => {
3
+ const [command = 'help', ...rest] = argv;
4
+ const positionals = [];
5
+ const flags = {};
6
+ for (let i = 0; i < rest.length; i++) {
7
+ const arg = rest[i];
8
+ if (arg === '--') {
9
+ positionals.push(...rest.slice(i + 1));
10
+ break;
11
+ }
12
+ if (arg.startsWith('--')) {
13
+ const [rawName, inlineValue] = arg.slice(2).split('=', 2);
14
+ const name = rawName;
15
+ if (inlineValue !== undefined) {
16
+ addFlag(flags, name, inlineValue);
17
+ continue;
18
+ }
19
+ const next = rest[i + 1];
20
+ if (next !== undefined && !next.startsWith('-')) {
21
+ addFlag(flags, name, next);
22
+ i++;
23
+ }
24
+ else {
25
+ flags[name] = true;
26
+ }
27
+ continue;
28
+ }
29
+ if (arg.startsWith('-') && arg.length > 1) {
30
+ for (const ch of arg.slice(1))
31
+ flags[ch] = true;
32
+ continue;
33
+ }
34
+ positionals.push(arg);
35
+ }
36
+ return { command, positionals, flags };
37
+ };
38
+ const addFlag = (flags, name, value) => {
39
+ const existing = flags[name];
40
+ if (existing === undefined) {
41
+ flags[name] = value;
42
+ return;
43
+ }
44
+ if (Array.isArray(existing))
45
+ existing.push(value);
46
+ else
47
+ flags[name] = [String(existing), value];
48
+ };
49
+ export const str = (p, name, fallback) => {
50
+ const v = p.flags[name];
51
+ if (v === undefined || v === true)
52
+ return fallback;
53
+ if (Array.isArray(v))
54
+ return v[v.length - 1];
55
+ return String(v);
56
+ };
57
+ export const bool = (p, name, fallback = false) => {
58
+ const v = p.flags[name];
59
+ if (v === undefined)
60
+ return fallback;
61
+ if (v === 'false' || v === '0' || v === 'no')
62
+ return false;
63
+ return Boolean(v);
64
+ };
65
+ export const int = (p, name, fallback) => {
66
+ const v = str(p, name);
67
+ if (v === undefined)
68
+ return fallback;
69
+ const n = Number(v);
70
+ if (!Number.isInteger(n))
71
+ throw new UserError(`--${name} must be an integer, got "${v}"`);
72
+ return n;
73
+ };
74
+ export const requirePositional = (p, index, what) => {
75
+ const v = p.positionals[index];
76
+ if (!v)
77
+ throw new UserError(`missing ${what}`, `see \`pipe help ${p.command}\``);
78
+ return v;
79
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import { parse, bool } from "./cli/args.js";
3
+ import { UserError, bold, dim, error as logError, info, out, setVerbose, yellow } from "./util/log.js";
4
+ const commands = {
5
+ login: {
6
+ summary: 'store a Pipefy API token',
7
+ help: async () => (await import("./commands/misc.js")).loginHelp,
8
+ run: async (p) => (await import("./commands/misc.js")).login(p),
9
+ },
10
+ doctor: {
11
+ summary: 'report what this endpoint can actually do',
12
+ help: async () => (await import("./commands/misc.js")).doctorHelp,
13
+ run: async (p) => (await import("./commands/misc.js")).doctor(p),
14
+ },
15
+ skills: {
16
+ summary: 'the Claude Code skills this install ships with; install them for every project',
17
+ help: async () => (await import("./commands/skills.js")).help,
18
+ run: async (p) => (await import("./commands/skills.js")).run(p),
19
+ },
20
+ create: {
21
+ summary: 'create an empty pipe and pull it, ready to be built from files',
22
+ help: async () => (await import("./commands/create.js")).help,
23
+ run: async (p) => (await import("./commands/create.js")).run(p),
24
+ },
25
+ pull: {
26
+ summary: 'pull a pipe and everything it connects to into a workspace',
27
+ help: async () => (await import("./commands/pull.js")).help,
28
+ run: async (p) => (await import("./commands/pull.js")).run(p),
29
+ },
30
+ export: {
31
+ summary: 'export one repo as a single snapshot-shaped payload',
32
+ help: async () => (await import("./commands/misc.js")).exportHelp,
33
+ run: async (p) => (await import("./commands/misc.js")).exportCmd(p),
34
+ },
35
+ validate: {
36
+ summary: 'check JSON shape and referential integrity, offline',
37
+ help: async () => (await import("./commands/misc.js")).validateHelp,
38
+ run: async (p) => (await import("./commands/misc.js")).validate(p),
39
+ },
40
+ diff: {
41
+ summary: 'what changed — against the baseline, live, or another pipe',
42
+ help: async () => (await import("./commands/diff.js")).help,
43
+ run: async (p) => (await import("./commands/diff.js")).run(p),
44
+ },
45
+ plan: {
46
+ summary: 'classify every change and order the applicable ones',
47
+ help: async () => (await import("./commands/plan.js")).help,
48
+ run: async (p) => (await import("./commands/plan.js")).run(p),
49
+ },
50
+ apply: {
51
+ summary: 'snapshot, apply step by step, verify',
52
+ help: async () => (await import("./commands/apply.js")).help,
53
+ run: async (p) => (await import("./commands/apply.js")).run(p),
54
+ },
55
+ verify: {
56
+ summary: 'read the pipe back and diff it against the workspace',
57
+ help: async () => (await import("./commands/verify.js")).help,
58
+ run: async (p) => (await import("./commands/verify.js")).run(p),
59
+ },
60
+ rollback: {
61
+ summary: 'restore a snapshot',
62
+ help: async () => (await import("./commands/misc.js")).rollbackHelp,
63
+ run: async (p) => (await import("./commands/misc.js")).rollback(p),
64
+ },
65
+ runs: {
66
+ summary: 'every run recorded in this workspace, and what each one did',
67
+ help: async () => (await import("./commands/runs.js")).help,
68
+ run: async (p) => (await import("./commands/runs.js")).run(p),
69
+ },
70
+ status: {
71
+ summary: 'what is in this workspace and how it was read',
72
+ help: async () => (await import("./commands/misc.js")).statusHelp,
73
+ run: async (p) => (await import("./commands/misc.js")).status(p),
74
+ },
75
+ capabilities: {
76
+ summary: 'the write-coverage registry',
77
+ help: async () => (await import("./commands/misc.js")).capsHelp,
78
+ run: async (p) => (await import("./commands/misc.js")).capabilities(p),
79
+ },
80
+ roundtrip: {
81
+ summary: 'codec self-check: pack(unpack(P)) must equal P',
82
+ help: async () => (await import("./commands/misc.js")).roundtripHelp,
83
+ run: async (p) => (await import("./commands/misc.js")).roundtrip(p),
84
+ },
85
+ viewer: {
86
+ summary: 'build the HTML diff view and open it',
87
+ help: async () => (await import("./commands/misc.js")).viewerHelp,
88
+ run: async (p) => (await import("./commands/misc.js")).viewer(p),
89
+ },
90
+ };
91
+ const usage = () => {
92
+ const width = Math.max(...Object.keys(commands).map((k) => k.length)) + 2;
93
+ out(bold('pipe') + ' — Pipefy process coder');
94
+ out('');
95
+ out(' Pull a pipe to disk as readable JSON, edit it with Claude, diff it, push it back.');
96
+ out('');
97
+ out(bold('usage'));
98
+ out(' pipe <command> [options]');
99
+ out('');
100
+ out(bold('commands'));
101
+ for (const [name, cmd] of Object.entries(commands)) {
102
+ out(` ${name.padEnd(width)} ${dim(cmd.summary)}`);
103
+ }
104
+ out('');
105
+ out(bold('the loop'));
106
+ out(` pipe pull <pipeId> --out ./my-pipe ${dim('# snapshot, or GraphQL if there are none')}`);
107
+ out(` pipe validate ${dim('# offline, instant')}`);
108
+ out(` pipe diff --open ${dim('# see it')}`);
109
+ out(` pipe plan --dry-run ${dim('# refuses what cannot be applied')}`);
110
+ out(` pipe apply ${dim('# snapshot, apply, verify')}`);
111
+ out('');
112
+ out(bold('global options'));
113
+ out(` --verbose ${dim('log every request, retry and dropped argument')}`);
114
+ out(` --help ${dim('help for a command')}`);
115
+ out('');
116
+ out(dim('First run: pipe login --token <token>, then pipe doctor.'));
117
+ };
118
+ const main = async () => {
119
+ const argv = process.argv.slice(2);
120
+ const parsed = parse(argv);
121
+ setVerbose(bool(parsed, 'verbose') || bool(parsed, 'v'));
122
+ /**
123
+ * `--version` before anything else. It is the first thing anyone types to check
124
+ * an install worked, and answering it with `unknown command "--version"` makes a
125
+ * working install look broken.
126
+ *
127
+ * The path is included because `npm link` puts a symlink on PATH: when two
128
+ * clones exist, or one was moved, "which one am I running" is the actual
129
+ * question behind the version check.
130
+ */
131
+ if (parsed.command === '--version' || parsed.command === '-V' || parsed.command === 'version') {
132
+ const { readFile } = await import('node:fs/promises');
133
+ const { fileURLToPath } = await import('node:url');
134
+ const { dirname, join } = await import('node:path');
135
+ const here = dirname(fileURLToPath(import.meta.url));
136
+ const pkg = JSON.parse(await readFile(join(here, '..', 'package.json'), 'utf8'));
137
+ out(`${pkg.name} ${pkg.version}`);
138
+ out(dim(` node ${process.version} (needs >=22.18 for native type stripping)`));
139
+ out(dim(` running ${fileURLToPath(import.meta.url)}`));
140
+ return 0;
141
+ }
142
+ if (parsed.command === 'help' || parsed.command === '--help' || parsed.command === '-h' || !argv.length) {
143
+ const topic = parsed.positionals[0];
144
+ if (topic && commands[topic]) {
145
+ out(await commands[topic].help());
146
+ return 0;
147
+ }
148
+ usage();
149
+ return 0;
150
+ }
151
+ const cmd = commands[parsed.command];
152
+ if (!cmd) {
153
+ logError(`unknown command "${parsed.command}"`);
154
+ const near = Object.keys(commands).filter((k) => k.startsWith(parsed.command[0] ?? ''));
155
+ if (near.length)
156
+ info(dim(`did you mean: ${near.join(', ')}?`));
157
+ info(dim('run `pipe help` for the list'));
158
+ return 64;
159
+ }
160
+ if (bool(parsed, 'help') || bool(parsed, 'h')) {
161
+ out(await cmd.help());
162
+ return 0;
163
+ }
164
+ const code = await cmd.run(parsed);
165
+ return typeof code === 'number' ? code : 0;
166
+ };
167
+ main()
168
+ .then((code) => {
169
+ process.exitCode = code;
170
+ })
171
+ .catch((e) => {
172
+ if (e instanceof UserError) {
173
+ logError(e.message);
174
+ if (e.hint)
175
+ info(`${yellow('hint')} ${e.hint}`);
176
+ process.exitCode = 65;
177
+ return;
178
+ }
179
+ logError(e.message);
180
+ if (process.env.PPC_DEBUG || bool(parse(process.argv.slice(2)), 'verbose')) {
181
+ info(dim(String(e.stack)));
182
+ }
183
+ else {
184
+ info(dim('run again with --verbose for a stack trace'));
185
+ }
186
+ process.exitCode = 70;
187
+ });