@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 +3 -0
- package/dist/commands/agents/list.js +1 -1
- package/dist/commands/agents/runs/list.js +1 -1
- package/dist/commands/agents/tokens/create.d.ts +9 -0
- package/dist/commands/agents/tokens/create.js +25 -0
- package/dist/commands/agents/tokens/list.d.ts +9 -0
- package/dist/commands/agents/tokens/list.js +25 -0
- package/dist/commands/agents/tokens/revoke.d.ts +12 -0
- package/dist/commands/agents/tokens/revoke.js +25 -0
- package/dist/commands/cron-monitors/list.js +1 -1
- package/dist/commands/errors/list.js +1 -1
- package/dist/commands/ideas/list.js +1 -1
- package/dist/commands/ideas/show.js +3 -3
- package/dist/commands/kb/ask.js +1 -1
- package/dist/commands/monitors/list.js +1 -1
- package/dist/commands/servers/list.js +1 -1
- package/dist/commands/tickets/list.js +1 -1
- package/dist/commands/tickets/show.js +8 -8
- package/dist/commands/tokens/list.js +1 -1
- package/dist/lib/output.d.ts +17 -1
- package/dist/lib/output.js +140 -5
- package/oclif.manifest.json +3214 -3089
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`\
|
|
21
|
+
this.log(`\n👥 Stakeholders: ${stakeholders.join(', ')}`);
|
|
22
22
|
}
|
|
23
23
|
if (problem) {
|
|
24
|
-
this.log('\
|
|
24
|
+
this.log('\n🎯 Problem:');
|
|
25
25
|
this.log((0, output_1.sanitizeMultiline)(String(problem)));
|
|
26
26
|
}
|
|
27
27
|
if (solution) {
|
|
28
|
-
this.log('\
|
|
28
|
+
this.log('\n💡 Solution:');
|
|
29
29
|
this.log((0, output_1.sanitizeMultiline)(String(solution)));
|
|
30
30
|
}
|
|
31
31
|
}
|
package/dist/commands/kb/ask.js
CHANGED
|
@@ -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('\
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`\
|
|
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(`\
|
|
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(
|
|
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(
|
|
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('
|
|
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
|
}
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/lib/output.js
CHANGED
|
@@ -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
|
|
40
|
-
const
|
|
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
|
-
|
|
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]) =>
|
|
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
|
}
|