@n42/cli 0.1.40 → 0.1.63

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/README.md CHANGED
@@ -46,7 +46,7 @@ Authenticate once using your Node42 account credentials.\
46
46
  Tokens are stored locally under `~/.node42/`.
47
47
 
48
48
  ``` bash
49
- n42 signin
49
+ n42 login
50
50
  ```
51
51
 
52
52
  Check authentication status:
@@ -62,7 +62,7 @@ n42 me
62
62
  ### Basic discovery
63
63
 
64
64
  ``` bash
65
- n42 discover peppol <environment> <participantId>
65
+ n42 discover peppol <participantId>
66
66
  ```
67
67
 
68
68
  ------------------------------------------------------------------------
@@ -0,0 +1,223 @@
1
+ # Node42 Local JSON DB – Usage Guide
2
+
3
+ This is a lightweight local document store used by the Node42 CLI.
4
+ It is **not a database server** — it is a structured JSON file with helper functions.
5
+
6
+ Location (Linux/macOS):
7
+ ```
8
+ ~/.node42/db.json
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Purpose
14
+
15
+ Designed for:
16
+
17
+ - Artefact history
18
+ - Participant lookups
19
+ - File references
20
+ - UUID tracking
21
+ - Small usage statistics
22
+ - Local cache
23
+
24
+ Intended scale: **1 – 10,000 records**.
25
+
26
+ ---
27
+
28
+ ## Data Structure
29
+
30
+ Example `db.json`:
31
+
32
+ ```json
33
+ {
34
+ "user": {},
35
+ "artefacts": [],
36
+ "usage": {},
37
+ }
38
+ ```
39
+
40
+ Collections are flexible and can hold any object structure.
41
+
42
+ ---
43
+
44
+ ## Core Functions
45
+
46
+ ```js
47
+ const db = require("./db");
48
+ ```
49
+
50
+ ### load()
51
+
52
+ Reads the JSON file into memory.
53
+
54
+ ```js
55
+ const db = db.load();
56
+ ```
57
+
58
+ If the file does not exist, returns a default structure.
59
+
60
+ ---
61
+
62
+ ### save(database)
63
+
64
+ Writes the in-memory object back to disk.
65
+
66
+ ```js
67
+ db.save(database);
68
+ ```
69
+
70
+ Always called after insert/update/delete.
71
+
72
+ ---
73
+
74
+ ### get(collection)
75
+
76
+ Returns a collection array or an empty array.
77
+
78
+ ```js
79
+ const artefacts = db.get("artefacts");
80
+ ```
81
+
82
+ Safe — never throws if missing.
83
+
84
+ ---
85
+
86
+ ### insert(collection, item)
87
+
88
+ Adds an item and persists.
89
+
90
+ ```js
91
+ db.insert("artefacts", {
92
+ id: "uuid",
93
+ participantId: "0007:123",
94
+ createdAt: Date.now()
95
+ });
96
+ ```
97
+
98
+ ---
99
+
100
+ ### find(collection, predicate)
101
+
102
+ Filters a collection.
103
+
104
+ ```js
105
+ const results = db.find("artefacts", x => x.participantId === pid);
106
+ ```
107
+
108
+ Works well for thousands of entries.
109
+
110
+ ---
111
+
112
+ ## Indexing (Fast Lookup)
113
+
114
+ ### indexBy(list, key)
115
+
116
+ Builds an in-memory index for repeated queries.
117
+
118
+ ```js
119
+ const artefacts = db.get("artefacts");
120
+ const byPid = db.indexBy(artefacts, "participantId");
121
+
122
+ const results = byPid["0007:123"] ?? [];
123
+ ```
124
+
125
+ Use when:
126
+ - Many lookups
127
+ - Same key repeatedly
128
+ - Performance matters
129
+
130
+ ---
131
+
132
+ ## Optional Advanced Helpers
133
+
134
+ ### indexByFn(list, fn)
135
+
136
+ Derived or computed keys.
137
+
138
+ ```js
139
+ const byDay = db.indexByFn(list, x => x.createdAt.slice(0, 10));
140
+ ```
141
+
142
+ Now **byDay** looks like:
143
+ ```json
144
+ {
145
+ "2026-01-29": [ {...}, {...} ],
146
+ "2026-01-30": [ {...} ]
147
+ }
148
+ ```
149
+
150
+ Retrieve all items for a day
151
+ ```js
152
+ const items = byDay["2026-01-29"] ?? [];
153
+ ```
154
+ ---
155
+
156
+ ### indexByMap(list, key)
157
+
158
+ Same as `indexBy` but uses `Map` instead of plain object.
159
+ Useful if keys are numbers or objects instead of strings.
160
+
161
+ ---
162
+
163
+ ## Typical CLI Workflow
164
+
165
+ ### Insert Record
166
+ ```js
167
+ db.insert("artefacts", obj);
168
+ ```
169
+
170
+ ### Simple Search
171
+ ```js
172
+ db.find("artefacts", x => x.id === uuid);
173
+ ```
174
+
175
+ ### Repeated Search
176
+ ```js
177
+ const list = db.get("artefacts");
178
+ const idx = db.indexBy(list, "participantId");
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Performance Expectations
184
+
185
+ | Records | Performance |
186
+ |--------|------------|
187
+ | 1k | Instant |
188
+ | 5k | Instant |
189
+ | 10k | Fine |
190
+ | 50k | Noticeable |
191
+ | 100k+ | Consider SQLite |
192
+
193
+ ---
194
+
195
+ ## File Safety
196
+
197
+ Recommended permissions:
198
+
199
+ ```
200
+ chmod 600 ~/.node42/db.json
201
+ ```
202
+
203
+
204
+ ## When to Upgrade to SQLite
205
+
206
+ - 100k+ records
207
+ - Complex joins
208
+ - Multi-field queries
209
+ - Concurrent writes
210
+
211
+ ---
212
+
213
+ ## Summary
214
+
215
+ This system is:
216
+
217
+ - Portable
218
+ - Dependency-free
219
+ - Easy to debug
220
+ - Perfect for CLI tools
221
+ - Sufficient for long-term local storage
222
+
223
+ It behaves like a **tiny embedded document database**, not a server DB.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n42/cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.63",
4
4
  "description": "Node42 CLI – Command-line interface for Peppol eDelivery path discovery, diagnostics, and tooling",
