@n42/cli 0.1.21

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.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (C) 2026, Node42. <code@node42.dev>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ 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,92 @@
1
+ # Node42 CLI (n42)
2
+
3
+ Command-line interface for **eDelivery discovery, diagnostics, and
4
+ validation**, with support for the Peppol network.
5
+
6
+ The Node42 CLI is designed for **system integrators, service providers,
7
+ and operators** who need fast, repeatable insight into eDelivery
8
+ routing, SMP resolution, and Access Point behavior.
9
+
10
+ ------------------------------------------------------------------------
11
+
12
+ ## Features
13
+
14
+ - Peppol eDelivery path discovery
15
+ - SMP and Access Point resolution diagnostics
16
+ - Supported document type detection
17
+ - PlantUML and SVG visualizations
18
+ - Authenticated API access
19
+ - Deterministic, script-friendly output
20
+ - No browser automation or UI side effects
21
+
22
+ ## Installation
23
+
24
+ ### Requirements
25
+
26
+ - Node.js **18+** (Node 20 recommended)
27
+ - npm
28
+
29
+ ### Install globally
30
+
31
+ ``` bash
32
+ npm install -g @n42/cli
33
+ ```
34
+
35
+ Verify installation:
36
+
37
+ ``` bash
38
+ n42 --version
39
+ ```
40
+
41
+ ------------------------------------------------------------------------
42
+
43
+ ## Authentication
44
+
45
+ Authenticate once using your Node42 account credentials.\
46
+ Tokens are stored locally under `~/.node42/`.
47
+
48
+ ``` bash
49
+ n42 signin
50
+ ```
51
+
52
+ Check authentication status:
53
+
54
+ ``` bash
55
+ n42 me
56
+ ```
57
+
58
+ ------------------------------------------------------------------------
59
+
60
+ ## Peppol Discovery
61
+
62
+ ### Basic discovery
63
+
64
+ ``` bash
65
+ n42 discover peppol <environment> <participantId>
66
+ ```
67
+
68
+ ------------------------------------------------------------------------
69
+
70
+ ## Artifacts
71
+
72
+ Artifacts are stored under:
73
+
74
+ ~/.node42/artifacts/
75
+
76
+ ------------------------------------------------------------------------
77
+
78
+ ## Error Handling
79
+
80
+ Errors are printed with a clickable reference link:
81
+
82
+ https://www.node42.dev/errors?code=XXXX
83
+
84
+ ## Security
85
+
86
+ - TLS verification enabled by default
87
+ - Explicit `--insecure` flag for testing only
88
+ - Tokens stored locally, never logged
89
+
90
+ ## License
91
+
92
+ MIT License
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@n42/cli",
3
+ "version": "0.1.21",
4
+ "description": "Node42 CLI – Command-line interface for eDelivery path discovery, diagnostics, and tooling",
5
+ "keywords": [
6
+ "node42"
7
+ ],
8
+ "homepage": "https://github.com/node42-dev/node42-cli",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/node42-dev/node42-cli.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Node42 @n42",
15
+ "type": "commonjs",
16
+ "bin": {
17
+ "n42": "src/cli.js"
18
+ },
19
+ "dependencies": {
20
+ "commander": "^11.1.0",
21
+ "inquirer": "^8.2.7",
22
+ "open": "^11.0.0"
23
+ }
24
+ }
package/src/auth.js ADDED
@@ -0,0 +1,123 @@
1
+ const fs = require("fs");
2
+ const { NODE42_DIR, TOKENS_FILE, API_URL, EP_REFRESH } = require("./config");
3
+ const { handleError } = require("./errors");
4
+ const { updateUserInfo, updateUserUsage } = require("./user");
5
+
6
+
7
+ function loadAuth() {
8
+ if (!fs.existsSync(TOKENS_FILE)) {
9
+ console.error("Not logged in. Run: n42 signin");
10
+ process.exit(1);
11
+ }
12
+ return JSON.parse(fs.readFileSync(TOKENS_FILE, "utf8"));
13
+ }
14
+
15
+ async function checkAuth() {
16
+ if (!fs.existsSync(TOKENS_FILE)) {
17
+ handleError({ code: "N42E-9033", message: "Token missing..."})
18
+ process.exit(1);
19
+ }
20
+
21
+ const res = await fetchWithAuth(`${API_URL}/users/me`, {
22
+ method: "GET",
23
+ headers: {
24
+ "Content-Type": "application/json"
25
+ }
26
+ });
27
+
28
+ if (!res) {
29
+ return false;
30
+ }
31
+
32
+ if (!res.ok) {
33
+ const err = await res.json();
34
+ await handleError(err);
35
+ return false;
36
+ }
37
+
38
+ const auth = await res.json();
39
+ if (auth) {
40
+ updateUserInfo({
41
+ userName: auth.userName,
42
+ userMail: auth.userMail,
43
+ role: auth.role,
44
+ });
45
+
46
+ updateUserUsage({ serviceUsage: auth.serviceUsage });
47
+ return true;
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ async function refreshSession() {
54
+ const { refreshToken } = loadAuth();
55
+ if (!refreshToken) {
56
+ return false;
57
+ }
58
+
59
+ const payload = {
60
+ refreshToken,
61
+ };
62
+
63
+ const res = await fetch(`${API_URL}/${EP_REFRESH}`, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json'
67
+ },
68
+ body: JSON.stringify(payload)
69
+ });
70
+
71
+ const data = await res.json();
72
+ //console.log(data);
73
+
74
+ if (!res.ok || data.__type) {
75
+ //console.log(data);
76
+ return false;
77
+ }
78
+
79
+ if (data) {
80
+ fs.mkdirSync(NODE42_DIR, { recursive: true });
81
+ fs.writeFileSync(
82
+ TOKENS_FILE,
83
+ JSON.stringify({
84
+ accessToken: data.accessToken,
85
+ refreshToken: data.refreshToken,
86
+ idToken: data.idToken
87
+ })
88
+ );
89
+ //console.log("Token refreshed");
90
+ }
91
+
92
+ return true;
93
+ }
94
+
95
+ async function fetchWithAuth(url, options = {}) {
96
+ const { accessToken } = loadAuth();
97
+ if (!accessToken) { // N42E-9032
98
+ //console.log("Token missing...");
99
+
100
+ handleError({ code: "N42E-9032" });
101
+ return;
102
+ }
103
+
104
+ const res = await fetch(url, {
105
+ ...options,
106
+ headers: {
107
+ ...(options.headers || {}),
108
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
109
+ }
110
+ });
111
+
112
+ if (res.status !== 401) {
113
+ return res;
114
+ }
115
+
116
+ const refreshed = await refreshSession();
117
+ if (!refreshed) { // N42E-9033
118
+ //console.log("Token expired...");
119
+ return res;
120
+ }
121
+ }
122
+
123
+ module.exports = { loadAuth, checkAuth, fetchWithAuth };
package/src/browser.js ADDED
@@ -0,0 +1,10 @@
1
+ const open = require("open").default;
2
+ let browserOpened = false;
3
+
4
+ async function openOnce(target) {
5
+ if (browserOpened) return;
6
+ browserOpened = true;
7
+ await open(target, { wait: false });
8
+ }
9
+
10
+ module.exports = { openOnce };
package/src/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ const { Command } = require("commander");
3
+ const { signin } = require("./signin");
4
+ const { checkAuth } = require("./auth");
5
+ const { getUserInfo } = require("./user");
6
+ const { runDiscovery } = require("./discover");
7
+ const { clearScreen, startSpinner, validateEnv, validateId} = require("./utils");
8
+
9
+ const program = new Command();
10
+ const pkg = require("../package.json");
11
+
12
+ program
13
+ .name("n42")
14
+ .description("Command-line interface for eDelivery path discovery and diagnostics")
15
+ .version(pkg.version);
16
+
17
+ program
18
+ .command("signin")
19
+ .description("Authenticate using username and password and store tokens locally")
20
+ .action(signin);
21
+
22
+ program
23
+ .command("me")
24
+ .description("returns identity and billing status for the authenticated user.")
25
+ .action(() => {
26
+ const stopSpinner = startSpinner();
27
+
28
+ checkAuth();
29
+ const user = getUserInfo();
30
+ console.log(
31
+ `Authenticated as ${user.userName} <${user.userMail}> (${user.role})`
32
+ );
33
+
34
+ stopSpinner();
35
+ });
36
+
37
+ const discover = program
38
+ .command("discover")
39
+ .description("Discovery and diagnostic tooling for eDelivery paths");
40
+
41
+ discover
42
+ .command("peppol <environment> <participantId>")
43
+ .description("Resolve the Peppol eDelivery message path")
44
+ .option("--output <type>", "Result type (json | plantuml)", "json")
45
+ .option("--format <format>", "When output=plantuml (svg | text)", "svg")
46
+ .option("--force-https", "Force HTTPS endpoints", true)
47
+ .option("--insecure", "Disable TLS certificate validation", false)
48
+ .option("--fetch-business-card", "Fetch Peppol business card", false)
49
+ .option("--reverse-lookup", "Enable reverse lookup", false)
50
+ .option("--probe-endpoints", "Probe resolved endpoints", false)
51
+ .action((environment, participantId, options) => {
52
+
53
+ clearScreen(`Node42 CLI v${pkg.version}`);
54
+
55
+ try { validateEnv(environment); }
56
+ catch (e) {
57
+ console.error(e.message);
58
+ process.exit(1);
59
+ }
60
+
61
+ try { validateId("participant", participantId); }
62
+ catch (e) {
63
+ console.error(e.message);
64
+ process.exit(1);
65
+ }
66
+
67
+ runDiscovery(environment.toUpperCase(), participantId, options);
68
+ });
69
+
70
+ program.parse(process.argv);
package/src/config.js ADDED
@@ -0,0 +1,46 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const config = {
6
+ APP_NAME: "n42",
7
+ API_URL: "https://api.node42.dev",
8
+ WWW_URL: "https://www.node42.dev",
9
+ API_TIMEOUT_MS: 30000,
10
+
11
+ NODE42_DIR: path.join(os.homedir(), ".node42"),
12
+ ARTEFACTS_DIR: null,
13
+ USAGE_FILE: null, // filled below
14
+ USER_FILE: null, // filled below
15
+ TOKENS_FILE: null, // filled below
16
+ CONFIG_FILE: null, // filled below
17
+
18
+ DEFAULT_OUTPUT: "json",
19
+ DEFAULT_FORMAT: "svg",
20
+
21
+ EP_SIGNIN: "auth/signin",
22
+ EP_REFRESH: "auth/refresh",
23
+ EP_DISCOVER: "discover/peppol"
24
+ };
25
+
26
+ config.ARTEFACTS_DIR = path.join(config.NODE42_DIR, "artefacts", "discovery");
27
+ config.USER_FILE = path.join(config.NODE42_DIR, "user.json");
28
+ config.USAGE_FILE = path.join(config.NODE42_DIR, "usage.json");
29
+ config.TOKENS_FILE = path.join(config.NODE42_DIR, "tokens.json");
30
+ config.CONFIG_FILE = path.join(config.NODE42_DIR, "config.json");
31
+
32
+ function createAppDirs() {
33
+ fs.mkdirSync(config.NODE42_DIR, { recursive: true });
34
+ fs.mkdirSync(config.ARTEFACTS_DIR, { recursive: true });
35
+
36
+ //if (!fs.existsSync(config.CONFIG_FILE)) {
37
+ fs.writeFileSync(
38
+ config.CONFIG_FILE,
39
+ JSON.stringify(config, null, 2)
40
+ );
41
+ //}
42
+ }
43
+
44
+ createAppDirs();
45
+
46
+ module.exports = config;
@@ -0,0 +1,151 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const pkg = require("../package.json");
4
+
5
+ const { fetchWithAuth } = require("./auth");
6
+ const { API_URL, EP_DISCOVER, DEFAULT_OUTPUT, DEFAULT_FORMAT, ARTEFACTS_DIR } = require("./config");
7
+ const { getUserUsage, updateUserUsage } = require("./user");
8
+ const { clearScreen, startSpinner, buildDocLabel, promptForDocument } = require("./utils");
9
+ const { handleError } = require("./errors");
10
+
11
+ const DEFAULT_DISCOVERY_INPUT = {
12
+ env: "TEST",
13
+ options: {
14
+ forceHttps: true,
15
+ insecure: false,
16
+ fetchBusinessCard: false,
17
+ reverseLookup: false,
18
+ probeEndpoints: false
19
+ },
20
+ participant: {
21
+ scheme: "iso6523-actorid-upis",
22
+ value: "",
23
+ },
24
+ document: {
25
+ scheme: "peppol-doctype-wildcard",
26
+ value: "",
27
+ },
28
+ overrides: {
29
+ smpUrl: "",
30
+ apUrl: "",
31
+ }
32
+ };
33
+ const discoveryInput = DEFAULT_DISCOVERY_INPUT;
34
+ let docSelected = false;
35
+
36
+
37
+ async function runDiscovery(environment, participantId, options) {
38
+ const {
39
+ output = DEFAULT_OUTPUT,
40
+ format = DEFAULT_FORMAT,
41
+ forceHttps,
42
+ insecure,
43
+ fetchBusinessCard,
44
+ reverseLookup,
45
+ probeEndpoints
46
+ } = options;
47
+
48
+ discoveryInput.participant.value = participantId;
49
+
50
+ const payload = {
51
+ ...discoveryInput,
52
+ env: environment,
53
+ options: {
54
+ output,
55
+ format,
56
+ forceHttps,
57
+ rejectUnauthorized: insecure,
58
+ fetchBusinessCard,
59
+ reverseLookup,
60
+ probeEndpoints
61
+ }
62
+ };
63
+
64
+ clearScreen(`Node42 CLI v${pkg.version}`);
65
+ const stopSpinner = startSpinner();
66
+
67
+ const url = `${API_URL}/${EP_DISCOVER}?output=${output}&format=${format}`;
68
+ const res = await fetchWithAuth(url, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json"
72
+ },
73
+ body: JSON.stringify(payload)
74
+ });
75
+
76
+ if (!res.ok) {
77
+ const err = await res.json();
78
+ stopSpinner();
79
+
80
+ if (err.code) {
81
+ await handleError(err);
82
+ }
83
+
84
+ process.exit(1);
85
+ }
86
+
87
+ if (output === "plantuml" && format === "svg") {
88
+ const svg = await res.text();
89
+ stopSpinner();
90
+
91
+ if (!svg || svg.trim().length === 0) {
92
+ await handleError({ code: "6123" });
93
+ process.exit(1);
94
+ }
95
+
96
+ const refId = res.headers.get("X-Node42-RefId");
97
+ const serviceUsage = res.headers.get("X-Node42-ServiceUsage");
98
+ const rateLimit = res.headers.get("X-Node42-RateLimit");
99
+
100
+ const userUsage = getUserUsage();
101
+ const currentMonth = new Date().toISOString().slice(0, 7);
102
+ userUsage.serviceUsage.discovery[currentMonth] = serviceUsage;
103
+
104
+ updateUserUsage(userUsage);
105
+
106
+ const encodedDocs = res.headers.get("X-Node42-Documents");
107
+ if (encodedDocs && !docSelected) {
108
+ const docs = JSON.parse(Buffer.from(encodedDocs, "base64").toString("utf8"))
109
+ .map(d => ({ ...d, label: buildDocLabel(d) }));
110
+
111
+ if (docs.length) {
112
+ console.log(`Discovery completed`);
113
+ console.log(`Found ${docs.length} supported document type(s)\n`);
114
+
115
+ docSelected = await promptForDocument(docs);
116
+
117
+ if (docSelected.scheme) {
118
+ discoveryInput.document.scheme = docSelected.scheme;
119
+ }
120
+
121
+ if (docSelected.value) {
122
+ discoveryInput.document.value = docSelected.value;
123
+ }
124
+
125
+ runDiscovery(environment, participantId, options);
126
+ }
127
+ }
128
+
129
+ const file = path.join(ARTEFACTS_DIR, `${refId}.svg`);
130
+ fs.writeFileSync(file, svg);
131
+
132
+ console.log(`Discovery completed`);
133
+ console.log(`Usage : ${serviceUsage} / ${rateLimit}`);
134
+ console.log(`Artifact : ${file}\n`);
135
+
136
+ stopSpinner();
137
+ return;
138
+ }
139
+
140
+ if (output === "plantuml" && output === "text") {
141
+ const text = await res.text();
142
+ console.log(text);
143
+ return;
144
+ }
145
+
146
+ // default: json
147
+ const json = await res.json();
148
+ console.log(JSON.stringify(json, null, 2));
149
+ }
150
+
151
+ module.exports = { runDiscovery };
package/src/errors.js ADDED
@@ -0,0 +1,27 @@
1
+ const { openOnce } = require("./browser");
2
+ const { WWW_URL } = require("./config");
3
+
4
+
5
+ async function handleError(err) {
6
+ //console.log(err);
7
+
8
+ const code = err.code?.startsWith("N42E-")
9
+ ? err.code.slice(5)
10
+ : undefined;
11
+
12
+ const message = err.message;
13
+
14
+ const url = code
15
+ ? `${WWW_URL}/errors?code=${code}`
16
+ : `${WWW_URL}/errors`;
17
+ //console.log(url);
18
+
19
+ if (message) {
20
+ console.error(`${err.message}\nSee details: ${url}\n`);
21
+ } else {
22
+ console.error(`See details: ${url}\n`);
23
+ }
24
+ //await openOnce(url);
25
+ }
26
+
27
+ module.exports = { handleError };
package/src/signin.js ADDED
@@ -0,0 +1,58 @@
1
+ const fs = require("fs");
2
+
3
+ const { NODE42_DIR, TOKENS_FILE, API_URL, EP_SIGNIN } = require("./config");
4
+ const { checkAuth } = require("./auth");
5
+ const { getUserInfo } = require("./user");
6
+ const { clearScreen, ask, startSpinner } = require("./utils");
7
+
8
+
9
+ async function signin() {
10
+ clearScreen("Sign in to Node42");
11
+
12
+ const username = await ask("Username: ");
13
+ const password = await ask("Password: ", true);
14
+
15
+ let stopSpinner = startSpinner();
16
+
17
+ const res = await fetch(`${API_URL}/${EP_SIGNIN}`, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ username, password })
21
+ });
22
+
23
+ if (!res.ok) {
24
+ stopSpinner();
25
+
26
+ console.error("Login failed: ", res.status);
27
+ process.exit(1);
28
+ }
29
+
30
+ const tokens = await res.json();
31
+
32
+ const { accessToken, refreshToken, idToken } = tokens;
33
+ if (!accessToken || !refreshToken || !idToken) {
34
+ stopSpinner();
35
+
36
+ console.error("Invalid auth response");
37
+ process.exit(1);
38
+ }
39
+
40
+ fs.mkdirSync(NODE42_DIR, { recursive: true });
41
+ fs.writeFileSync(
42
+ TOKENS_FILE,
43
+ JSON.stringify({ accessToken, refreshToken, idToken }, null, 2)
44
+ );
45
+
46
+ stopSpinner();
47
+ stopSpinner = startSpinner();
48
+
49
+ checkAuth();
50
+ const user = getUserInfo();
51
+ console.log(
52
+ `Authenticated as ${user.userName} <${user.userMail}> (${user.role})`
53
+ );
54
+
55
+ stopSpinner();
56
+ }
57
+
58
+ module.exports = { signin };
package/src/user.js ADDED
@@ -0,0 +1,64 @@
1
+ const fs = require("fs");
2
+ const { NODE42_DIR, USER_FILE, USAGE_FILE } = require("./config");
3
+
4
+ function updateUserInfo(user) {
5
+ fs.mkdirSync(NODE42_DIR, { recursive: true });
6
+
7
+ fs.writeFileSync(
8
+ USER_FILE,
9
+ JSON.stringify(user, null, 2)
10
+ );
11
+ }
12
+
13
+ function getUserInfo() {
14
+ try {
15
+ if (!fs.existsSync(USER_FILE)) {
16
+ return {
17
+ userName: "n/a",
18
+ userMail: "n/a",
19
+ role: "n/a"
20
+ };
21
+ }
22
+ return JSON.parse(fs.readFileSync(USER_FILE, "utf8"));
23
+ } catch {
24
+ return {
25
+ userName: "n/a",
26
+ userMail: "n/a",
27
+ role: "n/a"
28
+ };
29
+ }
30
+ }
31
+
32
+ function updateUserUsage(usage) {
33
+ fs.mkdirSync(NODE42_DIR, { recursive: true });
34
+
35
+ fs.writeFileSync(
36
+ USAGE_FILE,
37
+ JSON.stringify(usage, null, 2)
38
+ );
39
+ }
40
+
41
+ function getUserUsage() {
42
+ try {
43
+ if (!fs.existsSync(USAGE_FILE)) {
44
+ return {
45
+ serviceUsage: {
46
+ discovery: {},
47
+ validation: {},
48
+ transactions: {}
49
+ }
50
+ };
51
+ }
52
+ return JSON.parse(fs.readFileSync(USAGE_FILE, "utf8"));
53
+ } catch {
54
+ return {
55
+ serviceUsage: {
56
+ discovery: {},
57
+ validation: {},
58
+ transactions: {}
59
+ }
60
+ };
61
+ }
62
+ }
63
+
64
+ module.exports = { updateUserInfo, getUserInfo, updateUserUsage, getUserUsage };
package/src/utils.js ADDED
@@ -0,0 +1,134 @@
1
+ const inquirer = require("inquirer");
2
+ const readline = require("readline");
3
+
4
+ function clearScreen(text) {
5
+ process.stdout.write("\x1Bc");
6
+ if (text) {
7
+ process.stdout.write(text + "\n");
8
+ }
9
+ }
10
+
11
+ function ask(question, hidden=false) {
12
+ return new Promise(resolve => {
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout,
16
+ terminal: true
17
+ });
18
+
19
+ process.stdin.on("data", char => {
20
+ char = char + "";
21
+ switch (char) {
22
+ case "\n":
23
+ case "\r":
24
+ case "\u0004":
25
+ process.stdin.pause();
26
+ break;
27
+ default:
28
+ if (hidden) {
29
+ process.stdout.clearLine(0);
30
+ process.stdout.cursorTo(0);
31
+ process.stdout.write(question + "*".repeat(rl.line.length));
32
+ }
33
+ break;
34
+ }
35
+ });
36
+
37
+ rl.question(question, answer => {
38
+ rl.history = rl.history.slice(1);
39
+ rl.close();
40
+ resolve(answer);
41
+ });
42
+ });
43
+ }
44
+
45
+ function startSpinner(text = "Working") {
46
+ const frames = ["-", "\\", "|", "/"];
47
+ let i = 0;
48
+
49
+ const timer = setInterval(() => {
50
+ process.stdout.write("\r[" + frames[i++ % frames.length] + "] " + text);
51
+ }, 120);
52
+
53
+ return () => {
54
+ clearInterval(timer);
55
+ process.stdout.write("\r\x1b[K"); // carriage return + clear line
56
+ };
57
+ }
58
+
59
+ function buildDocLabel({ scheme, value }) {
60
+ // 1. Document name (after :: before ##)
61
+ const docMatch = value.match(/::([^#]+)##/);
62
+ const docName = docMatch ? docMatch[1].replace(/([A-Z])/g, " $1").trim() : "Document";
63
+
64
+ // 2. Wildcard
65
+ if (value.endsWith("##*")) {
66
+ return `Any ${docName} (Wildcard)`;
67
+ }
68
+
69
+ // 3. PINT profile
70
+ if (value.includes(":pint:")) {
71
+ const regionMatch = value.match(/@([a-z0-9-]+)/i);
72
+ const region = regionMatch
73
+ ? regionMatch[1].replace(/-1$/, "").toUpperCase()
74
+ : "PINT";
75
+
76
+ const prefix = scheme === "peppol-doctype-wildcard" ? "Wildcard" : "PINT";
77
+ return `${docName} (${prefix} ${region})`;
78
+ }
79
+
80
+ // 4. BIS profile
81
+ if (value.includes(":bis:") || value.includes("en16931")) {
82
+ const bisMatch = value.match(/bis:[^:]+:(\d+)/);
83
+ const version = bisMatch ? bisMatch[1] : "3";
84
+ return `${docName} (BIS ${version})`;
85
+ }
86
+
87
+ // 4. TRNS profile
88
+ if (value.includes(":trns:")) {
89
+ const trnsMatch = value.match(/:trns:([^:]+):([\d.]+)/);
90
+ const version = trnsMatch ? ` ${trnsMatch[2]}` : "";
91
+ return `${docName} (TRNS${version})`;
92
+ }
93
+
94
+ // 5. Fallback
95
+ return docName;
96
+ }
97
+
98
+ async function promptForDocument(docs) {
99
+ const { document } = await inquirer.prompt([
100
+ {
101
+ type: "list",
102
+ name: "document",
103
+ message: "Select document type:",
104
+ choices: docs.map(d => ({
105
+ name: d.label,
106
+ value: d
107
+ }))
108
+ }
109
+ ]);
110
+
111
+ return document;
112
+ }
113
+
114
+ function validateEnv(env) {
115
+ const allowedEnvs = ["TEST", "PROD"];
116
+ if (!allowedEnvs.includes(env.toUpperCase())) {
117
+ throw new Error(
118
+ `Invalid environment: ${env}\nAllowed values: ${allowedEnvs.join(", ")}`
119
+ );
120
+ }
121
+ }
122
+
123
+ function validateId(type, id) {
124
+ const value = id.replace(/\s+/g, "");
125
+
126
+ // ISO 6523–safe; participant id like 0000:12345 or 9915:abcde
127
+ if (!/^[0-9]{4}:[a-zA-Z0-9\-\._~]{1,135}$/.test(value)) {
128
+ throw new Error(
129
+ `Invalid ${type}Id: ${id}\nExpected format: 0007:123456789 or 0007:abcd`
130
+ );
131
+ }
132
+ }
133
+
134
+ module.exports = { clearScreen, startSpinner, ask, buildDocLabel, promptForDocument, validateEnv, validateId };