@bussolabs/closeyourit-cli 0.22.0 → 0.24.1
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 +44 -3
- package/dist/base.d.ts +23 -0
- package/dist/base.js +64 -4
- package/dist/commands/assistant/ask.d.ts +24 -0
- package/dist/commands/assistant/ask.js +112 -0
- package/dist/commands/assistant/conversations/create.d.ts +9 -0
- package/dist/commands/assistant/conversations/create.js +25 -0
- package/dist/commands/assistant/conversations/delete.d.ts +12 -0
- package/dist/commands/assistant/conversations/delete.js +25 -0
- package/dist/commands/assistant/conversations/list.d.ts +10 -0
- package/dist/commands/assistant/conversations/list.js +30 -0
- package/dist/commands/assistant/conversations/show.d.ts +9 -0
- package/dist/commands/assistant/conversations/show.js +25 -0
- package/dist/commands/coverage/show.d.ts +10 -0
- package/dist/commands/coverage/show.js +39 -0
- package/dist/commands/errors/assign.d.ts +14 -0
- package/dist/commands/errors/assign.js +52 -0
- package/dist/commands/errors/bulk-triage.d.ts +11 -0
- package/dist/commands/errors/bulk-triage.js +41 -0
- package/dist/commands/errors/delete.d.ts +13 -0
- package/dist/commands/errors/delete.js +28 -0
- package/dist/commands/errors/grouping-rules/create.d.ts +15 -0
- package/dist/commands/errors/grouping-rules/create.js +44 -0
- package/dist/commands/errors/grouping-rules/delete.d.ts +13 -0
- package/dist/commands/errors/grouping-rules/delete.js +27 -0
- package/dist/commands/errors/grouping-rules/list.d.ts +10 -0
- package/dist/commands/errors/grouping-rules/list.js +28 -0
- package/dist/commands/errors/grouping-rules/update.d.ts +18 -0
- package/dist/commands/errors/grouping-rules/update.js +43 -0
- package/dist/commands/errors/merge.d.ts +14 -0
- package/dist/commands/errors/merge.js +39 -0
- package/dist/commands/errors/split.d.ts +13 -0
- package/dist/commands/errors/split.js +34 -0
- package/dist/commands/invitations/create.js +9 -2
- package/dist/commands/invitations/resend.js +8 -2
- package/dist/commands/kb/attach.d.ts +12 -0
- package/dist/commands/kb/attach.js +49 -0
- package/dist/commands/kb/attachment-download.d.ts +14 -0
- package/dist/commands/kb/attachment-download.js +38 -0
- package/dist/commands/kb/attachments.d.ts +9 -0
- package/dist/commands/kb/attachments.js +36 -0
- package/dist/commands/kb/create.d.ts +4 -1
- package/dist/commands/kb/create.js +41 -5
- package/dist/commands/kb/detach.d.ts +13 -0
- package/dist/commands/kb/detach.js +26 -0
- package/dist/commands/kb/show.js +3 -0
- package/dist/commands/kb/update.d.ts +4 -0
- package/dist/commands/kb/update.js +69 -21
- package/dist/commands/metrics/bulk-triage.d.ts +11 -0
- package/dist/commands/metrics/bulk-triage.js +44 -0
- package/dist/commands/service-accounts/show.d.ts +9 -0
- package/dist/commands/service-accounts/show.js +26 -0
- package/dist/commands/service-accounts/tokens/list.d.ts +12 -0
- package/dist/commands/service-accounts/tokens/list.js +40 -0
- package/dist/commands/service-accounts/update.d.ts +31 -0
- package/dist/commands/service-accounts/update.js +105 -0
- package/dist/commands/shared/delete.d.ts +12 -0
- package/dist/commands/shared/delete.js +30 -0
- package/dist/commands/shared/list.d.ts +6 -0
- package/dist/commands/shared/list.js +45 -0
- package/dist/commands/shared/set.d.ts +14 -0
- package/dist/commands/shared/set.js +41 -0
- package/dist/commands/tickets/ask.d.ts +13 -0
- package/dist/commands/tickets/ask.js +81 -0
- package/dist/commands/tickets/clarifications.d.ts +21 -0
- package/dist/commands/tickets/clarifications.js +36 -0
- package/dist/commands/tickets/dependencies/add.d.ts +15 -0
- package/dist/commands/tickets/dependencies/add.js +62 -0
- package/dist/commands/tickets/dependencies/list.d.ts +12 -0
- package/dist/commands/tickets/dependencies/list.js +37 -0
- package/dist/commands/tickets/dependencies/remove.d.ts +14 -0
- package/dist/commands/tickets/dependencies/remove.js +59 -0
- package/dist/commands/tickets/work-context.d.ts +15 -0
- package/dist/commands/tickets/work-context.js +130 -0
- package/dist/commands/usage/list.d.ts +12 -0
- package/dist/commands/usage/list.js +43 -0
- package/dist/commands/usage/reporters.d.ts +10 -0
- package/dist/commands/usage/reporters.js +37 -0
- package/dist/commands/vault/requests/approve.d.ts +9 -0
- package/dist/commands/vault/requests/approve.js +40 -0
- package/dist/commands/vault/requests/list.d.ts +6 -0
- package/dist/commands/vault/requests/list.js +29 -0
- package/dist/commands/vault/requests/reject.d.ts +12 -0
- package/dist/commands/vault/requests/reject.js +52 -0
- package/dist/commands/workload/create.d.ts +15 -0
- package/dist/commands/workload/create.js +45 -0
- package/dist/commands/workload/delete.d.ts +12 -0
- package/dist/commands/workload/delete.js +26 -0
- package/dist/commands/workload/list.d.ts +9 -0
- package/dist/commands/workload/list.js +29 -0
- package/dist/commands/workload/participant/add.d.ts +10 -0
- package/dist/commands/workload/participant/add.js +27 -0
- package/dist/commands/workload/participant/remove.d.ts +10 -0
- package/dist/commands/workload/participant/remove.js +23 -0
- package/dist/commands/workload/promote.d.ts +17 -0
- package/dist/commands/workload/promote.js +58 -0
- package/dist/commands/workload/show.d.ts +9 -0
- package/dist/commands/workload/show.js +24 -0
- package/dist/commands/workload/update.d.ts +17 -0
- package/dist/commands/workload/update.js +45 -0
- package/dist/errors/error-codes.d.ts +17 -0
- package/dist/errors/error-codes.js +36 -0
- package/dist/lib/api.d.ts +7 -1
- package/dist/lib/api.js +8 -0
- package/dist/lib/assistant.d.ts +29 -0
- package/dist/lib/assistant.js +76 -0
- package/dist/lib/bulk-triage.d.ts +35 -0
- package/dist/lib/bulk-triage.js +55 -0
- package/dist/lib/grouping-rules.d.ts +28 -0
- package/dist/lib/grouping-rules.js +39 -0
- package/dist/lib/knowledge.d.ts +15 -0
- package/dist/lib/knowledge.js +36 -0
- package/dist/lib/output.js +10 -0
- package/dist/lib/poll.d.ts +51 -0
- package/dist/lib/poll.js +59 -0
- package/dist/lib/service-accounts.d.ts +65 -0
- package/dist/lib/service-accounts.js +132 -0
- package/dist/lib/ticket-clarifications.d.ts +13 -0
- package/dist/lib/ticket-clarifications.js +31 -0
- package/dist/lib/vault-requests.d.ts +38 -0
- package/dist/lib/vault-requests.js +125 -0
- package/dist/lib/workload.d.ts +37 -0
- package/dist/lib/workload.js +64 -0
- package/oclif.manifest.json +7495 -4994
- package/opencli.json +7424 -5644
- package/package.json +27 -3
|
@@ -0,0 +1,58 @@
|
|
|
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 limits_1 = require("../../lib/limits");
|
|
6
|
+
const output_1 = require("../../lib/output");
|
|
7
|
+
const ticket_lookup_1 = require("../../lib/ticket-lookup");
|
|
8
|
+
const workload_1 = require("../../lib/workload");
|
|
9
|
+
class WorkloadPromote extends base_1.BaseCommand {
|
|
10
|
+
static args = {
|
|
11
|
+
action: core_1.Args.string({ description: 'Workload action id', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static description = 'Generate a ticket from a workload action and link it back (the action keeps existing; an already linked action is refused)';
|
|
14
|
+
static examples = [
|
|
15
|
+
'<%= config.bin %> workload promote <action-id> --project acme-api',
|
|
16
|
+
'<%= config.bin %> workload promote <action-id> -p acme-api --title "Implement the partner API" --description "Agreed in the meeting" --kind story --priority High',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...base_1.projectFlag,
|
|
20
|
+
title: core_1.Flags.string({
|
|
21
|
+
description: `Ticket title (default: the action title, max ${limits_1.LENGTH_LIMITS.title} characters)`,
|
|
22
|
+
}),
|
|
23
|
+
description: core_1.Flags.string({ description: `Ticket description (max ${limits_1.LENGTH_LIMITS.description} characters)` }),
|
|
24
|
+
kind: core_1.Flags.string({ description: 'Ticket kind (default: task)', options: ['bug', 'story', 'task', 'epic'] }),
|
|
25
|
+
...ticket_lookup_1.newTicketLookupFlags,
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { args, flags } = await this.parse(WorkloadPromote);
|
|
29
|
+
// Il ticket nasce qui: valgono i tetti di `tickets create`, e si leggono tutti in un rifiuto
|
|
30
|
+
// solo, prima di qualunque chiamata (CYCL-32).
|
|
31
|
+
const tooLong = (0, limits_1.tooLongMessage)([
|
|
32
|
+
{ flag: '--title', value: flags.title, max: limits_1.LENGTH_LIMITS.title, orthography: true },
|
|
33
|
+
{ flag: '--description', value: flags.description, max: limits_1.LENGTH_LIMITS.description, orthography: true },
|
|
34
|
+
]);
|
|
35
|
+
if (tooLong)
|
|
36
|
+
this.error(tooLong, { exit: 2 });
|
|
37
|
+
const projectId = await this.resolveProjectId(flags.project);
|
|
38
|
+
const body = { project_id: projectId };
|
|
39
|
+
if (flags.title !== undefined)
|
|
40
|
+
body.title = flags.title;
|
|
41
|
+
if (flags.description !== undefined)
|
|
42
|
+
body.description = flags.description;
|
|
43
|
+
if (flags.kind !== undefined)
|
|
44
|
+
body.kind = flags.kind;
|
|
45
|
+
if (flags.status !== undefined)
|
|
46
|
+
body.status_id = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_STATUS, flags.status);
|
|
47
|
+
if (flags.priority !== undefined) {
|
|
48
|
+
body.priority_id = await this.resolveTicketLookupId(ticket_lookup_1.TICKET_PRIORITY, flags.priority);
|
|
49
|
+
}
|
|
50
|
+
const res = await this.api.post(`${workload_1.WORKLOAD_ACTIONS_PATH}/${encodeURIComponent(args.action)}/promotion`, body);
|
|
51
|
+
if (!this.jsonEnabled()) {
|
|
52
|
+
this.log('Ticket created from the workload action:');
|
|
53
|
+
this.log((0, output_1.renderRecord)(res.data ?? {}));
|
|
54
|
+
}
|
|
55
|
+
return res;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.default = WorkloadPromote;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
export default class WorkloadShow extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<unknown>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
const workload_1 = require("../../lib/workload");
|
|
7
|
+
class WorkloadShow extends base_1.BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
id: core_1.Args.string({ description: 'Workload action id', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Show one workload action (an action of another team answers 404, never 403)';
|
|
12
|
+
static examples = ['<%= config.bin %> workload show <action-id>'];
|
|
13
|
+
async run() {
|
|
14
|
+
const { args } = await this.parse(WorkloadShow);
|
|
15
|
+
const res = await this.api.get(`${workload_1.WORKLOAD_ACTIONS_PATH}/${encodeURIComponent(args.id)}`);
|
|
16
|
+
if (!this.jsonEnabled()) {
|
|
17
|
+
const data = res.data ?? {};
|
|
18
|
+
this.log((0, output_1.renderRecord)(data));
|
|
19
|
+
this.log((0, workload_1.participantsLine)(data));
|
|
20
|
+
}
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.default = WorkloadShow;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
export default class WorkloadUpdate extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
|
+
status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
|
+
'scheduled-at': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
'due-at': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
ticket: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
title: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<unknown>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
const workload_1 = require("../../lib/workload");
|
|
7
|
+
class WorkloadUpdate extends base_1.BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
id: core_1.Args.string({ description: 'Workload action id', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Update a workload action (only the passed fields change). The team never changes: moving an action would change who can see it.';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> workload update <action-id> --status done',
|
|
14
|
+
'<%= config.bin %> workload update <action-id> --title "Fiera 2027" --due-at 2027-09-10T18:00:00Z',
|
|
15
|
+
'<%= config.bin %> workload update <action-id> --ticket ""',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
title: core_1.Flags.string({ description: 'New title' }),
|
|
19
|
+
...workload_1.actionFieldFlags,
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(WorkloadUpdate);
|
|
23
|
+
const body = {};
|
|
24
|
+
if (flags.title !== undefined)
|
|
25
|
+
body.title = flags.title;
|
|
26
|
+
(0, workload_1.applyActionBody)(body, flags);
|
|
27
|
+
const ticketId = flags.ticket ? await this.resolveTicketId(flags.ticket) : undefined;
|
|
28
|
+
if (flags.ticket !== undefined)
|
|
29
|
+
body.ticket_id = ticketId ?? null;
|
|
30
|
+
const res = await this.api.put(`${workload_1.WORKLOAD_ACTIONS_PATH}/${encodeURIComponent(args.id)}`, body);
|
|
31
|
+
const data = res.data ?? {};
|
|
32
|
+
// Su una modifica il rifiuto silenzioso del server non lascia le cose come stavano: azzera il
|
|
33
|
+
// campo, quindi porta via anche il ticket che era collegato prima. Dirlo per intero.
|
|
34
|
+
if ((0, workload_1.ticketLinkMissing)(flags.ticket, ticketId, data)) {
|
|
35
|
+
this.warn(`Ticket ${flags.ticket} is not visible to you: the action now has no linked ticket at all, not even the one it had before.`);
|
|
36
|
+
}
|
|
37
|
+
if (!this.jsonEnabled()) {
|
|
38
|
+
this.log('Workload action updated:');
|
|
39
|
+
this.log((0, output_1.renderRecord)(data));
|
|
40
|
+
this.log((0, workload_1.participantsLine)(data));
|
|
41
|
+
}
|
|
42
|
+
return res;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.default = WorkloadUpdate;
|
|
@@ -22,9 +22,22 @@ export declare const ErrorCodes: {
|
|
|
22
22
|
readonly Team: {
|
|
23
23
|
readonly notFound: "C404-TEM-001";
|
|
24
24
|
};
|
|
25
|
+
readonly ErrorGroup: {
|
|
26
|
+
readonly notFound: "C404-ERG-001";
|
|
27
|
+
};
|
|
28
|
+
readonly MetricGroup: {
|
|
29
|
+
readonly notFound: "C404-MTG-001";
|
|
30
|
+
};
|
|
25
31
|
readonly Member: {
|
|
26
32
|
readonly notFound: "C404-MBR-001";
|
|
27
33
|
};
|
|
34
|
+
readonly ServiceAccount: {
|
|
35
|
+
readonly boundaryMismatch: "C404-SVA-001";
|
|
36
|
+
readonly restrictionNotApplied: "C500-SVA-002";
|
|
37
|
+
};
|
|
38
|
+
readonly Ticket: {
|
|
39
|
+
readonly notFound: "C404-TKT-001";
|
|
40
|
+
};
|
|
28
41
|
readonly Knowledge: {
|
|
29
42
|
readonly publishUnavailable: "C404-KNW-001";
|
|
30
43
|
};
|
|
@@ -38,6 +51,10 @@ export declare const ErrorCodes: {
|
|
|
38
51
|
readonly expired: "C408-DEV-001";
|
|
39
52
|
readonly failed: "C400-DEV-002";
|
|
40
53
|
};
|
|
54
|
+
readonly Ask: {
|
|
55
|
+
readonly timeout: "C408-ASK-001";
|
|
56
|
+
readonly failed: "C502-ASK-002";
|
|
57
|
+
};
|
|
41
58
|
readonly Input: {
|
|
42
59
|
readonly invalid: "C400-INP-001";
|
|
43
60
|
};
|
|
@@ -27,9 +27,37 @@ exports.ErrorCodes = {
|
|
|
27
27
|
Team: {
|
|
28
28
|
notFound: 'C404-TEM-001',
|
|
29
29
|
},
|
|
30
|
+
// Gruppi di monitoring chiesti a un'operazione in massa e non ritrovati nella risposta. Il server
|
|
31
|
+
// filtra la selezione sul progetto e risponde 200 su ciò che resta (`scope.where(id:)`), quindi
|
|
32
|
+
// un id di un altro progetto — o di un dato ormai vecchio — sparisce senza una parola: chi ha
|
|
33
|
+
// chiesto di chiudere trenta gruppi ne vedrebbe chiusi ventotto e nessuno glielo direbbe (CYCL-44).
|
|
34
|
+
ErrorGroup: {
|
|
35
|
+
notFound: 'C404-ERG-001',
|
|
36
|
+
},
|
|
37
|
+
MetricGroup: {
|
|
38
|
+
notFound: 'C404-MTG-001',
|
|
39
|
+
},
|
|
30
40
|
Member: {
|
|
31
41
|
notFound: 'C404-MBR-001',
|
|
32
42
|
},
|
|
43
|
+
// Il confine ambienti di un account di servizio, chiesto e non applicato (CYCL-46). Il server
|
|
44
|
+
// interseca i code ricevuti con quelli dell'organizzazione e risponde 200 su ciò che resta: un
|
|
45
|
+
// refuso non restringe niente, CANCELLA il confine — e una allow-list vuota vuol dire «ogni
|
|
46
|
+
// ambiente», cioè l'opposto di quello che si stava chiedendo, senza una parola. Il comando
|
|
47
|
+
// controlla i code prima di scrivere, ma leggere gli ambienti chiede un permesso che può mancare:
|
|
48
|
+
// il confronto fra chiesto e applicato resta la rete sotto, e vale in entrambi i versi (un code
|
|
49
|
+
// sparito, o uno comparso che nessuno aveva chiesto).
|
|
50
|
+
ServiceAccount: {
|
|
51
|
+
boundaryMismatch: 'C404-SVA-001',
|
|
52
|
+
restrictionNotApplied: 'C500-SVA-002',
|
|
53
|
+
},
|
|
54
|
+
// Un ticket indicato per collegarlo altrove (una attività del carico di lavoro) e non trovato: il
|
|
55
|
+
// riferimento non è né un id né un code leggibile ("DRFL-3"), oppure quel code non esiste tra i
|
|
56
|
+
// ticket visibili. Il server, ricevendo un id che non si può vedere, salverebbe «nessun ticket»
|
|
57
|
+
// senza dire niente (CYCL-42).
|
|
58
|
+
Ticket: {
|
|
59
|
+
notFound: 'C404-TKT-001',
|
|
60
|
+
},
|
|
33
61
|
// Il backend risponde 404 senza codice applicativo sulla route della pubblicazione atomica:
|
|
34
62
|
// non è il progetto a mancare, è il server a essere più vecchio del comando (CYCL-8).
|
|
35
63
|
Knowledge: {
|
|
@@ -47,6 +75,14 @@ exports.ErrorCodes = {
|
|
|
47
75
|
expired: 'C408-DEV-001',
|
|
48
76
|
failed: 'C400-DEV-002',
|
|
49
77
|
},
|
|
78
|
+
// Attesa di una risposta preparata altrove (l'assistente, «chiedi ai ticket»). `timeout` non è un
|
|
79
|
+
// guasto del server: la domanda è partita e la risposta sta ancora arrivando, è il comando che ha
|
|
80
|
+
// smesso di aspettare — il messaggio dice dove ritrovarla. `failed` copre il caso in cui il
|
|
81
|
+
// server dichiara fallita la risposta senza allegare un codice proprio (CYCL-43).
|
|
82
|
+
Ask: {
|
|
83
|
+
timeout: 'C408-ASK-001',
|
|
84
|
+
failed: 'C502-ASK-002',
|
|
85
|
+
},
|
|
50
86
|
// The command line itself was rejected (missing/unknown flag or arg, value outside the allowed
|
|
51
87
|
// options, a `this.error(...)` guard): the message says what to fix, and it never reached the
|
|
52
88
|
// server — the opposite of System.unexpected, which stays for real failures (CYCL-33).
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface UploadFile {
|
|
|
22
22
|
filename: string;
|
|
23
23
|
data: Blob;
|
|
24
24
|
}
|
|
25
|
-
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
25
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
26
26
|
export declare class CliApi {
|
|
27
27
|
private readonly config;
|
|
28
28
|
/**
|
|
@@ -47,6 +47,12 @@ export declare class CliApi {
|
|
|
47
47
|
get<T = unknown>(path: string, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
48
48
|
post<T = unknown>(path: string, body?: unknown, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
49
49
|
put<T = unknown>(path: string, body?: unknown, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
50
|
+
/**
|
|
51
|
+
* PATCH. Rails routes a resourceful `update` under both PUT and PATCH, so most commands here use
|
|
52
|
+
* `put`; a custom member route declared with `patch` (error group assignment, CYCL-44) answers to
|
|
53
|
+
* PATCH alone, and sending PUT to it is a plain 404.
|
|
54
|
+
*/
|
|
55
|
+
patch<T = unknown>(path: string, body?: unknown, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
50
56
|
delete<T = unknown>(path: string, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
51
57
|
/**
|
|
52
58
|
* Download a binary payload (e.g. a ticket attachment). The endpoint redirects to a signed
|
package/dist/lib/api.js
CHANGED
|
@@ -117,6 +117,14 @@ class CliApi {
|
|
|
117
117
|
put(path, body, opts) {
|
|
118
118
|
return this.request('PUT', path, { ...opts, body });
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* PATCH. Rails routes a resourceful `update` under both PUT and PATCH, so most commands here use
|
|
122
|
+
* `put`; a custom member route declared with `patch` (error group assignment, CYCL-44) answers to
|
|
123
|
+
* PATCH alone, and sending PUT to it is a plain 404.
|
|
124
|
+
*/
|
|
125
|
+
patch(path, body, opts) {
|
|
126
|
+
return this.request('PATCH', path, { ...opts, body });
|
|
127
|
+
}
|
|
120
128
|
delete(path, opts) {
|
|
121
129
|
return this.request('DELETE', path, opts);
|
|
122
130
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A message of an assistant conversation, as the backend serializes it (AssistantMessageSerializer). */
|
|
2
|
+
export interface AssistantMessage extends Record<string, unknown> {
|
|
3
|
+
id?: string;
|
|
4
|
+
role?: string;
|
|
5
|
+
/** `streaming` = still being written; `complete`/`failed` = final. */
|
|
6
|
+
status?: string;
|
|
7
|
+
content?: string | null;
|
|
8
|
+
tools_used?: unknown;
|
|
9
|
+
error_code?: string | null;
|
|
10
|
+
created_at?: string;
|
|
11
|
+
}
|
|
12
|
+
/** `{code, message}` for a failed reply: the server code when there is one, ours when there is not. */
|
|
13
|
+
export declare function assistantFailure(errorCode?: string | null): {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The `🧰 Tools used:` line, or '' when the reply used none. What the assistant read is what makes
|
|
19
|
+
* the answer checkable, so it is printed next to the answer and not hidden behind `--json`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderToolsUsed(tools: unknown, prefix?: string): string;
|
|
22
|
+
/** Every message of a conversation, oldest first (the order the backend serializes them in). */
|
|
23
|
+
export declare function renderMessages(messages: unknown): string;
|
|
24
|
+
/**
|
|
25
|
+
* The conversation list. ID first: it is the argument of `assistant conversations show|delete` and
|
|
26
|
+
* the value of `assistant ask --conversation`, so a table without it would force `--json` just to
|
|
27
|
+
* carry on the conversation you are looking at.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderConversationsTable(rows: Array<Record<string, unknown>>): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assistantFailure = assistantFailure;
|
|
4
|
+
exports.renderToolsUsed = renderToolsUsed;
|
|
5
|
+
exports.renderMessages = renderMessages;
|
|
6
|
+
exports.renderConversationsTable = renderConversationsTable;
|
|
7
|
+
const error_codes_1 = require("../errors/error-codes");
|
|
8
|
+
const output_1 = require("./output");
|
|
9
|
+
/**
|
|
10
|
+
* Why a reply failed, said in a way that tells the reader what to do next.
|
|
11
|
+
*
|
|
12
|
+
* The backend ships only the code (`Assistant::Message#error_kind` does the same reading on the web
|
|
13
|
+
* side), and the three cases ask for three different reactions: nothing will change until someone
|
|
14
|
+
* connects the AI service, or the question itself must be rewritten, or it is worth trying again in
|
|
15
|
+
* a minute. Collapsing them into one "the assistant failed" would send people to retry forever
|
|
16
|
+
* something that cannot succeed.
|
|
17
|
+
*/
|
|
18
|
+
const FAILURE_MESSAGES = {
|
|
19
|
+
// Integrations::Providers::NOT_CONNECTED_CODE
|
|
20
|
+
'R503-INTEGRATION-006': 'The AI service is not connected for your organization: connect it from the web app, then ask again.',
|
|
21
|
+
// Assistant::Message::NO_ANSWER_ERROR_CODES
|
|
22
|
+
'R502-GEMINI-004': 'The assistant did not produce an answer for this question — try rephrasing it.',
|
|
23
|
+
};
|
|
24
|
+
const FAILURE_FALLBACK = 'The assistant could not answer (temporary problem upstream); try again in a moment.';
|
|
25
|
+
/** `{code, message}` for a failed reply: the server code when there is one, ours when there is not. */
|
|
26
|
+
function assistantFailure(errorCode) {
|
|
27
|
+
const code = errorCode ?? undefined;
|
|
28
|
+
return {
|
|
29
|
+
code: code ?? error_codes_1.ErrorCodes.Ask.failed,
|
|
30
|
+
message: (code ? FAILURE_MESSAGES[code] : undefined) ?? FAILURE_FALLBACK,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The `🧰 Tools used:` line, or '' when the reply used none. What the assistant read is what makes
|
|
35
|
+
* the answer checkable, so it is printed next to the answer and not hidden behind `--json`.
|
|
36
|
+
*/
|
|
37
|
+
function renderToolsUsed(tools, prefix = '') {
|
|
38
|
+
const names = Array.isArray(tools) ? tools.map((tool) => (0, output_1.sanitize)(tool)).filter((tool) => tool.trim() !== '') : [];
|
|
39
|
+
return names.length === 0 ? '' : `${prefix}🧰 Tools used: ${names.join(', ')}`;
|
|
40
|
+
}
|
|
41
|
+
/** One message: who spoke and when, the text, the reason when it failed, the tools it used. */
|
|
42
|
+
function renderMessage(message) {
|
|
43
|
+
const when = (0, output_1.sanitize)(message.created_at ?? '');
|
|
44
|
+
const lines = [`${(0, output_1.statusDot)(message.status)} [${(0, output_1.sanitize)(message.role ?? '-')}] ${when}`.trimEnd()];
|
|
45
|
+
const content = String(message.content ?? '').trim();
|
|
46
|
+
if (content !== '')
|
|
47
|
+
lines.push((0, output_1.sanitizeMultiline)(message.content));
|
|
48
|
+
// A failed reply carries no text at all: without this line the conversation would show an empty
|
|
49
|
+
// bubble and no hint of what went wrong.
|
|
50
|
+
if (message.status === 'failed') {
|
|
51
|
+
const failure = assistantFailure(message.error_code);
|
|
52
|
+
lines.push(`⚠️ ${failure.code} — ${failure.message}`);
|
|
53
|
+
}
|
|
54
|
+
const tools = renderToolsUsed(message.tools_used);
|
|
55
|
+
if (tools !== '')
|
|
56
|
+
lines.push(tools);
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** Every message of a conversation, oldest first (the order the backend serializes them in). */
|
|
60
|
+
function renderMessages(messages) {
|
|
61
|
+
const rows = Array.isArray(messages) ? messages : [];
|
|
62
|
+
return rows.length === 0 ? '(no messages)' : rows.map((message) => renderMessage(message)).join('\n\n');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The conversation list. ID first: it is the argument of `assistant conversations show|delete` and
|
|
66
|
+
* the value of `assistant ask --conversation`, so a table without it would force `--json` just to
|
|
67
|
+
* carry on the conversation you are looking at.
|
|
68
|
+
*/
|
|
69
|
+
function renderConversationsTable(rows) {
|
|
70
|
+
return (0, output_1.renderTable)(['ID', 'TITLE', 'LAST MESSAGE', 'CREATED'], rows.map((row) => [
|
|
71
|
+
String(row.id ?? ''),
|
|
72
|
+
String(row.title ?? ''),
|
|
73
|
+
String(row.last_message_at ?? ''),
|
|
74
|
+
String(row.created_at ?? ''),
|
|
75
|
+
]));
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk triage of monitoring groups (errors, metrics) — CYCL-44.
|
|
3
|
+
*
|
|
4
|
+
* The two endpoints are the same shape (`POST .../bulk_triage` with `ids[]` + `bulk_action`) and
|
|
5
|
+
* share the same blind spot, so the guard lives here once instead of in both commands.
|
|
6
|
+
*/
|
|
7
|
+
/** The three actions the backend accepts, for errors and metrics alike (`Observability::Triage::ACTIONS`). */
|
|
8
|
+
export declare const TRIAGE_ACTIONS: readonly ["resolve", "ignore", "reopen"];
|
|
9
|
+
/**
|
|
10
|
+
* The ids to send: the order they were written in, each one once.
|
|
11
|
+
*
|
|
12
|
+
* The server matches with `where(id: ids)`, which never returns the same row twice — so a repeated
|
|
13
|
+
* `--group` would come back as one row and the check below would read it as "one went missing".
|
|
14
|
+
* Deduplicating is safe here precisely because this endpoint is not all-or-nothing: unlike merge,
|
|
15
|
+
* where the raw list is the evidence the server needs to refuse the whole operation.
|
|
16
|
+
*/
|
|
17
|
+
export declare function uniqueIds(values: string[]): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Refuse a partial triage the caller cannot see.
|
|
20
|
+
*
|
|
21
|
+
* `bulk_triage` scopes the selection to the project and answers 200 on whatever is left, so an id
|
|
22
|
+
* belonging to another project (or to a page refreshed on stale data) is dropped without a word.
|
|
23
|
+
* A script would go on believing it closed thirty groups after closing twenty-eight. Everything the
|
|
24
|
+
* server did touch stays done — the triage is reversible, and undoing the good part would be worse
|
|
25
|
+
* — but the command says so and exits non-zero.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertNothingDropped(opts: {
|
|
28
|
+
action: string;
|
|
29
|
+
code: string;
|
|
30
|
+
noun: string;
|
|
31
|
+
requested: string[];
|
|
32
|
+
rows: Array<Record<string, unknown>>;
|
|
33
|
+
}): void;
|
|
34
|
+
/** `1 error group` / `3 error groups` — the count line both commands print. */
|
|
35
|
+
export declare function triagedLine(count: number, noun: string, action: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TRIAGE_ACTIONS = void 0;
|
|
4
|
+
exports.uniqueIds = uniqueIds;
|
|
5
|
+
exports.assertNothingDropped = assertNothingDropped;
|
|
6
|
+
exports.triagedLine = triagedLine;
|
|
7
|
+
const api_1 = require("./api");
|
|
8
|
+
/**
|
|
9
|
+
* Bulk triage of monitoring groups (errors, metrics) — CYCL-44.
|
|
10
|
+
*
|
|
11
|
+
* The two endpoints are the same shape (`POST .../bulk_triage` with `ids[]` + `bulk_action`) and
|
|
12
|
+
* share the same blind spot, so the guard lives here once instead of in both commands.
|
|
13
|
+
*/
|
|
14
|
+
/** The three actions the backend accepts, for errors and metrics alike (`Observability::Triage::ACTIONS`). */
|
|
15
|
+
exports.TRIAGE_ACTIONS = ['resolve', 'ignore', 'reopen'];
|
|
16
|
+
/**
|
|
17
|
+
* The ids to send: the order they were written in, each one once.
|
|
18
|
+
*
|
|
19
|
+
* The server matches with `where(id: ids)`, which never returns the same row twice — so a repeated
|
|
20
|
+
* `--group` would come back as one row and the check below would read it as "one went missing".
|
|
21
|
+
* Deduplicating is safe here precisely because this endpoint is not all-or-nothing: unlike merge,
|
|
22
|
+
* where the raw list is the evidence the server needs to refuse the whole operation.
|
|
23
|
+
*/
|
|
24
|
+
function uniqueIds(values) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
return values.filter((value) => {
|
|
27
|
+
const key = value.toLowerCase();
|
|
28
|
+
if (seen.has(key))
|
|
29
|
+
return false;
|
|
30
|
+
seen.add(key);
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Refuse a partial triage the caller cannot see.
|
|
36
|
+
*
|
|
37
|
+
* `bulk_triage` scopes the selection to the project and answers 200 on whatever is left, so an id
|
|
38
|
+
* belonging to another project (or to a page refreshed on stale data) is dropped without a word.
|
|
39
|
+
* A script would go on believing it closed thirty groups after closing twenty-eight. Everything the
|
|
40
|
+
* server did touch stays done — the triage is reversible, and undoing the good part would be worse
|
|
41
|
+
* — but the command says so and exits non-zero.
|
|
42
|
+
*/
|
|
43
|
+
function assertNothingDropped(opts) {
|
|
44
|
+
const returned = new Set(opts.rows.map((row) => String(row.id ?? '').toLowerCase()));
|
|
45
|
+
const missing = opts.requested.filter((id) => !returned.has(id.toLowerCase()));
|
|
46
|
+
if (missing.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
throw new api_1.ApiRequestError(404, opts.code, `Triaged ${opts.rows.length} of ${opts.requested.length} ${opts.noun}s (${opts.action}). ` +
|
|
49
|
+
`Not found in this project: ${missing.join(', ')} — those were left untouched, ` +
|
|
50
|
+
`the other ${opts.rows.length} were changed.`);
|
|
51
|
+
}
|
|
52
|
+
/** `1 error group` / `3 error groups` — the count line both commands print. */
|
|
53
|
+
function triagedLine(count, noun, action) {
|
|
54
|
+
return `Triaged ${count} ${noun}${count === 1 ? '' : 's'} (${action}).`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error grouping rules (CYCL-44, server side CYRA-153).
|
|
3
|
+
*
|
|
4
|
+
* At ingest time the first active rule, in `position` order, whose match succeeds rewrites the
|
|
5
|
+
* occurrence's fingerprint: everything hitting rules with the same `fingerprint_key` lands in one
|
|
6
|
+
* group. This is how a team bends the automatic grouping.
|
|
7
|
+
*/
|
|
8
|
+
/** Occurrence field a rule looks at (`Errors::GroupingRule.fields`). */
|
|
9
|
+
export declare const GROUPING_RULE_FIELDS: readonly ["exception_type", "message", "culprit", "transaction"];
|
|
10
|
+
/**
|
|
11
|
+
* How the field is compared (`Errors::GroupingRule.operators`). Always case-insensitive and never a
|
|
12
|
+
* regular expression: on the ingest hot path a user-supplied pattern would be an open door to ReDoS.
|
|
13
|
+
*/
|
|
14
|
+
export declare const GROUPING_RULE_OPERATORS: readonly ["contains", "equals", "starts_with"];
|
|
15
|
+
export interface GroupingRuleFlags {
|
|
16
|
+
active?: boolean;
|
|
17
|
+
field?: string;
|
|
18
|
+
'fingerprint-key'?: string;
|
|
19
|
+
operator?: string;
|
|
20
|
+
position?: number;
|
|
21
|
+
value?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The rule attributes to send, skipping every flag left out. `create` fills the required ones by
|
|
25
|
+
* declaring them required; `update` sends only what it was told to change, so an omitted flag keeps
|
|
26
|
+
* the value the rule already has instead of blanking it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function groupingRuleBody(flags: GroupingRuleFlags): Record<string, unknown>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Custom error grouping rules (CYCL-44, server side CYRA-153).
|
|
4
|
+
*
|
|
5
|
+
* At ingest time the first active rule, in `position` order, whose match succeeds rewrites the
|
|
6
|
+
* occurrence's fingerprint: everything hitting rules with the same `fingerprint_key` lands in one
|
|
7
|
+
* group. This is how a team bends the automatic grouping.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.GROUPING_RULE_OPERATORS = exports.GROUPING_RULE_FIELDS = void 0;
|
|
11
|
+
exports.groupingRuleBody = groupingRuleBody;
|
|
12
|
+
/** Occurrence field a rule looks at (`Errors::GroupingRule.fields`). */
|
|
13
|
+
exports.GROUPING_RULE_FIELDS = ['exception_type', 'message', 'culprit', 'transaction'];
|
|
14
|
+
/**
|
|
15
|
+
* How the field is compared (`Errors::GroupingRule.operators`). Always case-insensitive and never a
|
|
16
|
+
* regular expression: on the ingest hot path a user-supplied pattern would be an open door to ReDoS.
|
|
17
|
+
*/
|
|
18
|
+
exports.GROUPING_RULE_OPERATORS = ['contains', 'equals', 'starts_with'];
|
|
19
|
+
/**
|
|
20
|
+
* The rule attributes to send, skipping every flag left out. `create` fills the required ones by
|
|
21
|
+
* declaring them required; `update` sends only what it was told to change, so an omitted flag keeps
|
|
22
|
+
* the value the rule already has instead of blanking it.
|
|
23
|
+
*/
|
|
24
|
+
function groupingRuleBody(flags) {
|
|
25
|
+
const body = {};
|
|
26
|
+
if (flags.field !== undefined)
|
|
27
|
+
body.field = flags.field;
|
|
28
|
+
if (flags.operator !== undefined)
|
|
29
|
+
body.operator = flags.operator;
|
|
30
|
+
if (flags.value !== undefined)
|
|
31
|
+
body.value = flags.value;
|
|
32
|
+
if (flags['fingerprint-key'] !== undefined)
|
|
33
|
+
body.fingerprint_key = flags['fingerprint-key'];
|
|
34
|
+
if (flags.position !== undefined)
|
|
35
|
+
body.position = flags.position;
|
|
36
|
+
if (flags.active !== undefined)
|
|
37
|
+
body.active = flags.active;
|
|
38
|
+
return body;
|
|
39
|
+
}
|
package/dist/lib/knowledge.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export declare const KNOWLEDGE_KINDS: readonly ["note", "decision", "guide"];
|
|
|
5
5
|
* `--kind note --kind decision,guide` → ['note', 'decision', 'guide'].
|
|
6
6
|
*/
|
|
7
7
|
export declare function normalizeKinds(values?: string[]): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Clean a repeatable flag into a list: trimmed, empties dropped, order preserved.
|
|
10
|
+
* `--tag ""` therefore means "leave nothing" — the deliberate way to clear tags on update.
|
|
11
|
+
*/
|
|
12
|
+
export declare function cleanList(values?: string[]): string[];
|
|
8
13
|
/** Collapse whitespace and truncate a cell so the list table stays single-line. */
|
|
9
14
|
export declare function truncate(value: string, max?: number): string;
|
|
10
15
|
/**
|
|
@@ -59,6 +64,16 @@ export declare function buildBooksQuery(opts: {
|
|
|
59
64
|
project?: string;
|
|
60
65
|
q?: string;
|
|
61
66
|
}): string;
|
|
67
|
+
/**
|
|
68
|
+
* The scope lines of a page — every project it belongs to, its groups, its tags — as readable
|
|
69
|
+
* `key: a, b` rows. `renderRecord` drops arrays, so without these a page connected to five projects
|
|
70
|
+
* read as if it had one, and its tags were invisible from the terminal (CYCL-45).
|
|
71
|
+
*
|
|
72
|
+
* `projects` shows up only past the first one: with a single project the `project` line above
|
|
73
|
+
* already says it, and repeating it would be noise on the pages that have always had just one.
|
|
74
|
+
* Groups and tags appear whenever there are any.
|
|
75
|
+
*/
|
|
76
|
+
export declare function pageScopeLines(page: Record<string, unknown>): Record<string, string>;
|
|
62
77
|
/**
|
|
63
78
|
* Render the `kb book list` table. ID first: it is the argument of `kb book show` and of
|
|
64
79
|
* `kb book add-page`, so a table without it would force `--json` just to act on what you are
|
package/dist/lib/knowledge.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.KNOWLEDGE_STATUSES = exports.KNOWLEDGE_KINDS = void 0;
|
|
4
4
|
exports.normalizeKinds = normalizeKinds;
|
|
5
|
+
exports.cleanList = cleanList;
|
|
5
6
|
exports.truncate = truncate;
|
|
6
7
|
exports.buildPagesQuery = buildPagesQuery;
|
|
7
8
|
exports.renderRelated = renderRelated;
|
|
8
9
|
exports.renderRelatedGroups = renderRelatedGroups;
|
|
9
10
|
exports.buildBooksQuery = buildBooksQuery;
|
|
11
|
+
exports.pageScopeLines = pageScopeLines;
|
|
10
12
|
exports.renderBooksTable = renderBooksTable;
|
|
11
13
|
exports.renderBook = renderBook;
|
|
12
14
|
exports.renderPagesTable = renderPagesTable;
|
|
@@ -25,6 +27,15 @@ function normalizeKinds(values) {
|
|
|
25
27
|
.map((value) => value.trim())
|
|
26
28
|
.filter((value) => value.length > 0);
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Clean a repeatable flag into a list: trimmed, empties dropped, order preserved.
|
|
32
|
+
* `--tag ""` therefore means "leave nothing" — the deliberate way to clear tags on update.
|
|
33
|
+
*/
|
|
34
|
+
function cleanList(values) {
|
|
35
|
+
if (!values)
|
|
36
|
+
return [];
|
|
37
|
+
return values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
38
|
+
}
|
|
28
39
|
/** Collapse whitespace and truncate a cell so the list table stays single-line. */
|
|
29
40
|
function truncate(value, max = 60) {
|
|
30
41
|
const flat = value.replace(/\s+/g, ' ').trim();
|
|
@@ -107,6 +118,31 @@ function joinList(value) {
|
|
|
107
118
|
return undefined;
|
|
108
119
|
return value.map((entry) => String(entry)).join(', ');
|
|
109
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The scope lines of a page — every project it belongs to, its groups, its tags — as readable
|
|
123
|
+
* `key: a, b` rows. `renderRecord` drops arrays, so without these a page connected to five projects
|
|
124
|
+
* read as if it had one, and its tags were invisible from the terminal (CYCL-45).
|
|
125
|
+
*
|
|
126
|
+
* `projects` shows up only past the first one: with a single project the `project` line above
|
|
127
|
+
* already says it, and repeating it would be noise on the pages that have always had just one.
|
|
128
|
+
* Groups and tags appear whenever there are any.
|
|
129
|
+
*/
|
|
130
|
+
function pageScopeLines(page) {
|
|
131
|
+
const lines = {};
|
|
132
|
+
const projects = Array.isArray(page.projects) ? page.projects : [];
|
|
133
|
+
if (projects.length > 1) {
|
|
134
|
+
const keys = projects.map((project) => String(project.key ?? '')).filter(Boolean);
|
|
135
|
+
if (keys.length > 0)
|
|
136
|
+
lines.projects = keys.join(', ');
|
|
137
|
+
}
|
|
138
|
+
const groups = joinList(page.groups);
|
|
139
|
+
if (groups)
|
|
140
|
+
lines.groups = groups;
|
|
141
|
+
const tags = joinList(page.tags);
|
|
142
|
+
if (tags)
|
|
143
|
+
lines.tags = tags;
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
110
146
|
/**
|
|
111
147
|
* Render the `kb book list` table. ID first: it is the argument of `kb book show` and of
|
|
112
148
|
* `kb book add-page`, so a table without it would force `--json` just to act on what you are
|