@wilm-ai/wilma-cli 0.0.1 → 0.0.2
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 +2 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +7 -1
- package/dist/index.js +59 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ wilma messages list --folder inbox --all --json
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Config
|
|
27
|
-
Local config is stored in
|
|
28
|
-
Use `wilma config clear` to remove it.
|
|
27
|
+
Local config is stored in `~/.config/wilmai/config.json` (or `$XDG_CONFIG_HOME/wilmai/config.json`).
|
|
28
|
+
Use `wilma config clear` to remove it. Override with `WILMAI_CONFIG_PATH`.
|
|
29
29
|
|
|
30
30
|
## Notes
|
|
31
31
|
- Credentials are stored with lightweight obfuscation for convenience.
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
const SALT = "wilmai::";
|
|
4
5
|
export function getConfigPath() {
|
|
5
6
|
const override = process.env.WILMAI_CONFIG_PATH;
|
|
6
7
|
if (override) {
|
|
7
8
|
return resolve(override);
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
11
|
+
const base = xdg ? resolve(xdg) : resolve(homedir(), ".config");
|
|
12
|
+
return resolve(base, "wilmai", "config.json");
|
|
10
13
|
}
|
|
11
14
|
export async function loadConfig() {
|
|
12
15
|
const path = getConfigPath();
|
|
@@ -25,6 +28,9 @@ export async function loadConfig() {
|
|
|
25
28
|
p.lastStudentNumber = p.studentNumber;
|
|
26
29
|
p.lastStudentName = p.studentName ?? p.studentNumber;
|
|
27
30
|
}
|
|
31
|
+
if (!p.tenantName) {
|
|
32
|
+
p.tenantName = p.tenantUrl;
|
|
33
|
+
}
|
|
28
34
|
delete p.studentNumber;
|
|
29
35
|
delete p.studentName;
|
|
30
36
|
return p;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2
3
|
import { select, input, password } from "@inquirer/prompts";
|
|
3
4
|
import { WilmaClient, listTenants, } from "@wilm-ai/wilma-client";
|
|
4
5
|
import { clearConfig, getConfigPath, loadConfig, obfuscateSecret, revealSecret, saveConfig, } from "./config.js";
|
|
6
|
+
// Enable keypress events for escape key detection
|
|
7
|
+
if (process.stdin.isTTY) {
|
|
8
|
+
emitKeypressEvents(process.stdin);
|
|
9
|
+
}
|
|
5
10
|
const ACTIONS = [
|
|
6
11
|
{ value: "news", name: "List news" },
|
|
7
12
|
{ value: "exams", name: "List exams" },
|
|
@@ -26,7 +31,7 @@ async function chooseProfile(config) {
|
|
|
26
31
|
if (config.profiles.length) {
|
|
27
32
|
const choices = config.profiles.map((p) => ({
|
|
28
33
|
value: p.id,
|
|
29
|
-
name: `${p.username} @ ${p.
|
|
34
|
+
name: `${p.username} @ ${p.tenantName ?? p.tenantUrl}`.trim(),
|
|
30
35
|
}));
|
|
31
36
|
choices.push({ value: "new", name: "Use a new login" });
|
|
32
37
|
const selected = await selectOrCancel({
|
|
@@ -91,6 +96,7 @@ async function chooseProfile(config) {
|
|
|
91
96
|
const stored = {
|
|
92
97
|
id: `${tenant.url}|${username}`,
|
|
93
98
|
tenantUrl: tenant.url,
|
|
99
|
+
tenantName: tenant.name,
|
|
94
100
|
username,
|
|
95
101
|
passwordObfuscated: obfuscateSecret(passwordValue),
|
|
96
102
|
students: students.map((s) => ({ studentNumber: s.studentNumber, name: s.name })),
|
|
@@ -106,8 +112,9 @@ async function chooseProfile(config) {
|
|
|
106
112
|
async function runInteractive(config) {
|
|
107
113
|
while (true) {
|
|
108
114
|
const profile = await chooseProfile(config);
|
|
109
|
-
if (!profile)
|
|
115
|
+
if (!profile) {
|
|
110
116
|
return;
|
|
117
|
+
}
|
|
111
118
|
const client = await WilmaClient.login(profile);
|
|
112
119
|
let nextAction = await selectOrCancel({
|
|
113
120
|
message: "What do you want to view?",
|
|
@@ -118,6 +125,7 @@ async function runInteractive(config) {
|
|
|
118
125
|
],
|
|
119
126
|
});
|
|
120
127
|
if (nextAction === null) {
|
|
128
|
+
// Esc from main menu -> back to student picker
|
|
121
129
|
continue;
|
|
122
130
|
}
|
|
123
131
|
while (nextAction !== "exit" && nextAction !== "back") {
|
|
@@ -125,6 +133,7 @@ async function runInteractive(config) {
|
|
|
125
133
|
await selectNewsToRead(client);
|
|
126
134
|
}
|
|
127
135
|
if (nextAction === "exams") {
|
|
136
|
+
console.clear();
|
|
128
137
|
await outputExams(client, { limit: 20, json: false });
|
|
129
138
|
}
|
|
130
139
|
if (nextAction === "messages") {
|
|
@@ -149,8 +158,9 @@ async function runInteractive(config) {
|
|
|
149
158
|
{ value: "back", name: "Back to students" },
|
|
150
159
|
{ value: "exit", name: "Exit" },
|
|
151
160
|
],
|
|
152
|
-
});
|
|
161
|
+
}, false); // Don't clear screen - preserve content output
|
|
153
162
|
if (nextAction === null) {
|
|
163
|
+
// Esc from action menu -> back to student picker
|
|
154
164
|
nextAction = "back";
|
|
155
165
|
}
|
|
156
166
|
}
|
|
@@ -577,8 +587,10 @@ async function selectNewsToRead(client) {
|
|
|
577
587
|
}
|
|
578
588
|
async function selectMessageToRead(client, folder) {
|
|
579
589
|
const messages = await client.messages.list(folder);
|
|
580
|
-
if (!messages.length)
|
|
590
|
+
if (!messages.length) {
|
|
591
|
+
console.log(`\nNo messages found in ${folder}.`);
|
|
581
592
|
return;
|
|
593
|
+
}
|
|
582
594
|
const choices = messages.slice(0, 30).map((msg) => {
|
|
583
595
|
const date = msg.sentAt.toISOString().slice(0, 10);
|
|
584
596
|
return {
|
|
@@ -595,9 +607,20 @@ async function selectMessageToRead(client, folder) {
|
|
|
595
607
|
return;
|
|
596
608
|
await outputMessageItem(client, Number(selected), false);
|
|
597
609
|
}
|
|
598
|
-
async function selectOrCancel(opts) {
|
|
610
|
+
async function selectOrCancel(opts, clearScreen = true) {
|
|
611
|
+
if (clearScreen) {
|
|
612
|
+
console.clear();
|
|
613
|
+
}
|
|
614
|
+
const prompt = select(opts, { clearPromptOnDone: true });
|
|
615
|
+
const onKeypress = (_ch, key) => {
|
|
616
|
+
if (key?.name === "escape") {
|
|
617
|
+
prompt.cancel();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
process.stdin.on("keypress", onKeypress);
|
|
599
621
|
try {
|
|
600
|
-
|
|
622
|
+
const result = await prompt;
|
|
623
|
+
return result;
|
|
601
624
|
}
|
|
602
625
|
catch (err) {
|
|
603
626
|
if (isPromptCancel(err)) {
|
|
@@ -605,10 +628,22 @@ async function selectOrCancel(opts) {
|
|
|
605
628
|
}
|
|
606
629
|
throw err;
|
|
607
630
|
}
|
|
631
|
+
finally {
|
|
632
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
633
|
+
}
|
|
608
634
|
}
|
|
609
635
|
async function inputOrCancel(opts) {
|
|
636
|
+
console.clear();
|
|
637
|
+
const prompt = input(opts, { clearPromptOnDone: true });
|
|
638
|
+
const onKeypress = (_ch, key) => {
|
|
639
|
+
if (key?.name === "escape") {
|
|
640
|
+
prompt.cancel();
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
process.stdin.on("keypress", onKeypress);
|
|
610
644
|
try {
|
|
611
|
-
|
|
645
|
+
const result = await prompt;
|
|
646
|
+
return result;
|
|
612
647
|
}
|
|
613
648
|
catch (err) {
|
|
614
649
|
if (isPromptCancel(err)) {
|
|
@@ -616,10 +651,22 @@ async function inputOrCancel(opts) {
|
|
|
616
651
|
}
|
|
617
652
|
throw err;
|
|
618
653
|
}
|
|
654
|
+
finally {
|
|
655
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
656
|
+
}
|
|
619
657
|
}
|
|
620
658
|
async function passwordOrCancel(opts) {
|
|
659
|
+
console.clear();
|
|
660
|
+
const prompt = password(opts, { clearPromptOnDone: true });
|
|
661
|
+
const onKeypress = (_ch, key) => {
|
|
662
|
+
if (key?.name === "escape") {
|
|
663
|
+
prompt.cancel();
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
process.stdin.on("keypress", onKeypress);
|
|
621
667
|
try {
|
|
622
|
-
|
|
668
|
+
const result = await prompt;
|
|
669
|
+
return result;
|
|
623
670
|
}
|
|
624
671
|
catch (err) {
|
|
625
672
|
if (isPromptCancel(err)) {
|
|
@@ -627,6 +674,9 @@ async function passwordOrCancel(opts) {
|
|
|
627
674
|
}
|
|
628
675
|
throw err;
|
|
629
676
|
}
|
|
677
|
+
finally {
|
|
678
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
679
|
+
}
|
|
630
680
|
}
|
|
631
681
|
function isPromptCancel(err) {
|
|
632
682
|
if (!err)
|
|
@@ -635,6 +685,7 @@ function isPromptCancel(err) {
|
|
|
635
685
|
const name = err instanceof Error ? err.name : "";
|
|
636
686
|
return (name === "AbortError" ||
|
|
637
687
|
name === "ExitPromptError" ||
|
|
688
|
+
name === "CancelPromptError" ||
|
|
638
689
|
message.includes("User force closed the prompt") ||
|
|
639
690
|
message.toLowerCase().includes("cancel") ||
|
|
640
691
|
message.toLowerCase().includes("aborted"));
|