@fusion.dev/helix-headless 0.202.0 → 0.204.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.
@@ -177,7 +177,13 @@ registerElementEvents({
177
177
  'helix-reorder',
178
178
  'input',
179
179
  ],
180
- 'helix-headless-log': ['helix-before-pin', 'helix-expand', 'helix-pin'],
180
+ 'helix-headless-log': [
181
+ 'helix-before-pin',
182
+ 'helix-expand',
183
+ 'helix-pin',
184
+ 'helix-select',
185
+ 'helix-selected',
186
+ ],
181
187
  'helix-headless-log-group': ['helix-before-close', 'helix-before-open', 'toggle'],
182
188
  'helix-headless-map': [
183
189
  'change',
@@ -3,7 +3,7 @@ export type GraphNodeShape = 'circle' | 'diamond' | 'ring' | 'square';
3
3
  /** How heavily a note is drawn, from the `emphasis` axis. */
4
4
  export type GraphNodeEmphasis = 'outlined' | 'solid' | 'subtle';
5
5
  /** What a note is drawn as made of, from the `material` axis. */
6
- export type GraphNodeMaterial = 'frosted' | 'glass';
6
+ export type GraphNodeMaterial = 'frosted' | 'glass' | 'mark';
7
7
  /** What a note means, from the `tone` axis, which colors it over its group. */
8
8
  export type GraphNodeTone = 'accent' | 'danger' | 'info' | 'neutral' | 'primary' | 'success' | 'warning';
9
9
  /** One note in a graph. */
@@ -20,7 +20,7 @@ export interface GraphNode {
20
20
  emphasis?: GraphNodeEmphasis | undefined;
21
21
  /** A kind the note belongs to, such as a tag, which a renderer colors it by. */
22
22
  group?: string | undefined;
23
- /** What it is drawn as made of: `glass` or `frosted`. Unset is the renderer's own. */
23
+ /** What it is drawn as made of: `glass`, `frosted`, or `mark`. Unset is the renderer's own. */
24
24
  material?: GraphNodeMaterial | undefined;
25
25
  /**
26
26
  * Its shape, which says what kind of note it is at a glance: a person as a
@@ -30,14 +30,41 @@ export interface LogGroup {
30
30
  /** Where it stands. */
31
31
  status?: LogStatus;
32
32
  }
33
+ /**
34
+ * What a parser says about one finished line: the line's fields, and whether
35
+ * it opens or closes a step.
36
+ */
37
+ export interface LogParsed extends Partial<LogLine> {
38
+ /**
39
+ * Closes the step that is open. Its status is `failed` where a line in it
40
+ * was at `error`, and `done` otherwise, and its duration runs from its first
41
+ * line's `at` to this one's, unless either is given here.
42
+ */
43
+ close?: boolean | {
44
+ duration?: string;
45
+ status?: LogStatus;
46
+ };
47
+ /**
48
+ * Opens a step with this heading, closing the one that is open. The lines
49
+ * after it belong to the step until another opens or this one closes.
50
+ */
51
+ open?: string;
52
+ }
33
53
  /**
34
54
  * Reads one finished line of written text and says what it is.
35
55
  *
36
- * Return the fields to set, such as a `level` read off a prefix or a `group`
37
- * read off a marker, and `text` to show something else. Return `null` to drop
38
- * the line, such as a marker that only opens a group.
56
+ * Return the fields to set, such as a `level` read off a prefix, and `text` to
57
+ * show something else. Return `null` to drop the line. Return `open` or
58
+ * `close` to start or end a step, and the log keeps its steps itself: a line
59
+ * carrying either is a marker and is dropped, unless the result sets `text`,
60
+ * in which case an opening line is the step's first and a closing line stands
61
+ * after it.
39
62
  */
40
- export type LogParser = (text: string) => Partial<LogLine> | null | undefined;
63
+ export type LogParser = (text: string) => LogParsed | null | undefined;
64
+ /** `seconds` as a step's duration reads: `12s`, `1m 12s`, or `1h 2m`. */
65
+ export declare const stepDuration: (seconds: number) => string;
66
+ /** Milliseconds since the epoch for `at`, or undefined where it names no time. */
67
+ export declare const timeOf: (at: number | string | undefined) => number | undefined;
41
68
  /**
42
69
  * The text of a run, kept as lines.
43
70
  *
@@ -56,6 +83,10 @@ export type LogParser = (text: string) => Partial<LogLine> | null | undefined;
56
83
  * **Color carries from line to line**, as it does in a terminal, so a block
57
84
  * that opens with green and closes many lines later is green throughout. It
58
85
  * carries through text a carriage return replaced as well.
86
+ *
87
+ * **Steps a parser opens are kept here**, in `steps`, each a new object
88
+ * whenever it changes, so a log can compare one set with the next and see a
89
+ * step finish.
59
90
  */
60
91
  export declare class LogBuffer {
61
92
  /** The finished lines kept, oldest first. */
@@ -66,6 +97,13 @@ export declare class LogBuffer {
66
97
  limit: number;
67
98
  /** Reads each finished line of written text. */
68
99
  parse: LogParser | undefined;
100
+ /** The steps `parse` opened, in order, replaced as a whole when one changes. */
101
+ steps: LogGroup[];
102
+ /** The step that is open, by its place in `steps`. */
103
+ private openStep;
104
+ /** Whether a line in the open step was at `error`, and when the step began. */
105
+ private stepFailed;
106
+ private stepStarted;
69
107
  /** The line still being written, as it reads now. */
70
108
  private partial;
71
109
  /** Whether the next text written starts the line over. */
@@ -94,6 +132,16 @@ export declare class LogBuffer {
94
132
  runsOf(line: LogLine): LogRun[];
95
133
  /** Finishes the line being written. */
96
134
  private finish;
135
+ /** When the open step's latest line was written. */
136
+ private stepEnded;
137
+ /** Opens a step named `heading`, closing the one that is open. */
138
+ private openStepNamed;
139
+ /**
140
+ * Closes the open step: failed where one of its lines was at `error`, done
141
+ * otherwise, timed from its first line to `at`, and folded unless it failed,
142
+ * as a CI page folds a step that passed.
143
+ */
144
+ private closeStep;
97
145
  /** Lets the oldest lines go past `limit`, all at once. */
98
146
  private trim;
99
147
  }
@@ -1,15 +1,39 @@
1
1
  import { parseAnsi } from './ansi.js';
2
+ export const stepDuration = (seconds) => {
3
+ const whole = Math.max(0, Math.round(seconds));
4
+ if (whole < 60) {
5
+ return `${whole}s`;
6
+ }
7
+ if (whole < 3600) {
8
+ const rest = whole % 60;
9
+ return rest === 0 ? `${Math.floor(whole / 60)}m` : `${Math.floor(whole / 60)}m ${rest}s`;
10
+ }
11
+ const minutes = Math.floor((whole % 3600) / 60);
12
+ return minutes === 0 ? `${Math.floor(whole / 3600)}h` : `${Math.floor(whole / 3600)}h ${minutes}m`;
13
+ };
14
+ export const timeOf = (at) => {
15
+ if (at === undefined || at === '') {
16
+ return undefined;
17
+ }
18
+ const time = typeof at === 'number' || /^\d+$/.test(at) ? Number(at) : Date.parse(at);
19
+ return Number.isFinite(time) ? time : undefined;
20
+ };
2
21
  export class LogBuffer {
3
22
  constructor() {
4
23
  this.lines = [];
5
24
  this.dropped = 0;
6
25
  this.limit = 0;
7
26
  this.parse = undefined;
27
+ this.steps = [];
28
+ this.openStep = undefined;
29
+ this.stepFailed = false;
30
+ this.stepStarted = undefined;
8
31
  this.partial = '';
9
32
  this.returned = false;
10
33
  this.lineStyle = {};
11
34
  this.runs = new WeakMap();
12
35
  this.shown = undefined;
36
+ this.stepEnded = undefined;
13
37
  }
14
38
  write(chunk) {
15
39
  let finished = 0;
@@ -42,6 +66,10 @@ export class LogBuffer {
42
66
  this.returned = false;
43
67
  this.lineStyle = {};
44
68
  this.shown = undefined;
69
+ this.steps = [];
70
+ this.openStep = undefined;
71
+ this.stepFailed = false;
72
+ this.stepStarted = undefined;
45
73
  this.trim();
46
74
  }
47
75
  clear() {
@@ -51,7 +79,7 @@ export class LogBuffer {
51
79
  if (this.partial === '') {
52
80
  return undefined;
53
81
  }
54
- const group = this.lines.at(-1)?.group;
82
+ const group = this.openStep === undefined ? this.lines.at(-1)?.group : this.steps[this.openStep]?.id;
55
83
  if (this.shown?.text !== this.partial || this.shown.group !== group) {
56
84
  this.shown = group === undefined ? { text: this.partial } : { group, text: this.partial };
57
85
  }
@@ -76,12 +104,68 @@ export class LogBuffer {
76
104
  this.returned = false;
77
105
  const read = this.parse?.(raw);
78
106
  if (read !== null) {
79
- const line = { ...read, text: read?.text ?? raw };
80
- this.runs.set(line, line.text === raw ? flow.runs : parseAnsi(line.text, this.lineStyle).runs);
81
- this.lines.push(line);
107
+ const { close, open, ...fields } = read ?? {};
108
+ const marker = close !== undefined || open !== undefined;
109
+ const at = timeOf(fields.at);
110
+ if (close) {
111
+ this.closeStep(close, at);
112
+ }
113
+ if (open !== undefined) {
114
+ this.openStepNamed(open, at);
115
+ }
116
+ if (!marker || Object.hasOwn(fields, 'text')) {
117
+ const step = this.openStep === undefined ? undefined : this.steps[this.openStep];
118
+ const line = {
119
+ ...(step && fields.group === undefined ? { group: step.id } : {}),
120
+ ...fields,
121
+ text: fields.text ?? raw,
122
+ };
123
+ if (step && line.group === step.id) {
124
+ this.stepFailed ||= line.level === 'error';
125
+ this.stepStarted ??= at;
126
+ this.stepEnded = at ?? this.stepEnded;
127
+ }
128
+ this.runs.set(line, line.text === raw ? flow.runs : parseAnsi(line.text, this.lineStyle).runs);
129
+ this.lines.push(line);
130
+ }
82
131
  }
83
132
  this.lineStyle = flow.style;
84
133
  }
134
+ openStepNamed(heading, at) {
135
+ if (this.openStep !== undefined) {
136
+ this.closeStep(true, at);
137
+ }
138
+ this.steps = [
139
+ ...this.steps,
140
+ { heading, id: `step-${this.steps.length + 1}`, open: true, status: 'running' },
141
+ ];
142
+ this.openStep = this.steps.length - 1;
143
+ this.stepFailed = false;
144
+ this.stepStarted = at;
145
+ this.stepEnded = at;
146
+ }
147
+ closeStep(close, at) {
148
+ const index = this.openStep;
149
+ const step = index === undefined ? undefined : this.steps[index];
150
+ if (index === undefined || !step) {
151
+ return;
152
+ }
153
+ const given = typeof close === 'object' ? close : {};
154
+ const status = given.status ?? (this.stepFailed ? 'failed' : 'done');
155
+ const end = at ?? this.stepEnded;
156
+ const duration = given.duration ??
157
+ (this.stepStarted !== undefined && end !== undefined
158
+ ? stepDuration((end - this.stepStarted) / 1000)
159
+ : undefined);
160
+ const closed = {
161
+ ...step,
162
+ ...(duration === undefined ? {} : { duration }),
163
+ open: status === 'failed',
164
+ status,
165
+ };
166
+ this.steps = this.steps.map((each, place) => (place === index ? closed : each));
167
+ this.openStep = undefined;
168
+ }
85
169
  trim() {
86
170
  const over = this.limit > 0 ? this.lines.length - this.limit : 0;
87
171
  if (over > 0) {
@@ -1,5 +1,6 @@
1
1
  export { type LogRun, type LogStyle, parseAnsi, stripAnsi } from './ansi.js';
2
- export type { LogGroup, LogLevel, LogLine, LogParser, LogStatus } from './buffer.js';
2
+ export type { LogGroup, LogLevel, LogLine, LogParsed, LogParser, LogStatus } from './buffer.js';
3
3
  export { HelixHeadlessLogGroup } from './group/log-group.js';
4
4
  export { HelixHeadlessLogLine, type LogTimes } from './line/log-line.js';
5
+ export { parseGitHubActions, parseVercel } from './parsers.js';
5
6
  export { HelixHeadlessLog, type LogScrollOptions, type LogSource } from './root/log-root.js';
package/dist/log/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { parseAnsi, stripAnsi } from './ansi.js';
2
2
  export { HelixHeadlessLogGroup } from './group/log-group.js';
3
3
  export { HelixHeadlessLogLine } from './line/log-line.js';
4
+ export { parseGitHubActions, parseVercel } from './parsers.js';
4
5
  export { HelixHeadlessLog } from './root/log-root.js';
@@ -1,11 +1,9 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import type { LogRun } from '../ansi.js';
4
- import type { LogLevel } from '../buffer.js';
4
+ import { type LogLevel } from '../buffer.js';
5
5
  /** How a line's time reads: the clock, or how far into the run it was written. */
6
6
  export type LogTimes = 'absolute' | 'relative';
7
- /** Milliseconds since the epoch for `at`, or undefined where it names no time. */
8
- export declare const timeOf: (at: number | string | undefined) => number | undefined;
9
7
  /**
10
8
  * @summary One line of a log, with its number, its time, and its text.
11
9
  *
@@ -28,9 +26,10 @@ export declare const timeOf: (at: number | string | undefined) => number | undef
28
26
  * @csspart line-debug - The row of a line at `debug`.
29
27
  * @csspart line-error - The row of a line at `error`.
30
28
  * @csspart line-marked - The row of a line `mark-lines` names on the log.
29
+ * @csspart line-selected - The row of a line in the log's selection.
31
30
  * @csspart line-warn - The row of a line at `warn`.
32
31
  * @csspart match - Text that matches the log's `query`, in a line the log draws from data.
33
- * @csspart number - The line's number, shown with `line-numbers`, and a link with `line-href`.
32
+ * @csspart number - The line's number, shown with `line-numbers`: a link with `line-href`, and a button on a `selectable` log.
34
33
  * @csspart run - A stretch of text drawn in an ANSI style, in a line the log draws from data. Its color is `--log-run-color` and its background `--log-run-background-color`.
35
34
  * @csspart run-bold - A run drawn bold.
36
35
  * @csspart run-dim - A run drawn faint.
@@ -66,8 +65,14 @@ export declare class HelixHeadlessLogLine extends HelixElement {
66
65
  logNumbers: boolean;
67
66
  /** @internal Set by the log: the text to mark in the runs, for a line drawn from data. */
68
67
  logQuery: string;
68
+ /** @internal Set by the log: whether pressing the number selects the line. */
69
+ logSelectable: boolean;
70
+ /** @internal Set by the log: whether the line is in the selection. */
71
+ logSelected: boolean;
69
72
  /** @internal Set by the log: when the first line was written, which `relative` counts from. */
70
73
  logStart: number | undefined;
74
+ /** @internal Set by the log: whether the number is the log's one tab stop among its numbers. */
75
+ logStop: boolean;
71
76
  /** @internal Set by the log: how to show the time, or empty to hide it. */
72
77
  logTimes: '' | LogTimes;
73
78
  /** @internal Set by the log: the text as styled runs, for a line drawn from data. */
@@ -78,8 +83,17 @@ export declare class HelixHeadlessLogLine extends HelixElement {
78
83
  */
79
84
  readonly isLogLine = true;
80
85
  protected willUpdate(changed: PropertyValues): void;
81
- /** The number, as a link where the log gave one, or as text a screen reader skips. */
86
+ /**
87
+ * The number: a link where the log gave `line-href`, a button where the log
88
+ * is `selectable`, and otherwise text a screen reader skips.
89
+ *
90
+ * **One number in the log is a tab stop, and the rest are reached with the
91
+ * arrow keys**, so a log of a thousand lines is one stop on the way through
92
+ * the page. The log says which, with `logStop`.
93
+ */
82
94
  private renderNumber;
95
+ /** @internal Called by the log: moves focus to the number, where it takes focus. */
96
+ focusNumber(): void;
83
97
  /** The time, on the clock or into the run, where the log asked for one and the line has one. */
84
98
  private renderTime;
85
99
  /**
@@ -2,15 +2,9 @@ import { __decorate } from "tslib";
2
2
  import { customElement, formatWords, HelixElement, words, wordsLocale, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
+ import { timeOf } from '../buffer.js';
5
6
  import { markMatches } from '../rows.js';
6
7
  const NAME = /^[a-z-]+$/;
7
- export const timeOf = (at) => {
8
- if (at === undefined || at === '') {
9
- return undefined;
10
- }
11
- const time = typeof at === 'number' || /^\d+$/.test(at) ? Number(at) : Date.parse(at);
12
- return Number.isFinite(time) ? time : undefined;
13
- };
14
8
  const elapsed = (seconds) => {
15
9
  const whole = Math.max(0, Math.floor(seconds));
16
10
  const hours = Math.floor(whole / 3600);
@@ -29,7 +23,10 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
29
23
  this.logNumber = 0;
30
24
  this.logNumbers = false;
31
25
  this.logQuery = '';
26
+ this.logSelectable = false;
27
+ this.logSelected = false;
32
28
  this.logStart = undefined;
29
+ this.logStop = false;
33
30
  this.logTimes = '';
34
31
  this.runs = undefined;
35
32
  this.isLogLine = true;
@@ -44,15 +41,31 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
44
41
  return nothing;
45
42
  }
46
43
  const number = String(this.logNumber);
44
+ const name = formatWords(this, 'lineLabel', { number: this.logNumber });
45
+ const stop = this.logStop ? 0 : -1;
47
46
  if (this.logHref !== '') {
48
47
  return html `<a
49
48
  part="number"
50
49
  href=${this.logHref.replaceAll('{n}', number)}
51
- aria-label=${formatWords(this, 'lineLabel', { number: this.logNumber })}
50
+ aria-label=${name}
51
+ aria-current=${this.logSelected ? 'true' : nothing}
52
+ tabindex=${stop}
52
53
  >${number}</a>`;
54
+ }
55
+ if (this.logSelectable) {
56
+ return html `<button
57
+ part="number"
58
+ type="button"
59
+ aria-label=${name}
60
+ aria-pressed=${this.logSelected ? 'true' : 'false'}
61
+ tabindex=${stop}
62
+ >${number}</button>`;
53
63
  }
54
64
  return html `<span part="number" aria-hidden="true">${number}</span>`;
55
65
  }
66
+ focusNumber() {
67
+ this.renderRoot.querySelector('a[part="number"], button[part="number"]')?.focus();
68
+ }
56
69
  renderTime() {
57
70
  const time = timeOf(this.at);
58
71
  if (this.logTimes === '' || time === undefined) {
@@ -98,7 +111,7 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
98
111
  }
99
112
  const level = this.level;
100
113
  return html `<div
101
- part="line${level ? { debug: ' line-debug', error: ' line-error', info: '', warn: ' line-warn' }[level] : ''}${this.logMarked ? ' line-marked' : ''}"
114
+ part="line${level ? { debug: ' line-debug', error: ' line-error', info: '', warn: ' line-warn' }[level] : ''}${this.logMarked ? ' line-marked' : ''}${this.logSelected ? ' line-selected' : ''}"
102
115
  >${this.renderNumber()}${this.renderTime()}<span part="text">${this.renderText()}</span></div>`;
103
116
  }
104
117
  };
@@ -130,9 +143,18 @@ __decorate([
130
143
  __decorate([
131
144
  property({ attribute: false })
132
145
  ], HelixHeadlessLogLine.prototype, "logQuery", void 0);
146
+ __decorate([
147
+ property({ attribute: false })
148
+ ], HelixHeadlessLogLine.prototype, "logSelectable", void 0);
149
+ __decorate([
150
+ property({ attribute: false })
151
+ ], HelixHeadlessLogLine.prototype, "logSelected", void 0);
133
152
  __decorate([
134
153
  property({ attribute: false })
135
154
  ], HelixHeadlessLogLine.prototype, "logStart", void 0);
155
+ __decorate([
156
+ property({ attribute: false })
157
+ ], HelixHeadlessLogLine.prototype, "logStop", void 0);
136
158
  __decorate([
137
159
  property({ attribute: false })
138
160
  ], HelixHeadlessLogLine.prototype, "logTimes", void 0);
@@ -0,0 +1,36 @@
1
+ import type { LogParser } from './buffer.js';
2
+ /**
3
+ * Reads a GitHub Actions log: the workflow commands a step writes, such as
4
+ * `::group::Install` and `::error file=app.ts,line=3::Missing`, and the
5
+ * `##[group]` form and leading times of a log downloaded from a run.
6
+ *
7
+ * - `group` and `endgroup` open and close a step, which folds once it passes
8
+ * and stays open where a line in it is an error.
9
+ * - `error`, `warning`, `notice` and `debug` set the line's level, with the
10
+ * file, line, column and title in front of the message.
11
+ * - `command`, the line GitHub writes for each command a step runs, stays as
12
+ * it is.
13
+ * - `add-mask`, `set-output` and the other commands GitHub never shows are
14
+ * dropped. Text after `stop-commands` is read as commands still.
15
+ * - A leading time becomes the line's `at`, which times each step and which
16
+ * `timestamps` shows.
17
+ */
18
+ export declare const parseGitHubActions: LogParser;
19
+ /**
20
+ * Reads a Vercel build log, as the CLI prints it with `vercel inspect --logs`
21
+ * and the dashboard shows it: a time and two spaces in front of each line.
22
+ *
23
+ * - `Running "…"` opens a step named for what it runs, and the line is its
24
+ * first. `Build Completed in …` closes it and stands after it.
25
+ * - `Deploying outputs…` opens a step named for it, and `Deployment
26
+ * completed` closes it.
27
+ * - A line starting `Error`, `Failed` or `npm ERR!`, or holding `error:`, is
28
+ * at `error`, and one starting `Warning` or `warn` is at `warn`.
29
+ * - The leading time becomes the line's `at`, which times each step.
30
+ */
31
+ export declare const parseVercel: LogParser;
32
+ /** The parsers `format` names. */
33
+ export declare const LOG_FORMATS: {
34
+ readonly 'github-actions': LogParser;
35
+ readonly vercel: LogParser;
36
+ };
@@ -0,0 +1,101 @@
1
+ import { stripAnsi } from './ansi.js';
2
+ const STAMP = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z) {1,2}/;
3
+ const stamped = (raw) => {
4
+ const found = STAMP.exec(raw);
5
+ return found ? { at: found[1], text: raw.slice(found[0].length) } : { at: undefined, text: raw };
6
+ };
7
+ const timed = (at) => (at === undefined ? {} : { at });
8
+ const ANNOTATIONS = {
9
+ debug: 'debug',
10
+ error: 'error',
11
+ notice: 'info',
12
+ warning: 'warn',
13
+ };
14
+ const HIDDEN = new Set([
15
+ 'add-mask',
16
+ 'add-matcher',
17
+ 'add-path',
18
+ 'echo',
19
+ 'remove-matcher',
20
+ 'save-state',
21
+ 'set-env',
22
+ 'set-output',
23
+ 'stop-commands',
24
+ ]);
25
+ const unescaped = (text) => text
26
+ .replace(/%0D/gi, '')
27
+ .replace(/%0A/gi, ' ')
28
+ .replace(/%3A/gi, ':')
29
+ .replace(/%2C/gi, ',')
30
+ .replace(/%25/g, '%');
31
+ const located = (properties, message) => {
32
+ const read = new Map();
33
+ for (const pair of properties.split(',')) {
34
+ const at = pair.indexOf('=');
35
+ if (at > 0) {
36
+ read.set(pair.slice(0, at).trim(), unescaped(pair.slice(at + 1).trim()));
37
+ }
38
+ }
39
+ const place = [read.get('file'), read.get('line'), read.get('col')].filter(Boolean).join(':');
40
+ const title = read.get('title');
41
+ return [place, title, unescaped(message)].filter(Boolean).join(': ');
42
+ };
43
+ export const parseGitHubActions = (raw) => {
44
+ const { at, text } = stamped(raw);
45
+ const plain = stripAnsi(text);
46
+ const downloaded = /^##\[([a-z-]+)\](.*)$/i.exec(plain);
47
+ const written = downloaded ? undefined : /^::([a-z-]+)(?: ([^:]*))?::(.*)$/i.exec(plain);
48
+ const kind = (downloaded?.[1] ?? written?.[1])?.toLowerCase();
49
+ if (kind === undefined) {
50
+ return at === undefined ? undefined : { at, text };
51
+ }
52
+ const properties = written?.[2] ?? '';
53
+ const body = downloaded?.[2] ?? written?.[3] ?? '';
54
+ if (kind === 'group') {
55
+ return { ...timed(at), open: unescaped(body).trim() || 'Group' };
56
+ }
57
+ if (kind === 'endgroup') {
58
+ return { ...timed(at), close: true };
59
+ }
60
+ const level = ANNOTATIONS[kind];
61
+ if (level !== undefined) {
62
+ return { ...timed(at), level, text: located(properties, body) };
63
+ }
64
+ if (kind === 'command') {
65
+ return { ...timed(at), text: body };
66
+ }
67
+ if (HIDDEN.has(kind)) {
68
+ return null;
69
+ }
70
+ return at === undefined ? undefined : { at, text };
71
+ };
72
+ const vercelLevel = (plain) => {
73
+ if (/^(?:error\b|failed\b|fatal\b|npm err!)|\berror:/i.test(plain)) {
74
+ return 'error';
75
+ }
76
+ if (/^(?:warning\b|warn\b|npm warn\b)|\bwarning:/i.test(plain)) {
77
+ return 'warn';
78
+ }
79
+ return undefined;
80
+ };
81
+ export const parseVercel = (raw) => {
82
+ const { at, text } = stamped(raw);
83
+ const plain = stripAnsi(text).trim();
84
+ const level = vercelLevel(plain);
85
+ const fields = { ...timed(at), ...(level === undefined ? {} : { level }), text };
86
+ const running = /^Running "([^"]+)"( command)?/.exec(plain);
87
+ if (running) {
88
+ return { ...fields, open: running[2] ? `${running[1]} command` : (running[1] ?? plain) };
89
+ }
90
+ if (/^Deploying outputs/i.test(plain)) {
91
+ return { ...fields, open: 'Deploying outputs' };
92
+ }
93
+ if (/^Build Completed in /i.test(plain) || /^Deployment completed/i.test(plain)) {
94
+ return { ...fields, close: true };
95
+ }
96
+ return at === undefined && level === undefined ? undefined : fields;
97
+ };
98
+ export const LOG_FORMATS = {
99
+ 'github-actions': parseGitHubActions,
100
+ vercel: parseVercel,
101
+ };