@pipefy/pipefy-process-coder 0.1.0 → 0.1.2

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 (162) 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 +476 -456
  20. package/LICENSE +202 -0
  21. package/NOTICE +11 -0
  22. package/README.md +830 -808
  23. package/bin/pipe.js +25 -4
  24. package/dist/apply/adopt.js +128 -0
  25. package/dist/apply/agentops.js +42 -0
  26. package/dist/apply/compile.js +722 -0
  27. package/dist/apply/execute.js +277 -0
  28. package/dist/apply/flowops.js +293 -0
  29. package/dist/apply/idmap.js +233 -0
  30. package/dist/apply/mutations.js +704 -0
  31. package/dist/apply/registry.js +348 -0
  32. package/dist/apply/types.js +18 -0
  33. package/dist/cli/args.js +79 -0
  34. package/dist/cli.js +196 -0
  35. package/dist/codec/flow.js +164 -0
  36. package/dist/codec/pack.js +88 -0
  37. package/dist/codec/roundtrip.js +86 -0
  38. package/dist/codec/unpack.js +181 -0
  39. package/dist/commands/agents.js +145 -0
  40. package/dist/commands/apply.js +982 -0
  41. package/dist/commands/context.js +79 -0
  42. package/dist/commands/create.js +63 -0
  43. package/dist/commands/diff.js +294 -0
  44. package/dist/commands/flows.js +348 -0
  45. package/dist/commands/misc.js +562 -0
  46. package/dist/commands/plan.js +301 -0
  47. package/dist/commands/pull.js +549 -0
  48. package/dist/commands/runs.js +74 -0
  49. package/dist/commands/skills.js +131 -0
  50. package/dist/commands/verify.js +205 -0
  51. package/dist/config.js +125 -0
  52. package/dist/diff/agents.js +80 -0
  53. package/dist/diff/diff.js +983 -0
  54. package/dist/diff/flow.js +258 -0
  55. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  56. package/dist/diff/render.js +90 -0
  57. package/dist/model/payload.js +84 -0
  58. package/dist/model/tree.js +7 -0
  59. package/dist/model/volatile.js +54 -0
  60. package/dist/pipefy/agents.js +131 -0
  61. package/dist/pipefy/automations.js +155 -0
  62. package/dist/pipefy/capability.js +84 -0
  63. package/dist/pipefy/client.js +204 -0
  64. package/dist/pipefy/discovery.js +148 -0
  65. package/dist/pipefy/internal.js +406 -0
  66. package/dist/pipefy/ipaas.js +269 -0
  67. package/dist/pipefy/reconstruct.js +676 -0
  68. package/dist/pipefy/reference.js +172 -0
  69. package/dist/pipefy/snapshot.js +185 -0
  70. package/dist/pipefy/toolkit.js +174 -0
  71. package/{src → dist}/pipefy/toolkit_bearer.py +60 -14
  72. package/dist/report/integrations.js +165 -0
  73. package/dist/report/run.js +356 -0
  74. package/dist/util/fsx.js +50 -0
  75. package/dist/util/git.js +25 -0
  76. package/dist/util/json.js +56 -0
  77. package/dist/util/log.js +69 -0
  78. package/dist/util/pool.js +42 -0
  79. package/dist/util/slug.js +23 -0
  80. package/dist/util/tui.js +287 -0
  81. package/dist/validate/index.js +86 -0
  82. package/dist/validate/integrity.js +370 -0
  83. package/dist/validate/reference.js +120 -0
  84. package/dist/validate/schema.js +278 -0
  85. package/dist/version.js +18 -0
  86. package/dist/workspace/agents.js +245 -0
  87. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  88. package/dist/workspace/flows.js +149 -0
  89. package/dist/workspace/layout.js +146 -0
  90. package/dist/workspace/lock.js +83 -0
  91. package/dist/workspace/read.js +168 -0
  92. package/dist/workspace/reference.js +24 -0
  93. package/dist/workspace/stamp.js +234 -0
  94. package/dist/workspace/write.js +194 -0
  95. package/package.json +38 -35
  96. package/src/apply/adopt.ts +0 -150
  97. package/src/apply/agentops.ts +0 -71
  98. package/src/apply/compile.ts +0 -875
  99. package/src/apply/execute.ts +0 -336
  100. package/src/apply/flowops.ts +0 -399
  101. package/src/apply/idmap.ts +0 -241
  102. package/src/apply/mutations.ts +0 -955
  103. package/src/apply/registry.ts +0 -430
  104. package/src/apply/types.ts +0 -134
  105. package/src/cli/args.ts +0 -88
  106. package/src/cli.ts +0 -211
  107. package/src/codec/flow.ts +0 -199
  108. package/src/codec/pack.ts +0 -103
  109. package/src/codec/roundtrip.ts +0 -94
  110. package/src/codec/unpack.ts +0 -198
  111. package/src/commands/agents.ts +0 -184
  112. package/src/commands/apply.ts +0 -1144
  113. package/src/commands/context.ts +0 -119
  114. package/src/commands/create.ts +0 -79
  115. package/src/commands/diff.ts +0 -314
  116. package/src/commands/flows.ts +0 -414
  117. package/src/commands/misc.ts +0 -644
  118. package/src/commands/plan.ts +0 -331
  119. package/src/commands/pull.ts +0 -567
  120. package/src/commands/runs.ts +0 -83
  121. package/src/commands/skills.ts +0 -137
  122. package/src/commands/verify.ts +0 -253
  123. package/src/config.ts +0 -168
  124. package/src/diff/agents.ts +0 -122
  125. package/src/diff/diff.ts +0 -1130
  126. package/src/diff/flow.ts +0 -318
  127. package/src/diff/render.ts +0 -101
  128. package/src/model/payload.ts +0 -154
  129. package/src/model/tree.ts +0 -99
  130. package/src/model/volatile.ts +0 -55
  131. package/src/pipefy/agents.ts +0 -165
  132. package/src/pipefy/automations.ts +0 -219
  133. package/src/pipefy/capability.ts +0 -119
  134. package/src/pipefy/client.ts +0 -267
  135. package/src/pipefy/discovery.ts +0 -209
  136. package/src/pipefy/internal.ts +0 -380
  137. package/src/pipefy/ipaas.ts +0 -365
  138. package/src/pipefy/reconstruct.ts +0 -775
  139. package/src/pipefy/reference.ts +0 -251
  140. package/src/pipefy/snapshot.ts +0 -245
  141. package/src/pipefy/toolkit.ts +0 -200
  142. package/src/report/integrations.ts +0 -231
  143. package/src/report/run.ts +0 -475
  144. package/src/util/fsx.ts +0 -45
  145. package/src/util/git.ts +0 -32
  146. package/src/util/json.ts +0 -55
  147. package/src/util/log.ts +0 -76
  148. package/src/util/pool.ts +0 -48
  149. package/src/util/slug.ts +0 -26
  150. package/src/util/tui.ts +0 -335
  151. package/src/validate/index.ts +0 -123
  152. package/src/validate/integrity.ts +0 -387
  153. package/src/validate/reference.ts +0 -136
  154. package/src/validate/schema.ts +0 -328
  155. package/src/workspace/agents.ts +0 -290
  156. package/src/workspace/flows.ts +0 -191
  157. package/src/workspace/layout.ts +0 -165
  158. package/src/workspace/lock.ts +0 -148
  159. package/src/workspace/read.ts +0 -165
  160. package/src/workspace/reference.ts +0 -24
  161. package/src/workspace/stamp.ts +0 -301
  162. package/src/workspace/write.ts +0 -225
