@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 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,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.tenantUrl} ${p.lastStudentName ? `(${p.lastStudentName})` : ""}`.trim(),
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
- return (await select(opts));
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
- return await input(opts);
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
- return await password(opts);
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"));
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.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",