5
5
  "keywords": [
6
6
  "node42"
@@ -20,15 +20,19 @@
20
20
  "lint": "eslint src",
21
21
  "test": "mocha"
22
22
  },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
23
26
  "dependencies": {
24
27
  "commander": "^11.1.0",
25
28
  "inquirer": "^8.2.7",
26
29
  "open": "^11.0.0"
27
30
  },
28
31
  "devDependencies": {
29
- "chai": "^6.2.2",
32
+ "chai": "^4.5.0",
30
33
  "eslint": "^9.39.2",
31
34
  "globals": "^17.2.0",
32
- "mocha": "^11.7.5"
35
+ "mocha": "^11.3.0",
36
+ "sinon": "^21.0.1"
33
37
  }
34
38
  }
package/src/auth.js CHANGED
@@ -1,12 +1,76 @@
1
1
  const fs = require("fs");
2
- const { NODE42_DIR, TOKENS_FILE, API_URL, EP_REFRESH } = require("./config");
2
+ const { NODE42_DIR, TOKENS_FILE, API_URL, EP_SIGNIN, EP_REFRESH } = require("./config");
3
3
  const { handleError } = require("./errors");
4
- const { updateUserInfo, updateUserUsage } = require("./user");
4
+ const { getUser } = require("./user");
5
+ const { clearScreen, ask, startSpinner } = require("./utils");
6
+ const db = require("./db");
5
7
 
6
8
 
