bayane 1.0.0 → 1.1.0

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/dist/cli/index.js CHANGED
@@ -3,12 +3,87 @@ import { Command } from "commander";
3
3
  import { password, input } from "@inquirer/prompts";
4
4
  import { writeFileSync } from "fs";
5
5
  import { spawnSync } from "child_process";
6
+ import { createHash, randomBytes } from "crypto";
7
+ import { createServer } from "http";
6
8
  import { loadConfig, saveConfig, CONFIG_PATH } from "../core/config.js";
7
9
  import { fetchStats, fetchReportPdf, fetchLatestReportDate } from "../core/api.js";
8
10
  import { formatStats, formatAlerts } from "../core/format.js";
9
11
  const DEFAULT_API_URL = "https://health-bd.nonas.app";
12
+ async function pkceLogin(apiUrl) {
13
+ // 1. Generate PKCE pair
14
+ const verifier = randomBytes(32).toString("base64url");
15
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
16
+ const state = randomBytes(16).toString("hex");
17
+ // 2. Start local callback server on a random port
18
+ const code = await new Promise((resolve, reject) => {
19
+ const server = createServer((req, res) => {
20
+ const url = new URL(req.url, "http://localhost");
21
+ const returnedCode = url.searchParams.get("code");
22
+ const returnedState = url.searchParams.get("state");
23
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
24
+ if (!returnedCode || returnedState !== state) {
25
+ res.end("<h2>Erreur d'authentification. Revenez au terminal.</h2>");
26
+ server.close();
27
+ reject(new Error("State mismatch or missing code."));
28
+ return;
29
+ }
30
+ res.end("<h2 style='font-family:sans-serif;text-align:center;margin-top:4rem'>✓ Authentification réussie. Revenez au terminal.</h2>");
31
+ server.close();
32
+ resolve(returnedCode);
33
+ });
34
+ server.listen(0, "127.0.0.1", () => {
35
+ const port = server.address().port;
36
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
37
+ const adminUrl = new URL(`${apiUrl.replace("health-bd", "health")}/fr/admin`);
38
+ adminUrl.searchParams.set("code_challenge", challenge);
39
+ adminUrl.searchParams.set("code_challenge_method", "S256");
40
+ adminUrl.searchParams.set("redirect_uri", redirectUri);
41
+ adminUrl.searchParams.set("state", state);
42
+ console.log("\nOuverture du navigateur pour l'authentification...");
43
+ console.log(`\nSi le navigateur ne s'ouvre pas, visitez:\n${adminUrl.toString()}\n`);
44
+ // Open browser
45
+ const cmd = process.platform === "darwin"
46
+ ? "open"
47
+ : process.platform === "win32"
48
+ ? "start"
49
+ : "xdg-open";
50
+ spawnSync(cmd, [adminUrl.toString()], { shell: process.platform === "win32" });
51
+ });
52
+ server.on("error", reject);
53
+ });
54
+ // 3. Exchange code for API key
55
+ const res = await fetch(`${apiUrl}/api/oauth/exchange`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ code, codeVerifier: verifier, state }),
59
+ });
60
+ if (!res.ok) {
61
+ const body = await res.json().catch(() => ({}));
62
+ throw new Error(body.error ?? `Exchange failed: ${res.status}`);
63
+ }
64
+ const { apiKey } = (await res.json());
65
+ return apiKey;
66
+ }
10
67
  const program = new Command();
11
68
  program.name("bayane").description("Hospital satisfaction survey CLI").version("1.0.0");
69
+ // bayane login — OAuth PKCE flow (opens browser)
70
+ program
71
+ .command("login")
72
+ .description("Authenticate via browser (opens admin dashboard)")
73
+ .option("--api-url <url>", "API URL", DEFAULT_API_URL)
74
+ .action(async (opts) => {
75
+ try {
76
+ console.log("Connexion à Bayane...");
77
+ const apiUrl = opts.apiUrl;
78
+ const apiKey = await pkceLogin(apiUrl);
79
+ saveConfig({ apiUrl, apiKey });
80
+ console.log(`\n✓ Connecté. Config enregistrée dans ${CONFIG_PATH}`);
81
+ }
82
+ catch (err) {
83
+ console.error("\n✗", err instanceof Error ? err.message : String(err));
84
+ process.exit(1);
85
+ }
86
+ });
12
87
  // bayane setup
13
88
  program
14
89
  .command("setup")
@@ -91,4 +166,19 @@ program
91
166
  console.log("Installing bayane skill...");
92
167
  spawnSync("npx", ["skills", "add", "anouar-bm/bayane"], { stdio: "inherit" });
93
168
  });
