@bussolabs/closeyourit-cli 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +39 -5
  2. package/dist/base.d.ts +22 -0
  3. package/dist/base.js +46 -0
  4. package/dist/commands/alerts/notifications/read-all.js +3 -0
  5. package/dist/commands/alerts/preferences/show.js +2 -0
  6. package/dist/commands/ideas/convert.d.ts +2 -2
  7. package/dist/commands/ideas/convert.js +25 -8
  8. package/dist/commands/kb/book/add-page.d.ts +18 -0
  9. package/dist/commands/kb/book/add-page.js +55 -0
  10. package/dist/commands/kb/book/list.d.ts +12 -0
  11. package/dist/commands/kb/book/list.js +33 -0
  12. package/dist/commands/kb/book/show.d.ts +9 -0
  13. package/dist/commands/kb/book/show.js +22 -0
  14. package/dist/commands/kb/create.js +29 -6
  15. package/dist/commands/kb/publish.d.ts +15 -0
  16. package/dist/commands/kb/publish.js +106 -0
  17. package/dist/commands/kb/update.js +26 -5
  18. package/dist/commands/logout.js +3 -0
  19. package/dist/commands/org/show.js +2 -0
  20. package/dist/commands/personal/import.js +2 -2
  21. package/dist/commands/personal/run.js +1 -1
  22. package/dist/commands/run.js +1 -1
  23. package/dist/commands/secrets/import.js +2 -2
  24. package/dist/commands/seo/ignore.d.ts +12 -0
  25. package/dist/commands/seo/ignore.js +27 -0
  26. package/dist/commands/seo/list.d.ts +13 -0
  27. package/dist/commands/seo/list.js +52 -0
  28. package/dist/commands/seo/pages.d.ts +11 -0
  29. package/dist/commands/seo/pages.js +44 -0
  30. package/dist/commands/seo/promote.d.ts +9 -0
  31. package/dist/commands/seo/promote.js +22 -0
  32. package/dist/commands/seo/reopen.d.ts +9 -0
  33. package/dist/commands/seo/reopen.js +21 -0
  34. package/dist/commands/seo/rescan.d.ts +9 -0
  35. package/dist/commands/seo/rescan.js +21 -0
  36. package/dist/commands/seo/show.d.ts +9 -0
  37. package/dist/commands/seo/show.js +35 -0
  38. package/dist/commands/seo-sites/create.d.ts +14 -0
  39. package/dist/commands/seo-sites/create.js +43 -0
  40. package/dist/commands/seo-sites/delete.d.ts +9 -0
  41. package/dist/commands/seo-sites/delete.js +21 -0
  42. package/dist/commands/seo-sites/list.d.ts +10 -0
  43. package/dist/commands/seo-sites/list.js +38 -0
  44. package/dist/commands/seo-sites/update.d.ts +17 -0
  45. package/dist/commands/seo-sites/update.js +45 -0
  46. package/dist/commands/tickets/comment.d.ts +14 -3
  47. package/dist/commands/tickets/comment.js +29 -1
  48. package/dist/commands/tickets/create.d.ts +6 -6
  49. package/dist/commands/tickets/create.js +21 -2
  50. package/dist/commands/tickets/report-show.d.ts +20 -0
  51. package/dist/commands/tickets/report-show.js +38 -0
  52. package/dist/commands/tickets/report-versions.d.ts +19 -0
  53. package/dist/commands/tickets/report-versions.js +40 -0
  54. package/dist/commands/tickets/report.d.ts +24 -0
  55. package/dist/commands/tickets/report.js +69 -0
  56. package/dist/commands/tickets/status.d.ts +1 -1
  57. package/dist/commands/tickets/status.js +14 -4
  58. package/dist/commands/tickets/update.d.ts +25 -6
  59. package/dist/commands/tickets/update.js +144 -9
  60. package/dist/commands/whoami.js +3 -0
  61. package/dist/errors/error-codes.d.ts +12 -0
  62. package/dist/errors/error-codes.js +19 -0
  63. package/dist/errors/input-error.d.ts +16 -0
  64. package/dist/errors/input-error.js +42 -0
  65. package/dist/lib/alert-events.js +1 -0
  66. package/dist/lib/knowledge.d.ts +24 -0
  67. package/dist/lib/knowledge.js +68 -0
  68. package/dist/lib/limits.d.ts +59 -0
  69. package/dist/lib/limits.js +92 -0
  70. package/dist/lib/stdin.d.ts +1 -2
  71. package/dist/lib/stdin.js +1 -2
  72. package/dist/lib/subprocess.d.ts +1 -1
  73. package/dist/lib/subprocess.js +1 -1
  74. package/dist/lib/ticket-body.d.ts +22 -6
  75. package/dist/lib/ticket-body.js +71 -9
  76. package/dist/lib/ticket-lookup.d.ts +53 -0
  77. package/dist/lib/ticket-lookup.js +78 -0
  78. package/dist/lib/ticket-report.d.ts +7 -0
  79. package/dist/lib/ticket-report.js +14 -0
  80. package/oclif.manifest.json +3931 -2876
  81. package/opencli.json +860 -113
  82. package/package.json +4 -1
