@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
package/dist/util/tui.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { bold, claimLogSink, dim, green, red, yellow, blue, magenta } from "./log.js";
|
|
2
|
+
/**
|
|
3
|
+
* A small live progress display for the long commands — pull, apply, dry-run.
|
|
4
|
+
*
|
|
5
|
+
* Two renderers behind one interface, chosen by whether stderr is a terminal:
|
|
6
|
+
*
|
|
7
|
+
* - **live**: redraws a block of lines in place, with a spinner and elapsed
|
|
8
|
+
* times, so a 63-second snapshot wait looks like work rather than a hang.
|
|
9
|
+
* - **plain**: one line per state change, no cursor control, no repainting.
|
|
10
|
+
* This is what CI, `2>file`, and a backgrounded run get — and it is not an
|
|
11
|
+
* afterthought, since that is how this tool is most often watched.
|
|
12
|
+
*
|
|
13
|
+
* Everything goes to stderr, leaving stdout clean for `--json`.
|
|
14
|
+
*/
|
|
15
|
+
const ESC = '\x1b[';
|
|
16
|
+
const HIDE_CURSOR = `${ESC}?25l`;
|
|
17
|
+
const SHOW_CURSOR = `${ESC}?25h`;
|
|
18
|
+
const CLEAR_LINE = `${ESC}2K`;
|
|
19
|
+
const cursorUp = (n) => (n > 0 ? `${ESC}${n}A` : '');
|
|
20
|
+
/** Braille spinner: 8 frames, reads as motion even at 80ms. */
|
|
21
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
|
|
22
|
+
const MARK = {
|
|
23
|
+
pending: dim,
|
|
24
|
+
running: blue,
|
|
25
|
+
done: green,
|
|
26
|
+
failed: red,
|
|
27
|
+
skipped: yellow,
|
|
28
|
+
};
|
|
29
|
+
const GLYPH = {
|
|
30
|
+
pending: '·',
|
|
31
|
+
running: '',
|
|
32
|
+
done: '+',
|
|
33
|
+
failed: 'x',
|
|
34
|
+
skipped: '-',
|
|
35
|
+
};
|
|
36
|
+
const secs = (ms) => `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
37
|
+
export class Progress {
|
|
38
|
+
tasks = [];
|
|
39
|
+
byId = new Map();
|
|
40
|
+
title;
|
|
41
|
+
stream;
|
|
42
|
+
live;
|
|
43
|
+
frame = 0;
|
|
44
|
+
timer = null;
|
|
45
|
+
linesDrawn = 0;
|
|
46
|
+
startedAt = Date.now();
|
|
47
|
+
finished = false;
|
|
48
|
+
/** Extra lines under the task list: warnings that must survive a repaint. */
|
|
49
|
+
notes = [];
|
|
50
|
+
cleanup = null;
|
|
51
|
+
/**
|
|
52
|
+
* Renders are coalesced onto the spinner tick rather than run on every state
|
|
53
|
+
* change. Building a 200-step plan calls add() 200 times, and repainting the
|
|
54
|
+
* whole block each time is both wasted work and visible flicker; a 90ms delay
|
|
55
|
+
* before a change appears is not perceptible.
|
|
56
|
+
*/
|
|
57
|
+
dirty = false;
|
|
58
|
+
constructor(opts) {
|
|
59
|
+
this.title = opts.title;
|
|
60
|
+
this.stream = opts.stream ?? process.stderr;
|
|
61
|
+
this.live = !opts.plain && Boolean(this.stream.isTTY) && !process.env.PPC_NO_TUI && !process.env.NO_TUI;
|
|
62
|
+
}
|
|
63
|
+
/** Begin painting. Safe to call once; a second call is ignored. */
|
|
64
|
+
start() {
|
|
65
|
+
if (this.timer || this.finished)
|
|
66
|
+
return;
|
|
67
|
+
/**
|
|
68
|
+
* Claimed in both modes, so the semantics do not depend on whether stderr
|
|
69
|
+
* is a terminal. Live, it stops an interleaved write garbling a repaint;
|
|
70
|
+
* plain, it stops the task list and the old progress chatter saying the same
|
|
71
|
+
* thing twice.
|
|
72
|
+
*/
|
|
73
|
+
claimLogSink({ note: (text) => this.note(text) });
|
|
74
|
+
if (!this.live) {
|
|
75
|
+
this.stream.write(bold(this.title) + '\n');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.stream.write(HIDE_CURSOR);
|
|
79
|
+
// Leaving the cursor hidden after a Ctrl-C would break the user's terminal.
|
|
80
|
+
const restore = () => this.stop();
|
|
81
|
+
process.once('SIGINT', restore);
|
|
82
|
+
process.once('SIGTERM', restore);
|
|
83
|
+
process.once('exit', restore);
|
|
84
|
+
this.cleanup = () => {
|
|
85
|
+
process.off('SIGINT', restore);
|
|
86
|
+
process.off('SIGTERM', restore);
|
|
87
|
+
process.off('exit', restore);
|
|
88
|
+
};
|
|
89
|
+
this.timer = setInterval(() => {
|
|
90
|
+
this.frame++;
|
|
91
|
+
// The spinner has to move even when nothing changed, so a running task
|
|
92
|
+
// always repaints; an idle list repaints only when it is stale.
|
|
93
|
+
if (this.dirty || this.tasks.some((t) => t.state === 'running'))
|
|
94
|
+
this.render();
|
|
95
|
+
}, 90);
|
|
96
|
+
this.timer.unref?.();
|
|
97
|
+
this.render();
|
|
98
|
+
}
|
|
99
|
+
add(task) {
|
|
100
|
+
const t = { state: 'pending', ...task };
|
|
101
|
+
this.tasks.push(t);
|
|
102
|
+
this.byId.set(t.id, t);
|
|
103
|
+
this.touch();
|
|
104
|
+
return t;
|
|
105
|
+
}
|
|
106
|
+
update(id, patch) {
|
|
107
|
+
const t = this.byId.get(id);
|
|
108
|
+
if (!t)
|
|
109
|
+
return;
|
|
110
|
+
const before = t.state;
|
|
111
|
+
Object.assign(t, patch);
|
|
112
|
+
if (patch.state === 'running' && !t.startedAt) {
|
|
113
|
+
t.startedAt = Date.now();
|
|
114
|
+
// Seeded so the first heartbeat is one interval away, not immediate:
|
|
115
|
+
// otherwise every task announces its first stage and fast work is noise.
|
|
116
|
+
this.lastHeartbeat.set(t.id, t.startedAt);
|
|
117
|
+
}
|
|
118
|
+
if (patch.state && patch.state !== 'running' && !t.endedAt)
|
|
119
|
+
t.endedAt = Date.now();
|
|
120
|
+
if (!this.live) {
|
|
121
|
+
if (patch.state && patch.state !== before)
|
|
122
|
+
this.onPlainTransition(t);
|
|
123
|
+
else if (patch.detail !== undefined)
|
|
124
|
+
this.plainHeartbeat(t);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.touch();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* One line per task in plain mode, plus a heartbeat for slow work.
|
|
132
|
+
*
|
|
133
|
+
* Every task reports its outcome, so nothing starts and then vanishes from the
|
|
134
|
+
* log. Work that announces a stage while running — a snapshot poll, a download
|
|
135
|
+
* — also reports that, throttled, because a log that goes silent for 63
|
|
136
|
+
* seconds is indistinguishable from a hang. A 200-step plan of fast calls is
|
|
137
|
+
* 200 lines.
|
|
138
|
+
*/
|
|
139
|
+
static HEARTBEAT_MS = 15_000;
|
|
140
|
+
lastHeartbeat = new Map();
|
|
141
|
+
onPlainTransition(t) {
|
|
142
|
+
// Terminal states always speak.
|
|
143
|
+
if (t.state === 'done' || t.state === 'failed' || t.state === 'skipped')
|
|
144
|
+
this.emitPlain(t);
|
|
145
|
+
}
|
|
146
|
+
plainHeartbeat(t) {
|
|
147
|
+
if (this.live || t.state !== 'running')
|
|
148
|
+
return;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const last = this.lastHeartbeat.get(t.id) ?? 0;
|
|
151
|
+
if (now - last < Progress.HEARTBEAT_MS)
|
|
152
|
+
return;
|
|
153
|
+
this.lastHeartbeat.set(t.id, now);
|
|
154
|
+
this.emitPlain(t);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Mark the frame stale. Before start() there is no timer, so paint at once —
|
|
158
|
+
* that is what makes a short-lived Progress still show something.
|
|
159
|
+
*/
|
|
160
|
+
touch() {
|
|
161
|
+
if (!this.live)
|
|
162
|
+
return;
|
|
163
|
+
this.dirty = true;
|
|
164
|
+
if (!this.timer)
|
|
165
|
+
this.render();
|
|
166
|
+
}
|
|
167
|
+
/** Whether a task with this id already exists. */
|
|
168
|
+
has(id) {
|
|
169
|
+
return this.byId.has(id);
|
|
170
|
+
}
|
|
171
|
+
/** The last few only: a frame that grows without bound scrolls the terminal. */
|
|
172
|
+
static MAX_NOTES = 5;
|
|
173
|
+
/** A line that stays visible under the task list. */
|
|
174
|
+
note(text) {
|
|
175
|
+
this.notes.push(text);
|
|
176
|
+
if (this.notes.length > Progress.MAX_NOTES)
|
|
177
|
+
this.notes.splice(0, this.notes.length - Progress.MAX_NOTES);
|
|
178
|
+
if (!this.live)
|
|
179
|
+
this.stream.write(text + '\n');
|
|
180
|
+
else
|
|
181
|
+
this.render();
|
|
182
|
+
}
|
|
183
|
+
/** Stop painting and leave the final frame on screen. */
|
|
184
|
+
stop(summary) {
|
|
185
|
+
if (this.finished)
|
|
186
|
+
return;
|
|
187
|
+
this.finished = true;
|
|
188
|
+
if (this.timer)
|
|
189
|
+
clearInterval(this.timer);
|
|
190
|
+
this.timer = null;
|
|
191
|
+
this.cleanup?.();
|
|
192
|
+
claimLogSink(null);
|
|
193
|
+
if (this.live) {
|
|
194
|
+
this.render(true);
|
|
195
|
+
this.stream.write(SHOW_CURSOR);
|
|
196
|
+
}
|
|
197
|
+
if (summary)
|
|
198
|
+
this.stream.write(summary + '\n');
|
|
199
|
+
}
|
|
200
|
+
get elapsed() {
|
|
201
|
+
return Date.now() - this.startedAt;
|
|
202
|
+
}
|
|
203
|
+
// ── rendering ─────────────────────────────────────────────────────────────
|
|
204
|
+
emitPlain(t) {
|
|
205
|
+
const paint = MARK[t.state];
|
|
206
|
+
const indent = ' '.repeat(t.depth ?? 0);
|
|
207
|
+
const tail = t.detail ? ` ${dim(t.detail)}` : '';
|
|
208
|
+
const took = t.startedAt && t.endedAt ? ` ${dim(secs(t.endedAt - t.startedAt))}` : '';
|
|
209
|
+
this.stream.write(`${indent}${paint(GLYPH[t.state] || '>')} ${t.label}${tail}${took}\n`);
|
|
210
|
+
}
|
|
211
|
+
line(t) {
|
|
212
|
+
const paint = MARK[t.state];
|
|
213
|
+
const glyph = t.state === 'running' ? blue(FRAMES[this.frame % FRAMES.length]) : paint(GLYPH[t.state]);
|
|
214
|
+
const indent = ' '.repeat(t.depth ?? 0);
|
|
215
|
+
const label = t.state === 'pending' ? dim(t.label) : t.label;
|
|
216
|
+
const tags = t.tags?.length ? ' ' + t.tags.map((g) => dim(`[${g}]`)).join(' ') : '';
|
|
217
|
+
let timing = '';
|
|
218
|
+
if (t.state === 'running' && t.startedAt)
|
|
219
|
+
timing = ` ${dim(secs(Date.now() - t.startedAt))}`;
|
|
220
|
+
else if (t.startedAt && t.endedAt)
|
|
221
|
+
timing = ` ${dim(secs(t.endedAt - t.startedAt))}`;
|
|
222
|
+
const detail = t.detail ? ` ${dim('— ' + t.detail)}` : '';
|
|
223
|
+
return `${indent} ${glyph} ${label}${tags}${detail}${timing}`;
|
|
224
|
+
}
|
|
225
|
+
render(final = false) {
|
|
226
|
+
if (!this.live)
|
|
227
|
+
return;
|
|
228
|
+
const counts = this.tasks.reduce((a, t) => {
|
|
229
|
+
a[t.state]++;
|
|
230
|
+
return a;
|
|
231
|
+
}, { pending: 0, running: 0, done: 0, failed: 0, skipped: 0 });
|
|
232
|
+
const head = bold(this.title) +
|
|
233
|
+
dim(` · ${counts.done}/${this.tasks.length}` +
|
|
234
|
+
(counts.failed ? ` · ${counts.failed} failed` : '') +
|
|
235
|
+
(counts.skipped ? ` · ${counts.skipped} skipped` : '') +
|
|
236
|
+
` · ${secs(this.elapsed)}`);
|
|
237
|
+
/**
|
|
238
|
+
* Only the tail is kept when the list is long: a 200-step plan must not
|
|
239
|
+
* scroll the terminal on every frame. Finished work above the window is
|
|
240
|
+
* summarised in the header, which is the number people actually watch.
|
|
241
|
+
*/
|
|
242
|
+
const MAX = 14;
|
|
243
|
+
const visible = this.tasks.length <= MAX
|
|
244
|
+
? this.tasks
|
|
245
|
+
: (() => {
|
|
246
|
+
const active = this.tasks.findIndex((t) => t.state === 'running' || t.state === 'pending');
|
|
247
|
+
const from = Math.max(0, Math.min(active === -1 ? this.tasks.length - MAX : active - 3, this.tasks.length - MAX));
|
|
248
|
+
return this.tasks.slice(from, from + MAX);
|
|
249
|
+
})();
|
|
250
|
+
const hiddenAbove = this.tasks.indexOf(visible[0]);
|
|
251
|
+
const hiddenBelow = this.tasks.length - (hiddenAbove + visible.length);
|
|
252
|
+
const body = [
|
|
253
|
+
head,
|
|
254
|
+
...(hiddenAbove > 0 ? [dim(` … ${hiddenAbove} earlier`)] : []),
|
|
255
|
+
...visible.map((t) => this.line(t)),
|
|
256
|
+
...(hiddenBelow > 0 ? [dim(` … ${hiddenBelow} more`)] : []),
|
|
257
|
+
...this.notes.map((n) => ` ${n}`),
|
|
258
|
+
];
|
|
259
|
+
// Repaint in place: up N lines, then clear-and-write each one.
|
|
260
|
+
const out = cursorUp(this.linesDrawn) + body.map((l) => CLEAR_LINE + l).join('\n') + '\n';
|
|
261
|
+
this.stream.write(out);
|
|
262
|
+
this.linesDrawn = final ? 0 : body.length;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* A single long wait with no sub-steps — a snapshot poll, a download.
|
|
267
|
+
* Returns a function to finish it.
|
|
268
|
+
*/
|
|
269
|
+
export const spinner = (title, opts = {}) => {
|
|
270
|
+
const p = new Progress({ title, plain: opts.plain });
|
|
271
|
+
const id = 'only';
|
|
272
|
+
p.add({ id, label: title, state: 'running', startedAt: Date.now() });
|
|
273
|
+
p.start();
|
|
274
|
+
return {
|
|
275
|
+
detail: (text) => p.update(id, { detail: text }),
|
|
276
|
+
done: (label) => {
|
|
277
|
+
p.update(id, { state: 'done', ...(label ? { label } : {}) });
|
|
278
|
+
p.stop();
|
|
279
|
+
},
|
|
280
|
+
fail: (label) => {
|
|
281
|
+
p.update(id, { state: 'failed', ...(label ? { label } : {}) });
|
|
282
|
+
p.stop();
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
export const isLive = () => Boolean(process.stderr.isTTY) && !process.env.PPC_NO_TUI && !process.env.NO_TUI;
|
|
287
|
+
export { magenta };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { green, red, yellow, dim, bold } from "../util/log.js";
|
|
2
|
+
import { validateSchema } from "./schema.js";
|
|
3
|
+
import { validateIntegrity, summarise } from "./integrity.js";
|
|
4
|
+
import { validateAgainstReference } from "./reference.js";
|
|
5
|
+
export { summarise } from "./integrity.js";
|
|
6
|
+
/**
|
|
7
|
+
* Entity plus message, deliberately *not* including the file.
|
|
8
|
+
*
|
|
9
|
+
* Renaming a phase changes the file label of every finding inside it, so a
|
|
10
|
+
* file-keyed fingerprint would report a pre-existing problem as newly
|
|
11
|
+
* introduced the moment the FDE renames something nearby.
|
|
12
|
+
*/
|
|
13
|
+
const fingerprint = (f) => `${f.entity}::${f.message}`;
|
|
14
|
+
export const validateTree = (tree, opts = {}) => {
|
|
15
|
+
const findings = [
|
|
16
|
+
...validateSchema(tree),
|
|
17
|
+
...validateIntegrity(tree, { knownRepoIds: opts.knownRepoIds, agents: opts.agents }),
|
|
18
|
+
...validateAgainstReference(tree, opts.reference ?? null),
|
|
19
|
+
];
|
|
20
|
+
if (!opts.baseline)
|
|
21
|
+
return findings;
|
|
22
|
+
/**
|
|
23
|
+
* A multiset, not a set: if the baseline had one dangling reference of a kind
|
|
24
|
+
* and the edit introduced a second, the second must still be reported.
|
|
25
|
+
*/
|
|
26
|
+
const budget = new Map();
|
|
27
|
+
for (const f of [
|
|
28
|
+
...validateSchema(opts.baseline),
|
|
29
|
+
...validateIntegrity(opts.baseline, { knownRepoIds: opts.knownRepoIds, agents: opts.agents }),
|
|
30
|
+
...validateAgainstReference(opts.baseline, opts.reference ?? null),
|
|
31
|
+
]) {
|
|
32
|
+
const key = fingerprint(f);
|
|
33
|
+
budget.set(key, (budget.get(key) ?? 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
return findings.map((f) => {
|
|
36
|
+
const key = fingerprint(f);
|
|
37
|
+
const left = budget.get(key) ?? 0;
|
|
38
|
+
if (left <= 0)
|
|
39
|
+
return f;
|
|
40
|
+
budget.set(key, left - 1);
|
|
41
|
+
return { ...f, severity: 'warning', preExisting: true };
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
/** Errors the edit introduced — what plan and apply actually gate on. */
|
|
45
|
+
export const blockingErrors = (findings) => findings.filter((f) => f.severity === 'error' && !f.preExisting);
|
|
46
|
+
export const renderFindings = (findings, label) => {
|
|
47
|
+
const introduced = findings.filter((f) => !f.preExisting);
|
|
48
|
+
const preExisting = findings.filter((f) => f.preExisting);
|
|
49
|
+
const { errors, warnings } = summarise(introduced);
|
|
50
|
+
const lines = [];
|
|
51
|
+
if (!findings.length) {
|
|
52
|
+
return green(`+ ${label}: valid`);
|
|
53
|
+
}
|
|
54
|
+
lines.push(bold(label));
|
|
55
|
+
const group = (items) => {
|
|
56
|
+
const byFile = new Map();
|
|
57
|
+
for (const f of items) {
|
|
58
|
+
const key = f.file ?? f.entity;
|
|
59
|
+
const arr = byFile.get(key) ?? [];
|
|
60
|
+
arr.push(f);
|
|
61
|
+
byFile.set(key, arr);
|
|
62
|
+
}
|
|
63
|
+
for (const [file, items2] of byFile) {
|
|
64
|
+
lines.push(` ${dim(file)}`);
|
|
65
|
+
for (const f of items2) {
|
|
66
|
+
const mark = f.severity === 'error' ? red('x') : yellow('!');
|
|
67
|
+
lines.push(` ${mark} ${f.message}`);
|
|
68
|
+
if (f.fix)
|
|
69
|
+
lines.push(` ${dim('-> ' + f.fix)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
if (introduced.length)
|
|
74
|
+
group(introduced);
|
|
75
|
+
if (preExisting.length) {
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(dim(` ${preExisting.length} pre-existing issue${preExisting.length === 1 ? '' : 's'} — present in the last pull, not caused by these edits:`));
|
|
78
|
+
for (const f of preExisting)
|
|
79
|
+
lines.push(` ${dim('·')} ${dim(`${f.entity}: ${f.message}`)}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`${errors ? red(`${errors} error${errors === 1 ? '' : 's'}`) : green('0 errors')}, ` +
|
|
83
|
+
`${warnings ? yellow(`${warnings} warning${warnings === 1 ? '' : 's'}`) : '0 warnings'}` +
|
|
84
|
+
`${preExisting.length ? dim(`, ${preExisting.length} pre-existing`) : ''}`);
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
};
|