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 +90 -0
- package/dist/core/config.js +10 -2
- package/package.json +1 -1
- package/skill/SKILL.md +47 -0
- package/src/cli/index.ts +105 -1
- package/src/core/config.ts +11 -2
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();
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
|
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
|
|
22
|
+
console.error("Config file is corrupted. Run: bayane login");
|
|
15
23
|
process.exit(1);
|
|
16
24
|
}
|
|
17
25
|
}
|
package/package.json
CHANGED
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();
|
package/src/core/config.ts
CHANGED
|
@@ -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
|
|
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
|
|
25
|
+
console.error("Config file is corrupted. Run: bayane login");
|
|
17
26
|
process.exit(1);
|
|
18
27
|
}
|
|
19
28
|
}
|