@bussolabs/closeyourit-cli 0.0.20 → 0.0.22

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 CHANGED
@@ -117,6 +117,9 @@ organization are stored locally.
117
117
  | `agents delete <id> --confirm` | Delete an agent and its run history. |
118
118
  | `agents runs list <agent-id> [--page]` | List an agent's run history. |
119
119
  | `agents runs show <agent-id> <run-id>` | Show a single run (with output). |
120
+ | `agents tokens list [--page]` | List the org automator tokens (cyi_a_). |
121
+ | `agents tokens create <name>` | Create an org automator token (secret shown only once). |
122
+ | `agents tokens revoke <id> --confirm` | Revoke an org automator token. |
120
123
  | `monitors create --project <id\|key> --url <url> [--environment] [--http-method] [--interval-seconds] [--expected-status] [--timeout-seconds] [--expected-body-keyword] [--ssl-expiry-warn-days] [--group-id]` | Create an uptime monitor (one per environment). |
121
124
  | `monitors update <id> --project <id\|key> [--url] [--http-method] [--interval-seconds] [--expected-status] [--timeout-seconds] [--expected-body-keyword] [--ssl-expiry-warn-days] [--group-id]` | Update an uptime monitor. |
122
125
  | `monitors publish\|unpublish <id> --project <id\|key>` | Publish / unpublish the monitor's public status page. |
@@ -24,7 +24,7 @@ class AgentsList extends base_1.BaseCommand {
24
24
  String(agent.kind ?? ''),
25
25
  String(agent.schedule ?? ''),
26
26
  String(agent.enabled ?? ''),
27
- String(agent.last_run_status ?? '-'),
27
+ agent.last_run_status ? (0, output_1.statusCell)(agent.last_run_status) : '-',
28
28
  String(agent.id ?? ''),
29
29
  ])));
30
30
  }
