azure-pipelines-tui 0.5.0
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/README.md +114 -0
- package/dist/cache.js +54 -0
- package/dist/debugRetry.js +150 -0
- package/dist/debugSignalR.js +151 -0
- package/dist/debugWarnings.js +169 -0
- package/dist/lib/api.js +256 -0
- package/dist/lib/format.js +580 -0
- package/dist/lib/types.js +3 -0
- package/dist/screens/EnvironmentsScreen.js +220 -0
- package/dist/screens/MappingScreen.js +165 -0
- package/dist/screens/PipelineRunScreen.js +408 -0
- package/dist/screens/PipelineRunsScreen.js +135 -0
- package/dist/screens/PipelinesScreen.js +184 -0
- package/dist/screens/StagesScreen.js +194 -0
- package/dist/screens/context.js +2 -0
- package/dist/signalr.js +117 -0
- package/dist/tui.js +358 -0
- package/package.json +31 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Formatting, tree building, and helper functions
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.APPLY_COL = exports.PLAN_COL = exports.BRANCH_COL = exports.PIPE_LEFT_COL = exports.LEFT_COL = void 0;
|
|
5
|
+
exports.padEnd = padEnd;
|
|
6
|
+
exports.timeAgo = timeAgo;
|
|
7
|
+
exports.shortBranch = shortBranch;
|
|
8
|
+
exports.formatLogLine = formatLogLine;
|
|
9
|
+
exports.buildEnvTree = buildEnvTree;
|
|
10
|
+
exports.countDescendantStats = countDescendantStats;
|
|
11
|
+
exports.flattenEnvTree = flattenEnvTree;
|
|
12
|
+
exports.envColHeaderStr = envColHeaderStr;
|
|
13
|
+
exports.formatEnvItem = formatEnvItem;
|
|
14
|
+
exports.buildPipeTree = buildPipeTree;
|
|
15
|
+
exports.countPipeDescendants = countPipeDescendants;
|
|
16
|
+
exports.flattenPipeTree = flattenPipeTree;
|
|
17
|
+
exports.formatPipeItem = formatPipeItem;
|
|
18
|
+
exports.formatRunItem = formatRunItem;
|
|
19
|
+
exports.parseStageKind = parseStageKind;
|
|
20
|
+
exports.buildStageBranchSummaries = buildStageBranchSummaries;
|
|
21
|
+
exports.branchHasRun = branchHasRun;
|
|
22
|
+
exports.statusCell = statusCell;
|
|
23
|
+
exports.isEntirelySkipped = isEntirelySkipped;
|
|
24
|
+
exports.buildFlatRunTree = buildFlatRunTree;
|
|
25
|
+
exports.runItemLabel = runItemLabel;
|
|
26
|
+
// ── Basic formatting ──────────────────────────────────────────────────────────
|
|
27
|
+
function padEnd(s, n) {
|
|
28
|
+
return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
|
|
29
|
+
}
|
|
30
|
+
function timeAgo(dateStr) {
|
|
31
|
+
if (!dateStr)
|
|
32
|
+
return "-";
|
|
33
|
+
const utc = /Z|[+-]\d\d:\d\d$/.test(dateStr) ? dateStr : dateStr + "Z";
|
|
34
|
+
const diff = Date.now() - new Date(utc).getTime();
|
|
35
|
+
if (diff < 0)
|
|
36
|
+
return "now";
|
|
37
|
+
const m = Math.floor(diff / 60_000);
|
|
38
|
+
if (m < 1)
|
|
39
|
+
return "<1m";
|
|
40
|
+
if (m < 60)
|
|
41
|
+
return `${m}m`;
|
|
42
|
+
const h = Math.floor(m / 60);
|
|
43
|
+
if (h < 24)
|
|
44
|
+
return `${h}h`;
|
|
45
|
+
return `${Math.floor(h / 24)}d`;
|
|
46
|
+
}
|
|
47
|
+
function shortBranch(branch) {
|
|
48
|
+
if (!branch)
|
|
49
|
+
return "-";
|
|
50
|
+
return branch.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "");
|
|
51
|
+
}
|
|
52
|
+
const TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z /;
|
|
53
|
+
const ADO_CMD_RE = /^##\[(\w+)\]/;
|
|
54
|
+
function formatLogLine(raw, keepTimestamps = false) {
|
|
55
|
+
const line = keepTimestamps ? raw : raw.replace(TIMESTAMP_RE, "");
|
|
56
|
+
const m = ADO_CMD_RE.exec(line);
|
|
57
|
+
if (!m)
|
|
58
|
+
return line;
|
|
59
|
+
const rest = line.slice(m[0].length);
|
|
60
|
+
switch (m[1]) {
|
|
61
|
+
case "error": return `{red-fg}✗ ${rest}{/}`;
|
|
62
|
+
case "warning": return `{yellow-fg}⚠ ${rest}{/}`;
|
|
63
|
+
case "section": return `{cyan-fg}{bold}── ${rest} ──{/}`;
|
|
64
|
+
case "command": return `{blue-fg}$ ${rest}{/}`;
|
|
65
|
+
case "group": return `{magenta-fg}▼ ${rest}{/}`;
|
|
66
|
+
case "endgroup": return `{gray-fg}── end ──{/}`;
|
|
67
|
+
default: return `{gray-fg}[${m[1]}]{/} ${rest}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ── Environment tree ──────────────────────────────────────────────────────────
|
|
71
|
+
exports.LEFT_COL = 36;
|
|
72
|
+
function buildEnvTree(rows) {
|
|
73
|
+
const root = { key: "", label: "", children: new Map() };
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const segs = row.env.name.split("-");
|
|
76
|
+
let node = root;
|
|
77
|
+
for (let i = 0; i < segs.length; i++) {
|
|
78
|
+
const seg = segs[i];
|
|
79
|
+
const key = segs.slice(0, i + 1).join("-");
|
|
80
|
+
if (!node.children.has(seg))
|
|
81
|
+
node.children.set(seg, { key, label: seg, children: new Map() });
|
|
82
|
+
node = node.children.get(seg);
|
|
83
|
+
}
|
|
84
|
+
node.row = row;
|
|
85
|
+
}
|
|
86
|
+
return root;
|
|
87
|
+
}
|
|
88
|
+
function countDescendantStats(node) {
|
|
89
|
+
let total = 0, ok = 0, fail = 0;
|
|
90
|
+
for (const child of node.children.values()) {
|
|
91
|
+
if (child.children.size === 0 && child.row) {
|
|
92
|
+
total++;
|
|
93
|
+
const r = child.row.deploy?.result;
|
|
94
|
+
if (r === "succeeded")
|
|
95
|
+
ok++;
|
|
96
|
+
else if (r === "failed")
|
|
97
|
+
fail++;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const s = countDescendantStats(child);
|
|
101
|
+
total += s.total;
|
|
102
|
+
ok += s.ok;
|
|
103
|
+
fail += s.fail;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { total, ok, fail };
|
|
107
|
+
}
|
|
108
|
+
function flattenEnvTree(node, collapsed, depth, items) {
|
|
109
|
+
const children = [...node.children.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
110
|
+
for (let i = 0; i < children.length; i++) {
|
|
111
|
+
const child = children[i];
|
|
112
|
+
const isLast = i === children.length - 1;
|
|
113
|
+
if (child.children.size === 0) {
|
|
114
|
+
if (child.row)
|
|
115
|
+
items.push({ kind: "leaf", key: child.key, label: child.label, depth, row: child.row, isLast });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const stats = countDescendantStats(child);
|
|
119
|
+
const isExpanded = !collapsed.has(child.key);
|
|
120
|
+
items.push({ kind: "group", key: child.key, label: child.label, depth, isExpanded, ...stats, ownRow: child.row });
|
|
121
|
+
if (isExpanded)
|
|
122
|
+
flattenEnvTree(child, collapsed, depth + 1, items);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function envColHeaderStr() {
|
|
127
|
+
return padEnd(" Environments", exports.LEFT_COL) + " " + padEnd("Pipeline", 36) + " " +
|
|
128
|
+
padEnd("Status", 12) + " " + padEnd("Branch", 20) + " Age";
|
|
129
|
+
}
|
|
130
|
+
function rowColumns(row) {
|
|
131
|
+
let pipLine;
|
|
132
|
+
let pipColor;
|
|
133
|
+
if (row.mapping) {
|
|
134
|
+
pipLine = padEnd(`${row.mapping.pipelineName} [cfg]`, 36);
|
|
135
|
+
pipColor = "cyan";
|
|
136
|
+
}
|
|
137
|
+
else if (row.deploy) {
|
|
138
|
+
pipLine = padEnd(`${row.deploy.definition.name} [auto]`, 36);
|
|
139
|
+
pipColor = "gray";
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
pipLine = padEnd("- [none]", 36);
|
|
143
|
+
pipColor = "gray";
|
|
144
|
+
}
|
|
145
|
+
let stText;
|
|
146
|
+
let stColor;
|
|
147
|
+
if (row.loading) {
|
|
148
|
+
stText = padEnd("loading…", 12);
|
|
149
|
+
stColor = "gray";
|
|
150
|
+
}
|
|
151
|
+
else if (!row.deploy) {
|
|
152
|
+
stText = padEnd("no deploys", 12);
|
|
153
|
+
stColor = "gray";
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
switch (row.deploy.result) {
|
|
157
|
+
case "succeeded":
|
|
158
|
+
stText = padEnd("✓ ok", 12);
|
|
159
|
+
stColor = "green";
|
|
160
|
+
break;
|
|
161
|
+
case "failed":
|
|
162
|
+
stText = padEnd("✗ failed", 12);
|
|
163
|
+
stColor = "red";
|
|
164
|
+
break;
|
|
165
|
+
case "canceled":
|
|
166
|
+
stText = padEnd("⊘ canceled", 12);
|
|
167
|
+
stColor = "gray";
|
|
168
|
+
break;
|
|
169
|
+
case "partiallySucceeded":
|
|
170
|
+
stText = padEnd("⚠ partial", 12);
|
|
171
|
+
stColor = "yellow";
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
stText = padEnd(row.deploy.result ?? "?", 12);
|
|
175
|
+
stColor = "white";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const branch = padEnd(shortBranch(row.build?.sourceBranch), 20);
|
|
179
|
+
const age = timeAgo(row.deploy?.finishTime ?? row.deploy?.startTime);
|
|
180
|
+
return ` {${pipColor}-fg}${pipLine}{/} {${stColor}-fg}${stText}{/} {gray-fg}${branch}{/} ${age}`;
|
|
181
|
+
}
|
|
182
|
+
function formatEnvItem(item) {
|
|
183
|
+
const indent = " ".repeat(item.depth);
|
|
184
|
+
if (item.kind === "group") {
|
|
185
|
+
const pfx = indent + (item.isExpanded ? "▼ " : "▶ ");
|
|
186
|
+
const label = padEnd(item.label, Math.max(1, exports.LEFT_COL - 1 - pfx.length));
|
|
187
|
+
const left = " " + pfx + label;
|
|
188
|
+
if (item.ownRow) {
|
|
189
|
+
const childBadge = item.total > 0 ? ` {gray-fg}(+${item.total}){/}` : "";
|
|
190
|
+
return `{bold}${left}{/}${rowColumns(item.ownRow)}${childBadge}`;
|
|
191
|
+
}
|
|
192
|
+
let stats = "";
|
|
193
|
+
if (item.ok > 0)
|
|
194
|
+
stats += ` {green-fg}${item.ok}✓{/}`;
|
|
195
|
+
if (item.fail > 0)
|
|
196
|
+
stats += ` {red-fg}${item.fail}✗{/}`;
|
|
197
|
+
const other = item.total - item.ok - item.fail;
|
|
198
|
+
if (other > 0)
|
|
199
|
+
stats += ` {gray-fg}${other}…{/}`;
|
|
200
|
+
if (!stats && item.total === 0)
|
|
201
|
+
stats = ` {gray-fg}empty{/}`;
|
|
202
|
+
return `{bold}${left}{/}${stats}`;
|
|
203
|
+
}
|
|
204
|
+
const pfx = indent + (item.isLast ? "└─ " : "├─ ");
|
|
205
|
+
const label = padEnd(item.label, Math.max(1, exports.LEFT_COL - 1 - pfx.length));
|
|
206
|
+
return " " + pfx + label + rowColumns(item.row);
|
|
207
|
+
}
|
|
208
|
+
// ── Pipeline tree ─────────────────────────────────────────────────────────────
|
|
209
|
+
exports.PIPE_LEFT_COL = 52;
|
|
210
|
+
function buildPipeTree(defs) {
|
|
211
|
+
const root = { key: "", label: "", children: new Map() };
|
|
212
|
+
for (const p of defs) {
|
|
213
|
+
const segs = p.path.split("\\").filter(Boolean);
|
|
214
|
+
let node = root;
|
|
215
|
+
for (let i = 0; i < segs.length; i++) {
|
|
216
|
+
const seg = segs[i];
|
|
217
|
+
const key = "\\" + segs.slice(0, i + 1).join("\\");
|
|
218
|
+
if (!node.children.has(seg))
|
|
219
|
+
node.children.set(seg, { key, label: seg, children: new Map() });
|
|
220
|
+
node = node.children.get(seg);
|
|
221
|
+
}
|
|
222
|
+
const leafKey = (node.key || "") + "\\" + p.id;
|
|
223
|
+
node.children.set(String(p.id), { key: leafKey, label: p.name, children: new Map(), pipeline: p });
|
|
224
|
+
}
|
|
225
|
+
return root;
|
|
226
|
+
}
|
|
227
|
+
function countPipeDescendants(node) {
|
|
228
|
+
let n = 0;
|
|
229
|
+
for (const child of node.children.values())
|
|
230
|
+
n += child.pipeline ? 1 : countPipeDescendants(child);
|
|
231
|
+
return n;
|
|
232
|
+
}
|
|
233
|
+
function flattenPipeTree(node, collapsed, depth, items) {
|
|
234
|
+
const children = [...node.children.values()].sort((a, b) => {
|
|
235
|
+
const af = !a.pipeline, bf = !b.pipeline;
|
|
236
|
+
if (af !== bf)
|
|
237
|
+
return af ? -1 : 1;
|
|
238
|
+
return a.label.localeCompare(b.label);
|
|
239
|
+
});
|
|
240
|
+
for (let i = 0; i < children.length; i++) {
|
|
241
|
+
const child = children[i];
|
|
242
|
+
if (child.pipeline) {
|
|
243
|
+
items.push({ kind: "pipeline", key: child.key, label: child.label, depth, pipeline: child.pipeline, isLast: i === children.length - 1 });
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const isExpanded = !collapsed.has(child.key);
|
|
247
|
+
items.push({ kind: "folder", key: child.key, label: child.label, depth, isExpanded, count: countPipeDescendants(child) });
|
|
248
|
+
if (isExpanded)
|
|
249
|
+
flattenPipeTree(child, collapsed, depth + 1, items);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function formatPipeItem(item) {
|
|
254
|
+
const indent = " ".repeat(item.depth);
|
|
255
|
+
if (item.kind === "folder") {
|
|
256
|
+
const pfx = indent + (item.isExpanded ? "▼ " : "▶ ");
|
|
257
|
+
return `{bold} ${pfx}${item.label}{/} {gray-fg}(${item.count}){/}`;
|
|
258
|
+
}
|
|
259
|
+
const pfx = indent + (item.isLast ? "└─ " : "├─ ");
|
|
260
|
+
const label = padEnd(item.label, Math.max(1, exports.PIPE_LEFT_COL - pfx.length));
|
|
261
|
+
return ` ${pfx}${label} {gray-fg}${item.pipeline.id}{/}`;
|
|
262
|
+
}
|
|
263
|
+
// ── Pipeline runs list ────────────────────────────────────────────────────────
|
|
264
|
+
function formatRunItem(run) {
|
|
265
|
+
let resultStr;
|
|
266
|
+
if (run.status === "completed") {
|
|
267
|
+
switch (run.result) {
|
|
268
|
+
case "succeeded":
|
|
269
|
+
resultStr = "{green-fg}✓ succeeded{/}";
|
|
270
|
+
break;
|
|
271
|
+
case "failed":
|
|
272
|
+
resultStr = "{red-fg}✗ failed{/}";
|
|
273
|
+
break;
|
|
274
|
+
case "canceled":
|
|
275
|
+
resultStr = "{gray-fg}⊘ canceled{/}";
|
|
276
|
+
break;
|
|
277
|
+
case "partiallySucceeded":
|
|
278
|
+
resultStr = "{yellow-fg}⚠ partial{/}";
|
|
279
|
+
break;
|
|
280
|
+
default: resultStr = `{gray-fg}${run.result ?? "?"}{/}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (run.status === "inProgress") {
|
|
284
|
+
resultStr = "{yellow-fg}▶ running{/}";
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
resultStr = `{gray-fg}${run.status}{/}`;
|
|
288
|
+
}
|
|
289
|
+
const num = padEnd(`#${run.buildNumber}`, 10);
|
|
290
|
+
const branch = padEnd(shortBranch(run.sourceBranch), 30);
|
|
291
|
+
const age = timeAgo(run.startTime);
|
|
292
|
+
return ` ${num} {gray-fg}${branch}{/} ${padEnd(resultStr, 30)} {gray-fg}${age}{/}`;
|
|
293
|
+
}
|
|
294
|
+
// ── Stage helpers ─────────────────────────────────────────────────────────────
|
|
295
|
+
exports.BRANCH_COL = 30;
|
|
296
|
+
exports.PLAN_COL = 22;
|
|
297
|
+
exports.APPLY_COL = 22;
|
|
298
|
+
function parseStageKind(name, planBases) {
|
|
299
|
+
const pm = name.match(/^plan(.*)$/i);
|
|
300
|
+
if (pm)
|
|
301
|
+
return { kind: "plan", base: pm[1].replace(/^[_\-\s]+/, "") };
|
|
302
|
+
const am = name.match(/^(?:apply|deploy)(.*)$/i);
|
|
303
|
+
if (am)
|
|
304
|
+
return { kind: "apply", base: am[1].replace(/^[_\-\s]+/, "") };
|
|
305
|
+
if (planBases?.has(name.toLowerCase()))
|
|
306
|
+
return { kind: "plan", base: name };
|
|
307
|
+
return { kind: "other", base: name };
|
|
308
|
+
}
|
|
309
|
+
function buildStageBranchSummaries(runs, stagesMap) {
|
|
310
|
+
const planBases = new Set();
|
|
311
|
+
for (const run of runs)
|
|
312
|
+
for (const stage of (stagesMap.get(run.id) ?? [])) {
|
|
313
|
+
const m = stage.name.match(/^(?:apply|deploy)\s+(.+)$/i);
|
|
314
|
+
if (m)
|
|
315
|
+
planBases.add(m[1].trim().toLowerCase());
|
|
316
|
+
}
|
|
317
|
+
const baseOrder = [];
|
|
318
|
+
const basePlanName = new Map();
|
|
319
|
+
const baseApplyName = new Map();
|
|
320
|
+
const summaries = new Map();
|
|
321
|
+
for (const run of runs) {
|
|
322
|
+
const branch = shortBranch(run.sourceBranch);
|
|
323
|
+
for (const stage of (stagesMap.get(run.id) ?? [])) {
|
|
324
|
+
const { kind, base } = parseStageKind(stage.name, planBases);
|
|
325
|
+
const key = kind === "other" ? `\x00${base}` : base;
|
|
326
|
+
if (!summaries.has(key)) {
|
|
327
|
+
summaries.set(key, new Map());
|
|
328
|
+
baseOrder.push(key);
|
|
329
|
+
}
|
|
330
|
+
if (kind === "plan" && !basePlanName.has(key))
|
|
331
|
+
basePlanName.set(key, stage.name);
|
|
332
|
+
if (kind === "apply" && !baseApplyName.has(key))
|
|
333
|
+
baseApplyName.set(key, stage.name);
|
|
334
|
+
const branchMap = summaries.get(key);
|
|
335
|
+
if (!branchMap.has(branch))
|
|
336
|
+
branchMap.set(branch, { branch });
|
|
337
|
+
const s = branchMap.get(branch);
|
|
338
|
+
const entry = { runId: run.id, result: stage.result, state: stage.state, finishTime: stage.finishTime, warningCount: stage.warningCount };
|
|
339
|
+
const isPassive = (r) => r === "skipped" || r === "canceled";
|
|
340
|
+
const isActive = (r) => !!r && !isPassive(r);
|
|
341
|
+
if (kind === "apply") {
|
|
342
|
+
if (!s.applyLatest) {
|
|
343
|
+
s.applyLatest = entry;
|
|
344
|
+
}
|
|
345
|
+
else if (!isActive(s.applyLatest.result) && !s.applyPrevActive && isActive(entry.result))
|
|
346
|
+
s.applyPrevActive = entry;
|
|
347
|
+
else if (!s.applyPrevOk) {
|
|
348
|
+
const eff = s.applyPrevActive ?? s.applyLatest;
|
|
349
|
+
if (eff.result !== "succeeded" && entry.result === "succeeded")
|
|
350
|
+
s.applyPrevOk = entry;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
if (!s.planLatest) {
|
|
355
|
+
s.planLatest = entry;
|
|
356
|
+
}
|
|
357
|
+
else if (!isActive(s.planLatest.result) && !s.planPrevActive && isActive(entry.result))
|
|
358
|
+
s.planPrevActive = entry;
|
|
359
|
+
else if (!s.planPrevOk) {
|
|
360
|
+
const eff = s.planPrevActive ?? s.planLatest;
|
|
361
|
+
if (eff.result !== "succeeded" && entry.result === "succeeded")
|
|
362
|
+
s.planPrevOk = entry;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return baseOrder
|
|
368
|
+
.map(key => {
|
|
369
|
+
const isOther = key.startsWith("\x00");
|
|
370
|
+
const base = isOther ? key.slice(1) : key;
|
|
371
|
+
const plan = basePlanName.get(key);
|
|
372
|
+
const apply = baseApplyName.get(key);
|
|
373
|
+
const displayName = base || (plan && apply ? `${plan} / ${apply}` : (plan ?? apply ?? key));
|
|
374
|
+
return { displayName, branches: summaries.get(key) };
|
|
375
|
+
})
|
|
376
|
+
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
377
|
+
}
|
|
378
|
+
function branchHasRun(summary) {
|
|
379
|
+
const eff = (e) => !!e && e.result !== "skipped" && e.result !== "canceled";
|
|
380
|
+
return eff(summary.planLatest) || eff(summary.applyLatest)
|
|
381
|
+
|| !!summary.planPrevActive || !!summary.applyPrevActive
|
|
382
|
+
|| !!summary.planPrevOk || !!summary.applyPrevOk;
|
|
383
|
+
}
|
|
384
|
+
function statusCell(entry, prevOk, W = exports.PLAN_COL, dim = false, prevActive) {
|
|
385
|
+
if (!entry)
|
|
386
|
+
return padEnd("-", W);
|
|
387
|
+
if (entry.result === "skipped" || entry.result === "canceled") {
|
|
388
|
+
const display = prevActive ?? prevOk;
|
|
389
|
+
if (display) {
|
|
390
|
+
const age = timeAgo(display.finishTime);
|
|
391
|
+
const warnings = (display.warningCount ?? 0) > 0;
|
|
392
|
+
let icon;
|
|
393
|
+
let color;
|
|
394
|
+
if (display.result === "failed") {
|
|
395
|
+
icon = "✗";
|
|
396
|
+
color = "red";
|
|
397
|
+
}
|
|
398
|
+
else if (warnings) {
|
|
399
|
+
icon = "⚠";
|
|
400
|
+
color = "#ff8700";
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
icon = "✓";
|
|
404
|
+
color = "green";
|
|
405
|
+
}
|
|
406
|
+
const mainStr = `${icon} ${age}`;
|
|
407
|
+
const fallback = (prevActive && display.result !== "succeeded" && prevOk)
|
|
408
|
+
? `(✓${timeAgo(prevOk.finishTime)})` : "";
|
|
409
|
+
const starFull = ` *${fallback}`;
|
|
410
|
+
const pad = Math.max(0, W - mainStr.length - starFull.length);
|
|
411
|
+
return `{${color}-fg}${mainStr}{/}{gray-fg}${starFull}{/}${" ".repeat(pad)}`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
let icon;
|
|
415
|
+
let color;
|
|
416
|
+
if (entry.state === "inProgress") {
|
|
417
|
+
icon = "▶";
|
|
418
|
+
color = "yellow";
|
|
419
|
+
}
|
|
420
|
+
else if (entry.result === "succeeded" && (entry.warningCount ?? 0) > 0) {
|
|
421
|
+
icon = "⚠";
|
|
422
|
+
color = "#ff8700";
|
|
423
|
+
}
|
|
424
|
+
else if (entry.result === "succeeded") {
|
|
425
|
+
icon = "✓";
|
|
426
|
+
color = "green";
|
|
427
|
+
}
|
|
428
|
+
else if (entry.result === "failed") {
|
|
429
|
+
icon = "✗";
|
|
430
|
+
color = "red";
|
|
431
|
+
}
|
|
432
|
+
else if (entry.result === "skipped" || entry.result === "canceled") {
|
|
433
|
+
icon = "⊘";
|
|
434
|
+
color = "gray";
|
|
435
|
+
}
|
|
436
|
+
else if (entry.state === "pending") {
|
|
437
|
+
icon = "○";
|
|
438
|
+
color = "gray";
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
icon = "?";
|
|
442
|
+
color = "white";
|
|
443
|
+
}
|
|
444
|
+
const age = timeAgo(entry.finishTime);
|
|
445
|
+
const mainStr = `${icon} ${age}`;
|
|
446
|
+
const dimColor = { green: "#005f00", red: "#5f0000", yellow: "#5f5f00", "#ff8700": "#5f3000", gray: "#3a3a3a", white: "#3a3a3a" };
|
|
447
|
+
const wrap = (s, c) => dim ? `{${dimColor[c] ?? "#3a3a3a"}-fg}${s}{/}` : `{${c}-fg}${s}{/}`;
|
|
448
|
+
if ((entry.state === "inProgress" || entry.state === "pending") && prevActive) {
|
|
449
|
+
const pAge = timeAgo(prevActive.finishTime);
|
|
450
|
+
const pWarn = (prevActive.warningCount ?? 0) > 0;
|
|
451
|
+
let pIcon;
|
|
452
|
+
let pColor;
|
|
453
|
+
if (prevActive.result === "failed") {
|
|
454
|
+
pIcon = "✗";
|
|
455
|
+
pColor = "red";
|
|
456
|
+
}
|
|
457
|
+
else if (pWarn) {
|
|
458
|
+
pIcon = "⚠";
|
|
459
|
+
pColor = "#ff8700";
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
pIcon = "✓";
|
|
463
|
+
pColor = "green";
|
|
464
|
+
}
|
|
465
|
+
const prevPart = `(${pIcon}${pAge})`;
|
|
466
|
+
const pad = Math.max(0, W - mainStr.length - 1 - prevPart.length);
|
|
467
|
+
return `${wrap(mainStr, color)} ${wrap(prevPart, pColor)}${" ".repeat(pad)}`;
|
|
468
|
+
}
|
|
469
|
+
let prevStr = "";
|
|
470
|
+
if (prevOk && entry.result !== "succeeded")
|
|
471
|
+
prevStr = ` (✓${timeAgo(prevOk.finishTime)})`;
|
|
472
|
+
const pad = Math.max(0, W - mainStr.length - prevStr.length);
|
|
473
|
+
return `${wrap(mainStr, color)}${prevStr ? wrap(prevStr, "gray") : ""}${" ".repeat(pad)}`;
|
|
474
|
+
}
|
|
475
|
+
// ── Run tree helpers ──────────────────────────────────────────────────────────
|
|
476
|
+
function isEntirelySkipped(r, byParent) {
|
|
477
|
+
if (r.result !== "skipped" && r.result !== "canceled")
|
|
478
|
+
return false;
|
|
479
|
+
return (byParent.get(r.id) ?? []).every(c => isEntirelySkipped(c, byParent));
|
|
480
|
+
}
|
|
481
|
+
function buildFlatRunTree(records, collapsed, expandedGroups) {
|
|
482
|
+
const knownIds = new Set(records.map(r => r.id));
|
|
483
|
+
const byParent = new Map();
|
|
484
|
+
for (const r of records) {
|
|
485
|
+
const key = r.parentId && knownIds.has(r.parentId) ? r.parentId : undefined;
|
|
486
|
+
if (!byParent.has(key))
|
|
487
|
+
byParent.set(key, []);
|
|
488
|
+
byParent.get(key).push(r);
|
|
489
|
+
}
|
|
490
|
+
for (const kids of byParent.values())
|
|
491
|
+
kids.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
492
|
+
const result = [];
|
|
493
|
+
function walk(parentId, depth) {
|
|
494
|
+
const siblings = byParent.get(parentId) ?? [];
|
|
495
|
+
const allSkipped = siblings.filter(r => isEntirelySkipped(r, byParent));
|
|
496
|
+
let groupEmitted = false;
|
|
497
|
+
for (const r of siblings) {
|
|
498
|
+
if (isEntirelySkipped(r, byParent)) {
|
|
499
|
+
if (allSkipped.length < 2) {
|
|
500
|
+
const hasChildren = (byParent.get(r.id) ?? []).length > 0;
|
|
501
|
+
result.push({ kind: "regular", record: r, depth, hasChildren, isExpanded: !collapsed.has(r.id) });
|
|
502
|
+
if (hasChildren && !collapsed.has(r.id))
|
|
503
|
+
walk(r.id, depth + 1);
|
|
504
|
+
}
|
|
505
|
+
else if (!groupEmitted) {
|
|
506
|
+
const typeName = depth === 0 ? "stages" : "steps";
|
|
507
|
+
const groupId = `__grp_${allSkipped[0].id}`;
|
|
508
|
+
const isExpanded = expandedGroups.has(groupId);
|
|
509
|
+
result.push({ kind: "group", id: groupId, depth, count: allSkipped.length, label: `${allSkipped.length} ${typeName} skipped`, isExpanded });
|
|
510
|
+
if (isExpanded) {
|
|
511
|
+
for (const gr of allSkipped) {
|
|
512
|
+
const hasChildren = (byParent.get(gr.id) ?? []).length > 0;
|
|
513
|
+
result.push({ kind: "regular", record: gr, depth, hasChildren, isExpanded: !collapsed.has(gr.id) });
|
|
514
|
+
if (hasChildren && !collapsed.has(gr.id))
|
|
515
|
+
walk(gr.id, depth + 1);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
groupEmitted = true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
const kids = byParent.get(r.id) ?? [];
|
|
523
|
+
const hasChildren = kids.length > 0;
|
|
524
|
+
const isExpanded = !collapsed.has(r.id);
|
|
525
|
+
result.push({ kind: "regular", record: r, depth, hasChildren, isExpanded });
|
|
526
|
+
if (hasChildren && isExpanded)
|
|
527
|
+
walk(r.id, depth + 1);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
walk(undefined, 0);
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
function runItemLabel(item) {
|
|
535
|
+
const indent = " ".repeat(item.depth);
|
|
536
|
+
if (item.kind === "group") {
|
|
537
|
+
const caret = item.isExpanded ? "{gray-fg}▼{/} " : "{gray-fg}▶{/} ";
|
|
538
|
+
return `${indent}${caret}{gray-fg}⊘ ${item.label}{/}`;
|
|
539
|
+
}
|
|
540
|
+
const { record: r, hasChildren, isExpanded } = item;
|
|
541
|
+
const caret = hasChildren ? (isExpanded ? "{gray-fg}▼{/} " : "{gray-fg}▶{/} ") : " ";
|
|
542
|
+
let icon, color;
|
|
543
|
+
if (r.type === "Checkpoint.Approval") {
|
|
544
|
+
icon = r.state === "completed" ? "✓" : "⏸";
|
|
545
|
+
color = r.state === "inProgress" ? "yellow" : r.result === "succeeded" ? "green" : "gray";
|
|
546
|
+
}
|
|
547
|
+
else if (r.type === "Checkpoint") {
|
|
548
|
+
icon = "⬡";
|
|
549
|
+
color = "gray";
|
|
550
|
+
}
|
|
551
|
+
else if (r.state === "pending") {
|
|
552
|
+
icon = "○";
|
|
553
|
+
color = "gray";
|
|
554
|
+
}
|
|
555
|
+
else if (r.state === "inProgress") {
|
|
556
|
+
icon = "▶";
|
|
557
|
+
color = "yellow";
|
|
558
|
+
}
|
|
559
|
+
else if (r.result === "succeeded") {
|
|
560
|
+
icon = "✓";
|
|
561
|
+
color = "green";
|
|
562
|
+
}
|
|
563
|
+
else if (r.result === "failed") {
|
|
564
|
+
icon = "✗";
|
|
565
|
+
color = "red";
|
|
566
|
+
}
|
|
567
|
+
else if (r.result === "skipped") {
|
|
568
|
+
icon = "⊘";
|
|
569
|
+
color = "gray";
|
|
570
|
+
}
|
|
571
|
+
else if (r.result === "canceled") {
|
|
572
|
+
icon = "⊘";
|
|
573
|
+
color = "gray";
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
icon = "?";
|
|
577
|
+
color = "white";
|
|
578
|
+
}
|
|
579
|
+
return `${indent}${caret}{${color}-fg}${icon}{/} ${r.name}`;
|
|
580
|
+
}
|