@bussolabs/closeyourit-cli 0.18.0 → 0.20.0

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.
Files changed (75) hide show
  1. package/README.md +29 -5
  2. package/dist/base.d.ts +11 -0
  3. package/dist/base.js +31 -0
  4. package/dist/commands/alerts/notifications/read-all.js +3 -0
  5. package/dist/commands/alerts/preferences/show.js +2 -0
  6. package/dist/commands/ideas/convert.d.ts +2 -2
  7. package/dist/commands/ideas/convert.js +25 -8
  8. package/dist/commands/kb/create.js +29 -6
  9. package/dist/commands/kb/update.js +26 -5
  10. package/dist/commands/logout.js +3 -0
  11. package/dist/commands/org/show.js +2 -0
  12. package/dist/commands/personal/import.js +2 -2
  13. package/dist/commands/personal/run.js +1 -1
  14. package/dist/commands/run.js +1 -1
  15. package/dist/commands/secrets/import.js +2 -2
  16. package/dist/commands/seo/ignore.d.ts +12 -0
  17. package/dist/commands/seo/ignore.js +27 -0
  18. package/dist/commands/seo/list.d.ts +13 -0
  19. package/dist/commands/seo/list.js +52 -0
  20. package/dist/commands/seo/pages.d.ts +11 -0
  21. package/dist/commands/seo/pages.js +44 -0
  22. package/dist/commands/seo/promote.d.ts +9 -0
  23. package/dist/commands/seo/promote.js +22 -0
  24. package/dist/commands/seo/reopen.d.ts +9 -0
  25. package/dist/commands/seo/reopen.js +21 -0
  26. package/dist/commands/seo/rescan.d.ts +9 -0
  27. package/dist/commands/seo/rescan.js +21 -0
  28. package/dist/commands/seo/show.d.ts +9 -0
  29. package/dist/commands/seo/show.js +35 -0
  30. package/dist/commands/seo-sites/create.d.ts +14 -0
  31. package/dist/commands/seo-sites/create.js +43 -0
  32. package/dist/commands/seo-sites/delete.d.ts +9 -0
  33. package/dist/commands/seo-sites/delete.js +21 -0
  34. package/dist/commands/seo-sites/list.d.ts +10 -0
  35. package/dist/commands/seo-sites/list.js +38 -0
  36. package/dist/commands/seo-sites/update.d.ts +17 -0
  37. package/dist/commands/seo-sites/update.js +45 -0
  38. package/dist/commands/servers/ignored-containers.d.ts +15 -0
  39. package/dist/commands/servers/ignored-containers.js +51 -0
  40. package/dist/commands/tickets/comment.d.ts +14 -3
  41. package/dist/commands/tickets/comment.js +29 -1
  42. package/dist/commands/tickets/create.d.ts +6 -6
  43. package/dist/commands/tickets/create.js +21 -2
  44. package/dist/commands/tickets/report-show.d.ts +20 -0
  45. package/dist/commands/tickets/report-show.js +38 -0
  46. package/dist/commands/tickets/report-versions.d.ts +19 -0
  47. package/dist/commands/tickets/report-versions.js +40 -0
  48. package/dist/commands/tickets/report.d.ts +24 -0
  49. package/dist/commands/tickets/report.js +69 -0
  50. package/dist/commands/tickets/status.d.ts +1 -1
  51. package/dist/commands/tickets/status.js +14 -4
  52. package/dist/commands/tickets/update.d.ts +25 -6
  53. package/dist/commands/tickets/update.js +144 -9
  54. package/dist/commands/whoami.js +3 -0
  55. package/dist/errors/error-codes.d.ts +9 -0
  56. package/dist/errors/error-codes.js +14 -0
  57. package/dist/errors/input-error.d.ts +16 -0
  58. package/dist/errors/input-error.js +42 -0
  59. package/dist/lib/alert-events.d.ts +8 -1
  60. package/dist/lib/alert-events.js +37 -1
  61. package/dist/lib/limits.d.ts +59 -0
  62. package/dist/lib/limits.js +92 -0
  63. package/dist/lib/stdin.d.ts +1 -2
  64. package/dist/lib/stdin.js +1 -2
  65. package/dist/lib/subprocess.d.ts +1 -1
  66. package/dist/lib/subprocess.js +1 -1
  67. package/dist/lib/ticket-body.d.ts +22 -6
  68. package/dist/lib/ticket-body.js +71 -9
  69. package/dist/lib/ticket-lookup.d.ts +53 -0
  70. package/dist/lib/ticket-lookup.js +78 -0
  71. package/dist/lib/ticket-report.d.ts +7 -0
  72. package/dist/lib/ticket-report.js +14 -0
  73. package/oclif.manifest.json +3596 -2674
  74. package/opencli.json +698 -54
  75. package/package.json +8 -8
