@tenyokjs/devdevian-cli 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +674 -0
- package/README.md +51 -2
- package/bin/devdevian.mjs +525 -0
- package/lib/client.mjs +140 -0
- package/package.json +40 -4
package/lib/client.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFile,
|
|
3
|
+
writeFile,
|
|
4
|
+
mkdir,
|
|
5
|
+
chmod,
|
|
6
|
+
rename,
|
|
7
|
+
unlink,
|
|
8
|
+
lstat,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
export function originOf(value) {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
if (
|
|
16
|
+
url.username ||
|
|
17
|
+
url.password ||
|
|
18
|
+
url.search ||
|
|
19
|
+
url.hash ||
|
|
20
|
+
url.pathname !== "/" ||
|
|
21
|
+
!(
|
|
22
|
+
url.protocol === "https:" ||
|
|
23
|
+
(url.protocol === "http:" &&
|
|
24
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
throw Error(
|
|
28
|
+
"Use an HTTPS server origin, or HTTP on loopback for local development.",
|
|
29
|
+
);
|
|
30
|
+
return url.origin;
|
|
31
|
+
}
|
|
32
|
+
export const clean = (value) =>
|
|
33
|
+
String(value ?? "")
|
|
34
|
+
.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, "")
|
|
35
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
36
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
37
|
+
export function configFile(env = process.env) {
|
|
38
|
+
return join(
|
|
39
|
+
env.DEVDEVIAN_CONFIG_DIR || join(homedir(), ".config", "devdevian"),
|
|
40
|
+
"credentials.json",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
export async function loadConfig() {
|
|
44
|
+
try {
|
|
45
|
+
const path = configFile(),
|
|
46
|
+
info = await lstat(path);
|
|
47
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
48
|
+
throw Error("The credentials path must be a regular file.");
|
|
49
|
+
if (process.platform !== "win32" && info.mode & 0o077)
|
|
50
|
+
throw Error(
|
|
51
|
+
"Credentials permissions are too broad. Run chmod 600 on " + path,
|
|
52
|
+
);
|
|
53
|
+
const config = JSON.parse(await readFile(path, "utf8"));
|
|
54
|
+
config.origin = originOf(config.origin);
|
|
55
|
+
if (!/^ddv_[A-Za-z0-9_-]{43}$/.test(config.token || ""))
|
|
56
|
+
throw Error("Invalid saved CLI credentials. Sign in again.");
|
|
57
|
+
return config;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e.code === "ENOENT") return null;
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function saveConfig(config) {
|
|
64
|
+
const file = configFile(),
|
|
65
|
+
dir = join(file, "..");
|
|
66
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
67
|
+
const info = await lstat(dir);
|
|
68
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
69
|
+
throw Error("The credentials directory must be a real directory.");
|
|
70
|
+
const temp = join(dir, ".credentials-" + randomBytes(8).toString("hex"));
|
|
71
|
+
try {
|
|
72
|
+
await writeFile(temp, JSON.stringify(config) + "\n", {
|
|
73
|
+
mode: 0o600,
|
|
74
|
+
flag: "wx",
|
|
75
|
+
});
|
|
76
|
+
await chmod(temp, 0o600);
|
|
77
|
+
await rename(temp, file);
|
|
78
|
+
} finally {
|
|
79
|
+
await unlink(temp).catch((e) => {
|
|
80
|
+
if (e.code !== "ENOENT") throw e;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function removeConfig() {
|
|
85
|
+
try {
|
|
86
|
+
await unlink(configFile());
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (e.code !== "ENOENT") throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function client(origin, token = null) {
|
|
92
|
+
origin = originOf(origin);
|
|
93
|
+
return async (path, { method = "GET", body } = {}) => {
|
|
94
|
+
if (!path.startsWith("/") || path.startsWith("//"))
|
|
95
|
+
throw Error("Invalid API path.");
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetch(origin + "/api" + path, {
|
|
99
|
+
method,
|
|
100
|
+
redirect: "error",
|
|
101
|
+
headers: {
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
104
|
+
...(token ? { Authorization: "Bearer " + token } : {}),
|
|
105
|
+
},
|
|
106
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
107
|
+
signal: AbortSignal.timeout(20000),
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
throw Error(
|
|
111
|
+
"Could not reach Devdevian. Check your connection and server address.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let size = 0;
|
|
116
|
+
for await (const chunk of response.body) {
|
|
117
|
+
size += chunk.length;
|
|
118
|
+
if (size > 2 * 1024 * 1024)
|
|
119
|
+
throw Error("Devdevian response is too large. Use a smaller page.");
|
|
120
|
+
chunks.push(chunk);
|
|
121
|
+
}
|
|
122
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
123
|
+
let data;
|
|
124
|
+
try {
|
|
125
|
+
data = JSON.parse(text);
|
|
126
|
+
} catch {
|
|
127
|
+
throw Error(
|
|
128
|
+
`Devdevian returned an unexpected response (HTTP ${response.status}). Try again later.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const error = Error(
|
|
133
|
+
clean(data.error || `Request failed (HTTP ${response.status}).`),
|
|
134
|
+
);
|
|
135
|
+
error.status = response.status;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return data;
|
|
139
|
+
};
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenyokjs/devdevian-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Devdevian in your terminal: discussions, questions and human code reviews.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"devdevian": "bin/devdevian.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=24"
|
|
17
|
+
},
|
|
18
|
+
"license": "GPL-3.0-only",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Tenyokj/devdevian.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://devdevian.vercel.app",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Tenyokj/devdevian/issues"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"devdevian",
|
|
34
|
+
"cli",
|
|
35
|
+
"forum",
|
|
36
|
+
"code-review",
|
|
37
|
+
"developers"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test/*.test.mjs"
|
|
41
|
+
}
|
|
42
|
+
}
|