@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,267 +0,0 @@
1
- import { ENDPOINTS, type TokenProvider } from '../config.ts';
2
- import { debug } from '../util/log.ts';
3
- import { retry } from '../util/pool.ts';
4
-
5
- export type GqlError = { message: string; path?: unknown[]; extensions?: Record<string, unknown> };
6
-
7
- export class GraphQLRequestError extends Error {
8
- errors: GqlError[];
9
- retryable: boolean;
10
- status: number;
11
- constructor(message: string, errors: GqlError[], status: number, retryable: boolean) {
12
- super(message);
13
- this.name = 'GraphQLRequestError';
14
- this.errors = errors;
15
- this.status = status;
16
- this.retryable = retryable;
17
- }
18
- }
19
-
20
- const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
21
-
22
- /** Rate limiting and transient upstream faults are retryable; a bad query is not. */
23
- const isRetryableMessage = (m: string) =>
24
- /rate limit|too many requests|timeout|timed out|temporarily|try again|internal server error/i.test(m);
25
-
26
- export type ClientOpts = {
27
- /**
28
- * A literal token, or a provider. The toolkit hands out short-lived OAuth
29
- * access tokens, so a long apply needs to be able to ask for a fresh one —
30
- * see src/config.ts.
31
- */
32
- token: string | TokenProvider;
33
- endpoint?: string;
34
- label?: string;
35
- };
36
-
37
- export class PipefyClient {
38
- private readonly tokenProvider: TokenProvider;
39
- readonly endpoint: string;
40
- /** Every request, for `--stats` and for measuring the rate-limit ceiling. */
41
- requestCount = 0;
42
- retryCount = 0;
43
- /** Times a 401 forced a credential refresh. Surfaced by `pipe doctor`. */
44
- reauthCount = 0;
45
-
46
- private typeFieldCache = new Map<string, Set<string> | null>();
47
- private inputFieldCache = new Map<string, Set<string> | null>();
48
- private requiredInputCache = new Map<string, Set<string>>();
49
- private mutationInputTypeCache = new Map<string, string | null>();
50
-
51
- constructor(opts: ClientOpts) {
52
- this.tokenProvider = typeof opts.token === 'string' ? async () => opts.token as string : opts.token;
53
- this.endpoint = opts.endpoint ?? ENDPOINTS.graphql;
54
- }
55
-
56
- /** The current bearer, refreshed only when something rejected the last one. */
57
- async token(forceRefresh = false): Promise<string> {
58
- if (forceRefresh) this.reauthCount++;
59
- return this.tokenProvider({ forceRefresh });
60
- }
61
-
62
- async raw<T>(query: string, variables?: Record<string, unknown>, label?: string): Promise<T> {
63
- let refreshed = false;
64
- return retry(
65
- async () => {
66
- this.requestCount++;
67
- const res = await fetch(this.endpoint, {
68
- method: 'POST',
69
- headers: {
70
- authorization: `Bearer ${await this.token()}`,
71
- 'content-type': 'application/json',
72
- accept: 'application/json',
73
- },
74
- body: JSON.stringify({ query, variables: variables ?? {} }),
75
- });
76
-
77
- /**
78
- * An expired OAuth access token looks like a 401. Refresh once and let
79
- * the retry loop send it again; a second 401 is a real permission
80
- * problem and must not spin.
81
- */
82
- if ((res.status === 401 || res.status === 403) && !refreshed) {
83
- refreshed = true;
84
- await this.token(true);
85
- const err = new Error(`HTTP ${res.status} — credential refreshed, retrying`);
86
- (err as { retryable?: boolean }).retryable = true;
87
- throw err;
88
- }
89
-
90
- const text = await res.text();
91
- let body: { data?: T; errors?: GqlError[] } = {};
92
- try {
93
- body = text ? (JSON.parse(text) as typeof body) : {};
94
- } catch {
95
- const retryable = RETRYABLE_STATUS.has(res.status);
96
- throw new GraphQLRequestError(
97
- `HTTP ${res.status} from ${this.endpoint}: ${text.slice(0, 200)}`,
98
- [],
99
- res.status,
100
- retryable,
101
- );
102
- }
103
-
104
- if (body.errors?.length) {
105
- const msg = body.errors.map((e) => e.message).join('; ');
106
- const retryable = RETRYABLE_STATUS.has(res.status) || isRetryableMessage(msg);
107
- throw new GraphQLRequestError(msg, body.errors, res.status, retryable);
108
- }
109
- if (!res.ok) {
110
- throw new GraphQLRequestError(
111
- `HTTP ${res.status} from ${this.endpoint}`,
112
- [],
113
- res.status,
114
- RETRYABLE_STATUS.has(res.status),
115
- );
116
- }
117
- if (body.data === undefined || body.data === null) {
118
- throw new GraphQLRequestError('GraphQL response had no data', [], res.status, false);
119
- }
120
- return body.data;
121
- },
122
- { label: label ?? 'graphql' },
123
- );
124
- }
125
-
126
- /** A query whose failure is a fact about the endpoint, not an error. */
127
- async tryRaw<T>(query: string, variables?: Record<string, unknown>, label?: string): Promise<T | null> {
128
- try {
129
- return await this.raw<T>(query, variables, label);
130
- } catch (e) {
131
- debug(`probe failed (${label ?? 'unnamed'}): ${(e as Error).message}`);
132
- return null;
133
- }
134
- }
135
-
136
- /**
137
- * Which fields a type actually has.
138
- *
139
- * The reconstruction path (reconstruct.ts) asks for columns that PLAN.md marks
140
- * unverified. Rather than sending a query that fails wholesale on one unknown
141
- * field, we introspect once and ask only for what exists. One cheap query
142
- * buys a read path that degrades field-by-field instead of all-or-nothing.
143
- */
144
- async typeFields(typeName: string): Promise<Set<string> | null> {
145
- if (this.typeFieldCache.has(typeName)) return this.typeFieldCache.get(typeName) ?? null;
146
- const data = await this.tryRaw<{ __type: { fields: Array<{ name: string }> | null } | null }>(
147
- 'query($n:String!){ __type(name:$n){ fields{ name } } }',
148
- { n: typeName },
149
- `introspect ${typeName}`,
150
- );
151
- const fields = data?.__type?.fields;
152
- const set = fields ? new Set(fields.map((f) => f.name)) : null;
153
- this.typeFieldCache.set(typeName, set);
154
- return set;
155
- }
156
-
157
- /** Whether a mutation exists — the basis of capability detection (PLAN.md §1.2). */
158
- async hasMutation(name: string): Promise<boolean> {
159
- const fields = await this.typeFields('Mutation');
160
- if (!fields) return false;
161
- return fields.has(name);
162
- }
163
-
164
- /**
165
- * The named type of a mutation's `input` argument.
166
- *
167
- * Never guessed. The registry documents traps like `createFieldConditionInput`
168
- * being lowercase-c while `UpdateFieldConditionInput` is not; asking the schema
169
- * removes a whole class of "mutation exists, input type name wrong" failures.
170
- */
171
- async mutationInputType(mutation: string): Promise<string | null> {
172
- if (this.mutationInputTypeCache.has(mutation)) return this.mutationInputTypeCache.get(mutation) ?? null;
173
- const data = await this.tryRaw<{
174
- __type: {
175
- fields: Array<{
176
- name: string;
177
- args: Array<{ name: string; type: TypeRef }>;
178
- }> | null;
179
- } | null;
180
- }>(
181
- `query {
182
- __type(name: "Mutation") {
183
- fields {
184
- name
185
- args { name type { kind name ofType { kind name ofType { kind name } } } }
186
- }
187
- }
188
- }`,
189
- undefined,
190
- 'introspect Mutation args',
191
- );
192
- const field = data?.__type?.fields?.find((f) => f.name === mutation);
193
- const arg = field?.args.find((a) => a.name === 'input') ?? field?.args[0];
194
- const named = arg ? unwrapTypeName(arg.type) : null;
195
- this.mutationInputTypeCache.set(mutation, named);
196
- return named;
197
- }
198
-
199
- /**
200
- * Input fields that are NON_NULL with no default — the ones a request must
201
- * carry a value for.
202
- *
203
- * CreatePipeRelationInput makes every boolean required, including
204
- * autoFillFieldEnabled, which the snapshot payload has no column for at all.
205
- * Sent without it the mutation is rejected mid-apply; knowing up front turns
206
- * that into something `pipe plan` can say before anything is written.
207
- */
208
- async requiredInputFields(typeName: string): Promise<Set<string>> {
209
- const cached = this.requiredInputCache.get(typeName);
210
- if (cached) return cached;
211
- const data = await this.tryRaw<{
212
- __type: { inputFields: Array<{ name: string; defaultValue: string | null; type: { kind: string } }> | null } | null;
213
- }>(
214
- 'query($n:String!){ __type(name:$n){ inputFields { name defaultValue type { kind } } } }',
215
- { n: typeName },
216
- `introspect required ${typeName}`,
217
- );
218
- const req = new Set<string>();
219
- for (const fld of data?.__type?.inputFields ?? []) {
220
- if (fld.type.kind === 'NON_NULL' && fld.defaultValue === null) req.add(fld.name);
221
- }
222
- this.requiredInputCache.set(typeName, req);
223
- return req;
224
- }
225
-
226
- /** The fields an input object type accepts. */
227
- async inputFields(typeName: string): Promise<Set<string> | null> {
228
- if (this.inputFieldCache.has(typeName)) return this.inputFieldCache.get(typeName) ?? null;
229
- const data = await this.tryRaw<{ __type: { inputFields: Array<{ name: string }> | null } | null }>(
230
- 'query($n:String!){ __type(name:$n){ inputFields{ name } } }',
231
- { n: typeName },
232
- `introspect input ${typeName}`,
233
- );
234
- const fields = data?.__type?.inputFields;
235
- const set = fields ? new Set(fields.map((f) => f.name)) : null;
236
- this.inputFieldCache.set(typeName, set);
237
- return set;
238
- }
239
-
240
- /**
241
- * Filter a wanted selection down to what the type has. `selection` entries may
242
- * be a bare field (`"label"`) or a field with a sub-selection
243
- * (`["connectedRepo", "{ id name }"]`).
244
- */
245
- async prune(typeName: string, selection: Array<string | [string, string]>): Promise<string> {
246
- const have = await this.typeFields(typeName);
247
- const keep: string[] = [];
248
- for (const s of selection) {
249
- const [name, sub] = Array.isArray(s) ? s : [s, ''];
250
- if (have && !have.has(name)) {
251
- debug(`skipping ${typeName}.${name} — not in schema`);
252
- continue;
253
- }
254
- keep.push(sub ? `${name} ${sub}` : name);
255
- }
256
- return keep.join('\n ');
257
- }
258
- }
259
-
260
- type TypeRef = { kind: string; name: string | null; ofType?: TypeRef | null };
261
-
262
- /** Unwrap NON_NULL / LIST wrappers down to the named type. */
263
- const unwrapTypeName = (t: TypeRef | null | undefined): string | null => {
264
- let cur: TypeRef | null | undefined = t;
265
- while (cur && !cur.name) cur = cur.ofType ?? null;
266
- return cur?.name ?? null;
267
- };
@@ -1,209 +0,0 @@
1
- import { debug, step, warn } from '../util/log.ts';
2
- import { rel, type RepoPayload, type Row } from '../model/payload.ts';
3
- import type { PipefyClient } from './client.ts';
4
-
5
- /**
6
- * Connection discovery. PLAN.md §2.2 — the finding that would have silently
7
- * broken the core requirement:
8
- *
9
- * - `relations.pipe_relations` is [] even for a pipe with two live connections
10
- * - `pipeMapGraph(depth: 3)` returns one node and zero edges for that pipe
11
- *
12
- * So neither is the source of truth. Discovery is the union of three sources,
13
- * walked transitively and deduplicated by repo identity.
14
- */
15
-
16
- export type RepoKind = 'pipe' | 'table';
17
-
18
- export type RepoRef = {
19
- kind: RepoKind;
20
- /** Numeric id, which both the pipe and table APIs accept. */
21
- id: number;
22
- /** Table API ids round-trip as a slug (`dzmxcuxz`); pipes return numbers. */
23
- apiId: string;
24
- name?: string | null;
25
- uuid?: string | null;
26
- /** How this repo was found, for the manifest and the UI. */
27
- via: string[];
28
- depth: number;
29
- };
30
-
31
- export type DiscoveryResult = {
32
- /** Every repo to pull, root first. */
33
- repos: RepoRef[];
34
- /** Edges, for the graph view and for MAPPING.md. */
35
- edges: Array<{ from: number; to: number; kind: string; label?: string }>;
36
- warnings: string[];
37
- };
38
-
39
- const asNum = (v: unknown): number | null => {
40
- if (v === null || v === undefined || v === '') return null;
41
- const n = Number(v);
42
- return Number.isFinite(n) ? n : null;
43
- };
44
-
45
- /** The three sources, applied to one already-read payload. */
46
- export const edgesFromPayload = (p: RepoPayload): Array<{ to: number; kind: string; label?: string }> => {
47
- const self = Number(p.id);
48
- const out: Array<{ to: number; kind: string; label?: string }> = [];
49
-
50
- // 1. pipe_relations
51
- for (const r of rel(p, 'pipe_relations')) {
52
- for (const key of ['parent_id', 'child_id']) {
53
- const id = asNum(r[key]);
54
- if (id !== null && id !== self) out.push({ to: id, kind: 'pipe_relation', label: String(r['name'] ?? '') });
55
- }
56
- }
57
-
58
- // 2. connector fields — the only source that finds the test pipe's two
59
- // connections, and the reason this function exists.
60
- for (const f of rel(p, 'fields')) {
61
- if (f['type_id'] !== 'connector') continue;
62
- const id = asNum(f['connected_pipe_id']);
63
- if (id !== null && id !== self) out.push({ to: id, kind: 'connector_field', label: String(f['label'] ?? '') });
64
- }
65
-
66
- // 3. cross-repo automations
67
- for (const a of rel(p, 'automations')) {
68
- for (const key of ['event_repo_id', 'action_repo_id']) {
69
- const id = asNum(a[key]);
70
- if (id !== null && id !== self) out.push({ to: id, kind: 'automation', label: String(a['name'] ?? '') });
71
- }
72
- }
73
-
74
- return out;
75
- };
76
-
77
- /**
78
- * Resolve a numeric repo id to what it actually is.
79
- *
80
- * **Neither root field discriminates.** Verified live: `pipe(id:)` and
81
- * `table(id:)` both answer for *every* repo id, and both return plausible data —
82
- * `pipe.phases` on a database returns its status phases, and `table.table_fields`
83
- * on a pipe returns its start-form fields. A repo is one object viewed two ways.
84
- * Trying `pipe` first and taking the win, as an earlier version did, classified
85
- * every database as a pipe.
86
- *
87
- * `Table.url` is the real discriminator: a pipe lives at `/pipes/<id>` and a
88
- * database at `/apollo_databases/<id>`. One query settles both the kind and
89
- * PLAN.md §2.2's id duality, since `Table` carries the slug as `id` and the
90
- * numeric form as `internal_id`.
91
- */
92
- /**
93
- * Resolve one repo id, in **either** spelling.
94
- *
95
- * `table(id:)` accepts a database's numeric internal id and its api id (a slug
96
- * like `dzmxcuxz`) and always answers with the slug — measured both ways. So
97
- * `pipe pull 302908356` and `pipe pull 3LhkZu_h` must reach the same database,
98
- * and the `RepoRef` this returns normalises to the numeric id either way, which
99
- * is what keeps one database from being pulled twice under two identities.
100
- */
101
- export const resolveRepo = async (client: PipefyClient, id: number | string): Promise<RepoRef | null> => {
102
- const res = await client.tryRaw<{
103
- table: { id: string; internal_id?: string | null; name: string; uuid?: string | null; url?: string | null } | null;
104
- }>(
105
- `query($id: ID!) { table(id: $id) { id internal_id name uuid url } }`,
106
- { id: String(id) },
107
- `resolve repo ${id}`,
108
- );
109
- const repo = res?.table;
110
- if (!repo) return null;
111
-
112
- const url = String(repo.url ?? '');
113
- const kind: RepoKind = /apollo_databases/.test(url) ? 'table' : 'pipe';
114
- debug(`repo ${id} is a ${kind} (${url || 'no url'})`);
115
-
116
- return {
117
- kind,
118
- id: Number(repo.internal_id ?? id),
119
- /** The slug for a database, the numeric string for a pipe. */
120
- apiId: kind === 'table' ? String(repo.id) : String(repo.internal_id ?? id),
121
- name: repo.name,
122
- uuid: repo.uuid ?? null,
123
- via: [],
124
- depth: 0,
125
- };
126
- };
127
-
128
- export type DiscoverOpts = {
129
- depth: number;
130
- /** Read a repo's payload; supplied by the pull command so both read paths work. */
131
- read: (ref: RepoRef) => Promise<RepoPayload | null>;
132
- };
133
-
134
- /**
135
- * Walk outward from a root repo. `read` is injected so discovery works
136
- * identically whether payloads come from snapshots or from reconstruction.
137
- */
138
- export const discover = async (
139
- client: PipefyClient,
140
- rootId: number,
141
- opts: DiscoverOpts,
142
- ): Promise<DiscoveryResult> => {
143
- const found = new Map<number, RepoRef>();
144
- const edges: DiscoveryResult['edges'] = [];
145
- const warnings: string[] = [];
146
-
147
- const root = await resolveRepo(client, rootId);
148
- if (!root) throw new Error(`repo ${rootId} is neither a readable pipe nor a readable table`);
149
- root.via = ['root'];
150
- found.set(rootId, root);
151
-
152
- let frontier: RepoRef[] = [root];
153
- for (let depth = 0; depth <= opts.depth && frontier.length; depth++) {
154
- const next: RepoRef[] = [];
155
- for (const ref of frontier) {
156
- if (depth === opts.depth) break;
157
- const payload = await opts.read(ref);
158
- if (!payload) {
159
- warnings.push(`${ref.kind} ${ref.id} (${ref.name ?? '?'}) could not be read — its connections are unexplored`);
160
- continue;
161
- }
162
- for (const e of edgesFromPayload(payload)) {
163
- edges.push({ from: ref.id, to: e.to, kind: e.kind, label: e.label });
164
- const seen = found.get(e.to);
165
- if (seen) {
166
- if (!seen.via.includes(e.kind)) seen.via.push(e.kind);
167
- continue;
168
- }
169
- const resolved = await resolveRepo(client, e.to);
170
- if (!resolved) {
171
- warnings.push(
172
- `${ref.kind} ${ref.id} references repo ${e.to} via ${e.kind}, but the token cannot read it — ` +
173
- `the pull is partial (PLAN.md §9, cross-repo permission)`,
174
- );
175
- continue;
176
- }
177
- resolved.via = [e.kind];
178
- resolved.depth = depth + 1;
179
- found.set(e.to, resolved);
180
- next.push(resolved);
181
- debug(`discovered ${resolved.kind} ${resolved.id} "${resolved.name}" via ${e.kind}`);
182
- }
183
- }
184
- frontier = next;
185
- }
186
-
187
- const repos = [...found.values()].sort((a, b) => a.depth - b.depth || a.id - b.id);
188
- step(`discovered ${repos.length} repo${repos.length === 1 ? '' : 's'} within depth ${opts.depth}`);
189
- for (const w of warnings) warn(w);
190
- return { repos, edges, warnings };
191
- };
192
-
193
- /**
194
- * `pipeMapGraph` is kept for the graph view and for `hasHiddenAutomations` —
195
- * never as the discovery mechanism. PLAN.md §2.2 and ask 7.
196
- */
197
- export const mapGraph = async (client: PipefyClient, repoId: number, depth = 2) => {
198
- const data = await client.tryRaw<{ pipeMapGraph: unknown }>(
199
- `query($id: ID!, $depth: Int) {
200
- pipeMapGraph(pipeId: $id, depth: $depth) {
201
- nodes { id name __typename hasHiddenAutomations hasHiddenIntegrations }
202
- edges { from to }
203
- }
204
- }`,
205
- { id: String(repoId), depth },
206
- 'pipeMapGraph',
207
- );
208
- return data?.pipeMapGraph ?? null;
209
- };