@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
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const core_1 = require("@oclif/core");
5
+ const base_1 = require("../../base");
6
+ const error_codes_1 = require("../../errors/error-codes");
7
+ const api_1 = require("../../lib/api");
8
+ const limits_1 = require("../../lib/limits");
9
+ const output_1 = require("../../lib/output");
10
+ // Copia dichiarata di `Knowledge::Page::PUBLICATION_KEY_FORMAT`: una chiave malformata è l'unico
11
+ // errore che si può riconoscere senza chiamare il server, e riconoscerlo qui evita di scoprire a
12
+ // metà di un ciclo di retry che tutte le chiamate erano destinate a un 422.
13
+ const PUBLICATION_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._~:-]{0,254}$/;
14
+ // Un errore che il backend ha davvero formulato: `error-codes.ts` riserva le iniziali R/G ai codici
15
+ // applicativi e li propaga intatti, mentre tutto ciò che inizia per C lo ha coniato la CLI davanti a
16
+ // una risposta che non parlava — corpo non JSON (`C000-NET-001`) o envelope senza codice
17
+ // (`C404-API-000`), che è la forma in cui si presenta una route inesistente.
18
+ const BACKEND_CODE_RE = /^[RG]/;
19
+ class KbPublish extends base_1.BaseCommand {
20
+ static description = `Idempotently create or update a Knowledge page by publication key.
21
+
22
+ Exactly one of --body or --body-file is required.`;
23
+ static usage = '-p <project> --publication-key <key> --title <title> [--kind <kind>] (--body <markdown> | --body-file <path>)';
24
+ static examples = [
25
+ '<%= config.bin %> kb publish --project CYCL --publication-key docs:release --title "Release guide" --body "Steps…"',
26
+ '<%= config.bin %> kb publish -p CYCL --publication-key docs:release --title "Release guide" --kind guide --body-file ./release.md',
27
+ ];
28
+ static flags = {
29
+ ...base_1.projectFlag,
30
+ 'publication-key': core_1.Flags.string({
31
+ description: 'Stable publication key (1-255 URL-safe characters)',
32
+ required: true,
33
+ }),
34
+ title: core_1.Flags.string({ description: `Page title (max ${limits_1.LENGTH_LIMITS.title} characters)`, required: true }),
35
+ // Nessun default qui: il server mette `note` solo quando la pagina nasce e altrimenti tiene il
36
+ // kind che ha già. Spedire `note` a ogni chiamata declasserebbe una guida a nota al primo
37
+ // publish ripetuto senza `--kind`, che è esattamente ciò che una fonte idempotente non deve fare.
38
+ kind: core_1.Flags.string({ description: 'Page kind: note|decision|guide (new page: note; existing: unchanged)' }),
39
+ body: core_1.Flags.string({
40
+ description: `Page body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters); exactly one of --body or --body-file is required`,
41
+ exactlyOne: ['body', 'body-file'],
42
+ }),
43
+ 'body-file': core_1.Flags.string({
44
+ description: `Read the page body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters) from a local file; exactly one of --body or --body-file is required`,
45
+ }),
46
+ };
47
+ async run() {
48
+ const { flags } = await this.parse(KbPublish);
49
+ const publicationKey = flags['publication-key'];
50
+ if (!PUBLICATION_KEY_RE.test(publicationKey)) {
51
+ this.error('Invalid --publication-key: use 1-255 characters, start with a letter or number, then only letters, numbers, . _ ~ : -.', { exit: 2 });
52
+ }
53
+ let body;
54
+ let bodyFlag = '--body';
55
+ if (flags.body === undefined) {
56
+ bodyFlag = '--body-file';
57
+ try {
58
+ body = await (0, promises_1.readFile)(flags['body-file'], 'utf8');
59
+ }
60
+ catch {
61
+ this.error(`File not found or unreadable: ${flags['body-file']}`, { exit: 2 });
62
+ }
63
+ }
64
+ else {
65
+ body = flags.body;
66
+ }
67
+ // Il publish è un upsert atomico e non rilegge la pagina prima di scrivere: senza un valore
68
+ // attuale da usare come salvaguardia il tetto vale pieno, come su `kb create` (CYCL-32).
69
+ const tooLong = (0, limits_1.tooLongMessage)([
70
+ { flag: '--title', value: flags.title, max: limits_1.LENGTH_LIMITS.title },
71
+ { flag: bodyFlag, value: body, max: limits_1.LENGTH_LIMITS.pageBody },
72
+ ]);
73
+ if (tooLong)
74
+ this.error(tooLong, { exit: 2 });
75
+ const projectId = await this.resolveProjectId(flags.project);
76
+ const path = `/cli/v1/projects/${projectId}/knowledge/publications/${encodeURIComponent(publicationKey)}`;
77
+ // `kind` esce dal payload quando non è stato passato: il server distingue "non me l'hai detto"
78
+ // (tiene quello che ha) da un valore esplicito, e una chiave presente ma vuota non è la stessa cosa.
79
+ const payload = { title: flags.title, ...(flags.kind === undefined ? {} : { kind: flags.kind }), body };
80
+ let res;
81
+ try {
82
+ res = await this.api.put(path, payload);
83
+ }
84
+ catch (error) {
85
+ // Un 404 che il backend non ha formulato è la route che non c'è: su un server più vecchio
86
+ // della pubblicazione atomica il messaggio dice cosa aggiornare, invece di far sembrare
87
+ // sparito il progetto. Ogni 404 applicativo passa intatto — R404-KNOWLEDGE-001 per il progetto
88
+ // invisibile e chiunque altro il backend aggiunga dopo, che qui non va mascherato.
89
+ if (error instanceof api_1.ApiRequestError && error.status === 404 && !BACKEND_CODE_RE.test(error.code)) {
90
+ throw new api_1.ApiRequestError(404, error_codes_1.ErrorCodes.Knowledge.publishUnavailable, 'Knowledge publish is not supported by this backend. Upgrade CloseYourIt and retry.');
91
+ }
92
+ throw error;
93
+ }
94
+ if (!this.jsonEnabled()) {
95
+ const meta = (res.meta ?? {});
96
+ const operation = meta.operation === 'created' ? 'created' : 'updated';
97
+ // Il corpo è appena stato scritto da chi ha lanciato il comando: ristamparlo per intero
98
+ // seppellirebbe l'unica cosa nuova, cioè se la pagina è nata ora o è stata aggiornata.
99
+ const { body: _body, ...page } = res.data ?? {};
100
+ this.log(`Knowledge publication ${operation}:`);
101
+ this.log((0, output_1.renderRecord)({ ...page, operation: meta.operation, adopted_legacy: meta.adopted_legacy }));
102
+ }
103
+ return res;
104
+ }
105
+ }
106
+ exports.default = KbPublish;
@@ -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 KbUpdate extends base_1.BaseCommand {
8
9
  static description = 'Update a knowledge page (only the fields you pass; the others keep their current value)';
@@ -15,17 +16,25 @@ class KbUpdate extends base_1.BaseCommand {
15
16
  id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
16
17
  };
17
18
  static flags = {
18
- title: core_1.Flags.string({ description: 'New title' }),
19
+ title: core_1.Flags.string({ description: `New title (max ${limits_1.LENGTH_LIMITS.title} characters)` }),
19
20
  kind: core_1.Flags.string({ description: 'New kind: note|decision|guide' }),
20
- body: core_1.Flags.string({ description: 'New body (markdown)' }),
21
- 'body-file': core_1.Flags.string({ description: 'Read the new body (markdown) from a local file' }),
22
- 'tech-spec': core_1.Flags.string({ description: 'New technical section, kept separate from the simple body' }),
23
- 'tech-spec-file': core_1.Flags.string({ description: 'Read the new technical section from a local file' }),
21
+ body: core_1.Flags.string({ description: `New body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters)` }),
22
+ 'body-file': core_1.Flags.string({
23
+ description: `Read the new body (markdown, max ${limits_1.LENGTH_LIMITS.pageBody} characters) from a local file`,
24
+ }),
25
+ 'tech-spec': core_1.Flags.string({
26
+ description: `New 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 new technical section (max ${limits_1.LENGTH_LIMITS.techSpec} characters) from a local file`,
30
+ }),
24
31
  };
25
32
  async run() {
26
33
  const { args, flags } = await this.parse(KbUpdate);
27
34
  let body = flags.body;
35
+ let bodyFlag = '--body';
28
36
  if (body === undefined && flags['body-file'] !== undefined) {
37
+ bodyFlag = '--body-file';
29
38
  try {
30
39
  body = await (0, promises_1.readFile)(flags['body-file'], 'utf8');
31
40
  }
@@ -34,7 +43,9 @@ class KbUpdate extends base_1.BaseCommand {
34
43
  }
35
44
  }
36
45
  let techSpec = flags['tech-spec'];
46
+ let techSpecFlag = '--tech-spec';
37
47
  if (techSpec === undefined && flags['tech-spec-file'] !== undefined) {
48
+ techSpecFlag = '--tech-spec-file';
38
49
  try {
39
50
  techSpec = await (0, promises_1.readFile)(flags['tech-spec-file'], 'utf8');
40
51
  }
@@ -53,6 +64,16 @@ class KbUpdate extends base_1.BaseCommand {
53
64
  const base = `/cli/v1/knowledge/pages/${encodeURIComponent(args.id)}`;
54
65
  const current = await this.api.get(base);
55
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 });
56
77
  const res = await this.api.put(base, {
57
78
  title: flags.title ?? page.title,
58
79
  kind: flags.kind ?? page.kind,
@@ -6,6 +6,9 @@ class Logout extends base_1.BaseCommand {
6
6
  static description = 'Clear the locally stored token and identity';
7
7
  static examples = ['<%= config.bin %> logout'];
8
8
  async run() {
9
+ // Validate the command line before touching the stored credentials: a mistyped flag must stop
10
+ // the command, not be ignored while it logs you out anyway (CYCL-33).
11
+ await this.parse(Logout);
9
12
  (0, config_1.clearToken)();
10
13
  if (!this.jsonEnabled()) {
11
14
  this.log('Logged out. Local credentials cleared.');
@@ -6,6 +6,8 @@ class OrgShow extends base_1.BaseCommand {
6
6
  static description = 'Show your organization settings';
7
7
  static examples = ['<%= config.bin %> org show', '<%= config.bin %> org show --json'];
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(OrgShow);
9
11
  const res = await this.api.get('/cli/v1/organization');
10
12
  if (!this.jsonEnabled()) {
11
13
  this.log((0, output_1.renderRecord)(res.data ?? {}));
@@ -11,7 +11,7 @@ class PersonalImport extends base_1.BaseCommand {
11
11
  static examples = [
12
12
  '<%= config.bin %> personal import --from-file .env',
13
13
  '<%= config.bin %> personal import --format json --from-file secrets.json',
14
- 'doppler secrets download --no-file --format json | <%= config.bin %> personal import --format json',
14
+ 'printf \'{"API_KEY":"value"}\' | <%= config.bin %> personal import --format json',
15
15
  ];
16
16
  static flags = {
17
17
  'from-file': core_1.Flags.string({ description: 'Path to a .env or JSON file (default: read from stdin)' }),
@@ -25,7 +25,7 @@ class PersonalImport extends base_1.BaseCommand {
25
25
  }
26
26
  else {
27
27
  if (process.stdin.isTTY) {
28
- this.error('Provide --from-file or pipe the input via stdin (e.g. `doppler secrets download --format json | cyi personal import --format json`)', { exit: 1 });
28
+ this.error('Provide --from-file or pipe JSON/ENV input via stdin (e.g. `printf \'{"API_KEY":"value"}\' | cyi personal import --format json`)', { exit: 1 });
29
29
  }
30
30
  text = await (0, stdin_1.readStream)();
31
31
  }
@@ -7,7 +7,7 @@ class PersonalRun extends base_1.BaseCommand {
7
7
  static strict = false;
8
8
  // The output belongs to the child process, not to us.
9
9
  static enableJsonFlag = false;
10
- static description = 'Run a command with your personal secrets injected as env vars (like `doppler run`). Secrets never touch disk.';
10
+ static description = 'Run a command with your personal secrets injected as env vars. Secrets never touch disk.';
11
11
  static examples = [
12
12
  '<%= config.bin %> personal run -- bundle exec rails server',
13
13
  '<%= config.bin %> personal run -- printenv OPENAI_API_KEY',
@@ -7,7 +7,7 @@ class Run extends base_1.BaseCommand {
7
7
  static strict = false;
8
8
  // The output belongs to the child process, not to us.
9
9
  static enableJsonFlag = false;
10
- static description = 'Run a command with the project/environment secrets injected as env vars (like `doppler run`). Secrets never touch disk.';
10
+ static description = 'Run a command with project/environment secrets injected as env vars. Secrets never touch disk.';
11
11
  static examples = [
12
12
  '<%= config.bin %> run -p acme-api -e production -- bundle exec rails server',
13
13
  '<%= config.bin %> run -p acme-api -e production -- printenv DATABASE_URL',
@@ -36,7 +36,7 @@ class SecretsImport extends base_1.BaseCommand {
36
36
  static examples = [
37
37
  '<%= config.bin %> secrets import -p acme-api -e production --from-file .env',
38
38
  '<%= config.bin %> secrets import -p acme-api -e production --format json --from-file secrets.json',
39
- 'doppler secrets download --config production --no-file --format json | <%= config.bin %> secrets import -p acme-api -e production --format json',
39
+ 'printf \'{"API_KEY":"value"}\' | <%= config.bin %> secrets import -p acme-api -e development --format json',
40
40
  ];
41
41
  static flags = {
42
42
  ...base_1.projectFlag,
@@ -53,7 +53,7 @@ class SecretsImport extends base_1.BaseCommand {
53
53
  }
54
54
  else {
55
55
  if (process.stdin.isTTY) {
56
- this.error('Provide --from-file or pipe the input via stdin (e.g. `doppler secrets download --format json | cyi secrets import --format json`)', { exit: 1 });
56
+ this.error('Provide --from-file or pipe JSON/ENV input via stdin (e.g. `printf \'{"API_KEY":"value"}\' | cyi secrets import -p acme-api -e development --format json`)', { exit: 1 });
57
57
  }
58
58
  text = await (0, stdin_1.readStream)();
59
59
  }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoIgnore 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
+ note: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,27 @@
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 SeoIgnore extends base_1.BaseCommand {
6
+ static description = 'Ignore an SEO finding: you accept living with it';
7
+ static examples = [
8
+ '<%= config.bin %> seo ignore <issue-id>',
9
+ '<%= config.bin %> seo ignore <issue-id> --note "Service page, not meant to be found."',
10
+ ];
11
+ static args = {
12
+ id: core_1.Args.string({ description: 'SEO finding id', required: true }),
13
+ };
14
+ static flags = {
15
+ note: core_1.Flags.string({ description: 'Why you are keeping it (shown alongside the ignored row)' }),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(SeoIgnore);
19
+ const res = await this.api.put(`/cli/v1/seo/${args.id}/ignore`, { triage_note: flags.note });
20
+ if (!this.jsonEnabled()) {
21
+ // An ignored finding never reopens on its own: the next check won't undo a decision taken.
22
+ this.log(`SEO finding ${args.id} ignored.`);
23
+ }
24
+ return res;
25
+ }
26
+ }
27
+ exports.default = SeoIgnore;
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoList 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
+ severity: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ area: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ }
@@ -0,0 +1,52 @@
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
+ class SeoList extends base_1.BaseCommand {
7
+ static description = 'List the SEO findings of your sites (cross-project)';
8
+ static examples = [
9
+ '<%= config.bin %> seo list',
10
+ '<%= config.bin %> seo list --severity critical',
11
+ '<%= config.bin %> seo list --project acme-site --status open',
12
+ '<%= config.bin %> seo list --area indexability --json',
13
+ ];
14
+ static flags = {
15
+ // Optional here (the endpoint is cross-project): without it, the list spans every visible project.
16
+ project: core_1.Flags.string({ char: 'p', description: 'Filter by project id (UUID) or key' }),
17
+ severity: core_1.Flags.string({ description: 'Filter by severity (critical, high, medium, low)' }),
18
+ status: core_1.Flags.string({ description: 'Filter by status (open, resolved, ignored)' }),
19
+ area: core_1.Flags.string({ description: 'Filter by area (indexability, structure, content, i18n, performance)' }),
20
+ ...base_1.pageFlag,
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(SeoList);
24
+ const query = new URLSearchParams({ page: String(flags.page) });
25
+ if (flags.project)
26
+ query.set('project_id', await this.resolveProjectId(flags.project));
27
+ if (flags.severity)
28
+ query.set('severity', flags.severity);
29
+ if (flags.status)
30
+ query.set('status', flags.status);
31
+ if (flags.area)
32
+ query.set('area', flags.area);
33
+ const res = await this.api.get(`/cli/v1/seo?${query.toString()}`);
34
+ const issues = res.data ?? [];
35
+ if (!this.jsonEnabled()) {
36
+ this.log((0, output_1.renderTable)(
37
+ // ID first, like `vulnerabilities list`: every other subcommand takes it as its argument,
38
+ // so a table without it forces `--json` just to triage what you are already looking at.
39
+ ['ID', 'PROJECT', 'CHECK', 'URL', 'SEVERITY', 'STATUS'], issues.map((issue) => [
40
+ String(issue.id ?? ''),
41
+ String(issue.project?.key ?? ''),
42
+ String(issue.check_key ?? ''),
43
+ // A site-wide finding (duplicate titles, unreachable sitemap) has no single URL.
44
+ String(issue.url ?? '— whole site'),
45
+ (0, output_1.statusCell)(issue.severity),
46
+ (0, output_1.statusCell)(issue.status),
47
+ ])));
48
+ }
49
+ return res;
50
+ }
51
+ }
52
+ exports.default = SeoList;
@@ -0,0 +1,11 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoPages 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
+ url: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, 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 output_1 = require("../../lib/output");
6
+ class SeoPages extends base_1.BaseCommand {
7
+ static description = 'List the pages the SEO check has visited, with their state';
8
+ static examples = [
9
+ '<%= config.bin %> seo pages',
10
+ '<%= config.bin %> seo pages --project acme-site',
11
+ '<%= config.bin %> seo pages --url /prezzi --json',
12
+ ];
13
+ static flags = {
14
+ project: core_1.Flags.string({ char: 'p', description: 'Filter by project id (UUID) or key' }),
15
+ url: core_1.Flags.string({ description: 'Filter by URL (partial match)' }),
16
+ ...base_1.pageFlag,
17
+ };
18
+ async run() {
19
+ const { flags } = await this.parse(SeoPages);
20
+ const query = new URLSearchParams({ page: String(flags.page) });
21
+ if (flags.project)
22
+ query.set('project_id', await this.resolveProjectId(flags.project));
23
+ if (flags.url)
24
+ query.set('url', flags.url);
25
+ const res = await this.api.get(`/cli/v1/seo/pages?${query.toString()}`);
26
+ const pages = res.data ?? [];
27
+ if (!this.jsonEnabled()) {
28
+ this.log((0, output_1.renderTable)(['PROJECT', 'PATH', 'STATUS', 'TITLE', 'H1', 'WORDS'], pages.map((page) => {
29
+ const headings = Array.isArray(page.h1s) ? page.h1s : [];
30
+ return [
31
+ String(page.project?.key ?? ''),
32
+ String(page.path ?? page.url ?? ''),
33
+ (0, output_1.statusCell)(page.status_code),
34
+ String(page.title ?? '—'),
35
+ // No h1 is the finding a green score never reports: it belongs in the column, not hidden.
36
+ headings.length === 0 ? 'missing' : String(headings.length),
37
+ String(page.word_count ?? 0),
38
+ ];
39
+ })));
40
+ }
41
+ return res;
42
+ }
43
+ }
44
+ exports.default = SeoPages;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoPromote 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 output_1 = require("../../lib/output");
6
+ class SeoPromote extends base_1.BaseCommand {
7
+ static description = 'Open a ticket from an SEO finding, evidence included';
8
+ static examples = ['<%= config.bin %> seo promote <issue-id>'];
9
+ static args = {
10
+ id: core_1.Args.string({ description: 'SEO finding id', required: true }),
11
+ };
12
+ async run() {
13
+ const { args } = await this.parse(SeoPromote);
14
+ // Idempotent server-side: promoting twice returns R422-SEO-001, never a second ticket.
15
+ const res = await this.api.put(`/cli/v1/seo/${args.id}/promotion`);
16
+ if (!this.jsonEnabled()) {
17
+ this.log((0, output_1.renderRecord)(res.data ?? {}));
18
+ }
19
+ return res;
20
+ }
21
+ }
22
+ exports.default = SeoPromote;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoReopen 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,21 @@
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 SeoReopen extends base_1.BaseCommand {
6
+ static description = 'Reopen an ignored SEO finding: you changed your mind';
7
+ static examples = ['<%= config.bin %> seo reopen <issue-id>'];
8
+ static args = {
9
+ id: core_1.Args.string({ description: 'SEO finding id', required: true }),
10
+ };
11
+ async run() {
12
+ const { args } = await this.parse(SeoReopen);
13
+ // Reopening is deleting the ignore, not a verb of its own — hence DELETE on the same resource.
14
+ const res = await this.api.delete(`/cli/v1/seo/${args.id}/ignore`);
15
+ if (!this.jsonEnabled()) {
16
+ this.log(`SEO finding ${args.id} reopened.`);
17
+ }
18
+ return res;
19
+ }
20
+ }
21
+ exports.default = SeoReopen;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoRescan extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ site: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<unknown>;
9
+ }
@@ -0,0 +1,21 @@
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 SeoRescan extends base_1.BaseCommand {
6
+ static description = 'Check a site now, without waiting for the next scheduled run';
7
+ static examples = ['<%= config.bin %> seo rescan --site <site-id>'];
8
+ // Required: a check always targets one site (a project can declare one per environment).
9
+ static flags = {
10
+ site: core_1.Flags.string({ description: 'Site id (from `seo-sites list`)', required: true }),
11
+ };
12
+ async run() {
13
+ const { flags } = await this.parse(SeoRescan);
14
+ const res = await this.api.post('/cli/v1/seo/rescan', { site_id: flags.site });
15
+ if (!this.jsonEnabled()) {
16
+ this.log(`Check queued for site ${flags.site}.`);
17
+ }
18
+ return res;
19
+ }
20
+ }
21
+ exports.default = SeoRescan;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoShow 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,35 @@
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
+ /** A nested object of the payload, or `{}` when the field is absent. */
7
+ function nested(data, key) {
8
+ const value = data[key];
9
+ return value && typeof value === 'object' ? value : {};
10
+ }
11
+ class SeoShow extends base_1.BaseCommand {
12
+ static description = 'Show a single SEO finding, with the evidence behind it';
13
+ static examples = ['<%= config.bin %> seo show <issue-id>'];
14
+ static args = {
15
+ id: core_1.Args.string({ description: 'SEO finding id', required: true }),
16
+ };
17
+ async run() {
18
+ const { args } = await this.parse(SeoShow);
19
+ const res = await this.api.get(`/cli/v1/seo/${args.id}`);
20
+ if (!this.jsonEnabled()) {
21
+ const data = res.data ?? {};
22
+ // Evidence in a block of its own: it is the reason the finding exists, and burying it among
23
+ // the metadata means reopening the browser to see what was actually found.
24
+ this.log((0, output_1.renderRecord)({ ...data, evidence: undefined }));
25
+ this.log((0, output_1.section)('Evidence', '🔎'));
26
+ this.log((0, output_1.renderRecord)(nested(data, 'evidence')));
27
+ this.log((0, output_1.section)('Site', '🌐'));
28
+ this.log((0, output_1.renderRecord)(nested(data, 'site')));
29
+ this.log((0, output_1.section)('Project', '📦'));
30
+ this.log((0, output_1.renderRecord)(nested(data, 'project')));
31
+ }
32
+ return res;
33
+ }
34
+ }
35
+ exports.default = SeoShow;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoSitesCreate extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ url: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ environment: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ frequency: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ 'max-pages': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'no-sitemap': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,43 @@
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
+ class SeoSitesCreate extends base_1.BaseCommand {
7
+ static description = 'Declare a site to keep an eye on (one per project and environment)';
8
+ static examples = [
9
+ '<%= config.bin %> seo-sites create --project acme-site --url https://www.acme.example --environment <env-id>',
10
+ '<%= config.bin %> seo-sites create -p acme-site --url https://www.acme.example --environment <env-id> --frequency weekly --max-pages 50',
11
+ ];
12
+ static flags = {
13
+ ...base_1.projectFlag,
14
+ url: core_1.Flags.string({ description: 'Public address the visit starts from', required: true }),
15
+ environment: core_1.Flags.string({ description: 'Environment id (must be declared by the project)', required: true }),
16
+ frequency: core_1.Flags.string({ description: 'How often to check it: daily (default) or weekly', options: ['daily', 'weekly'] }),
17
+ 'max-pages': core_1.Flags.integer({ description: 'How many pages to visit at most each run' }),
18
+ 'no-sitemap': core_1.Flags.boolean({ description: "Don't read the sitemap: start from the home page and follow links only" }),
19
+ };
20
+ async run() {
21
+ const { flags } = await this.parse(SeoSitesCreate);
22
+ const projectId = await this.resolveProjectId(flags.project);
23
+ const body = {
24
+ project_id: projectId,
25
+ environment_id: flags.environment,
26
+ base_url: flags.url,
27
+ };
28
+ if (flags.frequency !== undefined)
29
+ body.frequency = flags.frequency;
30
+ if (flags['max-pages'] !== undefined)
31
+ body.max_pages = flags['max-pages'];
32
+ if (flags['no-sitemap'])
33
+ body.follow_sitemap = false;
34
+ const res = await this.api.post('/cli/v1/seo_sites', body);
35
+ if (!this.jsonEnabled()) {
36
+ this.log((0, output_1.renderRecord)(res.data ?? {}));
37
+ // The first run starts on creation: without saying so, an empty list looks like a failure.
38
+ this.log('First check queued.');
39
+ }
40
+ return res;
41
+ }
42
+ }
43
+ exports.default = SeoSitesCreate;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class SeoSitesDelete 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
+ }