@bussolabs/closeyourit-cli 0.22.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 (116) hide show
  1. package/README.md +44 -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/service-accounts/show.d.ts +9 -0
  50. package/dist/commands/service-accounts/show.js +26 -0
  51. package/dist/commands/service-accounts/tokens/list.d.ts +12 -0
  52. package/dist/commands/service-accounts/tokens/list.js +40 -0
  53. package/dist/commands/service-accounts/update.d.ts +31 -0
  54. package/dist/commands/service-accounts/update.js +105 -0
  55. package/dist/commands/shared/delete.d.ts +12 -0
  56. package/dist/commands/shared/delete.js +30 -0
  57. package/dist/commands/shared/list.d.ts +6 -0
  58. package/dist/commands/shared/list.js +45 -0
  59. package/dist/commands/shared/set.d.ts +14 -0
  60. package/dist/commands/shared/set.js +41 -0
  61. package/dist/commands/tickets/ask.d.ts +13 -0
  62. package/dist/commands/tickets/ask.js +81 -0
  63. package/dist/commands/tickets/dependencies/add.d.ts +15 -0
  64. package/dist/commands/tickets/dependencies/add.js +62 -0
  65. package/dist/commands/tickets/dependencies/list.d.ts +12 -0
  66. package/dist/commands/tickets/dependencies/list.js +37 -0
  67. package/dist/commands/tickets/dependencies/remove.d.ts +14 -0
  68. package/dist/commands/tickets/dependencies/remove.js +59 -0
  69. package/dist/commands/tickets/work-context.d.ts +15 -0
  70. package/dist/commands/tickets/work-context.js +130 -0
  71. package/dist/commands/vault/requests/approve.d.ts +9 -0
  72. package/dist/commands/vault/requests/approve.js +40 -0
  73. package/dist/commands/vault/requests/list.d.ts +6 -0
  74. package/dist/commands/vault/requests/list.js +29 -0
  75. package/dist/commands/vault/requests/reject.d.ts +12 -0
  76. package/dist/commands/vault/requests/reject.js +52 -0
  77. package/dist/commands/workload/create.d.ts +15 -0
  78. package/dist/commands/workload/create.js +45 -0
  79. package/dist/commands/workload/delete.d.ts +12 -0
  80. package/dist/commands/workload/delete.js +26 -0
  81. package/dist/commands/workload/list.d.ts +9 -0
  82. package/dist/commands/workload/list.js +29 -0
  83. package/dist/commands/workload/participant/add.d.ts +10 -0
  84. package/dist/commands/workload/participant/add.js +27 -0
  85. package/dist/commands/workload/participant/remove.d.ts +10 -0
  86. package/dist/commands/workload/participant/remove.js +23 -0
  87. package/dist/commands/workload/promote.d.ts +17 -0
  88. package/dist/commands/workload/promote.js +58 -0
  89. package/dist/commands/workload/show.d.ts +9 -0
  90. package/dist/commands/workload/show.js +24 -0
  91. package/dist/commands/workload/update.d.ts +17 -0
  92. package/dist/commands/workload/update.js +45 -0
  93. package/dist/errors/error-codes.d.ts +17 -0
  94. package/dist/errors/error-codes.js +36 -0
  95. package/dist/lib/api.d.ts +7 -1
  96. package/dist/lib/api.js +8 -0
  97. package/dist/lib/assistant.d.ts +29 -0
  98. package/dist/lib/assistant.js +76 -0
  99. package/dist/lib/bulk-triage.d.ts +35 -0
  100. package/dist/lib/bulk-triage.js +55 -0
  101. package/dist/lib/grouping-rules.d.ts +28 -0
  102. package/dist/lib/grouping-rules.js +39 -0
  103. package/dist/lib/knowledge.d.ts +15 -0
  104. package/dist/lib/knowledge.js +36 -0
  105. package/dist/lib/output.js +10 -0
  106. package/dist/lib/poll.d.ts +51 -0
  107. package/dist/lib/poll.js +59 -0
  108. package/dist/lib/service-accounts.d.ts +65 -0
  109. package/dist/lib/service-accounts.js +132 -0
  110. package/dist/lib/vault-requests.d.ts +38 -0
  111. package/dist/lib/vault-requests.js +125 -0
  112. package/dist/lib/workload.d.ts +37 -0
  113. package/dist/lib/workload.js +64 -0
  114. package/oclif.manifest.json +6290 -4001
  115. package/opencli.json +2164 -549
  116. package/package.json +27 -3
