faces-cli 1.7.8 → 1.7.10
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.
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { BaseCommand } from '../../../base.js';
|
|
2
2
|
export default class CompileDoc extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
|
+
static examples: string[];
|
|
4
5
|
static flags: {
|
|
5
6
|
label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
content: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
file: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
perspective: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -16,4 +17,10 @@ export default class CompileDoc extends BaseCommand {
|
|
|
16
17
|
face_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
17
18
|
};
|
|
18
19
|
run(): Promise<unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* `documents` is always present and always in input order. For a single
|
|
22
|
+
* source the compiled/started body is also spread at the top level, so
|
|
23
|
+
* callers that already read `.document_id` keep working.
|
|
24
|
+
*/
|
|
25
|
+
private shape;
|
|
19
26
|
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { BaseCommand } from '../../../base.js';
|
|
4
5
|
import { FacesAPIError } from '../../../client.js';
|
|
5
6
|
import { pollCompileProgress } from '../../../poll.js';
|
|
6
7
|
export default class CompileDoc extends BaseCommand {
|
|
7
|
-
static description = 'Compile
|
|
8
|
+
static description = 'Compile one or more documents into a face (create → make in one step). ' +
|
|
9
|
+
'Repeat --file to submit several documents in a single command; each file becomes its own ' +
|
|
10
|
+
'document rather than being concatenated.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %> alice --file notes.txt',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> alice --file a.txt --file b.txt --file c.txt --no-wait --json',
|
|
14
|
+
];
|
|
8
15
|
static flags = {
|
|
9
16
|
...BaseCommand.baseFlags,
|
|
10
|
-
label: Flags.string({ description: 'Document label/title' }),
|
|
11
|
-
content: Flags.string({ description: 'Inline text content' }),
|
|
12
|
-
file: Flags.string({ description: 'Read content from file' }),
|
|
17
|
+
label: Flags.string({ description: 'Document label/title (single document only; with several files each is labelled by its filename)' }),
|
|
18
|
+
content: Flags.string({ description: 'Inline text content', exclusive: ['file'] }),
|
|
19
|
+
file: Flags.string({ description: 'Read content from file (repeatable — each file becomes its own document)', multiple: true }),
|
|
13
20
|
perspective: Flags.string({
|
|
14
21
|
description: 'Perspective',
|
|
15
22
|
options: ['first-person', 'third-person'],
|
|
16
23
|
default: 'first-person',
|
|
17
24
|
}),
|
|
18
|
-
timeout: Flags.integer({ description: 'Compile timeout in seconds (default: 600)', default: 600 }),
|
|
25
|
+
timeout: Flags.integer({ description: 'Compile timeout in seconds, per document (default: 600)', default: 600 }),
|
|
19
26
|
'no-wait': Flags.boolean({
|
|
20
27
|
description: 'Create and trigger compilation, then return immediately without polling.',
|
|
21
28
|
default: false,
|
|
@@ -28,79 +35,181 @@ export default class CompileDoc extends BaseCommand {
|
|
|
28
35
|
const { args, flags } = await this.parse(CompileDoc);
|
|
29
36
|
const client = this.makeClient(flags);
|
|
30
37
|
const json = this.jsonEnabled();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (flags.file) {
|
|
34
|
-
if (!fs.existsSync(flags.file))
|
|
35
|
-
this.error(`File not found: ${flags.file}`);
|
|
36
|
-
content = fs.readFileSync(flags.file, 'utf8');
|
|
37
|
-
}
|
|
38
|
-
if (!content)
|
|
38
|
+
const files = flags.file ?? [];
|
|
39
|
+
if (files.length === 0 && !flags.content)
|
|
39
40
|
this.error('Provide --content or --file.');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
process.stderr.write('Creating document... ');
|
|
43
|
-
let doc;
|
|
44
|
-
try {
|
|
45
|
-
const payload = { alias: args.face_id, content };
|
|
46
|
-
if (flags.label)
|
|
47
|
-
payload.label = flags.label;
|
|
48
|
-
if (flags.perspective)
|
|
49
|
-
payload.perspective = flags.perspective;
|
|
50
|
-
doc = await client.post('/v1/compile/documents', { body: payload });
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
if (err instanceof FacesAPIError)
|
|
54
|
-
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
55
|
-
throw err;
|
|
41
|
+
if (flags.label && files.length > 1) {
|
|
42
|
+
this.error('--label applies to a single document. With several --file flags each document is labelled by its filename.');
|
|
56
43
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
44
|
+
// Read every source up front, keeping failures in place rather than
|
|
45
|
+
// collecting them separately — results must come back in input order so a
|
|
46
|
+
// caller can line them up against what it passed. A file that cannot be
|
|
47
|
+
// used is a per-file failure, not a reason to abandon the files that are
|
|
48
|
+
// fine: a whole-call failure leaves the caller unable to tell which file
|
|
49
|
+
// was at fault.
|
|
50
|
+
const sources = files.map((file) => {
|
|
51
|
+
try {
|
|
52
|
+
return { name: file, content: readTextFile(file) };
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return { name: file, readError: err instanceof Error ? err.message : String(err) };
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (flags.content)
|
|
59
|
+
sources.push({ name: '(--content)', content: flags.content });
|
|
60
|
+
const results = [];
|
|
61
|
+
const multi = files.length > 1;
|
|
62
|
+
const started = [];
|
|
63
|
+
for (const source of sources) {
|
|
64
|
+
if (source.readError !== undefined) {
|
|
65
|
+
results.push({ file: source.name, error: source.readError });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const payload = { alias: args.face_id, content: source.content };
|
|
70
|
+
if (flags.label)
|
|
71
|
+
payload.label = flags.label;
|
|
72
|
+
else if (multi)
|
|
73
|
+
payload.label = path.basename(source.name);
|
|
74
|
+
if (flags.perspective)
|
|
75
|
+
payload.perspective = flags.perspective;
|
|
76
|
+
if (!json)
|
|
77
|
+
process.stderr.write(`Creating document${multi ? ` (${source.name})` : ''}... `);
|
|
78
|
+
const doc = (await client.post('/v1/compile/documents', { body: payload }));
|
|
79
|
+
const docId = String(doc.document_id ?? doc.id ?? '');
|
|
80
|
+
if (!docId)
|
|
81
|
+
throw new Error('Server did not return a document_id');
|
|
82
|
+
if (!json)
|
|
83
|
+
process.stderr.write(`done (${docId})\n`);
|
|
84
|
+
const makeResponse = (await client.post(`/v1/compile/documents/${docId}/make`));
|
|
85
|
+
const chunksTotal = makeResponse.chunks_total;
|
|
86
|
+
started.push({ source, docId, chunksTotal });
|
|
87
|
+
results.push({ file: source.name, document_id: docId, chunks_total: chunksTotal });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (!json)
|
|
91
|
+
process.stderr.write('failed\n');
|
|
92
|
+
const message = err instanceof FacesAPIError ? `Error (${err.statusCode}): ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
93
|
+
results.push({ file: source.name, error: message });
|
|
94
|
+
}
|
|
64
95
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
if (started.length === 0) {
|
|
97
|
+
// Nothing was accepted — report every reason, then fail.
|
|
98
|
+
for (const r of results)
|
|
99
|
+
if (r.error)
|
|
100
|
+
process.stderr.write(`${r.file}: ${r.error}\n`);
|
|
101
|
+
this.error(results.length === 1 ? 'Document was not accepted.' : 'No documents were accepted.');
|
|
69
102
|
}
|
|
70
|
-
|
|
103
|
+
if (!json)
|
|
104
|
+
for (const r of results)
|
|
105
|
+
if (r.error)
|
|
106
|
+
process.stderr.write(`${r.file}: ${r.error}\n`);
|
|
71
107
|
if (flags['no-wait']) {
|
|
72
108
|
if (!json) {
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
for (const s of started) {
|
|
110
|
+
process.stderr.write(`Compilation started in background${s.chunksTotal ? ` (${s.chunksTotal} chunks)` : ''}: ${s.docId}\n`);
|
|
111
|
+
}
|
|
112
|
+
process.stderr.write(`Poll with: faces compile:doc:get <id> --json\n`);
|
|
75
113
|
}
|
|
76
|
-
return
|
|
114
|
+
return this.shape(results);
|
|
77
115
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
for (const s of started) {
|
|
117
|
+
if (!json)
|
|
118
|
+
process.stderr.write(`Compiling ${multi ? `${s.source.name} ` : ''}${s.chunksTotal ? `(${s.chunksTotal} chunks)` : ''}:\n`);
|
|
119
|
+
try {
|
|
120
|
+
const result = (await pollCompileProgress(client, s.docId, {
|
|
121
|
+
timeoutMs: flags.timeout * 1000,
|
|
122
|
+
onProgress: (p) => {
|
|
123
|
+
if (json)
|
|
124
|
+
return;
|
|
87
125
|
const c = p.current_counts;
|
|
88
126
|
const counts = c ? `ε=${c.epsilon} β=${c.beta} δ=${c.delta} α=${c.alpha}` : '';
|
|
89
127
|
const phase = p.prepare_status === 'syncing' ? ' (syncing)' : '';
|
|
90
128
|
process.stderr.write(` [${p.chunks_completed ?? '?'}/${p.chunks_total ?? '?'}] ${counts}${phase}\n`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
},
|
|
130
|
+
}));
|
|
131
|
+
const hit = results.find((r) => r.document_id === s.docId);
|
|
132
|
+
if (hit)
|
|
133
|
+
hit.compiled = result;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const hit = results.find((r) => r.document_id === s.docId);
|
|
137
|
+
if (hit)
|
|
138
|
+
hit.error = err instanceof Error ? err.message : String(err);
|
|
139
|
+
if (!json)
|
|
140
|
+
process.stderr.write(`${s.source.name}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
141
|
+
}
|
|
99
142
|
}
|
|
100
143
|
if (!json)
|
|
101
144
|
process.stderr.write('Done.\n');
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
145
|
+
return this.shape(results);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `documents` is always present and always in input order. For a single
|
|
149
|
+
* source the compiled/started body is also spread at the top level, so
|
|
150
|
+
* callers that already read `.document_id` keep working.
|
|
151
|
+
*/
|
|
152
|
+
shape(results) {
|
|
153
|
+
const documents = results.map((r) => {
|
|
154
|
+
const out = { file: r.file };
|
|
155
|
+
if (r.document_id)
|
|
156
|
+
out.document_id = r.document_id;
|
|
157
|
+
if (r.chunks_total !== undefined)
|
|
158
|
+
out.chunks_total = r.chunks_total;
|
|
159
|
+
if (r.error)
|
|
160
|
+
out.error = r.error;
|
|
161
|
+
return out;
|
|
162
|
+
});
|
|
163
|
+
const body = { documents };
|
|
164
|
+
if (results.length === 1) {
|
|
165
|
+
const only = results[0];
|
|
166
|
+
Object.assign(body, only.compiled ?? {}, only.document_id ? { document_id: only.document_id } : {});
|
|
167
|
+
if (only.chunks_total !== undefined && body.chunks_total === undefined)
|
|
168
|
+
body.chunks_total = only.chunks_total;
|
|
169
|
+
body.documents = documents;
|
|
170
|
+
}
|
|
171
|
+
return body;
|
|
105
172
|
}
|
|
106
173
|
}
|
|
174
|
+
/** Extensions the compile-documents endpoint takes as inline text. */
|
|
175
|
+
const BINARY_HINTS = {
|
|
176
|
+
'.pdf': 'PDF is supported, but not by this command — use: faces compile:upload <alias> --file <path> --kind document',
|
|
177
|
+
'.docx': 'Word documents are supported, but not by this command — use: faces compile:upload <alias> --file <path> --kind document',
|
|
178
|
+
'.doc': "Legacy '.doc' is not supported. Open it in Word or Pages and save as .docx, then upload that with: faces compile:upload <alias> --file <path> --kind document",
|
|
179
|
+
'.pages': 'Pages documents are not supported. Export it as text or PDF first.',
|
|
180
|
+
'.rtf': 'RTF is not supported. Convert it to plain text first (e.g. `textutil -convert txt file.rtf`).',
|
|
181
|
+
'.key': 'Keynote files are not supported. Export the text first.',
|
|
182
|
+
'.pptx': 'PowerPoint files are not supported. Export the text first.',
|
|
183
|
+
'.xlsx': 'Excel files are not supported. Export as .csv first.',
|
|
184
|
+
'.zip': 'Archives are not supported. Extract it and pass the text files.',
|
|
185
|
+
'.epub': 'EPUB is not supported. Convert it to text first.',
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Read a file as text, refusing binary before it reaches the API.
|
|
189
|
+
*
|
|
190
|
+
* `compile:doc` posts its content as a text field, so a binary file used to
|
|
191
|
+
* travel all the way to the server and come back as a 500 ("A string literal
|
|
192
|
+
* cannot contain NUL (0x00) characters") that named neither the file nor the
|
|
193
|
+
* cause. The NUL scan is the general check — the extension table only makes the
|
|
194
|
+
* message more useful for formats people actually try.
|
|
195
|
+
*/
|
|
196
|
+
function readTextFile(file) {
|
|
197
|
+
if (!fs.existsSync(file))
|
|
198
|
+
throw new Error(`File not found: ${file}`);
|
|
199
|
+
const stat = fs.statSync(file);
|
|
200
|
+
if (stat.isDirectory())
|
|
201
|
+
throw new Error(`Not a file: ${file}`);
|
|
202
|
+
// Extension first, and independent of the byte scan: a small PDF can contain
|
|
203
|
+
// no NUL at all and would otherwise be posted as mojibake. A known format
|
|
204
|
+
// always gets the answer for that format.
|
|
205
|
+
const hint = BINARY_HINTS[path.extname(file).toLowerCase()];
|
|
206
|
+
if (hint)
|
|
207
|
+
throw new Error(hint);
|
|
208
|
+
const buf = fs.readFileSync(file);
|
|
209
|
+
if (buf.includes(0))
|
|
210
|
+
throw new Error('Looks like a binary file, not text. Convert it to text first.');
|
|
211
|
+
const content = buf.toString('utf8');
|
|
212
|
+
if (content.trim() === '')
|
|
213
|
+
throw new Error('File is empty.');
|
|
214
|
+
return content;
|
|
215
|
+
}
|
|
@@ -7,7 +7,7 @@ function sleep(ms) {
|
|
|
7
7
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
8
|
}
|
|
9
9
|
export default class CompileUpload extends BaseCommand {
|
|
10
|
-
static description = 'Upload a file (text/PDF/audio/video) to compile into a face';
|
|
10
|
+
static description = 'Upload a file (text/PDF/Word .docx/audio/video) to compile into a face';
|
|
11
11
|
static flags = {
|
|
12
12
|
...BaseCommand.baseFlags,
|
|
13
13
|
file: Flags.string({ description: 'File to upload', required: true }),
|