@purpleschool/infisical-env 0.1.2 → 0.1.4

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/package.json +3 -2
  2. package/src/cli.js +266 -269
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purpleschool/infisical-env",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "infisical-env": "src/cli.js"
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "picocolors": "^1.0.1",
16
+ "prompts": "^2.4.2",
16
17
  "yaml": "^2.4.5"
17
18
  }
18
- }
19
+ }
package/src/cli.js CHANGED
@@ -3,10 +3,10 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
- import readline from "node:readline/promises";
7
6
  import YAML from "yaml";
8
7
  import os from "node:os";
9
8
  import pc from "picocolors";
9
+ import prompts from "prompts";
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = path.dirname(__filename);
@@ -16,348 +16,345 @@ const DEFAULT_INFISICAL_BASE_URL = "https://secret.purplecode.ru";
16
16
  const DEFAULT_INFISICAL_CLIENT_ID = "f3e0d60c-a602-4296-bd03-3be228e89165";
17
17
 
18
18
  async function main() {
19
- try {
20
- const args = process.argv.slice(2);
21
- if (args.includes("--help") || args.includes("-h")) {
22
- printHelp();
23
- return;
24
- }
25
-
26
- const repoRoot = findRepoRoot(process.cwd());
27
- const packageJson = readJson(path.join(repoRoot, "package.json"));
19
+ try {
20
+ const args = process.argv.slice(2);
21
+ if (args.includes("--help") || args.includes("-h")) {
22
+ printHelp();
23
+ return;
24
+ }
28
25
 
29
- const projectId = resolveInfisicalProject(packageJson);
30
- const envName = await resolveEnvName(args);
26
+ const repoRoot = findRepoRoot(process.cwd());
27
+ const packageJson = readJson(path.join(repoRoot, "package.json"));
31
28
 
32
- const deploymentPath = resolveDeploymentPath(repoRoot);
33
- const envRefs = collectEnvRefs(deploymentPath);
29
+ const projectId = resolveInfisicalProject(packageJson);
30
+ const envName = await resolveEnvName(args);
34
31
 
35
- if (envRefs.length === 0) {
36
- throw new Error(`No env refs found in ${deploymentPath}`);
37
- }
32
+ const deploymentPath = resolveDeploymentPath(repoRoot);
33
+ const envRefs = collectEnvRefs(deploymentPath);
38
34
 
39
- const values = await resolveEnvValues({
40
- projectId,
41
- envName,
42
- envRefs,
43
- repoRoot,
44
- });
45
-
46
- const envFilePath = path.join(repoRoot, ".env");
47
- const lines = values.map((item) => `${item.name}=${item.value}`);
48
- fs.writeFileSync(envFilePath, lines.join(os.EOL), "utf8");
49
-
50
- console.log(pc.green(`.env written to ${envFilePath}`));
51
- } catch (err) {
52
- console.error(pc.red(formatError(err)));
53
- console.error(pc.yellow("Run with --help for usage."));
54
- process.exit(1);
35
+ if (envRefs.length === 0) {
36
+ throw new Error(`No env refs found in ${deploymentPath}`);
55
37
  }
38
+
39
+ const values = await resolveEnvValues({
40
+ projectId,
41
+ envName,
42
+ envRefs,
43
+ repoRoot,
44
+ });
45
+
46
+ const envFilePath = path.join(repoRoot, ".env");
47
+ const lines = values.map((item) => `${item.name}=${item.value}`);
48
+ fs.writeFileSync(envFilePath, lines.join(os.EOL), "utf8");
49
+
50
+ console.log(pc.green(`.env written to ${envFilePath}`));
51
+ } catch (err) {
52
+ console.error(pc.red(formatError(err)));
53
+ console.error(pc.yellow("Run with --help for usage."));
54
+ process.exit(1);
55
+ }
56
56
  }
57
57
 
58
58
  function printHelp() {
59
- console.log(
60
- [
61
- pc.bold("Usage:"),
62
- " infisical-env [--env stage|stage-2]",
63
- "",
64
- pc.bold("Options:"),
65
- " --env <name> Environment name (stage or stage-2)",
66
- " -h, --help Show this help",
67
- "",
68
- pc.bold("Environment variables:"),
69
- " INFISICAL_CLIENT_SECRET Required (machine identity secret)",
70
- " INFISICAL_CLIENT_ID Optional override",
71
- " INFISICAL_BASE_URL Optional override (default https://secret.purplecode.ru)",
72
- " INFISICAL_MOCK_PATH Optional mock json path for offline testing",
73
- ].join(os.EOL)
74
- );
59
+ console.log(
60
+ [
61
+ pc.bold("Usage:"),
62
+ " infisical-env [--env stage|stage-2]",
63
+ "",
64
+ pc.bold("Options:"),
65
+ " --env <name> Environment name (stage or stage-2)",
66
+ " -h, --help Show this help",
67
+ "",
68
+ pc.bold("Environment variables:"),
69
+ " INFISICAL_CLIENT_SECRET Required (machine identity secret)",
70
+ " INFISICAL_CLIENT_ID Optional override",
71
+ " INFISICAL_BASE_URL Optional override (default https://secret.purplecode.ru)",
72
+ " INFISICAL_MOCK_PATH Optional mock json path for offline testing",
73
+ ].join(os.EOL)
74
+ );
75
75
  }
76
76
 
77
77
  function findRepoRoot(startDir) {
78
- let current = startDir;
79
- while (true) {
80
- const candidate = path.join(current, "package.json");
81
- if (fs.existsSync(candidate)) {
82
- return current;
83
- }
84
- const parent = path.dirname(current);
85
- if (parent === current) {
86
- throw new Error("package.json not found in any parent directory");
87
- }
88
- current = parent;
78
+ let current = startDir;
79
+ while (true) {
80
+ const candidate = path.join(current, "package.json");
81
+ if (fs.existsSync(candidate)) {
82
+ return current;
83
+ }
84
+ const parent = path.dirname(current);
85
+ if (parent === current) {
86
+ throw new Error("package.json not found in any parent directory");
89
87
  }
88
+ current = parent;
89
+ }
90
90
  }
91
91
 
92
92
  function readJson(filePath) {
93
- const raw = fs.readFileSync(filePath, "utf8");
94
- return JSON.parse(raw);
93
+ const raw = fs.readFileSync(filePath, "utf8");
94
+ return JSON.parse(raw);
95
95
  }
96
96
 
97
97
  function resolveInfisicalProject(packageJson) {
98
- const name = typeof packageJson.name === "string" ? packageJson.name.toLowerCase() : "";
99
- if (name.includes("rugpt")) {
100
- return "48854e39-7129-4786-87ac-cfcce296991c";
101
- }
102
- if (name.includes("studdy") || name.includes('student-works')) {
103
- return "b4b0abbd-f8e6-428a-a296-6289369ca8dd";
104
- }
105
- if (name.includes("multisite")) {
106
- return "6531c08b-45e0-4d75-a8ab-08d14d0387bf";
107
- }
108
- throw new Error(
109
- "Unable to resolve Infisical project from package.json name. Expected name to include rugpt, studdy, or multisite."
110
- );
98
+ const name = typeof packageJson.name === "string" ? packageJson.name.toLowerCase() : "";
99
+ if (name.includes("rugpt")) {
100
+ return "48854e39-7129-4786-87ac-cfcce296991c";
101
+ }
102
+ if (name.includes("studdy") || name.includes("student-works")) {
103
+ return "b4b0abbd-f8e6-428a-a296-6289369ca8dd";
104
+ }
105
+ if (name.includes("multisite")) {
106
+ return "6531c08b-45e0-4d75-a8ab-08d14d0387bf";
107
+ }
108
+ throw new Error(
109
+ "Unable to resolve Infisical project from package.json name. Expected name to include rugpt, studdy, student-works, or multisite."
110
+ );
111
111
  }
112
112
 
113
113
  async function resolveEnvName(args) {
114
- const flagValue = parseEnvFlag(args);
115
- if (flagValue) {
116
- return validateEnv(flagValue);
117
- }
118
- return promptEnv();
114
+ const flagValue = parseEnvFlag(args);
115
+ if (flagValue) {
116
+ return validateEnv(flagValue);
117
+ }
118
+ return promptEnv();
119
119
  }
120
120
 
121
121
  function parseEnvFlag(args) {
122
- for (let i = 0; i < args.length; i += 1) {
123
- const arg = args[i];
124
- if (arg === "--env") {
125
- return args[i + 1] || "";
126
- }
127
- if (arg.startsWith("--env=")) {
128
- return arg.slice("--env=".length);
129
- }
122
+ for (let i = 0; i < args.length; i += 1) {
123
+ const arg = args[i];
124
+ if (arg === "--env") {
125
+ return args[i + 1] || "";
130
126
  }
131
- return "";
127
+ if (arg.startsWith("--env=")) {
128
+ return arg.slice("--env=".length);
129
+ }
130
+ }
131
+ return "";
132
132
  }
133
133
 
134
134
  function validateEnv(value) {
135
- if (!ALLOWED_ENVS.includes(value)) {
136
- throw new Error(`Invalid environment \"${value}\". Allowed: ${ALLOWED_ENVS.join(", ")}`);
137
- }
138
- return value;
135
+ if (!ALLOWED_ENVS.includes(value)) {
136
+ throw new Error(`Invalid environment \"${value}\". Allowed: ${ALLOWED_ENVS.join(", ")}`);
137
+ }
138
+ return value;
139
139
  }
140
140
 
141
141
  async function promptEnv() {
142
- const rl = readline.createInterface({
143
- input: process.stdin,
144
- output: process.stdout,
145
- });
146
-
147
- try {
148
- console.log(pc.bold("Select environment:"));
149
- ALLOWED_ENVS.forEach((name, idx) => {
150
- console.log(`${idx + 1}. ${name}`);
151
- });
152
- const answer = await rl.question("Enter number: ");
153
- const index = Number(answer.trim()) - 1;
154
- if (!Number.isInteger(index) || index < 0 || index >= ALLOWED_ENVS.length) {
155
- throw new Error("Invalid environment selection");
156
- }
157
- return ALLOWED_ENVS[index];
158
- } finally {
159
- rl.close();
142
+ const response = await prompts(
143
+ {
144
+ type: "select",
145
+ name: "env",
146
+ message: "Select environment",
147
+ choices: ALLOWED_ENVS.map((value) => ({ title: value, value })),
148
+ initial: 0,
149
+ },
150
+ {
151
+ onCancel: () => {
152
+ throw new Error("Environment selection canceled");
153
+ },
160
154
  }
155
+ );
156
+
157
+ return validateEnv(response.env);
161
158
  }
162
159
 
163
160
  function resolveDeploymentPath(repoRoot) {
164
- const candidates = [
165
- path.join(repoRoot, ".kube", "deployment.yml"),
166
- path.join(repoRoot, ".kube", "deployment.yaml"),
167
- path.join(repoRoot, "deployment.yml"),
168
- path.join(repoRoot, "deployment.yaml"),
169
- ];
170
-
171
- for (const candidate of candidates) {
172
- if (fs.existsSync(candidate)) {
173
- return candidate;
174
- }
161
+ const candidates = [
162
+ path.join(repoRoot, ".kube", "deployment.yml"),
163
+ path.join(repoRoot, ".kube", "deployment.yaml"),
164
+ path.join(repoRoot, "deployment.yml"),
165
+ path.join(repoRoot, "deployment.yaml"),
166
+ ];
167
+
168
+ for (const candidate of candidates) {
169
+ if (fs.existsSync(candidate)) {
170
+ return candidate;
175
171
  }
172
+ }
176
173
 
177
- throw new Error("Deployment file not found. Expected .kube/deployment.yml or .kube/deployment.yaml");
174
+ throw new Error("Deployment file not found. Expected .kube/deployment.yml or .kube/deployment.yaml");
178
175
  }
179
176
 
180
177
  function collectEnvRefs(deploymentPath) {
181
- const raw = fs.readFileSync(deploymentPath, "utf8");
182
- const docs = YAML.parseAllDocuments(raw);
183
- const refs = [];
184
-
185
- for (const doc of docs) {
186
- const data = doc.toJSON();
187
- const containers = data?.spec?.template?.spec?.containers || [];
188
- for (const container of containers) {
189
- const envList = container?.env || [];
190
- for (const envItem of envList) {
191
- const name = envItem?.name;
192
- const secretKeyRef = envItem?.valueFrom?.secretKeyRef;
193
- const folder = secretKeyRef?.name;
194
- const key = secretKeyRef?.key;
195
- if (name && folder && key) {
196
- refs.push({ name, folder, key });
197
- }
198
- }
178
+ const raw = fs.readFileSync(deploymentPath, "utf8");
179
+ const docs = YAML.parseAllDocuments(raw);
180
+ const refs = [];
181
+
182
+ for (const doc of docs) {
183
+ const data = doc.toJSON();
184
+ const containers = data?.spec?.template?.spec?.containers || [];
185
+ for (const container of containers) {
186
+ const envList = container?.env || [];
187
+ for (const envItem of envList) {
188
+ const name = envItem?.name;
189
+ const secretKeyRef = envItem?.valueFrom?.secretKeyRef;
190
+ const folder = secretKeyRef?.name;
191
+ const key = secretKeyRef?.key;
192
+ if (name && folder && key) {
193
+ refs.push({ name, folder, key });
199
194
  }
195
+ }
200
196
  }
197
+ }
201
198
 
202
- return refs;
199
+ return refs;
203
200
  }
204
201
 
205
202
  async function resolveEnvValues({ projectId, envName, envRefs, repoRoot }) {
206
- const mockPath = process.env.INFISICAL_MOCK_PATH;
207
- if (mockPath) {
208
- return resolveFromMock({ projectId, envName, envRefs, mockPath, repoRoot });
209
- }
210
-
211
- const clientId = process.env.INFISICAL_CLIENT_ID || DEFAULT_INFISICAL_CLIENT_ID;
212
- const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
213
- if (!clientId || !clientSecret) {
214
- throw new Error(
215
- "Missing Infisical credentials. Set INFISICAL_CLIENT_SECRET or use INFISICAL_MOCK_PATH."
216
- );
217
- }
203
+ const mockPath = process.env.INFISICAL_MOCK_PATH;
204
+ if (mockPath) {
205
+ return resolveFromMock({ projectId, envName, envRefs, mockPath, repoRoot });
206
+ }
207
+
208
+ const clientId = process.env.INFISICAL_CLIENT_ID || DEFAULT_INFISICAL_CLIENT_ID;
209
+ const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
210
+ if (!clientId || !clientSecret) {
211
+ throw new Error(
212
+ "Missing Infisical credentials. Set INFISICAL_CLIENT_SECRET or use INFISICAL_MOCK_PATH."
213
+ );
214
+ }
218
215
 
219
- const baseUrl = normalizeBaseUrl(process.env.INFISICAL_BASE_URL || DEFAULT_INFISICAL_BASE_URL);
220
- const accessToken = await loginUniversalAuth({ baseUrl, clientId, clientSecret });
216
+ const baseUrl = normalizeBaseUrl(process.env.INFISICAL_BASE_URL || DEFAULT_INFISICAL_BASE_URL);
217
+ const accessToken = await loginUniversalAuth({ baseUrl, clientId, clientSecret });
221
218
 
222
- return fetchSecretsFromInfisical({ baseUrl, accessToken, projectId, envName, envRefs });
219
+ return fetchSecretsFromInfisical({ baseUrl, accessToken, projectId, envName, envRefs });
223
220
  }
224
221
 
225
222
  function resolveFromMock({ projectId, envName, envRefs, mockPath, repoRoot }) {
226
- const absPath = path.isAbsolute(mockPath) ? mockPath : path.join(repoRoot, mockPath);
227
- if (!fs.existsSync(absPath)) {
228
- throw new Error(`Mock file not found: ${absPath}`);
229
- }
230
- const data = readJson(absPath);
231
- const projectData = data?.projects?.[projectId];
232
- if (!projectData) {
233
- throw new Error(`Project not found in mock: ${projectId}`);
223
+ const absPath = path.isAbsolute(mockPath) ? mockPath : path.join(repoRoot, mockPath);
224
+ if (!fs.existsSync(absPath)) {
225
+ throw new Error(`Mock file not found: ${absPath}`);
226
+ }
227
+ const data = readJson(absPath);
228
+ const projectData = data?.projects?.[projectId];
229
+ if (!projectData) {
230
+ throw new Error(`Project not found in mock: ${projectId}`);
231
+ }
232
+ const envData = projectData?.[envName];
233
+ if (!envData) {
234
+ throw new Error(`Environment not found in mock: ${envName}`);
235
+ }
236
+
237
+ return envRefs.map((ref) => {
238
+ const folderData = envData?.[ref.folder];
239
+ const value = folderData?.[ref.key];
240
+ if (value === undefined || value === null) {
241
+ throw new Error(`Missing value for ${ref.folder}.${ref.key}`);
234
242
  }
235
- const envData = projectData?.[envName];
236
- if (!envData) {
237
- throw new Error(`Environment not found in mock: ${envName}`);
238
- }
239
-
240
- return envRefs.map((ref) => {
241
- const folderData = envData?.[ref.folder];
242
- const value = folderData?.[ref.key];
243
- if (value === undefined || value === null) {
244
- throw new Error(`Missing value for ${ref.folder}.${ref.key}`);
245
- }
246
- return { name: ref.name, value: String(value) };
247
- });
243
+ return { name: ref.name, value: String(value) };
244
+ });
248
245
  }
249
246
 
250
247
  function formatError(err) {
251
- if (err instanceof Error) {
252
- return err.message;
253
- }
254
- return String(err);
248
+ if (err instanceof Error) {
249
+ return err.message;
250
+ }
251
+ return String(err);
255
252
  }
256
253
 
257
254
  function normalizeBaseUrl(value) {
258
- return value.replace(/\/+$/, "");
255
+ return value.replace(/\/+$/, "");
259
256
  }
260
257
 
261
258
  async function loginUniversalAuth({ baseUrl, clientId, clientSecret }) {
262
- const url = `${baseUrl}/api/v1/auth/universal-auth/login`;
263
- const body = new URLSearchParams();
264
- body.set("clientId", clientId);
265
- body.set("clientSecret", clientSecret);
266
-
267
- const res = await fetch(url, {
268
- method: "POST",
269
- headers: {
270
- "Content-Type": "application/x-www-form-urlencoded",
271
- },
272
- body,
273
- });
274
-
275
- if (!res.ok) {
276
- const text = await safeReadText(res);
277
- throw new Error(`Infisical login failed (${res.status}). ${text}`);
278
- }
279
-
280
- const data = await res.json();
281
- if (!data?.accessToken) {
282
- throw new Error("Infisical login response missing accessToken.");
283
- }
284
- return data.accessToken;
259
+ const url = `${baseUrl}/api/v1/auth/universal-auth/login`;
260
+ const body = new URLSearchParams();
261
+ body.set("clientId", clientId);
262
+ body.set("clientSecret", clientSecret);
263
+
264
+ const res = await fetch(url, {
265
+ method: "POST",
266
+ headers: {
267
+ "Content-Type": "application/x-www-form-urlencoded",
268
+ },
269
+ body,
270
+ });
271
+
272
+ if (!res.ok) {
273
+ const text = await safeReadText(res);
274
+ throw new Error(`Infisical login failed (${res.status}). ${text}`);
275
+ }
276
+
277
+ const data = await res.json();
278
+ if (!data?.accessToken) {
279
+ throw new Error("Infisical login response missing accessToken.");
280
+ }
281
+ return data.accessToken;
285
282
  }
286
283
 
287
284
  async function fetchSecretsFromInfisical({ baseUrl, accessToken, projectId, envName, envRefs }) {
288
- const refsByFolder = new Map();
289
- for (const ref of envRefs) {
290
- if (!refsByFolder.has(ref.folder)) {
291
- refsByFolder.set(ref.folder, []);
292
- }
293
- refsByFolder.get(ref.folder).push(ref);
285
+ const refsByFolder = new Map();
286
+ for (const ref of envRefs) {
287
+ if (!refsByFolder.has(ref.folder)) {
288
+ refsByFolder.set(ref.folder, []);
294
289
  }
290
+ refsByFolder.get(ref.folder).push(ref);
291
+ }
292
+
293
+ const secretCache = new Map();
294
+ for (const [folder, refs] of refsByFolder.entries()) {
295
+ const secretPath = folder.startsWith("/") ? folder : `/${folder}`;
296
+ const secrets = await listSecrets({
297
+ baseUrl,
298
+ accessToken,
299
+ projectId,
300
+ envName,
301
+ secretPath,
302
+ });
303
+ const map = new Map();
304
+ for (const item of secrets) {
305
+ if (item?.secretKey != null) {
306
+ map.set(item.secretKey, item.secretValue);
307
+ }
308
+ }
309
+ secretCache.set(folder, map);
295
310
 
296
- const secretCache = new Map();
297
- for (const [folder, refs] of refsByFolder.entries()) {
298
- const secretPath = folder.startsWith("/") ? folder : `/${folder}`;
299
- const secrets = await listSecrets({
300
- baseUrl,
301
- accessToken,
302
- projectId,
303
- envName,
304
- secretPath,
305
- });
306
- const map = new Map();
307
- for (const item of secrets) {
308
- if (item?.secretKey != null) {
309
- map.set(item.secretKey, item.secretValue);
310
- }
311
- }
312
- secretCache.set(folder, map);
313
-
314
- for (const ref of refs) {
315
- if (!map.has(ref.key)) {
316
- throw new Error(`Missing value for ${folder}.${ref.key} in environment ${envName}`);
317
- }
318
- }
311
+ for (const ref of refs) {
312
+ if (!map.has(ref.key)) {
313
+ throw new Error(`Missing value for ${folder}.${ref.key} in environment ${envName}`);
314
+ }
319
315
  }
316
+ }
320
317
 
321
- return envRefs.map((ref) => {
322
- const folderMap = secretCache.get(ref.folder);
323
- const value = folderMap?.get(ref.key);
324
- return { name: ref.name, value: String(value) };
325
- });
318
+ return envRefs.map((ref) => {
319
+ const folderMap = secretCache.get(ref.folder);
320
+ const value = folderMap?.get(ref.key);
321
+ return { name: ref.name, value: String(value) };
322
+ });
326
323
  }
327
324
 
328
325
  async function listSecrets({ baseUrl, accessToken, projectId, envName, secretPath }) {
329
- const params = new URLSearchParams({
330
- projectId,
331
- environment: envName,
332
- secretPath,
333
- viewSecretValue: "true",
334
- expandSecretReferences: "true",
335
- recursive: "false",
336
- includeImports: "true",
337
- });
338
-
339
- const url = `${baseUrl}/api/v4/secrets?${params.toString()}`;
340
- const res = await fetch(url, {
341
- headers: {
342
- Authorization: `Bearer ${accessToken}`,
343
- },
344
- });
345
-
346
- if (!res.ok) {
347
- const text = await safeReadText(res);
348
- throw new Error(`Infisical secrets list failed (${res.status}) for ${secretPath}. ${text}`);
349
- }
350
-
351
- const data = await res.json();
352
- return Array.isArray(data?.secrets) ? data.secrets : [];
326
+ const params = new URLSearchParams({
327
+ projectId,
328
+ environment: envName,
329
+ secretPath,
330
+ viewSecretValue: "true",
331
+ expandSecretReferences: "true",
332
+ recursive: "false",
333
+ includeImports: "true",
334
+ });
335
+
336
+ const url = `${baseUrl}/api/v4/secrets?${params.toString()}`;
337
+ const res = await fetch(url, {
338
+ headers: {
339
+ Authorization: `Bearer ${accessToken}`,
340
+ },
341
+ });
342
+
343
+ if (!res.ok) {
344
+ const text = await safeReadText(res);
345
+ throw new Error(`Infisical secrets list failed (${res.status}) for ${secretPath}. ${text}`);
346
+ }
347
+
348
+ const data = await res.json();
349
+ return Array.isArray(data?.secrets) ? data.secrets : [];
353
350
  }
354
351
 
355
352
  async function safeReadText(res) {
356
- try {
357
- return await res.text();
358
- } catch {
359
- return "";
360
- }
353
+ try {
354
+ return await res.text();
355
+ } catch {
356
+ return "";
357
+ }
361
358
  }
362
359
 
363
360
  await main();