@bussolabs/closeyourit-cli 0.22.0 → 0.24.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 (126) 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/coverage/show.d.ts +10 -0
  15. package/dist/commands/coverage/show.js +39 -0
  16. package/dist/commands/errors/assign.d.ts +14 -0
  17. package/dist/commands/errors/assign.js +52 -0
  18. package/dist/commands/errors/bulk-triage.d.ts +11 -0
  19. package/dist/commands/errors/bulk-triage.js +41 -0
  20. package/dist/commands/errors/delete.d.ts +13 -0
  21. package/dist/commands/errors/delete.js +28 -0
  22. package/dist/commands/errors/grouping-rules/create.d.ts +15 -0
  23. package/dist/commands/errors/grouping-rules/create.js +44 -0
  24. package/dist/commands/errors/grouping-rules/delete.d.ts +13 -0
  25. package/dist/commands/errors/grouping-rules/delete.js +27 -0
  26. package/dist/commands/errors/grouping-rules/list.d.ts +10 -0
  27. package/dist/commands/errors/grouping-rules/list.js +28 -0
  28. package/dist/commands/errors/grouping-rules/update.d.ts +18 -0
  29. package/dist/commands/errors/grouping-rules/update.js +43 -0
  30. package/dist/commands/errors/merge.d.ts +14 -0
  31. package/dist/commands/errors/merge.js +39 -0
  32. package/dist/commands/errors/split.d.ts +13 -0
  33. package/dist/commands/errors/split.js +34 -0
  34. package/dist/commands/invitations/create.js +9 -2
  35. package/dist/commands/invitations/resend.js +8 -2
  36. package/dist/commands/kb/attach.d.ts +12 -0
  37. package/dist/commands/kb/attach.js +49 -0
  38. package/dist/commands/kb/attachment-download.d.ts +14 -0
  39. package/dist/commands/kb/attachment-download.js +38 -0
  40. package/dist/commands/kb/attachments.d.ts +9 -0
  41. package/dist/commands/kb/attachments.js +36 -0
  42. package/dist/commands/kb/create.d.ts +4 -1
  43. package/dist/commands/kb/create.js +41 -5
  44. package/dist/commands/kb/detach.d.ts +13 -0
  45. package/dist/commands/kb/detach.js +26 -0
  46. package/dist/commands/kb/show.js +3 -0
  47. package/dist/commands/kb/update.d.ts +4 -0
  48. package/dist/commands/kb/update.js +69 -21
  49. package/dist/commands/metrics/bulk-triage.d.ts +11 -0
  50. package/dist/commands/metrics/bulk-triage.js +44 -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/clarifications.d.ts +21 -0
  66. package/dist/commands/tickets/clarifications.js +36 -0
  67. package/dist/commands/tickets/dependencies/add.d.ts +15 -0
  68. package/dist/commands/tickets/dependencies/add.js +62 -0
  69. package/dist/commands/tickets/dependencies/list.d.ts +12 -0
  70. package/dist/commands/tickets/dependencies/list.js +37 -0
  71. package/dist/commands/tickets/dependencies/remove.d.ts +14 -0
  72. package/dist/commands/tickets/dependencies/remove.js +59 -0
  73. package/dist/commands/tickets/work-context.d.ts +15 -0
  74. package/dist/commands/tickets/work-context.js +130 -0
  75. package/dist/commands/usage/list.d.ts +12 -0
  76. package/dist/commands/usage/list.js +43 -0
  77. package/dist/commands/usage/reporters.d.ts +10 -0
  78. package/dist/commands/usage/reporters.js +37 -0
  79. package/dist/commands/vault/requests/approve.d.ts +9 -0
  80. package/dist/commands/vault/requests/approve.js +40 -0
  81. package/dist/commands/vault/requests/list.d.ts +6 -0
  82. package/dist/commands/vault/requests/list.js +29 -0
  83. package/dist/commands/vault/requests/reject.d.ts +12 -0
  84. package/dist/commands/vault/requests/reject.js +52 -0
  85. package/dist/commands/workload/create.d.ts +15 -0
  86. package/dist/commands/workload/create.js +45 -0
  87. package/dist/commands/workload/delete.d.ts +12 -0
  88. package/dist/commands/workload/delete.js +26 -0
  89. package/dist/commands/workload/list.d.ts +9 -0
  90. package/dist/commands/workload/list.js +29 -0
  91. package/dist/commands/workload/participant/add.d.ts +10 -0
  92. package/dist/commands/workload/participant/add.js +27 -0
  93. package/dist/commands/workload/participant/remove.d.ts +10 -0
  94. package/dist/commands/workload/participant/remove.js +23 -0
  95. package/dist/commands/workload/promote.d.ts +17 -0
  96. package/dist/commands/workload/promote.js +58 -0
  97. package/dist/commands/workload/show.d.ts +9 -0
  98. package/dist/commands/workload/show.js +24 -0
  99. package/dist/commands/workload/update.d.ts +17 -0
  100. package/dist/commands/workload/update.js +45 -0
  101. package/dist/errors/error-codes.d.ts +17 -0
  102. package/dist/errors/error-codes.js +36 -0
  103. package/dist/lib/api.d.ts +7 -1
  104. package/dist/lib/api.js +8 -0
  105. package/dist/lib/assistant.d.ts +29 -0
  106. package/dist/lib/assistant.js +76 -0
  107. package/dist/lib/bulk-triage.d.ts +35 -0
  108. package/dist/lib/bulk-triage.js +55 -0
  109. package/dist/lib/grouping-rules.d.ts +28 -0
  110. package/dist/lib/grouping-rules.js +39 -0
  111. package/dist/lib/knowledge.d.ts +15 -0
  112. package/dist/lib/knowledge.js +36 -0
  113. package/dist/lib/output.js +10 -0
  114. package/dist/lib/poll.d.ts +51 -0
  115. package/dist/lib/poll.js +59 -0
  116. package/dist/lib/service-accounts.d.ts +65 -0
  117. package/dist/lib/service-accounts.js +132 -0
  118. package/dist/lib/ticket-clarifications.d.ts +13 -0
  119. package/dist/lib/ticket-clarifications.js +31 -0
  120. package/dist/lib/vault-requests.d.ts +38 -0
  121. package/dist/lib/vault-requests.js +125 -0
  122. package/dist/lib/workload.d.ts +37 -0
  123. package/dist/lib/workload.js +64 -0
  124. package/oclif.manifest.json +7495 -4994
  125. package/opencli.json +7424 -5644
  126. package/package.json +27 -3
