beliq-cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 beliq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # beliq-cli
2
+
3
+ Validate, generate, parse, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) from the terminal or CI, against beliq's authority-pinned, drift-checked rules.
4
+
5
+ The headline use is **validating e-invoices in CI**: point it at your invoice files, and a non-compliant document fails the build.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g beliq-cli
11
+ # or run without installing:
12
+ npx beliq-cli validate invoice.xml
13
+ ```
14
+
15
+ Requires Node.js >= 20.15.
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ beliq validate <file|-> [--format auto|cii|ubl] [--fail-on error|warning] [--content-type <mime>] [--json]
21
+ beliq generate <invoice.json|-> --standard xrechnung|zugferd|facturx|peppol-bis [--pdf] [--facturx-profile <p>] [--no-verify] [--output <file>] [--json]
22
+ beliq parse <file|-> [--format auto|cii|ubl] [--json]
23
+ beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format <f>] [--target-profile <p>] [--output <file>] [--json]
24
+ beliq me [--json]
25
+ ```
26
+
27
+ A file argument of `-` reads from stdin. `--json` prints the raw API result as the only thing on stdout, so it pipes cleanly.
28
+
29
+ ```bash
30
+ # Validate a file, human-readable
31
+ beliq validate invoice.xml
32
+
33
+ # Validate from a pipe, machine-readable, fail the shell on any warning too
34
+ cat invoice.xml | beliq validate - --json --fail-on warning
35
+
36
+ # Generate an XRechnung from a JSON invoice, write the XML to stdout
37
+ beliq generate examples/invoice.json --standard xrechnung > invoice.xml
38
+
39
+ # Convert a CII document to a Peppol BIS UBL document
40
+ beliq convert invoice.xml --target-format peppol-bis --output peppol.xml
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ Every setting has a flag that overrides the environment variable.
46
+
47
+ | Variable | Flag | Required | Default | Description |
48
+ |---|---|---|---|---|
49
+ | `BELIQ_API_KEY` | `--api-key` | yes | | Your beliq API key. The free tier is enough to evaluate. |
50
+ | `BELIQ_BASE_URL` | `--base-url` | no | `https://api.beliq.eu` | Override only for a self-hosted deployment. |
51
+ | `BELIQ_AUTH` | `--auth` | no | `header` | How the key is sent: `header` (X-API-Key) or `bearer`. |
52
+
53
+ ## Exit codes
54
+
55
+ The exit code is the contract that makes it useful in scripts and CI:
56
+
57
+ | Code | Meaning |
58
+ |---|---|
59
+ | 0 | success, or a valid document |
60
+ | 1 | document invalid (`validate`, per `--fail-on`) |
61
+ | 2 | usage error (bad flag, missing argument, missing API key, PDF without `--output`) |
62
+ | 3 | beliq API error (bad key, quota, engine, a rejected document) |
63
+ | 4 | I/O error (unreadable input, or an output path that already exists) |
64
+
65
+ ## In CI
66
+
67
+ Validate every invoice a build produces and fail on a non-compliant one:
68
+
69
+ ```yaml
70
+ - run: npx beliq-cli validate "dist/invoices/*.xml" --fail-on error
71
+ env:
72
+ BELIQ_API_KEY: ${{ secrets.BELIQ_API_KEY }}
73
+ ```
74
+
75
+ For a first-class GitHub Action with a per-file step summary, see `beliq-eu/beliq-validate-action`.
76
+
77
+ ## Development
78
+
79
+ ```bash
80
+ npm install
81
+ npm run build
82
+ npm test # unit tests, no network
83
+ npm run scrub:check # no em-dash
84
+ BELIQ_API_KEY=... npm run test:integration # live smoke against the real API
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT
package/dist/args.js ADDED
@@ -0,0 +1,97 @@
1
+ import { UsageError } from './errors.js';
2
+ /** Flags that take no value. */
3
+ const BOOLEAN_FLAGS = new Set(['json', 'pdf', 'no-verify', 'france-ctc']);
4
+ /** Flags that take a value (`--flag value` or `--flag=value`). */
5
+ const VALUE_FLAGS = new Set([
6
+ 'format',
7
+ 'target-format',
8
+ 'source-format',
9
+ 'target-profile',
10
+ 'facturx-profile',
11
+ 'content-type',
12
+ 'fail-on',
13
+ 'standard',
14
+ 'api-key',
15
+ 'base-url',
16
+ 'auth',
17
+ 'output',
18
+ ]);
19
+ /**
20
+ * Parse argv into a command, its positionals, and flags. Hand-rolled (no
21
+ * dependency): the surface is small and this stays pure and unit-testable. The
22
+ * first non-flag token is the command; a `-` positional means stdin. Unknown
23
+ * flags and unknown short options are usage errors.
24
+ */
25
+ export function parseArgs(argv) {
26
+ const out = { positionals: [], flags: {}, help: false, version: false };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const token = argv[i];
29
+ if (token === '--help' || token === '-h') {
30
+ out.help = true;
31
+ continue;
32
+ }
33
+ if (token === '--version' || token === '-v') {
34
+ out.version = true;
35
+ continue;
36
+ }
37
+ if (token.startsWith('--')) {
38
+ const body = token.slice(2);
39
+ const eq = body.indexOf('=');
40
+ const name = eq >= 0 ? body.slice(0, eq) : body;
41
+ const inlineValue = eq >= 0 ? body.slice(eq + 1) : undefined;
42
+ if (BOOLEAN_FLAGS.has(name)) {
43
+ if (inlineValue !== undefined)
44
+ throw new UsageError(`option --${name} takes no value`);
45
+ out.flags[name] = true;
46
+ }
47
+ else if (VALUE_FLAGS.has(name)) {
48
+ let value = inlineValue;
49
+ if (value === undefined) {
50
+ value = argv[++i];
51
+ if (value === undefined)
52
+ throw new UsageError(`option --${name} needs a value`);
53
+ }
54
+ out.flags[name] = value;
55
+ }
56
+ else {
57
+ throw new UsageError(`unknown option --${name}`);
58
+ }
59
+ continue;
60
+ }
61
+ // A lone "-" is the stdin positional; any other "-x" is an unknown short option.
62
+ if (token.startsWith('-') && token.length > 1) {
63
+ throw new UsageError(`unknown option ${token}`);
64
+ }
65
+ if (out.command === undefined)
66
+ out.command = token;
67
+ else
68
+ out.positionals.push(token);
69
+ }
70
+ return out;
71
+ }
72
+ export function flagBool(args, name) {
73
+ return args.flags[name] === true;
74
+ }
75
+ export function flagStr(args, name) {
76
+ const value = args.flags[name];
77
+ return typeof value === 'string' ? value : undefined;
78
+ }
79
+ export function requirePositional(args, usage) {
80
+ const value = args.positionals[0];
81
+ if (value === undefined)
82
+ throw new UsageError(`missing file argument. Usage: ${usage}`);
83
+ return value;
84
+ }
85
+ /**
86
+ * Validate a flag value against an allow-list. Returns the value (typed to the
87
+ * allowed union), the fallback when the flag is absent, or throws a UsageError
88
+ * when the value is not allowed.
89
+ */
90
+ export function oneOf(value, allowed, flagName, fallback) {
91
+ if (value === undefined)
92
+ return fallback;
93
+ if (!allowed.includes(value)) {
94
+ throw new UsageError(`--${flagName} must be one of: ${allowed.join(', ')}`);
95
+ }
96
+ return value;
97
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,73 @@
1
+ import { Beliq, BeliqApiError } from '@beliq/sdk';
2
+ import { parseArgs } from './args.js';
3
+ import { resolveConfig } from './config.js';
4
+ import { EXIT } from './exit.js';
5
+ import { IoError, UsageError } from './errors.js';
6
+ import { HELP, version } from './help.js';
7
+ import { runValidate } from './commands/validate.js';
8
+ import { runGenerate } from './commands/generate.js';
9
+ import { runParse } from './commands/parse.js';
10
+ import { runConvert } from './commands/convert.js';
11
+ import { runMe } from './commands/me.js';
12
+ const COMMANDS = {
13
+ validate: runValidate,
14
+ generate: runGenerate,
15
+ parse: runParse,
16
+ convert: runConvert,
17
+ me: runMe,
18
+ };
19
+ async function dispatch(args, io, env) {
20
+ const command = args.command ? COMMANDS[args.command] : undefined;
21
+ if (!command)
22
+ throw new UsageError(`unknown command "${args.command}". Run beliq --help.`);
23
+ const config = resolveConfig(args, env);
24
+ const client = new Beliq({ apiKey: config.apiKey, baseUrl: config.baseUrl, auth: config.auth });
25
+ return command(args, { client }, io);
26
+ }
27
+ /**
28
+ * The CLI entry: parse argv, handle --help / --version, dispatch to a command,
29
+ * and map every error class to its exit code (see EXIT). Pure in its IO + env
30
+ * seams so it can be driven from a test without touching real streams. Returns
31
+ * the process exit code.
32
+ */
33
+ export async function main(argv, io, env = process.env) {
34
+ let args;
35
+ try {
36
+ args = parseArgs(argv);
37
+ }
38
+ catch (err) {
39
+ if (err instanceof UsageError) {
40
+ io.stderr(`beliq: ${err.message}`);
41
+ return EXIT.USAGE;
42
+ }
43
+ throw err;
44
+ }
45
+ if (args.help || (!args.command && !args.version)) {
46
+ io.stdout(HELP);
47
+ return EXIT.OK;
48
+ }
49
+ if (args.version) {
50
+ io.stdout(`${version()}\n`);
51
+ return EXIT.OK;
52
+ }
53
+ try {
54
+ return await dispatch(args, io, env);
55
+ }
56
+ catch (err) {
57
+ if (err instanceof UsageError) {
58
+ io.stderr(`beliq: ${err.message}`);
59
+ return EXIT.USAGE;
60
+ }
61
+ if (err instanceof IoError) {
62
+ io.stderr(`beliq: ${err.message}`);
63
+ return EXIT.IO;
64
+ }
65
+ if (err instanceof BeliqApiError) {
66
+ const code = err.code ? ` (${err.code})` : '';
67
+ io.stderr(`beliq: API error ${err.status}${code}: ${err.message}`);
68
+ return EXIT.API;
69
+ }
70
+ io.stderr(`beliq: unexpected error: ${err.message}`);
71
+ return 1;
72
+ }
73
+ }
@@ -0,0 +1,46 @@
1
+ import { LIVE_CONVERT_SOURCE_FORMATS, LIVE_CONVERT_TARGET_FORMATS, } from '@beliq/sdk';
2
+ import { flagStr, oneOf, requirePositional } from '../args.js';
3
+ import { UsageError } from '../errors.js';
4
+ import { emitDocument } from './emit.js';
5
+ /** Convert targets that produce a hybrid PDF rather than an XML document. */
6
+ const PDF_TARGETS = new Set(['facturx', 'zugferd']);
7
+ /**
8
+ * Convert a document from one EN 16931 format to another. An XML target is
9
+ * printed to stdout (or --output); a PDF target (facturx / zugferd) must go to
10
+ * --output. Elements the conversion could not carry across are surfaced so a
11
+ * lossy conversion is visible.
12
+ */
13
+ export async function runConvert(args, deps, io) {
14
+ const file = requirePositional(args, 'beliq convert <file|-> --target-format <format>');
15
+ const targetFormat = oneOf(flagStr(args, 'target-format'), LIVE_CONVERT_TARGET_FORMATS, 'target-format');
16
+ if (!targetFormat) {
17
+ throw new UsageError(`--target-format is required (one of: ${LIVE_CONVERT_TARGET_FORMATS.join(', ')})`);
18
+ }
19
+ const sourceFormat = oneOf(flagStr(args, 'source-format'), LIVE_CONVERT_SOURCE_FORMATS, 'source-format');
20
+ const bytes = await io.readInput(file);
21
+ const result = await deps.client.convert(bytes, {
22
+ targetFormat: targetFormat,
23
+ sourceFormat: sourceFormat,
24
+ targetProfile: flagStr(args, 'target-profile'),
25
+ contentType: flagStr(args, 'content-type'),
26
+ });
27
+ const kind = PDF_TARGETS.has(targetFormat) ? 'pdf' : 'xml';
28
+ const from = result.meta.sourceFormat ? `${result.meta.sourceFormat} ` : '';
29
+ const to = result.meta.targetFormat ?? targetFormat;
30
+ const lost = result.meta.lostElementsCount && result.meta.lostElementsCount > 0
31
+ ? ` ${result.meta.lostElementsCount} element(s) could not be carried across.`
32
+ : '';
33
+ return emitDocument(io, args, {
34
+ kind,
35
+ bytes: result.bytes,
36
+ meta: {
37
+ sourceFormat: result.meta.sourceFormat,
38
+ targetFormat: to,
39
+ profileDetected: result.meta.profileDetected,
40
+ lostElementsCount: result.meta.lostElementsCount,
41
+ lostElements: result.meta.lostElements,
42
+ contentType: result.contentType,
43
+ },
44
+ summary: `Converted ${from}to ${to}.${lost}`,
45
+ });
46
+ }
@@ -0,0 +1,35 @@
1
+ import { flagBool, flagStr } from '../args.js';
2
+ import { UsageError } from '../errors.js';
3
+ import { EXIT } from '../exit.js';
4
+ /**
5
+ * Emit a produced document (generate / convert). A PDF must go to --output; XML
6
+ * without --output is written to stdout so it can be piped. The human summary
7
+ * goes to stderr so it never mixes into piped bytes. `--json` writes the
8
+ * metadata to stdout instead (plus the XML text for an XML result).
9
+ */
10
+ export async function emitDocument(io, args, opts) {
11
+ const outputPath = flagStr(args, 'output');
12
+ const json = flagBool(args, 'json');
13
+ if (opts.kind === 'pdf' && !outputPath) {
14
+ throw new UsageError('a PDF result needs --output <file> to write it to');
15
+ }
16
+ if (outputPath) {
17
+ await io.writeNewFile(outputPath, opts.bytes);
18
+ if (json) {
19
+ io.stdout(`${JSON.stringify({ ...opts.meta, outputPath, bytesWritten: opts.bytes.byteLength }, null, 2)}\n`);
20
+ }
21
+ else {
22
+ io.stderr(`${opts.summary} Written to ${outputPath} (${opts.bytes.byteLength} bytes).`);
23
+ }
24
+ return EXIT.OK;
25
+ }
26
+ const xml = new TextDecoder().decode(opts.bytes);
27
+ if (json) {
28
+ io.stdout(`${JSON.stringify({ ...opts.meta, xml }, null, 2)}\n`);
29
+ }
30
+ else {
31
+ io.stdout(xml.endsWith('\n') ? xml : `${xml}\n`);
32
+ io.stderr(opts.summary);
33
+ }
34
+ return EXIT.OK;
35
+ }
@@ -0,0 +1,48 @@
1
+ import { LIVE_GENERATE_STANDARDS } from '@beliq/sdk';
2
+ import { flagBool, flagStr, oneOf, requirePositional } from '../args.js';
3
+ import { UsageError } from '../errors.js';
4
+ import { emitDocument } from './emit.js';
5
+ /**
6
+ * Generate a compliant document from an EN 16931 invoice given as JSON. XML is
7
+ * printed to stdout (or --output); a PDF must go to --output. verify defaults on
8
+ * so a non-compliant document fails closed rather than being handed back.
9
+ */
10
+ export async function runGenerate(args, deps, io) {
11
+ const file = requirePositional(args, 'beliq generate <invoice.json|-> --standard <standard>');
12
+ const standard = oneOf(flagStr(args, 'standard'), LIVE_GENERATE_STANDARDS, 'standard');
13
+ if (!standard) {
14
+ throw new UsageError(`--standard is required (one of: ${LIVE_GENERATE_STANDARDS.join(', ')})`);
15
+ }
16
+ const raw = await io.readInput(file);
17
+ let invoice;
18
+ try {
19
+ invoice = JSON.parse(new TextDecoder().decode(raw));
20
+ }
21
+ catch {
22
+ const what = file === '-' ? 'stdin' : file;
23
+ throw new UsageError(`${what} is not valid JSON; generate takes an EN 16931 invoice as JSON`);
24
+ }
25
+ const output = flagBool(args, 'pdf') ? 'pdf' : 'xml';
26
+ const result = await deps.client.generate({
27
+ standard,
28
+ invoice,
29
+ output,
30
+ facturxProfile: flagStr(args, 'facturx-profile'),
31
+ verify: !flagBool(args, 'no-verify'),
32
+ });
33
+ const checked = result.meta.schematronVersion
34
+ ? `, checked against Schematron ${result.meta.schematronVersion}`
35
+ : '';
36
+ return emitDocument(io, args, {
37
+ kind: output,
38
+ bytes: result.bytes,
39
+ meta: {
40
+ output,
41
+ contentType: result.contentType,
42
+ schematronVersion: result.meta.schematronVersion,
43
+ pdfKind: result.meta.pdfKind,
44
+ outputEnvelope: result.meta.outputEnvelope,
45
+ },
46
+ summary: `Generated a ${standard} ${output} document${checked}.`,
47
+ });
48
+ }
@@ -0,0 +1,12 @@
1
+ import { flagBool } from '../args.js';
2
+ import { EXIT } from '../exit.js';
3
+ import { renderAccountHuman } from '../format/account.js';
4
+ /** Report the account behind the API key (GET /v1/me, no quota drawn). */
5
+ export async function runMe(args, deps, io) {
6
+ const account = await deps.client.me();
7
+ if (flagBool(args, 'json'))
8
+ io.stdout(`${JSON.stringify(account, null, 2)}\n`);
9
+ else
10
+ io.stdout(`${renderAccountHuman(account)}\n`);
11
+ return EXIT.OK;
12
+ }
@@ -0,0 +1,19 @@
1
+ import { LIVE_PARSE_FORMATS } from '@beliq/sdk';
2
+ import { flagBool, flagStr, oneOf, requirePositional } from '../args.js';
3
+ import { EXIT } from '../exit.js';
4
+ import { renderParseHuman } from '../format/parse.js';
5
+ /** Parse one document into a structured EN 16931 invoice. */
6
+ export async function runParse(args, deps, io) {
7
+ const file = requirePositional(args, 'beliq parse <file|->');
8
+ const format = oneOf(flagStr(args, 'format'), LIVE_PARSE_FORMATS, 'format');
9
+ const bytes = await io.readInput(file);
10
+ const result = await deps.client.parse(bytes, {
11
+ format: format,
12
+ contentType: flagStr(args, 'content-type'),
13
+ });
14
+ if (flagBool(args, 'json'))
15
+ io.stdout(`${JSON.stringify(result, null, 2)}\n`);
16
+ else
17
+ io.stdout(`${renderParseHuman(result)}\n`);
18
+ return EXIT.OK;
19
+ }
@@ -0,0 +1,25 @@
1
+ import { LIVE_VALIDATE_FORMATS } from '@beliq/sdk';
2
+ import { flagBool, flagStr, oneOf, requirePositional } from '../args.js';
3
+ import { computeExitCode } from '../exit.js';
4
+ import { renderValidationHuman } from '../format/validate.js';
5
+ const FAIL_ON = ['error', 'warning'];
6
+ /**
7
+ * Validate one document against beliq's authority-pinned rules. Prints the
8
+ * verdict (human table or, with --json, the raw ValidationResult) and returns
9
+ * the CI exit code: 0 if it passes the --fail-on threshold, 1 if it fails.
10
+ */
11
+ export async function runValidate(args, deps, io) {
12
+ const file = requirePositional(args, 'beliq validate <file|->');
13
+ const format = oneOf(flagStr(args, 'format'), LIVE_VALIDATE_FORMATS, 'format');
14
+ const failOn = (oneOf(flagStr(args, 'fail-on'), FAIL_ON, 'fail-on') ?? 'error');
15
+ const bytes = await io.readInput(file);
16
+ const result = await deps.client.validate(bytes, {
17
+ format: format,
18
+ contentType: flagStr(args, 'content-type'),
19
+ });
20
+ if (flagBool(args, 'json'))
21
+ io.stdout(`${JSON.stringify(result, null, 2)}\n`);
22
+ else
23
+ io.stdout(`${renderValidationHuman(result)}\n`);
24
+ return computeExitCode(result, failOn);
25
+ }
package/dist/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import { DEFAULT_BASE_URL } from '@beliq/sdk';
2
+ import { flagStr } from './args.js';
3
+ import { UsageError } from './errors.js';
4
+ /**
5
+ * Resolve the typed config with precedence flag > env > default. Throws a
6
+ * UsageError if no API key is available so the CLI reports a clean usage error
7
+ * rather than failing mid-request. The SDK reads no environment itself, so the
8
+ * CLI owns this.
9
+ */
10
+ export function resolveConfig(args, env = process.env) {
11
+ const apiKey = (flagStr(args, 'api-key') ?? env.BELIQ_API_KEY)?.trim();
12
+ if (!apiKey) {
13
+ throw new UsageError('no API key. Set BELIQ_API_KEY or pass --api-key. Create a key in the beliq dashboard under API Keys.');
14
+ }
15
+ const baseUrl = (flagStr(args, 'base-url') ?? env.BELIQ_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
16
+ const authRaw = (flagStr(args, 'auth') ?? env.BELIQ_AUTH ?? '').trim().toLowerCase();
17
+ const auth = authRaw === 'bearer' ? 'bearer' : 'header';
18
+ return { apiKey, baseUrl, auth };
19
+ }
package/dist/deps.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,6 @@
1
+ /** A user-facing usage problem (bad flag, missing argument, missing key). Maps to EXIT.USAGE. */
2
+ export class UsageError extends Error {
3
+ }
4
+ /** A failure reading a document or writing an output file. Maps to EXIT.IO. */
5
+ export class IoError extends Error {
6
+ }
package/dist/exit.js ADDED
@@ -0,0 +1,25 @@
1
+ export const EXIT = {
2
+ /** Success, or a document that passed validation. */
3
+ OK: 0,
4
+ /** A document that failed validation per the chosen --fail-on threshold. */
5
+ INVALID: 1,
6
+ /** A usage problem: bad flag, missing argument, missing API key, PDF without --output. */
7
+ USAGE: 2,
8
+ /** The beliq API returned an error (bad key, quota, engine, a rejected document). */
9
+ API: 3,
10
+ /** A local I/O error: an unreadable input or an output path that already exists. */
11
+ IO: 4,
12
+ };
13
+ /**
14
+ * The CI contract: EXIT.OK when the document passes the chosen threshold,
15
+ * EXIT.INVALID otherwise. `error` (default) fails on any error; `warning` also
16
+ * fails on any warning.
17
+ */
18
+ export function computeExitCode(result, failOn) {
19
+ const errorCount = result.errors?.length ?? 0;
20
+ const warningCount = result.warnings?.length ?? 0;
21
+ const failsOnError = !result.valid || errorCount > 0;
22
+ if (failOn === 'warning')
23
+ return failsOnError || warningCount > 0 ? EXIT.INVALID : EXIT.OK;
24
+ return failsOnError ? EXIT.INVALID : EXIT.OK;
25
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A compact human-readable account summary for `beliq me`: the org, plan, and
3
+ * key prefix, then the quota and rate limit. `--json` emits the raw AccountInfo.
4
+ */
5
+ export function renderAccountHuman(account) {
6
+ const lines = [];
7
+ const plan = account.plan?.name ? `plan ${account.plan.name}` : 'plan (none)';
8
+ const key = account.keyPrefix ? `key ${account.keyPrefix}` : '';
9
+ const org = `org ${account.org?.name ?? account.org?.id ?? 'unknown'}`;
10
+ lines.push([org, plan, key].filter(Boolean).join(' '));
11
+ const quota = account.quota;
12
+ if (quota) {
13
+ const resets = quota.resetsAt ? ` (resets ${quota.resetsAt})` : '';
14
+ lines.push(`quota ${quota.used}/${quota.limit} used, ${quota.remaining} remaining${resets}`);
15
+ }
16
+ if (typeof account.rateLimitPerMinute === 'number') {
17
+ lines.push(`rate limit ${account.rateLimitPerMinute}/min`);
18
+ }
19
+ return lines.join('\n');
20
+ }
@@ -0,0 +1,18 @@
1
+ function plural(n, word) {
2
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
3
+ }
4
+ /**
5
+ * A one-line summary of a parse: the detected syntax and profile, then the
6
+ * invoice number, line count, and gross total. `--json` emits the full parsed
7
+ * invoice instead.
8
+ */
9
+ export function renderParseHuman(result) {
10
+ const invoice = result.invoice;
11
+ const profile = result.profileDetected ? ` (profile ${result.profileDetected})` : '';
12
+ const number = invoice?.number ? `invoice ${invoice.number}` : 'invoice';
13
+ const lines = invoice?.lines?.length ?? 0;
14
+ const gross = invoice?.totalGrossAmount != null
15
+ ? `, gross ${invoice.totalGrossAmount} ${invoice.currencyCode ?? ''}`.trimEnd()
16
+ : '';
17
+ return `Parsed a ${result.format}${profile} document: ${number}, ${plural(lines, 'line')}${gross}`;
18
+ }
@@ -0,0 +1,33 @@
1
+ function plural(n, word) {
2
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
3
+ }
4
+ /**
5
+ * Render an aligned table. Every column except the last (message) is padded to
6
+ * its widest cell; the message is left unpadded so a long message never forces
7
+ * truncation or breaks alignment.
8
+ */
9
+ function renderTable(headers, rows) {
10
+ const last = headers.length - 1;
11
+ const widths = headers.map((h, col) => col === last ? 0 : Math.max(h.length, ...rows.map((r) => r[col].length)));
12
+ const line = (cells) => cells.map((cell, col) => (col === last ? cell : cell.padEnd(widths[col]))).join(' ');
13
+ return [line(headers), ...rows.map(line)].join('\n');
14
+ }
15
+ /**
16
+ * A human-readable verdict: the pass/fail line with format, profile, and
17
+ * ruleset version, the issue counts, and an aligned table of the errors then
18
+ * warnings. `--json` emits the raw result instead.
19
+ */
20
+ export function renderValidationHuman(result) {
21
+ const errors = (result.errors ?? []);
22
+ const warnings = (result.warnings ?? []);
23
+ const verdict = result.valid ? 'VALID' : 'INVALID';
24
+ const profile = result.profileDetected ? ` (profile ${result.profileDetected})` : '';
25
+ const schematron = result.schematronVersion ? ` checked against Schematron ${result.schematronVersion}` : '';
26
+ const head = `${verdict} ${result.format}${profile}${schematron}`;
27
+ const counts = `${plural(errors.length, 'error')}, ${plural(warnings.length, 'warning')}`;
28
+ const issues = [...errors, ...warnings];
29
+ if (issues.length === 0)
30
+ return `${head}\n${counts}`;
31
+ const rows = issues.map((i) => [i.severity, i.ruleId, i.location || '-', i.message]);
32
+ return `${head}\n${counts}\n\n${renderTable(['SEVERITY', 'RULE', 'LOCATION', 'MESSAGE'], rows)}`;
33
+ }
package/dist/help.js ADDED
@@ -0,0 +1,33 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export function version() {
4
+ const pkg = require('../package.json');
5
+ return pkg.version;
6
+ }
7
+ export const HELP = `beliq: validate, generate, parse, and convert EU e-invoices
8
+
9
+ Usage:
10
+ beliq validate <file|-> [--format auto|cii|ubl] [--fail-on error|warning] [--content-type <mime>] [--json]
11
+ beliq generate <invoice.json|-> --standard xrechnung|zugferd|facturx|peppol-bis [--pdf] [--facturx-profile <p>] [--no-verify] [--output <file>] [--json]
12
+ beliq parse <file|-> [--format auto|cii|ubl] [--json]
13
+ beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format <f>] [--target-profile <p>] [--output <file>] [--json]
14
+ beliq me [--json]
15
+
16
+ Global options:
17
+ --api-key <key> beliq API key (default: BELIQ_API_KEY)
18
+ --base-url <url> API base URL (default: BELIQ_BASE_URL, else https://api.beliq.eu)
19
+ --auth <mode> header (default, X-API-Key) or bearer
20
+ --json machine-readable JSON on stdout
21
+ -h, --help show this help
22
+ -v, --version show the version
23
+
24
+ Exit codes:
25
+ 0 success, or a valid document
26
+ 1 document invalid (validate, per --fail-on)
27
+ 2 usage error
28
+ 3 beliq API error
29
+ 4 I/O error
30
+
31
+ A file argument of "-" reads from stdin. Set BELIQ_API_KEY (or pass --api-key)
32
+ with a key from the beliq dashboard; the free tier is enough to evaluate.
33
+ `;
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli.js';
3
+ import { nodeIO } from './io.js';
4
+ main(process.argv.slice(2), nodeIO()).then((code) => {
5
+ process.exitCode = code;
6
+ }, (err) => {
7
+ process.stderr.write(`beliq: fatal: ${err.message}\n`);
8
+ process.exitCode = 1;
9
+ });
package/dist/io.js ADDED
@@ -0,0 +1,38 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { IoError } from './errors.js';
3
+ async function readStdin() {
4
+ const chunks = [];
5
+ for await (const chunk of process.stdin)
6
+ chunks.push(chunk);
7
+ return Buffer.concat(chunks);
8
+ }
9
+ export function nodeIO() {
10
+ return {
11
+ stdout(chunk) {
12
+ process.stdout.write(chunk);
13
+ },
14
+ stderr(line) {
15
+ process.stderr.write(`${line}\n`);
16
+ },
17
+ async readInput(pathOrDash) {
18
+ try {
19
+ return pathOrDash === '-' ? await readStdin() : await readFile(pathOrDash);
20
+ }
21
+ catch (err) {
22
+ const what = pathOrDash === '-' ? 'stdin' : pathOrDash;
23
+ throw new IoError(`could not read ${what}: ${err.message}`);
24
+ }
25
+ },
26
+ async writeNewFile(path, bytes) {
27
+ try {
28
+ await writeFile(path, bytes, { flag: 'wx' });
29
+ }
30
+ catch (err) {
31
+ if (err.code === 'EEXIST') {
32
+ throw new IoError(`a file already exists at ${path}; choose a path that does not exist`);
33
+ }
34
+ throw new IoError(`could not write ${path}: ${err.message}`);
35
+ }
36
+ },
37
+ };
38
+ }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "beliq-cli",
3
+ "version": "0.1.0",
4
+ "description": "beliq CLI: validate, generate, parse, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules, from the terminal or CI.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "author": {
8
+ "name": "beliq",
9
+ "email": "hello@beliq.eu"
10
+ },
11
+ "homepage": "https://beliq.eu",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/beliq-eu/beliq-cli.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/beliq-eu/beliq-cli/issues"
18
+ },
19
+ "keywords": [
20
+ "beliq",
21
+ "cli",
22
+ "e-invoice",
23
+ "einvoice",
24
+ "xrechnung",
25
+ "zugferd",
26
+ "factur-x",
27
+ "peppol",
28
+ "en16931",
29
+ "validation",
30
+ "compliance",
31
+ "ci"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20.15"
35
+ },
36
+ "bin": {
37
+ "beliq": "dist/index.js"
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "publishConfig": {
43
+ "provenance": true
44
+ },
45
+ "scripts": {
46
+ "clean": "rm -rf dist",
47
+ "build": "npm run clean && tsc",
48
+ "dev": "tsc --watch",
49
+ "start": "node dist/index.js",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "eslint src",
52
+ "lint:fix": "eslint src --fix",
53
+ "test": "vitest run --exclude test/integration.test.ts",
54
+ "test:integration": "vitest run test/integration.test.ts",
55
+ "scrub:check": "bash scripts/scrub-check.sh",
56
+ "prepublishOnly": "npm run build"
57
+ },
58
+ "dependencies": {
59
+ "@beliq/sdk": "^0.1.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^20.14.0",
63
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
64
+ "@typescript-eslint/parser": "^7.18.0",
65
+ "eslint": "^8.57.0",
66
+ "typescript": "^5.5.4",
67
+ "vitest": "^2.1.8"
68
+ }
69
+ }