neoctl-web 0.1.0 → 0.1.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/README.md +238 -238
- package/bin/neow.mjs +139 -123
- package/core-runtime.mjs +55 -55
- package/cpa-quota.mjs +209 -209
- package/dist/assets/{index-H7num-0s.js → index-B6NPbDJ9.js} +1 -1
- package/dist/assets/{index-BSFq6wfd.css → index-DG8NYKAP.css} +1 -1
- package/dist/favicon.svg +3 -3
- package/dist/index.html +2 -2
- package/memory-monitor.mjs +150 -150
- package/package.json +64 -64
- package/plugin-settings.mjs +67 -67
- package/plugins/downloads/downloads.mjs +147 -147
- package/plugins/downloads/index.mjs +19 -19
- package/plugins/downloads/neo-plugin.json +9 -9
- package/plugins/xhs-artifact/artifacts.mjs +388 -388
- package/plugins/xhs-artifact/editor-page.mjs +73 -73
- package/plugins/xhs-artifact/index.mjs +48 -48
- package/plugins/xhs-artifact/neo-plugin.json +9 -9
- package/plugins.mjs +111 -111
- package/runtime-router-cleanup.mjs +152 -152
- package/runtime-workspaces.mjs +515 -515
- package/server.mjs +450 -450
- package/tool-settings.mjs +80 -80
package/cpa-quota.mjs
CHANGED
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
import fsp from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_REFRESH_MS = 60_000;
|
|
5
|
-
const DEFAULT_TIMEOUT_MS = 12_000;
|
|
6
|
-
const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
7
|
-
const CODEX_USER_AGENT = 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal';
|
|
8
|
-
|
|
9
|
-
export function createCpaQuotaMonitor({
|
|
10
|
-
configFile,
|
|
11
|
-
refreshMs = DEFAULT_REFRESH_MS,
|
|
12
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
13
|
-
fetchImpl = fetch,
|
|
14
|
-
} = {}) {
|
|
15
|
-
let config = { url: '', password: '' };
|
|
16
|
-
let quotas = [];
|
|
17
|
-
let timer;
|
|
18
|
-
let refreshPromise;
|
|
19
|
-
|
|
20
|
-
async function start() {
|
|
21
|
-
config = await readConfig(configFile);
|
|
22
|
-
await refresh();
|
|
23
|
-
timer = setInterval(() => { void refresh(); }, refreshMs);
|
|
24
|
-
timer.unref?.();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stop() {
|
|
28
|
-
if (timer) clearInterval(timer);
|
|
29
|
-
timer = undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getPublicState() {
|
|
33
|
-
return {
|
|
34
|
-
config: {
|
|
35
|
-
url: config.url,
|
|
36
|
-
hasPassword: Boolean(config.password),
|
|
37
|
-
},
|
|
38
|
-
quotas,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function updateConfig(value) {
|
|
43
|
-
const next = normalizeConfig(value);
|
|
44
|
-
config = {
|
|
45
|
-
...next,
|
|
46
|
-
password: value?.preservePassword && config.password ? config.password : next.password,
|
|
47
|
-
};
|
|
48
|
-
await writeConfig(configFile, config);
|
|
49
|
-
await refresh();
|
|
50
|
-
return getPublicState();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function refresh() {
|
|
54
|
-
if (refreshPromise) return refreshPromise;
|
|
55
|
-
refreshPromise = (async () => {
|
|
56
|
-
try {
|
|
57
|
-
quotas = await fetchCpaQuotas(config, { fetchImpl, timeoutMs });
|
|
58
|
-
} catch {
|
|
59
|
-
quotas = [];
|
|
60
|
-
} finally {
|
|
61
|
-
refreshPromise = undefined;
|
|
62
|
-
}
|
|
63
|
-
return quotas;
|
|
64
|
-
})();
|
|
65
|
-
return refreshPromise;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { start, stop, refresh, updateConfig, getPublicState };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function fetchCpaQuotas(config, { fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
72
|
-
const normalized = normalizeConfig(config);
|
|
73
|
-
if (!normalized.url || !normalized.password) return [];
|
|
74
|
-
const managementBase = managementBaseUrl(normalized.url);
|
|
75
|
-
const headers = { Authorization: `Bearer ${normalized.password}` };
|
|
76
|
-
const authFiles = await fetchJson(`${managementBase}/auth-files`, { headers }, { fetchImpl, timeoutMs });
|
|
77
|
-
const credentials = selectCodexCredentials(authFiles?.files);
|
|
78
|
-
const quotas = [];
|
|
79
|
-
for (const credential of credentials) {
|
|
80
|
-
try {
|
|
81
|
-
const accountId = credential?.id_token?.chatgpt_account_id;
|
|
82
|
-
const apiCall = await fetchJson(`${managementBase}/api-call`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
authIndex: credential.auth_index,
|
|
87
|
-
method: 'GET',
|
|
88
|
-
url: CODEX_USAGE_URL,
|
|
89
|
-
header: {
|
|
90
|
-
Authorization: 'Bearer $TOKEN$',
|
|
91
|
-
'Content-Type': 'application/json',
|
|
92
|
-
'User-Agent': CODEX_USER_AGENT,
|
|
93
|
-
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
}, { fetchImpl, timeoutMs });
|
|
97
|
-
if (Number(apiCall?.status_code) < 200 || Number(apiCall?.status_code) >= 300) continue;
|
|
98
|
-
const quota = parseWeeklyQuota(parseJsonValue(apiCall?.body), credential);
|
|
99
|
-
if (quota) quotas.push(quota);
|
|
100
|
-
} catch {
|
|
101
|
-
// One invalid credential must not hide healthy credentials.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return quotas;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function parseWeeklyQuota(usage, credential = {}) {
|
|
108
|
-
if (!usage || typeof usage !== 'object') return null;
|
|
109
|
-
const rateLimit = usage.rate_limit ?? usage.rateLimit;
|
|
110
|
-
const windows = [rateLimit?.primary_window, rateLimit?.primaryWindow, rateLimit?.secondary_window, rateLimit?.secondaryWindow].filter(Boolean);
|
|
111
|
-
const weekly = windows.find((window) => Number(window?.limit_window_seconds ?? window?.limitWindowSeconds) === 604800)
|
|
112
|
-
?? windows.find((window) => Number(window?.limit_window_seconds ?? window?.limitWindowSeconds) >= 604800)
|
|
113
|
-
?? windows[0];
|
|
114
|
-
if (!weekly) return null;
|
|
115
|
-
const usedPercent = clampPercent(weekly.used_percent ?? weekly.usedPercent);
|
|
116
|
-
const resetAtSeconds = finiteNumber(weekly.reset_at ?? weekly.resetAt);
|
|
117
|
-
const resetAfterSeconds = finiteNumber(weekly.reset_after_seconds ?? weekly.resetAfterSeconds);
|
|
118
|
-
const resetAtMs = resetAtSeconds > 0
|
|
119
|
-
? resetAtSeconds * 1000
|
|
120
|
-
: resetAfterSeconds > 0 ? Date.now() + resetAfterSeconds * 1000 : NaN;
|
|
121
|
-
if (!Number.isFinite(usedPercent) || !Number.isFinite(resetAtMs)) return null;
|
|
122
|
-
return {
|
|
123
|
-
usedPercent,
|
|
124
|
-
remainingPercent: Math.max(0, 100 - usedPercent),
|
|
125
|
-
resetAt: new Date(resetAtMs).toISOString(),
|
|
126
|
-
account: String(credential.label || credential.email || usage.email || '').trim(),
|
|
127
|
-
planType: String(usage.plan_type ?? usage.planType ?? credential?.id_token?.plan_type ?? '').trim(),
|
|
128
|
-
updatedAt: new Date().toISOString(),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function selectCodexCredentials(files) {
|
|
133
|
-
if (!Array.isArray(files)) return [];
|
|
134
|
-
return files.filter((file) => {
|
|
135
|
-
const provider = String(file?.type || file?.provider || '').toLowerCase();
|
|
136
|
-
return provider === 'codex'
|
|
137
|
-
&& Boolean(file?.auth_index)
|
|
138
|
-
&& !normalizeBoolean(file?.disabled);
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function normalizeBoolean(value) {
|
|
143
|
-
if (typeof value === 'boolean') return value;
|
|
144
|
-
if (typeof value === 'number') return value !== 0;
|
|
145
|
-
return String(value || '').trim().toLowerCase() === 'true';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function managementBaseUrl(value) {
|
|
149
|
-
const url = new URL(String(value).trim());
|
|
150
|
-
const pathname = url.pathname.replace(/\/+$/, '');
|
|
151
|
-
url.pathname = pathname.endsWith('/v0/management')
|
|
152
|
-
? pathname
|
|
153
|
-
: `${pathname}/v0/management`;
|
|
154
|
-
url.search = '';
|
|
155
|
-
url.hash = '';
|
|
156
|
-
return url.toString().replace(/\/$/, '');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function fetchJson(url, options, { fetchImpl, timeoutMs }) {
|
|
160
|
-
const controller = new AbortController();
|
|
161
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
162
|
-
try {
|
|
163
|
-
const response = await fetchImpl(url, { ...options, signal: controller.signal });
|
|
164
|
-
if (!response.ok) throw new Error(`CPA HTTP ${response.status}`);
|
|
165
|
-
return await response.json();
|
|
166
|
-
} finally {
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function parseJsonValue(value) {
|
|
172
|
-
if (value && typeof value === 'object') return value;
|
|
173
|
-
if (typeof value !== 'string') return null;
|
|
174
|
-
try { return JSON.parse(value); } catch { return null; }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function finiteNumber(value) {
|
|
178
|
-
const number = Number(value);
|
|
179
|
-
return Number.isFinite(number) ? number : NaN;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function clampPercent(value) {
|
|
183
|
-
const number = finiteNumber(value);
|
|
184
|
-
return Number.isFinite(number) ? Math.min(100, Math.max(0, Math.round(number * 10) / 10)) : NaN;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function normalizeConfig(value) {
|
|
188
|
-
return {
|
|
189
|
-
url: String(value?.url || '').trim(),
|
|
190
|
-
password: String(value?.password || ''),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function readConfig(configFile) {
|
|
195
|
-
if (!configFile) return { url: '', password: '' };
|
|
196
|
-
try {
|
|
197
|
-
return normalizeConfig(JSON.parse(await fsp.readFile(configFile, 'utf8')));
|
|
198
|
-
} catch (error) {
|
|
199
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
200
|
-
return { url: '', password: '' };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function writeConfig(configFile, config) {
|
|
205
|
-
if (!configFile) return;
|
|
206
|
-
await fsp.mkdir(path.dirname(configFile), { recursive: true });
|
|
207
|
-
await fsp.writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
208
|
-
await fsp.chmod(configFile, 0o600).catch(() => {});
|
|
209
|
-
}
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_REFRESH_MS = 60_000;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 12_000;
|
|
6
|
+
const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
7
|
+
const CODEX_USER_AGENT = 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal';
|
|
8
|
+
|
|
9
|
+
export function createCpaQuotaMonitor({
|
|
10
|
+
configFile,
|
|
11
|
+
refreshMs = DEFAULT_REFRESH_MS,
|
|
12
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
13
|
+
fetchImpl = fetch,
|
|
14
|
+
} = {}) {
|
|
15
|
+
let config = { url: '', password: '' };
|
|
16
|
+
let quotas = [];
|
|
17
|
+
let timer;
|
|
18
|
+
let refreshPromise;
|
|
19
|
+
|
|
20
|
+
async function start() {
|
|
21
|
+
config = await readConfig(configFile);
|
|
22
|
+
await refresh();
|
|
23
|
+
timer = setInterval(() => { void refresh(); }, refreshMs);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stop() {
|
|
28
|
+
if (timer) clearInterval(timer);
|
|
29
|
+
timer = undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPublicState() {
|
|
33
|
+
return {
|
|
34
|
+
config: {
|
|
35
|
+
url: config.url,
|
|
36
|
+
hasPassword: Boolean(config.password),
|
|
37
|
+
},
|
|
38
|
+
quotas,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function updateConfig(value) {
|
|
43
|
+
const next = normalizeConfig(value);
|
|
44
|
+
config = {
|
|
45
|
+
...next,
|
|
46
|
+
password: value?.preservePassword && config.password ? config.password : next.password,
|
|
47
|
+
};
|
|
48
|
+
await writeConfig(configFile, config);
|
|
49
|
+
await refresh();
|
|
50
|
+
return getPublicState();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function refresh() {
|
|
54
|
+
if (refreshPromise) return refreshPromise;
|
|
55
|
+
refreshPromise = (async () => {
|
|
56
|
+
try {
|
|
57
|
+
quotas = await fetchCpaQuotas(config, { fetchImpl, timeoutMs });
|
|
58
|
+
} catch {
|
|
59
|
+
quotas = [];
|
|
60
|
+
} finally {
|
|
61
|
+
refreshPromise = undefined;
|
|
62
|
+
}
|
|
63
|
+
return quotas;
|
|
64
|
+
})();
|
|
65
|
+
return refreshPromise;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { start, stop, refresh, updateConfig, getPublicState };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchCpaQuotas(config, { fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
72
|
+
const normalized = normalizeConfig(config);
|
|
73
|
+
if (!normalized.url || !normalized.password) return [];
|
|
74
|
+
const managementBase = managementBaseUrl(normalized.url);
|
|
75
|
+
const headers = { Authorization: `Bearer ${normalized.password}` };
|
|
76
|
+
const authFiles = await fetchJson(`${managementBase}/auth-files`, { headers }, { fetchImpl, timeoutMs });
|
|
77
|
+
const credentials = selectCodexCredentials(authFiles?.files);
|
|
78
|
+
const quotas = [];
|
|
79
|
+
for (const credential of credentials) {
|
|
80
|
+
try {
|
|
81
|
+
const accountId = credential?.id_token?.chatgpt_account_id;
|
|
82
|
+
const apiCall = await fetchJson(`${managementBase}/api-call`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
authIndex: credential.auth_index,
|
|
87
|
+
method: 'GET',
|
|
88
|
+
url: CODEX_USAGE_URL,
|
|
89
|
+
header: {
|
|
90
|
+
Authorization: 'Bearer $TOKEN$',
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
'User-Agent': CODEX_USER_AGENT,
|
|
93
|
+
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
}, { fetchImpl, timeoutMs });
|
|
97
|
+
if (Number(apiCall?.status_code) < 200 || Number(apiCall?.status_code) >= 300) continue;
|
|
98
|
+
const quota = parseWeeklyQuota(parseJsonValue(apiCall?.body), credential);
|
|
99
|
+
if (quota) quotas.push(quota);
|
|
100
|
+
} catch {
|
|
101
|
+
// One invalid credential must not hide healthy credentials.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return quotas;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseWeeklyQuota(usage, credential = {}) {
|
|
108
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
109
|
+
const rateLimit = usage.rate_limit ?? usage.rateLimit;
|
|
110
|
+
const windows = [rateLimit?.primary_window, rateLimit?.primaryWindow, rateLimit?.secondary_window, rateLimit?.secondaryWindow].filter(Boolean);
|
|
111
|
+
const weekly = windows.find((window) => Number(window?.limit_window_seconds ?? window?.limitWindowSeconds) === 604800)
|
|
112
|
+
?? windows.find((window) => Number(window?.limit_window_seconds ?? window?.limitWindowSeconds) >= 604800)
|
|
113
|
+
?? windows[0];
|
|
114
|
+
if (!weekly) return null;
|
|
115
|
+
const usedPercent = clampPercent(weekly.used_percent ?? weekly.usedPercent);
|
|
116
|
+
const resetAtSeconds = finiteNumber(weekly.reset_at ?? weekly.resetAt);
|
|
117
|
+
const resetAfterSeconds = finiteNumber(weekly.reset_after_seconds ?? weekly.resetAfterSeconds);
|
|
118
|
+
const resetAtMs = resetAtSeconds > 0
|
|
119
|
+
? resetAtSeconds * 1000
|
|
120
|
+
: resetAfterSeconds > 0 ? Date.now() + resetAfterSeconds * 1000 : NaN;
|
|
121
|
+
if (!Number.isFinite(usedPercent) || !Number.isFinite(resetAtMs)) return null;
|
|
122
|
+
return {
|
|
123
|
+
usedPercent,
|
|
124
|
+
remainingPercent: Math.max(0, 100 - usedPercent),
|
|
125
|
+
resetAt: new Date(resetAtMs).toISOString(),
|
|
126
|
+
account: String(credential.label || credential.email || usage.email || '').trim(),
|
|
127
|
+
planType: String(usage.plan_type ?? usage.planType ?? credential?.id_token?.plan_type ?? '').trim(),
|
|
128
|
+
updatedAt: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function selectCodexCredentials(files) {
|
|
133
|
+
if (!Array.isArray(files)) return [];
|
|
134
|
+
return files.filter((file) => {
|
|
135
|
+
const provider = String(file?.type || file?.provider || '').toLowerCase();
|
|
136
|
+
return provider === 'codex'
|
|
137
|
+
&& Boolean(file?.auth_index)
|
|
138
|
+
&& !normalizeBoolean(file?.disabled);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeBoolean(value) {
|
|
143
|
+
if (typeof value === 'boolean') return value;
|
|
144
|
+
if (typeof value === 'number') return value !== 0;
|
|
145
|
+
return String(value || '').trim().toLowerCase() === 'true';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function managementBaseUrl(value) {
|
|
149
|
+
const url = new URL(String(value).trim());
|
|
150
|
+
const pathname = url.pathname.replace(/\/+$/, '');
|
|
151
|
+
url.pathname = pathname.endsWith('/v0/management')
|
|
152
|
+
? pathname
|
|
153
|
+
: `${pathname}/v0/management`;
|
|
154
|
+
url.search = '';
|
|
155
|
+
url.hash = '';
|
|
156
|
+
return url.toString().replace(/\/$/, '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchJson(url, options, { fetchImpl, timeoutMs }) {
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetchImpl(url, { ...options, signal: controller.signal });
|
|
164
|
+
if (!response.ok) throw new Error(`CPA HTTP ${response.status}`);
|
|
165
|
+
return await response.json();
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseJsonValue(value) {
|
|
172
|
+
if (value && typeof value === 'object') return value;
|
|
173
|
+
if (typeof value !== 'string') return null;
|
|
174
|
+
try { return JSON.parse(value); } catch { return null; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function finiteNumber(value) {
|
|
178
|
+
const number = Number(value);
|
|
179
|
+
return Number.isFinite(number) ? number : NaN;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function clampPercent(value) {
|
|
183
|
+
const number = finiteNumber(value);
|
|
184
|
+
return Number.isFinite(number) ? Math.min(100, Math.max(0, Math.round(number * 10) / 10)) : NaN;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeConfig(value) {
|
|
188
|
+
return {
|
|
189
|
+
url: String(value?.url || '').trim(),
|
|
190
|
+
password: String(value?.password || ''),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function readConfig(configFile) {
|
|
195
|
+
if (!configFile) return { url: '', password: '' };
|
|
196
|
+
try {
|
|
197
|
+
return normalizeConfig(JSON.parse(await fsp.readFile(configFile, 'utf8')));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
200
|
+
return { url: '', password: '' };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function writeConfig(configFile, config) {
|
|
205
|
+
if (!configFile) return;
|
|
206
|
+
await fsp.mkdir(path.dirname(configFile), { recursive: true });
|
|
207
|
+
await fsp.writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
208
|
+
await fsp.chmod(configFile, 0o600).catch(() => {});
|
|
209
|
+
}
|