@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,355 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { mkdirp, writeFile, writeJson, exists } from "../util/fsx.js";
|
|
4
|
+
import { paths } from "../workspace/layout.js";
|
|
5
|
+
import { integrationsFrom, renderIntegrations } from "./integrations.js";
|
|
6
|
+
const stamp = (d = new Date()) => d.toISOString().replace(/[:.]/g, '-').replace(/-\d{3}Z$/, 'Z');
|
|
7
|
+
/**
|
|
8
|
+
* The command as typed, for reproduction — with any token elided. `--token` is
|
|
9
|
+
* the one flag whose value must never reach a file, and `pipe login --token` is
|
|
10
|
+
* a real invocation someone might run inside a workspace.
|
|
11
|
+
*/
|
|
12
|
+
export const invocation = (argv = process.argv.slice(2)) => {
|
|
13
|
+
const parts = [];
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const a = argv[i];
|
|
16
|
+
if (/^--token(=|$)/.test(a)) {
|
|
17
|
+
parts.push('--token <elided>');
|
|
18
|
+
if (!a.includes('=') && argv[i + 1] && !argv[i + 1].startsWith('-'))
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
parts.push(a);
|
|
23
|
+
}
|
|
24
|
+
return `pipe ${parts.join(' ')}`.trim();
|
|
25
|
+
};
|
|
26
|
+
/** `runs/` lives at the workspace root, not under `.ppc/` — these are for reading. */
|
|
27
|
+
export const runsDir = (root) => join(paths(root).root, 'runs');
|
|
28
|
+
export const runDir = (root, id) => join(runsDir(root), id);
|
|
29
|
+
/**
|
|
30
|
+
* Collects what happened while a command runs.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately tolerant: a recorder that throws would turn a successful apply
|
|
33
|
+
* into a failed command, and the record is worth less than the work it
|
|
34
|
+
* describes. Everything here is additive and total.
|
|
35
|
+
*/
|
|
36
|
+
export class RunRecorder {
|
|
37
|
+
id;
|
|
38
|
+
startedAt;
|
|
39
|
+
notes = [];
|
|
40
|
+
warnings = [];
|
|
41
|
+
plan = null;
|
|
42
|
+
verification;
|
|
43
|
+
beforeEnv = null;
|
|
44
|
+
afterEnv = null;
|
|
45
|
+
kind;
|
|
46
|
+
ctx;
|
|
47
|
+
constructor(kind, ctx) {
|
|
48
|
+
this.kind = kind;
|
|
49
|
+
this.ctx = ctx;
|
|
50
|
+
/**
|
|
51
|
+
* `startedAt` is passed in by commands that only learn where the workspace
|
|
52
|
+
* is at the end — a pull cannot name its own output directory until
|
|
53
|
+
* discovery has run, and a run that reports 0.0s for a 60-second snapshot is
|
|
54
|
+
* worse than no timing at all.
|
|
55
|
+
*/
|
|
56
|
+
this.startedAt = ctx.startedAt ?? new Date();
|
|
57
|
+
this.id = `${stamp(this.startedAt)}--${kind}`;
|
|
58
|
+
}
|
|
59
|
+
note(text) {
|
|
60
|
+
this.notes.push(text);
|
|
61
|
+
}
|
|
62
|
+
warn(text) {
|
|
63
|
+
this.warnings.push(text);
|
|
64
|
+
}
|
|
65
|
+
/** The plan is the step list; recording it is how the report gets its detail. */
|
|
66
|
+
fromPlan(plan) {
|
|
67
|
+
this.plan = plan;
|
|
68
|
+
}
|
|
69
|
+
verified(ok, detail) {
|
|
70
|
+
this.verification = { ran: true, ok, ...(detail ? { detail } : {}) };
|
|
71
|
+
}
|
|
72
|
+
/** The payload the run started from, when the command knows it. */
|
|
73
|
+
before(env) {
|
|
74
|
+
this.beforeEnv = env;
|
|
75
|
+
}
|
|
76
|
+
after(env) {
|
|
77
|
+
this.afterEnv = env;
|
|
78
|
+
}
|
|
79
|
+
build(outcome, summary) {
|
|
80
|
+
const finished = new Date();
|
|
81
|
+
const steps = (this.plan?.steps ?? []).map((s) => {
|
|
82
|
+
const started = s.startedAt ? Date.parse(s.startedAt) : NaN;
|
|
83
|
+
const ended = s.finishedAt ? Date.parse(s.finishedAt) : NaN;
|
|
84
|
+
return {
|
|
85
|
+
index: s.index,
|
|
86
|
+
id: s.id,
|
|
87
|
+
description: s.description,
|
|
88
|
+
entity: String(s.entity),
|
|
89
|
+
op: s.op,
|
|
90
|
+
kind: s.kind,
|
|
91
|
+
status: s.status,
|
|
92
|
+
tags: [
|
|
93
|
+
...(s.destructive ? ['destructive'] : []),
|
|
94
|
+
...(s.unverified ? ['unverified'] : []),
|
|
95
|
+
...(s.kind.startsWith('internal') ? ['internal-api'] : []),
|
|
96
|
+
],
|
|
97
|
+
...(s.error ? { error: s.error } : {}),
|
|
98
|
+
...(producedId(s.result) !== undefined ? { produced: producedId(s.result) } : {}),
|
|
99
|
+
...(Number.isFinite(started) && Number.isFinite(ended) ? { durationMs: ended - started } : {}),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const count = (status) => steps.filter((s) => s.status === status).length;
|
|
103
|
+
const env = this.afterEnv;
|
|
104
|
+
return {
|
|
105
|
+
id: this.id,
|
|
106
|
+
kind: this.kind,
|
|
107
|
+
command: this.ctx.command,
|
|
108
|
+
tool: { name: 'pipefy-process-coder', version: '0.1.0' },
|
|
109
|
+
startedAt: this.startedAt.toISOString(),
|
|
110
|
+
finishedAt: finished.toISOString(),
|
|
111
|
+
durationMs: finished.getTime() - this.startedAt.getTime(),
|
|
112
|
+
workspace: this.ctx.workspace,
|
|
113
|
+
repo: this.ctx.repo,
|
|
114
|
+
outcome,
|
|
115
|
+
summary,
|
|
116
|
+
...(this.plan ? { planId: this.plan.id, preApplyVersionId: this.plan.preApplyVersionId ?? null } : {}),
|
|
117
|
+
counts: {
|
|
118
|
+
steps: steps.length,
|
|
119
|
+
done: count('done'),
|
|
120
|
+
failed: count('failed'),
|
|
121
|
+
skipped: count('skipped'),
|
|
122
|
+
pending: count('pending'),
|
|
123
|
+
blocked: this.plan?.blocked.length ?? 0,
|
|
124
|
+
},
|
|
125
|
+
steps,
|
|
126
|
+
blocked: (this.plan?.blocked ?? []).map((b) => ({
|
|
127
|
+
label: b.label,
|
|
128
|
+
entity: String(b.entity),
|
|
129
|
+
op: b.op,
|
|
130
|
+
...(b.reason ? { reason: b.reason } : {}),
|
|
131
|
+
...(b.ask ? { ask: b.ask } : {}),
|
|
132
|
+
})),
|
|
133
|
+
// Deduplicated: a multi-repo pull collects the same coverage note from
|
|
134
|
+
// every tree, and three copies of it read as three problems.
|
|
135
|
+
warnings: [...new Set([...(this.plan?.warnings ?? []), ...this.warnings])],
|
|
136
|
+
notes: [...new Set(this.notes)],
|
|
137
|
+
...(this.verification ? { verification: this.verification } : {}),
|
|
138
|
+
...(env ? { coverage: env.coverage } : {}),
|
|
139
|
+
integrations: {
|
|
140
|
+
...(this.beforeEnv ? { before: integrationsFrom(this.beforeEnv).counts } : {}),
|
|
141
|
+
...(this.afterEnv ? { after: integrationsFrom(this.afterEnv).counts } : {}),
|
|
142
|
+
},
|
|
143
|
+
artifacts: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Write the run folder. Never throws: a failure to record is reported as a
|
|
148
|
+
* note and nothing more, because the alternative is a command that did its
|
|
149
|
+
* work and then reported failure.
|
|
150
|
+
*/
|
|
151
|
+
async write(outcome, summary) {
|
|
152
|
+
try {
|
|
153
|
+
const record = this.build(outcome, summary);
|
|
154
|
+
const dir = runDir(this.ctx.workspace, record.id);
|
|
155
|
+
await mkdirp(dir);
|
|
156
|
+
const written = [];
|
|
157
|
+
const put = async (name, value) => {
|
|
158
|
+
await writeJson(join(dir, name), value);
|
|
159
|
+
written.push(name);
|
|
160
|
+
};
|
|
161
|
+
if (this.afterEnv) {
|
|
162
|
+
await put('pipe.json', this.afterEnv);
|
|
163
|
+
await put('integrations.json', integrationsFrom(this.afterEnv));
|
|
164
|
+
}
|
|
165
|
+
if (this.beforeEnv && this.kind !== 'pull') {
|
|
166
|
+
await put('pipe.before.json', this.beforeEnv);
|
|
167
|
+
await put('integrations.before.json', integrationsFrom(this.beforeEnv));
|
|
168
|
+
}
|
|
169
|
+
record.artifacts = [...written, 'report.md', 'run.json'];
|
|
170
|
+
await writeJson(join(dir, 'run.json'), record);
|
|
171
|
+
await writeFile(join(dir, 'report.md'), renderRunReport(record, this.afterEnv, this.beforeEnv));
|
|
172
|
+
await appendIndex(this.ctx.workspace, record);
|
|
173
|
+
return { dir };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
return { dir: runDir(this.ctx.workspace, this.id), error: e.message };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** The created id, from whatever shape the mutation answered with. */
|
|
181
|
+
const producedId = (result) => {
|
|
182
|
+
if (!result || typeof result !== 'object')
|
|
183
|
+
return undefined;
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
const walk = (v, depth) => {
|
|
186
|
+
if (depth > 4 || !v || typeof v !== 'object' || seen.has(v))
|
|
187
|
+
return undefined;
|
|
188
|
+
seen.add(v);
|
|
189
|
+
const o = v;
|
|
190
|
+
if (typeof o['id'] === 'string' || typeof o['id'] === 'number')
|
|
191
|
+
return o['id'];
|
|
192
|
+
for (const val of Object.values(o)) {
|
|
193
|
+
const hit = walk(val, depth + 1);
|
|
194
|
+
if (hit !== undefined)
|
|
195
|
+
return hit;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
};
|
|
199
|
+
return walk(result, 0);
|
|
200
|
+
};
|
|
201
|
+
const OUTCOME_WORD = {
|
|
202
|
+
ok: 'succeeded',
|
|
203
|
+
partial: 'partially applied',
|
|
204
|
+
failed: 'failed',
|
|
205
|
+
refused: 'refused before sending anything',
|
|
206
|
+
'no-op': 'had nothing to do',
|
|
207
|
+
};
|
|
208
|
+
export const renderRunReport = (r, after, before) => {
|
|
209
|
+
const L = [];
|
|
210
|
+
const secs = (r.durationMs / 1000).toFixed(1);
|
|
211
|
+
L.push(`# ${r.kind} — ${r.repo.name} (${r.repo.id})`, '');
|
|
212
|
+
L.push(`**${OUTCOME_WORD[r.outcome]}** · ${r.summary}`, '');
|
|
213
|
+
L.push('| | |', '| --- | --- |');
|
|
214
|
+
L.push(`| Run | \`${r.id}\` |`);
|
|
215
|
+
L.push(`| Command | \`${r.command}\` |`);
|
|
216
|
+
L.push(`| Started | ${r.startedAt} |`);
|
|
217
|
+
L.push(`| Duration | ${secs}s |`);
|
|
218
|
+
L.push(`| Workspace | \`${r.workspace}\` |`);
|
|
219
|
+
if (r.planId)
|
|
220
|
+
L.push(`| Plan | \`${r.planId}\` |`);
|
|
221
|
+
if (r.preApplyVersionId)
|
|
222
|
+
L.push(`| Pre-apply snapshot | \`${r.preApplyVersionId}\` |`);
|
|
223
|
+
if (after)
|
|
224
|
+
L.push(`| Read path | ${after.source}${after.versionId ? ` \`${after.versionId.slice(0, 8)}\`` : ''} |`);
|
|
225
|
+
if (r.counts.steps) {
|
|
226
|
+
L.push(`| Steps | ${r.counts.steps} — ${r.counts.done} done, ${r.counts.failed} failed, ` +
|
|
227
|
+
`${r.counts.skipped} skipped${r.counts.pending ? `, ${r.counts.pending} never attempted` : ''} |`);
|
|
228
|
+
}
|
|
229
|
+
if (r.verification)
|
|
230
|
+
L.push(`| Verification | ${r.verification.ok ? 'clean' : 'found differences'} |`);
|
|
231
|
+
L.push('');
|
|
232
|
+
if (r.steps.length) {
|
|
233
|
+
L.push('## Steps', '');
|
|
234
|
+
L.push('| # | Step | Entity | Op | Status | Result |', '| --- | --- | --- | --- | --- | --- |');
|
|
235
|
+
for (const s of r.steps) {
|
|
236
|
+
const result = s.error
|
|
237
|
+
? `\`${s.error.replace(/\|/g, '\\|').slice(0, 160)}\``
|
|
238
|
+
: s.produced !== undefined
|
|
239
|
+
? `id ${s.produced}`
|
|
240
|
+
: s.status === 'done'
|
|
241
|
+
? 'applied'
|
|
242
|
+
: '—';
|
|
243
|
+
const tags = s.tags.length ? ` <sub>${s.tags.join(' ')}</sub>` : '';
|
|
244
|
+
L.push(`| ${s.index} | ${s.description.replace(/\|/g, '\\|')}${tags} | ${s.entity} | ${s.op} | ` +
|
|
245
|
+
`${statusMark(s.status)} | ${result} |`);
|
|
246
|
+
}
|
|
247
|
+
L.push('');
|
|
248
|
+
const failures = r.steps.filter((s) => s.status === 'failed' || s.status === 'skipped');
|
|
249
|
+
if (failures.length) {
|
|
250
|
+
L.push('### What did not land', '');
|
|
251
|
+
for (const s of failures) {
|
|
252
|
+
L.push(`- **${s.description}** (${s.status}) — ${s.error ?? 'no reason recorded'}`);
|
|
253
|
+
}
|
|
254
|
+
L.push('');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (r.blocked.length) {
|
|
258
|
+
L.push('## Refused before sending', '');
|
|
259
|
+
L.push('These changes are in the workspace and have no write path. Nothing was attempted.', '');
|
|
260
|
+
for (const b of r.blocked) {
|
|
261
|
+
L.push(`- **${b.label}** — ${b.reason ?? 'no write path'}${b.ask ? ` (ask ${b.ask})` : ''}`);
|
|
262
|
+
}
|
|
263
|
+
L.push('');
|
|
264
|
+
}
|
|
265
|
+
if (r.warnings.length) {
|
|
266
|
+
L.push('## Warnings', '');
|
|
267
|
+
for (const w of r.warnings)
|
|
268
|
+
L.push(`- ${w}`);
|
|
269
|
+
L.push('');
|
|
270
|
+
}
|
|
271
|
+
if (after) {
|
|
272
|
+
const t = after.payload.relations;
|
|
273
|
+
L.push('## The pipe, as of the end of this run', '');
|
|
274
|
+
L.push('| Group | Count | Coverage |', '| --- | --- | --- |');
|
|
275
|
+
for (const [name, rows] of Object.entries(t)) {
|
|
276
|
+
const cov = (r.coverage ?? {})[name] ?? 'unknown';
|
|
277
|
+
if (!rows.length && cov === 'complete')
|
|
278
|
+
continue;
|
|
279
|
+
L.push(`| ${name} | ${rows.length} | ${cov}${cov === 'complete' ? '' : ' ⚠'} |`);
|
|
280
|
+
}
|
|
281
|
+
L.push('');
|
|
282
|
+
const gaps = Object.entries(r.coverage ?? {}).filter(([, c]) => c !== 'complete');
|
|
283
|
+
if (gaps.length) {
|
|
284
|
+
L.push(`⚠ ${gaps.length} group${gaps.length === 1 ? '' : 's'} not fully read. ` +
|
|
285
|
+
'A count of 0 there means "not read", not "empty".', '');
|
|
286
|
+
}
|
|
287
|
+
L.push('## Integrations', '');
|
|
288
|
+
if (before) {
|
|
289
|
+
const b = integrationsFrom(before).counts;
|
|
290
|
+
const a = integrationsFrom(after).counts;
|
|
291
|
+
const moved = Object.keys(a).filter((k) => a[k] !== b[k]);
|
|
292
|
+
if (moved.length) {
|
|
293
|
+
L.push(`Changed by this run: ${moved.map((k) => `${k} ${b[k]} → ${a[k]}`).join(', ')}.`, '');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
L.push(...renderIntegrations(integrationsFrom(after)));
|
|
297
|
+
L.push('');
|
|
298
|
+
}
|
|
299
|
+
if (r.notes.length) {
|
|
300
|
+
L.push('## Notes', '');
|
|
301
|
+
for (const n of r.notes)
|
|
302
|
+
L.push(`- ${n}`);
|
|
303
|
+
L.push('');
|
|
304
|
+
}
|
|
305
|
+
L.push('---', '', `Artifacts in this folder: ${r.artifacts.map((a) => `\`${a}\``).join(', ')}.`);
|
|
306
|
+
L.push('', '`pipe.json` and `integrations.json` are full copies of the pipe and include ' +
|
|
307
|
+
'webhook URLs and HTTP-request credentials. This report does not.');
|
|
308
|
+
return L.join('\n') + '\n';
|
|
309
|
+
};
|
|
310
|
+
const statusMark = (status) => status === 'done' ? 'done' : status === 'failed' ? '**failed**' : status === 'skipped' ? 'skipped' : status;
|
|
311
|
+
/**
|
|
312
|
+
* One line per run in `runs/index.md`, newest last.
|
|
313
|
+
*
|
|
314
|
+
* Appended rather than regenerated so that a run folder someone has deleted or
|
|
315
|
+
* moved does not silently vanish from the history.
|
|
316
|
+
*/
|
|
317
|
+
const appendIndex = async (root, r) => {
|
|
318
|
+
const path = join(runsDir(root), 'index.md');
|
|
319
|
+
const header = [
|
|
320
|
+
'# Runs',
|
|
321
|
+
'',
|
|
322
|
+
'Every invocation that read or wrote this pipe, oldest first. Each row links to',
|
|
323
|
+
'the run folder, which holds the report, the payload, and the integration surface',
|
|
324
|
+
'as of the end of that run.',
|
|
325
|
+
'',
|
|
326
|
+
'| When | Run | Kind | Outcome | Steps | Summary |',
|
|
327
|
+
'| --- | --- | --- | --- | --- | --- |',
|
|
328
|
+
].join('\n');
|
|
329
|
+
if (!(await exists(path)))
|
|
330
|
+
await writeFile(path, header + '\n');
|
|
331
|
+
const row = `| ${r.startedAt.replace('T', ' ').slice(0, 19)} | [${r.id}](${r.id}/report.md) | ${r.kind} | ` +
|
|
332
|
+
`${r.outcome} | ${r.counts.steps ? `${r.counts.done}/${r.counts.steps}` : '—'} | ` +
|
|
333
|
+
`${r.summary.replace(/\|/g, '\\|')} |\n`;
|
|
334
|
+
await fs.appendFile(path, row, 'utf8');
|
|
335
|
+
};
|
|
336
|
+
/** Read every run record in a workspace, oldest first. */
|
|
337
|
+
export const listRuns = async (root) => {
|
|
338
|
+
const dir = runsDir(root);
|
|
339
|
+
if (!(await exists(dir)))
|
|
340
|
+
return [];
|
|
341
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
342
|
+
const out = [];
|
|
343
|
+
for (const e of entries) {
|
|
344
|
+
if (!e.isDirectory())
|
|
345
|
+
continue;
|
|
346
|
+
try {
|
|
347
|
+
out.push(JSON.parse(await fs.readFile(join(dir, e.name, 'run.json'), 'utf8')));
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// A run folder without a readable run.json is skipped, not fatal: the
|
|
351
|
+
// report beside it may still be the thing someone wants to read.
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
355
|
+
};
|
package/dist/util/fsx.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { pretty } from "./json.js";
|
|
4
|
+
export const mkdirp = (p) => fs.mkdir(p, { recursive: true });
|
|
5
|
+
export const writeFile = async (p, content) => {
|
|
6
|
+
await mkdirp(dirname(p));
|
|
7
|
+
await fs.writeFile(p, content, 'utf8');
|
|
8
|
+
};
|
|
9
|
+
export const writeJson = (p, v) => writeFile(p, pretty(v));
|
|
10
|
+
export const readJson = async (p) => {
|
|
11
|
+
const raw = await fs.readFile(p, 'utf8');
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
throw new Error(`${p}: invalid JSON - ${e.message}`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export const exists = async (p) => {
|
|
20
|
+
try {
|
|
21
|
+
await fs.stat(p);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Every .json file under a directory, recursively, sorted. */
|
|
29
|
+
export const listJson = async (dir) => {
|
|
30
|
+
const acc = [];
|
|
31
|
+
const walk = async (d) => {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = (await fs.readdir(d, { withFileTypes: true }));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
const full = join(d, e.name);
|
|
41
|
+
if (e.isDirectory())
|
|
42
|
+
await walk(full);
|
|
43
|
+
else if (e.name.endsWith('.json'))
|
|
44
|
+
acc.push(full);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
await walk(dir);
|
|
48
|
+
return acc.sort();
|
|
49
|
+
};
|
|
50
|
+
export const rmrf = (p) => fs.rm(p, { recursive: true, force: true });
|
package/dist/util/git.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Best-effort, and never throws: a workspace with no local version control is
|
|
6
|
+
* an advisory, not a blocker. `pipe rollback` only restores the *latest*
|
|
7
|
+
* snapshot (commands/misc.ts), so it stops covering a rollback the moment
|
|
8
|
+
* anything re-snapshots the repo — which a normal `pipe apply` does on its own
|
|
9
|
+
* read-back. A local git history is what covers the gap.
|
|
10
|
+
*/
|
|
11
|
+
export const checkGit = async (root) => {
|
|
12
|
+
try {
|
|
13
|
+
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root });
|
|
14
|
+
return { installed: true, tracked: true };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
try {
|
|
18
|
+
await execFileAsync('git', ['--version']);
|
|
19
|
+
return { installed: true, tracked: false };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { installed: false, tracked: false };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON. PLAN.md §3: the round-trip exit criterion is *semantic*
|
|
3
|
+
* equality under a canonical form, never byte equality — `JSON.parse` turns
|
|
4
|
+
* `index: 6.0` into `6`, and fractional indices invite float noise.
|
|
5
|
+
*/
|
|
6
|
+
/** Normalise a number to a stable representation. -0 -> 0, 6.0 -> 6. */
|
|
7
|
+
const num = (n) => {
|
|
8
|
+
if (!Number.isFinite(n))
|
|
9
|
+
return n;
|
|
10
|
+
if (Object.is(n, -0))
|
|
11
|
+
return 0;
|
|
12
|
+
// Collapse float noise past 10 significant decimals; index and lateness are
|
|
13
|
+
// the only fractional numbers in the payload and never need more.
|
|
14
|
+
const r = Number(n.toFixed(10));
|
|
15
|
+
return Object.is(r, -0) ? 0 : r;
|
|
16
|
+
};
|
|
17
|
+
export const canonical = (v) => {
|
|
18
|
+
if (v === null || v === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof v === 'number')
|
|
21
|
+
return num(v);
|
|
22
|
+
if (typeof v === 'boolean' || typeof v === 'string')
|
|
23
|
+
return v;
|
|
24
|
+
if (Array.isArray(v))
|
|
25
|
+
return v.map(canonical);
|
|
26
|
+
if (typeof v === 'object') {
|
|
27
|
+
const o = v;
|
|
28
|
+
const res = {};
|
|
29
|
+
for (const k of Object.keys(o).sort()) {
|
|
30
|
+
if (o[k] === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
res[k] = canonical(o[k]);
|
|
33
|
+
}
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
return String(v);
|
|
37
|
+
};
|
|
38
|
+
export const canonicalString = (v) => JSON.stringify(canonical(v));
|
|
39
|
+
export const deepEqual = (a, b) => canonicalString(a) === canonicalString(b);
|
|
40
|
+
/** Stable pretty JSON for files a human and Claude will read and edit. */
|
|
41
|
+
export const pretty = (v) => JSON.stringify(v, null, 2) + '\n';
|
|
42
|
+
export const clone = (v) => (v === undefined ? v : JSON.parse(JSON.stringify(v)));
|
|
43
|
+
/** Walk every string in a structure, replacing it. Used by the %{id} rewriter. */
|
|
44
|
+
export const mapStrings = (v, fn) => {
|
|
45
|
+
if (typeof v === 'string')
|
|
46
|
+
return fn(v);
|
|
47
|
+
if (Array.isArray(v))
|
|
48
|
+
return v.map((x) => mapStrings(x, fn));
|
|
49
|
+
if (v && typeof v === 'object') {
|
|
50
|
+
const res = {};
|
|
51
|
+
for (const [k, val] of Object.entries(v))
|
|
52
|
+
res[k] = mapStrings(val, fn);
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
55
|
+
return v;
|
|
56
|
+
};
|
package/dist/util/log.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const ESC = '\x1b[';
|
|
2
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
3
|
+
const c = (code) => (s) => (useColor ? `${ESC}${code}m${s}${ESC}0m` : s);
|
|
4
|
+
export const dim = c('2');
|
|
5
|
+
export const bold = c('1');
|
|
6
|
+
export const red = c('31');
|
|
7
|
+
export const green = c('32');
|
|
8
|
+
export const yellow = c('33');
|
|
9
|
+
export const blue = c('36');
|
|
10
|
+
export const magenta = c('35');
|
|
11
|
+
let verbose = false;
|
|
12
|
+
export const setVerbose = (v) => { verbose = v; };
|
|
13
|
+
export const isVerbose = () => verbose;
|
|
14
|
+
let sink = null;
|
|
15
|
+
export const claimLogSink = (s) => {
|
|
16
|
+
sink = s;
|
|
17
|
+
};
|
|
18
|
+
export const hasLogSink = () => sink !== null;
|
|
19
|
+
/** Colour is for a terminal. A report is a file, so it gets none. */
|
|
20
|
+
export const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, '');
|
|
21
|
+
export const out = (s = '') => process.stdout.write(s + '\n');
|
|
22
|
+
export const info = (s) => {
|
|
23
|
+
if (sink)
|
|
24
|
+
sink.note(s);
|
|
25
|
+
else
|
|
26
|
+
process.stderr.write(s + '\n');
|
|
27
|
+
};
|
|
28
|
+
export const debug = (s) => { if (verbose)
|
|
29
|
+
process.stderr.write(dim('· ' + s) + '\n'); };
|
|
30
|
+
export const warn = (s) => {
|
|
31
|
+
if (sink)
|
|
32
|
+
sink.note(yellow('! ') + s);
|
|
33
|
+
else
|
|
34
|
+
process.stderr.write(yellow('! ') + s + '\n');
|
|
35
|
+
};
|
|
36
|
+
export const error = (s) => {
|
|
37
|
+
if (sink)
|
|
38
|
+
sink.note(red('x ') + s);
|
|
39
|
+
else
|
|
40
|
+
process.stderr.write(red('x ') + s + '\n');
|
|
41
|
+
};
|
|
42
|
+
export const ok = (s) => {
|
|
43
|
+
if (sink)
|
|
44
|
+
sink.note(green('+ ') + s);
|
|
45
|
+
else
|
|
46
|
+
process.stderr.write(green('+ ') + s + '\n');
|
|
47
|
+
};
|
|
48
|
+
export const step = (s) => {
|
|
49
|
+
if (sink)
|
|
50
|
+
return;
|
|
51
|
+
process.stderr.write(blue('> ') + s + '\n');
|
|
52
|
+
};
|
|
53
|
+
const CLEAR_LINE = `\r${ESC}2K`;
|
|
54
|
+
/** Single-line progress that collapses to nothing on a non-tty. */
|
|
55
|
+
export const progress = (s) => {
|
|
56
|
+
if (!process.stderr.isTTY)
|
|
57
|
+
return;
|
|
58
|
+
process.stderr.write(CLEAR_LINE + dim(' ' + s));
|
|
59
|
+
};
|
|
60
|
+
export const progressDone = () => { if (process.stderr.isTTY)
|
|
61
|
+
process.stderr.write(CLEAR_LINE); };
|
|
62
|
+
export class UserError extends Error {
|
|
63
|
+
hint;
|
|
64
|
+
constructor(message, hint) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'UserError';
|
|
67
|
+
this.hint = hint;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { debug } from "./log.js";
|
|
2
|
+
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3
|
+
/** Bounded-concurrency map that preserves input order. */
|
|
4
|
+
export const mapPool = async (items, limit, fn) => {
|
|
5
|
+
const results = new Array(items.length);
|
|
6
|
+
let next = 0;
|
|
7
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
8
|
+
for (;;) {
|
|
9
|
+
const i = next++;
|
|
10
|
+
if (i >= items.length)
|
|
11
|
+
return;
|
|
12
|
+
results[i] = await fn(items[i], i);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
await Promise.all(workers);
|
|
16
|
+
return results;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Exponential backoff with jitter. PLAN.md §9: rate limits are unmeasured, so
|
|
20
|
+
* every network call goes through this and every retry is logged - which is how
|
|
21
|
+
* we measure the ceiling instead of discovering it during a client apply.
|
|
22
|
+
*/
|
|
23
|
+
export const retry = async (fn, opts = {}) => {
|
|
24
|
+
const tries = opts.tries ?? 5;
|
|
25
|
+
const base = opts.baseMs ?? 600;
|
|
26
|
+
let lastErr;
|
|
27
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return await fn();
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
lastErr = e;
|
|
33
|
+
const retryable = e.retryable !== false;
|
|
34
|
+
if (!retryable || attempt === tries)
|
|
35
|
+
break;
|
|
36
|
+
const wait = Math.round(base * 2 ** (attempt - 1) * (0.7 + Math.random() * 0.6));
|
|
37
|
+
debug(`retry ${attempt}/${tries - 1} in ${wait}ms${opts.label ? ` (${opts.label})` : ''}: ${e.message}`);
|
|
38
|
+
await sleep(wait);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw lastErr;
|
|
42
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Filesystem-safe, stable, human-readable name segment. */
|
|
2
|
+
export const slug = (s, fallback = 'untitled') => {
|
|
3
|
+
const base = (s ?? '').normalize('NFKD').replace(/[̀-ͯ]/g, '');
|
|
4
|
+
const out = base
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/['"`]/g, '')
|
|
7
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
8
|
+
.replace(/^-+|-+$/g, '')
|
|
9
|
+
.slice(0, 60)
|
|
10
|
+
.replace(/-+$/g, '');
|
|
11
|
+
return out || fallback;
|
|
12
|
+
};
|
|
13
|
+
/** `01--new-request` - index-prefixed so directory order matches pipe order. */
|
|
14
|
+
export const indexed = (i, name) => `${String(Math.trunc(i)).padStart(2, '0')}--${slug(name)}`;
|
|
15
|
+
/** Disambiguate repeated slugs deterministically: name, name-2, name-3. */
|
|
16
|
+
export const uniquify = () => {
|
|
17
|
+
const seen = new Map();
|
|
18
|
+
return (s) => {
|
|
19
|
+
const n = (seen.get(s) ?? 0) + 1;
|
|
20
|
+
seen.set(s, n);
|
|
21
|
+
return n === 1 ? s : `${s}-${n}`;
|
|
22
|
+
};
|
|
23
|
+
};
|