@devicecloud.dev/dcd 5.11.0-beta.1 → 5.11.0-beta.3

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.
@@ -0,0 +1,105 @@
1
+ import { colors } from './styling.js';
2
+ import { ui } from './ui.js';
3
+ function isRecord(value) {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ }
6
+ function text(value) {
7
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
8
+ }
9
+ function toFlow(raw, file) {
10
+ const success = raw.success === true;
11
+ return {
12
+ file: text(raw.file) ?? file,
13
+ success,
14
+ // A failed flow's reason is in `error`; some reports only have `message`.
15
+ error: text(raw.error) ?? (success ? null : text(raw.message)),
16
+ };
17
+ }
18
+ /** The runner's JSON report in `body`, or null when it is not one. */
19
+ function parseRunnerJson(body) {
20
+ if (!body || !body.trimStart().startsWith('{'))
21
+ return null;
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(body);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ if (!isRecord(parsed))
30
+ return null;
31
+ if (Array.isArray(parsed.results)) {
32
+ return {
33
+ flows: parsed.results.filter((r) => isRecord(r)).map((r) => toFlow(r, null)),
34
+ message: text(parsed.message),
35
+ };
36
+ }
37
+ if (typeof parsed.success !== 'boolean')
38
+ return null;
39
+ // One flow: the report is that flow's result.
40
+ return { flows: [toFlow(parsed, null)], message: text(parsed.message) };
41
+ }
42
+ /**
43
+ * The flows a run reported, or null when the result carries no report the
44
+ * CLI can read (plain text): then the text is shown as it came.
45
+ */
46
+ export function parseRunReport(result) {
47
+ if (Array.isArray(result.flows)) {
48
+ return { flows: result.flows.map((f) => toFlow({ ...f }, null)), message: null };
49
+ }
50
+ return parseRunnerJson(result.error) ?? parseRunnerJson(result.output);
51
+ }
52
+ /** The failure message for a failed run: each failing flow and why. */
53
+ export function runFailureMessage(result, report) {
54
+ if (!report)
55
+ return result.error || 'The flow failed.';
56
+ const failed = report.flows.filter((f) => !f.success);
57
+ if (failed.length === 0)
58
+ return report.message ?? 'The flow failed.';
59
+ return failed
60
+ .map((f) => `${f.file ?? 'flow'}: ${f.error ?? 'failed'}`)
61
+ .join('; ');
62
+ }
63
+ /**
64
+ * Print a run's result for a person. `label` names a flow the report does not
65
+ * (a single flow's report has no file name).
66
+ */
67
+ export function printRunResult(log, passed, result, label) {
68
+ const report = parseRunReport(result);
69
+ if (!report || report.flows.length === 0) {
70
+ log(passed ? ui.success('Flow passed') : `${ui.statusSymbol('FAILED')} Flow failed`);
71
+ if (report) {
72
+ if (report.message)
73
+ log(ui.note(report.message));
74
+ return;
75
+ }
76
+ // Plain text, not a report the CLI can read: show it as it came.
77
+ if (result.output) {
78
+ log(ui.section('Output'));
79
+ log(result.output);
80
+ }
81
+ if (result.error) {
82
+ log(ui.section('Error'));
83
+ log(colors.error(result.error));
84
+ }
85
+ return;
86
+ }
87
+ const { flows } = report;
88
+ const failed = flows.filter((f) => !f.success).length;
89
+ const total = flows.length;
90
+ let summary;
91
+ if (total === 1)
92
+ summary = passed ? 'Flow passed' : 'Flow failed';
93
+ else if (failed === 0)
94
+ summary = `${total} flows passed`;
95
+ else
96
+ summary = `${failed} of ${total} flows failed`;
97
+ log(passed && failed === 0 ? ui.success(summary) : `${ui.statusSymbol('FAILED')} ${summary}`);
98
+ const rows = flows.map((f) => [
99
+ f.file ?? label,
100
+ f.success
101
+ ? ui.statusWord('PASSED')
102
+ : `${ui.statusWord('FAILED')}${f.error ? ` ${colors.error(f.error)}` : ''}`,
103
+ ]);
104
+ log(ui.branch(ui.fields(rows)));
105
+ }
@@ -4,9 +4,13 @@ declare class Action {
4
4
  private _lastPrinted;
5
5
  private interactive;
6
6
  start(title: string, initialStatus?: string, _opts?: unknown): void;
7
+ /** End the action as a success: `✓ message`. */
7
8
  stop(message?: string): void;
9
+ /** End the action as a failure: `✗ message`. */
10
+ fail(message?: string): void;
8
11
  set status(value: string);
9
12
  get status(): string;
13
+ private finish;
10
14
  private print;
11
15
  }