package/README.md CHANGED
@@ -85,6 +85,16 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
85
85
  | `vulnerabilities promote <id>` | Open a ticket from it — automatic promotion only fires on high/critical. |
86
86
  | `vulnerabilities rescan --project <id\|key>` | Rescan now, without waiting for the nightly run. |
87
87
  | `vulnerabilities runtimes [--project <id\|key>] [--page]` | Support state of the declared runtimes (Ruby, Node, Flutter…). |
88
+ | `seo list [--project <id\|key>] [--severity] [--status] [--area] [--page]` | List the SEO findings of your sites, worst first. |
89
+ | `seo show <id>` | Show one, with the evidence behind it. |
90
+ | `seo pages [--project <id\|key>] [--url] [--page]` | The pages the check visited: status, title, h1, words. |
91
+ | `seo ignore <id> [--note]` / `reopen <id>` | Accept living with a finding, or change your mind (requires seo.triage). |
92
+ | `seo promote <id>` | Open a ticket from it, evidence included. |
93
+ | `seo rescan --site <id>` | Check a site now, without waiting for the scheduled run (requires seo.manage). |
94
+ | `seo-sites list [--project <id\|key>] [--page]` | The sites under SEO check, with cadence and open findings. |
95
+ | `seo-sites create --project <id\|key> --url <url> --environment <id> [--frequency] [--max-pages] [--no-sitemap]` | Declare a site to watch; the first check starts right away. |
96
+ | `seo-sites update <id> [--url] [--frequency] [--max-pages] [--sitemap\|--no-sitemap] [--enable\|--disable]` | Change how a site is checked. |
97
+ | `seo-sites delete <id>` | Stop watching a site and remove its pages and findings. |
88
98
  | `tokens list --project <id\|key>` | List ingest tokens. |
89
99
  | `tokens create --project <id\|key> --name <name> --environment-id <id>` | Create a token (secret shown once). |
90
100
  | `tokens provision --project <id\|key> --name <name> --environment-id <code\|id> --to-project <id\|key> --to-environment-id <code\|id> --secret-name <NAME>` | Generate and deliver a token without revealing its value. GitHub sync is enabled by default; use `--no-sync-github` for vault-only delivery. |
@@ -99,9 +109,12 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
99
109
  | `secrets assets shared-list\|shared-upload\|delegate\|undelegate …` | Manage one organization-level ciphertext and delegate it to authorized projects. |
100
110
  | `tickets list --project <id\|key> [--page]` | List tickets. |
101
111
  | `tickets show <id> --project <id\|key>` | Show one ticket. |
102
- | `tickets create --project <id\|key> --title <t> [--kind bug\|story\|task\|epic] [--parent-id] [--step-*] [--description] [--status-id] [--priority-id]` | Create a ticket. |
103
- | `tickets statuses` | List the org ticket statuses (id, code, label) — the id feeds `create --status-id`. |
104
- | `tickets priorities` | List the org ticket priorities (id, code, label) — the id feeds `create --priority-id`. |
112
+ | `tickets create --project <id\|key> --title <t> [--kind bug\|story\|task\|epic] [--parent-id] [--step-*] [--description] [--status <code\|label\|id>] [--priority <code\|label\|id>]` | Create a ticket. `--status` defaults to `open` and `--priority` to `medium`, and both take a code or a label (`in_progress`, `"In Progress"`), so no id lookup is needed. Length caps: title 255, description 4000, technical analysis 1500 characters — every field over the cap is listed at once, before the call. |
113
+ | `tickets statuses` | List the org ticket statuses (id, code, label) — any of the three works as `--status`. |
114
+ | `tickets priorities` | List the org ticket priorities (id, code, label) — any of the three works as `--priority`. |
115
+ | `tickets report <id\|code> --project <id\|key> (--body <text> \| --body-file <path>)` | Write the work report — the long text a comment cannot hold (max 20000 characters). Each write adds a version, it never overwrites the previous one. |
116
+ | `tickets report-show <id\|code> --project <id\|key> [--version <n>]` | Show the current work report, or a past version. |
117
+ | `tickets report-versions <id\|code> --project <id\|key> [--page]` | List the versions of the work report (newest first). |
105
118
  | `tickets reject <id> --project <id\|key> --reason <text>` | Reject the review: back to work, the reason becomes a comment. |
