@yokio42/unit-labs-cli 0.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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require("../src/index")
4
+
5
+ run(process.argv.slice(2)).catch((error) => {
6
+ const message = error?.message || String(error)
7
+ console.error(`Error: ${message}`)
8
+ process.exit(1)
9
+ })
10
+
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@yokio42/unit-labs-cli",
3
+ "version": "0.1.0",
4
+ "description": "Unit Labs command line interface",
5
+ "bin": {
6
+ "unit-labs": "./bin/unit-labs.js"
7
+ },
8
+ "type": "commonjs",
9
+ "scripts": {
10
+ "start": "node ./bin/unit-labs.js",
11
+ "check": "node --check ./src/index.js && node --check ./src/config.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ }
16
+ }
package/src/config.js ADDED
@@ -0,0 +1,63 @@
1
+ const fs = require("fs")
2
+ const os = require("os")
3
+ const path = require("path")
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), ".unit-labs")
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
7
+ const DEFAULT_API_BASE = "https://popuitka2-be.onrender.com"
8
+
9
+ function normalizeApiBase(value) {
10
+ if (!value || typeof value !== "string") {
11
+ return DEFAULT_API_BASE
12
+ }
13
+
14
+ return value.trim().replace(/\/+$/, "")
15
+ }
16
+
17
+ function ensureConfigDir() {
18
+ if (!fs.existsSync(CONFIG_DIR)) {
19
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
20
+ }
21
+ }
22
+
23
+ function readConfig() {
24
+ ensureConfigDir()
25
+
26
+ if (!fs.existsSync(CONFIG_FILE)) {
27
+ return {
28
+ apiBase: DEFAULT_API_BASE,
29
+ token: null,
30
+ }
31
+ }
32
+
33
+ try {
34
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8")
35
+ const parsed = JSON.parse(raw)
36
+ return {
37
+ apiBase: normalizeApiBase(parsed.apiBase),
38
+ token: parsed.token || null,
39
+ }
40
+ } catch (error) {
41
+ return {
42
+ apiBase: DEFAULT_API_BASE,
43
+ token: null,
44
+ }
45
+ }
46
+ }
47
+
48
+ function writeConfig(config) {
49
+ ensureConfigDir()
50
+ const normalized = {
51
+ apiBase: normalizeApiBase(config.apiBase),
52
+ token: config.token || null,
53
+ }
54
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(normalized, null, 2), "utf-8")
55
+ }
56
+
57
+ module.exports = {
58
+ CONFIG_FILE,
59
+ DEFAULT_API_BASE,
60
+ normalizeApiBase,
61
+ readConfig,
62
+ writeConfig,
63
+ }
package/src/index.js ADDED
@@ -0,0 +1,267 @@
1
+ const readline = require("readline/promises")
2
+ const { stdin: input, stdout: output } = require("process")
3
+ const { CONFIG_FILE, DEFAULT_API_BASE, normalizeApiBase, readConfig, writeConfig } = require("./config")
4
+
5
+ const UNIT_LABS_BANNER = String.raw`
6
+ _ _ _ _ ___ _____ _ _ ____ ____
7
+ | | | | \ | |_ _|_ _| | | / \ | __ )/ ___|
8
+ | | | | \| || | | | | | / _ \ | _ \\___ \
9
+ | |_| | |\ || | | | | |___ / ___ \| |_) |___) |
10
+ \___/|_| \_|___| |_| |_____/_/ \_\____/|____/
11
+ `
12
+
13
+ const HELP_TEXT = `
14
+ Unit Labs CLI
15
+
16
+ Usage:
17
+ unit-labs auth login [--login <email>] [--password <password>] [--api <url>]
18
+ unit-labs auth logout
19
+ unit-labs auth whoami
20
+ unit-labs config show
21
+ unit-labs config set-api <url>
22
+ unit-labs --help
23
+
24
+ Options:
25
+ -l, --login Login (email) for login
26
+ -e, --email Alias for --login
27
+ -p, --password Password for login
28
+ --api API base URL (default: ${DEFAULT_API_BASE})
29
+ -h, --help Show help
30
+ `
31
+
32
+ function parseFlags(argv) {
33
+ const flags = {}
34
+
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const key = argv[i]
37
+ const value = argv[i + 1]
38
+
39
+ if (key === "-l" || key === "--login") {
40
+ flags.login = value
41
+ i += 1
42
+ continue
43
+ }
44
+ if (key === "-e" || key === "--email") {
45
+ flags.login = value
46
+ i += 1
47
+ continue
48
+ }
49
+ if (key === "-p" || key === "--password") {
50
+ flags.password = value
51
+ i += 1
52
+ continue
53
+ }
54
+ if (key === "--api") {
55
+ flags.apiBase = value
56
+ i += 1
57
+ continue
58
+ }
59
+ }
60
+
61
+ return flags
62
+ }
63
+
64
+ async function promptMissingCredentials({ login, password }) {
65
+ if (login && password) {
66
+ return { login, password }
67
+ }
68
+
69
+ const rl = readline.createInterface({ input, output })
70
+ const nextLogin = login || (await rl.question("Login: ")).trim()
71
+ const nextPassword = password || (await rl.question("Password: ")).trim()
72
+ rl.close()
73
+
74
+ return {
75
+ login: nextLogin,
76
+ password: nextPassword,
77
+ }
78
+ }
79
+
80
+ function printWelcomeBanner() {
81
+ console.log("Welcome to")
82
+ console.log(UNIT_LABS_BANNER)
83
+ }
84
+
85
+ async function request({ method, url, token, body }) {
86
+ const headers = {}
87
+
88
+ if (token) {
89
+ headers.Authorization = `Bearer ${token}`
90
+ }
91
+
92
+ if (body) {
93
+ headers["Content-Type"] = "application/json"
94
+ }
95
+
96
+ const response = await fetch(url, {
97
+ method,
98
+ headers,
99
+ body: body ? JSON.stringify(body) : undefined,
100
+ })
101
+
102
+ const text = await response.text()
103
+ let payload = text
104
+
105
+ try {
106
+ payload = text ? JSON.parse(text) : null
107
+ } catch (error) {
108
+ payload = text
109
+ }
110
+
111
+ if (!response.ok) {
112
+ const message = typeof payload === "string"
113
+ ? payload
114
+ : payload?.message || JSON.stringify(payload)
115
+ throw new Error(`${response.status} ${message}`)
116
+ }
117
+
118
+ return payload
119
+ }
120
+
121
+ async function authLogin(flags) {
122
+ const config = readConfig()
123
+ const apiBase = normalizeApiBase(flags.apiBase || config.apiBase || DEFAULT_API_BASE)
124
+ const credentials = await promptMissingCredentials(flags)
125
+
126
+ if (!credentials.login || !credentials.password) {
127
+ throw new Error("login and password are required")
128
+ }
129
+
130
+ const signinPayload = await request({
131
+ method: "POST",
132
+ url: `${apiBase}/signin`,
133
+ body: {
134
+ email: credentials.login,
135
+ password: credentials.password,
136
+ },
137
+ })
138
+
139
+ const token = signinPayload?.token
140
+ if (!token) {
141
+ throw new Error("signin response does not contain token")
142
+ }
143
+
144
+ writeConfig({
145
+ apiBase,
146
+ token,
147
+ })
148
+
149
+ let me
150
+ try {
151
+ me = await request({
152
+ method: "GET",
153
+ url: `${apiBase}/me`,
154
+ token,
155
+ })
156
+ } catch (error) {
157
+ me = null
158
+ }
159
+
160
+ printWelcomeBanner()
161
+
162
+ if (me?.email) {
163
+ console.log(`Logged in as ${me.email}`)
164
+ } else {
165
+ console.log("Logged in")
166
+ }
167
+ console.log(`Config saved: ${CONFIG_FILE}`)
168
+ }
169
+
170
+ function authLogout() {
171
+ const config = readConfig()
172
+ writeConfig({
173
+ apiBase: normalizeApiBase(config.apiBase || DEFAULT_API_BASE),
174
+ token: null,
175
+ })
176
+ console.log("Logged out")
177
+ }
178
+
179
+ async function authWhoAmI() {
180
+ const config = readConfig()
181
+ const token = config.token
182
+
183
+ if (!token) {
184
+ throw new Error("not logged in. run: unit-labs auth login")
185
+ }
186
+
187
+ const me = await request({
188
+ method: "GET",
189
+ url: `${normalizeApiBase(config.apiBase || DEFAULT_API_BASE)}/me`,
190
+ token,
191
+ })
192
+
193
+ console.log(JSON.stringify(me, null, 2))
194
+ }
195
+
196
+ function configShow() {
197
+ const config = readConfig()
198
+ console.log(JSON.stringify({
199
+ apiBase: normalizeApiBase(config.apiBase || DEFAULT_API_BASE),
200
+ loggedIn: Boolean(config.token),
201
+ }, null, 2))
202
+ }
203
+
204
+ function configSetApi(rawApi) {
205
+ if (!rawApi) {
206
+ throw new Error("api url is required. usage: unit-labs config set-api <url>")
207
+ }
208
+
209
+ const config = readConfig()
210
+ const apiBase = normalizeApiBase(rawApi)
211
+ writeConfig({
212
+ apiBase,
213
+ token: config.token || null,
214
+ })
215
+ console.log(`API saved: ${apiBase}`)
216
+ }
217
+
218
+ async function run(argv) {
219
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
220
+ console.log(HELP_TEXT.trim())
221
+ return
222
+ }
223
+
224
+ const [scope, command, ...rest] = argv
225
+
226
+ if (scope === "auth") {
227
+ const flags = parseFlags(rest)
228
+
229
+ if (command === "login") {
230
+ await authLogin(flags)
231
+ return
232
+ }
233
+
234
+ if (command === "logout") {
235
+ authLogout()
236
+ return
237
+ }
238
+
239
+ if (command === "whoami") {
240
+ await authWhoAmI()
241
+ return
242
+ }
243
+
244
+ throw new Error(`unknown auth command: ${command}`)
245
+ }
246
+
247
+ if (scope === "config") {
248
+ if (command === "show") {
249
+ configShow()
250
+ return
251
+ }
252
+
253
+ if (command === "set-api") {
254
+ configSetApi(rest[0])
255
+ return
256
+ }
257
+
258
+ throw new Error(`unknown config command: ${command}`)
259
+ }
260
+
261
+ throw new Error(`unknown scope: ${scope}`)
262
+ }
263
+
264
+ module.exports = {
265
+ run,
266
+ }
267
+