@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,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
+ };
@@ -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
+ };