106
119
  | `tickets approve <id> --project <id\|key>` | Approve the review: move to the first done status. |
107
120
  | `tickets links <id> --project <id\|key>` | List the ticket-to-ticket links (duplicate/related) involving a ticket. |
@@ -128,11 +141,15 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
128
141
  | `kb search <query…> [--project <id\|key>] [--kind] [--per] [--page] [--with-related]` | Search knowledge pages (semantic when available, title ILIKE fallback). |
129
142
  | `kb show <id> [--related] [--question <q>]` | Show one knowledge page: metadata header, raw markdown body, then the technical section. |
130
143
  | `kb related <id> [--question <q>] [--links-only]` | Pages to read next: linked with `[[wiki links]]` plus close matches by meaning. |
131
- | `kb create --project <id\|key> --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>) [--tech-spec <md> \| --tech-spec-file <path>] [--in-review] [--review-note <line>]` | Create a knowledge page (optional technical section, kept separate from the body). With `--in-review` the page waits for a human and stays out of search, answers and related panels. |
144
+ | `kb create --project <id\|key> --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>) [--tech-spec <md> \| --tech-spec-file <path>] [--in-review] [--review-note <line>]` | Create a knowledge page (optional technical section, kept separate from the body). Length caps: title 255, body 4000, technical section 1500, review note 240 characters — every field over the cap is listed at once, before the call. With `--in-review` the page waits for a human and stays out of search, answers and related panels. |
145
+ | `kb publish --project <id\|key> --publication-key <key> --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>)` | Create or update a knowledge page in one atomic call, keyed by a stable `--publication-key` (1-255 URL-safe characters). Safe to repeat: the same key always lands on the same page, and the output says whether it was `created` or `updated`. Exactly one body source is required. Omitting `--kind` keeps the kind the page already has (a new page is a `note`). Same title/body caps as `kb create`. |
132
146
  | `kb consolidate <id> --path <doc-path>` | Mark an accepted page as written to the versioned docs, recording where the document lives. |
133
- | `kb update <id> [--title] [--kind] [--body \| --body-file] [--tech-spec \| --tech-spec-file]` | Update a page (unpassed fields keep their current value). |
147
+ | `kb update <id> [--title] [--kind] [--body \| --body-file] [--tech-spec \| --tech-spec-file]` | Update a page (unpassed fields keep their current value). Same length caps as `kb create`; text already over the cap is refused only if it grows. |
134
148
  | `kb delete <id> --confirm` | Delete a knowledge page (irreversible). |
135
149
  | `kb ask <question…>` | Ask a question; the AI answers from the knowledge base and cites the pages it used. |
150
+ | `kb book list [--project <id\|key>] [--q <text>] [--per] [--page]` | List knowledge books (ordered collections of pages). Cross-project: omit `--project` for every visible project. |
151
+ | `kb book show <id>` | Show a book: metadata, then the summary of the pages it collects (`#` is the 0-based position). |
152
+ | `kb book add-page <id> --page <page-id> [--position <n>]` | Add a page to a book's summary. `--page` takes a page **id** (alias: `--page-id`), not a page number; `--position` is 0-based (`0` = top, omitted = last), like the position the summary shows back. Idempotent: repeating it never lists the page twice. |
136
153
  | `servers list [--status] [--page]` | List the monitored servers (fleet snapshot: status, cpu/mem/disk, last seen). |
137
154
  | `servers show <id>` | Show a monitored server (latest snapshot + machine details). |
138
155
  | `servers rename <id> --name <name>` | Rename a server (display name only). |
@@ -219,6 +236,23 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
219
236
  Every command accepts `--json` for machine-readable output and `--help` for usage details.
220
237
  `--project` accepts either a project UUID or its key (matched case-insensitively).
221
238
 
239
+ `kb publish` needs a backend exposing the atomic Knowledge publication route; an older one fails with
240
+ `C404-KNW-001` and says to upgrade. An unknown project key fails locally with `C404-PRJ-001`; the
241
+ backend `R404-KNOWLEDGE-001` is reserved for a project UUID that is missing or not visible to the
242
+ caller. `R403-KNOWLEDGE-003` means the key already belongs to a page you cannot manage, and
243
+ `R409-KNOWLEDGE-001` that two untagged pages share the title the key would adopt.
244
+
245
+ When a command fails in `--json` mode it exits non-zero and prints an error envelope on stdout —
246
+ never a silent success:
247
+
248
+ ```json
249
+ { "error": { "code": "C400-INP-001", "message": "Missing required flag project" } }
250
+ ```
251
+
252
+ `C400-INP-001` means the command line itself needs fixing (missing flag or argument, unknown flag,
253
+ missing `--confirm`) and `message` says what; `C500-SYS-001` is reserved for unexpected failures.
254
+ Codes starting with `R`/`G` come straight from the backend.
255
+
222
256
  ## Configuration
