@holin-work/holin-cli 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 (2) hide show
  1. package/bin/holin-cli.js +160 -0
  2. package/package.json +16 -0
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * holin-cli — Accio cli-login authorization tool
4
+ *
5
+ * Usage (invoked by Accio platform):
6
+ * HOLIN_API_KEY=holin_xxx holin-cli auth login
7
+ *
8
+ * On success: writes credentials to ~/.config/holin-auth/credentials.json, exits 0
9
+ * On failure: prints error to stderr, exits 1
10
+ */
11
+
12
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { join } from "path";
15
+
16
+ // ── Constants ────────────────────────────────────────────────────────────────
17
+
18
+ const MCP_URL = "https://api.holin.work/icbu/mcp";
19
+
20
+ const CREDENTIALS_FILE = join(
21
+ process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
22
+ "holin-auth",
23
+ "credentials.json"
24
+ );
25
+
26
+ // ── Helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ function writeCredentials(data) {
29
+ const dir = join(CREDENTIALS_FILE, "..");
30
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), "utf-8");
32
+ }
33
+
34
+ function readCredentials() {
35
+ try {
36
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Verify API key by calling holin_ping via MCP.
44
+ * Returns parsed ping result or throws on failure.
45
+ */
46
+ async function pingVerify(apiKey) {
47
+ const res = await fetch(MCP_URL, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "Accept": "application/json, text/event-stream",
52
+ "X-API-Key": apiKey,
53
+ },
54
+ body: JSON.stringify({
55
+ jsonrpc: "2.0",
56
+ id: 1,
57
+ method: "tools/call",
58
+ params: { name: "holin_ping", arguments: {} },
59
+ }),
60
+ signal: AbortSignal.timeout(10000),
61
+ });
62
+
63
+ if (!res.ok) {
64
+ throw new Error(`MCP server responded ${res.status}`);
65
+ }
66
+
67
+ const raw = await res.text();
68
+ // Parse SSE: find "data: {...}" line
69
+ for (const line of raw.split("\n")) {
70
+ const trimmed = line.trim();
71
+ if (trimmed.startsWith("data:")) {
72
+ const envelope = JSON.parse(trimmed.slice(5).trim());
73
+ const text = envelope?.result?.content?.[0]?.text;
74
+ if (text) return JSON.parse(text);
75
+ }
76
+ }
77
+ throw new Error("Unexpected response format from MCP server");
78
+ }
79
+
80
+ // ── Commands ─────────────────────────────────────────────────────────────────
81
+
82
+ async function cmdAuthLogin() {
83
+ const apiKey = process.env.HOLIN_API_KEY?.trim();
84
+ if (!apiKey) {
85
+ console.error("[holin-cli] Error: HOLIN_API_KEY environment variable is not set.");
86
+ process.exit(1);
87
+ }
88
+ if (!apiKey.startsWith("holin_")) {
89
+ console.error("[holin-cli] Error: Invalid API Key format (must start with 'holin_').");
90
+ process.exit(1);
91
+ }
92
+
93
+ console.log("[holin-cli] Verifying API Key...");
94
+ let result;
95
+ try {
96
+ result = await pingVerify(apiKey);
97
+ } catch (err) {
98
+ console.error(`[holin-cli] Error: Failed to reach Holin service — ${err.message}`);
99
+ process.exit(1);
100
+ }
101
+
102
+ if (!result?.authorized) {
103
+ console.error("[holin-cli] Error: API Key is invalid or unauthorized.");
104
+ process.exit(1);
105
+ }
106
+
107
+ const creds = {
108
+ api_key: apiKey,
109
+ customer_id: result.customer_id || "",
110
+ customer_name: result.customer_name || "",
111
+ plan: result.plan || "",
112
+ authorized_at: new Date().toISOString(),
113
+ };
114
+
115
+ writeCredentials(creds);
116
+ console.log(
117
+ `[holin-cli] Connected: ${creds.customer_name || creds.customer_id} (${creds.plan})`
118
+ );
119
+ }
120
+
121
+ function cmdAuthLogout() {
122
+ if (existsSync(CREDENTIALS_FILE)) {
123
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify({}, null, 2), "utf-8");
124
+ console.log("[holin-cli] Disconnected: credentials cleared.");
125
+ } else {
126
+ console.log("[holin-cli] No credentials found.");
127
+ }
128
+ }
129
+
130
+ function cmdAuthStatus() {
131
+ const creds = readCredentials();
132
+ if (!creds?.api_key) {
133
+ console.log("[holin-cli] Status: not connected.");
134
+ process.exit(1);
135
+ }
136
+ console.log(
137
+ `[holin-cli] Status: connected as ${creds.customer_name || creds.customer_id} (${creds.plan})`
138
+ );
139
+ }
140
+
141
+ // ── Entrypoint ────────────────────────────────────────────────────────────────
142
+
143
+ const [, , cmd, sub] = process.argv;
144
+
145
+ if (cmd === "auth" && sub === "login") {
146
+ cmdAuthLogin().catch((err) => {
147
+ console.error(`[holin-cli] Unexpected error: ${err.message}`);
148
+ process.exit(1);
149
+ });
150
+ } else if (cmd === "auth" && sub === "logout") {
151
+ cmdAuthLogout();
152
+ } else if (cmd === "auth" && sub === "status") {
153
+ cmdAuthStatus();
154
+ } else {
155
+ console.log("Usage:");
156
+ console.log(" holin-cli auth login — authorize with HOLIN_API_KEY env var");
157
+ console.log(" holin-cli auth logout — clear stored credentials");
158
+ console.log(" holin-cli auth status — check current auth status");
159
+ process.exit(1);
160
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@holin-work/holin-cli",
3
+ "version": "1.0.0",
4
+ "description": "Holin CLI — Accio plugin authorization tool for ICBU batch listing",
5
+ "type": "module",
6
+ "bin": {
7
+ "holin-cli": "bin/holin-cli.js"
8
+ },
9
+ "files": [
10
+ "bin/"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "MIT"
16
+ }