@@ -0,0 +1,20 @@
1
+ import { BaseCommand } from '../../base';
2
+ /**
3
+ * Legge il resoconto di lavorazione (CYCL-31): senza `--version` la stesura corrente, con `--version N`
4
+ * quella versione della cronologia — il numero è unico per ticket, quindi non serve l'id.
5
+ *
6
+ * Un ticket senza resoconto risponde `R404-REPORT-001` e il comando esce non-zero: uno script che
7
+ * pretende un resoconto se ne accorge, invece di leggere un output vuoto come "c'è ed è vuoto".
8
+ */
9
+ export default class TicketsReportShow extends BaseCommand {
10
+ static args: {
11
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ static description: string;
14
+ static examples: string[];
15
+ static flags: {
16
+ version: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ };
19
+ run(): Promise<unknown>;
20
+ }
@@ -0,0 +1,38 @@
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 ticket_report_1 = require("../../lib/ticket-report");
6
+ /**
7
+ * Legge il resoconto di lavorazione (CYCL-31): senza `--version` la stesura corrente, con `--version N`
8
+ * quella versione della cronologia — il numero è unico per ticket, quindi non serve l'id.
9
+ *
10
+ * Un ticket senza resoconto risponde `R404-REPORT-001` e il comando esce non-zero: uno script che
11
+ * pretende un resoconto se ne accorge, invece di leggere un output vuoto come "c'è ed è vuoto".
12
+ */
13
+ class TicketsReportShow extends base_1.BaseCommand {
14
+ static args = {
15
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
16
+ };
17
+ static description = 'Show the current work report of a ticket, or a past version with --version';
18
+ static examples = [
19
+ '<%= config.bin %> tickets report-show <ticket-id> --project acme-api',
20
+ '<%= config.bin %> tickets report-show DRFL-3 -p acme-api --version 2',
21
+ ];
22
+ static flags = {
23
+ ...base_1.projectFlag,
24
+ version: core_1.Flags.integer({ description: 'Version number to read instead of the current one' }),
25
+ };
26
+ async run() {
27
+ const { args, flags } = await this.parse(TicketsReportShow);
28
+ const projectId = await this.resolveProjectId(flags.project);
29
+ const base = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/report`;
30
+ const path = flags.version === undefined ? base : `${base}/versions/${encodeURIComponent(String(flags.version))}`;
31
+ const res = await this.api.get(path);
32
+ if (!this.jsonEnabled()) {
33
+ this.log((0, ticket_report_1.renderReport)(res.data ?? {}));
34
+ }
35
+ return res;
36
+ }
37
+ }
38
+ exports.default = TicketsReportShow;
@@ -0,0 +1,19 @@
1
+ import { BaseCommand } from '../../base';
2
+ /**
3
+ * Cronologia del resoconto di lavorazione (CYCL-31), dalla più recente alla più vecchia. La tabella
4
+ * porta i metadati e la LUNGHEZZA di ogni stesura, non il testo: i corpi sono lunghi migliaia di
5
+ * caratteri e incolonnarli renderebbe illeggibile la lista. Il testo di una versione si legge con
6
+ * `tickets report-show --version N`.
7
+ */
8
+ export default class TicketsReportVersions extends BaseCommand {
9
+ static args: {
10
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
11
+ };
12
+ static description: string;
13
+ static examples: string[];
14
+ static flags: {
15
+ page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ };
18
+ run(): Promise<unknown>;
19
+ }
@@ -0,0 +1,40 @@
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 limits_1 = require("../../lib/limits");
6
+ const output_1 = require("../../lib/output");
7
+ /**
8
+ * Cronologia del resoconto di lavorazione (CYCL-31), dalla più recente alla più vecchia. La tabella
9
+ * porta i metadati e la LUNGHEZZA di ogni stesura, non il testo: i corpi sono lunghi migliaia di
10
+ * caratteri e incolonnarli renderebbe illeggibile la lista. Il testo di una versione si legge con
11
+ * `tickets report-show --version N`.
12
+ */
13
+ class TicketsReportVersions extends base_1.BaseCommand {
14
+ static args = {
15
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
16
+ };
17
+ static description = 'List the versions of the work report of a ticket (newest first)';
18
+ static examples = [
19
+ '<%= config.bin %> tickets report-versions <ticket-id> --project acme-api',
20
+ '<%= config.bin %> tickets report-versions DRFL-3 -p acme-api --page 2 --json',
21
+ ];
22
+ static flags = { ...base_1.projectFlag, ...base_1.pageFlag };
23
+ async run() {
24
+ const { args, flags } = await this.parse(TicketsReportVersions);
25
+ const projectId = await this.resolveProjectId(flags.project);
26
+ const res = await this.api.get(`/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/report/versions?page=${flags.page}`);
27
+ const versions = res.data ?? [];
28
+ if (!this.jsonEnabled()) {
29
+ this.log((0, output_1.renderTable)(['VERSION', 'AUTHOR', 'SOURCE', 'CHARS', 'CREATED_AT'], versions.map((version) => [
30
+ String(version.version ?? ''),
31
+ String(version.author ?? '-'),
32
+ String(version.source ?? '-'),
33
+ String((0, limits_1.charCount)(version.body)),
34
+ String(version.created_at ?? ''),
35
+ ])));
36
+ }
37
+ return res;
38
+ }
39
+ }
40
+ exports.default = TicketsReportVersions;
@@ -0,0 +1,24 @@
1
+ import { BaseCommand } from '../../base';
2
+ /**
3
+ * Resoconto di lavorazione di un ticket (CYCL-31). È il campo che il server indica quando rifiuta un
4
+ * commento troppo lungo, e fino a qui esisteva solo nel form web: chi chiudeva una lavorazione dal
5
+ * terminale doveva aprire il sito o rinunciare a scrivere il resoconto.
6
+ *
7
+ * Ogni scrittura AGGIUNGE una versione, non sovrascrive la precedente: correggere un resoconto
8
+ * significa scriverne uno nuovo, e la cronologia si legge con `tickets report-versions`. Ripassare
9
+ * lo stesso identico testo non crea una versione in più — è il server a riconoscerlo, quindi un
10
+ * comando ripetuto (un retry di uno script) non sporca la cronologia.
11
+ */
12
+ export default class TicketsReport extends BaseCommand {
13
+ static args: {
14
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
15
+ };
16
+ static description: string;
17
+ static examples: string[];
18
+ static flags: {
19
+ body: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ 'body-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
22
+ };
23
+ run(): Promise<unknown>;
24
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const core_1 = require("@oclif/core");
5
+ const base_1 = require("../../base");
6
+ const limits_1 = require("../../lib/limits");
7
+ const ticket_report_1 = require("../../lib/ticket-report");
8
+ /**
9
+ * Resoconto di lavorazione di un ticket (CYCL-31). È il campo che il server indica quando rifiuta un
10
+ * commento troppo lungo, e fino a qui esisteva solo nel form web: chi chiudeva una lavorazione dal
11
+ * terminale doveva aprire il sito o rinunciare a scrivere il resoconto.
12
+ *
13
+ * Ogni scrittura AGGIUNGE una versione, non sovrascrive la precedente: correggere un resoconto
14
+ * significa scriverne uno nuovo, e la cronologia si legge con `tickets report-versions`. Ripassare
15
+ * lo stesso identico testo non crea una versione in più — è il server a riconoscerlo, quindi un
16
+ * comando ripetuto (un retry di uno script) non sporca la cronologia.
17
+ */
18
+ class TicketsReport extends base_1.BaseCommand {
19
+ static args = {
20
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
21
+ };
22
+ static description = 'Write the work report of a ticket — the long text a comment cannot hold. Each write adds a version, it never overwrites the previous one';
23
+ static examples = [
24
+ '<%= config.bin %> tickets report <ticket-id> --project acme-api --body "Cosa ho fatto, come si verifica"',
25
+ '<%= config.bin %> tickets report DRFL-3 -p acme-api --body-file ./report.md',
26
+ ];
27
+ static flags = {
28
+ ...base_1.projectFlag,
29
+ body: core_1.Flags.string({ description: `Report body (markdown, max ${limits_1.LENGTH_LIMITS.report} characters)` }),
30
+ 'body-file': core_1.Flags.string({
31
+ description: `Read the report body (max ${limits_1.LENGTH_LIMITS.report} characters) from a local file`,
32
+ }),
33
+ };
34
+ async run() {
35
+ const { args, flags } = await this.parse(TicketsReport);
36
+ // Una versione è immutabile: se i due valori divergono, sceglierne uno a caso lascia il
37
+ // resoconto sbagliato per sempre. Meglio fermarsi prima di scrivere.
38
+ if (flags.body !== undefined && flags['body-file'] !== undefined) {
39
+ this.error('Pass the report body with --body or --body-file, not both.', { exit: 2 });
40
+ }
41
+ let body = flags.body;
42
+ let bodyFlag = '--body';
43
+ if (body === undefined && flags['body-file'] !== undefined) {
44
+ bodyFlag = '--body-file';
45
+ try {
46
+ body = await (0, promises_1.readFile)(flags['body-file'], 'utf8');
47
+ }
48
+ catch {
49
+ this.error(`File not found or unreadable: ${flags['body-file']}`, { exit: 2 });
50
+ }
51
+ }
52
+ if (body === undefined) {
53
+ this.error('Provide the report body via --body or --body-file.', { exit: 2 });
54
+ }
55
+ // Ogni scrittura è una versione nuova, quindi il tetto vale sempre pieno: niente da salvaguardare
56
+ // e nessun motivo di spedire un testo che il server rifiuterebbe comunque (CYCL-32).
57
+ const tooLong = (0, limits_1.tooLongMessage)([{ flag: bodyFlag, value: body, max: limits_1.LENGTH_LIMITS.report, orthography: true }]);
58
+ if (tooLong)
59
+ this.error(tooLong, { exit: 2 });
60
+ const projectId = await this.resolveProjectId(flags.project);
61
+ const res = await this.api.post(`/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/report`, { body });
62
+ if (!this.jsonEnabled()) {
63
+ this.log('Work report saved:');
64
+ this.log((0, ticket_report_1.renderReport)(res.data ?? {}));
65
+ }
66
+ return res;
67
+ }
68
+ }
69
+ exports.default = TicketsReport;
@@ -6,7 +6,7 @@ export default class TicketsStatus extends BaseCommand {
6
6
  id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
- 'status-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
10
  project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
11
  };
12
12
  run(): Promise<unknown>;
@@ -3,20 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const base_1 = require("../../base");
5
5
  const output_1 = require("../../lib/output");
6
+ const ticket_lookup_1 = require("../../lib/ticket-lookup");
6
7
  class TicketsStatus extends base_1.BaseCommand {
7
- static description = 'Change a ticket status';
8
- static examples = ['<%= config.bin %> tickets status <ticket-id> --project acme-api --status-id <status-id>'];
8
+ static description = 'Change a ticket status (by code, label or id)';
9
+ static examples = [
10
+ '<%= config.bin %> tickets status <ticket-id> --project acme-api --status in_review',
11
+ '<%= config.bin %> tickets status <ticket-id> --project acme-api --status <status-id>',
12
+ ];
9
13
  static args = {
10
14
  id: core_1.Args.string({ description: 'Ticket id', required: true }),
11
15
  };
12
16
  static flags = {
13
17
  ...base_1.projectFlag,
14
- 'status-id': core_1.Flags.string({ description: 'Target status lookup id', required: true }),
18
+ // Lo stato si scrive come lo si legge: `--status-id` resta come alias per gli script (CYCL-34).
19
+ status: core_1.Flags.string({
20
+ aliases: ['status-id'],
21
+ description: 'Target status code, label or id (e.g. in_review, "In Review")',
22
+ required: true,
23
+ }),
15
24
  };
16
25
  async run() {
17
26
  const { args, flags } = await this.parse(TicketsStatus);
18
27
  const projectId = await this.resolveProjectId(flags.project);
19
- const res = await this.api.put(`/cli/v1/projects/${projectId}/tickets/${args.id}/status`, { status_id: flags['status-id'] });
28
+ const statusId = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_STATUS, flags.status);
29
+ const res = await this.api.put(`/cli/v1/projects/${projectId}/tickets/${args.id}/status`, { status_id: statusId });
20
30
  if (!this.jsonEnabled()) {
21
31
  this.log(`Ticket ${args.id} status updated:`);
22
32
  this.log((0, output_1.renderRecord)(res.data ?? {}));
@@ -6,6 +6,14 @@ export default class TicketsUpdate extends BaseCommand {
6
6
  id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
+ replace: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ clear: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ 'assignee-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ 'milestone-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ 'parent-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
+ 'platform-id': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ priority: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
17
  description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
18
  'technical-analysis': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
19
  scenarios: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -16,15 +24,26 @@ export default class TicketsUpdate extends BaseCommand {
16
24
  condition: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
25
  weight: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
26
  'due-at': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
- 'status-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
- 'priority-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
- 'assignee-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
- 'milestone-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
- 'parent-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
- 'platform-id': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
27
  title: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
28
  kind: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
29
  project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
28
30
  };
29
31
  run(): Promise<unknown>;
32
+ /**
33
+ * Lo stato attuale del ticket, nella forma che la PUT si aspetta, per i soli campi che chi ha
34
+ * scritto il comando non ha già deciso. I campi vuoti restano fuori dal corpo: ometterli o
35
+ * mandarli a null è la stessa cosa, e il corpo resta leggibile.
36
+ */
37
+ private currentState;
38
+ /** Rifiuta `--clear x` insieme a `--x`: impostare e svuotare lo stesso campo è una richiesta ambigua. */
39
+ private refuseClearConflicts;
40
+ /** Id della voce di lookup che corrisponde all'etichetta mostrata dalla GET del ticket. */
41
+ private lookupId;
42
+ /**
43
+ * Una lookup non trovata o ambigua ferma il comando: proseguire manderebbe una PUT senza quel
44
+ * campo, cioè lo cancellerebbe in silenzio — il difetto che questo comando esiste per evitare.
45
+ */
46
+ private matchId;
47
+ /** Ogni pagina di un endpoint di lookup: la voce cercata può stare oltre la prima. */
48
+ private lookupRows;
30
49
  }
@@ -2,40 +2,175 @@
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 limits_1 = require("../../lib/limits");
5
6
  const output_1 = require("../../lib/output");
6
7
  const ticket_body_1 = require("../../lib/ticket-body");
8
+ const ticket_lookup_1 = require("../../lib/ticket-lookup");
9
+ /** Campi che la GET del ticket restituisce già con la chiave e il valore attesi dalla PUT. */
10
+ const PRESERVED_KEYS = ['title', 'kind', 'description', 'technical_analysis', 'weight', 'due_at', 'parent_id'];
7
11
  class TicketsUpdate extends base_1.BaseCommand {
8
- // PUT /cli/v1/projects/:p/tickets/:id maps to Ticketing::UpdateTicket, a full replace:
9
- // fields you omit are cleared server-side. Pass the complete desired state, not a partial patch.
10
- static description = 'Update a ticket (full replace fields you omit are cleared server-side)';
12
+ // PUT /cli/v1/projects/:p/tickets/:id maps to Ticketing::UpdateTicket, a full replace built for the
13
+ // web form, which always posts every field: whatever the body omits is cleared server-side, and
14
+ // status/priority are mandatory, so `--title` alone did not even go through ("Priority must exist")
15
+ // while a body carrying them silently emptied description, technical analysis, weight, due date,
16
+ // assignee, milestone and platforms (CYCL-30).
17
+ //
18
+ // The command now reads the ticket first and re-sends its current state under the flags you passed:
19
+ // what you do not name stays as it was. Emptying is a separate, explicit request (`--clear <field>`),
20
+ // and `--replace` keeps the raw full replace for callers that really do send the whole desired state.
21
+ //
22
+ // Scenarios and conditions stay out of the merge: the backend replaces them when the body carries
23
+ // new ones and leaves them untouched otherwise, so re-sending them would only risk duplicates.
24
+ //
25
+ // Known limit — the read and the write are not one atomic operation, and this endpoint offers no
26
+ // optimistic locking (no lock_version, no If-Match/ETag: the GET exposes nothing to send back),
27
+ // so an edit landing between them is overwritten by the state this command read. It cannot be
28
+ // fixed from the client alone, and it is strictly narrower than what it replaces: the full replace
29
+ // wiped those fields on EVERY call, concurrency or not, while here the exposure is the round trip.
30
+ // Closing it for good needs a server-side merge (or a version token to send back) — the fix the
31
+ // ticket calls the "server remedy".
32
+ static description = 'Update a ticket, changing only the fields you pass (the others keep their current value; use --clear to empty one, --replace for the raw full replace)';
11
33
  static examples = [
12
34
  '<%= config.bin %> tickets update <ticket-id> --project acme-api --title "New title"',
35
+ '<%= config.bin %> tickets update <ticket-id> -p acme-api --clear assignee --clear due-at',
13
36
  '<%= config.bin %> tickets update <ticket-id> -p acme-api --title "Login bug" --scenarios \'[{"step_given":"logged out","step_when":"I submit","step_then":"it 500s","step_expected":"dashboard loads"}]\' --condition "login works"',
37
+ '<%= config.bin %> tickets update <ticket-id> -p acme-api --status in_review --priority High',
38
+ '<%= config.bin %> tickets update <ticket-id> -p acme-api --replace --title "Only this" --status open --priority medium',
14
39
  ];
15
40
  static args = {
16
41
  id: core_1.Args.string({ description: 'Ticket id', required: true }),
17
42
  };
18
43
  static flags = {
19
44
  ...base_1.projectFlag,
20
- title: core_1.Flags.string({ description: 'Ticket title' }),
45
+ title: core_1.Flags.string({ description: `Ticket title (max ${limits_1.LENGTH_LIMITS.title} characters)` }),
21
46
  kind: core_1.Flags.string({ description: 'Ticket kind (bug, story, task, epic)', options: ['bug', 'story', 'task', 'epic'] }),
22
47
  ...ticket_body_1.ticketBodyFlags,
48
+ ...ticket_body_1.clearFlag,
49
+ replace: core_1.Flags.boolean({
50
+ description: 'Send only the flags you passed (full replace — every field you omit is cleared server-side)',
51
+ default: false,
52
+ }),
23
53
  };
24
54
  async run() {
25
55
  const { args, flags } = await this.parse(TicketsUpdate);
26
56
  const projectId = await this.resolveProjectId(flags.project);
27
- const body = {};
57
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}`;
58
+ const requested = {};
28
59
  if (flags.title !== undefined)
29
- body.title = flags.title;
60
+ requested.title = flags.title;
30
61
  if (flags.kind !== undefined)
31
- body.kind = flags.kind;
32
- (0, ticket_body_1.applyTicketBody)(body, flags);
33
- const res = await this.api.put(`/cli/v1/projects/${projectId}/tickets/${args.id}`, body);
62
+ requested.kind = flags.kind;
63
+ (0, ticket_body_1.applyTicketBody)(requested, flags);
64
+ // Cambiare stato o priorità non costringe più a cercarne prima l'id: il code o l'etichetta
65
+ // bastano, e un id passa così com'è (CYCL-34).
66
+ if (flags.status !== undefined) {
67
+ requested.status_id = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_STATUS, flags.status);
68
+ }
69
+ if (flags.priority !== undefined) {
70
+ requested.priority_id = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_PRIORITY, flags.priority);
71
+ }
72
+ this.refuseClearConflicts(requested, flags.clear);
73
+ (0, ticket_body_1.applyTicketClears)(requested, flags.clear);
74
+ let body = requested;
75
+ if (!flags.replace) {
76
+ const ticket = (await this.api.get(path)).data ?? {};
77
+ // I campi troppo lunghi si scoprono qui, tutti insieme, invece che uno per rifiuto (CYCL-32).
78
+ // Il valore attuale fa da salvaguardia: un testo scritto prima che il tetto esistesse resta
79
+ // correggibile finché non si allunga, com'è dal server. Con `--replace` lo stato attuale non
80
+ // si legge, quindi la salvaguardia non è calcolabile e la misura resta al server.
81
+ const tooLong = (0, limits_1.tooLongMessage)((0, ticket_body_1.ticketLengthChecks)(flags, ticket));
82
+ if (tooLong)
83
+ this.error(tooLong, { exit: 2 });
84
+ body = { ...(await this.currentState(projectId, ticket, requested)), ...requested };
85
+ }
86
+ const res = await this.api.put(path, body);
34
87
  if (!this.jsonEnabled()) {
35
88
  this.log('Ticket updated:');
36
89
  this.log((0, output_1.renderRecord)(res.data ?? {}));
37
90
  }
38
91
  return res;
39
92
  }