@@ -1,191 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { mkdirp, exists, writeJson, readJson } from '../util/fsx.ts';
4
- import { flowBaselineFile } from './layout.ts';
5
- import { slug } from '../util/slug.ts';
6
- import { canonicalString } from '../util/json.ts';
7
- import { unlinkFlow, linkFlow, type FlowTree } from '../codec/flow.ts';
8
- import type { Row } from '../model/payload.ts';
9
- import type { ConnectionRow, RunRow } from '../pipefy/ipaas.ts';
10
-
11
- /**
12
- * The `flows/` half of a workspace.
13
- *
14
- * A pipe's iPaaS flows are entities of that pipe — the pipe's own webhook URLs
15
- * carry their ids — so they live beside `pipes/`, read by the same pull and
16
- * written by the same apply.
17
- *
18
- * pipes/<id>--<name>/flows/
19
- * _meta.json coverage, projectId, counts, read time
20
- * connections.json externalId, pieceName, displayName, status
21
- * pieces.json pieceName -> version the instance offers
22
- * _readonly/flow-runs.json recent outcomes; never written back
23
- * <flowId>--<slug>/
24
- * flow.json the flow row
25
- * version.json the version row, without the trigger
26
- * nodes/NN--<name>.json one node per file, in execution order
27
- *
28
- * Coverage matters here as much as anywhere: a pipe whose iPaaS could not be
29
- * reached gets `unknown`, which is never read as "this pipe has no flows".
30
- */
31
-
32
- export const FLOWS_DIR = 'flows';
33
-
34
- export type FlowsMeta = {
35
- coverage: 'complete' | 'unknown';
36
- projectId?: string;
37
- readAt: string;
38
- flows?: number;
39
- connections?: number;
40
- pieces?: number;
41
- reason?: string;
42
- notes: string[];
43
- };
44
-
45
- export type FlowsSection = {
46
- meta: FlowsMeta;
47
- /** flowId → the flow, in the ordered form. A new flow has `_new` and no id. */
48
- flows: FlowTree[];
49
- connections: ConnectionRow[];
50
- pieces: Record<string, string>;
51
- runs: RunRow[];
52
- };
53
-
54
- /**
55
- * The flows directory of ONE repo. Under the repo, not at the workspace root:
56
- * a workspace can hold several pipes, each with its own iPaaS workspace, and a
57
- * shared directory meant the second pipe's pull deleted the first pipe's flows.
58
- */
59
- export const flowsRoot = (repoDir: string) => join(repoDir, FLOWS_DIR);
60
-
61
- export const flowDirName = (tree: FlowTree) => {
62
- const id = tree.flow['id'] ? String(tree.flow['id']) : '_new';
63
- return `${id}--${slug(String(tree.version['displayName'] ?? 'flow'))}`;
64
- };
65
-
66
- /** The empty section, for a pipe with no iPaaS or one that could not be read. */
67
- export const unknownFlows = (reason: string): FlowsSection => ({
68
- meta: { coverage: 'unknown', readAt: new Date().toISOString(), reason, notes: [reason] },
69
- flows: [],
70
- connections: [],
71
- pieces: {},
72
- runs: [],
73
- });
74
-
75
- // ── write ───────────────────────────────────────────────────────────────────
76
-
77
- export const writeFlows = async (workspace: string, section: FlowsSection): Promise<string[]> => {
78
- const root = flowsRoot(workspace);
79
- /**
80
- * Rewritten wholesale rather than merged: a flow deleted upstream has to
81
- * disappear from the workspace too, and a leftover directory would be read as a
82
- * flow the author wants created.
83
- */
84
- if (await exists(root)) await fs.rm(root, { recursive: true, force: true });
85
- await mkdirp(join(root, '_readonly'));
86
-
87
- const written: string[] = [];
88
- const put = async (path: string, value: unknown) => {
89
- await writeJson(path, value);
90
- written.push(path);
91
- };
92
-
93
- await put(join(root, '_meta.json'), section.meta);
94
- await put(join(root, 'connections.json'), section.connections);
95
- await put(join(root, 'pieces.json'), section.pieces);
96
- await put(join(root, '_readonly', 'flow-runs.json'), section.runs);
97
-
98
- for (const tree of section.flows) {
99
- const dir = join(root, flowDirName(tree));
100
- await mkdirp(join(dir, 'nodes'));
101
- await put(join(dir, 'flow.json'), tree.flow);
102
- await put(join(dir, 'version.json'), tree.version);
103
- for (const [i, node] of tree.nodes.entries()) {
104
- await put(join(dir, 'nodes', `${String(i).padStart(2, '0')}--${node.name}.json`), node);
105
- }
106
- }
107
- return written;
108
- };
109
-
110
- // ── read ────────────────────────────────────────────────────────────────────
111
-
112
- export const readFlows = async (workspace: string): Promise<FlowsSection | null> => {
113
- const root = flowsRoot(workspace);
114
- if (!(await exists(root))) return null;
115
-
116
- const meta = (await readJson<FlowsMeta>(join(root, '_meta.json')).catch(() => null)) ?? {
117
- coverage: 'unknown' as const,
118
- readAt: new Date().toISOString(),
119
- notes: ['flows/_meta.json is missing, so coverage is unknown'],
120
- };
121
- const connections = (await readJson<ConnectionRow[]>(join(root, 'connections.json')).catch(() => [])) ?? [];
122
- const pieces = (await readJson<Record<string, string>>(join(root, 'pieces.json')).catch(() => ({}))) ?? {};
123
- const runs =
124
- (await readJson<RunRow[]>(join(root, '_readonly', 'flow-runs.json')).catch(() => [])) ?? [];
125
-
126
- const flows: FlowTree[] = [];
127
- for (const entry of await fs.readdir(root, { withFileTypes: true })) {
128
- if (!entry.isDirectory()) continue;
129
- /**
130
- * Only the tool's own metadata directories are skipped, not everything with a
131
- * leading underscore.
132
- *
133
- * `flowDirName` names a flow that does not exist yet `_new--<slug>`, so a
134
- * blanket `startsWith('_')` filter made an authored flow invisible to the
135
- * reader that produced it — the diff reported "no changes" over a whole flow
136
- * waiting to be created.
137
- */
138
- if (entry.name === '_readonly') continue;
139
- const dir = join(root, entry.name);
140
- const flow = await readJson<Row>(join(dir, 'flow.json'));
141
- const version = await readJson<Row>(join(dir, 'version.json'));
142
-
143
- const nodeDir = join(dir, 'nodes');
144
- const files = (await fs.readdir(nodeDir)).filter((f) => f.endsWith('.json')).sort();
145
- const nodes = [];
146
- for (const f of files) {
147
- const node = await readJson<Row>(join(nodeDir, f));
148
- nodes.push({ ...node, name: String(node['name'] ?? ''), type: String(node['type'] ?? '') });
149
- }
150
- /**
151
- * Order comes from the filename prefix, which is how the codec wrote it — but
152
- * the trigger must be first for `linkFlow` to find the head of the chain, and
153
- * a rename could put something else there.
154
- */
155
- const triggerAt = nodes.findIndex((n) => n.name === 'trigger');
156
- if (triggerAt > 0) nodes.unshift(...nodes.splice(triggerAt, 1));
157
- flows.push({ flow, version, nodes });
158
- }
159
-
160
- return { meta, flows, connections, pieces, runs };
161
- };
162
-
163
- /**
164
- * Record what was just read as the baseline, so the next diff has a past to
165
- * compare against without touching the network.
166
- */
167
- export const writeFlowBaseline = (workspaceRoot: string, repoId: number, section: FlowsSection) =>
168
- writeJson(flowBaselineFile(workspaceRoot, repoId), section);
169
-
170
- /** The recorded flow baseline, or null when this repo has never been pulled. */
171
- export const readFlowBaseline = async (workspaceRoot: string, repoId: number): Promise<FlowsSection | null> => {
172
- try {
173
- return await readJson<FlowsSection>(flowBaselineFile(workspaceRoot, repoId));
174
- } catch {
175
- return null;
176
- }
177
- };
178
-
179
- /** The flow row as the API would return it, for comparison and for operations. */
180
- export const asApiFlow = (tree: FlowTree): Row => linkFlow(tree);
181
-
182
- /** Whether two flow trees are the same, ignoring `_` metadata differences in order. */
183
- export const sameFlow = (a: FlowTree, b: FlowTree) => canonicalString(linkFlow(a)) === canonicalString(linkFlow(b));
184
-
185
- export const flowIdOf = (tree: FlowTree): string | null =>
186
- tree.flow['id'] ? String(tree.flow['id']) : null;
187
-
188
- export const isNewFlow = (tree: FlowTree): boolean => !flowIdOf(tree) || tree.flow['_new'] === true;
189
-
190
- /** Rebuild a tree from an API flow row, for the read path and for verification. */
191
- export const treeFromApi = (row: Row): FlowTree => unlinkFlow(row);
@@ -1,165 +0,0 @@
1
- import { join } from 'node:path';
2
- import { slug } from '../util/slug.ts';
3
- import type { RepoRef } from '../pipefy/discovery.ts';
4
-
5
- /**
6
- * Workspace layout — PLAN.md §4.
7
- *
8
- * Two deliberate deviations from the plan's sketch, both to make the edit loop
9
- * shorter, and both noted in the generated AGENTS.md:
10
- *
11
- * - `phase-jumps.json` is gone. A phase's jump set lives in the phase file as
12
- * `jump_target_ids`, because the internal endpoint is per-phase and
13
- * set-replacing (PLAN.md §2.1), so the set is the honest shape and there is
14
- * no second file to keep consistent.
15
- * - `webhooks.json` is one array file rather than a directory, matching
16
- * `labels.json`. Webhooks are few and have no sub-entities.
17
- */
18
-
19
- export const PPC_DIR = '.ppc';
20
-
21
- export const paths = (root: string) => ({
22
- root,
23
- workspaceFile: join(root, 'workspace.json'),
24
- /**
25
- * `AGENTS.md`, not `CLAUDE.md`: the emerging cross-tool convention (this
26
- * repo's own top-level docs already use it) — the file is read by whatever
27
- * agent is editing the workspace, not only Claude Code.
28
- */
29
- agentsMd: join(root, 'AGENTS.md'),
30
- mappingMd: join(root, 'MAPPING.md'),
31
- capabilitiesMd: join(root, 'CAPABILITIES.md'),
32
- referenceMd: join(root, 'REFERENCE.md'),
33
- pipe: join(root, PPC_DIR),
34
- lock: join(root, PPC_DIR, 'lock.json'),
35
- capabilities: join(root, PPC_DIR, 'capabilities.json'),
36
- /**
37
- * The closed sets the API publishes, read on the last pull. Cached so
38
- * validate, plan and apply can refuse a wrong value offline and name it.
39
- */
40
- reference: join(root, PPC_DIR, 'reference.json'),
41
- baselineDir: join(root, PPC_DIR, 'baseline'),
42
- plansDir: join(root, PPC_DIR, 'plans'),
43
- pipesDir: join(root, 'pipes'),
44
- databasesDir: join(root, 'databases'),
45
- gitignore: join(root, '.gitignore'),
46
- });
47
-
48
- /**
49
- * `pipes/<numericId>--<slug>` / `databases/<internalId>--<apiId>--<slug>`.
50
- *
51
- * A database carries two identities — a numeric internal id and an api id — and
52
- * `table(id:)` answers to both. Putting both in the directory name is what makes
53
- * the duality visible rather than a footnote: whichever spelling you pull with,
54
- * you land in the same directory, and the one you did not use is right there when
55
- * you need to cross-reference a connector field (which carries the numeric form)
56
- * against an API response (which carries the slug).
57
- */
58
- export const repoDirName = (ref: { kind: string; id: number; apiId?: string; name?: string | null }) =>
59
- ref.kind === 'table' && ref.apiId && ref.apiId !== String(ref.id)
60
- ? `${ref.id}--${ref.apiId}--${slug(ref.name)}`
61
- : `${ref.id}--${slug(ref.name)}`;
62
-
63
- export const repoDir = (root: string, ref: RepoRef) =>
64
- join(ref.kind === 'table' ? paths(root).databasesDir : paths(root).pipesDir, repoDirName(ref));
65
-
66
- /**
67
- * The files of one repo.
68
- *
69
- * `kind` decides one name: a database's root row is `table.json`, not
70
- * `pipe.json`. It is worth the parameter because a file called `pipe.json`
71
- * inside `databases/` teaches a reader something false about the platform, and
72
- * this workspace is read by agents that have no other source of truth.
73
- *
74
- * Required rather than defaulted: a caller who forgets would silently get pipe
75
- * paths for a database and a mysteriously absent root file.
76
- */
77
- export const repoPaths = (dir: string, kind: 'pipe' | 'table' = 'pipe') => ({
78
- dir,
79
- meta: join(dir, '_meta.json'),
80
- repo: join(dir, kind === 'table' ? 'table.json' : 'pipe.json'),
81
- /** Both spellings, so a directory can be recognised before its kind is known. */
82
- repoCandidates: [join(dir, 'pipe.json'), join(dir, 'table.json')],
83
- phasesDir: join(dir, 'phases'),
84
- automationsDir: join(dir, 'automations'),
85
- fieldConditionsDir: join(dir, 'field-conditions'),
86
- labels: join(dir, 'labels.json'),
87
- webhooks: join(dir, 'webhooks.json'),
88
- publicForm: join(dir, 'public-form.json'),
89
- preferences: join(dir, 'preferences.json'),
90
- relations: join(dir, 'relations.json'),
91
- readonlyDir: join(dir, '_readonly'),
92
- agentsDir: join(dir, 'agents'),
93
- });
94
-
95
- /** Baseline is stored as the payload envelope: one file, and diff-ready. */
96
- export const baselineFile = (root: string, repoId: number) => join(paths(root).baselineDir, `${repoId}.json`);
97
-
98
- /**
99
- * The agent baseline: what a repo's AI agents looked like at the last pull.
100
- *
101
- * Stored separately from the payload envelope for the same reason flows are:
102
- * agents are not in it, they come from a different query, keyed by the pipe's
103
- * uuid rather than its id (src/pipefy/agents.ts).
104
- */
105
- export const agentBaselineFile = (root: string, repoId: number) =>
106
- join(paths(root).baselineDir, `${repoId}.agents.json`);
107
-
108
- /**
109
- * The flow baseline: what a repo's iPaaS flows looked like at the last pull.
110
- *
111
- * Stored separately from the payload envelope because flows are not in it —
112
- * they come from a different endpoint. Without this the flow diff had no
113
- * recorded past to compare against and had to re-read the pipe live every time,
114
- * which meant no offline diff and no record of what was pulled.
115
- */
116
- export const flowBaselineFile = (root: string, repoId: number) =>
117
- join(paths(root).baselineDir, `${repoId}.flows.json`);
118
-
119
- export const planFile = (root: string, planId: string) => join(paths(root).plansDir, `${planId}.json`);
120
-
121
- /**
122
- * How a finished plan is named.
123
- *
124
- * A plan file is the record `--resume` replays, and it is written before the
125
- * first request and rewritten after every step, so a crash mid-apply is
126
- * resumable. That made `.ppc/plans/` a list of filenames that all look alike,
127
- * with no way to tell an applied plan from one still waiting — the outcome was
128
- * only inside the JSON, or via `pipe status`.
129
- *
130
- * So the outcome goes in the name once the apply is over. **No suffix means it
131
- * was never applied**, which is the state a bare `pipe plan` leaves behind.
132
- * The plan id does not change, so every `--resume <id>` instruction still works.
133
- */
134
- export const PLAN_SUFFIX: Record<string, string> = {
135
- applied: '--applied',
136
- 'partially-applied': '--partial',
137
- failed: '--failed',
138
- };
139
-
140
- export const planFileFor = (root: string, planId: string, status: string) =>
141
- join(paths(root).plansDir, `${planId}${PLAN_SUFFIX[status] ?? ''}.json`);
142
-
143
- /**
144
- * The file holding a plan, whatever it was renamed to. Returns null if there is
145
- * none — the caller decides whether that is an error.
146
- */
147
- export const findPlanFile = async (root: string, planId: string): Promise<string | null> => {
148
- const { access } = await import('node:fs/promises');
149
- const candidates = [planFile(root, planId), ...Object.values(PLAN_SUFFIX).map((sfx) => join(paths(root).plansDir, `${planId}${sfx}.json`))];
150
- for (const c of candidates) {
151
- try {
152
- await access(c);
153
- return c;
154
- } catch {
155
- // try the next spelling
156
- }
157
- }
158
- return null;
159
- };
160
-
161
- /** Paths under a repo dir that must never be edited — checked by path alone. */
162
- export const isReadonlyPath = (relPath: string) => {
163
- const p = relPath.replace(/\\/g, '/');
164
- return p.includes('/_readonly/') || p.startsWith('_readonly/');
165
- };
@@ -1,148 +0,0 @@
1
- import { relative } from 'node:path';
2
- import { exists, readJson, writeJson } from '../util/fsx.ts';
3
- import type { Coverage, EntityName } from '../model/payload.ts';
4
- import type { RepoRef } from '../pipefy/discovery.ts';
5
- import type { Tree } from '../model/tree.ts';
6
- import { baselineFile, paths, repoDir } from './layout.ts';
7
-
8
- /**
9
- * `.ppc/lock.json` — PLAN.md §4: id ↔ uuid ↔ path, the table-id duality, and
10
- * the version ids of what was pulled.
11
- *
12
- * It is also where the applier looks up "what did this uuid become", which is
13
- * the job the Change Migrator recipes do with a Pipefy table keyed
14
- * `dev_pipe_field_internal_id → prod_pipe_field_internal_id` and one API call
15
- * per id. Same idea, local and free.
16
- */
17
-
18
- export type LockRepo = {
19
- kind: 'pipe' | 'table';
20
- /** Numeric id, which every API accepts. */
21
- id: number;
22
- /**
23
- * What the API answers with. For tables this is a slug (`dzmxcuxz`) while the
24
- * connector field says `301908697` — store both or a connected database gets
25
- * pulled twice under two identities (PLAN.md §2.2).
26
- */
27
- apiId: string;
28
- uuid: string | null;
29
- name: string | null;
30
- dir: string;
31
- via: string[];
32
- depth: number;
33
- source: 'snapshot' | 'reconstructed';
34
- versionId: string | null;
35
- readAt: string;
36
- coverage: Record<EntityName, Coverage>;
37
- /** uuid → numeric id, for the four arrays that have a uuid. */
38
- identity: Partial<Record<EntityName, Record<string, number>>>;
39
- /** field numeric id → slug, so `%{433255467}` can be explained to a human. */
40
- fieldSlugs: Record<string, string>;
41
- };
42
-
43
- export type Lock = {
44
- version: 1;
45
- updatedAt: string;
46
- rootRepoId: number;
47
- organizationId: string | null;
48
- depth: number;
49
- repos: LockRepo[];
50
- edges: Array<{ from: number; to: number; kind: string; label?: string }>;
51
- };
52
-
53
- export const emptyLock = (rootRepoId: number, organizationId: string | null, depth: number): Lock => ({
54
- version: 1,
55
- updatedAt: new Date().toISOString(),
56
- rootRepoId,
57
- organizationId,
58
- depth,
59
- repos: [],
60
- edges: [],
61
- });
62
-
63
- export const readLock = async (root: string): Promise<Lock | null> => {
64
- const p = paths(root).lock;
65
- if (!(await exists(p))) return null;
66
- return readJson<Lock>(p);
67
- };
68
-
69
- export const writeLock = async (root: string, lock: Lock) => {
70
- lock.updatedAt = new Date().toISOString();
71
- await writeJson(paths(root).lock, lock);
72
- };
73
-
74
- const identityFor = (tree: Tree): LockRepo['identity'] => {
75
- const grab = (rows: Array<Record<string, unknown>>): Record<string, number> => {
76
- const out: Record<string, number> = {};
77
- for (const r of rows) {
78
- const uuid = r['uuid'];
79
- const id = Number(r['id']);
80
- if (typeof uuid === 'string' && uuid && Number.isFinite(id)) out[uuid] = id;
81
- }
82
- return out;
83
- };
84
- return {
85
- phases: grab(tree.phases),
86
- fields: grab(tree.phases.flatMap((p) => p.fields)),
87
- automations: grab(tree.automations),
88
- labels: grab(tree.labels),
89
- };
90
- };
91
-
92
- export const upsertRepo = (lock: Lock, ref: RepoRef, root: string, tree: Tree): LockRepo => {
93
- const entry: LockRepo = {
94
- kind: ref.kind,
95
- id: ref.id,
96
- apiId: ref.apiId,
97
- uuid: (tree.repo['uuid'] as string | undefined) ?? ref.uuid ?? null,
98
- name: (tree.repo['name'] as string | undefined) ?? ref.name ?? null,
99
- dir: relative(root, repoDir(root, ref)).replace(/\\/g, '/'),
100
- via: ref.via,
101
- depth: ref.depth,
102
- source: tree.meta.source,
103
- versionId: tree.meta.versionId ?? null,
104
- readAt: tree.meta.readAt,
105
- coverage: tree.meta.coverage,
106
- identity: identityFor(tree),
107
- fieldSlugs: Object.fromEntries(
108
- tree.phases
109
- .flatMap((p) => p.fields)
110
- .filter((f) => f['slug'])
111
- .map((f) => [String(f['id']), String(f['slug'])]),
112
- ),
113
- };
114
- const i = lock.repos.findIndex((r) => r.id === ref.id);
115
- if (i >= 0) lock.repos[i] = entry;
116
- else lock.repos.push(entry);
117
- return entry;
118
- };
119
-
120
- export const lookupRepo = (lock: Lock, idOrName: string): LockRepo | undefined => {
121
- const byId = lock.repos.find((r) => String(r.id) === idOrName || r.apiId === idOrName);
122
- if (byId) return byId;
123
- const needle = idOrName.toLowerCase();
124
- return lock.repos.find((r) => (r.name ?? '').toLowerCase() === needle || r.dir.toLowerCase().includes(needle));
125
- };
126
-
127
- export const rootRepo = (lock: Lock): LockRepo | undefined => lock.repos.find((r) => r.id === lock.rootRepoId);
128
-
129
- export const baselinePathFor = (root: string, repoId: number) => baselineFile(root, repoId);
130
-
131
- export type WorkspaceFile = {
132
- name: string;
133
- createdAt: string;
134
- rootRepoId: number;
135
- organizationId: string | null;
136
- depth: number;
137
- /** How the last pull read the structure — informational, per repo in lock.json. */
138
- lastPullSource: 'snapshot' | 'reconstructed' | 'mixed';
139
- tool: { name: 'pipefy-process-coder'; version: string };
140
- };
141
-
142
- export const readWorkspace = async (root: string): Promise<WorkspaceFile | null> => {
143
- const p = paths(root).workspaceFile;
144
- if (!(await exists(p))) return null;
145
- return readJson<WorkspaceFile>(p);
146
- };
147
-
148
- export const writeWorkspace = (root: string, w: WorkspaceFile) => writeJson(paths(root).workspaceFile, w);
@@ -1,165 +0,0 @@
1
- import { basename, dirname } from 'node:path';
2
- import { exists, listJson, readJson } from '../util/fsx.ts';
3
- import { UserError } from '../util/log.ts';
4
- import { fullCoverage, type Row } from '../model/payload.ts';
5
- import type { AutomationNode, FieldConditionNode, PhaseNode, Tree, TreeMeta } from '../model/tree.ts';
6
- import { repoPaths } from './layout.ts';
7
- import { AGENT_DIR_TAG } from './agents.ts';
8
-
9
- /**
10
- * files → tree. The exact inverse of write.ts, so that
11
- * `readTree(writeTree(t)) ≡ t` and the edited workspace can be diffed against
12
- * the baseline with no special cases.
13
- *
14
- * Ordering never comes from filenames: the index-prefixed names are for humans,
15
- * and `index` inside the file is authoritative. A phase Claude renumbered by
16
- * editing `index` therefore works without renaming any file.
17
- */
18
-
19
- const num = (v: unknown): number => {
20
- const n = Number(v);
21
- return Number.isFinite(n) ? n : 0;
22
- };
23
-
24
- /**
25
- * An id, or the placeholder that stands for one.
26
- *
27
- * `num` collapses anything non-numeric to 0, which is right for an index and
28
- * catastrophic for an id list: a phase jumping to a phase created in the same
29
- * edit is written `"%{_new:<uuid>}"`, exactly as the generated AGENTS.md
30
- * instructs, and `num` turned that into a jump to phase 0 — accepted by the
31
- * validator, sent to the API, and impossible to explain. A typo met the same
32
- * fate. Non-numeric values are kept verbatim so the validator can judge them.
33
- */
34
- const idOrPlaceholder = (v: unknown): number | string => {
35
- const n = Number(v);
36
- if (Number.isFinite(n) && v !== '' && v !== null) return n;
37
- return String(v);
38
- };
39
-
40
- const readArrayFile = async (path: string): Promise<Row[]> => {
41
- if (!(await exists(path))) return [];
42
- const v = await readJson<unknown>(path);
43
- if (v === null) return [];
44
- if (!Array.isArray(v)) throw new UserError(`${path}: expected a JSON array`);
45
- return v as Row[];
46
- };
47
-
48
- const readObjectFile = async (path: string): Promise<Row | null> => {
49
- if (!(await exists(path))) return null;
50
- const v = await readJson<unknown>(path);
51
- if (v === null) return null;
52
- if (typeof v !== 'object' || Array.isArray(v)) throw new UserError(`${path}: expected a JSON object or null`);
53
- return v as Row;
54
- };
55
-
56
- const readDirOf = async <T>(dir: string): Promise<Array<{ path: string; value: T }>> => {
57
- const files = (await listJson(dir)).filter((f) => basename(f) !== '_meta.json');
58
- const out: Array<{ path: string; value: T }> = [];
59
- for (const f of files) out.push({ path: f, value: await readJson<T>(f) });
60
- return out;
61
- };
62
-
63
- export const readTree = async (dir: string): Promise<Tree> => {
64
- /**
65
- * The kind comes from `_meta.json`, which is written first and whose path does
66
- * not depend on the kind. Falling back to probing both spellings keeps a
67
- * hand-made directory readable.
68
- */
69
- const meta = ((await readObjectFile(repoPaths(dir).meta)) ?? {}) as unknown as TreeMeta;
70
- const p = repoPaths(dir, meta.repoKind === 'table' ? 'table' : 'pipe');
71
- let repoFile = p.repo;
72
- if (!(await exists(repoFile))) {
73
- const found = [];
74
- for (const c of p.repoCandidates) if (await exists(c)) found.push(c);
75
- if (!found.length) {
76
- throw new UserError(`${dir} is not a pulled repo directory (no pipe.json or table.json)`);
77
- }
78
- repoFile = found[0] as string;
79
- }
80
-
81
- const repo = (await readObjectFile(repoFile)) ?? {};
82
-
83
- const phaseFiles = await readDirOf<PhaseNode>(p.phasesDir);
84
- const phases = phaseFiles
85
- .map(({ path, value }) => {
86
- if (!value || typeof value !== 'object') throw new UserError(`${path}: expected a phase object`);
87
- return {
88
- ...value,
89
- fields: Array.isArray(value.fields) ? value.fields : [],
90
- jump_target_ids: Array.isArray(value.jump_target_ids) ? value.jump_target_ids.map(idOrPlaceholder) : [],
91
- team_member_ids: Array.isArray(value.team_member_ids) ? value.team_member_ids.map(idOrPlaceholder) : [],
92
- } as PhaseNode;
93
- })
94
- .sort((a, b) => num(a['index']) - num(b['index']) || num(a['id']) - num(b['id']));
95
-
96
- const normaliseAutomation = (path: string, value: unknown): AutomationNode => {
97
- if (!value || typeof value !== 'object') throw new UserError(`${path}: expected an automation object`);
98
- const v = value as AutomationNode;
99
- return {
100
- ...v,
101
- condition: v.condition ?? null,
102
- field_maps: Array.isArray(v.field_maps) ? v.field_maps : [],
103
- distribute_assignments: Array.isArray(v.distribute_assignments) ? v.distribute_assignments : [],
104
- response_schemas: Array.isArray(v.response_schemas) ? v.response_schemas : [],
105
- };
106
- };
107
-
108
- const automations = (await readDirOf<AutomationNode>(p.automationsDir)).map(({ path, value }) =>
109
- normaliseAutomation(path, value),
110
- );
111
-
112
- /**
113
- * A behavior — an automation folded under its agent's `behaviors/` instead
114
- * of `automations/`, per workspace/agents.ts. `_agent_dir` is not read from
115
- * the file (it is never written there); it is derived from where the file
116
- * physically is, which is the only record of the link there is.
117
- */
118
- for (const path of await listJson(p.agentsDir)) {
119
- if (basename(dirname(path)) !== 'behaviors') continue;
120
- const agentDir = basename(dirname(dirname(path)));
121
- const value = await readJson<unknown>(path);
122
- automations.push({ ...normaliseAutomation(path, value), [AGENT_DIR_TAG]: agentDir } as AutomationNode);
123
- }
124
-
125
- const field_conditions = (await readDirOf<FieldConditionNode>(p.fieldConditionsDir))
126
- .map(({ path, value }) => {
127
- if (!value || typeof value !== 'object') throw new UserError(`${path}: expected a field condition object`);
128
- return {
129
- ...value,
130
- actions: Array.isArray(value.actions) ? value.actions : [],
131
- condition: value.condition ?? null,
132
- } as FieldConditionNode;
133
- })
134
- .sort((a, b) => num(a['index']) - num(b['index']) || num(a['id']) - num(b['id']));
135
-
136
- const relationsFile = (await readObjectFile(p.relations)) ?? {};
137
- const pipe_relations = Array.isArray(relationsFile['pipe_relations']) ? (relationsFile['pipe_relations'] as Row[]) : [];
138
-
139
- return {
140
- meta: {
141
- source: meta.source ?? 'reconstructed',
142
- versionId: meta.versionId ?? null,
143
- readAt: meta.readAt ?? new Date().toISOString(),
144
- repoKind: meta.repoKind ?? 'pipe',
145
- coverage: meta.coverage ?? fullCoverage('unknown'),
146
- repo_columns: meta.repo_columns ?? 'complete',
147
- notes: meta.notes ?? [],
148
- },
149
- repo,
150
- phases,
151
- automations,
152
- field_conditions,
153
- labels: await readArrayFile(p.labels),
154
- webhooks: await readArrayFile(p.webhooks),
155
- pipe_relations,
156
- public_form: await readObjectFile(p.publicForm),
157
- preferences: await readObjectFile(p.preferences),
158
- readonly: {
159
- email_templates: await readArrayFile(`${p.readonlyDir}/email-templates.json`),
160
- email_inboxes: await readArrayFile(`${p.readonlyDir}/email-inboxes.json`),
161
- visibilities: await readArrayFile(`${p.readonlyDir}/visibilities.json`),
162
- field_maps: await readArrayFile(`${p.readonlyDir}/field-maps.json`),
163
- },
164
- };
165
- };