@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 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 `.wilmai/config.json` in the current working directory.
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
@@ -1,6 +1,7 @@
1
1
  export interface StoredProfile {
2
2
  id: string;
3
3
  tenantUrl: string;
4
+ tenantName?: string | null;
4
5
  username: string;
5
6
  passwordObfuscated: string;
6
7
  students?: {
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
- return resolve(process.cwd(), ".wilmai", "config.json");
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.tenantUrl} ${p.lastStudentName ? `(${p.lastStudentName})` : ""}`.trim(),
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
- return (await select(opts));
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
- return await input(opts);
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
- return await password(opts);
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"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wilm-ai/wilma-cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",