@wilm-ai/wilma-cli 0.0.1 → 0.0.3
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 +90 -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,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2
3
|
import { select, input, password } from "@inquirer/prompts";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
3
6
|
import { WilmaClient, listTenants, } from "@wilm-ai/wilma-client";
|
|
4
7
|
import { clearConfig, getConfigPath, loadConfig, obfuscateSecret, revealSecret, saveConfig, } from "./config.js";
|
|
8
|
+
// Enable keypress events for escape key detection
|
|
9
|
+
if (process.stdin.isTTY) {
|
|
10
|
+
emitKeypressEvents(process.stdin);
|
|
11
|
+
}
|
|
5
12
|
const ACTIONS = [
|
|
6
13
|
{ value: "news", name: "List news" },
|
|
7
14
|
{ value: "exams", name: "List exams" },
|
|
@@ -10,6 +17,15 @@ const ACTIONS = [
|
|
|
10
17
|
];
|
|
11
18
|
async function main() {
|
|
12
19
|
const args = process.argv.slice(2);
|
|
20
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
21
|
+
printUsage();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
25
|
+
const version = await readPackageVersion();
|
|
26
|
+
console.log(version);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
13
29
|
if (args[0] === "config" && args[1] === "clear") {
|
|
14
30
|
await clearConfig();
|
|
15
31
|
console.log(`Cleared config at ${getConfigPath()}`);
|
|
@@ -26,7 +42,7 @@ async function chooseProfile(config) {
|
|
|
26
42
|
if (config.profiles.length) {
|
|
27
43
|
const choices = config.profiles.map((p) => ({
|
|
28
44
|
value: p.id,
|
|
29
|
-
name: `${p.username} @ ${p.
|
|
45
|
+
name: `${p.username} @ ${p.tenantName ?? p.tenantUrl}`.trim(),
|
|
30
46
|
}));
|
|
31
47
|
choices.push({ value: "new", name: "Use a new login" });
|
|
32
48
|
const selected = await selectOrCancel({
|
|
@@ -91,6 +107,7 @@ async function chooseProfile(config) {
|
|
|
91
107
|
const stored = {
|
|
92
108
|
id: `${tenant.url}|${username}`,
|
|
93
109
|
tenantUrl: tenant.url,
|
|
110
|
+
tenantName: tenant.name,
|
|
94
111
|
username,
|
|
95
112
|
passwordObfuscated: obfuscateSecret(passwordValue),
|
|
96
113
|
students: students.map((s) => ({ studentNumber: s.studentNumber, name: s.name })),
|
|
@@ -106,8 +123,9 @@ async function chooseProfile(config) {
|
|
|
106
123
|
async function runInteractive(config) {
|
|
107
124
|
while (true) {
|
|
108
125
|
const profile = await chooseProfile(config);
|
|
109
|
-
if (!profile)
|
|
126
|
+
if (!profile) {
|
|
110
127
|
return;
|
|
128
|
+
}
|
|
111
129
|
const client = await WilmaClient.login(profile);
|
|
112
130
|
let nextAction = await selectOrCancel({
|
|
113
131
|
message: "What do you want to view?",
|
|
@@ -118,6 +136,7 @@ async function runInteractive(config) {
|
|
|
118
136
|
],
|
|
119
137
|
});
|
|
120
138
|
if (nextAction === null) {
|
|
139
|
+
// Esc from main menu -> back to student picker
|
|
121
140
|
continue;
|
|
122
141
|
}
|
|
123
142
|
while (nextAction !== "exit" && nextAction !== "back") {
|
|
@@ -125,6 +144,7 @@ async function runInteractive(config) {
|
|
|
125
144
|
await selectNewsToRead(client);
|
|
126
145
|
}
|
|
127
146
|
if (nextAction === "exams") {
|
|
147
|
+
console.clear();
|
|
128
148
|
await outputExams(client, { limit: 20, json: false });
|
|
129
149
|
}
|
|
130
150
|
if (nextAction === "messages") {
|
|
@@ -149,8 +169,9 @@ async function runInteractive(config) {
|
|
|
149
169
|
{ value: "back", name: "Back to students" },
|
|
150
170
|
{ value: "exit", name: "Exit" },
|
|
151
171
|
],
|
|
152
|
-
});
|
|
172
|
+
}, false); // Don't clear screen - preserve content output
|
|
153
173
|
if (nextAction === null) {
|
|
174
|
+
// Esc from action menu -> back to student picker
|
|
154
175
|
nextAction = "back";
|
|
155
176
|
}
|
|
156
177
|
}
|
|
@@ -414,6 +435,20 @@ async function handleCommand(args, config) {
|
|
|
414
435
|
console.log(" wilma messages read <id> [--json]");
|
|
415
436
|
console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
|
|
416
437
|
console.log(" wilma config clear");
|
|
438
|
+
console.log(" wilma --help | -h");
|
|
439
|
+
console.log(" wilma --version | -v");
|
|
440
|
+
}
|
|
441
|
+
function printUsage() {
|
|
442
|
+
console.log("Usage:");
|
|
443
|
+
console.log(" wilma kids list [--json]");
|
|
444
|
+
console.log(" wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]");
|
|
445
|
+
console.log(" wilma news read <id> [--json]");
|
|
446
|
+
console.log(" wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]");
|
|
447
|
+
console.log(" wilma messages read <id> [--json]");
|
|
448
|
+
console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
|
|
449
|
+
console.log(" wilma config clear");
|
|
450
|
+
console.log(" wilma --help | -h");
|
|
451
|
+
console.log(" wilma --version | -v");
|
|
417
452
|
}
|
|
418
453
|
async function getProfileForCommandNonInteractive(config, flags) {
|
|
419
454
|
if (!config.lastProfileId) {
|
|
@@ -486,6 +521,12 @@ function parseArgs(args) {
|
|
|
486
521
|
}
|
|
487
522
|
return { command, subcommand, flags };
|
|
488
523
|
}
|
|
524
|
+
async function readPackageVersion() {
|
|
525
|
+
const pkgPath = resolve(dirname(new URL(import.meta.url).pathname), "..", "package.json");
|
|
526
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
527
|
+
const data = JSON.parse(raw);
|
|
528
|
+
return data.version ?? "unknown";
|
|
529
|
+
}
|
|
489
530
|
async function outputNews(client, opts) {
|
|
490
531
|
const news = await client.news.list();
|
|
491
532
|
const slice = news.slice(0, opts.limit);
|
|
@@ -577,8 +618,10 @@ async function selectNewsToRead(client) {
|
|
|
577
618
|
}
|
|
578
619
|
async function selectMessageToRead(client, folder) {
|
|
579
620
|
const messages = await client.messages.list(folder);
|
|
580
|
-
if (!messages.length)
|
|
621
|
+
if (!messages.length) {
|
|
622
|
+
console.log(`\nNo messages found in ${folder}.`);
|
|
581
623
|
return;
|
|
624
|
+
}
|
|
582
625
|
const choices = messages.slice(0, 30).map((msg) => {
|
|
583
626
|
const date = msg.sentAt.toISOString().slice(0, 10);
|
|
584
627
|
return {
|
|
@@ -595,9 +638,20 @@ async function selectMessageToRead(client, folder) {
|
|
|
595
638
|
return;
|
|
596
639
|
await outputMessageItem(client, Number(selected), false);
|
|
597
640
|
}
|
|
598
|
-
async function selectOrCancel(opts) {
|
|
641
|
+
async function selectOrCancel(opts, clearScreen = true) {
|
|
642
|
+
if (clearScreen) {
|
|
643
|
+
console.clear();
|
|
644
|
+
}
|
|
645
|
+
const prompt = select(opts, { clearPromptOnDone: true });
|
|
646
|
+
const onKeypress = (_ch, key) => {
|
|
647
|
+
if (key?.name === "escape") {
|
|
648
|
+
prompt.cancel();
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
process.stdin.on("keypress", onKeypress);
|
|
599
652
|
try {
|
|
600
|
-
|
|
653
|
+
const result = await prompt;
|
|
654
|
+
return result;
|
|
601
655
|
}
|
|
602
656
|
catch (err) {
|
|
603
657
|
if (isPromptCancel(err)) {
|
|
@@ -605,10 +659,22 @@ async function selectOrCancel(opts) {
|
|
|
605
659
|
}
|
|
606
660
|
throw err;
|
|
607
661
|
}
|
|
662
|
+
finally {
|
|
663
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
664
|
+
}
|
|
608
665
|
}
|
|
609
666
|
async function inputOrCancel(opts) {
|
|
667
|
+
console.clear();
|
|
668
|
+
const prompt = input(opts, { clearPromptOnDone: true });
|
|
669
|
+
const onKeypress = (_ch, key) => {
|
|
670
|
+
if (key?.name === "escape") {
|
|
671
|
+
prompt.cancel();
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
process.stdin.on("keypress", onKeypress);
|
|
610
675
|
try {
|
|
611
|
-
|
|
676
|
+
const result = await prompt;
|
|
677
|
+
return result;
|
|
612
678
|
}
|
|
613
679
|
catch (err) {
|
|
614
680
|
if (isPromptCancel(err)) {
|
|
@@ -616,10 +682,22 @@ async function inputOrCancel(opts) {
|
|
|
616
682
|
}
|
|
617
683
|
throw err;
|
|
618
684
|
}
|
|
685
|
+
finally {
|
|
686
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
687
|
+
}
|
|
619
688
|
}
|
|
620
689
|
async function passwordOrCancel(opts) {
|
|
690
|
+
console.clear();
|
|
691
|
+
const prompt = password(opts, { clearPromptOnDone: true });
|
|
692
|
+
const onKeypress = (_ch, key) => {
|
|
693
|
+
if (key?.name === "escape") {
|
|
694
|
+
prompt.cancel();
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
process.stdin.on("keypress", onKeypress);
|
|
621
698
|
try {
|
|
622
|
-
|
|
699
|
+
const result = await prompt;
|
|
700
|
+
return result;
|
|
623
701
|
}
|
|
624
702
|
catch (err) {
|
|
625
703
|
if (isPromptCancel(err)) {
|
|
@@ -627,6 +705,9 @@ async function passwordOrCancel(opts) {
|
|
|
627
705
|
}
|
|
628
706
|
throw err;
|
|
629
707
|
}
|
|
708
|
+
finally {
|
|
709
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
710
|
+
}
|
|
630
711
|
}
|
|
631
712
|
function isPromptCancel(err) {
|
|
632
713
|
if (!err)
|
|
@@ -635,6 +716,7 @@ function isPromptCancel(err) {
|
|
|
635
716
|
const name = err instanceof Error ? err.name : "";
|
|
636
717
|
return (name === "AbortError" ||
|
|
637
718
|
name === "ExitPromptError" ||
|
|
719
|
+
name === "CancelPromptError" ||
|
|
638
720
|
message.includes("User force closed the prompt") ||
|
|
639
721
|
message.toLowerCase().includes("cancel") ||
|
|
640
722
|
message.toLowerCase().includes("aborted"));
|