169
+ // bayane update
170
+ program
171
+ .command("update")
172
+ .description("Update bayane CLI to the latest version")
173
+ .action(() => {
174
+ console.log("Updating bayane...");
175
+ const result = spawnSync("npm", ["install", "-g", "bayane@latest"], { stdio: "inherit" });
176
+ if (result.status === 0) {
177
+ console.log("✓ bayane updated successfully.");
178
+ }
179
+ else {
180
+ console.error("✗ Update failed. Try: npm install -g bayane@latest");
181
+ process.exit(1);
182
+ }
183
+ });
94
184
  program.parse();
@@ -3,15 +3,23 @@ import { join } from "path";
3
3
  import { readFileSync, writeFileSync, existsSync } from "fs";
4
4
  export const CONFIG_PATH = join(homedir(), ".health-survey-rc");
5
5
  export function loadConfig() {
6
+ // Env var override — useful for CI / scripts
7
+ const envKey = process.env.BAYANE_API_KEY;
8
+ if (envKey) {
9
+ return {
10
+ apiUrl: process.env.BAYANE_API_URL ?? "https://health-bd.nonas.app",
11
+ apiKey: envKey,
12
+ };
13
+ }
6
14
  if (!existsSync(CONFIG_PATH)) {
7
- console.error("Not configured. Run: bayane setup");
15
+ console.error("Not configured. Run: bayane login");
8
16
  process.exit(1);
9
17
  }
10
18
  try {
11
19
  return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
12
20
  }
13
21
  catch {
14
- console.error("Config file is corrupted. Run: bayane setup");
22
+ console.error("Config file is corrupted. Run: bayane login");
15
23
  process.exit(1);
16
24
  }
17
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bayane",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CLI and MCP server for hospital satisfaction survey data",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md ADDED
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: bayane
3
+ description: Query hospital satisfaction data — stats, reports, alerts
4
+ triggers:
5
+ - satisfaction score
6
+ - hospital stats
7
+ - download report
8
+ - critical alerts
9
+ - average score
10
+ - how are we doing
11
+ ---
12
+
13
+ # bayane skill
14
+
15
+ Use the `bayane` CLI to answer questions about hospital satisfaction data.
16
+
17
+ ## Prerequisites
18
+ The user must have run `bayane login` once. Config stored in `~/.health-survey-rc`.
19
+
20
+ ## Commands
21
+
22
+ **Stats:**
23
+ ```bash
24
+ bayane stats # last 7 days
25
+ bayane stats --period 30d
26
+ bayane stats --period all
27
+ ```
28
+
29
+ **Download report:**
30
+ ```bash
31
+ bayane report --latest # most recent
32
+ bayane report --date 2026-03-21
33
+ bayane report --output ~/Desktop/report.pdf
34
+ ```
35
+
36
+ **Alerts:**
37
+ ```bash
38
+ bayane alerts
39
+ ```
40
+
41
+ **Who am I:**
42
+ ```bash
43
+ bayane whoami
44
+ ```
45
+
46
+ ## Usage
47
+ Run the appropriate command via the Bash tool and summarize the output.
package/src/cli/index.ts CHANGED
@@ -1,18 +1,107 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { password, input } from "@inquirer/prompts";
4
- import { writeFileSync } from "fs";
4
+ import { writeFileSync, existsSync, readFileSync } from "fs";
5
5
  import { spawnSync } from "child_process";
6
6
  import { execSync } from "child_process";
7
+ import { createHash, randomBytes } from "crypto";
8
+ import { createServer } from "http";
7
9
  import { loadConfig, saveConfig, CONFIG_PATH } from "../core/config.js";
8
10
  import { fetchStats, fetchReportPdf, fetchLatestReportDate } from "../core/api.js";
9
11
  import { formatStats, formatAlerts } from "../core/format.js";
10
12
 
11
13
  const DEFAULT_API_URL = "https://health-bd.nonas.app";
12
14
 
15
+ async function pkceLogin(apiUrl: string): Promise<string> {
16
+ // 1. Generate PKCE pair
17
+ const verifier = randomBytes(32).toString("base64url");
18
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
19
+ const state = randomBytes(16).toString("hex");
20
+
21
+ // 2. Start local callback server on a random port
22
+ const code = await new Promise<string>((resolve, reject) => {
23
+ const server = createServer((req, res) => {
24
+ const url = new URL(req.url!, "http://localhost");
25
+ const returnedCode = url.searchParams.get("code");
26
+ const returnedState = url.searchParams.get("state");
27
+
28
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
29
+ if (!returnedCode || returnedState !== state) {
30
+ res.end("<h2>Erreur d'authentification. Revenez au terminal.</h2>");
31
+ server.close();
32
+ reject(new Error("State mismatch or missing code."));
33
+ return;
34
+ }
35
+
36
+ res.end(
37
+ "<h2 style='font-family:sans-serif;text-align:center;margin-top:4rem'>✓ Authentification réussie. Revenez au terminal.</h2>"
38
+ );
39
+ server.close();
40
+ resolve(returnedCode);
41
+ });
42
+
43
+ server.listen(0, "127.0.0.1", () => {
44
+ const port = (server.address() as { port: number }).port;
45
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
46
+ const adminUrl = new URL(`${apiUrl.replace("health-bd", "health")}/fr/admin`);
47
+ adminUrl.searchParams.set("code_challenge", challenge);
48
+ adminUrl.searchParams.set("code_challenge_method", "S256");
49
+ adminUrl.searchParams.set("redirect_uri", redirectUri);
50
+ adminUrl.searchParams.set("state", state);
51
+
52
+ console.log("\nOuverture du navigateur pour l'authentification...");
53
+ console.log(`\nSi le navigateur ne s'ouvre pas, visitez:\n${adminUrl.toString()}\n`);
54
+
55
+ // Open browser
56
+ const cmd =
57
+ process.platform === "darwin"
58
+ ? "open"
59
+ : process.platform === "win32"
60
+ ? "start"
61
+ : "xdg-open";
62
+ spawnSync(cmd, [adminUrl.toString()], { shell: process.platform === "win32" });
63
+ });
64
+
65
+ server.on("error", reject);
66
+ });
67
+
68
+ // 3. Exchange code for API key
69
+ const res = await fetch(`${apiUrl}/api/oauth/exchange`, {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({ code, codeVerifier: verifier, state }),
73
+ });
74
+
75
+ if (!res.ok) {
76
+ const body = await res.json().catch(() => ({}));
77
+ throw new Error((body as any).error ?? `Exchange failed: ${res.status}`);
78
+ }
79
+
80
+ const { apiKey } = (await res.json()) as { apiKey: string };
81
+ return apiKey;
82
+ }
83
+
13
84
  const program = new Command();
14
85
  program.name("bayane").description("Hospital satisfaction survey CLI").version("1.0.0");
15
86
 
87
+ // bayane login — OAuth PKCE flow (opens browser)
88
+ program
89
+ .command("login")
90
+ .description("Authenticate via browser (opens admin dashboard)")
91
+ .option("--api-url <url>", "API URL", DEFAULT_API_URL)
92
+ .action(async (opts) => {
93
+ try {
94
+ console.log("Connexion à Bayane...");
95
+ const apiUrl = opts.apiUrl;
96
+ const apiKey = await pkceLogin(apiUrl);
97
+ saveConfig({ apiUrl, apiKey });
98
+ console.log(`\n✓ Connecté. Config enregistrée dans ${CONFIG_PATH}`);
99
+ } catch (err: unknown) {
100
+ console.error("\n✗", err instanceof Error ? err.message : String(err));
101
+ process.exit(1);
102
+ }
103
+ });
104
+
16
105
  // bayane setup
17
106
  program
18
107
  .command("setup")
@@ -99,4 +188,19 @@ program
99
188
  spawnSync("npx", ["skills", "add", "anouar-bm/bayane"], { stdio: "inherit" });
100
189
  });