@@ -148,10 +148,20 @@ const STATUS_TONE = {
148
148
  high: 'down',
149
149
  moderate: 'warn',
150
150
  low: 'neutral',
151
+ // workload action (carico di lavoro non-dev)
152
+ planned: 'neutral',
153
+ in_progress: 'warn',
154
+ done: 'ok',
155
+ cancelled: 'neutral',
151
156
  // runtime support state
152
157
  ending_soon: 'warn',
153
158
  eol: 'down',
154
159
  supported: 'ok',
160
+ // assistant reply + async AI request (CYCL-43): `streaming`/`pending` = still being written,
161
+ // `complete`/`done` = there is an answer, `failed` = there will not be one.
162
+ complete: 'ok',
163
+ failed: 'down',
164
+ streaming: 'warn',
155
165
  };
156
166
  /**
157
167
  * Badge colour (Tailwind family name) → tone. Ticket status/priority labels are org-customisable,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Wait for something the server is preparing elsewhere, asking again every so often (CYCL-43).
3
+ *
4
+ * Both AI flows the CLI exposes answer later than they reply: `assistant ask` gets a message that
5
+ * is still being written, `tickets ask` an AI request that is still pending. The waiting is the
6
+ * same in both, and it is the part that goes wrong in interesting ways — so it lives here once.
7
+ *
8
+ * Two guarantees the callers depend on: the pause grows between attempts (a slow answer must not
9
+ * turn into hundreds of requests) and the wait always ends (`timeoutMs`), because a command that
10
+ * hangs forever on a terminal is worse than one that gives up and says where the answer will be.
11
+ *
12
+ * `timeoutMs` bounds the pauses and the number of attempts, not a single request: one `fetch` that
13
+ * never returns still hangs, because `CliApi` sends without an abort signal — a gap that belongs to
14
+ * the HTTP layer and would have to be closed there, for every command at once.
15
+ */
16
+ export interface PollOptions<T> {
17
+ /** What the caller already holds (the POST response): already settled → no request at all. */
18
+ initial?: T;
19
+ /** Hard ceiling on the whole wait. */
20
+ timeoutMs: number;
21
+ /** First pause; each following one is `factor` times longer, capped at `maxDelayMs`. */
22
+ firstDelayMs?: number;
23
+ maxDelayMs?: number;
24
+ factor?: number;
25
+ /** Body of the timeout error: says how to get the answer that is still coming. */
26
+ timeoutMessage: string;
27
+ /** Injectable for tests; default sleeps for real. */
28
+ sleep?: (ms: number) => Promise<void>;
29
+ /** Injectable for tests; default is the wall clock. */
30
+ now?: () => number;
31
+ }
32
+ /**
33
+ * Default `--timeout` of the commands that wait for an AI answer. Three minutes is long enough for
34
+ * an assistant that reads several things before replying, and short enough that a terminal left
35
+ * open never looks frozen.
36
+ */
37
+ export declare const DEFAULT_TIMEOUT_SECONDS = 180;
38
+ /**
39
+ * Ask `fetch` again until `settled` says the value is final, then return it.
40
+ *
41
+ * Throws an `ApiRequestError` (status 408, `C408-ASK-001`) carrying `timeoutMessage` when the
42
+ * budget runs out — the shape `BaseCommand.catch` already prints as `code: message` on stderr and
43
+ * as an `{error:{code,message}}` envelope in `--json`, so a script can tell "still working" from
44
+ * "the server refused".
45
+ */
46
+ export declare function pollUntil<T>(fetch: () => Promise<T>, settled: (value: T) => boolean, opts: PollOptions<T>): Promise<T>;
47
+ /**
48
+ * Message for a `--timeout` that cannot wait for anything, or undefined when it is usable.
49
+ * Same shape as `paginationError`: the command line is refused before any call goes out.
50
+ */
51
+ export declare function timeoutError(seconds: number): string | undefined;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TIMEOUT_SECONDS = void 0;
4
+ exports.pollUntil = pollUntil;
5
+ exports.timeoutError = timeoutError;
6
+ const promises_1 = require("node:timers/promises");
7
+ const api_1 = require("./api");
8
+ const error_codes_1 = require("../errors/error-codes");
9
+ const FIRST_DELAY_MS = 1000;
10
+ const MAX_DELAY_MS = 5000;
11
+ const FACTOR = 1.5;
12
+ /**
13
+ * Default `--timeout` of the commands that wait for an AI answer. Three minutes is long enough for
14
+ * an assistant that reads several things before replying, and short enough that a terminal left
15
+ * open never looks frozen.
16
+ */
17
+ exports.DEFAULT_TIMEOUT_SECONDS = 180;
18
+ /**
19
+ * Ask `fetch` again until `settled` says the value is final, then return it.
20
+ *
21
+ * Throws an `ApiRequestError` (status 408, `C408-ASK-001`) carrying `timeoutMessage` when the
22
+ * budget runs out — the shape `BaseCommand.catch` already prints as `code: message` on stderr and
23
+ * as an `{error:{code,message}}` envelope in `--json`, so a script can tell "still working" from
24
+ * "the server refused".
25
+ */
26
+ async function pollUntil(fetch, settled, opts) {
27
+ if (opts.initial !== undefined && settled(opts.initial))
28
+ return opts.initial;
29
+ const now = opts.now ?? Date.now;
30
+ const sleep = opts.sleep ?? ((ms) => (0, promises_1.setTimeout)(ms));
31
+ const maxDelay = opts.maxDelayMs ?? MAX_DELAY_MS;
32
+ const factor = opts.factor ?? FACTOR;
33
+ const deadline = now() + opts.timeoutMs;
34
+ let wait = Math.min(opts.firstDelayMs ?? FIRST_DELAY_MS, maxDelay);
35
+ for (;;) {
36
+ const remaining = deadline - now();
37
+ if (remaining <= 0) {
38
+ throw new api_1.ApiRequestError(408, error_codes_1.ErrorCodes.Ask.timeout, opts.timeoutMessage);
39
+ }
40
+ // Never sleep past the deadline: the last attempt lands exactly on it, so the full budget is
41
+ // used instead of being cut short by a pause that would have overshot it.
42
+ // eslint-disable-next-line no-await-in-loop
43
+ await sleep(Math.min(wait, remaining));
44
+ // eslint-disable-next-line no-await-in-loop
45
+ const value = await fetch();
46
+ if (settled(value))
47
+ return value;
48
+ wait = Math.min(Math.round(wait * factor), maxDelay);
49
+ }
50
+ }
51
+ /**
52
+ * Message for a `--timeout` that cannot wait for anything, or undefined when it is usable.
53
+ * Same shape as `paginationError`: the command line is refused before any call goes out.
54
+ */
55
+ function timeoutError(seconds) {
56
+ if (seconds < 1)
57
+ return `Invalid --timeout ${seconds}: wait at least 1 second.`;
58
+ return undefined;
59
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Reading and correcting a service account without recreating it (CYCL-46).
3
+ *
4
+ * One rule runs through everything here: on the secret boundary of a service account, an EMPTY list
5
+ * of environment codes does not mean "no access", it means "every environment". So the empty list is
6
+ * never a neutral value to fall into by accident — it is a decision, and it has to be printed with
7
+ * words that cannot be misread.
8
+ */
9
+ /** The boundary in words: the codes, or the fact that there is no boundary at all. */
10
+ export declare function environmentsLine(codes: unknown): string;
11
+ /**
12
+ * Environment codes the way the server stores them: trimmed, lowercase, each one once, blanks
13
+ * dropped — the same normalisation `Connections::SetSecretAccess#sanitize` applies before saving.
14
+ * Sending them already in that shape is what makes "what I asked for" and "what came back"
15
+ * comparable at all: otherwise ` Staging ` would look dropped every single time.
16
+ */
17
+ export declare function normalizeEnvironmentCodes(values: unknown[]): string[];
18
+ /**
19
+ * The fields of a service account worth printing, as an explicit allow-list — the same discipline as
20
+ * `changeRequestSummary`: `renderRecord` prints every scalar it is handed, so handing it the raw
21
+ * payload would print whatever the server adds next, up to and including a field carrying a token.
22
+ * `secret_environment_codes` is left out on purpose: it is an array (renderRecord would swallow it)
23
+ * and it deserves the sentence `environmentsLine` builds, not a bare key.
24
+ */
25
+ export declare function accountSummary(account: Record<string, unknown>): Record<string, unknown>;
26
+ /**
27
+ * A copy of a token row without the secret, on both the human and the `--json` path.
28
+ *
29
+ * `UserApiTokenSerializer` never ships it — the secret exists only in the response of `tokens
30
+ * create`, reveal-once, and in the database there is just a prefix. This strips it anyway, for the
31
+ * same reason `withoutValue` does it on the vault queue: the CLI decides what leaves the terminal,
32
+ * so a payload that one day carried the secret (a serializer edit, a proxy, a hand-rolled backend)
33
+ * cannot turn a plain listing into a second reveal.
34
+ */
35
+ export declare function withoutSecret<T>(row: T): T;
36
+ /** `active` while the token works, `revoked` once it has been withdrawn — what the reader is after. */
37
+ export declare function tokenStatus(token: Record<string, unknown>): string;
38
+ /**
39
+ * The requested codes an organization does not declare — checked BEFORE writing.
40
+ *
41
+ * `SetSecretAccess` intersects what it receives with the codes the organization really has and
42
+ * saves what is left, answering 200 either way. On a typo that intersection is empty, and an empty
43
+ * allow-list is not "no environments", it is EVERY environment: `--secret-environment stagin` would
44
+ * hand the account the production secrets it was being restricted away from. Catching it here means
45
+ * the mistake costs an error message instead of an open door.
46
+ *
47
+ * `known` empty is treated as "could not read them" by the caller, not as "nothing is valid": a
48
+ * lookup that came back empty (no permission, a serializer without `code`) must not block a command
49
+ * the caller is entitled to run.
50
+ */
51
+ export declare function unknownEnvironmentCodes(requested: string[], known: string[]): string[];
52
+ /**
53
+ * Refuse an environment boundary the server did not actually put in place.
54
+ *
55
+ * The pre-flight check above catches the typo before it is written, but it cannot be the only
56
+ * guard: reading the environments needs `environments.view`, a permission managing members does not
57
+ * imply, and an environment can be removed between the two calls. So the request and the response
58
+ * are compared here too, and any difference — a code dropped OR a code appearing that nobody asked
59
+ * for, which would mean a wider boundary than requested — is an error, not a footnote.
60
+ *
61
+ * Whatever the server did save stays saved (the boundary is one call away from being fixed, and
62
+ * rolling it back blindly could lock out an agent mid-run), but the command says so and exits
63
+ * non-zero — the same shape as `assertNothingDropped` on a partial bulk triage.
64
+ */
65
+ export declare function assertBoundaryApplied(requested: string[], applied: string[]): void;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.environmentsLine = environmentsLine;
4
+ exports.normalizeEnvironmentCodes = normalizeEnvironmentCodes;
5
+ exports.accountSummary = accountSummary;
6
+ exports.withoutSecret = withoutSecret;
7
+ exports.tokenStatus = tokenStatus;
8
+ exports.unknownEnvironmentCodes = unknownEnvironmentCodes;
9
+ exports.assertBoundaryApplied = assertBoundaryApplied;
10
+ const api_1 = require("./api");
11
+ const error_codes_1 = require("../errors/error-codes");
12
+ /**
13
+ * Reading and correcting a service account without recreating it (CYCL-46).
14
+ *
15
+ * One rule runs through everything here: on the secret boundary of a service account, an EMPTY list
16
+ * of environment codes does not mean "no access", it means "every environment". So the empty list is
17
+ * never a neutral value to fall into by accident — it is a decision, and it has to be printed with
18
+ * words that cannot be misread.
19
+ */
20
+ /** The boundary in words: the codes, or the fact that there is no boundary at all. */
21
+ function environmentsLine(codes) {
22
+ const list = normalizeEnvironmentCodes(Array.isArray(codes) ? codes : []);
23
+ return `secret environments: ${list.length > 0 ? list.join(', ') : 'all (no restriction)'}`;
24
+ }
25
+ /**
26
+ * Environment codes the way the server stores them: trimmed, lowercase, each one once, blanks
27
+ * dropped — the same normalisation `Connections::SetSecretAccess#sanitize` applies before saving.
28
+ * Sending them already in that shape is what makes "what I asked for" and "what came back"
29
+ * comparable at all: otherwise ` Staging ` would look dropped every single time.
30
+ */
31
+ function normalizeEnvironmentCodes(values) {
32
+ const seen = new Set();
33
+ const codes = [];
34
+ for (const value of values) {
35
+ const code = String(value ?? '').trim().toLowerCase();
36
+ if (code === '' || seen.has(code))
37
+ continue;
38
+ seen.add(code);
39
+ codes.push(code);
40
+ }
41
+ return codes;
42
+ }
43
+ /**
44
+ * The fields of a service account worth printing, as an explicit allow-list — the same discipline as
45
+ * `changeRequestSummary`: `renderRecord` prints every scalar it is handed, so handing it the raw
46
+ * payload would print whatever the server adds next, up to and including a field carrying a token.
47
+ * `secret_environment_codes` is left out on purpose: it is an array (renderRecord would swallow it)
48
+ * and it deserves the sentence `environmentsLine` builds, not a bare key.
49
+ */
50
+ function accountSummary(account) {
51
+ const summary = {
52
+ id: account.id,
53
+ name: account.name,
54
+ handle: account.handle,
55
+ created_at: account.created_at,
56
+ active_tokens_count: account.active_tokens_count,
57
+ };
58
+ return Object.fromEntries(Object.entries(summary).filter(([, value]) => value !== undefined && value !== null));
59
+ }
60
+ /**
61
+ * A copy of a token row without the secret, on both the human and the `--json` path.
62
+ *
63
+ * `UserApiTokenSerializer` never ships it — the secret exists only in the response of `tokens
64
+ * create`, reveal-once, and in the database there is just a prefix. This strips it anyway, for the
65
+ * same reason `withoutValue` does it on the vault queue: the CLI decides what leaves the terminal,
66
+ * so a payload that one day carried the secret (a serializer edit, a proxy, a hand-rolled backend)
67
+ * cannot turn a plain listing into a second reveal.
68
+ */
69
+ function withoutSecret(row) {
70
+ if (row === null || typeof row !== 'object')
71
+ return row;
72
+ const { secret: _secret, ...rest } = row;
73
+ return rest;
74
+ }
75
+ /** `active` while the token works, `revoked` once it has been withdrawn — what the reader is after. */
76
+ function tokenStatus(token) {
77
+ return token.revoked_at ? 'revoked' : 'active';
78
+ }
79
+ /**
80
+ * The requested codes an organization does not declare — checked BEFORE writing.
81
+ *
82
+ * `SetSecretAccess` intersects what it receives with the codes the organization really has and
83
+ * saves what is left, answering 200 either way. On a typo that intersection is empty, and an empty
84
+ * allow-list is not "no environments", it is EVERY environment: `--secret-environment stagin` would
85
+ * hand the account the production secrets it was being restricted away from. Catching it here means
86
+ * the mistake costs an error message instead of an open door.
87
+ *
88
+ * `known` empty is treated as "could not read them" by the caller, not as "nothing is valid": a
89
+ * lookup that came back empty (no permission, a serializer without `code`) must not block a command
90
+ * the caller is entitled to run.
91
+ */
92
+ function unknownEnvironmentCodes(requested, known) {
93
+ const declared = new Set(normalizeEnvironmentCodes(known));
94
+ return requested.filter((code) => !declared.has(code));
95
+ }
96
+ /**
97
+ * Refuse an environment boundary the server did not actually put in place.
98
+ *
99
+ * The pre-flight check above catches the typo before it is written, but it cannot be the only
100
+ * guard: reading the environments needs `environments.view`, a permission managing members does not
101
+ * imply, and an environment can be removed between the two calls. So the request and the response
102
+ * are compared here too, and any difference — a code dropped OR a code appearing that nobody asked
103
+ * for, which would mean a wider boundary than requested — is an error, not a footnote.
104
+ *
105
+ * Whatever the server did save stays saved (the boundary is one call away from being fixed, and
106
+ * rolling it back blindly could lock out an agent mid-run), but the command says so and exits
107
+ * non-zero — the same shape as `assertNothingDropped` on a partial bulk triage.
108
+ */
109
+ function assertBoundaryApplied(requested, applied) {
110
+ const inForce = applied.length > 0 ? applied.join(', ') : 'all environments (no restriction)';
111
+ // --all-environments: the removal is the whole point, so a boundary still standing means the
112
+ // command did nothing while looking like it worked.
113
+ if (requested.length === 0) {
114
+ if (applied.length === 0)
115
+ return;
116
+ throw new api_1.ApiRequestError(500, error_codes_1.ErrorCodes.ServiceAccount.restrictionNotApplied, `The restriction was not removed: the account can still read secrets only in ${inForce}. ` +
117
+ 'Nothing else changed — run the command again, or check the account from the web vault.');
118
+ }
119
+ const kept = new Set(applied.map((code) => code.toLowerCase()));
120
+ const asked = new Set(requested);
121
+ const dropped = requested.filter((code) => !kept.has(code));
122
+ const extra = applied.filter((code) => !asked.has(code.toLowerCase()));
123
+ if (dropped.length === 0 && extra.length === 0)
124
+ return;
125
+ const what = dropped.length > 0
126
+ ? `Not applied: ${dropped.join(', ')} — the server kept a different boundary ` +
127
+ '(an environment removed in the meantime, or a code this organization does not have).'
128
+ : `Applied without being asked for: ${extra.join(', ')} — the boundary is wider than requested ` +
129
+ `(${requested.join(', ')}).`;
130
+ throw new api_1.ApiRequestError(404, error_codes_1.ErrorCodes.ServiceAccount.boundaryMismatch, `${what} In force now: ${inForce}. ` +
131
+ 'Run "environments list" to see the real codes, then set the boundary again.');
132
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lo stato del ciclo di chiarimenti, come lo racconta il SERVER (CYRA-628).
3
+ *
4
+ * Fino a qui quello stato viveva nel testo della discussione: un marcatore HTML che tre repository
5
+ * ri-parsavano per conto loro. Bastava che il sistema stesso scrivesse una riga qualsiasi — «resoconto
6
+ * aggiornato alla versione 2» — perché chi legge la scambiasse per una risposta e facesse ripartire un
7
+ * ticket a cui non aveva risposto nessuno. Intanto l'archivio continuava a dire «in attesa»: due parti
8
+ * dello stesso prodotto raccontavano cose diverse sullo stesso ticket, e decideva quella che sapeva meno.
9
+ *
10
+ * Il LIMITE di giri non compare qui e non compare nella risposta: il server espone i fatti — quanti
11
+ * giri, se è arrivata una risposta — e quando chiamare una persona resta policy di chi legge.
12
+ */
13
+ export declare function renderClarificationState(stato: Record<string, unknown>): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderClarificationState = renderClarificationState;
4
+ const output_1 = require("./output");
5
+ /**
6
+ * Lo stato del ciclo di chiarimenti, come lo racconta il SERVER (CYRA-628).
7
+ *
8
+ * Fino a qui quello stato viveva nel testo della discussione: un marcatore HTML che tre repository
9
+ * ri-parsavano per conto loro. Bastava che il sistema stesso scrivesse una riga qualsiasi — «resoconto
10
+ * aggiornato alla versione 2» — perché chi legge la scambiasse per una risposta e facesse ripartire un
11
+ * ticket a cui non aveva risposto nessuno. Intanto l'archivio continuava a dire «in attesa»: due parti
12
+ * dello stesso prodotto raccontavano cose diverse sullo stesso ticket, e decideva quella che sapeva meno.
13
+ *
14
+ * Il LIMITE di giri non compare qui e non compare nella risposta: il server espone i fatti — quanti
15
+ * giri, se è arrivata una risposta — e quando chiamare una persona resta policy di chi legge.
16
+ */
17
+ function renderClarificationState(stato) {
18
+ const rounds = Array.isArray(stato.rounds) ? stato.rounds : [];
19
+ const testa = `── ${(0, output_1.sanitize)(stato.state ?? '?')} · giri: ${(0, output_1.sanitize)(stato.cycles ?? 0)} · risposta: ${stato.has_reply ? 'sì' : 'no'}`;
20
+ if (rounds.length === 0)
21
+ return `${testa}\nNessuna domanda in corso.`;
22
+ const corpo = rounds.map((round) => {
23
+ const quando = round.answered_at ? `risposto ${(0, output_1.sanitize)(round.answered_at)}` : 'senza risposta';
24
+ return [
25
+ `\n· giro ${(0, output_1.sanitize)(round.cycle ?? '?')} — ${quando}`,
26
+ (0, output_1.sanitizeMultiline)(round.questions ?? ''),
27
+ round.response ? (0, output_1.sanitizeMultiline)(round.response) : '',
28
+ ].filter(Boolean).join('\n');
29
+ });
30
+ return [testa, ...corpo].join('\n');
31
+ }
@@ -0,0 +1,38 @@
1
+ /** Org-level collection of the secret changes waiting for a second approval (CYCL-40). */
2
+ export declare const CHANGE_REQUESTS_PATH = "/cli/v1/vault/change_requests";
3
+ /**
4
+ * A copy of a change-request row without the proposed value.
5
+ *
6
+ * `SecretChangeRequestSerializer` never ships `value` — it is encrypted at rest and a change
7
+ * request is only ever allowed to reveal the NAME and the intent. This strips it anyway, on both
8
+ * the human and the `--json` path: the CLI decides what leaves the terminal, so a payload that one
9
+ * day carried the secret (a serializer edit, a proxy, a hand-rolled backend) cannot leak it through
10
+ * a command whose whole point is reviewing secrets without seeing them.
11
+ */
12
+ export declare function withoutValue<T>(row: T): T;
13
+ /** One row of `vault requests list`: never the value, only what identifies the pending change. */
14
+ export declare function changeRequestRow(row: Record<string, unknown>): string[];
15
+ /** Column headers of `vault requests list`, in the order `changeRequestRow` fills them. */
16
+ export declare const CHANGE_REQUEST_HEADERS: string[];
17
+ /**
18
+ * Short human wording of what a request is about — `STRIPE_KEY (set) on ACME/production`. Empty when
19
+ * the payload names nothing, so the caller can fall back to a bare confirmation instead of printing
20
+ * a line full of question marks.
21
+ */
22
+ export declare function changeRequestLabel(row: Record<string, unknown>): string;
23
+ /**
24
+ * The fields of a decided request worth printing, as an explicit allow-list — the same discipline as
25
+ * `leaseSummary` in `tickets lease claim`: `renderRecord` prints every scalar it is handed, so
26
+ * handing it the raw payload would print whatever the server adds next. Absent/null fields are
27
+ * dropped rather than shown as `-`: an approval has no rejection reason, and a row of dashes reads
28
+ * like missing data instead of "not applicable".
29
+ */
30
+ export declare function changeRequestSummary(row: Record<string, unknown>): Record<string, unknown>;
31
+ /**
32
+ * Append the way out to a refused decision, keeping the server's own message and code intact — the
33
+ * pattern `fetchLookupRows` already uses on a 403 in `BaseCommand`. Anything else (a 404 on a
34
+ * request outside the visible projects, a plain "Permesso negato" from the RBAC gate, a network
35
+ * failure) passes through untouched: inventing an explanation for an error we have not identified
36
+ * would be worse than the bare code.
37
+ */
38
+ export declare function describeDecisionError(error: unknown): unknown;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHANGE_REQUEST_HEADERS = exports.CHANGE_REQUESTS_PATH = void 0;
4
+ exports.withoutValue = withoutValue;
5
+ exports.changeRequestRow = changeRequestRow;
6
+ exports.changeRequestLabel = changeRequestLabel;
7
+ exports.changeRequestSummary = changeRequestSummary;
8
+ exports.describeDecisionError = describeDecisionError;
9
+ const api_1 = require("./api");
10
+ /** Org-level collection of the secret changes waiting for a second approval (CYCL-40). */
11
+ exports.CHANGE_REQUESTS_PATH = '/cli/v1/vault/change_requests';
12
+ /**
13
+ * A copy of a change-request row without the proposed value.
14
+ *
15
+ * `SecretChangeRequestSerializer` never ships `value` — it is encrypted at rest and a change
16
+ * request is only ever allowed to reveal the NAME and the intent. This strips it anyway, on both
17
+ * the human and the `--json` path: the CLI decides what leaves the terminal, so a payload that one
18
+ * day carried the secret (a serializer edit, a proxy, a hand-rolled backend) cannot leak it through
19
+ * a command whose whole point is reviewing secrets without seeing them.
20
+ */
21
+ function withoutValue(row) {
22
+ if (row === null || typeof row !== 'object')
23
+ return row;
24
+ const { value: _value, ...rest } = row;
25
+ return rest;
26
+ }
27
+ /** `key` of the project the change belongs to (its name as a fallback), or '' when the payload is silent. */
28
+ function projectLabel(row) {
29
+ const project = (row.project ?? {});
30
+ return String(project.key ?? project.name ?? '');
31
+ }
32
+ /** `code` of the target environment (its label as a fallback), or '' when the payload is silent. */
33
+ function environmentLabel(row) {
34
+ const environment = (row.environment ?? {});
35
+ return String(environment.code ?? environment.label ?? '');
36
+ }
37
+ /**
38
+ * Who asked for the change: name, email as a fallback, '' when nobody is attached. The requester FK
39
+ * is nullified when the account is deleted (the audit outlives the person), so this is a real case,
40
+ * not a defensive flourish.
41
+ */
42
+ function requesterLabel(row) {
43
+ const requester = (row.requested_by ?? {});
44
+ return String(requester.name ?? requester.email ?? '');
45
+ }
46
+ /** One row of `vault requests list`: never the value, only what identifies the pending change. */
47
+ function changeRequestRow(row) {
48
+ return [
49
+ String(row.id ?? ''),
50
+ projectLabel(row),
51
+ environmentLabel(row),
52
+ String(row.action ?? ''),
53
+ String(row.name ?? ''),
54
+ requesterLabel(row),
55
+ String(row.created_at ?? ''),
56
+ ];
57
+ }
58
+ /** Column headers of `vault requests list`, in the order `changeRequestRow` fills them. */
59
+ exports.CHANGE_REQUEST_HEADERS = ['ID', 'PROJECT', 'ENVIRONMENT', 'ACTION', 'NAME', 'REQUESTED BY', 'ASKED AT'];
60
+ /**
61
+ * Short human wording of what a request is about — `STRIPE_KEY (set) on ACME/production`. Empty when
62
+ * the payload names nothing, so the caller can fall back to a bare confirmation instead of printing
63
+ * a line full of question marks.
64
+ */
65
+ function changeRequestLabel(row) {
66
+ const name = String(row.name ?? '').trim();
67
+ if (name === '')
68
+ return '';
69
+ const action = String(row.action ?? '').trim();
70
+ const where = [projectLabel(row), environmentLabel(row)].filter(Boolean).join('/');
71
+ return [name, action && `(${action})`, where && `on ${where}`].filter(Boolean).join(' ');
72
+ }
73
+ /**
74
+ * The fields of a decided request worth printing, as an explicit allow-list — the same discipline as
75
+ * `leaseSummary` in `tickets lease claim`: `renderRecord` prints every scalar it is handed, so
76
+ * handing it the raw payload would print whatever the server adds next. Absent/null fields are
77
+ * dropped rather than shown as `-`: an approval has no rejection reason, and a row of dashes reads
78
+ * like missing data instead of "not applicable".
79
+ */
80
+ function changeRequestSummary(row) {
81
+ const summary = {
82
+ id: row.id,
83
+ project: projectLabel(row) || undefined,
84
+ environment: environmentLabel(row) || undefined,
85
+ name: row.name,
86
+ action: row.action,
87
+ status: row.status,
88
+ requested_by: requesterLabel(row) || undefined,
89
+ requested_at: row.created_at,
90
+ decided_at: row.decided_at,
91
+ reason: row.reason,
92
+ };
93
+ return Object.fromEntries(Object.entries(summary).filter(([, value]) => value !== undefined && value !== null));
94
+ }
95
+ /**
96
+ * The four ways a decision is refused, each with the way out. The backend codes are stable and its
97
+ * messages are already clear about WHAT happened; what a refusal alone does not say is what to do
98
+ * next — and all four look identical from the outside ("it did not work"), while they call for
99
+ * opposite reactions: hand the request to a colleague, log in as yourself, refresh the queue, write
100
+ * a real reason.
101
+ */
102
+ const DECISION_HINTS = {
103
+ // Four-eyes separation: whoever decides is never whoever asked (Secrets::ChangeRequests::DecisionGuard).
104
+ 'R403-CHANGEREQUEST-001': 'The four-eyes rule holds here: this one has to be decided by someone else with secrets.manage on that project.',
105
+ // CYRA-640: the second approval is a person's, never a machine's — a service-account token
106
+ // authenticates like a personal one, so the refusal has to name the difference.
107
+ 'R403-CHANGEREQUEST-003': 'Only a person can decide a secret change request: a service account cannot, so run this with your own login.',
108
+ 'R409-CHANGEREQUEST-001': 'It was already decided (applied, rejected or withdrawn) — run "vault requests list" to see what is still waiting.',
109
+ 'R422-CHANGEREQUEST-001': 'Pass a --reason that says why: a rejection freezes the request, and whoever asked needs to know what to fix.',
110
+ };
111
+ /**
112
+ * Append the way out to a refused decision, keeping the server's own message and code intact — the
113
+ * pattern `fetchLookupRows` already uses on a 403 in `BaseCommand`. Anything else (a 404 on a
114
+ * request outside the visible projects, a plain "Permesso negato" from the RBAC gate, a network
115
+ * failure) passes through untouched: inventing an explanation for an error we have not identified
116
+ * would be worse than the bare code.
117
+ */
118
+ function describeDecisionError(error) {
119
+ if (!(error instanceof api_1.ApiRequestError))
120
+ return error;
121
+ const hint = DECISION_HINTS[error.code];
122
+ if (!hint)
123
+ return error;
124
+ return new api_1.ApiRequestError(error.status, error.code, `${error.message}. ${hint}`, error.details);
125
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * La board del carico di lavoro non-dev (fiere, materiale cartaceo, incontri, chiamate): una sola
3
+ * raccolta d'organizzazione, con radice di tenancy il TEAM e non il progetto — per questo l'endpoint
4
+ * non è annidato sotto `/projects/:id` come quasi tutto il resto della CLI.
5
+ */
6
+ export declare const WORKLOAD_ACTIONS_PATH = "/cli/v1/workload/actions";
7
+ /** Gli stati dell'enum del server, nell'ordine in cui un'attività li attraversa. */
8
+ export declare const WORKLOAD_STATUSES: string[];
9
+ /**
10
+ * I campi che `workload create` e `workload update` scrivono allo stesso modo. Il titolo resta
11
+ * fuori: nasce obbligatorio sulla creazione e facoltativo sulla modifica. Il team pure — è
12
+ * immutabile lato server (spostare un'attività cambierebbe chi la vede), quindi si indica solo
13
+ * quando l'attività nasce.
14
+ */
15
+ export declare const actionFieldFlags: {
16
+ description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ 'scheduled-at': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ 'due-at': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ ticket: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ };
22
+ type ActionFlags = Record<string, unknown>;
23
+ /** Popola `body` coi campi scalari presenti nei flag (chiavi snake_case attese dall'API CLI). */
24
+ export declare function applyActionBody(body: ActionFlags, flags: ActionFlags): void;
25
+ /**
26
+ * Vero quando il ticket indicato NON è finito sull'attività: il server sostituisce in silenzio con
27
+ * «nessun ticket» un id che chi scrive non può vedere (anti-BOLA), e senza dirlo l'attività
28
+ * risulterebbe collegata a chi l'ha appena salvata — mentre non lo è. Falso quando non c'era niente
29
+ * da collegare (flag assente, o stringa vuota che scollega apposta) o quando il legame c'è davvero.
30
+ *
31
+ * Sulla modifica la conseguenza è più pesante che sulla creazione — quel silenzio toglie anche il
32
+ * ticket che c'era prima — quindi l'avviso vero e proprio lo scrive ciascun comando.
33
+ */
34
+ export declare function ticketLinkMissing(reference: string | undefined, resolvedId: string | undefined, data: Record<string, unknown>): boolean;
35
+ /** La riga dei partecipanti: `renderRecord` stampa solo i campi scalari, e i nomi sono un elenco. */
36
+ export declare function participantsLine(data: Record<string, unknown>): string;
37
+ export {};
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.actionFieldFlags = exports.WORKLOAD_STATUSES = exports.WORKLOAD_ACTIONS_PATH = void 0;
4
+ exports.applyActionBody = applyActionBody;
5
+ exports.ticketLinkMissing = ticketLinkMissing;
6
+ exports.participantsLine = participantsLine;
7
+ const core_1 = require("@oclif/core");
8
+ const output_1 = require("./output");
9
+ /**
10
+ * La board del carico di lavoro non-dev (fiere, materiale cartaceo, incontri, chiamate): una sola
11
+ * raccolta d'organizzazione, con radice di tenancy il TEAM e non il progetto — per questo l'endpoint
12
+ * non è annidato sotto `/projects/:id` come quasi tutto il resto della CLI.
13
+ */
14
+ exports.WORKLOAD_ACTIONS_PATH = '/cli/v1/workload/actions';
15
+ /** Gli stati dell'enum del server, nell'ordine in cui un'attività li attraversa. */
16
+ exports.WORKLOAD_STATUSES = ['planned', 'in_progress', 'done', 'cancelled'];
17
+ /**
18
+ * I campi che `workload create` e `workload update` scrivono allo stesso modo. Il titolo resta
19
+ * fuori: nasce obbligatorio sulla creazione e facoltativo sulla modifica. Il team pure — è
20
+ * immutabile lato server (spostare un'attività cambierebbe chi la vede), quindi si indica solo
21
+ * quando l'attività nasce.
22
+ */
23
+ exports.actionFieldFlags = {
24
+ description: core_1.Flags.string({ description: 'What the activity is about (free text)' }),
25
+ status: core_1.Flags.string({
26
+ description: `Status (${exports.WORKLOAD_STATUSES.join(', ')})`,
27
+ options: exports.WORKLOAD_STATUSES,
28
+ }),
29
+ 'scheduled-at': core_1.Flags.string({ description: 'When it takes place (ISO 8601)' }),
30
+ 'due-at': core_1.Flags.string({ description: 'Deadline (ISO 8601)' }),
31
+ ticket: core_1.Flags.string({
32
+ description: 'Link an existing ticket by code (e.g. DRFL-3) or id — pass an empty string to unlink',
33
+ }),
34
+ };
35
+ /** Popola `body` coi campi scalari presenti nei flag (chiavi snake_case attese dall'API CLI). */
36
+ function applyActionBody(body, flags) {
37
+ if (flags.description !== undefined)
38
+ body.description = flags.description;
39
+ if (flags.status !== undefined)
40
+ body.status = flags.status;
41
+ if (flags['scheduled-at'] !== undefined)
42
+ body.scheduled_at = flags['scheduled-at'];
43
+ if (flags['due-at'] !== undefined)
44
+ body.due_at = flags['due-at'];
45
+ }
46
+ /**
47
+ * Vero quando il ticket indicato NON è finito sull'attività: il server sostituisce in silenzio con
48
+ * «nessun ticket» un id che chi scrive non può vedere (anti-BOLA), e senza dirlo l'attività
49
+ * risulterebbe collegata a chi l'ha appena salvata — mentre non lo è. Falso quando non c'era niente
50
+ * da collegare (flag assente, o stringa vuota che scollega apposta) o quando il legame c'è davvero.
51
+ *
52
+ * Sulla modifica la conseguenza è più pesante che sulla creazione — quel silenzio toglie anche il
53
+ * ticket che c'era prima — quindi l'avviso vero e proprio lo scrive ciascun comando.
54
+ */
55
+ function ticketLinkMissing(reference, resolvedId, data) {
56
+ if (!reference || !resolvedId)
57
+ return false;
58
+ return data.ticket_id !== resolvedId;
59
+ }
60
+ /** La riga dei partecipanti: `renderRecord` stampa solo i campi scalari, e i nomi sono un elenco. */
61
+ function participantsLine(data) {
62
+ const names = (Array.isArray(data.participants) ? data.participants : []).map((name) => (0, output_1.sanitize)(name));
63
+ return `participants: ${names.length > 0 ? names.join(', ') : '-'}`;
64
+ }