@bussolabs/closeyourit-cli 0.21.0 → 0.23.1

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 (118) hide show
  1. package/README.md +45 -3
  2. package/dist/base.d.ts +23 -0
  3. package/dist/base.js +64 -4
  4. package/dist/commands/assistant/ask.d.ts +24 -0
  5. package/dist/commands/assistant/ask.js +112 -0
  6. package/dist/commands/assistant/conversations/create.d.ts +9 -0
  7. package/dist/commands/assistant/conversations/create.js +25 -0
  8. package/dist/commands/assistant/conversations/delete.d.ts +12 -0
  9. package/dist/commands/assistant/conversations/delete.js +25 -0
  10. package/dist/commands/assistant/conversations/list.d.ts +10 -0
  11. package/dist/commands/assistant/conversations/list.js +30 -0
  12. package/dist/commands/assistant/conversations/show.d.ts +9 -0
  13. package/dist/commands/assistant/conversations/show.js +25 -0
  14. package/dist/commands/errors/assign.d.ts +14 -0
  15. package/dist/commands/errors/assign.js +52 -0
  16. package/dist/commands/errors/bulk-triage.d.ts +11 -0
  17. package/dist/commands/errors/bulk-triage.js +41 -0
  18. package/dist/commands/errors/delete.d.ts +13 -0
  19. package/dist/commands/errors/delete.js +28 -0
  20. package/dist/commands/errors/grouping-rules/create.d.ts +15 -0
  21. package/dist/commands/errors/grouping-rules/create.js +44 -0
  22. package/dist/commands/errors/grouping-rules/delete.d.ts +13 -0
  23. package/dist/commands/errors/grouping-rules/delete.js +27 -0
  24. package/dist/commands/errors/grouping-rules/list.d.ts +10 -0
  25. package/dist/commands/errors/grouping-rules/list.js +28 -0
  26. package/dist/commands/errors/grouping-rules/update.d.ts +18 -0
  27. package/dist/commands/errors/grouping-rules/update.js +43 -0
  28. package/dist/commands/errors/merge.d.ts +14 -0
  29. package/dist/commands/errors/merge.js +39 -0
  30. package/dist/commands/errors/split.d.ts +13 -0
  31. package/dist/commands/errors/split.js +34 -0
  32. package/dist/commands/invitations/create.js +9 -2
  33. package/dist/commands/invitations/resend.js +8 -2
  34. package/dist/commands/kb/attach.d.ts +12 -0
  35. package/dist/commands/kb/attach.js +49 -0
  36. package/dist/commands/kb/attachment-download.d.ts +14 -0
  37. package/dist/commands/kb/attachment-download.js +38 -0
  38. package/dist/commands/kb/attachments.d.ts +9 -0
  39. package/dist/commands/kb/attachments.js +36 -0
  40. package/dist/commands/kb/create.d.ts +4 -1
  41. package/dist/commands/kb/create.js +41 -5
  42. package/dist/commands/kb/detach.d.ts +13 -0
  43. package/dist/commands/kb/detach.js +26 -0
  44. package/dist/commands/kb/show.js +3 -0
  45. package/dist/commands/kb/update.d.ts +4 -0
  46. package/dist/commands/kb/update.js +69 -21
  47. package/dist/commands/metrics/bulk-triage.d.ts +11 -0
  48. package/dist/commands/metrics/bulk-triage.js +44 -0
  49. package/dist/commands/seo-sites/show.d.ts +15 -0
  50. package/dist/commands/seo-sites/show.js +107 -0
  51. package/dist/commands/service-accounts/show.d.ts +9 -0
  52. package/dist/commands/service-accounts/show.js +26 -0
  53. package/dist/commands/service-accounts/tokens/list.d.ts +12 -0
  54. package/dist/commands/service-accounts/tokens/list.js +40 -0
  55. package/dist/commands/service-accounts/update.d.ts +31 -0
  56. package/dist/commands/service-accounts/update.js +105 -0
  57. package/dist/commands/shared/delete.d.ts +12 -0
  58. package/dist/commands/shared/delete.js +30 -0
  59. package/dist/commands/shared/list.d.ts +6 -0
  60. package/dist/commands/shared/list.js +45 -0
  61. package/dist/commands/shared/set.d.ts +14 -0
  62. package/dist/commands/shared/set.js +41 -0
  63. package/dist/commands/tickets/ask.d.ts +13 -0
  64. package/dist/commands/tickets/ask.js +81 -0
  65. package/dist/commands/tickets/dependencies/add.d.ts +15 -0
  66. package/dist/commands/tickets/dependencies/add.js +62 -0
  67. package/dist/commands/tickets/dependencies/list.d.ts +12 -0
  68. package/dist/commands/tickets/dependencies/list.js +37 -0
  69. package/dist/commands/tickets/dependencies/remove.d.ts +14 -0
  70. package/dist/commands/tickets/dependencies/remove.js +59 -0
  71. package/dist/commands/tickets/work-context.d.ts +15 -0
  72. package/dist/commands/tickets/work-context.js +130 -0
  73. package/dist/commands/vault/requests/approve.d.ts +9 -0
  74. package/dist/commands/vault/requests/approve.js +40 -0
  75. package/dist/commands/vault/requests/list.d.ts +6 -0
  76. package/dist/commands/vault/requests/list.js +29 -0
  77. package/dist/commands/vault/requests/reject.d.ts +12 -0
  78. package/dist/commands/vault/requests/reject.js +52 -0
  79. package/dist/commands/workload/create.d.ts +15 -0
  80. package/dist/commands/workload/create.js +45 -0
  81. package/dist/commands/workload/delete.d.ts +12 -0
  82. package/dist/commands/workload/delete.js +26 -0
  83. package/dist/commands/workload/list.d.ts +9 -0
  84. package/dist/commands/workload/list.js +29 -0
  85. package/dist/commands/workload/participant/add.d.ts +10 -0
  86. package/dist/commands/workload/participant/add.js +27 -0
  87. package/dist/commands/workload/participant/remove.d.ts +10 -0
  88. package/dist/commands/workload/participant/remove.js +23 -0
  89. package/dist/commands/workload/promote.d.ts +17 -0
  90. package/dist/commands/workload/promote.js +58 -0
  91. package/dist/commands/workload/show.d.ts +9 -0
  92. package/dist/commands/workload/show.js +24 -0
  93. package/dist/commands/workload/update.d.ts +17 -0
  94. package/dist/commands/workload/update.js +45 -0
  95. package/dist/errors/error-codes.d.ts +17 -0
  96. package/dist/errors/error-codes.js +36 -0
  97. package/dist/lib/api.d.ts +7 -1
  98. package/dist/lib/api.js +8 -0
  99. package/dist/lib/assistant.d.ts +29 -0
  100. package/dist/lib/assistant.js +76 -0
  101. package/dist/lib/bulk-triage.d.ts +35 -0
  102. package/dist/lib/bulk-triage.js +55 -0
  103. package/dist/lib/grouping-rules.d.ts +28 -0
  104. package/dist/lib/grouping-rules.js +39 -0
  105. package/dist/lib/knowledge.d.ts +15 -0
  106. package/dist/lib/knowledge.js +36 -0
  107. package/dist/lib/output.js +10 -0
  108. package/dist/lib/poll.d.ts +51 -0
  109. package/dist/lib/poll.js +59 -0
  110. package/dist/lib/service-accounts.d.ts +65 -0
  111. package/dist/lib/service-accounts.js +132 -0
  112. package/dist/lib/vault-requests.d.ts +38 -0
  113. package/dist/lib/vault-requests.js +125 -0
  114. package/dist/lib/workload.d.ts +37 -0
  115. package/dist/lib/workload.js +64 -0
  116. package/oclif.manifest.json +6830 -4502
  117. package/opencli.json +2177 -547
  118. package/package.json +27 -3
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class KbAttachments 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
+ run(): Promise<unknown>;
9
+ }
@@ -0,0 +1,36 @@
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
+ const paginate_1 = require("../../lib/paginate");
7
+ class KbAttachments extends base_1.BaseCommand {
8
+ static args = {
9
+ id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
10
+ };
11
+ static description = "List a knowledge page's attachments";
12
+ static examples = [
13
+ '<%= config.bin %> kb attachments <page-id>',
14
+ '<%= config.bin %> kb attachments <page-id> --json',
15
+ ];
16
+ async run() {
17
+ const { args } = await this.parse(KbAttachments);
18
+ // Elenco completo, non la sola prima pagina: il comando non ha un --page, quindi fermarsi a
19
+ // cento allegati significherebbe nasconderne il resto senza modo di chiederli.
20
+ const attachments = await (0, paginate_1.fetchAllPages)(this.api, `/cli/v1/knowledge/pages/${encodeURIComponent(args.id)}/attachments`);
21
+ if (!this.jsonEnabled()) {
22
+ this.log((0, output_1.renderTable)(['ID', 'FILENAME', 'CONTENT_TYPE', 'BYTE_SIZE', 'SCRIPT', 'CREATED_AT'], attachments.map((attachment) => [
23
+ String(attachment.id ?? ''),
24
+ String(attachment.filename ?? ''),
25
+ String(attachment.content_type ?? ''),
26
+ String(attachment.byte_size ?? ''),
27
+ // Il server dichiara se il file è uno script: sapere che si sta scaricando qualcosa di
28
+ // eseguibile è la sola cosa che l'elenco può dire prima che il file sia sul disco.
29
+ attachment.script === undefined ? '' : (attachment.script ? 'yes' : 'no'),
30
+ String(attachment.created_at ?? ''),
31
+ ])));
32
+ }
33
+ return { data: attachments, meta: (0, paginate_1.aggregatedMeta)(attachments.length) };
34
+ }
35
+ }
36
+ exports.default = KbAttachments;
@@ -3,6 +3,9 @@ export default class KbCreate extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ group: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ tag: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
6
9
  title: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
