@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/dist/cli.mjs CHANGED
@@ -1,14 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli.ts
4
- import { existsSync, realpathSync } from "fs";
5
- import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
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/errors.ts
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/auth/token-manager.ts
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, fallbackName) {
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 instanceof Uint8Array) {
325
- return {
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(buf)]),
335
- filename: fallbackName,
336
- contentType: contentTypeFor(fallbackName)
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 ?? (input.file !== void 0 ? [input.file] : []);
343
- if (list.length === 0) throw new Error("runFlow requires `file` or `files`");
344
- const fallback = input.fileName ?? "upload.bin";
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/polling/poll.ts
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, input) {
447
- const parts = await resolveInputs(input);
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
- input.signal,
466
- input.useDraftVersion ?? false,
467
- input.context,
468
- input.callbackUrl,
469
- input.language
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, input) {
479
- const { executionResultId } = await this.runFlowAsync(flowId, input);
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, input.signal);
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
- input.onPoll?.({
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
- input.onRateLimit?.({
519
+ options.onRateLimit?.({
500
520
  executionResultId,
501
521
  attempt,
502
522
  elapsedMs: Date.now() - startedAt,
503
- retryAfterMs: err.retryAfterMs ?? input.pollIntervalMs ?? this.config.pollIntervalMs
523
+ retryAfterMs: err.retryAfterMs ?? options.pollIntervalMs ?? this.config.pollIntervalMs
504
524
  });
505
525
  }
506
526
  throw err;
507
527
  }
508
528
  },
509
- intervalMs: input.pollIntervalMs ?? this.config.pollIntervalMs,
510
- maxIntervalMs: input.pollMaxIntervalMs ?? this.config.pollMaxIntervalMs,
511
- timeoutMs: input.timeoutMs ?? this.config.timeoutMs,
512
- signal: input.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, input.signal, input.language);
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
- var HELP = {
546
- root: "Run Docmana document-analysis flows.",
547
- flow: "Manage and run Docmana flows.",
548
- config: "Manage Docmana CLI configuration.",
549
- configInit: "Create or update a local Docmana CLI config file.",
550
- login: "Acquire and cache a Docmana OAuth2 access token.",
551
- run: "Upload documents, run a Docmana flow, wait for completion, and print the result."
552
- };
553
- var DEFAULT_CONFIG_FILE = "docmana.config.json";
554
- var DEFAULT_TOKEN_CACHE_FILE = "docmana.token.json";
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
- try {
565
- await program.parseAsync(argv, { from: "user" });
566
- return 0;
567
- } catch (err) {
568
- if (err instanceof CliSilentError) {
569
- return err.exitCode;
570
- }
571
- if (err instanceof CliUsageError) {
572
- io.stderr(`Error: ${err.message}
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 buildProgram(env, io, clientFactory, deps) {
585
- const program = new Command();
586
- program.name("docmana").description(HELP.root).showHelpAfterError().configureOutput({
587
- writeOut: io.stdout,
588
- writeErr: io.stderr,
589
- outputError: (str, write) => write(str)
590
- }).exitOverride();
591
- 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) => {
592
- await loginCommand(options, env, io, deps);
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
- async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
609
- try {
610
- const files = parseFiles(options.files);
611
- const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
612
- const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
613
- if (!apiBaseUrl) {
614
- throw new CliUsageError("Missing API base URL. Use --api-url or DOCMANA_API_URL.");
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
- const tokenCache = createFileTokenCache(
617
- resolveTokenCachePath(options.tokenCache, getCwd(deps))
618
- );
619
- const accessToken = firstNonEmpty(options.accessToken, env.DOCMANA_ACCESS_TOKEN) ?? await resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io);
620
- const client = clientFactory({
621
- apiBaseUrl,
622
- accessToken
623
- });
624
- const result = await client.runFlow(flowId, {
625
- files: files.map((path) => ({ path })),
626
- language: options.language,
627
- useDraftVersion: options.draft === true,
628
- onPoll: (event) => {
629
- const nextPoll = event.nextPollAfterMs === void 0 ? "" : `, next poll in ${formatElapsedSeconds(event.nextPollAfterMs)}`;
630
- io.stderr(
631
- `Polling execution ${event.executionResultId}: ${event.status} (attempt ${event.attempt}, elapsed ${formatElapsedSeconds(event.elapsedMs)}${nextPoll})
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
- io.stdout(formatHumanResult(result));
648
- } catch (err) {
649
- if (options.json) {
650
- if (err instanceof DocmanaExecutionError && err.result) {
651
- io.stdout(`${JSON.stringify(err.result, null, 2)}
652
- `);
653
- } else {
654
- const errorPayload = {
655
- error: err instanceof Error ? err.message : String(err)
656
- };
657
- if (err instanceof DocmanaError) {
658
- errorPayload.code = err.code;
659
- if (err.status !== void 0) errorPayload.status = err.status;
660
- if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
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
- throw err;
714
+ return lines.join("\n");
668
715
  }
669
- }
670
- async function resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io) {
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
- const tokenManager = new TokenManager({
681
- clientId,
682
- clientSecret,
683
- tokenEndpoint: firstNonEmpty(env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint,
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
- async function readFreshCachedToken(tokenCache) {
693
- const cached = await tokenCache.read().catch(() => null);
694
- if (!cached || Date.now() >= cached.expiresAt) return void 0;
695
- return cached.accessToken;
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
- async function loginCommand(options, env, io, deps) {
698
- const auth = await resolveAuthConfig(options, env, deps);
699
- const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
700
- const tokenCache = createFileTokenCache(tokenCachePath);
701
- const hadFreshCachedToken = Boolean(await readFreshCachedToken(tokenCache));
702
- const tokenManager = new TokenManager({
703
- clientId: auth.clientId,
704
- clientSecret: auth.clientSecret,
705
- tokenEndpoint: auth.tokenEndpoint,
706
- scope: auth.scope,
707
- fetchImpl: deps.fetch ?? fetch,
708
- tokenCache
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
- io.stdout(`Logged in. Token cached at ${tokenCachePath}
716
- `);
717
- if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
744
+ return String(e);
718
745
  }
719
- function formatTokenExpiry(expiresAt) {
720
- const expiresAtDate = new Date(expiresAt);
721
- return [
722
- `Token expires at local time ${expiresAtDate.toLocaleString()}`,
723
- `Token expires at UTC ${expiresAtDate.toISOString()}`
724
- ].join("\n") + "\n";
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
- async function resolveAuthConfig(options, env, deps) {
727
- const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
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
- async function configInitCommand(options, io, deps) {
744
- const cwd = getCwd(deps);
745
- const configPath = resolveConfigPath(options.config, cwd);
746
- const existing = await loadCliConfig(options.config, cwd, false);
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
- function parseFiles(filesCsv) {
893
- const files = filesCsv?.split(",").map((file) => file.trim()).filter(Boolean) ?? [];
894
- if (files.length === 0) throw new CliUsageError("Missing files. Use --files <path[,path]>.");
895
- return files;
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
- function firstNonEmpty(...values) {
898
- return values.map((value) => value?.trim()).find(Boolean);
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 formatHumanResult(result) {
901
- const lines = ["Docmana flow completed", `Status: ${String(result.status ?? "Unknown")}`];
902
- const executionId = result.executionId;
903
- if (executionId) lines.push(`Execution id: ${String(executionId)}`);
904
- if (result.documents && result.documents.mappings) {
905
- lines.push(`Results: ${result.documents.mappings.length}`);
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 (result.errors && result.errors.length > 0) {
908
- lines.push(`Errors: ${result.errors.length}`);
1062
+ if (!clientSecret) {
1063
+ throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
909
1064
  }
910
- const previewData = {};
911
- if (result.documents && result.documents.mappings) {
912
- for (const mapping of result.documents.mappings.slice(0, 3)) {
913
- const docKey = mapping.reference;
914
- previewData[mapping.name] = result.documents[docKey];
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
- function formatResultsPreview(results) {
924
- if (!results) return void 0;
925
- const preview = JSON.stringify(results, null, 2);
926
- if (!preview) return void 0;
927
- return preview.length > 2e3 ? `${preview.slice(0, 2e3)}
928
- ...` : preview;
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
- function formatRuntimeError(err) {
931
- return formatHumanError(err);
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
- function formatHumanError(err) {
934
- if (err instanceof DocmanaExecutionError) {
935
- const lines = [`Error: ${err.message}`];
936
- if (err.errors && err.errors.length > 0) {
937
- lines.push("Global errors:");
938
- for (const e of err.errors) {
939
- lines.push(` - ${formatSingleError(e)}`);
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
- const result = err.result;
943
- if (result) {
944
- if (result.documents && result.documents.mappings && result.documents.mappings.length > 0) {
945
- lines.push("Document execution results:");
946
- for (const mapping of result.documents.mappings) {
947
- const docKey = mapping.reference;
948
- const docName = mapping.name;
949
- const docResult = result.documents[docKey];
950
- if (docResult) {
951
- const status = docResult.status || "Unknown";
952
- const nodes = [];
953
- if (docResult.classifications) nodes.push(...Object.values(docResult.classifications));
954
- if (docResult.metadataExtraction) nodes.push(docResult.metadataExtraction);
955
- if (docResult.extractions) nodes.push(...Object.values(docResult.extractions));
956
- if (docResult.validations) nodes.push(...Object.values(docResult.validations));
957
- const hasNodeFailures = nodes.some(
958
- (n) => n.status === "Failed" || n.errors && n.errors.length > 0
959
- );
960
- if (docResult.status === "Failed" || hasNodeFailures) {
961
- lines.push(` - Document: ${docName} (Status: ${status})`);
962
- if (docResult.classifications) {
963
- for (const [nodeName, node] of Object.entries(docResult.classifications)) {
964
- if (node.errors && node.errors.length > 0) {
965
- lines.push(` Classification: ${nodeName} (Status: ${node.status})`);
966
- for (const nodeErr of node.errors) {
967
- lines.push(` - ${nodeErr}`);
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
- return lines.join("\n");
1014
- }
1015
- if (err instanceof DocmanaError) {
1016
- return `Error: ${err.message} (Code: ${err.code})`;
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
- if (err instanceof Error) {
1019
- return `Error: ${err.message}`;
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
- return `Error: ${String(err)}`;
1315
+ io.stdout(`Logged in. Token cached at ${tokenCachePath}
1316
+ `);
1317
+ if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
1022
1318
  }
1023
- function formatSingleError(e) {
1024
- if (typeof e === "string") return e;
1025
- if (e && typeof e === "object") {
1026
- const obj = e;
1027
- if (typeof obj.msg === "string") return obj.msg;
1028
- if (typeof obj.message === "string") return obj.message;
1029
- if (typeof obj.error === "string") return obj.error;
1030
- try {
1031
- return JSON.stringify(e);
1032
- } catch {
1033
- return String(e);
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
- return String(e);
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(resolve(entry)) === realpathSync(fileURLToPath(import.meta.url));
1409
+ return realpathSync(resolve2(entry)) === realpathSync(fileURLToPath(import.meta.url));
1043
1410
  } catch {
1044
1411
  return false;
1045
1412
  }