9
+ async function login() {
10
+ clearScreen("Sign in to Node42");
11
+ let user = getUser();
12
+
13
+ const username = await ask("Username", user.userMail ?? "");
14
+ const password = await ask("Password", null, true);
15
+
16
+ console.log(username + ", " + password);
17
+
18
+ let stopSpinner = startSpinner();
19
+
20
+ const res = await fetch(`${API_URL}/${EP_SIGNIN}`, {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({ username, password })
24
+ });
25
+
26
+ if (!res.ok) {
27
+ stopSpinner();
28
+
29
+ console.error(`Login failed (${res.status}) – Invalid credentials`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const tokens = await res.json();
34
+
35
+ const { accessToken, refreshToken, idToken } = tokens;
36
+ if (!accessToken || !refreshToken || !idToken) {
37
+ stopSpinner();
38
+
39
+ console.error("Invalid auth response");
40
+ process.exit(1);
41
+ }
42
+
43
+ fs.mkdirSync(NODE42_DIR, { recursive: true });
44
+ fs.writeFileSync(
45
+ TOKENS_FILE,
46
+ JSON.stringify({ accessToken, refreshToken, idToken }, null, 2)
47
+ );
48
+
49
+ stopSpinner();
50
+ stopSpinner = startSpinner();
51
+
52
+ await checkAuth();
53
+ user = getUser();
54
+
55
+ console.log(
56
+ `Authenticated as ${user.userName} <${user.userMail}> (${user.role})`
57
+ );
58
+
59
+ stopSpinner();
60
+ }
61
+
62
+ function logout() {
63
+ if (fs.existsSync(TOKENS_FILE)) {
64
+ fs.unlinkSync(TOKENS_FILE);
65
+ }
66
+
67
+ let user = getUser();
68
+ db.delete("user", user.id);
69
+ }
70
+
7
71
  function loadAuth() {
8
72
  if (!fs.existsSync(TOKENS_FILE)) {
9
- console.error("Not logged in. Run: n42 signin");
73
+ console.error("Not logged in. Run: n42 login");
10
74
  process.exit(1);
11
75
  }
12
76
  return JSON.parse(fs.readFileSync(TOKENS_FILE, "utf8"));
@@ -36,14 +100,20 @@ async function checkAuth() {
36
100
  }
37
101
 
38
102
  const auth = await res.json();
103
+ //console.log(auth);
39
104
  if (auth) {
40
- updateUserInfo({
105
+
106
+ db.upsert("user", {
107
+ id: auth.sub,
41
108
  userName: auth.userName,
42
109
  userMail: auth.userMail,
43
110
  role: auth.role,
44
- });
111
+ rateLimits: auth.rateLimits,
112
+ serviceUsage: auth.serviceUsage,
113
+ })
45
114
 
46
- updateUserUsage({ serviceUsage: auth.serviceUsage });
115
+ db.replace("serviceUsage", auth.serviceUsage);
116
+ db.replace("rateLimits", auth.rateLimits);
47
117
  return true;
48
118
  }
49
119
 
@@ -125,4 +195,4 @@ async function fetchWithAuth(url, options = {}) {
125
195
  });
126
196
  }
127
197
 
128
- module.exports = { loadAuth, checkAuth, fetchWithAuth };
198
+ module.exports = { login, logout, loadAuth, checkAuth, fetchWithAuth };
package/src/browser.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const open = require("open").default;
2
2
  let browserOpened = false;
3
3
 
4
+
4
5
  async function openOnce(target) {
5
6
  if (browserOpened) return;
6
7
  browserOpened = true;
package/src/cli.js CHANGED
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  const { Command } = require("commander");
3
- const { signin } = require("./signin");
4
- const { checkAuth } = require("./auth");
5
- const { getUserInfo } = require("./user");
4
+ const { login, logout, checkAuth } = require("./auth");
5
+ const { getUser, getUserUsage } = require("./user");
6
6
  const { runDiscovery } = require("./discover");
7
- const { clearScreen, startSpinner, validateEnv, validateId} = require("./utils");
7
+ const { clearScreen, startSpinner, validateEnv, validateId, createAppDirs, getArtefactExt } = require("./utils");
8
+ const { NODE42_DIR, ARTEFACTS_DIR, DEFAULT_OUTPUT, DEFAULT_FORMAT } = require("./config");
9
+
10
+ createAppDirs();
8
11
 
9
12
  const program = new Command();
10
13
  const pkg = require("../package.json");
14
+ const db = require("./db");
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
11
18
 
12
19
  program
13
20
  .name("n42")
@@ -15,23 +22,91 @@ program
15
22
  .version(pkg.version);
16
23
 
17
24
  program
18
- .command("signin")
25
+ .command("completion <shell>")
26
+ .description("Install shell completion")
27
+ .action((shell) => {
28
+
29
+ if (shell !== "bash") {
30
+ console.error("Only bash supported");
31
+ return;
32
+ }
33
+
34
+ const src = path.join(__dirname, "completion/bash.sh");
35
+ const dest = path.join(NODE42_DIR, "completion.bash");
36
+ fs.copyFileSync(src, dest);
37
+
38
+ console.log(`Completion script saved to ${dest}`);
39
+ console.log(`Run this once:`);
40
+ console.log(`source ${dest}`);
41
+ });
42
+
43
+ program
44
+ .command("login")
19
45
  .description("Authenticate using username and password and store tokens locally")
20
- .action(signin);
46
+ .action(login);
47
+
48
+ program
49
+ .command("logout")
50
+ .description("Terminate user session and delete all local tokens")
51
+ .action(logout);
21
52
 
22
53
  program
23
54
  .command("me")
24
- .description("returns identity and billing status for the authenticated user.")
25
- .action(() => {
55
+ .description("Returns identity and billing status for the authenticated user.")
56
+ .action(async () => {
26
57
  const stopSpinner = startSpinner();
27
58
 
28
- checkAuth();
29
- const user = getUserInfo();
30
- console.log(
31
- `Authenticated as ${user.userName} <${user.userMail}> (${user.role})`
32
- );
33
-
59
+ await checkAuth();
60
+ const user = getUser();
34
61
  stopSpinner();
62
+
63
+ const currentMonth = new Date().toISOString().slice(0, 7);
64
+ console.log(`Node42 CLI v${pkg.version}
65
+ User
66
+ ID : ${user.id}
67
+ Name : ${user.userName}
68
+ Email : ${user.userMail}
69
+ Role : ${user.role}
70
+
71
+ Rate Limits
72
+ Discovery : ${user.rateLimits.discovery}
73
+ Transactions : ${user.rateLimits.transactions}
74
+ Validation : ${user.rateLimits.validation}
75
+
76
+ Usage (Current Month)
77
+ Discovery : ${user.serviceUsage.discovery[currentMonth] ?? 0}
78
+ Transactions : ${user.serviceUsage.transactions[currentMonth] ?? 0}
79
+ Validation : ${user.serviceUsage.validation[currentMonth] ?? 0}
80
+ `);
81
+ });
82
+
83
+ program
84
+ .command("usage <service>")
85
+ .description("Returns usage for the authenticated user.")
86
+ .action((service) => {
87
+ const userUsage = getUserUsage();
88
+ const currentMonth = new Date().toISOString().slice(0, 7);
89
+
90
+ const usage =
91
+ userUsage?.serviceUsage?.[service]?.[currentMonth] ?? 0;
92
+
93
+ clearScreen(`Node42 CLI v${pkg.version}`);
94
+ console.log(`Usage for ${service} (${currentMonth}): ${usage}`);
95
+ });
96
+
97
+ program
98
+ .command("history <participantId>")
99
+ .description("Show local discovery history for a participant")
100
+ .action((participantId) => {
101
+ const artefacts = db.artefactsByParticipant(participantId);
102
+
103
+ for (const item of artefacts) {
104
+ const d = new Date(item.createdAt);
105
+ const dt = d.toISOString().slice(0,19).replace("T"," ");
106
+ const ext = getArtefactExt(item.output, item.format);
107
+ const file = path.join(ARTEFACTS_DIR, `${item.id}.${ext}`);
108
+ console.log(`${dt}: ${file}`);
109
+ }
35
110
  });
36
111
 
37
112
  const discover = program
@@ -39,20 +114,20 @@ const discover = program
39
114
  .description("Discovery and diagnostic tooling for eDelivery paths");
40
115
 
41
116
  discover
42
- .command("peppol <environment> <participantId>")
117
+ .command("peppol <participantId>")
43
118
  .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")
119
+ .option("-e, --env <environment>", "Environment", "TEST")
120
+ .option("-o, --output <type>", "Result type (json | plantuml)", DEFAULT_OUTPUT)
121
+ .option("-f, --format <format>", "When output=plantuml (svg | text)", DEFAULT_FORMAT)
46
122
  .option("--force-https", "Force HTTPS endpoints", true)
47
123
  .option("--insecure", "Disable TLS certificate validation", false)
48
124
  .option("--fetch-business-card", "Fetch Peppol business card", false)
49
125
  .option("--reverse-lookup", "Enable reverse lookup", false)
50
126
  .option("--probe-endpoints", "Probe resolved endpoints", false)
51
- .action((environment, participantId, options) => {
52
-
127
+ .action((participantId, options) => {
53
128
  clearScreen(`Node42 CLI v${pkg.version}`);
54
129
 
55
- try { validateEnv(environment); }
130
+ try { validateEnv(options.env); }
56
131
  catch (e) {
57
132
  console.error(e.message);
58
133
  process.exit(1);
@@ -64,7 +139,7 @@ discover
64
139
  process.exit(1);
65
140
  }
66
141
 
67
- runDiscovery(environment.toUpperCase(), participantId, options);
142
+ runDiscovery(participantId, options);
68
143
  });
69
144
 
70
145
  program.parse(process.argv);
@@ -0,0 +1,19 @@
1
+ _n42_completions()
2
+ {
3
+ local cur prev words cword
4
+ _init_completion || return
5
+
6
+ local commands="login logout me usage history discover"
7
+
8
+ if [[ $cword -eq 1 ]]; then
9
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
10
+ return
11
+ fi
12
+
13
+ if [[ ${words[1]} == "discover" && $cword -eq 2 ]]; then
14
+ COMPREPLY=( $(compgen -W "peppol" -- "$cur") )
15
+ return
16
+ fi
17
+ }
18
+
19
+ complete -F _n42_completions n42
package/src/config.js CHANGED
@@ -1,4 +1,3 @@
1
- const fs = require("fs");
2
1
  const path = require("path");
3
2
  const os = require("os");
4
3
 
@@ -6,6 +5,7 @@ const config = {
6
5
  APP_NAME: "n42",
7
6
  API_URL: "https://api.node42.dev",
8
7
  WWW_URL: "https://www.node42.dev",
8
+
9
9
  API_TIMEOUT_MS: 30000,
10
10
 
11
11
  NODE42_DIR: path.join(os.homedir(), ".node42"),
@@ -14,12 +14,11 @@ const config = {
14
14
  TRANSACTIONS_DIR: null,
15
15
  VALIDATION_DIR: null,
16
16
 
17
- USAGE_FILE: null,
18
- USER_FILE: null,
17
+ DATABASE_FILE: null,
19
18
  TOKENS_FILE: null,
20
19
  CONFIG_FILE: null,
21
20
 
22
- DEFAULT_OUTPUT: "json",
21
+ DEFAULT_OUTPUT: "plantuml",
23
22
  DEFAULT_FORMAT: "svg",
24
23
 
25
24
  EP_SIGNIN: "auth/signin",
@@ -31,24 +30,8 @@ config.ARTEFACTS_DIR = path.join(config.NODE42_DIR, "artefacts", "discovery");
31
30
  config.TRANSACTIONS_DIR = path.join(config.NODE42_DIR, "artefacts", "transactions");
32
31
  config.VALIDATION_DIR = path.join(config.NODE42_DIR, "artefacts", "validation");
33
32
 
34
- config.USER_FILE = path.join(config.NODE42_DIR, "user.json");
35
- config.USAGE_FILE = path.join(config.NODE42_DIR, "usage.json");
33
+ config.DATABASE_FILE = path.join(config.NODE42_DIR, "db.json");
36
34
  config.TOKENS_FILE = path.join(config.NODE42_DIR, "tokens.json");
37
35
  config.CONFIG_FILE = path.join(config.NODE42_DIR, "config.json");
38
36
 
39
- function createAppDirs() {
40
- fs.mkdirSync(config.NODE42_DIR, { recursive: true });
41
-
42
- fs.mkdirSync(config.ARTEFACTS_DIR, { recursive: true });
43
- fs.mkdirSync(config.TRANSACTIONS_DIR, { recursive: true });
44
- fs.mkdirSync(config.VALIDATION_DIR, { recursive: true });
45
-
46
- fs.writeFileSync(
47
- config.CONFIG_FILE,
48
- JSON.stringify(config, null, 2)
49
- );
50
- }
51
-
52
- createAppDirs();
53
-
54
- module.exports = config;
37
+ module.exports = config;