@@ -19,7 +19,7 @@ class AgentsRunsList extends base_1.BaseCommand {
19
19
  const runs = res.data ?? [];
20
20
  if (!this.jsonEnabled()) {
21
21
  this.log((0, output_1.renderTable)(['STATUS', 'PROJECT', 'DURATION', 'STARTED', 'ID'], runs.map((run) => [
22
- String(run.status ?? ''),
22
+ (0, output_1.statusCell)(run.status),
23
23
  String(run.project_id ?? '-'),
24
24
  run.duration_ms == null ? '-' : `${run.duration_ms} ms`,
25
25
  String(run.started_at ?? '-'),
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class AgentsTokensCreate extends BaseCommand {
3
+ static args: {
4
+ name: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<unknown>;
9
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ class AgentsTokensCreate extends base_1.BaseCommand {
6
+ static args = {
7
+ name: core_1.Args.string({ description: 'Token name (e.g. automator)', required: true }),
8
+ };
9
+ static description = 'Create an org automator token (cyi_a_ — the secret is shown only once)';
10
+ static examples = ['<%= config.bin %> agents tokens create automator'];
11
+ async run() {
12
+ const { args } = await this.parse(AgentsTokensCreate);
13
+ const res = await this.api.post('/cli/v1/agents/tokens', { name: args.name });
14
+ const token = res.data ?? {};
15
+ if (!this.jsonEnabled()) {
16
+ this.log('Automator token created. Store the secret now — it is shown only once:');
17
+ this.log('');
18
+ this.log(` secret: ${token.secret ?? '-'}`);
19
+ this.log('');
20
+ this.warn('The secret cannot be retrieved again. If you lose it, revoke and create a new one.');
21
+ }
22
+ return res;
23
+ }
24
+ }
25
+ exports.default = AgentsTokensCreate;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class AgentsTokensList extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<unknown>;
9
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const base_1 = require("../../../base");
4
+ const output_1 = require("../../../lib/output");
5
+ class AgentsTokensList extends base_1.BaseCommand {
6
+ static description = 'List the org automator tokens (closeyourit-automator authenticates with these)';
7
+ static examples = ['<%= config.bin %> agents tokens list'];
8
+ static flags = { ...base_1.pageFlag };
9
+ async run() {
10
+ const { flags } = await this.parse(AgentsTokensList);
11
+ const res = await this.api.get(`/cli/v1/agents/tokens?page=${flags.page}`);
12
+ const tokens = res.data ?? [];
13
+ if (!this.jsonEnabled()) {
14
+ this.log((0, output_1.renderTable)(['NAME', 'PREFIX', 'LAST USED', 'REVOKED', 'ID'], tokens.map((token) => [
15
+ String(token.name ?? ''),
16
+ String(token.token_prefix ?? ''),
17
+ String(token.last_used_at ?? '-'),
18
+ String(token.revoked_at ?? '-'),
19
+ String(token.id ?? ''),
20
+ ])));
21
+ }
22
+ return res;
23
+ }
24
+ }
25
+ exports.default = AgentsTokensList;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class AgentsTokensRevoke extends BaseCommand {
3
+ static args: {
4
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ confirm: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ class AgentsTokensRevoke extends base_1.BaseCommand {
6
+ static args = {
7
+ id: core_1.Args.string({ description: 'Automator token id', required: true }),
8
+ };
9
+ static description = 'Revoke an org automator token (the automator using it stops syncing)';
10
+ static examples = ['<%= config.bin %> agents tokens revoke <token-id> --confirm'];
11
+ static flags = {
12
+ confirm: core_1.Flags.boolean({ description: 'Required: confirm the revocation' }),
13
+ };
14
+ async run() {
15
+ const { args, flags } = await this.parse(AgentsTokensRevoke);
16
+ if (!flags.confirm) {
17
+ this.error('Refusing to revoke without --confirm.', { exit: 2 });
18
+ }
19
+ await this.api.delete(`/cli/v1/agents/tokens/${encodeURIComponent(args.id)}`);
20
+ if (!this.jsonEnabled())
21
+ this.log(`Revoked automator token ${args.id}`);
22
+ return { revoked: args.id };
23
+ }
24
+ }
25
+ exports.default = AgentsTokensRevoke;
@@ -26,7 +26,7 @@ class CronMonitorsList extends base_1.BaseCommand {
26
26
  if (!this.jsonEnabled()) {
27
27
  this.log((0, output_1.renderTable)(['NAME', 'STATUS', 'INTERVAL (min)', 'LAST CHECK-IN', 'ID'], monitors.map((monitor) => [
28
28
  String(monitor.name ?? ''),
29
- String(monitor.status ?? ''),
29
+ (0, output_1.statusCell)(monitor.status),
30
30
  String(monitor.expected_interval_minutes ?? '-'),
31
31
  String(monitor.last_check_in_at ?? '-'),
32
32
  String(monitor.id ?? ''),
@@ -26,7 +26,7 @@ class ErrorsList extends base_1.BaseCommand {
26
26
  this.log((0, output_1.renderTable)(['ID', 'TITLE', 'STATUS', 'EVENTS'], groups.map((group) => [
27
27
  String(group.id ?? ''),
28
28
  String(group.title ?? ''),
29
- String(group.status ?? ''),
29
+ (0, output_1.statusCell)(group.status),
30
30
  String(group.events_count ?? ''),
31
31
  ])));
32
32
  }
@@ -29,7 +29,7 @@ class IdeasList extends base_1.BaseCommand {
29
29
  this.log((0, output_1.renderTable)(['ID', 'TITLE', 'STATUS', 'VOTES', 'COMMENTS'], ideas.map((idea) => [
30
30
  String(idea.id ?? ''),
31
31
  String(idea.title ?? ''),
32
- String(idea.status ?? ''),
32
+ (0, output_1.statusCell)(idea.status),
33
33
  String(idea.votes_count ?? 0),
34
34
  String(idea.comments_count ?? 0),
35
35
  ])));
@@ -18,14 +18,14 @@ class IdeasShow extends base_1.BaseCommand {
18
18
  const { problem, solution, stakeholders, ...fields } = res.data ?? {};
19
19
  this.log((0, output_1.renderRecord)(fields));
20
20
  if (Array.isArray(stakeholders) && stakeholders.length > 0) {
21
- this.log(`\nStakeholders: ${stakeholders.join(', ')}`);
21
+ this.log(`\n👥 Stakeholders: ${stakeholders.join(', ')}`);
22
22
  }
23
23
  if (problem) {
24
- this.log('\nProblem:');
24
+ this.log('\n🎯 Problem:');
25
25
  this.log((0, output_1.sanitizeMultiline)(String(problem)));
26
26
  }
27
27
  if (solution) {
28
- this.log('\nSolution:');
28
+ this.log('\n💡 Solution:');
29
29
  this.log((0, output_1.sanitizeMultiline)(String(solution)));
30
30
  }
31
31
  }
@@ -33,7 +33,7 @@ class KbAsk extends base_1.BaseCommand {
33
33
  this.log((0, output_1.sanitizeMultiline)(data.answer));
34
34
  const pages = Array.isArray(data.pages) ? data.pages : [];
35
35
  if (pages.length > 0) {
36
- this.log('\nSources:');
36
+ this.log('\n📚 Sources:');
37
37
  for (const page of pages) {
38
38
  this.log(` [${(0, output_1.sanitize)(page.kind)}] ${(0, output_1.sanitize)(page.title)} · ${(0, output_1.sanitize)(page.id)}`);
39
39
  }
@@ -18,7 +18,7 @@ class MonitorsList extends base_1.BaseCommand {
18
18
  this.log((0, output_1.renderTable)(['NAME', 'URL', 'STATUS', 'ACTIVE', 'ID'], monitors.map((monitor) => [
19
19
  String(monitor.name ?? ''),
20
20
  String(monitor.url ?? ''),
21
- String(monitor.current_status ?? ''),
21
+ (0, output_1.statusCell)(monitor.current_status),
22
22
  String(monitor.active ?? ''),
23
23
  String(monitor.id ?? ''),
24
24
  ])));
@@ -27,7 +27,7 @@ class ServersList extends base_1.BaseCommand {
27
27
  if (!this.jsonEnabled()) {
28
28
  this.log((0, output_1.renderTable)(['NAME', 'STATUS', 'CPU%', 'MEM%', 'DISK%', 'LAST SEEN', 'ID'], hosts.map((host) => [
29
29
  String(host.name ?? ''),
30
- String(host.status ?? ''),
30
+ (0, output_1.statusCell)(host.status),
31
31
  String(host.cpu_pct ?? '-'),
32
32
  String(host.mem_pct ?? '-'),
33
33
  String(host.disk_pct ?? '-'),
@@ -15,7 +15,7 @@ class TicketsList extends base_1.BaseCommand {
15
15
  this.log((0, output_1.renderTable)(['CODE', 'TITLE', 'STATUS'], tickets.map((ticket) => [
16
16
  String(ticket.code ?? ''),
17
17
  String(ticket.title ?? ''),
18
- String(ticket.status ?? ''),
18
+ (0, output_1.statusCell)(ticket.status, typeof ticket.status_color === 'string' ? ticket.status_color : undefined),
19
19
  ])));
20
20
  }
21
21
  return res;
@@ -46,7 +46,7 @@ class TicketsShow extends base_1.BaseCommand {
46
46
  return all;
47
47
  }
48
48
  renderAttachments(attachments) {
49
- this.log(`\nAttachments (${attachments.length})`);
49
+ this.log(`\n📎 Attachments (${attachments.length})`);
50
50
  if (attachments.length === 0)
51
51
  return;
52
52
  this.log((0, output_1.renderTable)(['ID', 'FILENAME', 'CONTENT_TYPE', 'BYTE_SIZE', 'CREATED_AT'], attachments.map((attachment) => [
@@ -58,7 +58,7 @@ class TicketsShow extends base_1.BaseCommand {
58
58
  ])));
59
59
  }
60
60
  renderComments(comments) {
61
- this.log(`\nComments (${comments.length})`);
61
+ this.log(`\n💬 Comments (${comments.length})`);
62
62
  for (const comment of comments) {
63
63
  this.log(`\n── ${(0, output_1.sanitize)(comment.author ?? '?')} · ${(0, output_1.sanitize)(comment.created_at ?? '')} · ${(0, output_1.sanitize)(comment.id ?? '')}`);
64
64
  this.log((0, output_1.sanitizeMultiline)(comment.body ?? ''));
@@ -90,13 +90,13 @@ class TicketsShow extends base_1.BaseCommand {
90
90
  }));
91
91
  // Body: description + technical analysis (only if present).
92
92
  const scalarBlocks = [
93
- ['Description', ticket.description],
94
- ['Technical analysis', ticket.technical_analysis],
93
+ ['📝', 'Description', ticket.description],
94
+ ['🔧', 'Technical analysis', ticket.technical_analysis],
95
95
  ];
96
- for (const [label, value] of scalarBlocks) {
96
+ for (const [icon, label, value] of scalarBlocks) {
97
97
  if (value === null || value === undefined || String(value).trim() === '')
98
98
  continue;
99
- this.log(`\n${label}\n${'-'.repeat(label.length)}`);
99
+ this.log((0, output_1.section)(label, icon));
100
100
  this.log((0, output_1.sanitizeMultiline)(value));
101
101
  }
102
102
  this.renderScenarios(Array.isArray(ticket.scenarios) ? ticket.scenarios : []);
@@ -112,7 +112,7 @@ class TicketsShow extends base_1.BaseCommand {
112
112
  ];
113
113
  scenarios.forEach((scenario, index) => {
114
114
  const title = scenario.title ? `Scenario ${index + 1}: ${(0, output_1.sanitize)(scenario.title)}` : `Scenario ${index + 1}`;
115
- this.log(`\n${title}\n${'-'.repeat(title.length)}`);
115
+ this.log((0, output_1.section)(title, '🧪'));
116
116
  for (const [label, key] of steps) {
117
117
  const value = scenario[key];
118
118
  if (value === null || value === undefined || String(value).trim() === '')
@@ -125,7 +125,7 @@ class TicketsShow extends base_1.BaseCommand {
125
125
  renderConditions(conditions) {
126
126
  if (conditions.length === 0)
127
127
  return;
128
- this.log('\nDefinition of Done\n------------------');
128
+ this.log((0, output_1.section)('Definition of Done', '✅'));
129
129
  for (const condition of conditions)
130
130
  this.log(`- ${(0, output_1.sanitizeMultiline)(condition.text ?? '')}`);
131
131
  }
@@ -16,7 +16,7 @@ class TokensList extends base_1.BaseCommand {
16
16
  String(token.id ?? ''),
17
17
  String(token.name ?? ''),
18
18
  String(token.token_prefix ?? ''),
19
- String(token.status ?? ''),
19
+ (0, output_1.statusCell)(token.status),
20
20
  ])));
21
21
  }
22
22
  return res;
@@ -11,7 +11,23 @@ export declare function sanitize(value: unknown): string;
11
11
  * Every other C0/C1 control char is still defused per line.
12
12
  */
13
13
  export declare function sanitizeMultiline(value: unknown): string;
14
+ /** Terminal display width of a string (wide code points count as 2, zero-width joiners/VS as 0). */
15
+ export declare function displayWidth(value: string): number;
14
16
  /** Minimal dependency-free table renderer for human output. */
15
17
  export declare function renderTable(headers: string[], rows: string[][]): string;
16
- /** Pretty-print the scalar fields of a record as `key: value` lines. */
18
+ /**
19
+ * Status dots make problems scannable at a glance. Four fixed-width circles (no variation
20
+ * selectors, so table columns stay aligned): down=🔴, warn=🟡, ok=🟢, neutral=⚪.
21
+ */
22
+ export type StatusTone = 'down' | 'warn' | 'ok' | 'neutral';
23
+ /**
24
+ * Status dot for a value. With `color` (a Tailwind family name) it uses the colour map — the stable
25
+ * token for org-renamed status/priority; otherwise the monitoring string-enum map. Unknown → neutral.
26
+ */
27
+ export declare function statusDot(value: unknown, color?: string): string;
28
+ /** A table cell with its status dot prefixed (the emoji survives `sanitize`). */
29
+ export declare function statusCell(value: unknown, color?: string): string;
30
+ /** A section heading: optional emoji + title, underlined with dashes sized to the title. */
31
+ export declare function section(title: string, icon?: string): string;
32
+ /** Pretty-print the scalar fields of a record as `key: value` lines (status keys get a dot). */
17
33
  export declare function renderRecord(obj: Record<string, unknown>): string;
@@ -2,7 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitize = sanitize;
4
4
  exports.sanitizeMultiline = sanitizeMultiline;
5
+ exports.displayWidth = displayWidth;
5
6
  exports.renderTable = renderTable;
7
+ exports.statusDot = statusDot;
8
+ exports.statusCell = statusCell;
9
+ exports.section = section;
6
10
  exports.renderRecord = renderRecord;
7
11
  /**
8
12
  * Neutralize terminal control sequences (CWE-150) in server-supplied strings.
@@ -29,6 +33,32 @@ function sanitizeMultiline(value) {
29
33
  .map((line) => sanitize(line))
30
34
  .join('\n');
31
35
  }
36
+ // Code-point ranges that render as 2 terminal columns (CJK + emoji + symbols). Emoji like 🔴 have a
37
+ // JS `.length` of 2 while ⚪ (U+26AA) has `.length` 1 — yet both render 2-wide, so padding a column by
38
+ // `.length` misaligns rows that mix them. Measuring by code point keeps the status column square.
39
+ const WIDE_RANGES = [
40
+ [0x1100, 0x115f], // Hangul Jamo
41
+ [0x2600, 0x27bf], // Misc symbols + Dingbats (⚪ ⚫ ✅ …)
42
+ [0x2b00, 0x2bff], // Misc symbols and arrows
43
+ [0x2e80, 0xa4cf], // CJK & friends
44
+ [0xac00, 0xd7a3], // Hangul syllables
45
+ [0xf900, 0xfaff], // CJK compatibility ideographs
46
+ [0xfe30, 0xfe4f], // CJK compatibility forms
47
+ [0xff00, 0xff60], // Fullwidth forms
48
+ [0xffe0, 0xffe6], // Fullwidth signs
49
+ [0x1f000, 0x1faff], // emoji & pictographs (🔴 🟡 🟢 …)
50
+ ];
51
+ /** Terminal display width of a string (wide code points count as 2, zero-width joiners/VS as 0). */
52
+ function displayWidth(value) {
53
+ let width = 0;
54
+ for (const ch of value) {
55
+ const cp = ch.codePointAt(0) ?? 0;
56
+ if (cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0x0300 && cp <= 0x036f))
57
+ continue;
58
+ width += WIDE_RANGES.some(([lo, hi]) => cp >= lo && cp <= hi) ? 2 : 1;
59
+ }
60
+ return width;
61
+ }
32
62
  /** Minimal dependency-free table renderer for human output. */
33
63
  function renderTable(headers, rows) {
34
64
  const safeHeaders = headers.map(sanitize);
@@ -36,15 +66,120 @@ function renderTable(headers, rows) {
36
66
  if (safeRows.length === 0) {
37
67
  return `${safeHeaders.join(' ')}\n(no results)`;
38
68
  }
39
- const widths = safeHeaders.map((header, col) => Math.max(header.length, ...safeRows.map((row) => (row[col] ?? '').length)));
40
- const line = (cols) => cols.map((cell, col) => (cell ?? '').padEnd(widths[col])).join(' ').trimEnd();
69
+ const widths = safeHeaders.map((header, col) => Math.max(displayWidth(header), ...safeRows.map((row) => displayWidth(row[col] ?? ''))));
70
+ const pad = (cell, col) => cell + ' '.repeat(Math.max(0, widths[col] - displayWidth(cell)));
71
+ const line = (cols) => cols.map((cell, col) => pad(cell ?? '', col)).join(' ').trimEnd();
41
72
  const separator = widths.map((width) => '-'.repeat(width));
42
73
  return [line(safeHeaders), line(separator), ...safeRows.map(line)].join('\n');
43
74
  }
44
- /** Pretty-print the scalar fields of a record as `key: value` lines. */
75
+ const TONE_DOT = {
76
+ down: '🔴',
77
+ neutral: '⚪',
78
+ ok: '🟢',
79
+ warn: '🟡',
80
+ };
81
+ /**
82
+ * Stable enum strings of the monitoring domains → tone (keys are lowercase). These come straight
83
+ * from the model enums and never get org-renamed, so the CLI maps them without any backend help.
84
+ * Caveats baked in: agent-run failure is `fail` (not `failed`); a paused monitor/host is neutral.
85
+ */
86
+ const STATUS_TONE = {
87
+ // ideas
88
+ archived: 'neutral',
89
+ // errors — status
90
+ ignored: 'neutral',
91
+ resolved: 'ok',
92
+ unresolved: 'down',
93
+ // errors — level
94
+ debug: 'neutral',
95
+ error: 'down',
96
+ fatal: 'down',
97
+ info: 'ok',
98
+ warning: 'warn',
99
+ // uptime monitor + server host
100
+ down: 'down',
101
+ paused: 'neutral',
102
+ pending: 'warn',
103
+ unknown: 'neutral',
104
+ up: 'ok',
105
+ // cron monitor
106
+ late: 'warn',
107
+ missed: 'down',
108
+ ok: 'ok',
109
+ // ideas
110
+ converted: 'ok',
111
+ open: 'warn',
112
+ // incident phase
113
+ detected: 'down',
114
+ fixing: 'warn',
115
+ investigating: 'warn',
116
+ monitoring: 'warn',
117
+ // agent run
118
+ fail: 'down',
119
+ running: 'warn',
120
+ success: 'ok',
121
+ // token
122
+ active: 'ok',
123
+ expired: 'neutral',
124
+ revoked: 'neutral',
125
+ };
126
+ /**
127
+ * Badge colour (Tailwind family name) → tone. Ticket status/priority labels are org-customisable,
128
+ * so the serializer ships the badge colour as a stable token and the CLI translates it here —
129
+ * identical to what the web badge shows. Unknown colour → falls back to the value string map.
130
+ */
131
+ const COLOR_TONE = {
132
+ amber: 'warn',
133
+ emerald: 'ok',
134
+ gray: 'neutral',
135
+ green: 'ok',
136
+ indigo: 'warn',
137
+ orange: 'down',
138
+ red: 'down',
139
+ sky: 'ok',
140
+ teal: 'ok',
141
+ violet: 'warn',
142
+ };
143
+ /**
144
+ * Status dot for a value. With `color` (a Tailwind family name) it uses the colour map — the stable
145
+ * token for org-renamed status/priority; otherwise the monitoring string-enum map. Unknown → neutral.
146
+ */
147
+ function statusDot(value, color) {
148
+ const tone = (color ? COLOR_TONE[color.trim().toLowerCase()] : undefined) ??
149
+ STATUS_TONE[String(value ?? '').trim().toLowerCase()] ??
150
+ 'neutral';
151
+ return TONE_DOT[tone];
152
+ }
153
+ /** A table cell with its status dot prefixed (the emoji survives `sanitize`). */
154
+ function statusCell(value, color) {
155
+ return `${statusDot(value, color)} ${value ?? ''}`;
156
+ }
157
+ /** A section heading: optional emoji + title, underlined with dashes sized to the title. */
158
+ function section(title, icon) {
159
+ return `\n${icon ? `${icon} ` : ''}${title}\n${'-'.repeat(title.length)}`;
160
+ }
161
+ // Record keys whose value is a status/level/phase → get a dot. Keys with a `<key>_color` sibling
162
+ // (ticket status/priority) dot by colour; the enum-string keys below dot by the string map.
163
+ const ENUM_STATUS_KEYS = new Set(['status', 'current_status', 'last_run_status', 'level', 'phase']);
164
+ // Metadata keys used only to pick a dot colour — never printed as their own line.
165
+ const HIDDEN_KEYS = new Set(['status_color', 'priority_color']);
166
+ /** The dot prefix (with trailing space) for a record line, or '' when the key isn't status-like. */
167
+ function recordDot(key, value, obj) {
168
+ const color = obj[`${key}_color`];
169
+ if (typeof color === 'string' && color)
170
+ return `${statusDot(value, color)} `;
171
+ if (ENUM_STATUS_KEYS.has(key))
172
+ return `${statusDot(value)} `;
173
+ return '';
174
+ }
175
+ /** Pretty-print the scalar fields of a record as `key: value` lines (status keys get a dot). */
45
176
  function renderRecord(obj) {
46
177
  const lines = Object.entries(obj)
47
- .filter(([, value]) => value === null || ['string', 'number', 'boolean'].includes(typeof value))
48
- .map(([key, value]) => `${sanitize(key)}: ${value === null ? '-' : sanitize(value)}`);
178
+ .filter(([key, value]) => !HIDDEN_KEYS.has(key) && (value === null || ['string', 'number', 'boolean'].includes(typeof value)))
179
+ .map(([key, value]) => {
180
+ if (value === null)
181
+ return `${sanitize(key)}: -`;
182
+ return `${sanitize(key)}: ${recordDot(key, value, obj)}${sanitize(value)}`;
183
+ });
49
184
  return lines.length > 0 ? lines.join('\n') : JSON.stringify(obj, null, 2);
50
185
  }