faces-cli 1.7.14 → 1.7.16
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.
|
@@ -2,6 +2,7 @@ import { BaseCommand } from '../../../base.js';
|
|
|
2
2
|
export default class CompileDocList extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
5
6
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Args } from '@oclif/core';
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../../base.js';
|
|
3
3
|
import { FacesAPIError } from '../../../client.js';
|
|
4
4
|
export default class CompileDocList extends BaseCommand {
|
|
5
|
-
static description =
|
|
5
|
+
static description = "List documents for a face. Each document's full text is omitted unless --verbose, so a face with a " +
|
|
6
|
+
'real corpus stays readable. For a table of every source, documents and threads together, use face:sources.';
|
|
6
7
|
static flags = {
|
|
7
8
|
...BaseCommand.baseFlags,
|
|
9
|
+
verbose: Flags.boolean({ description: "Include each document's full text", default: false }),
|
|
8
10
|
};
|
|
9
11
|
static args = {
|
|
10
12
|
face_id: Args.string({ description: 'Face alias', required: true }),
|
|
@@ -21,8 +23,21 @@ export default class CompileDocList extends BaseCommand {
|
|
|
21
23
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
22
24
|
throw err;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// --json is untouched: machine callers may already read `content`, and
|
|
27
|
+
// dropping a field from structured output would break them silently.
|
|
28
|
+
if (!this.jsonEnabled()) {
|
|
29
|
+
const items = Array.isArray(data) ? data : [];
|
|
30
|
+
const omitted = flags.verbose ? 0 : items.filter((d) => typeof d.content === 'string' && d.content !== '').length;
|
|
31
|
+
this.printHuman(flags.verbose
|
|
32
|
+
? data
|
|
33
|
+
: items.map((d) => Object.fromEntries(Object.entries(d).filter(([k]) => k !== 'content'))));
|
|
34
|
+
// Say that something was left out. A list that quietly drops a field
|
|
35
|
+
// reads as complete, which is how this became a problem in the first place.
|
|
36
|
+
if (omitted > 0) {
|
|
37
|
+
this.log('');
|
|
38
|
+
this.log(`Text omitted for ${omitted} document(s). Pass --verbose to include it.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
return data;
|
|
27
42
|
}
|
|
28
43
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class FaceSources extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
static args: {
|
|
12
|
+
alias: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<unknown>;
|
|
15
|
+
private fetch;
|
|
16
|
+
private statusOf;
|
|
17
|
+
private toRow;
|
|
18
|
+
private toJson;
|
|
19
|
+
private render;
|
|
20
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { FacesAPIError } from '../../client.js';
|
|
4
|
+
/** Exit code for "no such face", so a caller can tell it from an empty face. */
|
|
5
|
+
const EXIT_NO_SUCH_FACE = 4;
|
|
6
|
+
export default class FaceSources extends BaseCommand {
|
|
7
|
+
static description = 'List every source compiled into a face: documents and threads together, one row each. ' +
|
|
8
|
+
`Exits ${EXIT_NO_SUCH_FACE} if the face does not exist, which is a different answer from a face that ` +
|
|
9
|
+
'has no sources. Content is never printed; read a single source with compile:doc:get or compile:thread:get.';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> <%= command.id %> alice',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> alice --type thread',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> alice --json',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
type: Flags.string({ description: 'Show only one kind of source', options: ['doc', 'thread'] }),
|
|
18
|
+
};
|
|
19
|
+
static args = {
|
|
20
|
+
alias: Args.string({ description: 'Face alias', required: true }),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(FaceSources);
|
|
24
|
+
const client = this.makeClient(flags);
|
|
25
|
+
const json = this.jsonEnabled();
|
|
26
|
+
const alias = encodeURIComponent(args.alias);
|
|
27
|
+
const wantDocs = flags.type !== 'thread';
|
|
28
|
+
const wantThreads = flags.type !== 'doc';
|
|
29
|
+
// A face that does not exist and a face with nothing in it are opposite
|
|
30
|
+
// answers. The API distinguishes them with a 404, so this must too — by
|
|
31
|
+
// message and by exit code, since a script only sees the latter.
|
|
32
|
+
const docs = wantDocs ? await this.fetch(client, `/v1/compile/documents?alias=${alias}`, args.alias) : [];
|
|
33
|
+
// include_uploads defaults to excluding upload-created threads. This command
|
|
34
|
+
// claims to list every source, so it asks for them explicitly.
|
|
35
|
+
const threads = wantThreads
|
|
36
|
+
? await this.fetch(client, `/v1/compile/threads?alias=${alias}&include_uploads=true`, args.alias)
|
|
37
|
+
: [];
|
|
38
|
+
const rows = [...docs.map((d) => this.toRow(d, 'doc')), ...threads.map((t) => this.toRow(t, 'thread'))];
|
|
39
|
+
rows.sort((a, b) => (b.updated || '').localeCompare(a.updated || '') || a.label.localeCompare(b.label));
|
|
40
|
+
if (json) {
|
|
41
|
+
return {
|
|
42
|
+
alias: args.alias,
|
|
43
|
+
documents: docs.map((d) => this.toJson(d, 'doc')),
|
|
44
|
+
threads: threads.map((t) => this.toJson(t, 'thread')),
|
|
45
|
+
totals: {
|
|
46
|
+
sources: rows.length,
|
|
47
|
+
tokens: rows.reduce((n, r) => n + r.tokens, 0),
|
|
48
|
+
not_synced: rows.filter((r) => !r.synced).length,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
this.render(args.alias, rows, flags.type);
|
|
53
|
+
return { alias: args.alias, sources: rows.length };
|
|
54
|
+
}
|
|
55
|
+
async fetch(client, path, alias) {
|
|
56
|
+
try {
|
|
57
|
+
const data = await client.get(path);
|
|
58
|
+
return (Array.isArray(data) ? data : (data.data ?? []));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (err instanceof FacesAPIError) {
|
|
62
|
+
if (err.statusCode === 404) {
|
|
63
|
+
this.error(`No face named '${alias}'. It does not exist, or is not owned by this account.`, {
|
|
64
|
+
exit: EXIT_NO_SUCH_FACE,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
statusOf(s) {
|
|
73
|
+
if (s.synced)
|
|
74
|
+
return 'synced';
|
|
75
|
+
const st = s.prepare_status;
|
|
76
|
+
const done = s.chunks_completed;
|
|
77
|
+
const total = s.chunks_total;
|
|
78
|
+
const progress = done !== null && done !== undefined && total ? ` ${done}/${total}` : '';
|
|
79
|
+
if (st)
|
|
80
|
+
return `${st}${progress}`;
|
|
81
|
+
return 'not compiled';
|
|
82
|
+
}
|
|
83
|
+
toRow(s, type) {
|
|
84
|
+
return {
|
|
85
|
+
type,
|
|
86
|
+
id: String(s.document_id ?? s.thread_id ?? ''),
|
|
87
|
+
label: s.label?.trim() || '(untitled)',
|
|
88
|
+
tokens: s.token_count ?? 0,
|
|
89
|
+
status: this.statusOf(s),
|
|
90
|
+
medium: s.medium?.trim() || '-',
|
|
91
|
+
updated: (s.updated_at ?? s.created_at ?? '').slice(0, 10) || '-',
|
|
92
|
+
synced: Boolean(s.synced),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
toJson(s, type) {
|
|
96
|
+
const out = {
|
|
97
|
+
type,
|
|
98
|
+
// A stable id, because anything that later deletes or recompiles a source
|
|
99
|
+
// needs one — the label is for people and is not unique.
|
|
100
|
+
id: String(s.document_id ?? s.thread_id ?? ''),
|
|
101
|
+
label: s.label ?? null,
|
|
102
|
+
token_count: s.token_count ?? 0,
|
|
103
|
+
status: this.statusOf(s),
|
|
104
|
+
synced: Boolean(s.synced),
|
|
105
|
+
prepare_status: s.prepare_status ?? null,
|
|
106
|
+
medium: s.medium ?? null,
|
|
107
|
+
created_at: s.created_at ?? null,
|
|
108
|
+
updated_at: s.updated_at ?? null,
|
|
109
|
+
read_only: Boolean(s.read_only),
|
|
110
|
+
};
|
|
111
|
+
if (type === 'doc')
|
|
112
|
+
out.document_id = s.document_id;
|
|
113
|
+
if (type === 'thread') {
|
|
114
|
+
out.thread_id = s.thread_id;
|
|
115
|
+
out.message_count = s.message_count ?? null;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
render(alias, rows, type) {
|
|
120
|
+
if (rows.length === 0) {
|
|
121
|
+
const what = type ? `${type} sources` : 'sources';
|
|
122
|
+
this.log(`'${alias}' exists and has no ${what}.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const n = (v) => v.toLocaleString('en-US');
|
|
126
|
+
const cols = [
|
|
127
|
+
['TYPE', (r) => r.type],
|
|
128
|
+
['LABEL', (r) => r.label],
|
|
129
|
+
['TOKENS', (r) => n(r.tokens)],
|
|
130
|
+
['STATUS', (r) => r.status],
|
|
131
|
+
['MEDIUM', (r) => r.medium],
|
|
132
|
+
['UPDATED', (r) => r.updated],
|
|
133
|
+
];
|
|
134
|
+
const widths = cols.map(([h, get]) => Math.max(h.length, ...rows.map((r) => get(r).length)));
|
|
135
|
+
const line = (cells) => cells.map((c, i) => (cols[i][0] === 'TOKENS' ? c.padStart(widths[i]) : c.padEnd(widths[i]))).join(' ').trimEnd();
|
|
136
|
+
this.log(line(cols.map(([h]) => h)));
|
|
137
|
+
for (const r of rows)
|
|
138
|
+
this.log(line(cols.map(([, get]) => get(r))));
|
|
139
|
+
// Say what the total counts. Every listed row is included, so the column
|
|
140
|
+
// adds up to the figure shown; anything not yet compiled is called out
|
|
141
|
+
// separately rather than quietly left out of the sum.
|
|
142
|
+
const tokens = rows.reduce((a, r) => a + r.tokens, 0);
|
|
143
|
+
const pending = rows.filter((r) => !r.synced).length;
|
|
144
|
+
this.log('');
|
|
145
|
+
this.log(`${n(tokens)} tokens across all ${rows.length} source(s) listed.`);
|
|
146
|
+
if (pending > 0) {
|
|
147
|
+
this.log(`${pending} not yet synced. Their tokens are included in that total.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|