@bussolabs/closeyourit-cli 0.0.6 → 0.0.7

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,15 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class TicketsAttachmentDownload extends BaseCommand {
3
+ static args: {
4
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
5
+ attachment: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static examples: string[];
9
+ static flags: {
10
+ out: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ filename: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<unknown>;
15
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const node_path_1 = require("node:path");
5
+ const core_1 = require("@oclif/core");
6
+ const base_1 = require("../../base");
7
+ class TicketsAttachmentDownload extends base_1.BaseCommand {
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 }),
11
+ };
12
+ static description = 'Download a ticket attachment to disk';
13
+ static examples = [
14
+ '<%= config.bin %> tickets attachment-download <ticket-id> <attachment-id> --project acme-api',
15
+ '<%= config.bin %> tickets attachment-download <ticket-id> <attachment-id> -p acme-api --out ./downloads',
16
+ ];
17
+ static flags = {
18
+ ...base_1.projectFlag,
19
+ out: core_1.Flags.string({ default: '.', description: 'Output directory' }),
20
+ filename: core_1.Flags.string({ description: 'Override the output filename' }),
21
+ };
22
+ async run() {
23
+ const { args, flags } = await this.parse(TicketsAttachmentDownload);
24
+ const projectId = await this.resolveProjectId(flags.project);
25
+ const { data, filename } = await this.api.download(`/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/attachments/${encodeURIComponent(args.attachment)}/download`);
26
+ const name = flags.filename ?? filename ?? args.attachment;
27
+ const dest = (0, node_path_1.join)(flags.out, name);
28
+ await (0, promises_1.writeFile)(dest, data);
29
+ if (!this.jsonEnabled()) {
30
+ this.log(`Saved ${data.byteLength} bytes → ${dest}`);
31
+ }
32
+ return { bytes: data.byteLength, filename: name, path: dest };
33
+ }
34
+ }
35
+ exports.default = TicketsAttachmentDownload;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class TicketsAttachments 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
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,33 @@
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 TicketsAttachments extends base_1.BaseCommand {
7
+ static args = {
8
+ id: core_1.Args.string({ description: 'Ticket id', required: true }),
9
+ };
10
+ static description = "List a ticket's attachments";
11
+ static examples = [
12
+ '<%= config.bin %> tickets attachments <ticket-id> --project acme-api',
13
+ '<%= config.bin %> tickets attachments <ticket-id> -p acme-api --json',
14
+ ];
15
+ static flags = { ...base_1.projectFlag };
16
+ async run() {
17
+ const { args, flags } = await this.parse(TicketsAttachments);
18
+ const projectId = await this.resolveProjectId(flags.project);
19
+ const res = await this.api.get(`/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/attachments?per=100`);
20
+ const attachments = res.data ?? [];
21
+ if (!this.jsonEnabled()) {
22
+ this.log((0, output_1.renderTable)(['ID', 'FILENAME', 'CONTENT_TYPE', 'BYTE_SIZE', 'CREATED_AT'], attachments.map((attachment) => [
23
+ String(attachment.id ?? ''),
24
+ String(attachment.filename ?? ''),
25
+ String(attachment.content_type ?? ''),
26
+ String(attachment.byte_size ?? ''),
27
+ String(attachment.created_at ?? ''),
28
+ ])));
29
+ }
30
+ return res;
31
+ }
32
+ }
33
+ exports.default = TicketsAttachments;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class TicketsComments 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
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,44 @@
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 TicketsComments extends base_1.BaseCommand {
6
+ static args = {
7
+ id: core_1.Args.string({ description: 'Ticket id', required: true }),
8
+ };
9
+ static description = 'List all comments of a ticket (oldest first)';
10
+ static examples = [
11
+ '<%= config.bin %> tickets comments <ticket-id> --project acme-api',
12
+ '<%= config.bin %> tickets comments <ticket-id> -p acme-api --json',
13
+ ];
14
+ static flags = { ...base_1.projectFlag };
15
+ async run() {
16
+ const { args, flags } = await this.parse(TicketsComments);
17
+ const projectId = await this.resolveProjectId(flags.project);
18
+ const base = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/comments`;
19
+ // Read every page so `comments` returns the full discussion, not just the first page.
20
+ const all = [];
21
+ let page = 1;
22
+ let totalPages = 1;
23
+ do {
24
+ // eslint-disable-next-line no-await-in-loop
25
+ const res = await this.api.get(`${base}?page=${page}&per=100`);
26
+ all.push(...(res.data ?? []));
27
+ totalPages = Number(res.meta?.total_pages ?? 1);
28
+ page += 1;
29
+ } while (page <= totalPages);
30
+ if (!this.jsonEnabled()) {
31
+ if (all.length === 0) {
32
+ this.log('(nessun commento)');
33
+ }
34
+ else {
35
+ for (const comment of all) {
36
+ this.log(`\n── ${String(comment.author ?? '?')} · ${String(comment.created_at ?? '')} · ${String(comment.id ?? '')}`);
37
+ this.log(String(comment.body ?? ''));
38
+ }
39
+ }
40
+ }
41
+ return { data: all };
42
+ }
43
+ }
44
+ exports.default = TicketsComments;
package/dist/lib/api.d.ts CHANGED
@@ -41,5 +41,16 @@ export declare class CliApi {
41
41
  post<T = unknown>(path: string, body?: unknown, opts?: RequestOptions): Promise<Envelope<T>>;
42
42
  put<T = unknown>(path: string, body?: unknown, opts?: RequestOptions): Promise<Envelope<T>>;
43
43
  delete<T = unknown>(path: string, opts?: RequestOptions): Promise<Envelope<T>>;
44
+ /**
45
+ * Download a binary payload (e.g. a ticket attachment). The endpoint redirects to a signed
46
+ * ActiveStorage blob URL; fetch follows the redirect (and drops the Authorization header on the
47
+ * cross-origin hop to storage, as intended). Returns the bytes plus the filename parsed from
48
+ * Content-Disposition when present. Error responses are JSON envelopes → mapped like every call.
49
+ */
50
+ download(path: string): Promise<{
51
+ contentType?: string;
52
+ data: Uint8Array;
53
+ filename?: string;
54
+ }>;
44
55
  }
45
56
  export {};
package/dist/lib/api.js CHANGED
@@ -105,5 +105,32 @@ class CliApi {
105
105
  delete(path, opts) {
106
106
  return this.request('DELETE', path, opts);
107
107
  }
108
+ /**
109
+ * Download a binary payload (e.g. a ticket attachment). The endpoint redirects to a signed
110
+ * ActiveStorage blob URL; fetch follows the redirect (and drops the Authorization header on the
111
+ * cross-origin hop to storage, as intended). Returns the bytes plus the filename parsed from
112
+ * Content-Disposition when present. Error responses are JSON envelopes → mapped like every call.
113
+ */
114
+ async download(path) {
115
+ const headers = this.authHeaders(true);
116
+ headers.Accept = '*/*';
117
+ const url = this.resolveUrl(path);
118
+ let res;
119
+ try {
120
+ res = await fetch(url, { method: 'GET', headers, redirect: 'follow' });
121
+ }
122
+ catch (err) {
123
+ throw new ApiRequestError(0, error_codes_1.ErrorCodes.Network.requestFailed, `Request to ${url} failed: ${err.message}`);
124
+ }
125
+ if (!res.ok) {
126
+ await this.handleResponse(res); // always throws: maps the JSON error envelope
127
+ throw new ApiRequestError(res.status, `C${res.status}-API-000`, `Download failed (${res.status})`);
128
+ }
129
+ const data = new Uint8Array(await res.arrayBuffer());
130
+ const disposition = res.headers.get('content-disposition') ?? '';
131
+ const match = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(disposition);
132
+ const filename = match ? decodeURIComponent(match[1].replace(/"$/, '')) : undefined;
133
+ return { contentType: res.headers.get('content-type') ?? undefined, data, filename };
134
+ }
108
135
  }
109
136
  exports.CliApi = CliApi;