@bussolabs/closeyourit-cli 0.24.1 → 0.25.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 +5 -0
- package/dist/base.d.ts +12 -0
- package/dist/base.js +47 -1
- package/dist/commands/kb/approve.d.ts +15 -0
- package/dist/commands/kb/approve.js +28 -0
- package/dist/commands/kb/reject.d.ts +14 -0
- package/dist/commands/kb/reject.js +27 -0
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +25 -3
- package/oclif.manifest.json +6113 -4024
- package/opencli.json +1535 -2
- package/package.json +106 -1
package/README.md
CHANGED
|
@@ -162,6 +162,8 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
162
162
|
| `kb related <id> [--question <q>] [--links-only]` | Pages to read next: linked with `[[wiki links]]` plus close matches by meaning. |
|
|
163
163
|
| `kb create [--project <id\|key>…] [--group <id\|name>…] [--tag <tag>…] --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>) [--tech-spec <md> \| --tech-spec-file <path>] [--in-review] [--review-note <line>] [--author-origin <name>]` | Create a knowledge page (optional technical section, kept separate from the body). `--project`, `--group` and `--tag` are repeatable — a page can belong to several projects and groups at once, and to groups alone; at least one project or group is required. 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. `--author-origin` declares the assistant or skill that wrote the text, which is not the owner of the token. |
|
|
164
164
|
| `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`. |
|
|
165
|
+
| `kb approve <id>` | Accept a page waiting for review: it enters search, answers and related panels. A human decision: a service token is refused, and a page the automatic reviewer rejected must be fixed and saved first. |
|
|
166
|
+
| `kb reject <id>` | Discard a page waiting for review: it stays archived as rejected, out of search and answers, so it is not proposed again. Nothing is deleted. |
|
|
165
167
|
| `kb consolidate <id> --path <doc-path>` | Mark an accepted page as written to the versioned docs, recording where the document lives. |
|
|
166
168
|
| `kb update <id> [--title] [--kind] [--body \| --body-file] [--tech-spec \| --tech-spec-file] [--project <id\|key>…] [--group <id\|name>…] [--tag <tag>…] [--author-origin <name>]` | Update a page (unpassed fields keep their current value — including projects, groups, tags and attachments). Passing `--project`, `--group` or `--tag` **replaces** that list with what you name; `--tag ""` clears the tags. Changing only scope or tags never rewrites the text, so a concurrent edit is not overwritten. Same length caps as `kb create`; text already over the cap is refused only if it grows. |
|
|
167
169
|
| `kb delete <id> --confirm` | Delete a knowledge page (irreversible). |
|
|
@@ -272,6 +274,9 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
272
274
|
| `workload promote <action-id> --project <id\|key> [--title] [--description] [--kind] [--status] [--priority]` | Generate a ticket from the action and link it back. |
|
|
273
275
|
|
|
274
276
|
Every command accepts `--json` for machine-readable output and `--help` for usage details.
|
|
277
|
+
Every command also accepts `--yes`: the server refuses a write that goes through a dangerous
|
|
278
|
+
permission (deleting a project, importing or deleting secrets, …) with `R422-CONFIRM-001` until you
|
|
279
|
+
confirm it, and `--yes` is that confirmation. Reads never need it.
|
|
275
280
|
`--project` accepts either a project UUID or its key (matched case-insensitively).
|
|
276
281
|
|
|
277
282
|
`kb publish` needs a backend exposing the atomic Knowledge publication route; an older one fails with
|
package/dist/base.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command, type Interfaces } from '@oclif/core';
|
|
2
|
+
import type { ArgOutput, FlagOutput, Input, ParserOutput } from '@oclif/core/lib/interfaces/parser';
|
|
2
3
|
import { type CliConfig } from './lib/config';
|
|
3
4
|
import { CliApi } from './lib/api';
|
|
4
5
|
import { type TicketLookup } from './lib/ticket-lookup';
|
|
@@ -28,9 +29,20 @@ export declare function paginationError(opts: {
|
|
|
28
29
|
}): string | undefined;
|
|
29
30
|
export declare abstract class BaseCommand extends Command {
|
|
30
31
|
static enableJsonFlag: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* `--yes` on every command (CYCL-53): the server asks an explicit confirmation for writes that pass
|
|
34
|
+
* through a dangerous permission key (`R422-CONFIRM-001`, CYRA-728). Which keys are dangerous is
|
|
35
|
+
* the server's catalogue, not ours, so the flag is global instead of being sprinkled on the
|
|
36
|
+
* commands we believe are risky today. Harmless elsewhere: the parameter is simply ignored.
|
|
37
|
+
*/
|
|
38
|
+
static baseFlags: {
|
|
39
|
+
yes: Interfaces.BooleanFlag<boolean>;
|
|
40
|
+
};
|
|
31
41
|
protected cfg: CliConfig;
|
|
32
42
|
protected api: CliApi;
|
|
33
43
|
init(): Promise<void>;
|
|
44
|
+
/** Parse as oclif does, then hand `--yes` to the api client before any request leaves. */
|
|
45
|
+
protected parse<F extends FlagOutput, B extends FlagOutput, A extends ArgOutput>(options?: Input<F, B, A>, argv?: string[]): Promise<ParserOutput<F, B, A>>;
|
|
34
46
|
/**
|
|
35
47
|
* Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive).
|
|
36
48
|
* Walks every page of `/cli/v1/projects`: a key can live past the first page, so a single-page
|
package/dist/base.js
CHANGED
|
@@ -38,6 +38,15 @@ function paginationError(opts) {
|
|
|
38
38
|
}
|
|
39
39
|
class BaseCommand extends core_1.Command {
|
|
40
40
|
static enableJsonFlag = true;
|
|
41
|
+
/**
|
|
42
|
+
* `--yes` on every command (CYCL-53): the server asks an explicit confirmation for writes that pass
|
|
43
|
+
* through a dangerous permission key (`R422-CONFIRM-001`, CYRA-728). Which keys are dangerous is
|
|
44
|
+
* the server's catalogue, not ours, so the flag is global instead of being sprinkled on the
|
|
45
|
+
* commands we believe are risky today. Harmless elsewhere: the parameter is simply ignored.
|
|
46
|
+
*/
|
|
47
|
+
static baseFlags = {
|
|
48
|
+
yes: core_1.Flags.boolean({ description: 'Confirm a dangerous action the server would otherwise refuse (R422-CONFIRM-001)', default: false }),
|
|
49
|
+
};
|
|
41
50
|
cfg;
|
|
42
51
|
api;
|
|
43
52
|
async init() {
|
|
@@ -45,6 +54,13 @@ class BaseCommand extends core_1.Command {
|
|
|
45
54
|
this.cfg = (0, config_1.loadConfig)();
|
|
46
55
|
this.api = new api_1.CliApi(this.cfg);
|
|
47
56
|
}
|
|
57
|
+
/** Parse as oclif does, then hand `--yes` to the api client before any request leaves. */
|
|
58
|
+
async parse(options, argv) {
|
|
59
|
+
const parsed = await super.parse(options, argv);
|
|
60
|
+
if (this.api)
|
|
61
|
+
this.api.confirm = parsed.flags.yes === true;
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
48
64
|
/**
|
|
49
65
|
* Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive).
|
|
50
66
|
* Walks every page of `/cli/v1/projects`: a key can live past the first page, so a single-page
|
|
@@ -191,10 +207,12 @@ class BaseCommand extends core_1.Command {
|
|
|
191
207
|
async catch(error) {
|
|
192
208
|
if (error instanceof api_1.ApiRequestError) {
|
|
193
209
|
if (this.jsonEnabled()) {
|
|
194
|
-
this.logJson({ error: { code: error.code, message: error.message } });
|
|
210
|
+
this.logJson({ error: { code: error.code, message: error.message, ...(error.details === undefined ? {} : { details: error.details }) } });
|
|
195
211
|
}
|
|
196
212
|
else {
|
|
197
213
|
this.logToStderr(`${error.code}: ${error.message}`);
|
|
214
|
+
for (const line of reviewVerdictLines(error))
|
|
215
|
+
this.logToStderr(line);
|
|
198
216
|
}
|
|
199
217
|
return this.exit(1);
|
|
200
218
|
}
|
|
@@ -237,3 +255,31 @@ class BaseCommand extends core_1.Command {
|
|
|
237
255
|
}
|
|
238
256
|
}
|
|
239
257
|
exports.BaseCommand = BaseCommand;
|
|
258
|
+
/**
|
|
259
|
+
* The automatic reviewer of knowledge pages (R422-KNOWLEDGE-013, CYRA-764) answers with the whole
|
|
260
|
+
* verdict in `details`: every rule violated, the format it recognised, a suggested title, the page it
|
|
261
|
+
* looks like a duplicate of. The message carries only the first three rules, so a human reading the
|
|
262
|
+
* terminal would otherwise have to open the site to learn what to fix.
|
|
263
|
+
*/
|
|
264
|
+
function reviewVerdictLines(error) {
|
|
265
|
+
if (error.code !== 'R422-KNOWLEDGE-013' || typeof error.details !== 'object' || error.details === null)
|
|
266
|
+
return [];
|
|
267
|
+
const details = error.details;
|
|
268
|
+
const lines = [];
|
|
269
|
+
const violations = Array.isArray(details.violations) ? details.violations : [];
|
|
270
|
+
if (violations.length > 0) {
|
|
271
|
+
lines.push('Rules violated:');
|
|
272
|
+
for (const v of violations)
|
|
273
|
+
lines.push(` ${v.code ?? '?'}${v.blocking === false ? ' (warning)' : ''}: ${v.message ?? ''}`);
|
|
274
|
+
}
|
|
275
|
+
if (details.format && details.format !== 'unknown')
|
|
276
|
+
lines.push(`Format recognised: ${details.format}`);
|
|
277
|
+
if (details.suggested_title)
|
|
278
|
+
lines.push(`Suggested title: ${details.suggested_title}`);
|
|
279
|
+
if (details.duplicate_of)
|
|
280
|
+
lines.push(`Looks like a duplicate of: ${details.duplicate_of}`);
|
|
281
|
+
if (Array.isArray(details.split_suggestion) && details.split_suggestion.length > 0) {
|
|
282
|
+
lines.push(`Split into: ${details.split_suggestion.join(' · ')}`);
|
|
283
|
+
}
|
|
284
|
+
return lines;
|
|
285
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
/**
|
|
3
|
+
* First step of the review flow: a page proposed with `kb create --in-review` enters the knowledge
|
|
4
|
+
* (search, answers, related panels). The server insists the decision comes from a person: a service
|
|
5
|
+
* token gets R403-KNOWLEDGE-005, and a page the automatic reviewer rejected must be fixed and saved
|
|
6
|
+
* first (R422-KNOWLEDGE-009).
|
|
7
|
+
*/
|
|
8
|
+
export default class KbApprove extends BaseCommand {
|
|
9
|
+
static args: {
|
|
10
|
+
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
static description: string;
|
|
13
|
+
static examples: string[];
|
|
14
|
+
run(): Promise<unknown>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
* First step of the review flow: a page proposed with `kb create --in-review` enters the knowledge
|
|
8
|
+
* (search, answers, related panels). The server insists the decision comes from a person: a service
|
|
9
|
+
* token gets R403-KNOWLEDGE-005, and a page the automatic reviewer rejected must be fixed and saved
|
|
10
|
+
* first (R422-KNOWLEDGE-009).
|
|
11
|
+
*/
|
|
12
|
+
class KbApprove extends base_1.BaseCommand {
|
|
13
|
+
static args = {
|
|
14
|
+
id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
|
|
15
|
+
};
|
|
16
|
+
static description = 'Accept a page waiting for review: it enters search, answers and related panels';
|
|
17
|
+
static examples = ['<%= config.bin %> kb approve <page-id>', '<%= config.bin %> kb approve <page-id> --json'];
|
|
18
|
+
async run() {
|
|
19
|
+
const { args } = await this.parse(KbApprove);
|
|
20
|
+
const res = await this.api.post(`/cli/v1/knowledge/pages/${args.id}/approve`);
|
|
21
|
+
if (!this.jsonEnabled()) {
|
|
22
|
+
this.log('Knowledge page accepted:');
|
|
23
|
+
this.log((0, output_1.renderRecord)(res.data ?? {}));
|
|
24
|
+
}
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.default = KbApprove;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
/**
|
|
3
|
+
* Mirror of `kb approve`: the proposal stays archived as rejected — out of search, answers, related
|
|
4
|
+
* panels and lists — so whoever proposes can see it when hunting duplicates and not propose it again.
|
|
5
|
+
* Nothing is deleted (that is `kb delete`). A human decision, like approve.
|
|
6
|
+
*/
|
|
7
|
+
export default class KbReject extends BaseCommand {
|
|
8
|
+
static args: {
|
|
9
|
+
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static description: string;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
run(): Promise<unknown>;
|
|
14
|
+
}
|
|
@@ -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
|
+
const output_1 = require("../../lib/output");
|
|
6
|
+
/**
|
|
7
|
+
* Mirror of `kb approve`: the proposal stays archived as rejected — out of search, answers, related
|
|
8
|
+
* panels and lists — so whoever proposes can see it when hunting duplicates and not propose it again.
|
|
9
|
+
* Nothing is deleted (that is `kb delete`). A human decision, like approve.
|
|
10
|
+
*/
|
|
11
|
+
class KbReject extends base_1.BaseCommand {
|
|
12
|
+
static args = {
|
|
13
|
+
id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
|
|
14
|
+
};
|
|
15
|
+
static description = 'Discard a page waiting for review: it stays archived as rejected, out of search and answers';
|
|
16
|
+
static examples = ['<%= config.bin %> kb reject <page-id>', '<%= config.bin %> kb reject <page-id> --json'];
|
|
17
|
+
async run() {
|
|
18
|
+
const { args } = await this.parse(KbReject);
|
|
19
|
+
const res = await this.api.post(`/cli/v1/knowledge/pages/${args.id}/reject`);
|
|
20
|
+
if (!this.jsonEnabled()) {
|
|
21
|
+
this.log('Knowledge page rejected:');
|
|
22
|
+
this.log((0, output_1.renderRecord)(res.data ?? {}));
|
|
23
|
+
}
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.default = KbReject;
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface UploadFile {
|
|
|
23
23
|
data: Blob;
|
|
24
24
|
}
|
|
25
25
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
26
|
+
/** Error code the server answers when a dangerous write arrives without the confirm gesture (CYRA-728). */
|
|
27
|
+
export declare const CONFIRMATION_REQUIRED_CODE = "R422-CONFIRM-001";
|
|
26
28
|
export declare class CliApi {
|
|
27
29
|
private readonly config;
|
|
28
30
|
/**
|
|
@@ -32,6 +34,14 @@ export declare class CliApi {
|
|
|
32
34
|
* commands (CYCL-11). The instance is per-command, so this is that command's last response.
|
|
33
35
|
*/
|
|
34
36
|
lastMeta?: Record<string, unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* The user confirmed dangerous actions (`--yes`, CYCL-53). The server (CYRA-728) refuses any
|
|
39
|
+
* write through a `dangerous` permission key without the `confirm` parameter and answers
|
|
40
|
+
* `R422-CONFIRM-001`: not a permission problem, a missing gesture. `BaseCommand.parse` sets it;
|
|
41
|
+
* `request()` then adds `confirm=1` to every writing call — in the query string, so DELETE
|
|
42
|
+
* without a body is covered too. Reads never carry it: looking is not executing.
|
|
43
|
+
*/
|
|
44
|
+
confirm: boolean;
|
|
35
45
|
constructor(config: CliConfig);
|
|
36
46
|
request<T = unknown>(method: HttpMethod, path: string, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
37
47
|
/**
|
|
@@ -39,6 +49,8 @@ export declare class CliApi {
|
|
|
39
49
|
* The boundary Content-Type is derived by fetch from the FormData body — never set it by hand.
|
|
40
50
|
*/
|
|
41
51
|
upload<T = unknown>(method: 'POST' | 'PUT', path: string, files: UploadFile[], opts?: RequestOptions): Promise<Envelope<T>>;
|
|
52
|
+
/** Add `confirm=1` to a writing request when the user passed `--yes`; reads are left alone. */
|
|
53
|
+
private withConfirmation;
|
|
42
54
|
private resolveUrl;
|
|
43
55
|
/** Build the shared Accept + Authorization headers; throws (before any fetch) if an authed call has no token. */
|
|
44
56
|
private authHeaders;
|
package/dist/lib/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CliApi = exports.ApiRequestError = void 0;
|
|
3
|
+
exports.CliApi = exports.CONFIRMATION_REQUIRED_CODE = exports.ApiRequestError = void 0;
|
|
4
4
|
const error_codes_1 = require("../errors/error-codes");
|
|
5
5
|
/** Error carrying the HTTP status and the structured code from the backend envelope (R/G prefix) or a local C-prefix fallback. */
|
|
6
6
|
class ApiRequestError extends Error {
|
|
@@ -16,6 +16,9 @@ class ApiRequestError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
exports.ApiRequestError = ApiRequestError;
|
|
19
|
+
/** Error code the server answers when a dangerous write arrives without the confirm gesture (CYRA-728). */
|
|
20
|
+
exports.CONFIRMATION_REQUIRED_CODE = 'R422-CONFIRM-001';
|
|
21
|
+
const READ_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
19
22
|
class CliApi {
|
|
20
23
|
config;
|
|
21
24
|
/**
|
|
@@ -25,13 +28,21 @@ class CliApi {
|
|
|
25
28
|
* commands (CYCL-11). The instance is per-command, so this is that command's last response.
|
|
26
29
|
*/
|
|
27
30
|
lastMeta;
|
|
31
|
+
/**
|
|
32
|
+
* The user confirmed dangerous actions (`--yes`, CYCL-53). The server (CYRA-728) refuses any
|
|
33
|
+
* write through a `dangerous` permission key without the `confirm` parameter and answers
|
|
34
|
+
* `R422-CONFIRM-001`: not a permission problem, a missing gesture. `BaseCommand.parse` sets it;
|
|
35
|
+
* `request()` then adds `confirm=1` to every writing call — in the query string, so DELETE
|
|
36
|
+
* without a body is covered too. Reads never carry it: looking is not executing.
|
|
37
|
+
*/
|
|
38
|
+
confirm = false;
|
|
28
39
|
constructor(config) {
|
|
29
40
|
this.config = config;
|
|
30
41
|
}
|
|
31
42
|
async request(method, path, opts = {}) {
|
|
32
43
|
const headers = this.authHeaders(opts.auth !== false);
|
|
33
44
|
headers['Content-Type'] = 'application/json';
|
|
34
|
-
const url = this.resolveUrl(path);
|
|
45
|
+
const url = this.resolveUrl(this.withConfirmation(method, path));
|
|
35
46
|
let res;
|
|
36
47
|
try {
|
|
37
48
|
res = await fetch(url, {
|
|
@@ -70,6 +81,12 @@ class CliApi {
|
|
|
70
81
|
}
|
|
71
82
|
return this.handleResponse(res);
|
|
72
83
|
}
|
|
84
|
+
/** Add `confirm=1` to a writing request when the user passed `--yes`; reads are left alone. */
|
|
85
|
+
withConfirmation(method, path) {
|
|
86
|
+
if (!this.confirm || READ_METHODS.has(method))
|
|
87
|
+
return path;
|
|
88
|
+
return `${path}${path.includes('?') ? '&' : '?'}confirm=1`;
|
|
89
|
+
}
|
|
73
90
|
resolveUrl(path) {
|
|
74
91
|
return this.config.apiUrl.replace(/\/+$/, '') + path;
|
|
75
92
|
}
|
|
@@ -102,7 +119,12 @@ class CliApi {
|
|
|
102
119
|
if (!res.ok) {
|
|
103
120
|
const envelope = (json ?? {});
|
|
104
121
|
const error = envelope.error ?? {};
|
|
105
|
-
|
|
122
|
+
let message = error.message ?? res.statusText ?? `Request failed (${res.status})`;
|
|
123
|
+
// The server names the gesture it wants but not the flag that makes it: say it here, once.
|
|
124
|
+
if (error.code === exports.CONFIRMATION_REQUIRED_CODE && !message.includes('--yes')) {
|
|
125
|
+
message = `${message} Re-run with --yes to confirm this dangerous action.`;
|
|
126
|
+
}
|
|
127
|
+
throw new ApiRequestError(res.status, error.code ?? `C${res.status}-API-000`, message, error.details);
|
|
106
128
|
}
|
|
107
129
|
const envelope = (json ?? {});
|
|
108
130
|
this.lastMeta = envelope.meta;
|