@bussolabs/closeyourit-cli 0.0.9 → 0.0.11
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/alerts/channels/create.d.ts +16 -0
- package/dist/commands/alerts/channels/create.js +41 -0
- package/dist/commands/alerts/channels/delete.d.ts +12 -0
- package/dist/commands/alerts/channels/delete.js +25 -0
- package/dist/commands/alerts/channels/list.d.ts +9 -0
- package/dist/commands/alerts/channels/list.js +25 -0
- package/dist/commands/alerts/create.d.ts +2 -0
- package/dist/commands/alerts/create.js +8 -2
- package/dist/commands/alerts/update.d.ts +2 -0
- package/dist/commands/alerts/update.js +8 -2
- package/dist/commands/tickets/attachment-download.js +3 -3
- package/dist/commands/tickets/comments.js +9 -4
- package/dist/commands/tickets/show.d.ts +5 -0
- package/dist/commands/tickets/show.js +90 -6
- package/dist/lib/alert-events.d.ts +2 -0
- package/dist/lib/alert-events.js +21 -0
- package/dist/lib/output.d.ts +13 -0
- package/dist/lib/output.js +13 -0
- package/oclif.manifest.json +1035 -819
- package/opencli.json +84 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,9 @@ organization are stored locally.
|
|
|
72
72
|
| `servers tokens list` | List the fleet enrollment tokens. |
|
|
73
73
|
| `servers tokens create <name>` | Create an enrollment token (secret shown once). |
|
|
74
74
|
| `servers tokens revoke <id> --confirm` | Revoke an enrollment token. |
|
|
75
|
+
| `alerts channels list` | List the external alert channels (webhook / Telegram). |
|
|
76
|
+
| `alerts channels create <name> --kind telegram\|webhook …` | Create a channel (`--bot-token`/`--chat-id` or `--url`/`--secret`). |
|
|
77
|
+
| `alerts channels delete <id> --confirm` | Delete a channel. |
|
|
75
78
|
|
|
76
79
|
Every command accepts `--json` for machine-readable output and `--help` for usage details.
|
|
77
80
|
`--project` accepts either a project UUID or its key (matched case-insensitively).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base';
|
|
2
|
+
export default class AlertsChannelsCreate 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
|
+
static flags: {
|
|
9
|
+
kind: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
|
+
url: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
|
+
secret: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
'bot-token': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
'chat-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<unknown>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const base_1 = require("../../../base");
|
|
5
|
+
const output_1 = require("../../../lib/output");
|
|
6
|
+
class AlertsChannelsCreate extends base_1.BaseCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
name: core_1.Args.string({ description: 'Channel name', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Create an external alert channel (webhook or Telegram)';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> alerts channels create "Ops Telegram" --kind telegram --bot-token <token> --chat-id <id>',
|
|
13
|
+
'<%= config.bin %> alerts channels create "Ops hook" --kind webhook --url https://example.com/hook --secret s3cret',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
kind: core_1.Flags.string({ description: 'Channel kind', options: ['webhook', 'telegram'], required: true }),
|
|
17
|
+
url: core_1.Flags.string({ description: 'Webhook URL (kind webhook)' }),
|
|
18
|
+
secret: core_1.Flags.string({ description: 'Webhook signing secret (kind webhook, optional)' }),
|
|
19
|
+
'bot-token': core_1.Flags.string({ description: 'Telegram bot token from @BotFather (kind telegram)' }),
|
|
20
|
+
'chat-id': core_1.Flags.string({ description: 'Telegram chat id receiving the messages (kind telegram)' }),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(AlertsChannelsCreate);
|
|
24
|
+
const body = { name: args.name, kind: flags.kind };
|
|
25
|
+
if (flags.url !== undefined)
|
|
26
|
+
body.webhook_url = flags.url;
|
|
27
|
+
if (flags.secret !== undefined)
|
|
28
|
+
body.webhook_secret = flags.secret;
|
|
29
|
+
if (flags['bot-token'] !== undefined)
|
|
30
|
+
body.telegram_bot_token = flags['bot-token'];
|
|
31
|
+
if (flags['chat-id'] !== undefined)
|
|
32
|
+
body.telegram_chat_id = flags['chat-id'];
|
|
33
|
+
const res = await this.api.post('/cli/v1/alert_channels', body);
|
|
34
|
+
if (!this.jsonEnabled()) {
|
|
35
|
+
this.log('Channel created:');
|
|
36
|
+
this.log((0, output_1.renderRecord)(res.data ?? {}));
|
|
37
|
+
}
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.default = AlertsChannelsCreate;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base';
|
|
2
|
+
export default class AlertsChannelsDelete 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 AlertsChannelsDelete extends base_1.BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
id: core_1.Args.string({ description: 'Channel id', required: true }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Delete an external alert channel (rules keep delivering in-app/email)';
|
|
10
|
+
static examples = ['<%= config.bin %> alerts channels delete <channel-id> --confirm'];
|
|
11
|
+
static flags = {
|
|
12
|
+
confirm: core_1.Flags.boolean({ description: 'Required: confirm the deletion' }),
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { args, flags } = await this.parse(AlertsChannelsDelete);
|
|
16
|
+
if (!flags.confirm) {
|
|
17
|
+
this.error('Refusing to delete without --confirm.', { exit: 2 });
|
|
18
|
+
}
|
|
19
|
+
await this.api.delete(`/cli/v1/alert_channels/${encodeURIComponent(args.id)}`);
|
|
20
|
+
if (!this.jsonEnabled())
|
|
21
|
+
this.log(`Deleted channel ${args.id}`);
|
|
22
|
+
return { deleted: args.id };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.default = AlertsChannelsDelete;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base';
|
|
2
|
+
export default class AlertsChannelsList 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 AlertsChannelsList extends base_1.BaseCommand {
|
|
6
|
+
static description = 'List the external alert channels (webhook / Telegram)';
|
|
7
|
+
static examples = ['<%= config.bin %> alerts channels list'];
|
|
8
|
+
static flags = { ...base_1.pageFlag };
|
|
9
|
+
async run() {
|
|
10
|
+
const { flags } = await this.parse(AlertsChannelsList);
|
|
11
|
+
const res = await this.api.get(`/cli/v1/alert_channels?page=${flags.page}`);
|
|
12
|
+
const channels = res.data ?? [];
|
|
13
|
+
if (!this.jsonEnabled()) {
|
|
14
|
+
this.log((0, output_1.renderTable)(['NAME', 'KIND', 'TARGET', 'ENABLED', 'ID'], channels.map((channel) => [
|
|
15
|
+
String(channel.name ?? ''),
|
|
16
|
+
String(channel.kind ?? ''),
|
|
17
|
+
String(channel.target ?? '-'),
|
|
18
|
+
String(channel.enabled ?? ''),
|
|
19
|
+
String(channel.id ?? ''),
|
|
20
|
+
])));
|
|
21
|
+
}
|
|
22
|
+
return res;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.default = AlertsChannelsList;
|
|
@@ -9,6 +9,8 @@ export default class AlertsCreate extends BaseCommand {
|
|
|
9
9
|
'event-type': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
10
|
'min-level': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
11
|
'threshold-ms': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
threshold: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
channel: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
14
|
'throttle-minutes': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
15
|
enabled: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
16
|
project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const base_1 = require("../../base");
|
|
5
|
+
const alert_events_1 = require("../../lib/alert-events");
|
|
5
6
|
const output_1 = require("../../lib/output");
|
|
6
|
-
const EVENT_TYPES = ['error_new', 'error_regression', 'uptime_down', 'uptime_up', 'metric_threshold'];
|
|
7
7
|
class AlertsCreate extends base_1.BaseCommand {
|
|
8
8
|
static args = {
|
|
9
9
|
name: core_1.Args.string({ description: 'Rule name', required: true }),
|
|
@@ -14,9 +14,11 @@ class AlertsCreate extends base_1.BaseCommand {
|
|
|
14
14
|
'<%= config.bin %> alerts create "API down" --event-type uptime_down --project acme-api --throttle-minutes 30',
|
|
15
15
|
];
|
|
16
16
|
static flags = {
|
|
17
|
-
'event-type': core_1.Flags.string({ description: 'Event that triggers the rule', options:
|
|
17
|
+
'event-type': core_1.Flags.string({ description: 'Event that triggers the rule', options: alert_events_1.ALERT_EVENT_TYPES, required: true }),
|
|
18
18
|
'min-level': core_1.Flags.string({ description: 'Minimum error level that triggers the rule' }),
|
|
19
19
|
'threshold-ms': core_1.Flags.integer({ description: 'Duration threshold in milliseconds (metric rules)' }),
|
|
20
|
+
threshold: core_1.Flags.integer({ description: 'Server threshold: percent for cpu/mem/disk, °C for temp' }),
|
|
21
|
+
channel: core_1.Flags.string({ description: 'External channel id to deliver to (repeatable; REPLACES the set)', multiple: true }),
|
|
20
22
|
'throttle-minutes': core_1.Flags.integer({ description: 'Minimum minutes between notifications' }),
|
|
21
23
|
enabled: core_1.Flags.boolean({ allowNo: true, description: 'Whether the rule is enabled' }),
|
|
22
24
|
project: core_1.Flags.string({ description: 'Scope the rule to a project id (UUID)' }),
|
|
@@ -29,6 +31,10 @@ class AlertsCreate extends base_1.BaseCommand {
|
|
|
29
31
|
body.min_level = flags['min-level'];
|
|
30
32
|
if (flags['threshold-ms'] !== undefined)
|
|
31
33
|
body.threshold_ms = flags['threshold-ms'];
|
|
34
|
+
if (flags.threshold !== undefined)
|
|
35
|
+
body.threshold = flags.threshold;
|
|
36
|
+
if (flags.channel !== undefined)
|
|
37
|
+
body.channel_ids = flags.channel;
|
|
32
38
|
if (flags['throttle-minutes'] !== undefined)
|
|
33
39
|
body.throttle_minutes = flags['throttle-minutes'];
|
|
34
40
|
if (flags.enabled !== undefined)
|
|
@@ -10,6 +10,8 @@ export default class AlertsUpdate extends BaseCommand {
|
|
|
10
10
|
'event-type': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
11
|
'min-level': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
12
|
'threshold-ms': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
threshold: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
channel: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
15
|
'throttle-minutes': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
16
|
enabled: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const base_1 = require("../../base");
|
|
5
|
+
const alert_events_1 = require("../../lib/alert-events");
|
|
5
6
|
const output_1 = require("../../lib/output");
|
|
6
|
-
const EVENT_TYPES = ['error_new', 'error_regression', 'uptime_down', 'uptime_up', 'metric_threshold'];
|
|
7
7
|
class AlertsUpdate extends base_1.BaseCommand {
|
|
8
8
|
static args = {
|
|
9
9
|
id: core_1.Args.string({ description: 'Alert rule id', required: true }),
|
|
@@ -15,9 +15,11 @@ class AlertsUpdate extends base_1.BaseCommand {
|
|
|
15
15
|
];
|
|
16
16
|
static flags = {
|
|
17
17
|
name: core_1.Flags.string({ description: 'Rule name' }),
|
|
18
|
-
'event-type': core_1.Flags.string({ description: 'Event that triggers the rule', options:
|
|
18
|
+
'event-type': core_1.Flags.string({ description: 'Event that triggers the rule', options: alert_events_1.ALERT_EVENT_TYPES }),
|
|
19
19
|
'min-level': core_1.Flags.string({ description: 'Minimum error level that triggers the rule' }),
|
|
20
20
|
'threshold-ms': core_1.Flags.integer({ description: 'Duration threshold in milliseconds (metric rules)' }),
|
|
21
|
+
threshold: core_1.Flags.integer({ description: 'Server threshold: percent for cpu/mem/disk, °C for temp' }),
|
|
22
|
+
channel: core_1.Flags.string({ description: 'External channel id to deliver to (repeatable; REPLACES the set)', multiple: true }),
|
|
21
23
|
'throttle-minutes': core_1.Flags.integer({ description: 'Minimum minutes between notifications' }),
|
|
22
24
|
enabled: core_1.Flags.boolean({ allowNo: true, description: 'Whether the rule is enabled' }),
|
|
23
25
|
project: core_1.Flags.string({ description: 'Scope the rule to a project id (UUID)' }),
|
|
@@ -34,6 +36,10 @@ class AlertsUpdate extends base_1.BaseCommand {
|
|
|
34
36
|
body.min_level = flags['min-level'];
|
|
35
37
|
if (flags['threshold-ms'] !== undefined)
|
|
36
38
|
body.threshold_ms = flags['threshold-ms'];
|
|
39
|
+
if (flags.threshold !== undefined)
|
|
40
|
+
body.threshold = flags.threshold;
|
|
41
|
+
if (flags.channel !== undefined)
|
|
42
|
+
body.channel_ids = flags.channel;
|
|
37
43
|
if (flags['throttle-minutes'] !== undefined)
|
|
38
44
|
body.throttle_minutes = flags['throttle-minutes'];
|
|
39
45
|
if (flags.enabled !== undefined)
|
|
@@ -6,10 +6,10 @@ const core_1 = require("@oclif/core");
|
|
|
6
6
|
const base_1 = require("../../base");
|
|
7
7
|
class TicketsAttachmentDownload extends base_1.BaseCommand {
|
|
8
8
|
static args = {
|
|
9
|
-
id: core_1.Args.string({ description: 'Ticket id', required: true }),
|
|
10
|
-
attachment: core_1.Args.string({ description: 'Attachment id', required: true }),
|
|
9
|
+
id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
|
|
10
|
+
attachment: core_1.Args.string({ description: 'Attachment id (ticket attachment or comment file)', required: true }),
|
|
11
11
|
};
|
|
12
|
-
static description = 'Download a ticket
|
|
12
|
+
static description = 'Download a ticket binary to disk: works for ticket attachments AND files attached to its comments';
|
|
13
13
|
static examples = [
|
|
14
14
|
'<%= config.bin %> tickets attachment-download <ticket-id> <attachment-id> --project acme-api',
|
|
15
15
|
'<%= config.bin %> tickets attachment-download <ticket-id> <attachment-id> -p acme-api --out ./downloads',
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const base_1 = require("../../base");
|
|
5
|
+
const output_1 = require("../../lib/output");
|
|
5
6
|
class TicketsComments extends base_1.BaseCommand {
|
|
6
7
|
static args = {
|
|
7
|
-
id: core_1.Args.string({ description: 'Ticket id', required: true }),
|
|
8
|
+
id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
|
|
8
9
|
};
|
|
9
|
-
static description = 'List all comments of a ticket (oldest first)';
|
|
10
|
+
static description = 'List all comments of a ticket (oldest first), including attached files — download them with tickets attachment-download';
|
|
10
11
|
static examples = [
|
|
11
12
|
'<%= config.bin %> tickets comments <ticket-id> --project acme-api',
|
|
12
13
|
'<%= config.bin %> tickets comments <ticket-id> -p acme-api --json',
|
|
@@ -33,8 +34,12 @@ class TicketsComments extends base_1.BaseCommand {
|
|
|
33
34
|
}
|
|
34
35
|
else {
|
|
35
36
|
for (const comment of all) {
|
|
36
|
-
this.log(`\n── ${
|
|
37
|
-
this.log(
|
|
37
|
+
this.log(`\n── ${(0, output_1.sanitize)(comment.author ?? '?')} · ${(0, output_1.sanitize)(comment.created_at ?? '')} · ${(0, output_1.sanitize)(comment.id ?? '')}`);
|
|
38
|
+
this.log((0, output_1.sanitizeMultiline)(comment.body ?? ''));
|
|
39
|
+
const files = Array.isArray(comment.files) ? comment.files : [];
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
this.log(` [file] ${(0, output_1.sanitize)(file.id)} · ${(0, output_1.sanitize)(file.filename)} · ${(0, output_1.sanitize)(file.content_type)} · ${(0, output_1.sanitize)(file.byte_size)} bytes`);
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
}
|
|
@@ -9,4 +9,9 @@ export default class TicketsShow extends BaseCommand {
|
|
|
9
9
|
project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
10
|
};
|
|
11
11
|
run(): Promise<unknown>;
|
|
12
|
+
/** Read every page of a collection endpoint: the discussion must be complete, not just page 1. */
|
|
13
|
+
private fetchAllPages;
|
|
14
|
+
private renderAttachments;
|
|
15
|
+
private renderComments;
|
|
16
|
+
private renderTicket;
|
|
12
17
|
}
|
|
@@ -4,20 +4,104 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const base_1 = require("../../base");
|
|
5
5
|
const output_1 = require("../../lib/output");
|
|
6
6
|
class TicketsShow extends base_1.BaseCommand {
|
|
7
|
-
static description = 'Show a single ticket';
|
|
8
|
-
static examples = [
|
|
7
|
+
static description = 'Show a single ticket in full: body (description + Given/When/Then/Expected), comments (with their files) and attachments';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> tickets show <ticket-id> --project acme-api',
|
|
10
|
+
'<%= config.bin %> tickets show DRFL-3 --project driverone-flutter',
|
|
11
|
+
'<%= config.bin %> tickets show DRFL-3 -p driverone-flutter --json',
|
|
12
|
+
];
|
|
9
13
|
static args = {
|
|
10
|
-
id: core_1.Args.string({ description: 'Ticket id or code', required: true }),
|
|
14
|
+
id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
|
|
11
15
|
};
|
|
12
16
|
static flags = { ...base_1.projectFlag };
|
|
13
17
|
async run() {
|
|
14
18
|
const { args, flags } = await this.parse(TicketsShow);
|
|
15
19
|
const projectId = await this.resolveProjectId(flags.project);
|
|
16
|
-
const
|
|
20
|
+
const base = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}`;
|
|
21
|
+
// The show is the analysis entry point: always the WHOLE ticket in one command
|
|
22
|
+
// (fields + full discussion + attachments), so nothing stays hidden behind extra calls.
|
|
23
|
+
const res = await this.api.get(base);
|
|
24
|
+
const ticket = res.data ?? {};
|
|
25
|
+
const comments = await this.fetchAllPages(`${base}/comments`);
|
|
26
|
+
const attachments = await this.fetchAllPages(`${base}/attachments`);
|
|
17
27
|
if (!this.jsonEnabled()) {
|
|
18
|
-
this.
|
|
28
|
+
this.renderTicket(ticket);
|
|
29
|
+
this.renderComments(comments);
|
|
30
|
+
this.renderAttachments(attachments);
|
|
31
|
+
}
|
|
32
|
+
return { data: { ...ticket, comments, attachments } };
|
|
33
|
+
}
|
|
34
|
+
/** Read every page of a collection endpoint: the discussion must be complete, not just page 1. */
|
|
35
|
+
async fetchAllPages(path) {
|
|
36
|
+
const all = [];
|
|
37
|
+
let page = 1;
|
|
38
|
+
let totalPages = 1;
|
|
39
|
+
do {
|
|
40
|
+
// eslint-disable-next-line no-await-in-loop
|
|
41
|
+
const res = await this.api.get(`${path}?page=${page}&per=100`);
|
|
42
|
+
all.push(...(Array.isArray(res.data) ? res.data : []));
|
|
43
|
+
totalPages = Number(res.meta?.total_pages ?? 1);
|
|
44
|
+
page += 1;
|
|
45
|
+
} while (page <= totalPages);
|
|
46
|
+
return all;
|
|
47
|
+
}
|
|
48
|
+
renderAttachments(attachments) {
|
|
49
|
+
this.log(`\nAttachments (${attachments.length})`);
|
|
50
|
+
if (attachments.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
this.log((0, output_1.renderTable)(['ID', 'FILENAME', 'CONTENT_TYPE', 'BYTE_SIZE', 'CREATED_AT'], attachments.map((attachment) => [
|
|
53
|
+
String(attachment.id ?? ''),
|
|
54
|
+
String(attachment.filename ?? ''),
|
|
55
|
+
String(attachment.content_type ?? ''),
|
|
56
|
+
String(attachment.byte_size ?? ''),
|
|
57
|
+
String(attachment.created_at ?? ''),
|
|
58
|
+
])));
|
|
59
|
+
}
|
|
60
|
+
renderComments(comments) {
|
|
61
|
+
this.log(`\nComments (${comments.length})`);
|
|
62
|
+
for (const comment of comments) {
|
|
63
|
+
this.log(`\n── ${(0, output_1.sanitize)(comment.author ?? '?')} · ${(0, output_1.sanitize)(comment.created_at ?? '')} · ${(0, output_1.sanitize)(comment.id ?? '')}`);
|
|
64
|
+
this.log((0, output_1.sanitizeMultiline)(comment.body ?? ''));
|
|
65
|
+
const files = Array.isArray(comment.files) ? comment.files : [];
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
this.log(` [file] ${(0, output_1.sanitize)(file.id)} · ${(0, output_1.sanitize)(file.filename)} · ${(0, output_1.sanitize)(file.content_type)} · ${(0, output_1.sanitize)(file.byte_size)} bytes`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
renderTicket(ticket) {
|
|
72
|
+
const platforms = Array.isArray(ticket.platforms) ? ticket.platforms.map(String).join(', ') : null;
|
|
73
|
+
this.log((0, output_1.renderRecord)({
|
|
74
|
+
id: ticket.id,
|
|
75
|
+
code: ticket.code,
|
|
76
|
+
title: ticket.title,
|
|
77
|
+
kind: ticket.kind,
|
|
78
|
+
status: ticket.status,
|
|
79
|
+
priority: ticket.priority,
|
|
80
|
+
weight: ticket.weight,
|
|
81
|
+
votes_count: ticket.votes_count,
|
|
82
|
+
assignee: ticket.assignee,
|
|
83
|
+
reporter: ticket.reporter,
|
|
84
|
+
reviewer: ticket.reviewer,
|
|
85
|
+
milestone: ticket.milestone,
|
|
86
|
+
platforms: platforms || null,
|
|
87
|
+
project_id: ticket.project_id,
|
|
88
|
+
created_at: ticket.created_at,
|
|
89
|
+
updated_at: ticket.updated_at,
|
|
90
|
+
}));
|
|
91
|
+
// Multi-line body blocks: description first, then the 4 BDD clauses (only those present).
|
|
92
|
+
const blocks = [
|
|
93
|
+
['Description', ticket.description],
|
|
94
|
+
['Given', ticket.step_given],
|
|
95
|
+
['When', ticket.step_when],
|
|
96
|
+
['Then', ticket.step_then],
|
|
97
|
+
['Expected', ticket.step_expected],
|
|
98
|
+
];
|
|
99
|
+
for (const [label, value] of blocks) {
|
|
100
|
+
if (value === null || value === undefined || String(value).trim() === '')
|
|
101
|
+
continue;
|
|
102
|
+
this.log(`\n${label}\n${'-'.repeat(label.length)}`);
|
|
103
|
+
this.log((0, output_1.sanitizeMultiline)(value));
|
|
19
104
|
}
|
|
20
|
-
return res;
|
|
21
105
|
}
|
|
22
106
|
}
|
|
23
107
|
exports.default = TicketsShow;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ALERT_EVENT_TYPES = void 0;
|
|
4
|
+
/** Event types accepted by alert rules — mirror of the backend enum (Alerting::Rule). */
|
|
5
|
+
exports.ALERT_EVENT_TYPES = [
|
|
6
|
+
'error_new',
|
|
7
|
+
'error_regression',
|
|
8
|
+
'uptime_down',
|
|
9
|
+
'uptime_up',
|
|
10
|
+
'metric_threshold',
|
|
11
|
+
'uptime_ssl_expiring',
|
|
12
|
+
'cron_missed',
|
|
13
|
+
'server_down',
|
|
14
|
+
'server_up',
|
|
15
|
+
'server_cpu',
|
|
16
|
+
'server_mem',
|
|
17
|
+
'server_disk',
|
|
18
|
+
'server_temp',
|
|
19
|
+
'server_service_failed',
|
|
20
|
+
'server_smart_failing',
|
|
21
|
+
];
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutralize terminal control sequences (CWE-150) in server-supplied strings.
|
|
3
|
+
* Log/error/ticket text is attacker-influenceable; printing raw ESC (0x1B) & co.
|
|
4
|
+
* to a TTY allows cursor/color/title injection. Replace every C0/C1 control char
|
|
5
|
+
* (0x00–0x1F, 0x7F–0x9F) with a space — ANSI is defused, width is preserved.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitize(value: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Like `sanitize`, but preserves line breaks: for multi-line bodies (ticket description,
|
|
10
|
+
* BDD clauses, comment text) where collapsing `\n` to spaces would destroy readability.
|
|
11
|
+
* Every other C0/C1 control char is still defused per line.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeMultiline(value: unknown): string;
|
|
1
14
|
/** Minimal dependency-free table renderer for human output. */
|
|
2
15
|
export declare function renderTable(headers: string[], rows: string[][]): string;
|
|
3
16
|
/** Pretty-print the scalar fields of a record as `key: value` lines. */
|
package/dist/lib/output.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitize = sanitize;
|
|
4
|
+
exports.sanitizeMultiline = sanitizeMultiline;
|
|
3
5
|
exports.renderTable = renderTable;
|
|
4
6
|
exports.renderRecord = renderRecord;
|
|
5
7
|
/**
|
|
@@ -16,6 +18,17 @@ function sanitize(value) {
|
|
|
16
18
|
}
|
|
17
19
|
return out;
|
|
18
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Like `sanitize`, but preserves line breaks: for multi-line bodies (ticket description,
|
|
23
|
+
* BDD clauses, comment text) where collapsing `\n` to spaces would destroy readability.
|
|
24
|
+
* Every other C0/C1 control char is still defused per line.
|
|
25
|
+
*/
|
|
26
|
+
function sanitizeMultiline(value) {
|
|
27
|
+
return String(value ?? '')
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map((line) => sanitize(line))
|
|
30
|
+
.join('\n');
|
|
31
|
+
}
|
|
19
32
|
/** Minimal dependency-free table renderer for human output. */
|
|
20
33
|
function renderTable(headers, rows) {
|
|
21
34
|
const safeHeaders = headers.map(sanitize);
|