apihealthz 1.0.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.
Files changed (39) hide show
  1. package/README.md +189 -0
  2. package/dist/commands/add.js +122 -0
  3. package/dist/commands/add.js.map +1 -0
  4. package/dist/commands/addApiCheck.js +14 -0
  5. package/dist/commands/addApiCheck.js.map +1 -0
  6. package/dist/commands/apiCheck.js +12 -0
  7. package/dist/commands/apiCheck.js.map +1 -0
  8. package/dist/commands/auth.js +26 -0
  9. package/dist/commands/auth.js.map +1 -0
  10. package/dist/commands/delete.js +45 -0
  11. package/dist/commands/delete.js.map +1 -0
  12. package/dist/commands/list.js +35 -0
  13. package/dist/commands/list.js.map +1 -0
  14. package/dist/index.js +19 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/services/api.js +20 -0
  17. package/dist/services/api.js.map +1 -0
  18. package/dist/services/apiHealth.js +21 -0
  19. package/dist/services/apiHealth.js.map +1 -0
  20. package/dist/services/apiHealthz.js +48 -0
  21. package/dist/services/apiHealthz.js.map +1 -0
  22. package/dist/services/auth.js +102 -0
  23. package/dist/services/auth.js.map +1 -0
  24. package/dist/services/timezone.js +29 -0
  25. package/dist/services/timezone.js.map +1 -0
  26. package/dist/utils/config.js +27 -0
  27. package/dist/utils/config.js.map +1 -0
  28. package/package.json +28 -0
  29. package/src/commands/add.ts +136 -0
  30. package/src/commands/auth.ts +28 -0
  31. package/src/commands/delete.ts +47 -0
  32. package/src/commands/list.ts +37 -0
  33. package/src/index.ts +21 -0
  34. package/src/services/api.ts +15 -0
  35. package/src/services/apiHealth.ts +25 -0
  36. package/src/services/auth.ts +121 -0
  37. package/src/services/timezone.ts +24 -0
  38. package/src/utils/config.ts +20 -0
  39. package/tsconfig.json +33 -0
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loginWithProvider = loginWithProvider;
7
+ exports.logout = logout;
8
+ exports.authStatus = authStatus;
9
+ const open_1 = __importDefault(require("open"));
10
+ const axios_1 = __importDefault(require("axios"));
11
+ const api_1 = require("./api");
12
+ const config_1 = require("../utils/config");
13
+ async function loginWithProvider(provider) {
14
+ if (!provider) {
15
+ console.log("Choose provider: github | google");
16
+ process.exit(1);
17
+ }
18
+ const { data } = await api_1.api.post("/auth/device/start", {
19
+ provider,
20
+ });
21
+ console.log("\nOpen this URL in your browser:");
22
+ console.log(data.verification_url);
23
+ console.log("\nEnter code:", data.user_code);
24
+ await (0, open_1.default)(data.verification_url);
25
+ await pollForToken(data.device_code);
26
+ }
27
+ async function pollForToken(deviceCode) {
28
+ process.stdout.write("\nWaiting for authorization");
29
+ let pollInterval = 3000; // Default 3 seconds
30
+ while (true) {
31
+ await new Promise((r) => setTimeout(r, pollInterval));
32
+ process.stdout.write(".");
33
+ let data;
34
+ try {
35
+ const response = await api_1.api.post("/auth/device/poll", {
36
+ device_code: deviceCode,
37
+ });
38
+ data = response.data;
39
+ // Update poll interval if provided by server
40
+ if (data.poll_interval) {
41
+ pollInterval = data.poll_interval * 1000; // Convert seconds to milliseconds
42
+ }
43
+ }
44
+ catch (error) {
45
+ console.log("\n✖ Error polling for authorization");
46
+ if (axios_1.default.isAxiosError(error)) {
47
+ if (error.response) {
48
+ // Server responded with error status
49
+ console.log(` Status: ${error.response.status}`);
50
+ console.log(` Message: ${error.response.statusText}`);
51
+ if (error.response.data) {
52
+ console.log(` Details:`, error.response.data);
53
+ }
54
+ }
55
+ else if (error.request) {
56
+ // Request was made but no response received
57
+ console.log(` No response received from server`);
58
+ }
59
+ else {
60
+ // Error setting up the request
61
+ console.log(` Error: ${error.message}`);
62
+ }
63
+ }
64
+ else {
65
+ // Unknown error type
66
+ console.log(` Error:`, error);
67
+ }
68
+ process.exit(1);
69
+ }
70
+ if (!data) {
71
+ console.log("\n✖ Error: No data received");
72
+ process.exit(1);
73
+ }
74
+ if (data.status === "approved") {
75
+ if (!data.token) {
76
+ console.log("\n✖ Error: No token received");
77
+ process.exit(1);
78
+ }
79
+ (0, config_1.saveToken)(data.token);
80
+ console.log("\n✔ Logged in successfully");
81
+ return;
82
+ }
83
+ if (data.status === "expired") {
84
+ console.log("\n✖ Login expired");
85
+ process.exit(1);
86
+ }
87
+ }
88
+ }
89
+ async function logout() {
90
+ (0, config_1.clearToken)();
91
+ console.log("✔ Logged out");
92
+ }
93
+ async function authStatus() {
94
+ const token = (0, config_1.getToken)();
95
+ if (!token) {
96
+ console.log("Not logged in");
97
+ return;
98
+ }
99
+ const { data } = await api_1.api.get("/auth/me");
100
+ console.log(`✔ Logged in as ${data?.email}`);
101
+ }
102
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/services/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatDate = exports.timezone = void 0;
4
+ // Get user's timezone
5
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
6
+ exports.timezone = timezone;
7
+ // Format date in user's timezone
8
+ const formatDate = (dateString) => {
9
+ if (!dateString)
10
+ return "N/A";
11
+ try {
12
+ const date = new Date(dateString);
13
+ return new Intl.DateTimeFormat("en-US", {
14
+ timeZone: timezone,
15
+ year: "numeric",
16
+ month: "short",
17
+ day: "numeric",
18
+ hour: "2-digit",
19
+ minute: "2-digit",
20
+ second: "2-digit",
21
+ hour12: true,
22
+ }).format(date);
23
+ }
24
+ catch {
25
+ return dateString;
26
+ }
27
+ };
28
+ exports.formatDate = formatDate;
29
+ //# sourceMappingURL=timezone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.js","sourceRoot":"","sources":["../../src/services/timezone.ts"],"names":[],"mappings":";;;AAAA,sBAAsB;AACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ,CAAC;AAsBzD,4BAAQ;AApBjB,iCAAiC;AACjC,MAAM,UAAU,GAAG,CAAC,UAAyB,EAAE,EAAE;IAC/C,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC;QAClC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;YACtC,QAAQ,EAAE,QAAQ;YAClB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO;YACd,GAAG,EAAE,SAAS;YACd,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,IAAI;SACb,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,UAAU,CAAC;IACpB,CAAC;AACH,CAAC,CAAC;AAEiB,gCAAU"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveToken = saveToken;
7
+ exports.getToken = getToken;
8
+ exports.clearToken = clearToken;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), ".apihealthz");
13
+ const TOKEN_PATH = path_1.default.join(CONFIG_DIR, "token");
14
+ function saveToken(token) {
15
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
16
+ fs_1.default.writeFileSync(TOKEN_PATH, token, { mode: 0o600 });
17
+ }
18
+ function getToken() {
19
+ if (!fs_1.default.existsSync(TOKEN_PATH))
20
+ return null;
21
+ return fs_1.default.readFileSync(TOKEN_PATH, "utf-8");
22
+ }
23
+ function clearToken() {
24
+ if (fs_1.default.existsSync(TOKEN_PATH))
25
+ fs_1.default.unlinkSync(TOKEN_PATH);
26
+ }
27
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":";;;;;AAOA,8BAGC;AAED,4BAGC;AAED,gCAEC;AAnBD,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,YAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AAC1D,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAElD,SAAgB,SAAS,CAAC,KAAa;IACrC,YAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,YAAE,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,SAAgB,QAAQ;IACtB,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,YAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC;AAED,SAAgB,UAAU;IACxB,IAAI,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC3D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "apihealthz",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "apihealthz": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "setup": "npm run build && npm link"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "type": "commonjs",
17
+ "dependencies": {
18
+ "@inquirer/prompts": "^8.1.0",
19
+ "axios": "^1.13.2",
20
+ "commander": "^14.0.2",
21
+ "open": "^11.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^25.0.3",
25
+ "ts-node": "^10.9.2",
26
+ "typescript": "^5.9.3"
27
+ }
28
+ }
@@ -0,0 +1,136 @@
1
+ import { input, select, confirm } from "@inquirer/prompts";
2
+ import { getToken } from "../utils/config";
3
+ import { createHealthCheck } from "../services/apiHealth";
4
+
5
+ export async function add() {
6
+ const token = getToken();
7
+
8
+ if (!token) {
9
+ console.error("✖ Authentication required. Run `apihealthz login`");
10
+ process.exit(1);
11
+ }
12
+
13
+ try {
14
+ const url = await input({
15
+ message: "API/Site URL to monitor:",
16
+ validate: (v) =>
17
+ v.startsWith("http://") || v.startsWith("https://")
18
+ ? true
19
+ : "Invalid URL",
20
+ });
21
+
22
+ const interval = await select({
23
+ message: "Select check interval:",
24
+ choices: [
25
+ { name: "Every 5 minutes", value: 300 },
26
+ { name: "Every 15 minutes", value: 900 },
27
+ { name: "Every 30 minutes", value: 1800 },
28
+ { name: "Every 1 hour", value: 3600 },
29
+ ],
30
+ });
31
+
32
+ const intervalName =
33
+ interval === 300
34
+ ? "Every 5 minutes"
35
+ : interval === 900
36
+ ? "Every 15 minutes"
37
+ : interval === 1800
38
+ ? "Every 30 minutes"
39
+ : interval === 3600
40
+ ? "Every 1 hour"
41
+ : `${interval} seconds`;
42
+
43
+ const alertType = await select({
44
+ message: "How do you want to receive alerts?",
45
+ choices: [
46
+ { name: "Slack", value: "slack" },
47
+ { name: "Email", value: "email" },
48
+ { name: "WhatsApp", value: "whatsapp" },
49
+ { name: "None", value: null },
50
+ ],
51
+ });
52
+
53
+ let slack_webhook;
54
+ let email;
55
+
56
+ if (alertType === "slack") {
57
+ slack_webhook = await input({
58
+ message: "Slack webhook URL:",
59
+ validate: (v) =>
60
+ v.startsWith("https://hooks.slack.com/")
61
+ ? true
62
+ : "Invalid Slack webhook URL",
63
+ });
64
+ }
65
+
66
+ if (alertType === "email") {
67
+ email = await input({
68
+ message: "Email address:",
69
+ validate: (v) => (v.includes("@") ? true : "Invalid email address"),
70
+ });
71
+ }
72
+
73
+ // Show a preview summary before confirmation
74
+ console.log("\n=== Health Check Preview ===");
75
+ console.log(`URL: ${url}`);
76
+ console.log(`Interval: ${intervalName}`);
77
+ console.log(
78
+ `Alert: ${
79
+ alertType === null
80
+ ? "None"
81
+ : alertType === "slack"
82
+ ? "Slack"
83
+ : alertType === "email"
84
+ ? "Email"
85
+ : "N/A"
86
+ }`
87
+ );
88
+ if (slack_webhook) console.log(`Slack Webhook: ${slack_webhook}`);
89
+ if (email) console.log(`Email: ${email}`);
90
+ console.log("===========================\n");
91
+
92
+ const proceed = await confirm({
93
+ message: "Create this health check?",
94
+ default: true,
95
+ });
96
+
97
+ if (!proceed) {
98
+ console.log("Cancelled");
99
+ process.exit(0);
100
+ }
101
+
102
+ const payload = {
103
+ url,
104
+ interval_sec: interval,
105
+ slack_webhook,
106
+ email,
107
+ };
108
+
109
+ if (slack_webhook) payload.slack_webhook = slack_webhook;
110
+ if (email) payload.email = email;
111
+
112
+ try {
113
+ process.stdout.write("⠋ Creating health check...\r");
114
+ const response = await createHealthCheck({
115
+ url,
116
+ interval_sec: interval,
117
+ slack_webhook,
118
+ email,
119
+ });
120
+ console.log(`✔ Health check created successfully: ${response.id}`);
121
+ } catch (err) {
122
+ console.error(
123
+ `✖ ${err instanceof Error ? err.message : "Unknown error"}`
124
+ );
125
+ process.exit(1);
126
+ }
127
+ } catch (err: any) {
128
+ // Handle user cancellation (Ctrl+C)
129
+ if (err?.name === "ExitPromptError" || err?.message?.includes("SIGINT")) {
130
+ console.log("\nCancelled");
131
+ process.exit(0);
132
+ }
133
+ // Re-throw other errors
134
+ throw err;
135
+ }
136
+ }
@@ -0,0 +1,28 @@
1
+ import { Command } from "commander";
2
+ import { authStatus, loginWithProvider, logout } from "../services/auth";
3
+
4
+ export const authCommand = new Command("auth").description(
5
+ "Authentication commands"
6
+ );
7
+
8
+ authCommand
9
+ .command("login")
10
+ .description("Login to ApiHealthz")
11
+ .option("-p, --provider <provider>", "google")
12
+ .action(async (options) => {
13
+ await loginWithProvider(options.provider);
14
+ });
15
+
16
+ authCommand
17
+ .command("logout")
18
+ .description("Logout from ApiHealthz")
19
+ .action(async () => {
20
+ await logout();
21
+ });
22
+
23
+ authCommand
24
+ .command("status")
25
+ .description("Show auth status")
26
+ .action(async () => {
27
+ await authStatus();
28
+ });
@@ -0,0 +1,47 @@
1
+ import { confirm, input } from "@inquirer/prompts";
2
+ import { getToken } from "../utils/config";
3
+ import { api } from "../services/api";
4
+
5
+ export async function del() {
6
+ const token = getToken();
7
+
8
+ if (!token) {
9
+ console.error("✖ Authentication required. Run `apihealthz login`");
10
+ process.exit(1);
11
+ }
12
+
13
+ try {
14
+ const id = await input({
15
+ message: "Enter the ID of the health check to delete:",
16
+ });
17
+
18
+ const proceed = await confirm({
19
+ message: `Delete check ${id}?`,
20
+ default: true,
21
+ });
22
+
23
+ if (!proceed) {
24
+ console.log("Cancelled");
25
+ process.exit(0);
26
+ }
27
+
28
+ try {
29
+ await api.delete(`/checks/${id}`);
30
+ console.log("✔ Health check deleted successfully");
31
+ process.exit(0);
32
+ } catch (err) {
33
+ console.error(
34
+ `✖ ${err instanceof Error ? err.message : "Unknown error"}`
35
+ );
36
+ process.exit(1);
37
+ }
38
+ } catch (err: any) {
39
+ // Handle user cancellation (Ctrl+C)
40
+ if (err?.name === "ExitPromptError" || err?.message?.includes("SIGINT")) {
41
+ console.log("\nCancelled");
42
+ process.exit(0);
43
+ }
44
+ // Re-throw other errors
45
+ throw err;
46
+ }
47
+ }
@@ -0,0 +1,37 @@
1
+ import { api } from "../services/api";
2
+ import { formatDate } from "../services/timezone";
3
+ import { getToken } from "../utils/config";
4
+
5
+ export async function list() {
6
+ const token = getToken();
7
+
8
+ if (!token) {
9
+ console.error("✖ Authentication required. Run `apihealthz login`");
10
+ process.exit(1);
11
+ }
12
+
13
+ try {
14
+ const response = await api.get("/checks");
15
+ const data = response.data;
16
+
17
+ if (!data || data.length === 0) {
18
+ console.log("✖ No health checks found, add one with: `apihealthz add`");
19
+ process.exit(0);
20
+ }
21
+
22
+ console.table(
23
+ data.map((c: any) => ({
24
+ ID: c.id,
25
+ URL: c.url,
26
+ Interval: `${c.interval_sec} seconds`,
27
+ "Last Status": c.last_status ? "✅ UP" : "❌ DOWN",
28
+ "Last Checked": c.last_checked_at
29
+ ? formatDate(c.last_checked_at)
30
+ : "N/A",
31
+ }))
32
+ );
33
+ } catch (err) {
34
+ console.error(`✖ ${err instanceof Error ? err.message : "Unknown error"}`);
35
+ process.exit(1);
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { authCommand } from "./commands/auth";
5
+ import { add } from "./commands/add";
6
+ import { list } from "./commands/list";
7
+ import { del } from "./commands/delete";
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name("apihealthz")
13
+ .description("API uptime & health monitoring CLI")
14
+ .version("1.0.0");
15
+
16
+ program.addCommand(authCommand);
17
+ program.command("add").description("Add a new API health check").action(add);
18
+ program.command("list").description("List all API health checks").action(list);
19
+ program.command("delete").description("Delete a health check").action(del);
20
+
21
+ program.parse(process.argv);
@@ -0,0 +1,15 @@
1
+ import axios from "axios";
2
+ import { getToken } from "../utils/config";
3
+
4
+ export const api = axios.create({
5
+ baseURL: "https://be.apihealthz.com",
6
+ timeout: 10000,
7
+ });
8
+
9
+ api.interceptors.request.use((config) => {
10
+ const token = getToken();
11
+ if (token) {
12
+ config.headers.Authorization = `Bearer ${token}`;
13
+ }
14
+ return config;
15
+ });
@@ -0,0 +1,25 @@
1
+ import { clearToken, getToken } from "../utils/config";
2
+ import { api } from "./api";
3
+
4
+ export async function createHealthCheck(payload: {
5
+ url: string;
6
+ interval_sec: number;
7
+ slack_webhook?: string;
8
+ email?: string;
9
+ }) {
10
+ const token = getToken();
11
+ if (!token) {
12
+ console.error("✖ Authentication required. Run `apihealthz login`");
13
+ process.exit(1);
14
+ }
15
+
16
+ const body = {
17
+ url: payload.url,
18
+ interval_sec: payload.interval_sec,
19
+ slack_webhook: payload.slack_webhook,
20
+ email: payload.email,
21
+ };
22
+
23
+ const response = await api.post("/checks", body);
24
+ return response.data;
25
+ }
@@ -0,0 +1,121 @@
1
+ import open from "open";
2
+ import axios from "axios";
3
+ import { api } from "./api";
4
+ import { saveToken, clearToken, getToken } from "../utils/config";
5
+
6
+ interface DeviceStartResponse {
7
+ device_code: string;
8
+ user_code: string;
9
+ verification_url: string;
10
+ expires_in: number;
11
+ poll_interval: number;
12
+ }
13
+
14
+ interface DevicePollResponse {
15
+ status: "approved" | "expired" | "pending";
16
+ token?: string;
17
+ poll_interval?: number;
18
+ }
19
+
20
+ export async function loginWithProvider(provider: string) {
21
+ if (provider !== "google") {
22
+ console.log("Choose provider: google");
23
+ process.exit(1);
24
+ }
25
+
26
+ const { data } = await api.post<DeviceStartResponse>("/auth/device/start", {
27
+ provider,
28
+ });
29
+
30
+ console.log("\nOpen this URL in your browser:");
31
+ console.log(data.verification_url);
32
+ console.log("\nEnter code:", data.user_code);
33
+
34
+ await open(data.verification_url);
35
+
36
+ await pollForToken(data.device_code);
37
+ }
38
+
39
+ async function pollForToken(deviceCode: string) {
40
+ process.stdout.write("\nWaiting for authorization");
41
+
42
+ let pollInterval = 3000; // Default 3 seconds
43
+
44
+ while (true) {
45
+ await new Promise((r) => setTimeout(r, pollInterval));
46
+ process.stdout.write(".");
47
+
48
+ let data;
49
+ try {
50
+ const response = await api.post<DevicePollResponse>("/auth/device/poll", {
51
+ device_code: deviceCode,
52
+ });
53
+ data = response.data;
54
+
55
+ // Update poll interval if provided by server
56
+ if (data.poll_interval) {
57
+ pollInterval = data.poll_interval * 1000; // Convert seconds to milliseconds
58
+ }
59
+ } catch (error) {
60
+ console.log("\n✖ Error polling for authorization");
61
+
62
+ if (axios.isAxiosError(error)) {
63
+ if (error.response) {
64
+ // Server responded with error status
65
+ console.log(` Status: ${error.response.status}`);
66
+ console.log(` Message: ${error.response.statusText}`);
67
+ if (error.response.data) {
68
+ console.log(` Details:`, error.response.data);
69
+ }
70
+ } else if (error.request) {
71
+ // Request was made but no response received
72
+ console.log(` No response received from server`);
73
+ } else {
74
+ // Error setting up the request
75
+ console.log(` Error: ${error.message}`);
76
+ }
77
+ } else {
78
+ // Unknown error type
79
+ console.log(` Error:`, error);
80
+ }
81
+
82
+ process.exit(1);
83
+ }
84
+
85
+ if (!data) {
86
+ console.log("\n✖ Error: No data received");
87
+ process.exit(1);
88
+ }
89
+
90
+ if (data.status === "approved") {
91
+ if (!data.token) {
92
+ console.log("\n✖ Error: No token received");
93
+ process.exit(1);
94
+ }
95
+ saveToken(data.token);
96
+ console.log("\n✔ Logged in successfully");
97
+ return;
98
+ }
99
+
100
+ if (data.status === "expired") {
101
+ console.log("\n✖ Login expired");
102
+ process.exit(1);
103
+ }
104
+ }
105
+ }
106
+
107
+ export async function logout() {
108
+ clearToken();
109
+ console.log("✔ Logged out");
110
+ }
111
+
112
+ export async function authStatus() {
113
+ const token = getToken();
114
+ if (!token) {
115
+ console.log("Not logged in");
116
+ return;
117
+ }
118
+
119
+ const { data } = await api.get("/auth/me");
120
+ console.log(`✔ Logged in as ${data?.email}`);
121
+ }
@@ -0,0 +1,24 @@
1
+ // Get user's timezone
2
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
3
+
4
+ // Format date in user's timezone
5
+ const formatDate = (dateString: string | null) => {
6
+ if (!dateString) return "N/A";
7
+ try {
8
+ const date = new Date(dateString);
9
+ return new Intl.DateTimeFormat("en-US", {
10
+ timeZone: timezone,
11
+ year: "numeric",
12
+ month: "short",
13
+ day: "numeric",
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ second: "2-digit",
17
+ hour12: true,
18
+ }).format(date);
19
+ } catch {
20
+ return dateString;
21
+ }
22
+ };
23
+
24
+ export { timezone, formatDate };