223
257
 
224
258
  The CLI stores its state in:
package/dist/base.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command, type Interfaces } from '@oclif/core';
2
2
  import { type CliConfig } from './lib/config';
3
3
  import { CliApi } from './lib/api';
4
+ import { type TicketLookup } from './lib/ticket-lookup';
4
5
  export declare const UUID_RE: RegExp;
5
6
  /** Shared `--project <id|key>` flag (required). */
6
7
  export declare const projectFlag: {
@@ -14,6 +15,17 @@ export declare const environmentFlag: {
14
15
  export declare const pageFlag: {
15
16
  page: Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
16
17
  };
18
+ /**
19
+ * Message for a `--page`/`--per` outside the sensible range, or undefined when both are fine.
20
+ *
21
+ * The backend silently clamps them (`Pagination`): `--per 0` quietly returns the default page size
22
+ * and `--page 0` returns page 1, so a script that computed the wrong number gets plausible data
23
+ * back instead of an error. Say it before the call, and exit 2 like any other rejected command line.
24
+ */
25
+ export declare function paginationError(opts: {
26
+ page?: number;
27
+ per?: number;
28
+ }): string | undefined;
17
29
  export declare abstract class BaseCommand extends Command {
18
30
  static enableJsonFlag: boolean;
19
31
  protected cfg: CliConfig;
@@ -33,6 +45,16 @@ export declare abstract class BaseCommand extends Command {
33
45
  protected resolveTeamId(value: string): Promise<string>;
34
46
  /** Resolve a member reference (UUID account id passes through; otherwise matched by email). */
35
47
  protected resolveMemberId(value: string): Promise<string>;
48
+ /**
49
+ * Resolve a ticket status/priority reference: a UUID passes through, anything else is matched
50
+ * against the organization lookup by code first, then by label (case-insensitive).
51
+ *
52
+ * `undefined` means the flag was not passed, so the lookup's own default applies (`open`,
53
+ * `medium`): the server demands both fields on a new ticket while the help gave them as optional,
54
+ * and getting through used to take two id hunts (`tickets statuses`, `tickets priorities`) and a
55
+ * pair of UUIDs pasted by hand (CYCL-34).
56
+ */
57
+ protected resolveTicketLookupId(lookup: TicketLookup, value?: string): Promise<string>;
36
58
  /** Resolve each value of a list to an id, preserving order. */
37
59
  protected resolveAll(values: string[], resolve: (value: string) => Promise<string>): Promise<string[]>;
38
60
  /** Resolve an `{id, name}` reference by name (UUID passes through). */
package/dist/base.js CHANGED
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseCommand = exports.pageFlag = exports.environmentFlag = exports.projectFlag = exports.UUID_RE = void 0;
4
+ exports.paginationError = paginationError;
4
5
  const core_1 = require("@oclif/core");
5
6
  const config_1 = require("./lib/config");
6
7
  const api_1 = require("./lib/api");
7
8
  const output_1 = require("./lib/output");
8
9
  const error_codes_1 = require("./errors/error-codes");
10
+ const input_error_1 = require("./errors/input-error");
11
+ const ticket_lookup_1 = require("./lib/ticket-lookup");
9
12
  exports.UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
13
  /** Shared `--project <id|key>` flag (required). */
11
14
  exports.projectFlag = {
@@ -19,6 +22,20 @@ exports.environmentFlag = {
19
22
  exports.pageFlag = {
20
23
  page: core_1.Flags.integer({ description: 'Page number', default: 1 }),
21
24
  };
25
+ /**
26
+ * Message for a `--page`/`--per` outside the sensible range, or undefined when both are fine.
27
+ *
28
+ * The backend silently clamps them (`Pagination`): `--per 0` quietly returns the default page size
29
+ * and `--page 0` returns page 1, so a script that computed the wrong number gets plausible data
30
+ * back instead of an error. Say it before the call, and exit 2 like any other rejected command line.
31
+ */
32
+ function paginationError(opts) {
33
+ if (opts.page !== undefined && opts.page < 1)
34
+ return `Invalid --page ${opts.page}: the first page is 1.`;
35
+ if (opts.per !== undefined && opts.per < 1)
36
+ return `Invalid --per ${opts.per}: ask for at least 1 row.`;
37
+ return undefined;
38
+ }
22
39
  class BaseCommand extends core_1.Command {
23
40
  static enableJsonFlag = true;
24
41
  cfg;
@@ -72,6 +89,25 @@ class BaseCommand extends core_1.Command {
72
89
  }
73
90
  return match.account_id;
74
91
  }
92
+ /**
93
+ * Resolve a ticket status/priority reference: a UUID passes through, anything else is matched
94
+ * against the organization lookup by code first, then by label (case-insensitive).
95
+ *
96
+ * `undefined` means the flag was not passed, so the lookup's own default applies (`open`,
97
+ * `medium`): the server demands both fields on a new ticket while the help gave them as optional,
98
+ * and getting through used to take two id hunts (`tickets statuses`, `tickets priorities`) and a
99
+ * pair of UUIDs pasted by hand (CYCL-34).
100
+ */
101
+ async resolveTicketLookupId(lookup, value) {
102
+ const wanted = value ?? lookup.fallback;
103
+ if (exports.UUID_RE.test(wanted))
104
+ return wanted;
105
+ const rows = (await this.api.get(lookup.path)).data;
106
+ const matches = (0, ticket_lookup_1.matchTicketLookup)(rows, wanted);
107
+ if (matches.length === 1)
108
+ return matches[0].id;
109
+ throw new api_1.ApiRequestError(404, lookup.errorCode, (0, ticket_lookup_1.ticketLookupErrorMessage)(lookup, wanted, rows, { matched: matches.length, isDefault: value === undefined }));
110
+ }
75
111
  /** Resolve each value of a list to an id, preserving order. */
76
112
  resolveAll(values, resolve) {
77
113
  return Promise.all(values.map((value) => resolve(value)));
@@ -102,6 +138,16 @@ class BaseCommand extends core_1.Command {
102
138
  }
103
139
  return this.exit(1);
104
140
  }
141
+ // A rejected command line is not a server failure: oclif refuses it before the command body runs
142
+ // (missing required flag/arg, nonexistent flag, value outside the options) and the `this.error(...)`
143
+ // guards refuse it inside. Both used to collapse into the generic C500-SYS-001 "Command failed" in
144
+ // --json, so a script could not tell a forgotten flag from an outage. Emit the same message the
145
+ // human mode prints, under a dedicated code (CYCL-33). Human mode is untouched: oclif keeps
146
+ // rendering its own error plus the usage help.
147
+ if ((0, input_error_1.isInputError)(error) && this.jsonEnabled()) {
148
+ this.logJson({ error: { code: error_codes_1.ErrorCodes.Input.invalid, message: (0, input_error_1.inputErrorMessage)(error) } });
149
+ return this.exit(1);
150
+ }
105
151
  // Oclif's default JSON formatter serializes the complete CommandError. That object references
106
152
  // the command instance and therefore cfg/api.config, including the bearer token. Never pass an
107
153
  // unknown error to that formatter in JSON mode: emit a deliberately small allow-listed envelope.
@@ -5,6 +5,9 @@ class AlertsNotificationsReadAll extends base_1.BaseCommand {
5
5
  static description = 'Mark all your unread notifications as read';
6
6
  static examples = ['<%= config.bin %> alerts notifications read-all'];
7
7
  async run() {
8
+ // Validate the command line before marking anything read: a mistyped flag must stop the
9
+ // command instead of being ignored (CYCL-33).
10
+ await this.parse(AlertsNotificationsReadAll);
8
11
  const res = await this.api.put('/cli/v1/alert_notifications/read_all');
9
12
  if (!this.jsonEnabled()) {
10
13
  this.log(`Marked ${res.data?.marked_read ?? 0} notification(s) read`);
@@ -6,6 +6,8 @@ class AlertsPreferencesShow extends base_1.BaseCommand {
6
6
  static description = 'Show your personal alert preferences (per organization)';
7
7
  static examples = ['<%= config.bin %> alerts preferences show'];
8
8
  async run() {
9
+ // No flags of its own, but the parser is what rejects a mistyped one (CYCL-33).
10
+ await this.parse(AlertsPreferencesShow);
9
11
  const res = await this.api.get('/cli/v1/alert_preferences');
10
12
  if (!this.jsonEnabled()) {
11
13
  this.log((0, output_1.renderRecord)(res.data ?? {}));
@@ -6,11 +6,11 @@ export default class IdeasConvert extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ priority: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
11
  title: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
12
  description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
13
  kind: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
- 'status-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
- 'priority-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
14
  project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
15
  };
16
16
  run(): Promise<unknown>;
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const base_1 = require("../../base");
5
+ const limits_1 = require("../../lib/limits");
5
6
  const output_1 = require("../../lib/output");
7
+ const ticket_lookup_1 = require("../../lib/ticket-lookup");
6
8
  class IdeasConvert extends base_1.BaseCommand {
7
9
  static args = {
8
10
  id: core_1.Args.string({ description: 'Idea id', required: true }),
@@ -14,14 +16,28 @@ class IdeasConvert extends base_1.BaseCommand {
14
16
  ];
15
17
  static flags = {
16
18
  ...base_1.projectFlag,
17
- title: core_1.Flags.string({ description: 'Ticket title (skips AI when set together with --description)' }),
18
- description: core_1.Flags.string({ description: 'Ticket description (skips AI when set together with --title)' }),
19
+ // Il ticket nasce qui, quindi valgono i tetti di `tickets create` (CYCL-32).
20
+ title: core_1.Flags.string({
21
+ description: `Ticket title (skips AI when set together with --description, max ${limits_1.LENGTH_LIMITS.title} characters)`,
22
+ }),
23
+ description: core_1.Flags.string({
24
+ description: `Ticket description (skips AI when set together with --title, max ${limits_1.LENGTH_LIMITS.description} characters)`,
25
+ }),
19
26
  kind: core_1.Flags.string({ description: 'Ticket kind (default: story)', options: ['bug', 'story', 'task', 'epic'] }),
20
- 'status-id': core_1.Flags.string({ description: 'Status lookup id (default: open)' }),
21
- 'priority-id': core_1.Flags.string({ description: 'Priority lookup id (default: medium)' }),
27
+ // Il ticket nasce qui: stato e priorità si indicano per code o etichetta, e omettendoli vale il
28
+ // predefinito che il server applica alla conversione (CYCL-34).
29
+ ...ticket_lookup_1.newTicketLookupFlags,
22
30
  };
23
31
  async run() {
24
32
  const { args, flags } = await this.parse(IdeasConvert);
33
+ // Il ticket è nuovo: niente da salvaguardare, tetto pieno, e i campi troppo lunghi si leggono
34
+ // tutti in questo unico rifiuto — prima dei ~30 secondi di sintesi AI, non dopo.
35
+ const tooLong = (0, limits_1.tooLongMessage)([
36
+ { flag: '--title', value: flags.title, max: limits_1.LENGTH_LIMITS.title, orthography: true },
37
+ { flag: '--description', value: flags.description, max: limits_1.LENGTH_LIMITS.description, orthography: true },
38
+ ]);
39
+ if (tooLong)
40
+ this.error(tooLong, { exit: 2 });
25
41
  const projectId = await this.resolveProjectId(flags.project);
26
42
  const body = {};
27
43
  if (flags.title !== undefined)
@@ -30,10 +46,11 @@ class IdeasConvert extends base_1.BaseCommand {
30
46
  body.description = flags.description;
31
47
  if (flags.kind !== undefined)
32
48
  body.kind = flags.kind;
33
- if (flags['status-id'] !== undefined)
34
- body.status_id = flags['status-id'];
35
- if (flags['priority-id'] !== undefined)
36
- body.priority_id = flags['priority-id'];
49
+ if (flags.status !== undefined)
50
+ body.status_id = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_STATUS, flags.status);
51
+ if (flags.priority !== undefined) {
52
+ body.priority_id = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_PRIORITY, flags.priority);
53
+ }
37
54
  if (!this.jsonEnabled() && (flags.title === undefined || flags.description === undefined)) {
38
55
  this.log('Synthesizing the ticket draft with AI (idea + comments), this may take ~30s…');
39
56
  }
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from '../../../base';
2
+ /**
3
+ * Put one page in a book's summary. Idempotent server-side: repeating the same call leaves the
4
+ * summary as it is (the page is moved, never added twice), which is what makes it safe for an
5
+ * automation that cannot know whether the previous run got through.
6
+ */
7
+ export default class KbBookAddPage extends BaseCommand {
8
+ static description: string;
9
+ static examples: string[];
10
+ static args: {
11
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ static flags: {
14
+ page: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ position: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ };
17
+ run(): Promise<unknown>;
18
+ }
@@ -0,0 +1,55 @@
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 knowledge_1 = require("../../../lib/knowledge");
6
+ /**
7
+ * Put one page in a book's summary. Idempotent server-side: repeating the same call leaves the
8
+ * summary as it is (the page is moved, never added twice), which is what makes it safe for an
9
+ * automation that cannot know whether the previous run got through.
10
+ */
11
+ class KbBookAddPage extends base_1.BaseCommand {
12
+ static description = 'Add a knowledge page to a book, optionally at a given position in the summary';
13
+ static examples = [
14
+ '<%= config.bin %> kb book add-page <book-id> --page <page-id>',
15
+ '<%= config.bin %> kb book add-page <book-id> --page <page-id> --position 0',
16
+ '<%= config.bin %> kb book add-page <book-id> --page <page-id> --json',
17
+ ];
18
+ static args = {
19
+ id: core_1.Args.string({ description: 'Knowledge book id', required: true }),
20
+ };
21
+ static flags = {
22
+ // `--page` is the name the Automator already calls (closeyourit-automator, lib/book.ts
23
+ // `assignPage`), and this command has no pagination, so there is nothing to collide with. It
24
+ // still takes a page *id*, unlike the `--page` of every list command: `--page-id` is accepted
25
+ // as an alias so a human can be explicit about that.
26
+ page: core_1.Flags.string({
27
+ aliases: ['page-id'],
28
+ description: 'Id of the knowledge page to add (an id, not a page number; alias: --page-id)',
29
+ required: true,
30
+ }),
31
+ // 0-based, like the server (Knowledge::Books::AddPage) and like the `position` the summary
32
+ // shows back. A caller counting from 1 must subtract one: passing its own number would put the
33
+ // page one slot too low, and the backend clamps instead of refusing.
34
+ position: core_1.Flags.integer({ description: 'Where to put it in the summary (0 = top; omit to append at the end)' }),
35
+ };
36
+ async run() {
37
+ const { args, flags } = await this.parse(KbBookAddPage);
38
+ if (flags.position !== undefined && flags.position < 0) {
39
+ this.error(`Invalid --position ${flags.position}: the summary starts at 0 (the top).`, { exit: 2 });
40
+ }
41
+ // `!== undefined`, not a truthiness test: position 0 is the top of the summary, not "unset".
42
+ const body = { page_id: flags.page };
43
+ if (flags.position !== undefined)
44
+ body.position = flags.position;
45
+ const res = await this.api.post(`/cli/v1/knowledge/books/${encodeURIComponent(args.id)}/pages`, body);
46
+ if (!this.jsonEnabled()) {
47
+ // "updated", not "added": the call is idempotent, so on a repeat nothing was inserted — what
48
+ // is worth showing either way is the resulting order.
49
+ this.log('Book summary updated:');
50
+ this.log((0, knowledge_1.renderBook)(res.data ?? {}));
51
+ }
52
+ return res;
53
+ }
54
+ }
55
+ exports.default = KbBookAddPage;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class KbBookList extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ q: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ per: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const knowledge_1 = require("../../../lib/knowledge");
6
+ class KbBookList extends base_1.BaseCommand {
7
+ static description = 'List knowledge books (ordered collections of pages) across the projects you can see';
8
+ static examples = [
9
+ '<%= config.bin %> kb book list',
10
+ '<%= config.bin %> kb book list --project acme-api',
11
+ '<%= config.bin %> kb book list --q deploy --per 50',
12
+ '<%= config.bin %> kb book list --json',
13
+ ];
14
+ static flags = {
15
+ // Optional (the endpoint is cross-project): omit it to span every visible project.
16
+ project: core_1.Flags.string({ char: 'p', description: 'Filter by project (key or UUID)' }),
17
+ q: core_1.Flags.string({ description: 'Filter by title (substring, case-insensitive)' }),
18
+ per: core_1.Flags.integer({ description: 'Page size' }),
19
+ ...base_1.pageFlag,
20
+ };
21
+ async run() {
22
+ const { flags } = await this.parse(KbBookList);
23
+ const invalid = (0, base_1.paginationError)(flags);
24
+ if (invalid)
25
+ this.error(invalid, { exit: 2 });
26
+ const res = await this.api.get(`/cli/v1/knowledge/books?${(0, knowledge_1.buildBooksQuery)(flags)}`);
27
+ if (!this.jsonEnabled()) {
28
+ this.log((0, knowledge_1.renderBooksTable)(res.data ?? []));
29
+ }
30
+ return res;
31
+ }
32
+ }
33
+ exports.default = KbBookList;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class KbBookShow 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
+ }
@@ -0,0 +1,22 @@
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 knowledge_1 = require("../../../lib/knowledge");
6
+ class KbBookShow extends base_1.BaseCommand {
7
+ static description = 'Show a knowledge book: its metadata, then the summary of the pages it collects';
8
+ static examples = ['<%= config.bin %> kb book show <book-id>', '<%= config.bin %> kb book show <book-id> --json'];
9
+ static args = {
10
+ id: core_1.Args.string({ description: 'Knowledge book id', required: true }),
11
+ };
12
+ async run() {
13
+ const { args } = await this.parse(KbBookShow);
14
+ const res = await this.api.get(`/cli/v1/knowledge/books/${encodeURIComponent(args.id)}`);
15
+ // The summary only lists the pages of the projects the reader can see, so a multi-project book
16
+ // never shows titles from a project this account has no access to.
17
+ if (!this.jsonEnabled())
18
+ this.log((0, knowledge_1.renderBook)(res.data ?? {}));
19
+ return res;
20
+ }
21
+ }
22
+ exports.default = KbBookShow;
@@ -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 limits_1 = require("../../lib/limits");
6
7
  const output_1 = require("../../lib/output");
7
8
  class KbCreate extends base_1.BaseCommand {
8
9
  static description = 'Create a knowledge page in a project';
@@ -15,21 +16,31 @@ class KbCreate extends base_1.BaseCommand {
15
16
  static flags = {
16
17
  // Required — passed straight to the server as `project` (key or UUID), resolved there.
17
18
  ...base_1.projectFlag,
18
- title: core_1.Flags.string({ description: 'Page title', required: true }),
19
+ title: core_1.Flags.string({ description: `Page title (max ${limits_1.LENGTH_LIMITS.title} characters)`, required: true }),
19
20
  kind: core_1.Flags.string({ description: 'Page kind: note|decision|guide', default: 'note' }),
20
- body: core_1.Flags.string({ description: 'Page body (markdown)' }),
21
- 'body-file': core_1.Flags.string({ description: 'Read the body (markdown) from a local file' }),
22
- 'tech-spec': core_1.Flags.string({ description: 'Technical section, kept separate from the simple body' }),
23
- 'tech-spec-file': core_1.Flags.string({ description: 'Read the technical section from a local file' }),
21
+ body: core_1.Flags.string({ description: `Page body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters)` }),
22
+ 'body-file': core_1.Flags.string({
23
+ description: `Read the body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters) from a local file`,
24
+ }),
25
+ 'tech-spec': core_1.Flags.string({
26
+ description: `Technical section, kept separate from the simple body (max ${limits_1.LENGTH_LIMITS.techSpec} characters)`,
27
+ }),
28
+ 'tech-spec-file': core_1.Flags.string({
29
+ description: `Read the technical section (max ${limits_1.LENGTH_LIMITS.techSpec} characters) from a local file`,
30
+ }),
24
31
  // Propose instead of publish: the page waits for a human in the review queue and stays out of
25
32
  // search, answers and related panels until it is accepted (see `kb list --status in_review`).
26
33
  'in-review': core_1.Flags.boolean({ description: 'Create the page in review instead of publishing it' }),
27
- 'review-note': core_1.Flags.string({ description: 'One line explaining why the page is worth keeping (review queue)' }),
34
+ 'review-note': core_1.Flags.string({
35
+ description: `One line explaining why the page is worth keeping (review queue, max ${limits_1.LENGTH_LIMITS.reviewNote} characters)`,
36
+ }),
28
37
  };
29
38
  async run() {
30
39
  const { flags } = await this.parse(KbCreate);
31
40
  let body = flags.body;
41
+ let bodyFlag = '--body';
32
42
  if (body === undefined && flags['body-file'] !== undefined) {
43
+ bodyFlag = '--body-file';
33
44
  try {
34
45
  body = await (0, promises_1.readFile)(flags['body-file'], 'utf8');
35
46
  }
@@ -41,7 +52,9 @@ class KbCreate extends base_1.BaseCommand {
41
52
  this.error('Provide the page body via --body or --body-file.', { exit: 2 });
42
53
  }
43
54
  let techSpec = flags['tech-spec'];
55
+ let techSpecFlag = '--tech-spec';
44
56
  if (techSpec === undefined && flags['tech-spec-file'] !== undefined) {
57
+ techSpecFlag = '--tech-spec-file';
45
58
  try {
46
59
  techSpec = await (0, promises_1.readFile)(flags['tech-spec-file'], 'utf8');
47
60
  }
@@ -52,6 +65,16 @@ class KbCreate extends base_1.BaseCommand {
52
65
  if (flags['review-note'] !== undefined && !flags['in-review']) {
53
66
  this.error('--review-note only applies to a page created with --in-review.', { exit: 2 });
54
67
  }
68
+ // Una pagina nuova non ha niente da salvaguardare: il tetto vale pieno su ogni campo, e i campi
69
+ // troppo lunghi si leggono tutti in questo unico rifiuto invece che uno per tentativo (CYCL-32).
70
+ const tooLong = (0, limits_1.tooLongMessage)([
71
+ { flag: '--title', value: flags.title, max: limits_1.LENGTH_LIMITS.title },
72
+ { flag: bodyFlag, value: body, max: limits_1.LENGTH_LIMITS.pageBody },
73
+ { flag: techSpecFlag, value: techSpec, max: limits_1.LENGTH_LIMITS.techSpec },
74
+ { flag: '--review-note', value: flags['review-note'], max: limits_1.LENGTH_LIMITS.reviewNote },
75
+ ]);
76
+ if (tooLong)
77
+ this.error(tooLong, { exit: 2 });
55
78
  const res = await this.api.post('/cli/v1/knowledge/pages', {
56
79
  project: flags.project,
57
80
  title: flags.title,
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class KbPublish extends BaseCommand {
3
+ static description: string;
4
+ static usage: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ 'publication-key': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ title: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ kind: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ body: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ 'body-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<unknown>;
15
+ }