@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
@@ -0,0 +1,131 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { spawn } from 'node:child_process';
5
+ import { exists } from "../util/fsx.js";
6
+ import { UserError, bold, dim, out, info } from "../util/log.js";
7
+ export const help = `pipe skills [list|install] [options]
8
+
9
+ list the agent skills this install ships with (default). Local,
10
+ offline, no network
11
+
12
+ install hand off to \`npx skills add\` (vercel-labs/skills), which
13
+ installs these skills for whichever agents you have —
14
+ Claude Code, Cursor, Codex, and 75+ others — choosing each
15
+ agent's own directory and, by default, symlinking rather than
16
+ copying. Needs network (fetches the \`skills\` package via npx
17
+ on first run) and Node's npx on PATH. Every flag after
18
+ \`install\` is forwarded to it verbatim; see
19
+ https://github.com/vercel-labs/skills or \`npx skills add --help\`.
20
+ Common ones:
21
+ -g, --global install to the home directory, not the
22
+ project
23
+ -a, --agent <...> target specific agents (e.g. claude-code)
24
+ -s, --skill <...> install specific skills, not all four
25
+ -y, --yes skip confirmation prompts
26
+ --copy copy instead of symlinking`;
27
+ /**
28
+ * These skills live at \`.agents/skills/\` in this repo — one of the standard
29
+ * discovery locations \`npx skills add\` already walks — so they only ever
30
+ * triggered for an agent session opened *inside* this repo until installed
31
+ * somewhere every session reads from. A pulled workspace (PLAN.md, README
32
+ * "Where `pipe` can run") is meant to live anywhere — a client's own folder,
33
+ * a completely different repo — so the guidance on how to use `pipe` needs
34
+ * to travel with the binary, not stay pinned to one checkout.
35
+ *
36
+ * `install` used to copy the folders and manage a `~/.claude/skills`
37
+ * compatibility link by hand — this repo's own version of the exact problem
38
+ * vercel-labs/skills already solves generically, for 75+ agents, not just
39
+ * Claude Code. Delegating to it is less code and more correct than
40
+ * reinventing it, at the cost of the one thing this CLI otherwise has none
41
+ * of: a runtime dependency (network + npx) for this one subcommand only.
42
+ */
43
+ /** Resolved from this file's own URL — survives `npm link` and `npm i -g .`
44
+ * the same way `pipe --version` locates package.json (cli.ts). */
45
+ const packageRoot = () => {
46
+ const here = dirname(fileURLToPath(import.meta.url));
47
+ return join(here, '..', '..');
48
+ };
49
+ const sourceDir = () => join(packageRoot(), '.agents', 'skills');
50
+ const skillDirs = async (src) => {
51
+ let entries;
52
+ try {
53
+ entries = await readdir(src, { withFileTypes: true });
54
+ }
55
+ catch (e) {
56
+ throw new UserError(`could not read ${src}`, e.message);
57
+ }
58
+ const names = [];
59
+ for (const e of entries) {
60
+ if (e.isDirectory() && (await exists(join(src, e.name, 'SKILL.md'))))
61
+ names.push(e.name);
62
+ }
63
+ return names.sort();
64
+ };
65
+ const readSkillMeta = async (dir) => {
66
+ try {
67
+ const md = await readFile(join(dir, 'SKILL.md'), 'utf8');
68
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md)?.[1] ?? '';
69
+ const description = /^description:\s*(.+)$/m.exec(frontmatter)?.[1]?.trim().replace(/\r$/, '');
70
+ return { description };
71
+ }
72
+ catch {
73
+ return {};
74
+ }
75
+ };
76
+ const list = async () => {
77
+ const src = sourceDir();
78
+ const names = await skillDirs(src);
79
+ if (!names.length)
80
+ throw new UserError(`no skills found under ${src}`, 'the install may be incomplete — re-clone or re-install');
81
+ out(bold(`${names.length} skill(s) ship with this install`));
82
+ for (const name of names) {
83
+ const meta = await readSkillMeta(join(src, name));
84
+ out(` ${name}`);
85
+ if (meta.description)
86
+ out(dim(` ${meta.description}`));
87
+ }
88
+ info('');
89
+ info(dim(' `pipe skills install` hands off to `npx skills add`, which installs'));
90
+ info(dim(' these for whichever agents you have — Claude Code, Cursor, Codex and'));
91
+ info(dim(' 75+ others — not only a session opened inside this repo.'));
92
+ return 0;
93
+ };
94
+ /**
95
+ * Everything typed after the `install`/`list` subcommand, forwarded to
96
+ * `npx skills add` byte for byte — reconstructing it from the parsed flags
97
+ * would round-trip lossily (repeated `-a`, bare `-y`, `--skill '*'`, …).
98
+ * `argv` is `process.argv` (node, script, `skills`, `install`, …flags).
99
+ */
100
+ export const npxInstallArgs = (argv, root) => {
101
+ const raw = argv.slice(3);
102
+ const forwarded = raw[0] === 'install' || raw[0] === 'list' ? raw.slice(1) : raw;
103
+ return ['skills', 'add', root, ...forwarded];
104
+ };
105
+ const install = async () => {
106
+ const src = sourceDir();
107
+ const names = await skillDirs(src);
108
+ if (!names.length)
109
+ throw new UserError(`no skills found under ${src}`, 'the install may be incomplete — re-clone or re-install');
110
+ const args = npxInstallArgs(process.argv, packageRoot());
111
+ out(dim(`+ npx ${args.join(' ')}`));
112
+ // `shell: true` on Windows: `npx` resolves to `npx.cmd`, a batch file, and
113
+ // Node refuses to spawn `.cmd` directly without a shell (the CVE-2024-27980
114
+ // fix) — spawning `npx.cmd` outright fails with EINVAL. Node warns that the
115
+ // args array is then shell-concatenated unescaped, which matters for
116
+ // untrusted input; there is none here — `args` is this CLI's own resolved
117
+ // package path plus whatever the person invoking `pipe` typed themselves.
118
+ return new Promise((resolve, reject) => {
119
+ const child = spawn('npx', args, { stdio: 'inherit', shell: process.platform === 'win32' });
120
+ child.on('error', (e) => reject(new UserError('could not run `npx skills add`', e.message)));
121
+ child.on('exit', (code) => resolve(code ?? 1));
122
+ });
123
+ };
124
+ export const run = async (p) => {
125
+ const sub = p.positionals[0] ?? 'list';
126
+ if (sub === 'list')
127
+ return list();
128
+ if (sub === 'install')
129
+ return install();
130
+ throw new UserError(`unknown \`pipe skills ${sub}\``, 'use `pipe skills list` or `pipe skills install`');
131
+ };
@@ -0,0 +1,205 @@
1
+ import { bool } from "../cli/args.js";
2
+ import { writeJson } from "../util/fsx.js";
3
+ import { dim, info, ok, out, stripAnsi, warn } from "../util/log.js";
4
+ import { fullCoverage } from "../model/payload.js";
5
+ import { unpack } from "../codec/unpack.js";
6
+ import { readTree } from "../workspace/read.js";
7
+ import { baselineFile } from "../workspace/layout.js";
8
+ import { diffTrees } from "../diff/diff.js";
9
+ import { renderChangeSet } from "../diff/render.js";
10
+ import { connect, openWorkspace } from "./context.js";
11
+ import { RunRecorder, invocation } from "../report/run.js";
12
+ export const help = `pipe verify [workspace] [options]
13
+
14
+ --repo <id> which repo in the workspace (default: the root pipe)
15
+ --update adopt the live state wholesale, once it matches the workspace
16
+ --adopt adopt only the server-assigned ids of entities the workspace
17
+ still marks as new, keeping every unapplied edit. This is the
18
+ repair for a partial apply: without it a re-plan would create
19
+ those entities a second time.
20
+ --json machine-readable output
21
+ --report write a run report into runs/ (payload copy included)`;
22
+ /**
23
+ * `pipe verify` — read the pipe back and diff it against what the workspace asks
24
+ * for. PLAN.md §5 step 6: "Empty diff = provably correct."
25
+ *
26
+ * This is the same routine the apply runs at the end, and the same one a draft
27
+ * rehearsal would run first. That symmetry is the safety model, and it survives
28
+ * the Route B applier swap untouched.
29
+ */
30
+ export const run = async (p) => {
31
+ const { root, lock, repo, repoDir: dir } = await openWorkspace(p);
32
+ const net = await connect(p);
33
+ /**
34
+ * `--adopt` runs before any comparison, because its whole purpose is to fix a
35
+ * workspace that cannot verify: entities that exist on the pipe but still read
36
+ * `"id": null` in the files they were authored in.
37
+ */
38
+ if (bool(p, 'adopt')) {
39
+ const { stampCreated, renderStamp } = await import("../workspace/stamp.js");
40
+ const { writeTree } = await import("../workspace/write.js");
41
+ const caps = await net.capabilities();
42
+ info(dim(`reading pipe ${repo.id} to match against (${caps.snapshots ? 'snapshot' : 'GraphQL reconstruction'})`));
43
+ const envelope = caps.snapshots
44
+ ? await (async () => {
45
+ const { payload, versionId } = await net.snapshots.pull(repo.id);
46
+ return {
47
+ source: 'snapshot',
48
+ versionId,
49
+ readAt: new Date().toISOString(),
50
+ repoKind: repo.kind,
51
+ coverage: fullCoverage('complete'),
52
+ notes: [],
53
+ payload,
54
+ };
55
+ })()
56
+ : await net.reconstructor.reconstruct(repo.id, { organizationId: lock.organizationId });
57
+ const authored = await readTree(dir);
58
+ const { loadBaselineEnvelope } = await import("./diff.js");
59
+ const previous = await loadBaselineEnvelope(root, repo.id);
60
+ const stamp = stampCreated(authored, unpack(envelope), previous ? unpack(previous) : null);
61
+ if (!stamp.changed && !stamp.unmatched.length) {
62
+ ok('nothing to adopt — the workspace marks no entity as new');
63
+ return 0;
64
+ }
65
+ if (stamp.changed)
66
+ await writeTree(dir, authored);
67
+ // The baseline is updated regardless: the read just done is newer than it,
68
+ // and leaving it stale is what makes a plan re-create what already exists.
69
+ /**
70
+ * The baseline has to learn about them too.
71
+ *
72
+ * A diff is baseline versus workspace. Stamping the ids into the files while
73
+ * leaving the baseline at its pre-apply state means the baseline has never
74
+ * heard of those entities, so the diff goes on proposing to create them —
75
+ * which is the very loop this is meant to break. The read that was just done
76
+ * is exactly the right new baseline.
77
+ */
78
+ await writeJson(baselineFile(root, repo.id), envelope);
79
+ const adoptRec = recorderFor(p, root, repo, 'verify');
80
+ adoptRec?.after(envelope);
81
+ for (const line of renderStamp(stamp))
82
+ adoptRec?.note(stripAnsi(line).trim());
83
+ adoptRec?.note(`adopted ${stamp.stamped.length} server id(s); ${stamp.unmatched.length} still unmatched`);
84
+ await write(adoptRec, stamp.changed ? 'ok' : 'no-op', `--adopt: ${stamp.stamped.length} id(s) adopted into the workspace`);
85
+ for (const line of renderStamp(stamp))
86
+ info(line);
87
+ info(dim(' the baseline now reflects the pipe, so the next diff shows only what is still unapplied'));
88
+ return 0;
89
+ }
90
+ const rec = recorderFor(p, root, repo, 'verify');
91
+ const result = await verifyRepo({ net, root, lock, repo, expectedTreeDir: dir });
92
+ rec?.after(result.envelope);
93
+ rec?.verified(result.ok, result.ok ? undefined : `${result.changeCount} difference(s) remain`);
94
+ if (bool(p, 'json')) {
95
+ out(JSON.stringify({ ok: result.ok, changes: result.changeCount }, null, 2));
96
+ }
97
+ else {
98
+ out(result.render);
99
+ }
100
+ if (result.ok && bool(p, 'update')) {
101
+ /**
102
+ * Adopt the live state as both the files and the baseline. Updating only the
103
+ * baseline would leave a created entity still reading `"id": null` in the
104
+ * file it came from, so the next diff would propose creating it again.
105
+ */
106
+ const { writeTree } = await import("../workspace/write.js");
107
+ const { upsertRepo, writeLock } = await import("../workspace/lock.js");
108
+ const { refFor } = await import("./diff.js");
109
+ const { tagBehaviors, behaviorDirMapOnDisk } = await import("../workspace/agents.js");
110
+ const live = unpack(result.envelope);
111
+ // See the identical comment in apply.ts's success path: a live read-back
112
+ // does not know which automations are behaviors on its own.
113
+ live.automations = tagBehaviors(live.automations, await behaviorDirMapOnDisk(dir));
114
+ await writeTree(dir, live);
115
+ await writeJson(baselineFile(root, repo.id), result.envelope);
116
+ upsertRepo(lock, refFor(repo), root, live);
117
+ await writeLock(root, lock);
118
+ ok('workspace and baseline updated to the live state');
119
+ rec?.note('--update: workspace and baseline adopted the live state');
120
+ }
121
+ await write(rec, result.ok ? 'ok' : 'failed', result.ok ? 'the pipe satisfies the workspace intent' : `${result.changeCount} difference(s) between the pipe and the workspace`);
122
+ return result.ok ? 0 : 1;
123
+ };
124
+ /**
125
+ * A verify is a run too — it is the read that says whether the pipe is what the
126
+ * workspace claims, and the payload it read is the evidence. Written only on
127
+ * `--report`, and never for `--json`, where the caller is a script that wants
128
+ * one line of output.
129
+ */
130
+ const recorderFor = (p, root, repo, kind) => !bool(p, 'report') || bool(p, 'json')
131
+ ? null
132
+ : new RunRecorder(kind, {
133
+ workspace: root,
134
+ repo: { id: repo.id, uuid: repo.uuid, name: repo.name ?? String(repo.id), kind: repo.kind },
135
+ command: invocation(),
136
+ });
137
+ const write = async (rec, outcome, summary) => {
138
+ if (!rec)
139
+ return;
140
+ const res = await rec.write(outcome, summary);
141
+ if (res?.error)
142
+ warn(`could not write the run report: ${res.error}`);
143
+ else if (res)
144
+ info(dim(` run recorded in runs/${rec.id}/`));
145
+ };
146
+ export const verifyRepo = async (args) => {
147
+ const { net, lock, repo, expectedTreeDir, idMap, preferReconstruct } = args;
148
+ const caps = await net.capabilities();
149
+ const useSnapshot = caps.snapshots && !preferReconstruct;
150
+ const authored = await readTree(expectedTreeDir);
151
+ const expected = idMap ? idMap.rewrite(authored) : authored;
152
+ info(dim(`reading pipe ${repo.id} back (${useSnapshot ? 'snapshot' : 'GraphQL reconstruction'})`));
153
+ let envelope;
154
+ /**
155
+ * A database reads back through the snapshot path too.
156
+ *
157
+ * It used to fall through to reconstruction, which is a *weaker* check: no
158
+ * reconstructed group is ever 'complete', so coverage suppresses part of the
159
+ * comparison and a verified-clean apply proves less than it appears to.
160
+ * `preferReconstruct` trades that away deliberately — see its doc comment.
161
+ */
162
+ if (useSnapshot) {
163
+ const { payload, versionId } = await net.snapshots.pull(repo.id, `post-apply/${new Date().toISOString()}`);
164
+ envelope = {
165
+ source: 'snapshot',
166
+ versionId,
167
+ readAt: new Date().toISOString(),
168
+ repoKind: repo.kind,
169
+ coverage: fullCoverage('complete'),
170
+ notes: [],
171
+ payload,
172
+ };
173
+ }
174
+ else {
175
+ envelope = await net.reconstructor.reconstruct(repo.id, { organizationId: lock.organizationId });
176
+ }
177
+ const live = unpack(envelope);
178
+ /**
179
+ * Direction matters: live -> expected. A change in this diff means "the pipe
180
+ * still needs this done", which is exactly what verification should report.
181
+ */
182
+ const createdIds = idMap
183
+ ? Object.values(idMap.snapshot().placeholders).map(Number).filter((n) => Number.isFinite(n))
184
+ : [];
185
+ if (!idMap && JSON.stringify(authored).includes('%{_new:')) {
186
+ warn('this workspace still names entities by placeholder, and there is no apply to resolve them against — ' +
187
+ 'those values are reported as satisfied rather than checked. Run pipe apply, which verifies with its own id map.');
188
+ }
189
+ const changeSet = diffTrees(live, expected, { intentOnly: true, createdIds });
190
+ if (changeSet.changes.length === 0) {
191
+ return { ok: true, changeCount: 0, render: 'verified — the pipe matches the workspace', envelope, changeSet };
192
+ }
193
+ const residual = changeSet.changes.filter((c) => !c.flags.includes('readonly-edit'));
194
+ if (residual.length === 0) {
195
+ warn('the only differences are in read-only entities, which were never going to be applied');
196
+ return { ok: true, changeCount: 0, render: renderChangeSet(changeSet), envelope, changeSet };
197
+ }
198
+ return {
199
+ ok: false,
200
+ changeCount: residual.length,
201
+ render: renderChangeSet(changeSet),
202
+ envelope,
203
+ changeSet,
204
+ };
205
+ };
package/dist/config.js ADDED
@@ -0,0 +1,125 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { promises as fs } from 'node:fs';
4
+ import { exists, readJson, writeJson } from "./util/fsx.js";
5
+ import { UserError, debug } from "./util/log.js";
6
+ import { readBearer, INSTALL_HINT } from "./pipefy/toolkit.js";
7
+ /**
8
+ * Endpoints. `internal` is the unversioned app endpoint the web UI uses; both
9
+ * accept the same personal API token, which is why this CLI stays token-only
10
+ * with no login webview or session store (PLAN.md §2.1).
11
+ */
12
+ export const ENDPOINTS = {
13
+ graphql: process.env.PPC_GRAPHQL_URL ?? 'https://api.pipefy.com/graphql',
14
+ internal: process.env.PPC_INTERNAL_URL ?? 'https://app.pipefy.com/internal_api',
15
+ internalSettings: process.env.PPC_INTERNAL_SETTINGS_URL ?? 'https://app.pipefy.com/internal_api/settings',
16
+ };
17
+ const configDir = () => process.env.PPC_CONFIG_DIR ?? join(homedir(), '.ppc');
18
+ const credPath = () => join(configDir(), 'credentials.json');
19
+ /**
20
+ * Credential resolution, in order:
21
+ *
22
+ * 1. `PIPEFY_TOKEN` / `PPC_TOKEN` — an explicit override, and the only path
23
+ * that needs nothing installed. CI uses this.
24
+ * 2. The **Pipefy AI toolkit** — `pipefy auth login` stores an OAuth session in
25
+ * the OS keychain, and the toolkit resolves its own precedence (static
26
+ * token, service account, stored session) on top of it. This is the
27
+ * intended path: one login shared with the MCP server and the `pipefy` CLI.
28
+ * 3. `~/.ppc/credentials.json` — `pipe login --token`, for machines without
29
+ * the toolkit.
30
+ *
31
+ * The toolkit's bearer is short-lived, so nothing caches it to disk and callers
32
+ * take a provider that can re-resolve. See src/pipefy/toolkit.ts.
33
+ */
34
+ export const resolveCredential = async () => {
35
+ const env = process.env.PIPEFY_TOKEN ?? process.env.PPC_TOKEN;
36
+ if (env && env.trim()) {
37
+ return { token: env.trim(), source: 'env', detail: 'PIPEFY_TOKEN in the environment' };
38
+ }
39
+ const toolkit = await readBearer();
40
+ if (toolkit?.ok) {
41
+ return {
42
+ token: toolkit.token,
43
+ source: `toolkit:${toolkit.source}`,
44
+ detail: toolkit.source === 'stored-session'
45
+ ? `ai-toolkit session${toolkit.issuer ? ` (${toolkit.issuer})` : ''}`
46
+ : `ai-toolkit ${toolkit.source}`,
47
+ };
48
+ }
49
+ if (await exists(credPath())) {
50
+ const c = await readJson(credPath());
51
+ if (c.token)
52
+ return { token: c.token, source: 'file', detail: credPath() };
53
+ }
54
+ if (toolkit && !toolkit.ok) {
55
+ debug(`toolkit holds no credential: ${toolkit.reason}`);
56
+ }
57
+ return null;
58
+ };
59
+ /** Legacy shape, kept for callers that only need a token. */
60
+ export const readToken = async () => (await resolveCredential())?.token ?? null;
61
+ export const requireCredential = async () => {
62
+ const c = await resolveCredential();
63
+ if (!c) {
64
+ const toolkit = await readBearer();
65
+ throw new UserError('No Pipefy credential found.', toolkit === null
66
+ ? `${INSTALL_HINT}. Or run \`pipe login --token <token>\`, or set PIPEFY_TOKEN.`
67
+ : 'run `pipe login` (browser sign-in through the Pipefy AI toolkit), ' +
68
+ '`pipe login --token <token>`, or set PIPEFY_TOKEN.');
69
+ }
70
+ return c;
71
+ };
72
+ export const requireToken = async () => (await requireCredential()).token;
73
+ export const credentialProvider = async () => {
74
+ const initial = await requireCredential();
75
+ let cached = initial;
76
+ const provider = async (opts) => {
77
+ if (!opts?.forceRefresh)
78
+ return cached.token;
79
+ // Only the toolkit can mint a fresh token; a static token that stopped
80
+ // working will not become valid by asking again.
81
+ if (!cached.source.startsWith('toolkit:'))
82
+ return cached.token;
83
+ const next = await resolveCredential();
84
+ if (next) {
85
+ cached = next;
86
+ debug(`credential re-resolved from ${next.source}`);
87
+ }
88
+ return cached.token;
89
+ };
90
+ return { provider, initial };
91
+ };
92
+ export const writeToken = async (token, organizationId) => {
93
+ const p = credPath();
94
+ const current = (await exists(p)) ? await readJson(p) : {};
95
+ await writeJson(p, { ...current, token, ...(organizationId ? { organizationId } : {}) });
96
+ try {
97
+ await fs.chmod(p, 0o600);
98
+ }
99
+ catch {
100
+ /* best effort — Windows ACLs are not chmod */
101
+ }
102
+ return p;
103
+ };
104
+ export const readOrgId = async () => {
105
+ const env = process.env.PIPEFY_ORG_ID ?? process.env.PPC_ORG_ID;
106
+ if (env)
107
+ return env;
108
+ if (await exists(credPath())) {
109
+ const c = await readJson(credPath());
110
+ if (c.organizationId)
111
+ return String(c.organizationId);
112
+ }
113
+ return null;
114
+ };
115
+ export const credentialsPath = credPath;
116
+ /** Tunables that matter when a real client pipe is 50x the test pipe. */
117
+ export const LIMITS = {
118
+ /** Parallel GraphQL/internal reads. Conservative: rate limits are unmeasured. */
119
+ readConcurrency: Number(process.env.PPC_READ_CONCURRENCY ?? 4),
120
+ /** Snapshot generation measured at ~63s (PLAN.md §1.1). */
121
+ snapshotPollMs: Number(process.env.PPC_SNAPSHOT_POLL_MS ?? 5_000),
122
+ snapshotTimeoutMs: Number(process.env.PPC_SNAPSHOT_TIMEOUT_MS ?? 300_000),
123
+ /** Writes are serial by default: ordering is the correctness argument. */
124
+ writeConcurrency: 1,
125
+ };
@@ -0,0 +1,80 @@
1
+ import { canonicalString } from "../util/json.js";
2
+ import { agentDirName } from "../workspace/agents.js";
3
+ const isNewAgent = (a) => !a['uuid'] || a['_new'] === true;
4
+ /** Strip the tool's own routing tag before comparing — never part of the real row. */
5
+ const forCompare = (b) => {
6
+ const { _agent_dir, ...rest } = b;
7
+ return rest;
8
+ };
9
+ const sameBehaviors = (a, b) => {
10
+ const norm = (list) => list.map((x) => canonicalString(forCompare(x))).sort();
11
+ return canonicalString(norm(a)) === canonicalString(norm(b));
12
+ };
13
+ const sameAgentFields = (before, after) => String(before['name'] ?? '') === String(after['name'] ?? '') &&
14
+ String(before['instruction'] ?? '') === String(after['instruction'] ?? '') &&
15
+ canonicalString(before['dataSourceIds'] ?? []) === canonicalString(after['dataSourceIds'] ?? []) &&
16
+ canonicalString(before['disabledAt'] ?? null) === canonicalString(after['disabledAt'] ?? null);
17
+ /**
18
+ * @param authoredBehaviorsByDir The workspace's current behaviors for each
19
+ * agent, grouped by `agentDirName` — from `tree.automations`, filtered to the
20
+ * ones `_agent_dir` tags, and grouped by that tag. `readAgents` alone cannot
21
+ * answer this: `agent.json` never carries `behaviors` (workspace/agents.ts).
22
+ */
23
+ export const diffAgents = (before, after, authoredBehaviorsByDir) => {
24
+ const changes = [];
25
+ const beforeByUuid = new Map((before?.agents ?? []).filter((a) => a['uuid']).map((a) => [String(a['uuid']), a]));
26
+ const matched = new Set();
27
+ for (const a of after?.agents ?? []) {
28
+ const uuid = a['uuid'] ? String(a['uuid']) : null;
29
+ const dir = agentDirName(a);
30
+ const behaviors = authoredBehaviorsByDir.get(dir) ?? [];
31
+ const label = String(a['name'] ?? uuid ?? dir);
32
+ const b = uuid ? beforeByUuid.get(uuid) : undefined;
33
+ if (!b || isNewAgent(a)) {
34
+ changes.push({ key: uuid ? `uuid:${uuid}` : `new:${dir}`, op: 'create', label, after: a, behaviors });
35
+ continue;
36
+ }
37
+ matched.add(uuid);
38
+ if (!sameAgentFields(b, a) || !sameBehaviors(b.behaviors ?? [], behaviors)) {
39
+ changes.push({ key: `uuid:${uuid}`, op: 'update', label, before: b, after: a, behaviors });
40
+ }
41
+ }
42
+ for (const [uuid, b] of beforeByUuid) {
43
+ if (matched.has(uuid))
44
+ continue;
45
+ changes.push({
46
+ key: `uuid:${uuid}`,
47
+ op: 'delete',
48
+ label: String(b['name'] ?? uuid),
49
+ before: b,
50
+ behaviors: [],
51
+ destructive: true,
52
+ });
53
+ }
54
+ return { changes };
55
+ };
56
+ export const renderAgentChanges = (cs) => {
57
+ if (!cs.changes.length)
58
+ return '';
59
+ const lines = [];
60
+ for (const c of cs.changes) {
61
+ const mark = c.op === 'create' ? '+' : c.op === 'delete' ? '-' : '~';
62
+ const tag = c.destructive ? ' [destructive]' : '';
63
+ const behaviorNote = c.op === 'delete' ? '' : ` (${c.behaviors.length} behavior${c.behaviors.length === 1 ? '' : 's'})`;
64
+ lines.push(` ${mark} agents ${c.label}${tag}${behaviorNote}`);
65
+ }
66
+ return lines.join('\n');
67
+ };
68
+ /** This workspace's automations, grouped by which agent's `behaviors/` they came from. */
69
+ export const groupBehaviorsByAgentDir = (automations) => {
70
+ const byDir = new Map();
71
+ for (const a of automations) {
72
+ const dir = a['_agent_dir'];
73
+ if (!dir)
74
+ continue;
75
+ const arr = byDir.get(dir) ?? [];
76
+ arr.push(a);
77
+ byDir.set(dir, arr);
78
+ }
79
+ return byDir;
80
+ };