10
  kind: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
11
  body: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -11,7 +14,7 @@ export default class KbCreate extends BaseCommand {
11
14
  'tech-spec-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
15
  'in-review': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
16
  'review-note': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
- project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ 'author-origin': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
18
  };
16
19
  run(): Promise<unknown>;
17
20
  }
@@ -3,19 +3,33 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const promises_1 = require("node:fs/promises");
4
4
  const core_1 = require("@oclif/core");
5
5
  const base_1 = require("../../base");
6
+ const knowledge_1 = require("../../lib/knowledge");
6
7
  const limits_1 = require("../../lib/limits");
7
8
  const output_1 = require("../../lib/output");
8
9
  class KbCreate extends base_1.BaseCommand {
9
- static description = 'Create a knowledge page in a project';
10
+ static description = 'Create a knowledge page in one or more projects (and groups)';
10
11
  static examples = [
11
12
  '<%= config.bin %> kb create --project acme-api --title "Kamal rollback" --body "Steps…"',
12
13
  '<%= config.bin %> kb create -p acme-api --title "ADR: queue backend" --kind decision --body-file ./adr.md',
13
14
  '<%= config.bin %> kb create -p acme-api --title "Deploy" --body-file ./body.md --tech-spec-file ./tech.md',
14
15
  '<%= config.bin %> kb create -p acme-api --title "Worktree trap" --body-file ./note.md --in-review --review-note "Hit it today, not obvious from the code"',
16
+ '<%= config.bin %> kb create -p acme-api -p billing --group Piattaforma --tag deploy --tag kamal --title "Rollback" --body-file ./note.md',
15
17
  ];
16
18
  static flags = {
17
- // Required passed straight to the server as `project` (key or UUID), resolved there.
18
- ...base_1.projectFlag,
19
+ // Repeatable: a page belongs to every project you name (plus every project of the groups).
20
+ // The first one goes out as `project` (key or UUID, resolved server-side, no extra lookup);
21
+ // the others are resolved here because `project_ids` only takes ids.
22
+ //
23
+ // Not `required`: a page can live on groups alone (a procedure that follows a whole
24
+ // macro-project, including the projects it will gain later). What a page cannot do is belong
25
+ // nowhere — the guard in `run` asks for one of the two.
26
+ project: core_1.Flags.string({
27
+ char: 'p',
28
+ description: 'Project id (UUID) or key the page belongs to (repeatable)',
29
+ multiple: true,
30
+ }),
31
+ group: core_1.Flags.string({ description: 'Group id (UUID) or name the page belongs to (repeatable)', multiple: true }),
32
+ tag: core_1.Flags.string({ description: 'Tag for the page (repeatable; the server lowercases and dedupes them)', multiple: true }),
19
33
  title: core_1.Flags.string({ description: `Page title (max ${limits_1.LENGTH_LIMITS.title} characters)`, required: true }),
20
34
  kind: core_1.Flags.string({ description: 'Page kind: note|decision|guide', default: 'note' }),
21
35
  body: core_1.Flags.string({ description: `Page body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters)` }),
@@ -34,6 +48,11 @@ class KbCreate extends base_1.BaseCommand {
34
48
  'review-note': core_1.Flags.string({
35
49
  description: `One line explaining why the page is worth keeping (review queue, max ${limits_1.LENGTH_LIMITS.reviewNote} characters)`,
36
50
  }),
51
+ // Chi ha scritto materialmente il testo, che NON è il proprietario del token: il nome
52
+ // dell'assistente o della skill. Senza, una proposta in revisione resta attribuita al canale.
53
+ 'author-origin': core_1.Flags.string({
54
+ description: 'Name of the assistant or skill that wrote the text (declares an automatic author)',
55
+ }),
37
56
  };
38
57
  async run() {
39
58
  const { flags } = await this.parse(KbCreate);
@@ -75,18 +94,35 @@ class KbCreate extends base_1.BaseCommand {
75
94
  ]);
76
95
  if (tooLong)
77
96
  this.error(tooLong, { exit: 2 });
97
+ const [primary, ...extraProjects] = (0, knowledge_1.cleanList)(flags.project);
98
+ const groupRefs = (0, knowledge_1.cleanList)(flags.group);
99
+ // Una pagina deve appartenere a qualcosa: senza scope il server la creerebbe org-wide, visibile
100
+ // soltanto a chi ha accesso pieno — cioè invisibile a chi l'ha appena scritta.
101
+ if (primary === undefined && groupRefs.length === 0) {
102
+ this.error('Provide at least one --project or one --group: a knowledge page has to belong somewhere.', { exit: 2 });
103
+ }
104
+ const projectIds = await this.resolveAll(extraProjects, (ref) => this.resolveProjectId(ref));
105
+ const groupIds = await this.resolveAll(groupRefs, (ref) => this.resolveGroupId(ref));
106
+ const tags = (0, knowledge_1.cleanList)(flags.tag);
78
107
  const res = await this.api.post('/cli/v1/knowledge/pages', {
79
- project: flags.project,
108
+ // Chiavi presenti solo quando c'è qualcosa da collegare: il server unisce `project` e
109
+ // `project_ids`, quindi un elenco vuoto sarebbe rumore in ogni creazione mono-progetto.
110
+ ...(primary !== undefined && { project: primary }),
111
+ ...(projectIds.length > 0 && { project_ids: projectIds }),
112
+ ...(groupIds.length > 0 && { group_ids: groupIds }),
113
+ ...(tags.length > 0 && { tags }),
80
114
  title: flags.title,
81
115
  kind: flags.kind,
82
116
  body,
83
117
  tech_spec: techSpec,
84
118
  in_review: flags['in-review'] ? true : undefined,
85
119
  review_note: flags['review-note'],
120
+ author_origin: flags['author-origin'],
86
121
  });
87
122
  if (!this.jsonEnabled()) {
123
+ const page = res.data ?? {};
88
124
  this.log(flags['in-review'] ? 'Knowledge page proposed, waiting for review:' : 'Knowledge page created:');
89
- this.log((0, output_1.renderRecord)(res.data ?? {}));
125
+ this.log((0, output_1.renderRecord)({ ...page, ...(0, knowledge_1.pageScopeLines)(page) }));
90
126
  }
91
127
  return res;
92
128
  }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class KbDetach extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ attachment: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ confirm: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ }
