@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,331 @@
1
+ import { resolve } from 'node:path';
2
+ import { loadReference } from '../workspace/reference.ts';
3
+ import { bool, str, type Parsed } from '../cli/args.ts';
4
+ import { readJson, writeJson } from '../util/fsx.ts';
5
+ import { UserError, bold, dim, green, info, ok, out, red, warn, yellow } from '../util/log.ts';
6
+ import { readTree } from '../workspace/read.ts';
7
+ import { planFile, findPlanFile } from '../workspace/layout.ts';
8
+ import { diffTrees } from '../diff/diff.ts';
9
+ import { compile } from '../apply/compile.ts';
10
+ import { readAutomationCatalogue } from '../pipefy/automations.ts';
11
+ import type { Plan, PlanOptions } from '../apply/types.ts';
12
+ import { validateTree, renderFindings, blockingErrors } from '../validate/index.ts';
13
+ import { connect, openWorkspace } from './context.ts';
14
+ import { loadBaseline, writeHtml, openViewer } from './diff.ts';
15
+
16
+ export const help = `pipe plan [workspace] [options]
17
+
18
+ --repo <id> which repo in the workspace (default: the root pipe)
19
+ --dry-run print the plan and do not save it
20
+ --save save the plan to .ppc/plans/ (implied unless --dry-run)
21
+ --automation-recreate delete+create automations instead of updating them in place
22
+ --allow-card-moves permit phase deletes, which require moving cards first
23
+ --force compile a plan even though some changes are unsupported
24
+ --html <file> write the diff + plan as an HTML view
25
+ --open open that view
26
+ --json machine-readable output`;
27
+
28
+ /**
29
+ * `pipe plan` — classify every change against the capability registry, refuse the
30
+ * unsupported ones by name, and order the rest.
31
+ *
32
+ * PLAN.md §5: any UNSUPPORTED change aborts here, with the entity and the
33
+ * reason. That refusal is the product working, not the product failing — the
34
+ * alternative is an apply that reports success having ignored something.
35
+ */
36
+ /**
37
+ * The flow half of a plan.
38
+ *
39
+ * Extracted so it is reachable when the pipe half is clean — a flow-only change
40
+ * is the ordinary case now that flows are in the loop, and the early return for an
41
+ * empty pipe diff was reporting "nothing to do" over real pending work.
42
+ *
43
+ * Returns how much flow work there is, so the caller can decide whether "nothing
44
+ * to do" is still true.
45
+ */
46
+ const planFlowHalf = async (p: Parsed, root: string, dir: string, repoId: number): Promise<number> => {
47
+ if (bool(p, 'json') || bool(p, 'no-flows')) return 0;
48
+ const { readFlows } = await import('../workspace/flows.ts');
49
+ if (!(await readFlows(dir))) return 0;
50
+ try {
51
+ const { diffFlowsAgainstLive, planFlows, renderFlowPlan } = await import('./flows.ts');
52
+ const net = await connect(p);
53
+ const { changeSet: flowChanges, live } = await diffFlowsAgainstLive(net.ipaas, repoId, dir);
54
+ const flowPlan = planFlows(flowChanges, {
55
+ allowArm: bool(p, 'allow-flow-enable'),
56
+ connections: live.connections,
57
+ });
58
+ const rendered = renderFlowPlan(flowPlan);
59
+ if (rendered.trim()) {
60
+ out('');
61
+ out(bold('flows'));
62
+ out(rendered);
63
+ }
64
+ return flowPlan.steps.length + flowPlan.blocked.length;
65
+ } catch (e) {
66
+ info(dim(` flows not planned: ${(e as Error).message}`));
67
+ return 0;
68
+ }
69
+ };
70
+
71
+ /**
72
+ * The agent half of a plan. Offline, like the pipe half — the agent baseline is
73
+ * the file `pipe pull` wrote, not a live read — so unlike `planFlowHalf` this
74
+ * needs no network for the diff itself; only `planAgents` takes a client, and
75
+ * only to resolve nested input type names when one is available.
76
+ */
77
+ const planAgentHalf = async (p: Parsed, root: string, dir: string, repoId: number): Promise<number> => {
78
+ if (bool(p, 'json')) return 0;
79
+ try {
80
+ const { diffAgentsFor, planAgents, renderAgentPlan } = await import('./agents.ts');
81
+ const agentChanges = await diffAgentsFor(root, repoId, dir);
82
+ if (!agentChanges.changes.length) return 0;
83
+ const net = await connect(p).catch(() => null);
84
+ const agentPlan = await planAgents(net?.client ?? null, agentChanges);
85
+ const rendered = renderAgentPlan(agentPlan);
86
+ if (rendered.trim()) {
87
+ out('');
88
+ out(bold('agents'));
89
+ out(rendered);
90
+ }
91
+ return agentPlan.steps.length;
92
+ } catch (e) {
93
+ info(dim(` agents not planned: ${(e as Error).message}`));
94
+ return 0;
95
+ }
96
+ };
97
+
98
+ export const run = async (p: Parsed) => {
99
+ const { root, lock, repo, repoDir: dir } = await openWorkspace(p);
100
+
101
+ const edited = await readTree(dir);
102
+ const baseline = await loadBaseline(root, repo.id);
103
+
104
+ // Validation first: planning an invalid tree wastes a network round trip and
105
+ // produces a worse error message than validate does.
106
+ const { readAgents } = await import('../workspace/agents.ts');
107
+ const findings = validateTree(edited, {
108
+ knownRepoIds: lock.repos.map((r) => r.id),
109
+ baseline,
110
+ reference: await loadReference(root),
111
+ agents: await readAgents(dir),
112
+ });
113
+ const blocking = blockingErrors(findings);
114
+ if (blocking.length) {
115
+ out(renderFindings(findings, `${repo.name ?? repo.id}`));
116
+ throw new UserError(`${blocking.length} validation error${blocking.length === 1 ? '' : 's'} — fix these before planning`);
117
+ }
118
+
119
+ const changeSet = diffTrees(baseline, edited);
120
+ if (!changeSet.changes.length) {
121
+ /**
122
+ * The pipe half is clean, which is not the same as nothing to do: a flow-only
123
+ * change is the ordinary case now that flows are in the loop, and returning
124
+ * here reported "nothing to do" over real pending work.
125
+ */
126
+ const flowWork = await planFlowHalf(p, root, dir, repo.id);
127
+ const agentWork = await planAgentHalf(p, root, dir, repo.id);
128
+ if (!flowWork && !agentWork) ok('nothing to do — the workspace matches the baseline');
129
+ return 0;
130
+ }
131
+
132
+ const options: PlanOptions = {
133
+ automationStrategy: bool(p, 'automation-recreate') ? 'recreate' : 'update',
134
+ allowCardMoves: bool(p, 'allow-card-moves'),
135
+ force: bool(p, 'force'),
136
+ };
137
+
138
+ /**
139
+ * Planning offline is a first-class mode, not a fallback: classification is a
140
+ * pure function of the ChangeSet and the registry, and it is the loop Claude
141
+ * iterates against. A missing token degrades the plan, it does not block it.
142
+ */
143
+ const net = await connect(p).catch((e: unknown) => {
144
+ if (e instanceof UserError) {
145
+ warn('planning offline — ' + e.message.toLowerCase());
146
+ return null;
147
+ }
148
+ throw e;
149
+ });
150
+ const caps = net ? await net.capabilities() : null;
151
+
152
+ const plan = await compile(changeSet, {
153
+ client: net?.client ?? null,
154
+ capabilities: caps,
155
+ repoId: repo.id,
156
+ repoUuid: repo.uuid,
157
+ workspace: root,
158
+ organizationId: lock.organizationId,
159
+ /**
160
+ * The automation catalogue, so a wrong event or action parameter is refused
161
+ * here rather than discovered as "is invalid" mid-apply. Offline planning has
162
+ * no client, and then the check is simply skipped.
163
+ */
164
+ automationCatalogue: net
165
+ ? await readAutomationCatalogue(net.client, repo.id).catch(() => undefined)
166
+ : undefined,
167
+ livePhaseIds: edited.phases.map((ph) => Number(ph['id'])).filter(Number.isFinite),
168
+ repoKind: repo.kind,
169
+ /** A database has one form, so its column order is that form's fields by index. */
170
+ tableFieldOrder:
171
+ repo.kind === 'table'
172
+ ? [...(edited.phases[0]?.fields ?? [])].sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
173
+ : undefined,
174
+ baselineTableFieldOrder:
175
+ repo.kind === 'table'
176
+ ? [...(baseline?.phases[0]?.fields ?? [])]
177
+ .sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
178
+ .map((f) => String(f['slug'] ?? ''))
179
+ : undefined,
180
+ fieldUuidById: Object.fromEntries(
181
+ edited.phases
182
+ .flatMap((ph) => ph.fields)
183
+ .filter((fl) => fl['id'] && fl['uuid'])
184
+ .map((fl) => [String(fl['id']), String(fl['uuid'])]),
185
+ ),
186
+ /** For `updateTable.title_field_id`, which refuses the numeric id. */
187
+ fieldSlugById: Object.fromEntries(
188
+ edited.phases
189
+ .flatMap((ph) => ph.fields)
190
+ .filter((fl) => fl['id'] && fl['slug'])
191
+ .map((fl) => [String(fl['id']), String(fl['slug'])]),
192
+ ),
193
+ startFormPhaseId:
194
+ (edited.phases.find((ph) => Number(ph['index'] ?? -1) === 0)?.['id'] as number | undefined) ?? null,
195
+ conditionOrder: Object.fromEntries(
196
+ [...new Set(edited.field_conditions.map((fc) => String(fc['phase_id'] ?? '')))]
197
+ .filter((phaseId) => phaseId !== '')
198
+ .map((phaseId) => [
199
+ phaseId,
200
+ edited.field_conditions
201
+ .filter((fc) => String(fc['phase_id'] ?? '') === phaseId)
202
+ .slice()
203
+ .sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
204
+ .map((fc) => ({ name: String(fc['name'] ?? ''), id: fc['id'] ? Number(fc['id']) : null })),
205
+ ]),
206
+ ),
207
+ baselineConditionOrder: Object.fromEntries(
208
+ [...new Set((baseline?.field_conditions ?? []).map((fc) => String(fc['phase_id'] ?? '')))]
209
+ .filter((phaseId) => phaseId !== '')
210
+ .map((phaseId) => [
211
+ phaseId,
212
+ (baseline?.field_conditions ?? [])
213
+ .filter((fc) => String(fc['phase_id'] ?? '') === phaseId)
214
+ .slice()
215
+ .sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
216
+ .map((fc) => String(fc['id'])),
217
+ ]),
218
+ ),
219
+ }, options);
220
+
221
+ if (bool(p, 'json')) {
222
+ out(JSON.stringify(plan, null, 2));
223
+ } else {
224
+ out(renderPlan(plan));
225
+ }
226
+
227
+ await planFlowHalf(p, root, dir, repo.id);
228
+ await planAgentHalf(p, root, dir, repo.id);
229
+
230
+ const htmlPath = str(p, 'html') ?? (bool(p, 'open') ? resolve(root, '.ppc', 'plan.html') : undefined);
231
+ if (htmlPath) {
232
+ await writeHtml(htmlPath, changeSet, root, plan);
233
+ info('');
234
+ ok(`HTML view written to ${htmlPath}`);
235
+ if (bool(p, 'open')) await openViewer(htmlPath);
236
+ }
237
+
238
+ const dryRun = bool(p, 'dry-run');
239
+ if (!dryRun) {
240
+ const path = planFile(root, plan.id);
241
+ await writeJson(path, plan);
242
+ info('');
243
+ ok(`plan saved — ${dim(path)}`);
244
+ info(`${yellow('next')} pipe apply ${dim(`--plan ${plan.id}`)}`);
245
+ }
246
+
247
+ if (plan.blocked.length && !options.force) return 2;
248
+ return 0;
249
+ };
250
+
251
+ export const renderPlan = (plan: Plan): string => {
252
+ const lines: string[] = [];
253
+ lines.push(bold(`plan ${plan.id}`));
254
+ lines.push(
255
+ dim(
256
+ `repo ${plan.repoId} · ${plan.mode} · automations: ${plan.options.automationStrategy}` +
257
+ `${plan.options.allowCardMoves ? ' · card moves allowed' : ''}`,
258
+ ),
259
+ );
260
+ lines.push('');
261
+
262
+ if (plan.blocked.length) {
263
+ lines.push(red(`${plan.blocked.length} change${plan.blocked.length === 1 ? '' : 's'} cannot be applied:`));
264
+ for (const b of plan.blocked) {
265
+ lines.push(` ${red('x')} ${bold(`${b.entity}.${b.op}`)} — ${b.label}`);
266
+ lines.push(` ${dim(b.reason ?? 'no write path is registered')}`);
267
+ if (b.ask) lines.push(` ${dim(`would be fixed by platform ask ${b.ask} (ROUTE-B-V2.md §8)`)}`);
268
+ }
269
+ lines.push('');
270
+ lines.push(
271
+ plan.options.force
272
+ ? yellow('--force given: the unsupported changes will be skipped and the rest applied')
273
+ : red('the apply is blocked. Revert these edits, or run with --force to apply only the rest.'),
274
+ );
275
+ lines.push('');
276
+ }
277
+
278
+ const partial = plan.classifications.filter((c) => c.status === 'PARTIAL');
279
+ if (partial.length) {
280
+ lines.push(yellow(`${partial.length} change${partial.length === 1 ? '' : 's'} apply through a partial path:`));
281
+ for (const c of partial) lines.push(` ${yellow('!')} ${c.entity}.${c.op} — ${c.label} ${dim(`(${c.via ?? ''})`)}`);
282
+ lines.push('');
283
+ }
284
+
285
+ if (plan.warnings.length) {
286
+ for (const w of plan.warnings) lines.push(`${yellow('!')} ${w}`);
287
+ lines.push('');
288
+ }
289
+
290
+ lines.push(bold(`${plan.steps.length} step${plan.steps.length === 1 ? '' : 's'}`));
291
+ for (const s of plan.steps) {
292
+ const marks = [
293
+ s.destructive ? red('destructive') : '',
294
+ s.unverified ? yellow('unverified') : '',
295
+ s.kind.startsWith('internal') ? dim('internal-api') : '',
296
+ s.needs.length ? dim(`needs ${s.needs.length} id${s.needs.length === 1 ? '' : 's'}`) : '',
297
+ ]
298
+ .filter(Boolean)
299
+ .join(' ');
300
+ lines.push(` ${dim(s.id)} ${s.description}${marks ? ' ' + marks : ''}`);
301
+ }
302
+
303
+ lines.push('');
304
+ const destructive = plan.steps.filter((s) => s.destructive).length;
305
+ lines.push(
306
+ destructive
307
+ ? red(`${destructive} destructive step${destructive === 1 ? '' : 's'} — these delete configuration or data`)
308
+ : green('no destructive steps'),
309
+ );
310
+ return lines.join('\n');
311
+ };
312
+
313
+ export const loadPlan = async (root: string, planId: string): Promise<Plan> => {
314
+ try {
315
+ // A finished plan carries its outcome in the filename, so resolve rather
316
+ // than assume the bare `<id>.json`.
317
+ const found = await findPlanFile(root, planId);
318
+ return await readJson<Plan>(found ?? planFile(root, planId));
319
+ } catch {
320
+ throw new UserError(`no saved plan "${planId}" in ${root}/.ppc/plans`, 'run `pipe plan` first, or check `pipe status`');
321
+ }
322
+ };
323
+
324
+ export const warnUnverifiedSteps = (plan: Plan) => {
325
+ const unverified = plan.steps.filter((s) => s.unverified);
326
+ if (!unverified.length) return;
327
+ warn(
328
+ `${unverified.length} step${unverified.length === 1 ? '' : 's'} use a call whose shape could not be verified ` +
329
+ `against this schema (phase jumps, phase settings, public form). They report their own failure rather than silently no-op.`,
330
+ );
331
+ };