@@ -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,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;
@@ -0,0 +1,31 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class ServiceAccountsUpdate 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
+ 'secret-environment': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'all-environments': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ /**
14
+ * Refuse a code this organization does not declare, before the write goes out.
15
+ *
16
+ * The server would answer 200 to it: `SetSecretAccess` intersects the list with the real codes and
17
+ * saves the remainder, so a single typo empties the allow-list — which grants EVERY environment
18
+ * instead of restricting to one. Checked after the flag guards and never for `--all-environments`
19
+ * (there is no code to check), so the extra read only happens when it can prevent something.
20
+ */
21
+ private refuseUnknownEnvironments;
22
+ /**
23
+ * The environment codes of the organization, or `undefined` when they cannot be read.
24
+ *
25
+ * Listing them needs `environments.view`, which managing service accounts does not imply: a 403
26
+ * here says nothing about the update being legitimate, so it must not block it — the request/response
27
+ * comparison after the write stays as the guard. An empty list is read the same way rather than as
28
+ * "every code is invalid", so a lookup that changes shape can never refuse a correct command.
29
+ */
30
+ private organizationEnvironmentCodes;
31
+ }
@@ -0,0 +1,105 @@
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 api_1 = require("../../lib/api");
6
+ const output_1 = require("../../lib/output");
7
+ const paginate_1 = require("../../lib/paginate");
8
+ const service_accounts_1 = require("../../lib/service-accounts");
9
+ class ServiceAccountsUpdate extends base_1.BaseCommand {
10
+ // Only the secret boundary, on purpose: projects, groups and roles are three separate writes on
11
+ // the backend, and a command that offered them here would have to change access one piece at a
12
+ // time — a half-applied grant is worse than none. They stay on the access page until there is a
13
+ // single atomic update (CYCL-46).
14
+ static description = "Update a service account's secret access: the environments it may read secrets in (full replace)";
15
+ static examples = [
16
+ '<%= config.bin %> service-accounts update <id> --secret-environment staging --secret-environment production',
17
+ '<%= config.bin %> service-accounts update <id> --all-environments',
18
+ ];
19
+ static args = {
20
+ id: core_1.Args.string({ description: 'Service account id', required: true }),
21
+ };
22
+ static flags = {
23
+ 'secret-environment': core_1.Flags.string({
24
+ description: 'Environment code the account may read secrets in (repeatable; replaces the whole list)',
25
+ multiple: true,
26
+ }),
27
+ 'all-environments': core_1.Flags.boolean({
28
+ description: 'Remove the restriction entirely: secrets readable in every environment',
29
+ default: false,
30
+ }),
31
+ };
32
+ async run() {
33
+ const { args, flags } = await this.parse(ServiceAccountsUpdate);
34
+ const codes = (0, service_accounts_1.normalizeEnvironmentCodes)(flags['secret-environment'] ?? []);
35
+ const openUp = flags['all-environments'];
36
+ // The two flags say opposite things about the same list. Guessing which one wins would either
37
+ // widen the access silently or ignore the codes that were spelled out.
38
+ if (openUp && codes.length > 0) {
39
+ this.error('Pass either --secret-environment (the codes to allow) or --all-environments (no restriction at all), not both.', { exit: 2 });
40
+ }
41
+ // No flag, or nothing but blanks: the body would carry an empty list, and on this endpoint an
42
+ // empty list is not "leave it alone" — it is "every environment". An `update` that meant to
43
+ // change nothing would hand out the production secrets.
44
+ if (!openUp && codes.length === 0) {
45
+ this.error('Nothing to set: pass --secret-environment <code> (repeatable) to restrict the account, or ' +
46
+ '--all-environments to remove the restriction. An empty list means every environment, so it has to be asked for.', { exit: 2 });
47
+ }
48
+ await this.refuseUnknownEnvironments(codes);
49
+ const res = await this.api.put(`/cli/v1/service/accounts/${encodeURIComponent(args.id)}`, { secret_environment_codes: codes });
50
+ const account = (0, service_accounts_1.withoutSecret)(res.data ?? {});
51
+ const applied = (0, service_accounts_1.normalizeEnvironmentCodes)(Array.isArray(account.secret_environment_codes) ? account.secret_environment_codes : []);
52
+ if (!this.jsonEnabled()) {
53
+ this.log('Service account updated:');
54
+ this.log((0, output_1.renderRecord)((0, service_accounts_1.accountSummary)(account)));
55
+ this.log((0, service_accounts_1.environmentsLine)(applied));
56
+ }
57
+ // After printing: the boundary that is actually in force is what the reader needs most when the
58
+ // server kept something else, and only then does the command refuse to call it a success.
59
+ (0, service_accounts_1.assertBoundaryApplied)(codes, applied);
60
+ return { ...res, data: account };
61
+ }
62
+ /**
63
+ * Refuse a code this organization does not declare, before the write goes out.
64
+ *
65
+ * The server would answer 200 to it: `SetSecretAccess` intersects the list with the real codes and
66
+ * saves the remainder, so a single typo empties the allow-list — which grants EVERY environment
67
+ * instead of restricting to one. Checked after the flag guards and never for `--all-environments`
68
+ * (there is no code to check), so the extra read only happens when it can prevent something.
69
+ */
70
+ async refuseUnknownEnvironments(codes) {
71
+ if (codes.length === 0)
72
+ return;
73
+ const known = await this.organizationEnvironmentCodes();
74
+ if (known === undefined)
75
+ return;
76
+ const unknown = (0, service_accounts_1.unknownEnvironmentCodes)(codes, known);
77
+ if (unknown.length === 0)
78
+ return;
79
+ this.error(`Unknown environment ${unknown.length === 1 ? 'code' : 'codes'}: ${unknown.join(', ')}. ` +
80
+ `This organization declares: ${known.join(', ')}. ` +
81
+ 'Nothing was changed — a code that does not exist is dropped by the server, which would leave the ' +
82
+ 'account with no restriction at all (every environment).', { exit: 2 });
83
+ }
84
+ /**
85
+ * The environment codes of the organization, or `undefined` when they cannot be read.
86
+ *
87
+ * Listing them needs `environments.view`, which managing service accounts does not imply: a 403
88
+ * here says nothing about the update being legitimate, so it must not block it — the request/response
89
+ * comparison after the write stays as the guard. An empty list is read the same way rather than as
90
+ * "every code is invalid", so a lookup that changes shape can never refuse a correct command.
91
+ */
92
+ async organizationEnvironmentCodes() {
93
+ try {
94
+ const rows = await (0, paginate_1.fetchAllPages)(this.api, '/cli/v1/environments');
95
+ const codes = (0, service_accounts_1.normalizeEnvironmentCodes)(rows.map((row) => row.code));
96
+ return codes.length > 0 ? codes : undefined;
97
+ }
98
+ catch (error) {
99
+ if (error instanceof api_1.ApiRequestError)
100
+ return undefined;
101
+ throw error;
102
+ }
103
+ }
104
+ }
105
+ exports.default = ServiceAccountsUpdate;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SharedDelete 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
+ confirm: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,30 @@
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 SharedDelete extends base_1.BaseCommand {
6
+ static args = {
7
+ name: core_1.Args.string({ description: 'Shared secret name, or its id', required: true }),
8
+ };
9
+ // The backend has no per-environment destroy: like the web matrix, it deletes the whole row.
10
+ // Say so in the description, in the --confirm refusal and in the success line, so nobody reads
11
+ // this as "removes the production cell" (CYCL-41).
12
+ static description = 'Delete an organization shared secret from EVERY environment at once (irreversible). There is no per-environment deletion.';
13
+ static examples = ['<%= config.bin %> shared delete API_KEY --confirm'];
14
+ static flags = {
15
+ confirm: core_1.Flags.boolean({ description: 'Required: confirm the deletion across every environment' }),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(SharedDelete);
19
+ if (!flags.confirm) {
20
+ this.error(`Refusing to delete without --confirm: this removes ${args.name} from every environment at once, not just one.`, { exit: 2 });
21
+ }
22
+ const ref = base_1.UUID_RE.test(args.name) ? args.name : args.name.toUpperCase();
23
+ await this.api.delete(`/cli/v1/shared_secrets/${encodeURIComponent(ref)}`);
24
+ if (!this.jsonEnabled()) {
25
+ this.log(`Shared secret ${ref} deleted from every environment.`);
26
+ }
27
+ return { deleted: ref, environments: 'all' };
28
+ }
29
+ }
30
+ exports.default = SharedDelete;