@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.
- package/bin/unit-labs.js +10 -0
- package/package.json +16 -0
- package/src/config.js +63 -0
- package/src/index.js +267 -0
package/bin/unit-labs.js
ADDED
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
|
+
|