@bussolabs/closeyourit-cli 0.21.0 → 0.22.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.
- package/README.md +1 -0
- package/dist/commands/seo-sites/show.d.ts +15 -0
- package/dist/commands/seo-sites/show.js +107 -0
- package/oclif.manifest.json +4272 -4233
- package/opencli.json +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
92
92
|
| `seo promote <id>` | Open a ticket from it, evidence included. |
|
|
93
93
|
| `seo rescan --site <id>` | Check a site now, without waiting for the scheduled run (requires seo.manage). |
|
|
94
94
|
| `seo-sites list [--project <id\|key>] [--page]` | The sites under SEO check, with cadence and open findings. |
|
|
95
|
+
| `seo-sites show <id>` | One site in full: setup, open findings, last run and vitals. |
|
|
95
96
|
| `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
97
|
| `seo-sites update <id> [--url] [--frequency] [--max-pages] [--sitemap\|--no-sitemap] [--enable\|--disable]` | Change how a site is checked. |
|
|
97
98
|
| `seo-sites delete <id>` | Stop watching a site and remove its pages and findings. |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
export default class SeoSitesShow extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* One nested block as a section of its own — or nothing at all when the payload has no such
|
|
11
|
+
* block. An empty "Vitals" heading reads as "this site has no vitals measured", when it really
|
|
12
|
+
* means "this server never sent them": same reason `tickets show` skips absent guidance (CYCL-2).
|
|
13
|
+
*/
|
|
14
|
+
private printBlock;
|
|
15
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const base_1 = require("../../base");
|
|
5
|
+
const output_1 = require("../../lib/output");
|
|
6
|
+
/**
|
|
7
|
+
* The blocks the card opens with, in reading order, each with the heading a reader recognises.
|
|
8
|
+
* Everything else the payload carries is printed after these, under a heading derived from its own
|
|
9
|
+
* key: the endpoint is new (CYRA-541) and keeps growing, and a client that only knows five names
|
|
10
|
+
* would drop the sixth block without ever saying so.
|
|
11
|
+
*/
|
|
12
|
+
const KNOWN_BLOCKS = [
|
|
13
|
+
{ icon: '🔎', key: 'open_issues', title: 'Open findings' },
|
|
14
|
+
{ icon: '🕒', key: 'last_audit', title: 'Last run' },
|
|
15
|
+
{ icon: '⚡', key: 'vitals', title: 'Vitals' },
|
|
16
|
+
{ icon: '📦', key: 'project', title: 'Project' },
|
|
17
|
+
{ icon: '🌱', key: 'environment', title: 'Environment' },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* The known block keys, each mapped to `undefined`, to spread over the head record. `renderRecord`
|
|
21
|
+
* prints a null scalar as `key: -`, so a site that was never checked (`last_audit: null`) would
|
|
22
|
+
* grow a `last_audit: -` line among its settings — a block that is not there, listed as if it were
|
|
23
|
+
* a setting you could change. Same trick `seo show` uses to lift `evidence` out of its head.
|
|
24
|
+
*/
|
|
25
|
+
const BLOCKS_OFF_THE_HEAD = Object.fromEntries(KNOWN_BLOCKS.map((block) => [block.key, undefined]));
|
|
26
|
+
/** A value `renderRecord`/`renderTable` can print as a cell (null included: it prints as `-`). */
|
|
27
|
+
function isScalar(value) {
|
|
28
|
+
return value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
29
|
+
}
|
|
30
|
+
/** `recent_audits` → `Recent audits`: the heading for a block nobody taught this client about. */
|
|
31
|
+
function titleFromKey(key) {
|
|
32
|
+
const words = key.split('_').join(' ');
|
|
33
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A list of objects as a table. The columns are every scalar field the rows carry, in the order
|
|
37
|
+
* they first appear: taking the shape from the first row alone would silently crop a field that
|
|
38
|
+
* only the failed run has (its error). Returns '' when no row has anything printable.
|
|
39
|
+
*/
|
|
40
|
+
function renderRows(rows) {
|
|
41
|
+
const columns = [];
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
for (const [key, value] of Object.entries(row)) {
|
|
44
|
+
if (isScalar(value) && !columns.includes(key))
|
|
45
|
+
columns.push(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (columns.length === 0)
|
|
49
|
+
return '';
|
|
50
|
+
return (0, output_1.renderTable)(columns.map((column) => column.toUpperCase()), rows.map((row) => columns.map((column) => String(row[column] ?? ''))));
|
|
51
|
+
}
|
|
52
|
+
/** The printable body of a nested block, or '' when there is nothing in it to print. */
|
|
53
|
+
function renderBlock(value) {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
const rows = value.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item));
|
|
56
|
+
if (rows.length > 0)
|
|
57
|
+
return renderRows(rows);
|
|
58
|
+
// A list of plain values (enabled checks, urls) reads as one line, not as a one-column table.
|
|
59
|
+
const items = value.filter((item) => isScalar(item) && item !== null);
|
|
60
|
+
return items.map((item) => (0, output_1.sanitize)(item)).join(', ');
|
|
61
|
+
}
|
|
62
|
+
if (value && typeof value === 'object') {
|
|
63
|
+
const record = value;
|
|
64
|
+
return Object.keys(record).length > 0 ? (0, output_1.renderRecord)(record) : '';
|
|
65
|
+
}
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
class SeoSitesShow extends base_1.BaseCommand {
|
|
69
|
+
static description = 'Show one site under SEO check: how it is set up, what is open, how the last run went and how fast it is';
|
|
70
|
+
static examples = ['<%= config.bin %> seo-sites show <site-id>', '<%= config.bin %> seo-sites show <site-id> --json'];
|
|
71
|
+
static args = {
|
|
72
|
+
id: core_1.Args.string({ description: 'Site id', required: true }),
|
|
73
|
+
};
|
|
74
|
+
async run() {
|
|
75
|
+
const { args } = await this.parse(SeoSitesShow);
|
|
76
|
+
const res = await this.api.get(`/cli/v1/seo_sites/${encodeURIComponent(args.id)}`);
|
|
77
|
+
if (!this.jsonEnabled()) {
|
|
78
|
+
const data = res.data ?? {};
|
|
79
|
+
// Configuration and cadence first: everything you would change with `seo-sites update`.
|
|
80
|
+
this.log((0, output_1.renderRecord)({ ...data, ...BLOCKS_OFF_THE_HEAD }));
|
|
81
|
+
for (const { icon, key, title } of KNOWN_BLOCKS)
|
|
82
|
+
this.printBlock(title, data[key], icon);
|
|
83
|
+
const known = new Set(KNOWN_BLOCKS.map((block) => block.key));
|
|
84
|
+
for (const [key, value] of Object.entries(data)) {
|
|
85
|
+
if (known.has(key) || isScalar(value))
|
|
86
|
+
continue;
|
|
87
|
+
// The heading comes from a server-supplied key: sanitize it like every other printed
|
|
88
|
+
// string (CWE-150). `section` interpolates its title raw, so this is the last stop.
|
|
89
|
+
this.printBlock((0, output_1.sanitize)(titleFromKey(key)), value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return res;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One nested block as a section of its own — or nothing at all when the payload has no such
|
|
96
|
+
* block. An empty "Vitals" heading reads as "this site has no vitals measured", when it really
|
|
97
|
+
* means "this server never sent them": same reason `tickets show` skips absent guidance (CYCL-2).
|
|
98
|
+
*/
|
|
99
|
+
printBlock(title, value, icon) {
|
|
100
|
+
const body = renderBlock(value);
|
|
101
|
+
if (!body)
|
|
102
|
+
return;
|
|
103
|
+
this.log((0, output_1.section)(title, icon));
|
|
104
|
+
this.log(body);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.default = SeoSitesShow;
|