101
190
 
191
+ // bayane update
192
+ program
193
+ .command("update")
194
+ .description("Update bayane CLI to the latest version")
195
+ .action(() => {
196
+ console.log("Updating bayane...");
197
+ const result = spawnSync("npm", ["install", "-g", "bayane@latest"], { stdio: "inherit" });
198
+ if (result.status === 0) {
199
+ console.log("✓ bayane updated successfully.");
200
+ } else {
201
+ console.error("✗ Update failed. Try: npm install -g bayane@latest");
202
+ process.exit(1);
203
+ }
204
+ });
205
+
102
206
  program.parse();
@@ -6,14 +6,23 @@ import { Config } from "./types.js";
6
6
  export const CONFIG_PATH = join(homedir(), ".health-survey-rc");
7
7
 
8
8
  export function loadConfig(): Config {
9
+ // Env var override — useful for CI / scripts
10
+ const envKey = process.env.BAYANE_API_KEY;
11
+ if (envKey) {
12
+ return {
13
+ apiUrl: process.env.BAYANE_API_URL ?? "https://health-bd.nonas.app",
14
+ apiKey: envKey,
15
+ };
16
+ }
17
+
9
18
  if (!existsSync(CONFIG_PATH)) {
10
- console.error("Not configured. Run: bayane setup");
19
+ console.error("Not configured. Run: bayane login");
11
20
  process.exit(1);
12
21
  }
13
22
  try {
14
23
  return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as Config;
15
24
  } catch {
16
- console.error("Config file is corrupted. Run: bayane setup");
25
+ console.error("Config file is corrupted. Run: bayane login");
17
26
  process.exit(1);
18
27
  }
19
28
  }