@docmana/sdk 0.4.0 → 0.8.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/CHANGELOG.md +33 -4
- package/README.md +101 -26
- package/dist/api/flow-definition.d.ts +3 -0
- package/dist/api/flow-metadata.d.ts +3 -0
- package/dist/api/list-flows.d.ts +3 -0
- package/dist/api/list-organizations.d.ts +3 -0
- package/dist/api/organization-documents.d.ts +7 -0
- package/dist/cli.mjs +892 -525
- package/dist/cli.mjs.map +1 -1
- package/dist/client.d.ts +12 -7
- package/dist/files/resolve-input.d.ts +1 -3
- package/dist/http/http-client.d.ts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +184 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -57
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +85 -16
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { dirname, resolve } from "path";
|
|
7
|
-
import { createInterface } from "readline/promises";
|
|
3
|
+
// src/cli/cli.ts
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { resolve as resolve2 } from "path";
|
|
8
6
|
import { fileURLToPath } from "url";
|
|
9
7
|
import { Command, CommanderError } from "commander";
|
|
10
8
|
|
|
11
|
-
// src/
|
|
9
|
+
// src/sdk/config.ts
|
|
10
|
+
var DEFAULTS = {
|
|
11
|
+
tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
|
|
12
|
+
scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
|
|
13
|
+
timeoutMs: 3e5,
|
|
14
|
+
pollIntervalMs: 2e3,
|
|
15
|
+
pollMaxIntervalMs: 1e4
|
|
16
|
+
};
|
|
17
|
+
function resolveConfig(config) {
|
|
18
|
+
if (!config.apiBaseUrl) throw new Error("DocmanaConfig.apiBaseUrl is required");
|
|
19
|
+
if (!config.accessToken && !config.getAccessToken) {
|
|
20
|
+
throw new Error("DocmanaConfig.accessToken or DocmanaConfig.getAccessToken is required");
|
|
21
|
+
}
|
|
22
|
+
const apiBaseUrl = normalizeApiBaseUrl(config.apiBaseUrl);
|
|
23
|
+
return {
|
|
24
|
+
apiBaseUrl,
|
|
25
|
+
accessToken: config.accessToken,
|
|
26
|
+
getAccessToken: config.getAccessToken,
|
|
27
|
+
timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
28
|
+
pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
|
|
29
|
+
pollMaxIntervalMs: config.pollMaxIntervalMs ?? DEFAULTS.pollMaxIntervalMs,
|
|
30
|
+
fetchImpl: config.fetch ?? fetch,
|
|
31
|
+
headers: config.headers ?? {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function normalizeApiBaseUrl(value) {
|
|
35
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
36
|
+
return /\/v\d+$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/sdk/errors.ts
|
|
12
40
|
var DocmanaError = class extends Error {
|
|
13
41
|
status;
|
|
14
42
|
requestId;
|
|
@@ -86,117 +114,7 @@ function errorFromResponse(status, message, requestId, retryAfterMs) {
|
|
|
86
114
|
}
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
// src/
|
|
90
|
-
var SAFETY_MARGIN_MS = 6e4;
|
|
91
|
-
var TokenManager = class {
|
|
92
|
-
constructor(opts) {
|
|
93
|
-
this.opts = opts;
|
|
94
|
-
}
|
|
95
|
-
opts;
|
|
96
|
-
token = null;
|
|
97
|
-
expiresAt = 0;
|
|
98
|
-
inflight = null;
|
|
99
|
-
invalidate() {
|
|
100
|
-
this.token = null;
|
|
101
|
-
this.expiresAt = 0;
|
|
102
|
-
void this.opts.tokenCache?.clear().catch(() => void 0);
|
|
103
|
-
}
|
|
104
|
-
async getToken() {
|
|
105
|
-
if (this.token && Date.now() < this.expiresAt) return this.token;
|
|
106
|
-
const cached = await this.readCachedToken();
|
|
107
|
-
if (cached) return cached;
|
|
108
|
-
if (this.inflight) return this.inflight;
|
|
109
|
-
this.inflight = this.acquire().finally(() => {
|
|
110
|
-
this.inflight = null;
|
|
111
|
-
});
|
|
112
|
-
return this.inflight;
|
|
113
|
-
}
|
|
114
|
-
async acquire() {
|
|
115
|
-
const body = new URLSearchParams({
|
|
116
|
-
grant_type: "client_credentials",
|
|
117
|
-
client_id: this.opts.clientId,
|
|
118
|
-
client_secret: this.opts.clientSecret,
|
|
119
|
-
scope: this.opts.scope
|
|
120
|
-
});
|
|
121
|
-
const res = await this.opts.fetchImpl(this.opts.tokenEndpoint, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
124
|
-
body: body.toString()
|
|
125
|
-
});
|
|
126
|
-
if (!res.ok) {
|
|
127
|
-
throw new DocmanaAuthError("Failed to acquire Docmana access token", { status: res.status });
|
|
128
|
-
}
|
|
129
|
-
const json = await res.json();
|
|
130
|
-
if (typeof json.access_token !== "string" || json.access_token.length === 0 || typeof json.expires_in !== "number" || !Number.isFinite(json.expires_in)) {
|
|
131
|
-
throw new DocmanaAuthError("Malformed token response from Docmana CIAM", {
|
|
132
|
-
status: res.status
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
this.token = json.access_token;
|
|
136
|
-
this.expiresAt = Date.now() + json.expires_in * 1e3 - SAFETY_MARGIN_MS;
|
|
137
|
-
await this.writeCachedToken().catch(() => void 0);
|
|
138
|
-
return this.token;
|
|
139
|
-
}
|
|
140
|
-
async readCachedToken() {
|
|
141
|
-
if (!this.opts.tokenCache) return null;
|
|
142
|
-
let entry;
|
|
143
|
-
try {
|
|
144
|
-
entry = await this.opts.tokenCache.read();
|
|
145
|
-
} catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
if (!entry) return null;
|
|
149
|
-
if (entry.clientId !== this.opts.clientId || entry.tokenEndpoint !== this.opts.tokenEndpoint || entry.scope !== this.opts.scope || Date.now() >= entry.expiresAt) {
|
|
150
|
-
await this.opts.tokenCache.clear().catch(() => void 0);
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
this.token = entry.accessToken;
|
|
154
|
-
this.expiresAt = entry.expiresAt;
|
|
155
|
-
return this.token;
|
|
156
|
-
}
|
|
157
|
-
async writeCachedToken() {
|
|
158
|
-
if (!this.opts.tokenCache || !this.token) return;
|
|
159
|
-
await this.opts.tokenCache.write({
|
|
160
|
-
accessToken: this.token,
|
|
161
|
-
expiresAt: this.expiresAt,
|
|
162
|
-
clientId: this.opts.clientId,
|
|
163
|
-
tokenEndpoint: this.opts.tokenEndpoint,
|
|
164
|
-
scope: this.opts.scope
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
// src/config.ts
|
|
170
|
-
var DEFAULTS = {
|
|
171
|
-
tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
|
|
172
|
-
scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
|
|
173
|
-
timeoutMs: 3e5,
|
|
174
|
-
pollIntervalMs: 2e3,
|
|
175
|
-
pollMaxIntervalMs: 1e4
|
|
176
|
-
};
|
|
177
|
-
function resolveConfig(config) {
|
|
178
|
-
if (!config.apiBaseUrl) throw new Error("DocmanaConfig.apiBaseUrl is required");
|
|
179
|
-
if (!config.accessToken && !config.getAccessToken) {
|
|
180
|
-
throw new Error("DocmanaConfig.accessToken or DocmanaConfig.getAccessToken is required");
|
|
181
|
-
}
|
|
182
|
-
const apiBaseUrl = normalizeApiBaseUrl(config.apiBaseUrl);
|
|
183
|
-
return {
|
|
184
|
-
apiBaseUrl,
|
|
185
|
-
accessToken: config.accessToken,
|
|
186
|
-
getAccessToken: config.getAccessToken,
|
|
187
|
-
timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
188
|
-
pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
|
|
189
|
-
pollMaxIntervalMs: config.pollMaxIntervalMs ?? DEFAULTS.pollMaxIntervalMs,
|
|
190
|
-
fetchImpl: config.fetch ?? fetch,
|
|
191
|
-
headers: config.headers ?? {}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
function normalizeApiBaseUrl(value) {
|
|
195
|
-
const trimmed = value.replace(/\/+$/, "");
|
|
196
|
-
return /\/v\d+$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// src/http/http-client.ts
|
|
117
|
+
// src/sdk/http/http-client.ts
|
|
200
118
|
var HttpClient = class {
|
|
201
119
|
constructor(opts) {
|
|
202
120
|
this.opts = opts;
|
|
@@ -212,6 +130,10 @@ var HttpClient = class {
|
|
|
212
130
|
throw new DocmanaError("Invalid JSON response from Docmana", { status: res.status });
|
|
213
131
|
}
|
|
214
132
|
}
|
|
133
|
+
async requestBytes(method, path, options = {}) {
|
|
134
|
+
const res = await this.requestRaw(method, path, options);
|
|
135
|
+
return { bytes: new Uint8Array(await res.arrayBuffer()), headers: res.headers };
|
|
136
|
+
}
|
|
215
137
|
async requestRaw(method, path, options = {}) {
|
|
216
138
|
let res = await this.send(method, path, options);
|
|
217
139
|
if (res.status === 401 && this.opts.getAccessToken) {
|
|
@@ -281,7 +203,7 @@ function parseRetryAfterMs(value) {
|
|
|
281
203
|
return void 0;
|
|
282
204
|
}
|
|
283
205
|
|
|
284
|
-
// src/files/resolve-input.ts
|
|
206
|
+
// src/sdk/files/resolve-input.ts
|
|
285
207
|
import { readFile } from "fs/promises";
|
|
286
208
|
import { basename } from "path";
|
|
287
209
|
var CONTENT_TYPES = {
|
|
@@ -306,6 +228,11 @@ function isReadable(x) {
|
|
|
306
228
|
function isPathInput(x) {
|
|
307
229
|
return typeof x === "object" && x !== null && typeof x.path === "string";
|
|
308
230
|
}
|
|
231
|
+
function isNamedFileInput(x) {
|
|
232
|
+
if (typeof x !== "object" || x === null) return false;
|
|
233
|
+
const value = x;
|
|
234
|
+
return typeof value.filename === "string" && (value.data instanceof Uint8Array || isReadable(value.data));
|
|
235
|
+
}
|
|
309
236
|
async function streamToBuffer(stream) {
|
|
310
237
|
const chunks = [];
|
|
311
238
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
@@ -314,38 +241,30 @@ async function streamToBuffer(stream) {
|
|
|
314
241
|
function toBlobBytes(bytes) {
|
|
315
242
|
return new Uint8Array(bytes);
|
|
316
243
|
}
|
|
317
|
-
async function resolveOne(input
|
|
244
|
+
async function resolveOne(input) {
|
|
318
245
|
if (typeof input === "string" || isPathInput(input)) {
|
|
319
246
|
const path = typeof input === "string" ? input : input.path;
|
|
320
247
|
const data = await readFile(path);
|
|
321
248
|
const filename = basename(path);
|
|
322
249
|
return { data: new Blob([toBlobBytes(data)]), filename, contentType: contentTypeFor(filename) };
|
|
323
250
|
}
|
|
324
|
-
if (input
|
|
325
|
-
|
|
326
|
-
data: new Blob([toBlobBytes(input)]),
|
|
327
|
-
filename: fallbackName,
|
|
328
|
-
contentType: contentTypeFor(fallbackName)
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
if (isReadable(input)) {
|
|
332
|
-
const buf = await streamToBuffer(input);
|
|
251
|
+
if (isNamedFileInput(input)) {
|
|
252
|
+
const bytes = input.data instanceof Uint8Array ? input.data : await streamToBuffer(input.data);
|
|
333
253
|
return {
|
|
334
|
-
data: new Blob([toBlobBytes(
|
|
335
|
-
filename:
|
|
336
|
-
contentType: contentTypeFor(
|
|
254
|
+
data: new Blob([toBlobBytes(bytes)]),
|
|
255
|
+
filename: input.filename,
|
|
256
|
+
contentType: input.contentType ?? contentTypeFor(input.filename)
|
|
337
257
|
};
|
|
338
258
|
}
|
|
339
259
|
throw new Error("Unsupported file input type");
|
|
340
260
|
}
|
|
341
261
|
async function resolveInputs(input) {
|
|
342
|
-
const list = input.files
|
|
343
|
-
if (list.length === 0) throw new Error("runFlow requires `
|
|
344
|
-
|
|
345
|
-
return Promise.all(list.map((item) => resolveOne(item, fallback)));
|
|
262
|
+
const list = input.files;
|
|
263
|
+
if (list.length === 0) throw new Error("runFlow requires `files`");
|
|
264
|
+
return Promise.all(list.map((item) => resolveOne(item)));
|
|
346
265
|
}
|
|
347
266
|
|
|
348
|
-
// src/api/run-flow.ts
|
|
267
|
+
// src/sdk/api/run-flow.ts
|
|
349
268
|
async function runFlow(http, flowId, parts, signal, useDraftVersion = false, context, callbackUrl, language) {
|
|
350
269
|
const form = new FormData();
|
|
351
270
|
for (const part of parts) {
|
|
@@ -362,7 +281,7 @@ async function runFlow(http, flowId, parts, signal, useDraftVersion = false, con
|
|
|
362
281
|
);
|
|
363
282
|
}
|
|
364
283
|
|
|
365
|
-
// src/api/execution-status.ts
|
|
284
|
+
// src/sdk/api/execution-status.ts
|
|
366
285
|
async function getStatus(http, executionResultId, signal) {
|
|
367
286
|
const res = await http.requestRaw("GET", `/executions/${executionResultId}/status`, { signal });
|
|
368
287
|
const text = await res.text();
|
|
@@ -376,7 +295,7 @@ function parsePositiveInt(value) {
|
|
|
376
295
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
377
296
|
}
|
|
378
297
|
|
|
379
|
-
// src/api/execution-result.ts
|
|
298
|
+
// src/sdk/api/execution-result.ts
|
|
380
299
|
async function getResult(http, executionResultId, signal, language) {
|
|
381
300
|
return http.requestJson("GET", `/executions/${executionResultId}/result`, {
|
|
382
301
|
query: language ? { language } : void 0,
|
|
@@ -384,7 +303,119 @@ async function getResult(http, executionResultId, signal, language) {
|
|
|
384
303
|
});
|
|
385
304
|
}
|
|
386
305
|
|
|
387
|
-
// src/
|
|
306
|
+
// src/sdk/api/list-flows.ts
|
|
307
|
+
async function listFlows(http) {
|
|
308
|
+
const flows = await http.requestJson("GET", "/flows");
|
|
309
|
+
return Array.isArray(flows) ? flows.map(toFlowListItem) : [];
|
|
310
|
+
}
|
|
311
|
+
function toFlowListItem(flow) {
|
|
312
|
+
const value = flow && typeof flow === "object" ? flow : {};
|
|
313
|
+
return {
|
|
314
|
+
id: toStringValue(value.id),
|
|
315
|
+
name: toStringValue(value.name),
|
|
316
|
+
state: toStateValue(value.state)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function toStringValue(value) {
|
|
320
|
+
return typeof value === "string" ? value : "";
|
|
321
|
+
}
|
|
322
|
+
function toStateValue(value) {
|
|
323
|
+
return typeof value === "string" && value.trim() ? value : "unknown";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/sdk/api/list-organizations.ts
|
|
327
|
+
async function listOrganizations(http) {
|
|
328
|
+
const organizations = await http.requestJson("GET", "/organizations");
|
|
329
|
+
return Array.isArray(organizations) ? organizations.map(toAccessibleOrganization).filter(isAccessibleOrganization) : [];
|
|
330
|
+
}
|
|
331
|
+
function toAccessibleOrganization(organization) {
|
|
332
|
+
const value = organization && typeof organization === "object" ? organization : {};
|
|
333
|
+
const id = toStringValue2(value.id).trim();
|
|
334
|
+
if (!id) return void 0;
|
|
335
|
+
const name = toStringValue2(value.name).trim() || id;
|
|
336
|
+
return {
|
|
337
|
+
id,
|
|
338
|
+
name
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function isAccessibleOrganization(organization) {
|
|
342
|
+
return organization !== void 0;
|
|
343
|
+
}
|
|
344
|
+
function toStringValue2(value) {
|
|
345
|
+
return typeof value === "string" ? value : "";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/sdk/api/flow-metadata.ts
|
|
349
|
+
async function getFlowMetadata(http, flowId) {
|
|
350
|
+
return http.requestJson("GET", `/flows/${encodeURIComponent(flowId)}/metadata`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/sdk/api/flow-definition.ts
|
|
354
|
+
async function getFlowDefinition(http, flowId) {
|
|
355
|
+
return http.requestJson("GET", `/flows/${encodeURIComponent(flowId)}/definition`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/sdk/api/organization-documents.ts
|
|
359
|
+
async function listOrganizationDocuments(http, options = {}) {
|
|
360
|
+
const result = await http.requestJson("GET", "/organization-documents", {
|
|
361
|
+
query: options.prefix ? { prefix: options.prefix } : void 0
|
|
362
|
+
});
|
|
363
|
+
const value = result && typeof result === "object" ? result : {};
|
|
364
|
+
return {
|
|
365
|
+
configured: value.configured === true,
|
|
366
|
+
documents: Array.isArray(value.documents) ? value.documents.map(toOrganizationDocument).filter(isOrganizationDocument) : []
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
async function downloadOrganizationDocument(http, path) {
|
|
370
|
+
const result = await http.requestBytes("GET", "/organization-documents/download", {
|
|
371
|
+
query: { path }
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
bytes: result.bytes,
|
|
375
|
+
filename: filenameFromHeaders(result.headers) ?? filenameFromPath(path),
|
|
376
|
+
contentType: result.headers.get("content-type") || void 0
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async function downloadOrganizationDocuments(http, paths) {
|
|
380
|
+
return Promise.all(paths.map((path) => downloadOrganizationDocument(http, path)));
|
|
381
|
+
}
|
|
382
|
+
function toOrganizationDocument(document) {
|
|
383
|
+
const value = document && typeof document === "object" ? document : {};
|
|
384
|
+
const path = toStringValue3(value.path).trim();
|
|
385
|
+
if (!path) return void 0;
|
|
386
|
+
const contentType = toStringValue3(value.contentType).trim();
|
|
387
|
+
return {
|
|
388
|
+
path,
|
|
389
|
+
name: toStringValue3(value.name).trim() || filenameFromPath(path),
|
|
390
|
+
size: toNumberValue(value.size),
|
|
391
|
+
lastModified: toStringValue3(value.lastModified).trim(),
|
|
392
|
+
...contentType ? { contentType } : {}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function isOrganizationDocument(document) {
|
|
396
|
+
return document !== void 0;
|
|
397
|
+
}
|
|
398
|
+
function filenameFromHeaders(headers) {
|
|
399
|
+
const disposition = headers.get("content-disposition");
|
|
400
|
+
if (!disposition) return void 0;
|
|
401
|
+
const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(disposition);
|
|
402
|
+
if (utf8?.[1]) return decodeURIComponent(utf8[1]);
|
|
403
|
+
const quoted = /filename="([^"]+)"/i.exec(disposition);
|
|
404
|
+
if (quoted?.[1]) return quoted[1];
|
|
405
|
+
const plain = /filename=([^;]+)/i.exec(disposition);
|
|
406
|
+
return plain?.[1]?.trim();
|
|
407
|
+
}
|
|
408
|
+
function filenameFromPath(path) {
|
|
409
|
+
return path.replace(/\\/g, "/").split("/").filter(Boolean).pop() || path;
|
|
410
|
+
}
|
|
411
|
+
function toStringValue3(value) {
|
|
412
|
+
return typeof value === "string" ? value : "";
|
|
413
|
+
}
|
|
414
|
+
function toNumberValue(value) {
|
|
415
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/sdk/polling/poll.ts
|
|
388
419
|
var TERMINAL = /* @__PURE__ */ new Set(["Completed", "Failed"]);
|
|
389
420
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
390
421
|
async function pollUntilTerminal(opts) {
|
|
@@ -429,7 +460,7 @@ function fullJitterDelay(intervalMs, maxIntervalMs, attempt, random) {
|
|
|
429
460
|
return Math.round(intervalMs + random() * (capDelay - intervalMs));
|
|
430
461
|
}
|
|
431
462
|
|
|
432
|
-
// src/client.ts
|
|
463
|
+
// src/sdk/client.ts
|
|
433
464
|
var Docmana = class {
|
|
434
465
|
config;
|
|
435
466
|
http;
|
|
@@ -443,30 +474,17 @@ var Docmana = class {
|
|
|
443
474
|
headers: this.config.headers
|
|
444
475
|
});
|
|
445
476
|
}
|
|
446
|
-
async runFlowAsync(flowId,
|
|
447
|
-
const parts = await resolveInputs(
|
|
448
|
-
return runFlow(
|
|
449
|
-
this.http,
|
|
450
|
-
flowId,
|
|
451
|
-
parts,
|
|
452
|
-
input.signal,
|
|
453
|
-
input.useDraftVersion ?? false,
|
|
454
|
-
input.context,
|
|
455
|
-
void 0,
|
|
456
|
-
input.language
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
async runFlowWithCallback(flowId, input) {
|
|
460
|
-
const parts = await resolveInputs(input);
|
|
477
|
+
async runFlowAsync(flowId, params, options = {}) {
|
|
478
|
+
const parts = await resolveInputs(params);
|
|
461
479
|
return runFlow(
|
|
462
480
|
this.http,
|
|
463
481
|
flowId,
|
|
464
482
|
parts,
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
483
|
+
options.signal,
|
|
484
|
+
params.useDraftVersion ?? false,
|
|
485
|
+
params.context,
|
|
486
|
+
params.callbackUrl,
|
|
487
|
+
params.language
|
|
470
488
|
);
|
|
471
489
|
}
|
|
472
490
|
async getStatus(executionResultId, signal) {
|
|
@@ -475,18 +493,20 @@ var Docmana = class {
|
|
|
475
493
|
async getResult(executionResultId, signal, language) {
|
|
476
494
|
return getResult(this.http, executionResultId, signal, language);
|
|
477
495
|
}
|
|
478
|
-
async runFlow(flowId,
|
|
479
|
-
const { executionResultId } = await this.runFlowAsync(flowId,
|
|
496
|
+
async runFlow(flowId, params, options = {}) {
|
|
497
|
+
const { executionResultId } = await this.runFlowAsync(flowId, params, {
|
|
498
|
+
signal: options.signal
|
|
499
|
+
});
|
|
480
500
|
let attempt = 0;
|
|
481
501
|
const startedAt = Date.now();
|
|
482
502
|
await pollUntilTerminal({
|
|
483
503
|
check: async () => {
|
|
484
504
|
attempt += 1;
|
|
485
505
|
try {
|
|
486
|
-
const statusResult = await this.getStatus(executionResultId,
|
|
506
|
+
const statusResult = await this.getStatus(executionResultId, options.signal);
|
|
487
507
|
const status = statusResult.status;
|
|
488
508
|
const nextPollAfterMs = this.isTerminalStatus(status) ? void 0 : statusResult.pollAfterMs;
|
|
489
|
-
|
|
509
|
+
options.onPoll?.({
|
|
490
510
|
executionResultId,
|
|
491
511
|
status,
|
|
492
512
|
attempt,
|
|
@@ -496,22 +516,22 @@ var Docmana = class {
|
|
|
496
516
|
return { status, nextDelayMs: nextPollAfterMs };
|
|
497
517
|
} catch (err) {
|
|
498
518
|
if (err instanceof DocmanaRateLimitError) {
|
|
499
|
-
|
|
519
|
+
options.onRateLimit?.({
|
|
500
520
|
executionResultId,
|
|
501
521
|
attempt,
|
|
502
522
|
elapsedMs: Date.now() - startedAt,
|
|
503
|
-
retryAfterMs: err.retryAfterMs ??
|
|
523
|
+
retryAfterMs: err.retryAfterMs ?? options.pollIntervalMs ?? this.config.pollIntervalMs
|
|
504
524
|
});
|
|
505
525
|
}
|
|
506
526
|
throw err;
|
|
507
527
|
}
|
|
508
528
|
},
|
|
509
|
-
intervalMs:
|
|
510
|
-
maxIntervalMs:
|
|
511
|
-
timeoutMs:
|
|
512
|
-
signal:
|
|
529
|
+
intervalMs: options.pollIntervalMs ?? this.config.pollIntervalMs,
|
|
530
|
+
maxIntervalMs: options.pollMaxIntervalMs ?? this.config.pollMaxIntervalMs,
|
|
531
|
+
timeoutMs: options.timeoutMs ?? this.config.timeoutMs,
|
|
532
|
+
signal: options.signal
|
|
513
533
|
});
|
|
514
|
-
const result = await this.getResult(executionResultId,
|
|
534
|
+
const result = await this.getResult(executionResultId, options.signal, params.language);
|
|
515
535
|
if (result.status === "Failed") {
|
|
516
536
|
throw new DocmanaExecutionError(
|
|
517
537
|
`Flow ${flowId} execution failed`,
|
|
@@ -521,12 +541,33 @@ var Docmana = class {
|
|
|
521
541
|
}
|
|
522
542
|
return result;
|
|
523
543
|
}
|
|
544
|
+
async listFlows() {
|
|
545
|
+
return listFlows(this.http);
|
|
546
|
+
}
|
|
547
|
+
async listOrganizations() {
|
|
548
|
+
return listOrganizations(this.http);
|
|
549
|
+
}
|
|
550
|
+
async getFlowMetadata(flowId) {
|
|
551
|
+
return getFlowMetadata(this.http, flowId);
|
|
552
|
+
}
|
|
553
|
+
async getFlowDefinition(flowId) {
|
|
554
|
+
return getFlowDefinition(this.http, flowId);
|
|
555
|
+
}
|
|
556
|
+
async listOrganizationDocuments(options = {}) {
|
|
557
|
+
return listOrganizationDocuments(this.http, options);
|
|
558
|
+
}
|
|
559
|
+
async downloadOrganizationDocument(path) {
|
|
560
|
+
return downloadOrganizationDocument(this.http, path);
|
|
561
|
+
}
|
|
562
|
+
async downloadOrganizationDocuments(paths) {
|
|
563
|
+
return downloadOrganizationDocuments(this.http, paths);
|
|
564
|
+
}
|
|
524
565
|
isTerminalStatus(status) {
|
|
525
566
|
return status === "Completed" || status === "Failed";
|
|
526
567
|
}
|
|
527
568
|
};
|
|
528
569
|
|
|
529
|
-
// src/cli.ts
|
|
570
|
+
// src/cli/errors.ts
|
|
530
571
|
var CliUsageError = class extends Error {
|
|
531
572
|
constructor(message) {
|
|
532
573
|
super(message);
|
|
@@ -542,243 +583,212 @@ var CliSilentError = class extends Error {
|
|
|
542
583
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
543
584
|
}
|
|
544
585
|
};
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
async function runCli(argv = process.argv.slice(2), env = process.env, io = {
|
|
556
|
-
stdout: (text) => process.stdout.write(text),
|
|
557
|
-
stderr: (text) => process.stderr.write(text)
|
|
558
|
-
}, clientFactory = (config) => new Docmana(config), deps = {}) {
|
|
559
|
-
const program = buildProgram(env, io, clientFactory, deps);
|
|
560
|
-
if (argv.length === 0) {
|
|
561
|
-
io.stdout(program.helpInformation());
|
|
562
|
-
return 0;
|
|
586
|
+
|
|
587
|
+
// src/cli/formatting.ts
|
|
588
|
+
function formatFlowList(flows) {
|
|
589
|
+
if (flows.length === 0) return "No flows found.\n";
|
|
590
|
+
const lines = ["Docmana flows"];
|
|
591
|
+
for (const flow of flows) {
|
|
592
|
+
const id = flow.id || "(no id)";
|
|
593
|
+
const name = flow.name || "(no name)";
|
|
594
|
+
const state = flow.state ? ` [${flow.state}]` : "";
|
|
595
|
+
lines.push(`- ${id} - ${name}${state}`);
|
|
563
596
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
`);
|
|
574
|
-
return 2;
|
|
575
|
-
}
|
|
576
|
-
if (err instanceof CommanderError) {
|
|
577
|
-
return err.exitCode === 0 ? 0 : 2;
|
|
578
|
-
}
|
|
579
|
-
io.stderr(`${formatRuntimeError(err)}
|
|
580
|
-
`);
|
|
581
|
-
return 1;
|
|
597
|
+
return `${lines.join("\n")}
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
function formatOrganizationList(organizations) {
|
|
601
|
+
if (organizations.length === 0) return "No organizations found.\n";
|
|
602
|
+
const lines = ["Docmana organizations"];
|
|
603
|
+
for (const organization of organizations) {
|
|
604
|
+
const id = organization.id || "(no id)";
|
|
605
|
+
const name = organization.name || "(no name)";
|
|
606
|
+
lines.push(`- ${id} - ${name}`);
|
|
582
607
|
}
|
|
608
|
+
return `${lines.join("\n")}
|
|
609
|
+
`;
|
|
583
610
|
}
|
|
584
|
-
function
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
});
|
|
594
|
-
const flow = program.command("flow").description(HELP.flow).action(() => {
|
|
595
|
-
io.stdout(flow.helpInformation());
|
|
596
|
-
});
|
|
597
|
-
flow.command("run").description(HELP.run).argument("<flow-id>", "Docmana flow id").requiredOption("--files <csv>", "Comma-separated file paths to upload").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON result").option("--language <lang>", "Translate output to specified language").option("--draft", "Run the current draft flow version").action(async (flowId, options) => {
|
|
598
|
-
await runFlowCommand(flowId, options, env, io, clientFactory, deps);
|
|
599
|
-
});
|
|
600
|
-
const config = program.command("config").description(HELP.config).action(() => {
|
|
601
|
-
io.stdout(config.helpInformation());
|
|
602
|
-
});
|
|
603
|
-
config.command("init").description(HELP.configInit).option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).action(async (options) => {
|
|
604
|
-
await configInitCommand(options, io, deps);
|
|
605
|
-
});
|
|
606
|
-
return program;
|
|
611
|
+
function formatOrganizationDocumentList(documents) {
|
|
612
|
+
if (documents.length === 0) return "No organization documents found.\n";
|
|
613
|
+
const lines = ["Docmana organization documents"];
|
|
614
|
+
for (const document of documents) {
|
|
615
|
+
const contentType = document.contentType ? ` [${document.contentType}]` : "";
|
|
616
|
+
lines.push(`- ${document.path} - ${document.name} (${document.size} bytes)${contentType}`);
|
|
617
|
+
}
|
|
618
|
+
return `${lines.join("\n")}
|
|
619
|
+
`;
|
|
607
620
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
621
|
+
function formatHumanResult(result) {
|
|
622
|
+
const lines = ["Docmana flow completed", `Status: ${String(result.status ?? "Unknown")}`];
|
|
623
|
+
const executionId = result.executionId;
|
|
624
|
+
if (executionId) lines.push(`Execution id: ${String(executionId)}`);
|
|
625
|
+
if (result.documents && result.documents.mappings) {
|
|
626
|
+
lines.push(`Results: ${result.documents.mappings.length}`);
|
|
627
|
+
}
|
|
628
|
+
if (result.errors && result.errors.length > 0) {
|
|
629
|
+
lines.push(`Errors: ${result.errors.length}`);
|
|
630
|
+
}
|
|
631
|
+
const previewData = {};
|
|
632
|
+
if (result.documents && result.documents.mappings) {
|
|
633
|
+
for (const mapping of result.documents.mappings.slice(0, 3)) {
|
|
634
|
+
const docKey = mapping.reference;
|
|
635
|
+
previewData[mapping.name] = result.documents[docKey];
|
|
615
636
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
`
|
|
633
|
-
);
|
|
634
|
-
},
|
|
635
|
-
onRateLimit: (event) => {
|
|
636
|
-
io.stderr(
|
|
637
|
-
`Polling execution ${event.executionResultId}: rate limited (attempt ${event.attempt}, retrying in ${formatElapsedSeconds(event.retryAfterMs)})
|
|
638
|
-
`
|
|
639
|
-
);
|
|
637
|
+
}
|
|
638
|
+
const preview = formatResultsPreview(previewData);
|
|
639
|
+
if (preview) lines.push("", "Result preview:", preview);
|
|
640
|
+
lines.push("", "Use --json to print the complete result payload.");
|
|
641
|
+
return `${lines.join("\n")}
|
|
642
|
+
`;
|
|
643
|
+
}
|
|
644
|
+
function formatRuntimeError(err) {
|
|
645
|
+
return formatHumanError(err);
|
|
646
|
+
}
|
|
647
|
+
function formatHumanError(err) {
|
|
648
|
+
if (err instanceof DocmanaExecutionError) {
|
|
649
|
+
const lines = [`Error: ${err.message}`];
|
|
650
|
+
if (err.errors && err.errors.length > 0) {
|
|
651
|
+
lines.push("Global errors:");
|
|
652
|
+
for (const e of err.errors) {
|
|
653
|
+
lines.push(` - ${formatSingleError(e)}`);
|
|
640
654
|
}
|
|
641
|
-
});
|
|
642
|
-
if (options.json) {
|
|
643
|
-
io.stdout(`${JSON.stringify(result, null, 2)}
|
|
644
|
-
`);
|
|
645
|
-
return;
|
|
646
655
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
656
|
+
const result = err.result;
|
|
657
|
+
if (result?.documents?.mappings && result.documents.mappings.length > 0) {
|
|
658
|
+
lines.push("Document execution results:");
|
|
659
|
+
for (const mapping of result.documents.mappings) {
|
|
660
|
+
const docKey = mapping.reference;
|
|
661
|
+
const docName = mapping.name;
|
|
662
|
+
const docResult = result.documents[docKey];
|
|
663
|
+
if (!docResult) {
|
|
664
|
+
lines.push(` - Document: ${docName} (Status: Unknown)`);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const status = docResult.status || "Unknown";
|
|
668
|
+
const nodes = [];
|
|
669
|
+
if (docResult.classifications) nodes.push(...Object.values(docResult.classifications));
|
|
670
|
+
if (docResult.metadataExtraction) nodes.push(docResult.metadataExtraction);
|
|
671
|
+
if (docResult.extractions) nodes.push(...Object.values(docResult.extractions));
|
|
672
|
+
if (docResult.validations) nodes.push(...Object.values(docResult.validations));
|
|
673
|
+
const hasNodeFailures = nodes.some(
|
|
674
|
+
(n) => n.status === "Failed" || n.errors && n.errors.length > 0
|
|
675
|
+
);
|
|
676
|
+
lines.push(` - Document: ${docName} (Status: ${status})`);
|
|
677
|
+
if (docResult.status !== "Failed" && !hasNodeFailures) continue;
|
|
678
|
+
if (docResult.classifications) {
|
|
679
|
+
for (const [nodeName, node] of Object.entries(docResult.classifications)) {
|
|
680
|
+
if (node.errors && node.errors.length > 0) {
|
|
681
|
+
lines.push(` Classification: ${nodeName} (Status: ${node.status})`);
|
|
682
|
+
for (const nodeErr of node.errors) lines.push(` - ${nodeErr}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (docResult.metadataExtraction?.errors?.length) {
|
|
687
|
+
lines.push(` MetadataExtraction: (Status: ${docResult.metadataExtraction.status})`);
|
|
688
|
+
for (const nodeErr of docResult.metadataExtraction.errors) {
|
|
689
|
+
lines.push(` - ${nodeErr}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (docResult.extractions) {
|
|
693
|
+
for (const [nodeName, node] of Object.entries(docResult.extractions)) {
|
|
694
|
+
if (node.status === "Failed" || node.errors && node.errors.length > 0) {
|
|
695
|
+
lines.push(` Extraction: ${nodeName} (Status: ${node.status})`);
|
|
696
|
+
if (node.errors) {
|
|
697
|
+
for (const nodeErr of node.errors) lines.push(` - ${nodeErr}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (docResult.validations) {
|
|
703
|
+
for (const [nodeName, node] of Object.entries(docResult.validations)) {
|
|
704
|
+
if (node.status === "Failed" || node.errors && node.errors.length > 0) {
|
|
705
|
+
lines.push(` Validation: ${nodeName} (Status: ${node.status})`);
|
|
706
|
+
if (node.errors) {
|
|
707
|
+
for (const nodeErr of node.errors) lines.push(` - ${nodeErr}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
661
711
|
}
|
|
662
|
-
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
663
|
-
`);
|
|
664
712
|
}
|
|
665
|
-
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
666
713
|
}
|
|
667
|
-
|
|
714
|
+
return lines.join("\n");
|
|
668
715
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const cached = await readFreshCachedToken(tokenCache);
|
|
672
|
-
if (cached) return cached;
|
|
673
|
-
const clientId = firstNonEmpty(env.DOCMANA_CLIENT_ID, cliConfig.clientId);
|
|
674
|
-
const clientSecret = firstNonEmpty(env.DOCMANA_CLIENT_SECRET, cliConfig.clientSecret);
|
|
675
|
-
if (!clientId || !clientSecret) {
|
|
676
|
-
throw new CliUsageError(
|
|
677
|
-
"Missing access token. Use --access-token, DOCMANA_ACCESS_TOKEN, docmana login, or configure OAuth credentials."
|
|
678
|
-
);
|
|
716
|
+
if (err instanceof DocmanaError) {
|
|
717
|
+
return `Error: ${err.message} (Code: ${err.code})`;
|
|
679
718
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
scope: firstNonEmpty(env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope,
|
|
685
|
-
fetchImpl: deps.fetch ?? fetch,
|
|
686
|
-
tokenCache
|
|
687
|
-
});
|
|
688
|
-
const token = await tokenManager.getToken();
|
|
689
|
-
io.stderr("Generated new access token and updated the token cache.\n");
|
|
690
|
-
return token;
|
|
719
|
+
if (err instanceof Error) {
|
|
720
|
+
return `Error: ${err.message}`;
|
|
721
|
+
}
|
|
722
|
+
return `Error: ${String(err)}`;
|
|
691
723
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
return
|
|
724
|
+
function formatResultsPreview(results) {
|
|
725
|
+
if (!results) return void 0;
|
|
726
|
+
const preview = JSON.stringify(results, null, 2);
|
|
727
|
+
if (!preview) return void 0;
|
|
728
|
+
return preview.length > 2e3 ? `${preview.slice(0, 2e3)}
|
|
729
|
+
...` : preview;
|
|
696
730
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
});
|
|
710
|
-
await tokenManager.getToken();
|
|
711
|
-
const cached = await tokenCache.read();
|
|
712
|
-
if (!hadFreshCachedToken) {
|
|
713
|
-
io.stdout("Generated new access token and updated the token cache.\n");
|
|
731
|
+
function formatSingleError(e) {
|
|
732
|
+
if (typeof e === "string") return e;
|
|
733
|
+
if (e && typeof e === "object") {
|
|
734
|
+
const obj = e;
|
|
735
|
+
if (typeof obj.msg === "string") return obj.msg;
|
|
736
|
+
if (typeof obj.message === "string") return obj.message;
|
|
737
|
+
if (typeof obj.error === "string") return obj.error;
|
|
738
|
+
try {
|
|
739
|
+
return JSON.stringify(e);
|
|
740
|
+
} catch {
|
|
741
|
+
return String(e);
|
|
742
|
+
}
|
|
714
743
|
}
|
|
715
|
-
|
|
716
|
-
`);
|
|
717
|
-
if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
|
|
744
|
+
return String(e);
|
|
718
745
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
746
|
+
|
|
747
|
+
// src/cli/types.ts
|
|
748
|
+
var DEFAULT_CONFIG_FILE = "docmana.config.json";
|
|
749
|
+
var DEFAULT_TOKEN_CACHE_FILE = "docmana.token.json";
|
|
750
|
+
var HELP = {
|
|
751
|
+
root: "Run Docmana document-analysis flows.",
|
|
752
|
+
flow: "Manage and run Docmana flows.",
|
|
753
|
+
organization: "Manage Docmana organizations.",
|
|
754
|
+
organizationDocument: "Manage organization documents.",
|
|
755
|
+
config: "Manage Docmana CLI configuration.",
|
|
756
|
+
configInit: "Create or update a local Docmana CLI config file.",
|
|
757
|
+
login: "Acquire and cache a Docmana OAuth2 access token.",
|
|
758
|
+
run: "Upload documents, run a Docmana flow, wait for completion, and print the result.",
|
|
759
|
+
list: "List Docmana flows available to the Bearer token.",
|
|
760
|
+
metadata: "Print Docmana flow runtime metadata as JSON.",
|
|
761
|
+
definition: "Print Docmana flow definition as JSON.",
|
|
762
|
+
organizationList: "List Docmana organizations available to the Bearer token.",
|
|
763
|
+
organizationDocumentList: "List organization documents available to the Bearer token.",
|
|
764
|
+
organizationDocumentDownload: "Download an organization document."
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/cli/config-store.ts
|
|
768
|
+
import { existsSync } from "fs";
|
|
769
|
+
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
770
|
+
import { dirname, resolve } from "path";
|
|
771
|
+
import { createInterface } from "readline/promises";
|
|
772
|
+
|
|
773
|
+
// src/cli/utils.ts
|
|
774
|
+
function getCwd(deps) {
|
|
775
|
+
return deps.cwd ?? process.cwd();
|
|
725
776
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
|
|
729
|
-
const clientSecret = firstNonEmpty(
|
|
730
|
-
options.clientSecret,
|
|
731
|
-
env.DOCMANA_CLIENT_SECRET,
|
|
732
|
-
cliConfig.clientSecret
|
|
733
|
-
);
|
|
734
|
-
const tokenEndpoint = firstNonEmpty(options.tokenUrl, env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint;
|
|
735
|
-
const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope;
|
|
736
|
-
if (!clientId)
|
|
737
|
-
throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
|
|
738
|
-
if (!clientSecret) {
|
|
739
|
-
throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
|
|
740
|
-
}
|
|
741
|
-
return { clientId, clientSecret, tokenEndpoint, scope };
|
|
777
|
+
function firstNonEmpty(...values) {
|
|
778
|
+
return values.map((value) => value?.trim()).find(Boolean);
|
|
742
779
|
}
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const defaultPrompt = deps.prompt ? void 0 : createDefaultPrompt();
|
|
748
|
-
const prompt = deps.prompt ?? defaultPrompt?.prompt;
|
|
749
|
-
if (!prompt) throw new CliUsageError("Unable to initialize interactive prompt");
|
|
750
|
-
try {
|
|
751
|
-
const config = {
|
|
752
|
-
apiBaseUrl: await prompt("API base URL", {
|
|
753
|
-
defaultValue: existing.apiBaseUrl
|
|
754
|
-
}),
|
|
755
|
-
tokenEndpoint: await prompt("OAuth2 token endpoint", {
|
|
756
|
-
defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
|
|
757
|
-
}),
|
|
758
|
-
scope: await prompt("OAuth2 scope", {
|
|
759
|
-
defaultValue: existing.scope ?? DEFAULTS.scope
|
|
760
|
-
}),
|
|
761
|
-
clientId: await prompt("Client id", {
|
|
762
|
-
defaultValue: existing.clientId
|
|
763
|
-
}),
|
|
764
|
-
clientSecret: await prompt("Client secret", {
|
|
765
|
-
defaultValue: existing.clientSecret,
|
|
766
|
-
secret: true
|
|
767
|
-
})
|
|
768
|
-
};
|
|
769
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
770
|
-
await writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
771
|
-
`, "utf8");
|
|
772
|
-
io.stdout(`Wrote ${configPath}
|
|
773
|
-
`);
|
|
774
|
-
io.stdout(
|
|
775
|
-
`Warning: ${DEFAULT_CONFIG_FILE} contains credentials and should stay ignored by Git.
|
|
776
|
-
`
|
|
777
|
-
);
|
|
778
|
-
} finally {
|
|
779
|
-
defaultPrompt?.close();
|
|
780
|
+
function parseFiles(filesCsv) {
|
|
781
|
+
const files = filesCsv?.split(",").map((file) => file.trim()).filter(Boolean) ?? [];
|
|
782
|
+
if (files.length === 0) {
|
|
783
|
+
throw new CliUsageError("Missing files. Use --files <path[,path]>.");
|
|
780
784
|
}
|
|
785
|
+
return files;
|
|
781
786
|
}
|
|
787
|
+
function formatElapsedSeconds(elapsedMs) {
|
|
788
|
+
return `${Math.floor(elapsedMs / 1e3)}s`;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/cli/config-store.ts
|
|
782
792
|
async function loadCliConfig(configPathOption, cwd, requireExisting) {
|
|
783
793
|
const configPath = resolveConfigPath(configPathOption, cwd);
|
|
784
794
|
if (!existsSync(configPath)) {
|
|
@@ -810,18 +820,12 @@ function normalizeCliConfig(config) {
|
|
|
810
820
|
clientSecret: stringValue(config.clientSecret)
|
|
811
821
|
};
|
|
812
822
|
}
|
|
813
|
-
function stringValue(value) {
|
|
814
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
815
|
-
}
|
|
816
823
|
function resolveConfigPath(configPathOption, cwd) {
|
|
817
824
|
return resolve(cwd, configPathOption ?? DEFAULT_CONFIG_FILE);
|
|
818
825
|
}
|
|
819
826
|
function resolveTokenCachePath(tokenCachePathOption, cwd) {
|
|
820
827
|
return resolve(cwd, tokenCachePathOption ?? DEFAULT_TOKEN_CACHE_FILE);
|
|
821
828
|
}
|
|
822
|
-
function getCwd(deps) {
|
|
823
|
-
return deps.cwd ?? process.cwd();
|
|
824
|
-
}
|
|
825
829
|
function createFileTokenCache(path) {
|
|
826
830
|
return {
|
|
827
831
|
async read() {
|
|
@@ -854,6 +858,11 @@ function createFileTokenCache(path) {
|
|
|
854
858
|
}
|
|
855
859
|
};
|
|
856
860
|
}
|
|
861
|
+
async function writeCliConfig(configPath, config) {
|
|
862
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
863
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
864
|
+
`, "utf8");
|
|
865
|
+
}
|
|
857
866
|
function createDefaultPrompt() {
|
|
858
867
|
if (!process.stdin.isTTY) {
|
|
859
868
|
let index = 0;
|
|
@@ -877,169 +886,527 @@ function createDefaultPrompt() {
|
|
|
877
886
|
close: () => rl.close()
|
|
878
887
|
};
|
|
879
888
|
}
|
|
889
|
+
function stringValue(value) {
|
|
890
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
891
|
+
}
|
|
880
892
|
function formatPrompt(label, options) {
|
|
881
893
|
const defaultHint = options.defaultValue && !options.secret ? ` [${options.defaultValue}]` : options.defaultValue && options.secret ? " [current value hidden]" : "";
|
|
882
894
|
return `${label}${defaultHint}: `;
|
|
883
895
|
}
|
|
884
|
-
function formatElapsedSeconds(elapsedMs) {
|
|
885
|
-
return `${Math.floor(elapsedMs / 1e3)}s`;
|
|
886
|
-
}
|
|
887
896
|
async function readStdinLines() {
|
|
888
897
|
let input = "";
|
|
889
898
|
for await (const chunk of process.stdin) input += String(chunk);
|
|
890
899
|
return input.split(/\r?\n/);
|
|
891
900
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
901
|
+
|
|
902
|
+
// src/cli/commands/config-init.ts
|
|
903
|
+
async function configInitCommand(options, io, deps) {
|
|
904
|
+
const cwd = getCwd(deps);
|
|
905
|
+
const configPath = resolveConfigPath(options.config, cwd);
|
|
906
|
+
const existing = await loadCliConfig(options.config, cwd, false);
|
|
907
|
+
const defaultPrompt = deps.prompt ? void 0 : createDefaultPrompt();
|
|
908
|
+
const prompt = deps.prompt ?? defaultPrompt?.prompt;
|
|
909
|
+
if (!prompt) throw new CliUsageError("Unable to initialize interactive prompt");
|
|
910
|
+
try {
|
|
911
|
+
const config = {
|
|
912
|
+
apiBaseUrl: await prompt("API base URL", {
|
|
913
|
+
defaultValue: existing.apiBaseUrl
|
|
914
|
+
}),
|
|
915
|
+
tokenEndpoint: await prompt("OAuth2 token endpoint", {
|
|
916
|
+
defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
|
|
917
|
+
}),
|
|
918
|
+
scope: await prompt("OAuth2 scope", {
|
|
919
|
+
defaultValue: existing.scope ?? DEFAULTS.scope
|
|
920
|
+
}),
|
|
921
|
+
clientId: await prompt("Client id", {
|
|
922
|
+
defaultValue: existing.clientId
|
|
923
|
+
}),
|
|
924
|
+
clientSecret: await prompt("Client secret", {
|
|
925
|
+
defaultValue: existing.clientSecret,
|
|
926
|
+
secret: true
|
|
927
|
+
})
|
|
928
|
+
};
|
|
929
|
+
await writeCliConfig(configPath, config);
|
|
930
|
+
io.stdout(`Wrote ${configPath}
|
|
931
|
+
`);
|
|
932
|
+
io.stdout(
|
|
933
|
+
`Warning: ${DEFAULT_CONFIG_FILE} contains credentials and should stay ignored by Git.
|
|
934
|
+
`
|
|
935
|
+
);
|
|
936
|
+
} finally {
|
|
937
|
+
defaultPrompt?.close();
|
|
938
|
+
}
|
|
896
939
|
}
|
|
897
|
-
|
|
898
|
-
|
|
940
|
+
|
|
941
|
+
// src/cli/auth/token-manager.ts
|
|
942
|
+
var SAFETY_MARGIN_MS = 6e4;
|
|
943
|
+
var TokenManager = class {
|
|
944
|
+
constructor(opts) {
|
|
945
|
+
this.opts = opts;
|
|
946
|
+
}
|
|
947
|
+
opts;
|
|
948
|
+
token = null;
|
|
949
|
+
expiresAt = 0;
|
|
950
|
+
inflight = null;
|
|
951
|
+
invalidate() {
|
|
952
|
+
this.token = null;
|
|
953
|
+
this.expiresAt = 0;
|
|
954
|
+
void this.opts.tokenCache?.clear().catch(() => void 0);
|
|
955
|
+
}
|
|
956
|
+
async getToken() {
|
|
957
|
+
if (this.token && Date.now() < this.expiresAt) return this.token;
|
|
958
|
+
const cached = await this.readCachedToken();
|
|
959
|
+
if (cached) return cached;
|
|
960
|
+
if (this.inflight) return this.inflight;
|
|
961
|
+
this.inflight = this.acquire().finally(() => {
|
|
962
|
+
this.inflight = null;
|
|
963
|
+
});
|
|
964
|
+
return this.inflight;
|
|
965
|
+
}
|
|
966
|
+
async acquire() {
|
|
967
|
+
const body = new URLSearchParams({
|
|
968
|
+
grant_type: "client_credentials",
|
|
969
|
+
client_id: this.opts.clientId,
|
|
970
|
+
client_secret: this.opts.clientSecret,
|
|
971
|
+
scope: this.opts.scope
|
|
972
|
+
});
|
|
973
|
+
const res = await this.opts.fetchImpl(this.opts.tokenEndpoint, {
|
|
974
|
+
method: "POST",
|
|
975
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
976
|
+
body: body.toString()
|
|
977
|
+
});
|
|
978
|
+
if (!res.ok) {
|
|
979
|
+
throw new DocmanaAuthError("Failed to acquire Docmana access token", { status: res.status });
|
|
980
|
+
}
|
|
981
|
+
const json = await res.json();
|
|
982
|
+
if (typeof json.access_token !== "string" || json.access_token.length === 0 || typeof json.expires_in !== "number" || !Number.isFinite(json.expires_in)) {
|
|
983
|
+
throw new DocmanaAuthError("Malformed token response from Docmana CIAM", {
|
|
984
|
+
status: res.status
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
this.token = json.access_token;
|
|
988
|
+
this.expiresAt = Date.now() + json.expires_in * 1e3 - SAFETY_MARGIN_MS;
|
|
989
|
+
await this.writeCachedToken().catch(() => void 0);
|
|
990
|
+
return this.token;
|
|
991
|
+
}
|
|
992
|
+
async readCachedToken() {
|
|
993
|
+
if (!this.opts.tokenCache) return null;
|
|
994
|
+
let entry;
|
|
995
|
+
try {
|
|
996
|
+
entry = await this.opts.tokenCache.read();
|
|
997
|
+
} catch {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
if (!entry) return null;
|
|
1001
|
+
if (entry.clientId !== this.opts.clientId || entry.tokenEndpoint !== this.opts.tokenEndpoint || entry.scope !== this.opts.scope || Date.now() >= entry.expiresAt) {
|
|
1002
|
+
await this.opts.tokenCache.clear().catch(() => void 0);
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
this.token = entry.accessToken;
|
|
1006
|
+
this.expiresAt = entry.expiresAt;
|
|
1007
|
+
return this.token;
|
|
1008
|
+
}
|
|
1009
|
+
async writeCachedToken() {
|
|
1010
|
+
if (!this.opts.tokenCache || !this.token) return;
|
|
1011
|
+
await this.opts.tokenCache.write({
|
|
1012
|
+
accessToken: this.token,
|
|
1013
|
+
expiresAt: this.expiresAt,
|
|
1014
|
+
clientId: this.opts.clientId,
|
|
1015
|
+
tokenEndpoint: this.opts.tokenEndpoint,
|
|
1016
|
+
scope: this.opts.scope
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
// src/cli/auth/access-token.ts
|
|
1022
|
+
async function resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io) {
|
|
1023
|
+
const cached = await readFreshCachedToken(tokenCache);
|
|
1024
|
+
if (cached) return cached;
|
|
1025
|
+
const clientId = firstNonEmpty(env.DOCMANA_CLIENT_ID, cliConfig.clientId);
|
|
1026
|
+
const clientSecret = firstNonEmpty(env.DOCMANA_CLIENT_SECRET, cliConfig.clientSecret);
|
|
1027
|
+
if (!clientId || !clientSecret) {
|
|
1028
|
+
throw new CliUsageError(
|
|
1029
|
+
"Missing access token. Use --access-token, DOCMANA_ACCESS_TOKEN, docmana login, or configure OAuth credentials."
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
const tokenManager = new TokenManager({
|
|
1033
|
+
clientId,
|
|
1034
|
+
clientSecret,
|
|
1035
|
+
tokenEndpoint: firstNonEmpty(env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint,
|
|
1036
|
+
scope: firstNonEmpty(env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope,
|
|
1037
|
+
fetchImpl: deps.fetch ?? fetch,
|
|
1038
|
+
tokenCache
|
|
1039
|
+
});
|
|
1040
|
+
const token = await tokenManager.getToken();
|
|
1041
|
+
io.stderr("Generated new access token and updated the token cache.\n");
|
|
1042
|
+
return token;
|
|
899
1043
|
}
|
|
900
|
-
function
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1044
|
+
async function readFreshCachedToken(tokenCache) {
|
|
1045
|
+
const cached = await tokenCache.read().catch(() => null);
|
|
1046
|
+
if (!cached || Date.now() >= cached.expiresAt) return void 0;
|
|
1047
|
+
return cached.accessToken;
|
|
1048
|
+
}
|
|
1049
|
+
async function resolveAuthConfig(options, env, deps) {
|
|
1050
|
+
const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
|
|
1051
|
+
const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
|
|
1052
|
+
const clientSecret = firstNonEmpty(
|
|
1053
|
+
options.clientSecret,
|
|
1054
|
+
env.DOCMANA_CLIENT_SECRET,
|
|
1055
|
+
cliConfig.clientSecret
|
|
1056
|
+
);
|
|
1057
|
+
const tokenEndpoint = firstNonEmpty(options.tokenUrl, env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint;
|
|
1058
|
+
const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope;
|
|
1059
|
+
if (!clientId) {
|
|
1060
|
+
throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
|
|
906
1061
|
}
|
|
907
|
-
if (
|
|
908
|
-
|
|
1062
|
+
if (!clientSecret) {
|
|
1063
|
+
throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
|
|
909
1064
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1065
|
+
return { clientId, clientSecret, tokenEndpoint, scope };
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/cli/commands/api-client.ts
|
|
1069
|
+
async function resolveApiClient(options, env, io, clientFactory, deps) {
|
|
1070
|
+
const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
|
|
1071
|
+
const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
|
|
1072
|
+
if (!apiBaseUrl) {
|
|
1073
|
+
throw new CliUsageError("Missing API base URL. Use --api-url or DOCMANA_API_URL.");
|
|
1074
|
+
}
|
|
1075
|
+
const tokenCache = createFileTokenCache(resolveTokenCachePath(options.tokenCache, getCwd(deps)));
|
|
1076
|
+
const accessToken = firstNonEmpty(options.accessToken, env.DOCMANA_ACCESS_TOKEN) ?? await resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io);
|
|
1077
|
+
return clientFactory({ apiBaseUrl, accessToken });
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/cli/commands/flow-definition.ts
|
|
1081
|
+
async function flowDefinitionCommand(flowId, options, env, io, clientFactory, deps) {
|
|
1082
|
+
try {
|
|
1083
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1084
|
+
const definition = await client.getFlowDefinition(flowId);
|
|
1085
|
+
io.stdout(`${JSON.stringify(definition, null, 2)}
|
|
1086
|
+
`);
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
const errorPayload = {
|
|
1089
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1090
|
+
};
|
|
1091
|
+
if (err instanceof DocmanaError) {
|
|
1092
|
+
errorPayload.code = err.code;
|
|
1093
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1094
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
915
1095
|
}
|
|
1096
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1097
|
+
`);
|
|
1098
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
916
1099
|
}
|
|
917
|
-
const preview = formatResultsPreview(previewData);
|
|
918
|
-
if (preview) lines.push("", "Result preview:", preview);
|
|
919
|
-
lines.push("", "Use --json to print the complete result payload.");
|
|
920
|
-
return `${lines.join("\n")}
|
|
921
|
-
`;
|
|
922
1100
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1101
|
+
|
|
1102
|
+
// src/cli/commands/flow-list.ts
|
|
1103
|
+
async function listFlowsCommand(options, env, io, clientFactory, deps) {
|
|
1104
|
+
try {
|
|
1105
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1106
|
+
const flows = await client.listFlows();
|
|
1107
|
+
if (options.json) {
|
|
1108
|
+
io.stdout(`${JSON.stringify(flows, null, 2)}
|
|
1109
|
+
`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
io.stdout(formatFlowList(flows));
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
if (options.json) {
|
|
1115
|
+
const errorPayload = {
|
|
1116
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1117
|
+
};
|
|
1118
|
+
if (err instanceof DocmanaError) {
|
|
1119
|
+
errorPayload.code = err.code;
|
|
1120
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1121
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
1122
|
+
}
|
|
1123
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1124
|
+
`);
|
|
1125
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
1126
|
+
}
|
|
1127
|
+
throw err;
|
|
1128
|
+
}
|
|
929
1129
|
}
|
|
930
|
-
|
|
931
|
-
|
|
1130
|
+
|
|
1131
|
+
// src/cli/commands/flow-metadata.ts
|
|
1132
|
+
async function flowMetadataCommand(flowId, options, env, io, clientFactory, deps) {
|
|
1133
|
+
try {
|
|
1134
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1135
|
+
const metadata = await client.getFlowMetadata(flowId);
|
|
1136
|
+
io.stdout(`${JSON.stringify(metadata, null, 2)}
|
|
1137
|
+
`);
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
const errorPayload = {
|
|
1140
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1141
|
+
};
|
|
1142
|
+
if (err instanceof DocmanaError) {
|
|
1143
|
+
errorPayload.code = err.code;
|
|
1144
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1145
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
1146
|
+
}
|
|
1147
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1148
|
+
`);
|
|
1149
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
1150
|
+
}
|
|
932
1151
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1152
|
+
|
|
1153
|
+
// src/cli/commands/organization-document-download.ts
|
|
1154
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1155
|
+
import { dirname as dirname2, join } from "path";
|
|
1156
|
+
async function organizationDocumentDownloadCommand(path, options, env, io, clientFactory, deps) {
|
|
1157
|
+
try {
|
|
1158
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1159
|
+
const document = await client.downloadOrganizationDocument(path);
|
|
1160
|
+
const outputPath = options.output ?? join(getCwd(deps), path);
|
|
1161
|
+
await mkdir2(dirname2(outputPath), { recursive: true });
|
|
1162
|
+
await writeFile2(outputPath, document.bytes);
|
|
1163
|
+
io.stdout(`Downloaded ${document.filename} to ${outputPath}
|
|
1164
|
+
`);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
const errorPayload = {
|
|
1167
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1168
|
+
};
|
|
1169
|
+
if (err instanceof DocmanaError) {
|
|
1170
|
+
errorPayload.code = err.code;
|
|
1171
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1172
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
1173
|
+
}
|
|
1174
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1175
|
+
`);
|
|
1176
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/cli/commands/organization-document-list.ts
|
|
1181
|
+
async function organizationDocumentListCommand(options, env, io, clientFactory, deps) {
|
|
1182
|
+
try {
|
|
1183
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1184
|
+
const payload = await client.listOrganizationDocuments({ prefix: options.prefix });
|
|
1185
|
+
if (options.json) {
|
|
1186
|
+
io.stdout(`${JSON.stringify(payload, null, 2)}
|
|
1187
|
+
`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (!payload.configured) {
|
|
1191
|
+
io.stdout("Organization document depot is not configured.\n");
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
io.stdout(formatOrganizationDocumentList(payload.documents));
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
const errorPayload = {
|
|
1197
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1198
|
+
};
|
|
1199
|
+
if (err instanceof DocmanaError) {
|
|
1200
|
+
errorPayload.code = err.code;
|
|
1201
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1202
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
1203
|
+
}
|
|
1204
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1205
|
+
`);
|
|
1206
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/cli/commands/organization-list.ts
|
|
1211
|
+
async function listOrganizationsCommand(options, env, io, clientFactory, deps) {
|
|
1212
|
+
try {
|
|
1213
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1214
|
+
const organizations = await client.listOrganizations();
|
|
1215
|
+
if (options.json) {
|
|
1216
|
+
io.stdout(`${JSON.stringify(organizations, null, 2)}
|
|
1217
|
+
`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
io.stdout(formatOrganizationList(organizations));
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
if (options.json) {
|
|
1223
|
+
const errorPayload = {
|
|
1224
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1225
|
+
};
|
|
1226
|
+
if (err instanceof DocmanaError) {
|
|
1227
|
+
errorPayload.code = err.code;
|
|
1228
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1229
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
940
1230
|
}
|
|
1231
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1232
|
+
`);
|
|
1233
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
941
1234
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
if (docResult.metadataExtraction?.errors && docResult.metadataExtraction.errors.length > 0) {
|
|
973
|
-
lines.push(
|
|
974
|
-
` MetadataExtraction: (Status: ${docResult.metadataExtraction.status})`
|
|
975
|
-
);
|
|
976
|
-
for (const nodeErr of docResult.metadataExtraction.errors) {
|
|
977
|
-
lines.push(` - ${nodeErr}`);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
if (docResult.extractions) {
|
|
981
|
-
for (const [nodeName, node] of Object.entries(docResult.extractions)) {
|
|
982
|
-
if (node.status === "Failed" || node.errors && node.errors.length > 0) {
|
|
983
|
-
lines.push(` Extraction: ${nodeName} (Status: ${node.status})`);
|
|
984
|
-
if (node.errors) {
|
|
985
|
-
for (const nodeErr of node.errors) {
|
|
986
|
-
lines.push(` - ${nodeErr}`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
if (docResult.validations) {
|
|
993
|
-
for (const [nodeName, node] of Object.entries(docResult.validations)) {
|
|
994
|
-
if (node.status === "Failed" || node.errors && node.errors.length > 0) {
|
|
995
|
-
lines.push(` Validation: ${nodeName} (Status: ${node.status})`);
|
|
996
|
-
if (node.errors) {
|
|
997
|
-
for (const nodeErr of node.errors) {
|
|
998
|
-
lines.push(` - ${nodeErr}`);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
} else {
|
|
1005
|
-
lines.push(` - Document: ${docName} (Status: ${status})`);
|
|
1006
|
-
}
|
|
1007
|
-
} else {
|
|
1008
|
-
lines.push(` - Document: ${docName} (Status: Unknown)`);
|
|
1009
|
-
}
|
|
1235
|
+
throw err;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/cli/commands/flow-run.ts
|
|
1240
|
+
async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
|
|
1241
|
+
try {
|
|
1242
|
+
const files = parseFiles(options.files);
|
|
1243
|
+
const client = await resolveApiClient(options, env, io, clientFactory, deps);
|
|
1244
|
+
const result = await client.runFlow(
|
|
1245
|
+
flowId,
|
|
1246
|
+
{
|
|
1247
|
+
files: files.map((path) => ({ path })),
|
|
1248
|
+
language: options.language,
|
|
1249
|
+
useDraftVersion: options.draft === true
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
onPoll: (event) => {
|
|
1253
|
+
const nextPoll = event.nextPollAfterMs === void 0 ? "" : `, next poll in ${formatElapsedSeconds(event.nextPollAfterMs)}`;
|
|
1254
|
+
io.stderr(
|
|
1255
|
+
`Polling execution ${event.executionResultId}: ${event.status} (attempt ${event.attempt}, elapsed ${formatElapsedSeconds(event.elapsedMs)}${nextPoll})
|
|
1256
|
+
`
|
|
1257
|
+
);
|
|
1258
|
+
},
|
|
1259
|
+
onRateLimit: (event) => {
|
|
1260
|
+
io.stderr(
|
|
1261
|
+
`Polling execution ${event.executionResultId}: rate limited (attempt ${event.attempt}, retrying in ${formatElapsedSeconds(event.retryAfterMs)})
|
|
1262
|
+
`
|
|
1263
|
+
);
|
|
1010
1264
|
}
|
|
1011
1265
|
}
|
|
1266
|
+
);
|
|
1267
|
+
if (options.json) {
|
|
1268
|
+
io.stdout(`${JSON.stringify(result, null, 2)}
|
|
1269
|
+
`);
|
|
1270
|
+
return;
|
|
1012
1271
|
}
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1272
|
+
io.stdout(formatHumanResult(result));
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
if (options.json) {
|
|
1275
|
+
if (err instanceof DocmanaExecutionError && err.result) {
|
|
1276
|
+
io.stdout(`${JSON.stringify(err.result, null, 2)}
|
|
1277
|
+
`);
|
|
1278
|
+
} else {
|
|
1279
|
+
const errorPayload = {
|
|
1280
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1281
|
+
};
|
|
1282
|
+
if (err instanceof DocmanaError) {
|
|
1283
|
+
errorPayload.code = err.code;
|
|
1284
|
+
if (err.status !== void 0) errorPayload.status = err.status;
|
|
1285
|
+
if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
|
|
1286
|
+
}
|
|
1287
|
+
io.stdout(`${JSON.stringify(errorPayload, null, 2)}
|
|
1288
|
+
`);
|
|
1289
|
+
}
|
|
1290
|
+
throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
|
|
1291
|
+
}
|
|
1292
|
+
throw err;
|
|
1017
1293
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// src/cli/commands/login.ts
|
|
1297
|
+
async function loginCommand(options, env, io, deps) {
|
|
1298
|
+
const auth = await resolveAuthConfig(options, env, deps);
|
|
1299
|
+
const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
|
|
1300
|
+
const tokenCache = createFileTokenCache(tokenCachePath);
|
|
1301
|
+
const hadFreshCachedToken = Boolean(await readFreshCachedToken(tokenCache));
|
|
1302
|
+
const tokenManager = new TokenManager({
|
|
1303
|
+
clientId: auth.clientId,
|
|
1304
|
+
clientSecret: auth.clientSecret,
|
|
1305
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
1306
|
+
scope: auth.scope,
|
|
1307
|
+
fetchImpl: deps.fetch ?? fetch,
|
|
1308
|
+
tokenCache
|
|
1309
|
+
});
|
|
1310
|
+
await tokenManager.getToken();
|
|
1311
|
+
const cached = await tokenCache.read();
|
|
1312
|
+
if (!hadFreshCachedToken) {
|
|
1313
|
+
io.stdout("Generated new access token and updated the token cache.\n");
|
|
1020
1314
|
}
|
|
1021
|
-
|
|
1315
|
+
io.stdout(`Logged in. Token cached at ${tokenCachePath}
|
|
1316
|
+
`);
|
|
1317
|
+
if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
|
|
1022
1318
|
}
|
|
1023
|
-
function
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1319
|
+
function formatTokenExpiry(expiresAt) {
|
|
1320
|
+
const expiresAtDate = new Date(expiresAt);
|
|
1321
|
+
return [
|
|
1322
|
+
`Token expires at local time ${expiresAtDate.toLocaleString()}`,
|
|
1323
|
+
`Token expires at UTC ${expiresAtDate.toISOString()}`
|
|
1324
|
+
].join("\n") + "\n";
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/cli/cli.ts
|
|
1328
|
+
async function runCli(argv = process.argv.slice(2), env = process.env, io = {
|
|
1329
|
+
stdout: (text) => process.stdout.write(text),
|
|
1330
|
+
stderr: (text) => process.stderr.write(text)
|
|
1331
|
+
}, clientFactory = (config) => new Docmana(config), deps = {}) {
|
|
1332
|
+
const program = buildProgram(env, io, clientFactory, deps);
|
|
1333
|
+
if (argv.length === 0) {
|
|
1334
|
+
io.stdout(program.helpInformation());
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
await program.parseAsync(argv, { from: "user" });
|
|
1339
|
+
return 0;
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
if (err instanceof CliSilentError) {
|
|
1342
|
+
return err.exitCode;
|
|
1343
|
+
}
|
|
1344
|
+
if (err instanceof CliUsageError) {
|
|
1345
|
+
io.stderr(`Error: ${err.message}
|
|
1346
|
+
`);
|
|
1347
|
+
return 2;
|
|
1348
|
+
}
|
|
1349
|
+
if (err instanceof CommanderError) {
|
|
1350
|
+
return err.exitCode === 0 ? 0 : 2;
|
|
1034
1351
|
}
|
|
1352
|
+
io.stderr(`${formatRuntimeError(err)}
|
|
1353
|
+
`);
|
|
1354
|
+
return 1;
|
|
1035
1355
|
}
|
|
1036
|
-
|
|
1356
|
+
}
|
|
1357
|
+
function buildProgram(env, io, clientFactory, deps) {
|
|
1358
|
+
const program = new Command();
|
|
1359
|
+
program.name("docmana").description(HELP.root).showHelpAfterError().configureOutput({
|
|
1360
|
+
writeOut: io.stdout,
|
|
1361
|
+
writeErr: io.stderr,
|
|
1362
|
+
outputError: (str, write) => write(str)
|
|
1363
|
+
}).exitOverride();
|
|
1364
|
+
program.command("login").description(HELP.login).option("--client-id <id>", "OAuth2 client id; defaults to DOCMANA_CLIENT_ID").option("--client-secret <secret>", "OAuth2 client secret; defaults to DOCMANA_CLIENT_SECRET").option("--token-url <url>", "OAuth2 token endpoint; defaults to DOCMANA_TOKEN_URL").option("--scope <scope>", "OAuth2 scope; defaults to DOCMANA_SCOPE").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).action(async (options) => {
|
|
1365
|
+
await loginCommand(options, env, io, deps);
|
|
1366
|
+
});
|
|
1367
|
+
const flow = program.command("flow").description(HELP.flow).action(() => {
|
|
1368
|
+
io.stdout(flow.helpInformation());
|
|
1369
|
+
});
|
|
1370
|
+
flow.command("list").description(HELP.list).option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON flow list").action(async (options) => {
|
|
1371
|
+
await listFlowsCommand(options, env, io, clientFactory, deps);
|
|
1372
|
+
});
|
|
1373
|
+
flow.command("run").description(HELP.run).argument("<flow-id>", "Docmana flow id").requiredOption("--files <csv>", "Comma-separated file paths to upload").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON result").option("--language <lang>", "Translate output to specified language").option("--draft", "Run the current draft flow version").action(async (flowId, options) => {
|
|
1374
|
+
await runFlowCommand(flowId, options, env, io, clientFactory, deps);
|
|
1375
|
+
});
|
|
1376
|
+
flow.command("metadata").description(HELP.metadata).argument("<flow-id>", "Docmana flow id").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).action(async (flowId, options) => {
|
|
1377
|
+
await flowMetadataCommand(flowId, options, env, io, clientFactory, deps);
|
|
1378
|
+
});
|
|
1379
|
+
flow.command("definition").description(HELP.definition).argument("<flow-id>", "Docmana flow id").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).action(async (flowId, options) => {
|
|
1380
|
+
await flowDefinitionCommand(flowId, options, env, io, clientFactory, deps);
|
|
1381
|
+
});
|
|
1382
|
+
const organization = program.command("organization").description(HELP.organization).action(() => {
|
|
1383
|
+
io.stdout(organization.helpInformation());
|
|
1384
|
+
});
|
|
1385
|
+
organization.command("list").description(HELP.organizationList).option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON organization list").action(async (options) => {
|
|
1386
|
+
await listOrganizationsCommand(options, env, io, clientFactory, deps);
|
|
1387
|
+
});
|
|
1388
|
+
const organizationDocument = organization.command("document").description(HELP.organizationDocument).action(() => {
|
|
1389
|
+
io.stdout(organizationDocument.helpInformation());
|
|
1390
|
+
});
|
|
1391
|
+
organizationDocument.command("list").description(HELP.organizationDocumentList).option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--prefix <prefix>", "Filter documents by path prefix").option("--json", "Print the raw JSON organization document list").action(async (options) => {
|
|
1392
|
+
await organizationDocumentListCommand(options, env, io, clientFactory, deps);
|
|
1393
|
+
});
|
|
1394
|
+
organizationDocument.command("download").description(HELP.organizationDocumentDownload).argument("<path>", "Organization document path").option("--output <file>", "Output file path; defaults to ./{path}").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).action(async (path, options) => {
|
|
1395
|
+
await organizationDocumentDownloadCommand(path, options, env, io, clientFactory, deps);
|
|
1396
|
+
});
|
|
1397
|
+
const config = program.command("config").description(HELP.config).action(() => {
|
|
1398
|
+
io.stdout(config.helpInformation());
|
|
1399
|
+
});
|
|
1400
|
+
config.command("init").description(HELP.configInit).option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).action(async (options) => {
|
|
1401
|
+
await configInitCommand(options, io, deps);
|
|
1402
|
+
});
|
|
1403
|
+
return program;
|
|
1037
1404
|
}
|
|
1038
1405
|
function isDirectRun() {
|
|
1039
1406
|
const entry = process.argv[1];
|
|
1040
1407
|
if (!entry) return false;
|
|
1041
1408
|
try {
|
|
1042
|
-
return realpathSync(
|
|
1409
|
+
return realpathSync(resolve2(entry)) === realpathSync(fileURLToPath(import.meta.url));
|
|
1043
1410
|
} catch {
|
|
1044
1411
|
return false;
|
|
1045
1412
|
}
|