93
+ /**
94
+ * Lo stato attuale del ticket, nella forma che la PUT si aspetta, per i soli campi che chi ha
95
+ * scritto il comando non ha già deciso. I campi vuoti restano fuori dal corpo: ometterli o
96
+ * mandarli a null è la stessa cosa, e il corpo resta leggibile.
97
+ */
98
+ async currentState(projectId, ticket, requested) {
99
+ const current = {};
100
+ for (const key of PRESERVED_KEYS) {
101
+ const value = ticket[key];
102
+ if (key in requested || value === null || value === undefined || value === '')
103
+ continue;
104
+ current[key] = value;
105
+ }
106
+ // Status e priorità sono obbligatorie per il backend: senza di loro l'update non passa. La GET
107
+ // espone l'id dello status, mentre della priorità dà solo l'etichetta, da rileggere nella lookup.
108
+ if (!('status_id' in requested)) {
109
+ const statusId = ticket.status_ref?.id;
110
+ if (typeof statusId !== 'string' || statusId === '') {
111
+ this.error('Cannot read the current status of this ticket: pass --status <code|label|id> or use --replace.', {
112
+ exit: 2,
113
+ });
114
+ }
115
+ current.status_id = statusId;
116
+ }
117
+ if (!('priority_id' in requested)) {
118
+ current.priority_id = await this.lookupId(ticket_lookup_1.TICKET_PRIORITY.path, ticket.priority, 'priority', '--priority');
119
+ }
120
+ if (!('assignee_id' in requested) && ticket.assignee) {
121
+ current.assignee_id = await this.lookupId('/cli/v1/members', ticket.assignee, 'assignee', '--assignee-id', 'account_id');
122
+ }
123
+ if (!('milestone_id' in requested) && ticket.milestone) {
124
+ const milestones = `/cli/v1/projects/${encodeURIComponent(projectId)}/milestones`;
125
+ current.milestone_id = await this.lookupId(milestones, ticket.milestone, 'milestone', '--milestone-id');
126
+ }
127
+ const platforms = Array.isArray(ticket.platforms) ? ticket.platforms : [];
128
+ if (!('platform_ids' in requested) && platforms.length > 0) {
129
+ const rows = await this.lookupRows('/cli/v1/platforms');
130
+ current.platform_ids = platforms.map((platform) => this.matchId(rows, platform, 'platform', '--platform-id'));
131
+ }
132
+ return current;
133
+ }
134
+ /** Rifiuta `--clear x` insieme a `--x`: impostare e svuotare lo stesso campo è una richiesta ambigua. */
135
+ refuseClearConflicts(requested, fields) {
136
+ for (const field of fields ?? []) {
137
+ if (ticket_body_1.CLEARABLE_FIELDS[field] in requested) {
138
+ this.error(`--clear ${field} conflicts with the value passed for the same field: pick one.`, { exit: 2 });
139
+ }
140
+ }
141
+ }
142
+ /** Id della voce di lookup che corrisponde all'etichetta mostrata dalla GET del ticket. */
143
+ async lookupId(path, label, field, flag, idKey = 'id') {
144
+ const rows = await this.lookupRows(path);
145
+ return this.matchId(rows, label, field, flag, idKey);
146
+ }
147
+ /**
148
+ * Una lookup non trovata o ambigua ferma il comando: proseguire manderebbe una PUT senza quel
149
+ * campo, cioè lo cancellerebbe in silenzio — il difetto che questo comando esiste per evitare.
150
+ */
151
+ matchId(rows, label, field, flag, idKey = 'id') {
152
+ const wanted = String(label ?? '').toLowerCase();
153
+ const matches = rows.filter((row) => [row.label, row.code, row.name].some((value) => typeof value === 'string' && value.toLowerCase() === wanted));
154
+ if (matches.length !== 1 || typeof matches[0][idKey] !== 'string') {
155
+ const reason = matches.length > 1 ? `${matches.length} entries match it` : 'no entry matches it';
156
+ this.error(`Cannot keep the current ${field} "${String(label ?? '')}": ${reason}. Pass ${flag} <id> or use --replace.`, { exit: 2 });
157
+ }
158
+ return matches[0][idKey];
159
+ }
160
+ /** Ogni pagina di un endpoint di lookup: la voce cercata può stare oltre la prima. */
161
+ async lookupRows(path) {
162
+ const rows = [];
163
+ const separator = path.includes('?') ? '&' : '?';
164
+ let page = 1;
165
+ let totalPages = 1;
166
+ do {
167
+ // eslint-disable-next-line no-await-in-loop
168
+ const res = await this.api.get(`${path}${separator}page=${page}&per=100`);
169
+ rows.push(...(Array.isArray(res.data) ? res.data : []));
170
+ totalPages = Number(res.meta?.total_pages ?? 1);
171
+ page += 1;
172
+ } while (page <= totalPages);
173
+ return rows;
174
+ }
40
175
  }
