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