12
16
  export declare const ux: {
@@ -10,9 +10,20 @@
10
10
  * non-interactive environments we skip the spinner entirely and instead print a
11
11
  * plain line once per *distinct* status, so CI logs show real progress without
12
12
  * the flood.
13
+ *
14
+ * Styling: the spinner uses the CLI's own palette (cyan braille frames) rather
15
+ * than clack's magenta default, and no `│` guide bar, since the rest of the CLI
16
+ * is a `⏺`/`⎿` tree rather than a clack session. The final line is written by
17
+ * us, not clack, so it carries the same `✓` / `✗` symbols as every other
18
+ * command (see STYLE_GUIDE.md).
13
19
  */
14
20
  import * as p from '@clack/prompts';
15
21
  import { isCI } from './ci.js';
22
+ import { colors, symbols } from './styling.js';
23
+ // Braille dots where the terminal can draw them, plain ASCII otherwise.
24
+ const FRAMES = p.unicode
25
+ ? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
26
+ : ['-', '\\', '|', '/'];
16
27
  class Action {
17
28
  current = null;
18
29
  _status = '';
@@ -23,37 +34,30 @@ class Action {
23
34
  }
24
35
  start(title, initialStatus, _opts) {
25
36
  this._status = initialStatus ?? '';
26
- const line = initialStatus ? `${title} — ${initialStatus}` : title;
37
+ const line = initialStatus ? `${title} ${colors.dim('·')} ${initialStatus}` : title;
27
38
  if (!this.interactive()) {
28
39
  this.current = null;
29
40
  this.print(line);
30
41
  return;
31
42
  }
32
43
  if (this.current) {
33
- this.current.stop();
44
+ this.current.clear();
34
45
  }
35
- this.current = p.spinner();
46
+ this.current = p.spinner({
47
+ delay: 80,
48
+ frames: FRAMES,
49
+ styleFrame: colors.highlight,
50
+ withGuide: false,
51
+ });
36
52
  this.current.start(line);
37
53
  }
54
+ /** End the action as a success: `✓ message`. */
38
55
  stop(message) {
39
- if (!this.interactive()) {
40
- if (message)
41
- this.print(message);
42
- this.current = null;
43
- this._status = '';
44
- this._lastPrinted = '';
45
- return;
46
- }
47
- if (!this.current) {
48
- if (message) {
49
- // eslint-disable-next-line no-console
50
- console.log(message);
51
- }
52
- return;
53
- }
54
- this.current.stop(message ?? '');
55
- this.current = null;
56
- this._status = '';
56
+ this.finish(symbols.success, message);
57
+ }
58
+ /** End the action as a failure: `✗ message`. */
59
+ fail(message) {
60
+ this.finish(symbols.error, message);
57
61
  }
58
62
  set status(value) {
59
63
  this._status = value;
@@ -70,6 +74,26 @@ class Action {
70
74
  get status() {
71
75
  return this._status;
72
76
  }
77
+ // Erase the spinner (if one is live) and print the final line ourselves, so
78
+ // the symbol matches the rest of the CLI instead of clack's `◇`.
79
+ finish(symbol, message) {
80
+ const line = message ? `${symbol} ${message}` : '';
81
+ if (!this.interactive()) {
82
+ if (line)
83
+ this.print(line);
84
+ this.current = null;
85
+ this._status = '';
86
+ this._lastPrinted = '';
87
+ return;
88
+ }
89
+ this.current?.clear();
90
+ this.current = null;
91
+ this._status = '';
92
+ if (line) {
93
+ // eslint-disable-next-line no-console
94
+ console.log(line);
95
+ }
96
+ }
73
97
  // Emit a line only when it differs from the previous one, so repeated polls
74
98
  // with no state change stay quiet.
75
99
  print(line) {
package/package.json CHANGED
@@ -68,7 +68,7 @@
68
68
  "test:unit": "node scripts/test-runner.mjs --unit",
69
69
  "typecheck": "tsc --noEmit -p tsconfig.test.json"
70
70
  },
71
- "version": "5.11.0-beta.1",
71
+ "version": "5.11.0-beta.3",
72
72
  "bugs": {
73
73
  "url": "https://discord.gg/gm3mJwcNw8"
74
74
  },
@@ -109,7 +109,7 @@
109
109
  "ws@>=8.0.0 <8.21.0": "8.21.0",
110
110
  "esbuild@<0.28.1": ">=0.28.1",
111
111
  "qs@<6.16.0": "6.16.0",
112
- "fast-uri@>=3.0.0 <3.1.8": "3.1.8",
112
+ "fast-uri@>=3.0.0 <3.1.9": "3.1.9",
113
113
  "hono@>=4.0.0 <4.13.11": "4.13.11",
114
114
  "source-map-js@<1.2.2": "1.2.2",
115
115
  "micromatch>picomatch": "^2.3.2",