41
176
  exports.default = TicketsUpdate;
@@ -5,6 +5,9 @@ class Whoami extends base_1.BaseCommand {
5
5
  static description = 'Show the authenticated account, organization, token prefix and permissions';
6
6
  static examples = ['<%= config.bin %> whoami', '<%= config.bin %> whoami --json'];
7
7
  async run() {
8
+ // Nothing to read from the parser, but oclif validates the command line only here: without this
9
+ // call a mistyped flag is silently ignored and the command answers as if it were right (CYCL-33).
10
+ await this.parse(Whoami);
8
11
  const res = await this.api.get('/cli/v1/whoami');
9
12
  // Rebuild from an allow-list. If a future backend accidentally includes a token secret or other
10
13
  // authentication internals, `--json` must not proxy them to stdout.
@@ -25,10 +25,19 @@ export declare const ErrorCodes: {
25
25
  readonly Member: {
26
26
  readonly notFound: "C404-MBR-001";
27
27
  };
28
+ readonly TicketStatus: {
29
+ readonly notFound: "C404-STA-001";
30
+ };
31
+ readonly TicketPriority: {
32
+ readonly notFound: "C404-PRI-001";
33
+ };
28
34
  readonly Device: {
29
35
  readonly expired: "C408-DEV-001";
30
36
  readonly failed: "C400-DEV-002";
31
37
  };
38
+ readonly Input: {
39
+ readonly invalid: "C400-INP-001";
40
+ };
32
41
  readonly System: {
33
42
  readonly unexpected: "C500-SYS-001";
34
43
  };
@@ -30,10 +30,24 @@ exports.ErrorCodes = {
30
30
  Member: {
31
31
  notFound: 'C404-MBR-001',
32
32
  },
33
+ // Stato e priorità di un ticket, indicati per code o etichetta e non trovati nelle lookup
34
+ // dell'organizzazione (o corrispondenti a più di una voce) — CYCL-34.
35
+ TicketStatus: {
36
+ notFound: 'C404-STA-001',
37
+ },
38
+ TicketPriority: {
39
+ notFound: 'C404-PRI-001',
40
+ },
33
41
  Device: {
34
42
  expired: 'C408-DEV-001',
35
43
  failed: 'C400-DEV-002',
36
44
  },
45
+ // The command line itself was rejected (missing/unknown flag or arg, value outside the allowed
46
+ // options, a `this.error(...)` guard): the message says what to fix, and it never reached the
47
+ // server — the opposite of System.unexpected, which stays for real failures (CYCL-33).
48
+ Input: {
49
+ invalid: 'C400-INP-001',
50
+ },
37
51
  System: {
38
52
  unexpected: 'C500-SYS-001',
39
53
  },
@@ -0,0 +1,16 @@
1
+ /**
2
+ * True for the failures oclif has already worded for whoever typed the command: a parse/validation
3
+ * error (missing required flag or arg, nonexistent flag, value outside the allowed options) and the
4
+ * `this.error(...)` guards inside a command (`--confirm` missing, nothing to update, file unreadable).
5
+ *
6
+ * Deliberately NOT input errors, although both extend `CLIError`:
7
+ * - `ExitError`, thrown by `this.exit(code)` — `cyi run` uses it to propagate the child exit code;
8
+ * - `ModuleLoadError`, which is a real internal failure and keeps the generic system code.
9
+ */
10
+ export declare function isInputError(error: unknown): error is Error;
11
+ /**
12
+ * Flatten oclif's multi-line, ANSI-decorated message into the single line a JSON consumer reads:
13
+ * `The following error occurred:\n Missing required flag project\nSee more help with --help`
14
+ * becomes `Missing required flag project` (CYCL-33).
15
+ */
16
+ export declare function inputErrorMessage(error: Error): string;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isInputError = isInputError;
4
+ exports.inputErrorMessage = inputErrorMessage;
5
+ const core_1 = require("@oclif/core");
6
+ /** oclif dims parts of a parse error with ANSI when the terminal has colors; machine output must not. */
7
+ const ANSI = /\x1B\[[\d;]*m/g;
8
+ /** Every parse error ends with this hint, and multi-reason ones open with a header line. */
9
+ const HELP_HINT = 'See more help with --help';
10
+ const HEADER = /^The following errors? occurred:$/;
11
+ /**
12
+ * True for the failures oclif has already worded for whoever typed the command: a parse/validation
13
+ * error (missing required flag or arg, nonexistent flag, value outside the allowed options) and the
14
+ * `this.error(...)` guards inside a command (`--confirm` missing, nothing to update, file unreadable).
15
+ *
16
+ * Deliberately NOT input errors, although both extend `CLIError`:
17
+ * - `ExitError`, thrown by `this.exit(code)` — `cyi run` uses it to propagate the child exit code;
18
+ * - `ModuleLoadError`, which is a real internal failure and keeps the generic system code.
19
+ */
20
+ function isInputError(error) {
21
+ if (!(error instanceof core_1.Errors.CLIError))
22
+ return false;
23
+ return !(error instanceof core_1.Errors.ExitError) && !(error instanceof core_1.Errors.ModuleLoadError);
24
+ }
25
+ /**
26
+ * Flatten oclif's multi-line, ANSI-decorated message into the single line a JSON consumer reads:
27
+ * `The following error occurred:\n Missing required flag project\nSee more help with --help`
28
+ * becomes `Missing required flag project` (CYCL-33).
29
+ */
30
+ function inputErrorMessage(error) {
31
+ const plain = error.message.replace(ANSI, '');
32
+ const lines = plain
33
+ .split('\n')
34
+ .map((line) => line.trim())
35
+ .filter((line) => line !== '' && line !== HELP_HINT && !HEADER.test(line));
36
+ // Only noise (a bare hint, or an empty message): keep whatever the error carried rather than nothing.
37
+ if (lines.length === 0)
38
+ return plain.trim();
39
+ // A line ending in `:` introduces the next one ("Missing 1 required arg:" + the arg itself);
40
+ // anything else is a reason of its own ("Missing required flag environment" + "…flag project").
41
+ return lines.reduce((acc, line) => (acc.endsWith(':') ? `${acc} ${line}` : `${acc}; ${line}`));
42
+ }
@@ -1,2 +1,9 @@
1
- /** Event types accepted by alert rules — mirror of the backend enum (Alerting::Rule). */
1
+ /**
2
+ * Event types accepted by alert rules — mirror of the backend enum (Alerting::Rule).
3
+ *
4
+ * CYRA-519: the list had stopped at the first fifteen, so `--event-type` rejected every type added
5
+ * since (containers, replication, inodes, automation, vulnerabilities…) — the flag looked broken
6
+ * while the backend accepted them. Keep this in the same order as the backend enum: appending there
7
+ * means appending here.
8
+ */
2
9
  export declare const ALERT_EVENT_TYPES: string[];