@@ -0,0 +1,26 @@
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 KbDetach extends base_1.BaseCommand {
6
+ static description = 'Remove an attachment from a knowledge page (irreversible)';
7
+ static examples = ['<%= config.bin %> kb detach <page-id> --attachment <attachment-id> --confirm'];
8
+ static args = {
9
+ id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
10
+ };
11
+ static flags = {
12
+ attachment: core_1.Flags.string({ description: 'Attachment id to remove', required: true }),
13
+ confirm: core_1.Flags.boolean({ description: 'Required: confirm the irreversible deletion' }),
14
+ };
15
+ async run() {
16
+ const { args, flags } = await this.parse(KbDetach);
17
+ if (!flags.confirm) {
18
+ this.error('Refusing to delete without --confirm (the action is irreversible).', { exit: 2 });
19
+ }
20
+ await this.api.delete(`/cli/v1/knowledge/pages/${encodeURIComponent(args.id)}/attachments/${encodeURIComponent(flags.attachment)}`);
21
+ if (!this.jsonEnabled())
22
+ this.log(`Removed attachment ${flags.attachment} from knowledge page ${args.id}`);
23
+ return { detached: flags.attachment, page: args.id };
24
+ }
25
+ }
26
+ exports.default = KbDetach;
@@ -37,6 +37,9 @@ class KbShow extends base_1.BaseCommand {
37
37
  kind: page.kind,
38
38
  project: page.project,
39
39
  project_name: page.project_name,
40
+ // Gli altri progetti, i gruppi e i tag: una pagina condivisa fra più ambiti si leggeva
41
+ // come se appartenesse solo al primo (CYCL-45).
42
+ ...(0, knowledge_1.pageScopeLines)(page),
40
43
  author: page.author,
41
44
  created_at: page.created_at,
42
45
  updated_at: page.updated_at,
@@ -7,6 +7,10 @@ export default class KbUpdate extends BaseCommand {
7
7
  };
8
8
  static flags: {
9
9
  title: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ group: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ tag: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ 'author-origin': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
14
  kind: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
15
  body: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
16
  'body-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const promises_1 = require("node:fs/promises");
4
4
  const core_1 = require("@oclif/core");
5
5
  const base_1 = require("../../base");
6
+ const knowledge_1 = require("../../lib/knowledge");
6
7
  const limits_1 = require("../../lib/limits");
7
8
  const output_1 = require("../../lib/output");
8
9
  class KbUpdate extends base_1.BaseCommand {
@@ -11,12 +12,32 @@ class KbUpdate extends base_1.BaseCommand {
11
12
  '<%= config.bin %> kb update <page-id> --title "New title"',
12
13
  '<%= config.bin %> kb update <page-id> --kind decision --body-file ./adr.md',
13
14
  '<%= config.bin %> kb update <page-id> --tech-spec-file ./tech.md',
15
+ '<%= config.bin %> kb update <page-id> --project acme-api --project billing --group Piattaforma',
16
+ '<%= config.bin %> kb update <page-id> --tag deploy --tag kamal',
14
17
  ];
15
18
  static args = {
16
19
  id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
17
20
  };
18
21
  static flags = {
19
22
  title: core_1.Flags.string({ description: `New title (max ${limits_1.LENGTH_LIMITS.title} characters)` }),
23
+ // Ambiti e tag: passarli SOSTITUISCE l'elenco (è come sono fatti sul server), non passarli lo
24
+ // lascia intatto. Per questo viaggiano solo quando il comando li riceve davvero.
25
+ project: core_1.Flags.string({
26
+ char: 'p',
27
+ description: 'Replace the projects the page belongs to: id (UUID) or key (repeatable)',
28
+ multiple: true,
29
+ }),
30
+ group: core_1.Flags.string({
31
+ description: 'Replace the groups the page belongs to: id (UUID) or name (repeatable)',
32
+ multiple: true,
33
+ }),
34
+ tag: core_1.Flags.string({
35
+ description: 'Replace the page tags (repeatable; `--tag ""` clears them)',
36
+ multiple: true,
37
+ }),
38
+ 'author-origin': core_1.Flags.string({
39
+ description: 'Name of the assistant or skill that rewrote the text (only applies with a text change)',
40
+ }),
20
41
  kind: core_1.Flags.string({ description: 'New kind: note|decision|guide' }),
21
42
  body: core_1.Flags.string({ description: `New body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters)` }),
22
43
  'body-file': core_1.Flags.string({
@@ -56,33 +77,60 @@ class KbUpdate extends base_1.BaseCommand {
56
77
  if (flags.title === undefined &&
57
78
  flags.kind === undefined &&
58
79
  body === undefined &&
59
- techSpec === undefined) {
60
- this.error('Provide at least one of --title, --kind, --body, --body-file, --tech-spec or --tech-spec-file.', { exit: 2 });
80
+ techSpec === undefined &&
81
+ flags.project === undefined &&
82
+ flags.group === undefined &&
83
+ flags.tag === undefined) {
84
+ this.error('Provide at least one of --title, --kind, --body, --body-file, --tech-spec, --tech-spec-file, --project, --group or --tag.', { exit: 2 });
61
85
  }
62
86
  // The backend expects the full title/body/kind/tech_spec on update → read the current page and
63
87
  // merge, so a partial edit (e.g. only --tech-spec) doesn't blank out the other fields.
88
+ //
89
+ // Solo quando il testo cambia davvero: un update di soli ambiti o tag non ha niente da fondere,
90
+ // e rimandare il testo appena letto cancellerebbe la modifica di chi ha scritto nel frattempo
91
+ // (CYCL-45). Senza campi di testo non c'è nemmeno niente da misurare, quindi la lettura salta.
64
92
  const base = `/cli/v1/knowledge/pages/${encodeURIComponent(args.id)}`;
65
- const current = await this.api.get(base);
66
- const page = current.data ?? {};
67
- // I campi troppo lunghi si scoprono qui, tutti insieme, invece che uno per rifiuto (CYCL-32). Il
68
- // valore attuale fa da salvaguardia: una pagina scritta prima che il tetto esistesse si può
69
- // ancora correggere e accorciare per gradi, com'è dal server.
70
- const tooLong = (0, limits_1.tooLongMessage)([
71
- { flag: '--title', value: flags.title, max: limits_1.LENGTH_LIMITS.title, previous: page.title },
72
- { flag: bodyFlag, value: body, max: limits_1.LENGTH_LIMITS.pageBody, previous: page.body },
73
- { flag: techSpecFlag, value: techSpec, max: limits_1.LENGTH_LIMITS.techSpec, previous: page.tech_spec },
74
- ]);
75
- if (tooLong)
76
- this.error(tooLong, { exit: 2 });
77
- const res = await this.api.put(base, {
78
- title: flags.title ?? page.title,
79
- kind: flags.kind ?? page.kind,
80
- body: body ?? page.body,
81
- tech_spec: techSpec ?? page.tech_spec,
82
- });
93
+ const editsText = flags.title !== undefined || flags.kind !== undefined || body !== undefined || techSpec !== undefined;
94
+ let text = {};
95
+ if (editsText) {
96
+ const current = await this.api.get(base);
97
+ const page = current.data ?? {};
98
+ // I campi troppo lunghi si scoprono qui, tutti insieme, invece che uno per rifiuto (CYCL-32). Il
99
+ // valore attuale fa da salvaguardia: una pagina scritta prima che il tetto esistesse si può
100
+ // ancora correggere e accorciare per gradi, com'è dal server.
101
+ const tooLong = (0, limits_1.tooLongMessage)([
102
+ { flag: '--title', value: flags.title, max: limits_1.LENGTH_LIMITS.title, previous: page.title },
103
+ { flag: bodyFlag, value: body, max: limits_1.LENGTH_LIMITS.pageBody, previous: page.body },
104
+ { flag: techSpecFlag, value: techSpec, max: limits_1.LENGTH_LIMITS.techSpec, previous: page.tech_spec },
105
+ ]);
106
+ if (tooLong)
107
+ this.error(tooLong, { exit: 2 });
108
+ text = {
109
+ title: flags.title ?? page.title,
110
+ kind: flags.kind ?? page.kind,
111
+ body: body ?? page.body,
112
+ tech_spec: techSpec ?? page.tech_spec,
113
+ };
114
+ }
115
+ // Scope e tag: la chiave viaggia SOLO se il flag è stato passato, perché il server aggiorna
116
+ // esattamente quello che riceve — mandare un elenco vuoto per "non li tocco" scollegherebbe la
117
+ // pagina da progetti e gruppi e le cancellerebbe i tag ad ogni correzione di una virgola (CYCL-45).
118
+ const scope = {};
119
+ if (flags.project !== undefined) {
120
+ scope.project_ids = await this.resolveAll((0, knowledge_1.cleanList)(flags.project), (ref) => this.resolveProjectId(ref));
121
+ }
122
+ if (flags.group !== undefined) {
123
+ scope.group_ids = await this.resolveAll((0, knowledge_1.cleanList)(flags.group), (ref) => this.resolveGroupId(ref));
124
+ }
125
+ if (flags.tag !== undefined)
126
+ scope.tags = (0, knowledge_1.cleanList)(flags.tag);
127
+ if (flags['author-origin'] !== undefined)
128
+ scope.author_origin = flags['author-origin'];
129
+ const res = await this.api.put(base, { ...text, ...scope });
83
130
  if (!this.jsonEnabled()) {
131
+ const updated = res.data ?? {};
84
132
  this.log('Knowledge page updated:');
85
- this.log((0, output_1.renderRecord)(res.data ?? {}));
133
+ this.log((0, output_1.renderRecord)({ ...updated, ...(0, knowledge_1.pageScopeLines)(updated) }));
86
134
  }
87
135
  return res;
88
136
  }
@@ -0,0 +1,11 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class MetricsBulkTriage extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ action: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ group: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<unknown>;
11
+ }
@@ -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
+ const error_codes_1 = require("../../errors/error-codes");
6
+ const bulk_triage_1 = require("../../lib/bulk-triage");
7
+ const output_1 = require("../../lib/output");
8
+ class MetricsBulkTriage extends base_1.BaseCommand {
9
+ static description = 'Resolve, ignore or reopen several metric groups in one call';
10
+ static examples = [
11
+ '<%= config.bin %> metrics bulk-triage -p acme-api --action resolve --group <id-a> --group <id-b>',
12
+ '<%= config.bin %> metrics bulk-triage -p acme-api --action ignore --group <id-a> --json',
13
+ ];
14
+ static flags = {
15
+ ...base_1.projectFlag,
16
+ action: core_1.Flags.string({ description: 'What to do with them', options: [...bulk_triage_1.TRIAGE_ACTIONS], required: true }),
17
+ group: core_1.Flags.string({ description: 'Metric group id (repeatable)', multiple: true, required: true }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(MetricsBulkTriage);
21
+ const projectId = await this.resolveProjectId(flags.project);
22
+ const ids = (0, bulk_triage_1.uniqueIds)(flags.group);
23
+ const res = await this.api.post(`/cli/v1/projects/${encodeURIComponent(projectId)}/metric_groups/bulk_triage`, { bulk_action: flags.action, ids });
24
+ const groups = res.data ?? [];
25
+ if (!this.jsonEnabled()) {
26
+ this.log((0, bulk_triage_1.triagedLine)(groups.length, 'metric group', flags.action));
27
+ this.log((0, output_1.renderTable)(['ID', 'KIND', 'NAME', 'STATUS'], groups.map((group) => [
28
+ String(group.id ?? ''),
29
+ String(group.kind ?? ''),
30
+ String(group.name ?? group.title ?? ''),
31
+ (0, output_1.statusCell)(group.status),
32
+ ])));
33
+ }
34
+ (0, bulk_triage_1.assertNothingDropped)({
35
+ action: flags.action,
36
+ code: error_codes_1.ErrorCodes.MetricGroup.notFound,
37
+ noun: 'metric group',
38
+ requested: ids,
39
+ rows: groups,
40
+ });
41
+ return res;
42
+ }
43
+ }
44
+ exports.default = MetricsBulkTriage;
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoSitesShow extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ run(): Promise<unknown>;
9
+ /**
10
+ * One nested block as a section of its own — or nothing at all when the payload has no such
11
+ * block. An empty "Vitals" heading reads as "this site has no vitals measured", when it really
12
+ * means "this server never sent them": same reason `tickets show` skips absent guidance (CYCL-2).
13
+ */
14
+ private printBlock;
15
+ }
@@ -0,0 +1,107 @@
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
+ /**
7
+ * The blocks the card opens with, in reading order, each with the heading a reader recognises.
8
+ * Everything else the payload carries is printed after these, under a heading derived from its own
9
+ * key: the endpoint is new (CYRA-541) and keeps growing, and a client that only knows five names
10
+ * would drop the sixth block without ever saying so.
11
+ */
12
+ const KNOWN_BLOCKS = [
13
+ { icon: '🔎', key: 'open_issues', title: 'Open findings' },
14
+ { icon: '🕒', key: 'last_audit', title: 'Last run' },
15
+ { icon: '⚡', key: 'vitals', title: 'Vitals' },
16
+ { icon: '📦', key: 'project', title: 'Project' },
17
+ { icon: '🌱', key: 'environment', title: 'Environment' },
18
+ ];
19
+ /**
20
+ * The known block keys, each mapped to `undefined`, to spread over the head record. `renderRecord`
21
+ * prints a null scalar as `key: -`, so a site that was never checked (`last_audit: null`) would
22
+ * grow a `last_audit: -` line among its settings — a block that is not there, listed as if it were
23
+ * a setting you could change. Same trick `seo show` uses to lift `evidence` out of its head.
24
+ */
25
+ const BLOCKS_OFF_THE_HEAD = Object.fromEntries(KNOWN_BLOCKS.map((block) => [block.key, undefined]));
26
+ /** A value `renderRecord`/`renderTable` can print as a cell (null included: it prints as `-`). */
27
+ function isScalar(value) {
28
+ return value === null || ['string', 'number', 'boolean'].includes(typeof value);
29
+ }
30
+ /** `recent_audits` → `Recent audits`: the heading for a block nobody taught this client about. */
31
+ function titleFromKey(key) {
32
+ const words = key.split('_').join(' ');
33
+ return words.charAt(0).toUpperCase() + words.slice(1);
34
+ }
35
+ /**
36
+ * A list of objects as a table. The columns are every scalar field the rows carry, in the order
37
+ * they first appear: taking the shape from the first row alone would silently crop a field that
38
+ * only the failed run has (its error). Returns '' when no row has anything printable.
39
+ */
40
+ function renderRows(rows) {
41
+ const columns = [];
42
+ for (const row of rows) {
43
+ for (const [key, value] of Object.entries(row)) {
44
+ if (isScalar(value) && !columns.includes(key))
45
+ columns.push(key);
46
+ }
47
+ }
48
+ if (columns.length === 0)
49
+ return '';
50
+ return (0, output_1.renderTable)(columns.map((column) => column.toUpperCase()), rows.map((row) => columns.map((column) => String(row[column] ?? ''))));
51
+ }
52
+ /** The printable body of a nested block, or '' when there is nothing in it to print. */
53
+ function renderBlock(value) {
54
+ if (Array.isArray(value)) {
55
+ const rows = value.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item));
56
+ if (rows.length > 0)
57
+ return renderRows(rows);
58
+ // A list of plain values (enabled checks, urls) reads as one line, not as a one-column table.
59
+ const items = value.filter((item) => isScalar(item) && item !== null);
60
+ return items.map((item) => (0, output_1.sanitize)(item)).join(', ');
61
+ }
62
+ if (value && typeof value === 'object') {
63
+ const record = value;
64
+ return Object.keys(record).length > 0 ? (0, output_1.renderRecord)(record) : '';
65
+ }
66
+ return '';
67
+ }
68
+ class SeoSitesShow extends base_1.BaseCommand {
69
+ static description = 'Show one site under SEO check: how it is set up, what is open, how the last run went and how fast it is';
70
+ static examples = ['<%= config.bin %> seo-sites show <site-id>', '<%= config.bin %> seo-sites show <site-id> --json'];
71
+ static args = {
72
+ id: core_1.Args.string({ description: 'Site id', required: true }),
73
+ };
74
+ async run() {
75
+ const { args } = await this.parse(SeoSitesShow);
76
+ const res = await this.api.get(`/cli/v1/seo_sites/${encodeURIComponent(args.id)}`);
77
+ if (!this.jsonEnabled()) {
78
+ const data = res.data ?? {};
79
+ // Configuration and cadence first: everything you would change with `seo-sites update`.
80
+ this.log((0, output_1.renderRecord)({ ...data, ...BLOCKS_OFF_THE_HEAD }));
81
+ for (const { icon, key, title } of KNOWN_BLOCKS)
82
+ this.printBlock(title, data[key], icon);
83
+ const known = new Set(KNOWN_BLOCKS.map((block) => block.key));
84
+ for (const [key, value] of Object.entries(data)) {
85
+ if (known.has(key) || isScalar(value))
86
+ continue;
87
+ // The heading comes from a server-supplied key: sanitize it like every other printed
88
+ // string (CWE-150). `section` interpolates its title raw, so this is the last stop.
89
+ this.printBlock((0, output_1.sanitize)(titleFromKey(key)), value);
90
+ }
91
+ }
92
+ return res;
93
+ }
94
+ /**
95
+ * One nested block as a section of its own — or nothing at all when the payload has no such
96
+ * block. An empty "Vitals" heading reads as "this site has no vitals measured", when it really
97
+ * means "this server never sent them": same reason `tickets show` skips absent guidance (CYCL-2).
98
+ */
99
+ printBlock(title, value, icon) {
100
+ const body = renderBlock(value);
101
+ if (!body)
102
+ return;
103
+ this.log((0, output_1.section)(title, icon));
104
+ this.log(body);
105
+ }
106
+ }
107
+ exports.default = SeoSitesShow;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class ServiceAccountsShow 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
+ run(): Promise<unknown>;
9
+ }
@@ -0,0 +1,26 @@
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
+ const service_accounts_1 = require("../../lib/service-accounts");
7
+ class ServiceAccountsShow extends base_1.BaseCommand {
8
+ static args = {
9
+ id: core_1.Args.string({ description: 'Service account id', required: true }),
10
+ };
11
+ static description = 'Show a service account: identity, how many tokens are active, and the environments it can read secrets in';
12
+ static examples = ['<%= config.bin %> service-accounts show <id>', '<%= config.bin %> service-accounts show <id> --json'];
13
+ async run() {
14
+ const { args } = await this.parse(ServiceAccountsShow);
15
+ const res = await this.api.get(`/cli/v1/service/accounts/${encodeURIComponent(args.id)}`);
16
+ const account = (0, service_accounts_1.withoutSecret)(res.data ?? {});
17
+ if (!this.jsonEnabled()) {
18
+ this.log((0, output_1.renderRecord)((0, service_accounts_1.accountSummary)(account)));
19
+ this.log((0, service_accounts_1.environmentsLine)(account.secret_environment_codes));
20
+ }
21
+ // Stripped on the machine path too, not just on screen: the human branch prints an allow-list of
22
+ // fields, so leaving `--json` on the raw payload would make the quieter output the leakier one.
23
+ return { ...res, data: account };
24
+ }
25
+ }
26
+ exports.default = ServiceAccountsShow;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class ServiceAccountsTokensList extends BaseCommand {
3
+ static args: {
4
+ account: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -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 output_1 = require("../../../lib/output");
6
+ const service_accounts_1 = require("../../../lib/service-accounts");
7
+ class ServiceAccountsTokensList extends base_1.BaseCommand {
8
+ static args = {
9
+ account: core_1.Args.string({ description: 'Service account id', required: true }),
10
+ };
11
+ static description = 'List the CLI tokens of a service account — which ones still work and which are revoked. Never the secret.';
12
+ static examples = [
13
+ '<%= config.bin %> service-accounts tokens list <account-id>',
14
+ '<%= config.bin %> service-accounts tokens list <account-id> --json',
15
+ ];
16
+ static flags = { ...base_1.pageFlag };
17
+ async run() {
18
+ const { args, flags } = await this.parse(ServiceAccountsTokensList);
19
+ // The backend clamps a page below 1 back to page 1 without a word, so a script that computed the
20
+ // wrong number would get plausible rows instead of an error.
21
+ const invalid = (0, base_1.paginationError)(flags);
22
+ if (invalid)
23
+ this.error(invalid, { exit: 2 });
24
+ const res = await this.api.get(`/cli/v1/service/accounts/${encodeURIComponent(args.account)}/tokens?page=${flags.page}`);
25
+ // Stripped on both paths: a listing tells you which tokens exist, never what they are.
26
+ const tokens = (res.data ?? []).map((token) => (0, service_accounts_1.withoutSecret)(token));
27
+ if (!this.jsonEnabled()) {
28
+ this.log((0, output_1.renderTable)(['ID', 'NAME', 'PREFIX', 'STATUS', 'LAST USED', 'REVOKED AT'], tokens.map((token) => [
29
+ String(token.id ?? ''),
30
+ String(token.name ?? ''),
31
+ String(token.token_prefix ?? ''),
32
+ (0, output_1.statusCell)((0, service_accounts_1.tokenStatus)(token)),
33
+ String(token.last_used_at ?? '-'),
34
+ String(token.revoked_at ?? '-'),
35
+ ])));
36
+ }
37
+ return { ...res, data: tokens };
38
+ }
39
+ }
40
+ exports.default = ServiceAccountsTokensList;