@bussolabs/closeyourit-cli 0.20.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 +11 -0
- package/dist/base.d.ts +11 -0
- package/dist/base.js +15 -0
- package/dist/commands/kb/book/add-page.d.ts +18 -0
- package/dist/commands/kb/book/add-page.js +55 -0
- package/dist/commands/kb/book/list.d.ts +12 -0
- package/dist/commands/kb/book/list.js +33 -0
- package/dist/commands/kb/book/show.d.ts +9 -0
- package/dist/commands/kb/book/show.js +22 -0
- package/dist/commands/kb/publish.d.ts +15 -0
- package/dist/commands/kb/publish.js +106 -0
- package/dist/commands/seo-sites/show.d.ts +15 -0
- package/dist/commands/seo-sites/show.js +107 -0
- package/dist/errors/error-codes.d.ts +3 -0
- package/dist/errors/error-codes.js +5 -0
- package/dist/lib/knowledge.d.ts +24 -0
- package/dist/lib/knowledge.js +68 -0
- package/oclif.manifest.json +3572 -3288
- package/opencli.json +207 -1
- package/package.json +4 -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. |
|
|
@@ -142,10 +143,14 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
142
143
|
| `kb show <id> [--related] [--question <q>]` | Show one knowledge page: metadata header, raw markdown body, then the technical section. |
|
|
143
144
|
| `kb related <id> [--question <q>] [--links-only]` | Pages to read next: linked with `[[wiki links]]` plus close matches by meaning. |
|
|
144
145
|
| `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. |
|
|
146
|
+
| `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`. |
|
|
145
147
|
| `kb consolidate <id> --path <doc-path>` | Mark an accepted page as written to the versioned docs, recording where the document lives. |
|
|
146
148
|
| `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. |
|
|
147
149
|
| `kb delete <id> --confirm` | Delete a knowledge page (irreversible). |
|
|
148
150
|
| `kb ask <question…>` | Ask a question; the AI answers from the knowledge base and cites the pages it used. |
|
|
151
|
+
| `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. |
|
|
152
|
+
| `kb book show <id>` | Show a book: metadata, then the summary of the pages it collects (`#` is the 0-based position). |
|
|
153
|
+
| `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. |
|
|
149
154
|
| `servers list [--status] [--page]` | List the monitored servers (fleet snapshot: status, cpu/mem/disk, last seen). |
|
|
150
155
|
| `servers show <id>` | Show a monitored server (latest snapshot + machine details). |
|
|
151
156
|
| `servers rename <id> --name <name>` | Rename a server (display name only). |
|
|
@@ -232,6 +237,12 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
232
237
|
Every command accepts `--json` for machine-readable output and `--help` for usage details.
|
|
233
238
|
`--project` accepts either a project UUID or its key (matched case-insensitively).
|
|
234
239
|
|
|
240
|
+
`kb publish` needs a backend exposing the atomic Knowledge publication route; an older one fails with
|
|
241
|
+
`C404-KNW-001` and says to upgrade. An unknown project key fails locally with `C404-PRJ-001`; the
|
|
242
|
+
backend `R404-KNOWLEDGE-001` is reserved for a project UUID that is missing or not visible to the
|
|
243
|
+
caller. `R403-KNOWLEDGE-003` means the key already belongs to a page you cannot manage, and
|
|
244
|
+
`R409-KNOWLEDGE-001` that two untagged pages share the title the key would adopt.
|
|
245
|
+
|
|
235
246
|
When a command fails in `--json` mode it exits non-zero and prints an error envelope on stdout —
|
|
236
247
|
never a silent success:
|
|
237
248
|
|
package/dist/base.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export declare const environmentFlag: {
|
|
|
15
15
|
export declare const pageFlag: {
|
|
16
16
|
page: Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
17
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;
|
|
18
29
|
export declare abstract class BaseCommand extends Command {
|
|
19
30
|
static enableJsonFlag: boolean;
|
|
20
31
|
protected cfg: CliConfig;
|
package/dist/base.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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");
|
|
@@ -21,6 +22,20 @@ exports.environmentFlag = {
|
|
|
21
22
|
exports.pageFlag = {
|
|
22
23
|
page: core_1.Flags.integer({ description: 'Page number', default: 1 }),
|
|
23
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
|
+
}
|
|
24
39
|
class BaseCommand extends core_1.Command {
|
|
25
40
|
static enableJsonFlag = true;
|
|
26
41
|
cfg;
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -30,6 +30,11 @@ exports.ErrorCodes = {
|
|
|
30
30
|
Member: {
|
|
31
31
|
notFound: 'C404-MBR-001',
|
|
32
32
|
},
|
|
33
|
+
// Il backend risponde 404 senza codice applicativo sulla route della pubblicazione atomica:
|
|
34
|
+
// non è il progetto a mancare, è il server a essere più vecchio del comando (CYCL-8).
|
|
35
|
+
Knowledge: {
|
|
36
|
+
publishUnavailable: 'C404-KNW-001',
|
|
37
|
+
},
|
|
33
38
|
// Stato e priorità di un ticket, indicati per code o etichetta e non trovati nelle lookup
|
|
34
39
|
// dell'organizzazione (o corrispondenti a più di una voce) — CYCL-34.
|
|
35
40
|
TicketStatus: {
|
package/dist/lib/knowledge.d.ts
CHANGED
|
@@ -48,6 +48,30 @@ export declare function renderRelated(rows: RelatedRow[], heading?: string, inde
|
|
|
48
48
|
* order as the table above it. Results without related pages are simply left out.
|
|
49
49
|
*/
|
|
50
50
|
export declare function renderRelatedGroups(pages: Array<Record<string, unknown>>, grouped: Record<string, RelatedRow[]>): string;
|
|
51
|
+
/**
|
|
52
|
+
* Build the query string for `kb book list`. `project` goes out as the raw reference: the books
|
|
53
|
+
* endpoint resolves a key or a UUID by itself (and 404s outside the scope), exactly like the pages
|
|
54
|
+
* endpoint — so no extra lookup call here.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildBooksQuery(opts: {
|
|
57
|
+
page: number;
|
|
58
|
+
per?: number;
|
|
59
|
+
project?: string;
|
|
60
|
+
q?: string;
|
|
61
|
+
}): string;
|
|
62
|
+
/**
|
|
63
|
+
* Render the `kb book list` table. ID first: it is the argument of `kb book show` and of
|
|
64
|
+
* `kb book add-page`, so a table without it would force `--json` just to act on what you are
|
|
65
|
+
* already looking at.
|
|
66
|
+
*/
|
|
67
|
+
export declare function renderBooksTable(books: Array<Record<string, unknown>>): string;
|
|
68
|
+
/**
|
|
69
|
+
* Render a book with its summary: the metadata header, then the table of contents — the response
|
|
70
|
+
* both `kb book show` and `kb book add-page` get back, so the reader sees the resulting order in
|
|
71
|
+
* the same shape either way. `#` is the 0-based position, the very number `--position` takes.
|
|
72
|
+
* Page bodies are not here on purpose: the TOC is for orientation, `kb show <page>` for reading.
|
|
73
|
+
*/
|
|
74
|
+
export declare function renderBook(book: Record<string, unknown>): string;
|
|
51
75
|
/**
|
|
52
76
|
* Render the shared list/search table: TITLE, KIND, PROJECT, AUTHOR, UPDATED.
|
|
53
77
|
*
|
package/dist/lib/knowledge.js
CHANGED
|
@@ -6,6 +6,9 @@ exports.truncate = truncate;
|
|
|
6
6
|
exports.buildPagesQuery = buildPagesQuery;
|
|
7
7
|
exports.renderRelated = renderRelated;
|
|
8
8
|
exports.renderRelatedGroups = renderRelatedGroups;
|
|
9
|
+
exports.buildBooksQuery = buildBooksQuery;
|
|
10
|
+
exports.renderBooksTable = renderBooksTable;
|
|
11
|
+
exports.renderBook = renderBook;
|
|
9
12
|
exports.renderPagesTable = renderPagesTable;
|
|
10
13
|
const output_1 = require("./output");
|
|
11
14
|
/** Knowledge page kinds accepted by the backend (Knowledge::Page). */
|
|
@@ -82,6 +85,71 @@ function renderRelatedGroups(pages, grouped) {
|
|
|
82
85
|
.map(({ rows, title }) => renderRelated(rows, ` ${(0, output_1.sanitize)(title)}`, ' '));
|
|
83
86
|
return blocks.length === 0 ? '' : ['\n🔗 Related:', ...blocks].join('\n');
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the query string for `kb book list`. `project` goes out as the raw reference: the books
|
|
90
|
+
* endpoint resolves a key or a UUID by itself (and 404s outside the scope), exactly like the pages
|
|
91
|
+
* endpoint — so no extra lookup call here.
|
|
92
|
+
*/
|
|
93
|
+
function buildBooksQuery(opts) {
|
|
94
|
+
const query = new URLSearchParams();
|
|
95
|
+
query.set('page', String(opts.page));
|
|
96
|
+
if (opts.per !== undefined)
|
|
97
|
+
query.set('per', String(opts.per));
|
|
98
|
+
if (opts.project)
|
|
99
|
+
query.set('project', opts.project);
|
|
100
|
+
if (opts.q)
|
|
101
|
+
query.set('q', opts.q);
|
|
102
|
+
return query.toString();
|
|
103
|
+
}
|
|
104
|
+
/** A list of names (project keys, group names) as one cell, or undefined when there is nothing. */
|
|
105
|
+
function joinList(value) {
|
|
106
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
return value.map((entry) => String(entry)).join(', ');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Render the `kb book list` table. ID first: it is the argument of `kb book show` and of
|
|
112
|
+
* `kb book add-page`, so a table without it would force `--json` just to act on what you are
|
|
113
|
+
* already looking at.
|
|
114
|
+
*/
|
|
115
|
+
function renderBooksTable(books) {
|
|
116
|
+
return (0, output_1.renderTable)(['ID', 'TITLE', 'PAGES', 'PROJECTS', 'AUTHOR', 'UPDATED'], books.map((book) => [
|
|
117
|
+
String(book.id ?? ''),
|
|
118
|
+
truncate(String(book.title ?? '')),
|
|
119
|
+
String(book.pages_count ?? ''),
|
|
120
|
+
joinList(book.projects) ?? '',
|
|
121
|
+
String(book.author ?? ''),
|
|
122
|
+
String(book.updated_at ?? ''),
|
|
123
|
+
]));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render a book with its summary: the metadata header, then the table of contents — the response
|
|
127
|
+
* both `kb book show` and `kb book add-page` get back, so the reader sees the resulting order in
|
|
128
|
+
* the same shape either way. `#` is the 0-based position, the very number `--position` takes.
|
|
129
|
+
* Page bodies are not here on purpose: the TOC is for orientation, `kb show <page>` for reading.
|
|
130
|
+
*/
|
|
131
|
+
function renderBook(book) {
|
|
132
|
+
const pages = Array.isArray(book.pages) ? book.pages : [];
|
|
133
|
+
const header = (0, output_1.renderRecord)({
|
|
134
|
+
id: book.id,
|
|
135
|
+
title: book.title,
|
|
136
|
+
description: book.description,
|
|
137
|
+
projects: joinList(book.projects),
|
|
138
|
+
groups: joinList(book.groups),
|
|
139
|
+
author: book.author,
|
|
140
|
+
pages_count: book.pages_count,
|
|
141
|
+
created_at: book.created_at,
|
|
142
|
+
updated_at: book.updated_at,
|
|
143
|
+
});
|
|
144
|
+
const toc = (0, output_1.renderTable)(['#', 'TITLE', 'KIND', 'PROJECT', 'ID'], pages.map((page) => [
|
|
145
|
+
String(page.position ?? ''),
|
|
146
|
+
truncate(String(page.title ?? '')),
|
|
147
|
+
String(page.kind ?? ''),
|
|
148
|
+
String(page.project ?? ''),
|
|
149
|
+
String(page.id ?? ''),
|
|
150
|
+
]));
|
|
151
|
+
return [header, (0, output_1.section)('Contents', '📖'), toc].join('\n');
|
|
152
|
+
}
|
|
85
153
|
/**
|
|
86
154
|
* Render the shared list/search table: TITLE, KIND, PROJECT, AUTHOR, UPDATED.
|
|
87
155
|
*
|