@valbuild/cli 0.72.4 → 0.73.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/src/login.ts ADDED
@@ -0,0 +1,112 @@
1
+ import pc from "picocolors";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { getPersonalAccessTokenPath } from "@valbuild/server";
5
+
6
+ const host = process.env.VAL_BUILD_URL || "https://app.val.build";
7
+
8
+ export async function login(options: { root?: string }) {
9
+ try {
10
+ console.log(pc.cyan("\nStarting login process...\n"));
11
+
12
+ // Step 1: Initiate login and get token and URL
13
+ const response = await fetch(`${host}/api/login`, {
14
+ method: "POST",
15
+ headers: {
16
+ "Content-Type": "application/json",
17
+ },
18
+ });
19
+ let token;
20
+ let url;
21
+ if (!response.headers.get("content-type")?.includes("application/json")) {
22
+ const text = await response.text();
23
+ console.error(
24
+ pc.red(
25
+ "Unexpected failure while trying to login (content type was not JSON). Server response:",
26
+ ),
27
+ text || "<empty>",
28
+ );
29
+ process.exit(1);
30
+ }
31
+ const json = await response.json();
32
+ if (json) {
33
+ token = json.nonce;
34
+ url = json.url;
35
+ }
36
+ if (!token || !url) {
37
+ console.error(pc.red("Unexpected response from the server."), json);
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(pc.green("Open the following URL in your browser to log in:"));
42
+ console.log(pc.underline(pc.blue(url)));
43
+ console.log(pc.dim("\nWaiting for login confirmation...\n"));
44
+
45
+ // Step 2: Poll for login confirmation
46
+ const result = await pollForConfirmation(token);
47
+
48
+ // Step 3: Save the token
49
+ const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
50
+ saveToken(result, filePath);
51
+ } catch (error) {
52
+ console.error(
53
+ pc.red(
54
+ "An error occurred during the login process. Check your internet connection. Details:",
55
+ ),
56
+ error instanceof Error ? error.message : JSON.stringify(error, null, 2),
57
+ );
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
63
+ async function pollForConfirmation(token: string): Promise<{
64
+ profile: { username: string };
65
+ pat: string;
66
+ }> {
67
+ const start = Date.now();
68
+ while (Date.now() - start < MAX_DURATION) {
69
+ await new Promise((resolve) => setTimeout(resolve, 1000));
70
+ const response = await fetch(
71
+ `${host}/api/login?token=${token}&consume=true`,
72
+ );
73
+ if (response.status === 500) {
74
+ console.error(pc.red("An error occurred on the server."));
75
+ process.exit(1);
76
+ }
77
+ if (response.status === 200) {
78
+ const json = await response.json();
79
+ if (json) {
80
+ if (
81
+ typeof json.profile.username === "string" &&
82
+ typeof json.pat === "string"
83
+ ) {
84
+ return json;
85
+ } else {
86
+ console.error(pc.red("Unexpected response from the server."));
87
+ process.exit(1);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ console.error(pc.red("Login confirmation timed out."));
93
+ process.exit(1);
94
+ }
95
+
96
+ function saveToken(
97
+ result: {
98
+ profile: { username: string };
99
+ pat: string;
100
+ },
101
+ filePath: string,
102
+ ) {
103
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
104
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2));
105
+ console.log(
106
+ pc.green(
107
+ `Token for ${pc.cyan(
108
+ result.profile.username,
109
+ )} saved to ${pc.cyan(filePath)}`,
110
+ ),
111
+ );
112
+ }
@@ -0,0 +1,90 @@
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+ import vm from "node:vm";
4
+ import ts from "typescript"; // TODO: make this dependency optional (only required if the file is val.config.ts not val.config.js)
5
+ import z from "zod";
6
+ import { ValConfig } from "@valbuild/core";
7
+
8
+ const ValConfigSchema = z.object({
9
+ project: z.string().optional(),
10
+ root: z.string().optional(),
11
+ files: z
12
+ .object({
13
+ directory: z
14
+ .string()
15
+ .refine((val): val is `/public/val` => val.startsWith("/public/val"), {
16
+ message: "files.directory must start with '/public/val'",
17
+ }),
18
+ })
19
+ .optional(),
20
+ gitCommit: z.string().optional(),
21
+ gitBranch: z.string().optional(),
22
+ defaultTheme: z.union([z.literal("light"), z.literal("dark")]).optional(),
23
+ ai: z
24
+ .object({
25
+ commitMessages: z
26
+ .object({
27
+ disabled: z.boolean().optional(),
28
+ })
29
+ .optional(),
30
+ })
31
+ .optional(),
32
+ });
33
+
34
+ export async function evalValConfigFile(
35
+ projectRoot: string,
36
+ configFileName: string,
37
+ ): Promise<ValConfig | null> {
38
+ const valConfigPath = path.join(projectRoot, configFileName);
39
+
40
+ let code: string | null = null;
41
+ try {
42
+ code = await fs.readFile(valConfigPath, "utf-8");
43
+ } catch (err) {
44
+ //
45
+ }
46
+ if (!code) {
47
+ return null;
48
+ }
49
+
50
+ const transpiled = ts.transpileModule(code, {
51
+ compilerOptions: {
52
+ target: ts.ScriptTarget.ES2020,
53
+ module: ts.ModuleKind.CommonJS,
54
+ esModuleInterop: true,
55
+ },
56
+ fileName: valConfigPath,
57
+ });
58
+
59
+ const exportsObj = {};
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ const sandbox: Record<string, any> = {
62
+ exports: exportsObj,
63
+ module: { exports: exportsObj },
64
+ require, // NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
65
+ __filename: valConfigPath,
66
+ __dirname: path.dirname(valConfigPath),
67
+ console,
68
+ process,
69
+ };
70
+ sandbox.global = sandbox;
71
+
72
+ const context = vm.createContext(sandbox);
73
+ const script = new vm.Script(transpiled.outputText, {
74
+ filename: valConfigPath,
75
+ });
76
+ script.runInContext(context);
77
+ const valConfig = sandbox.module.exports.config;
78
+ if (!valConfig) {
79
+ throw Error(
80
+ `Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`,
81
+ );
82
+ }
83
+ const result = ValConfigSchema.safeParse(valConfig);
84
+ if (!result.success) {
85
+ throw Error(
86
+ `Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`,
87
+ );
88
+ }
89
+ return result.data;
90
+ }
@@ -0,0 +1,4 @@
1
+ export function getFileExt(filePath: string) {
2
+ // NOTE: We do not import the path module. This code is copied in different projects. We want the same implementation and which means that this might running in browser where path is not available).
3
+ return filePath.split(".").pop() || "";
4
+ }
@@ -0,0 +1,5 @@
1
+ import { getVersions } from "../getVersions";
2
+
3
+ export function getValCoreVersion() {
4
+ return getVersions().coreVersion || "unknown";
5
+ }
package/src/validate.ts CHANGED
@@ -1,10 +1,20 @@
1
1
  import path from "path";
2
- import { createFixPatch, createService } from "@valbuild/server";
3
2
  import {
3
+ createFixPatch,
4
+ createService,
5
+ getSettings,
6
+ getPersonalAccessTokenPath,
7
+ parsePersonalAccessTokenFile,
8
+ uploadRemoteFile,
9
+ } from "@valbuild/server";
10
+ import {
11
+ DEFAULT_VAL_REMOTE_HOST,
4
12
  FILE_REF_PROP,
5
13
  Internal,
6
14
  ModuleFilePath,
7
15
  ModulePath,
16
+ SerializedFileSchema,
17
+ SerializedImageSchema,
8
18
  SourcePath,
9
19
  ValidationFix,
10
20
  } from "@valbuild/core";
@@ -12,6 +22,7 @@ import { glob } from "fast-glob";
12
22
  import picocolors from "picocolors";
13
23
  import { ESLint } from "eslint";
14
24
  import fs from "fs/promises";
25
+ import { evalValConfigFile } from "./utils/evalValConfigFile";
15
26
 
16
27
  export async function validate({
17
28
  root,
@@ -22,11 +33,20 @@ export async function validate({
22
33
  fix?: boolean;
23
34
  noEslint?: boolean;
24
35
  }) {
36
+ const valRemoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
25
37
  const projectRoot = root ? path.resolve(root) : process.cwd();
26
38
  const eslint = new ESLint({
27
39
  cwd: projectRoot,
28
40
  ignore: false,
29
41
  });
42
+ const valConfigFile =
43
+ (await evalValConfigFile(projectRoot, "val.config.ts")) ||
44
+ (await evalValConfigFile(projectRoot, "val.config.js"));
45
+ console.log(
46
+ picocolors.greenBright(
47
+ `Validating project${valConfigFile?.project ? ` '${picocolors.inverse(valConfigFile?.project)}'` : ""}...`,
48
+ ),
49
+ );
30
50
  const service = await createService(projectRoot, {});
31
51
  const checkKeyIsValid = async (
32
52
  key: string,
@@ -119,8 +139,8 @@ export async function validate({
119
139
  "files",
120
140
  );
121
141
  }
122
- console.log("Validating...", valFiles.length, "files");
123
-
142
+ console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
143
+ let publicProjectId: string | undefined;
124
144
  let didFix = false; // TODO: ugly
125
145
  async function validateFile(file: string): Promise<number> {
126
146
  const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
@@ -135,6 +155,12 @@ export async function validate({
135
155
  "utf-8",
136
156
  );
137
157
  const eslintResult = eslintResultsByFile?.[file];
158
+ const remoteFiles: Record<
159
+ SourcePath,
160
+ { ref: string; metadata?: Record<string, unknown> }
161
+ > = {};
162
+ let remoteFileBuckets: string[] | null = null;
163
+ let remoteFilesCounter = 0;
138
164
  eslintResult?.messages.forEach((m) => {
139
165
  // display surrounding code
140
166
  logEslintMessage(fileContent, moduleFilePath, m);
@@ -152,6 +178,7 @@ export async function validate({
152
178
  (prev, m) => (m.severity >= 2 ? prev + 1 : prev),
153
179
  0,
154
180
  ) || 0;
181
+ let fixedErrors = 0;
155
182
  if (valModule.errors) {
156
183
  if (valModule.errors.validation) {
157
184
  for (const [sourcePath, validationErrors] of Object.entries(
@@ -178,24 +205,31 @@ export async function validate({
178
205
  valModule.source,
179
206
  valModule.schema,
180
207
  );
181
- const filePath = path.join(
182
- projectRoot,
183
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
- (fileSource.source as any)?.[FILE_REF_PROP],
185
- );
208
+ let filePath: string | null = null;
186
209
  try {
210
+ filePath = path.join(
211
+ projectRoot,
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ (fileSource.source as any)?.[FILE_REF_PROP],
214
+ );
187
215
  await fs.access(filePath);
188
216
  } catch {
189
- console.log(
190
- picocolors.red("✘"),
191
- `File ${filePath} does not exist`,
192
- );
217
+ if (filePath) {
218
+ console.log(
219
+ picocolors.red("✘"),
220
+ `File ${filePath} does not exist`,
221
+ );
222
+ } else {
223
+ console.log(
224
+ picocolors.red("✘"),
225
+ `Expected file to be defined at: ${sourcePath} but no file was found`,
226
+ );
227
+ }
193
228
  errors += 1;
194
229
  continue;
195
230
  }
196
231
  }
197
232
  } else if (v.fixes.includes("keyof:check-keys")) {
198
- const prevErrors = errors;
199
233
  if (
200
234
  v.value &&
201
235
  typeof v.value === "object" &&
@@ -238,31 +272,274 @@ export async function validate({
238
272
  );
239
273
  errors += 1;
240
274
  }
241
- if (prevErrors < errors) {
275
+ } else if (
276
+ v.fixes.includes("image:upload-remote") ||
277
+ v.fixes.includes("file:upload-remote")
278
+ ) {
279
+ if (!fix) {
280
+ console.log(
281
+ picocolors.red("✘"),
282
+ `Remote file ${sourcePath} needs to be uploaded (use --fix to upload)`,
283
+ );
284
+ errors += 1;
285
+ continue;
286
+ }
287
+ const [, modulePath] =
288
+ Internal.splitModuleFilePathAndModulePath(
289
+ sourcePath as SourcePath,
290
+ );
291
+ if (valModule.source && valModule.schema) {
292
+ const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
293
+ modulePath,
294
+ valModule.source,
295
+ valModule.schema,
296
+ );
297
+ let filePath: string | null = null;
298
+ try {
299
+ filePath = path.join(
300
+ projectRoot,
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
+ (resolvedRemoteFileAtSourcePath.source as any)?.[
303
+ FILE_REF_PROP
304
+ ],
305
+ );
306
+ await fs.access(filePath);
307
+ } catch {
308
+ if (filePath) {
309
+ console.log(
310
+ picocolors.red("✘"),
311
+ `File ${filePath} does not exist`,
312
+ );
313
+ } else {
314
+ console.log(
315
+ picocolors.red("✘"),
316
+ `Expected file to be defined at: ${sourcePath} but no file was found`,
317
+ );
318
+ }
319
+ errors += 1;
320
+ continue;
321
+ }
322
+ const patFile = getPersonalAccessTokenPath(projectRoot);
323
+ try {
324
+ await fs.access(patFile);
325
+ } catch {
326
+ // TODO: display this error only once:
327
+ console.log(
328
+ picocolors.red("✘"),
329
+ `File: ${path.join(projectRoot, file)} has remote images that are not uploaded and you are not logged in.\n\nFix this error by logging in:\n\t"npx val login"\n`,
330
+ );
331
+ errors += 1;
332
+ continue;
333
+ }
334
+
335
+ const parsedPatFile = parsePersonalAccessTokenFile(
336
+ await fs.readFile(patFile, "utf-8"),
337
+ );
338
+ if (!parsedPatFile.success) {
339
+ console.log(
340
+ picocolors.red("✘"),
341
+ `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`,
342
+ );
343
+ errors += 1;
344
+ continue;
345
+ }
346
+ const { pat } = parsedPatFile.data;
347
+
348
+ if (remoteFiles[sourcePath as SourcePath]) {
349
+ console.log(
350
+ picocolors.yellow("⚠"),
351
+ `Remote file ${filePath} already uploaded`,
352
+ );
353
+ continue;
354
+ }
355
+ // TODO: parallelize this:
356
+ console.log(
357
+ picocolors.yellow("⚠"),
358
+ `Uploading remote file ${filePath}...`,
359
+ );
360
+
361
+ if (!resolvedRemoteFileAtSourcePath.schema) {
362
+ console.log(
363
+ picocolors.red("✘"),
364
+ `Cannot upload remote file: schema not found for ${sourcePath}`,
365
+ );
366
+ errors += 1;
367
+ continue;
368
+ }
369
+
370
+ const actualRemoteFileSource =
371
+ resolvedRemoteFileAtSourcePath.source;
372
+ const fileSourceMetadata = Internal.isFile(
373
+ actualRemoteFileSource,
374
+ )
375
+ ? actualRemoteFileSource.metadata
376
+ : undefined;
377
+ const resolveRemoteFileSchema =
378
+ resolvedRemoteFileAtSourcePath.schema;
379
+ if (!resolveRemoteFileSchema) {
380
+ console.log(
381
+ picocolors.red("✘"),
382
+ `Could not resolve schema for remote file: ${sourcePath}`,
383
+ );
384
+ errors += 1;
385
+ continue;
386
+ }
387
+ if (!publicProjectId || !remoteFileBuckets) {
388
+ let projectName = process.env.VAL_PROJECT;
389
+ if (!projectName) {
390
+ projectName = valConfigFile?.project;
391
+ }
392
+ if (!projectName) {
393
+ console.log(
394
+ picocolors.red("✘"),
395
+ "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config",
396
+ );
397
+ errors += 1;
398
+ continue;
399
+ }
400
+ const settingsRes = await getSettings(projectName, {
401
+ pat,
402
+ });
403
+ if (!settingsRes.success) {
404
+ console.log(
405
+ picocolors.red("✘"),
406
+ `Could not get public project id: ${settingsRes.message}.`,
407
+ );
408
+ errors += 1;
409
+ continue;
410
+ }
411
+ publicProjectId = settingsRes.data.publicProjectId;
412
+ remoteFileBuckets =
413
+ settingsRes.data.remoteFileBuckets.map((b) => b.bucket);
414
+ }
415
+ if (!publicProjectId) {
416
+ console.log(
417
+ picocolors.red("✘"),
418
+ "Could not get public project id",
419
+ );
420
+ errors += 1;
421
+ continue;
422
+ }
423
+ if (
424
+ resolveRemoteFileSchema.type !== "image" &&
425
+ resolveRemoteFileSchema.type !== "file"
426
+ ) {
427
+ console.log(
428
+ picocolors.red("✘"),
429
+ `The schema is the remote is neither image nor file: ${sourcePath}`,
430
+ );
431
+ }
432
+ remoteFilesCounter += 1;
433
+ const bucket =
434
+ remoteFileBuckets[
435
+ remoteFilesCounter % remoteFileBuckets.length
436
+ ];
437
+ if (!bucket) {
438
+ console.log(
439
+ picocolors.red("✘"),
440
+ `Internal error: could not allocate a bucket for the remote file located at ${sourcePath}`,
441
+ );
442
+ errors += 1;
443
+ continue;
444
+ }
445
+ let fileBuffer: Buffer;
446
+ try {
447
+ fileBuffer = await fs.readFile(filePath);
448
+ } catch (e) {
449
+ console.log(
450
+ picocolors.red("✘"),
451
+ `Error reading file: ${e}`,
452
+ );
453
+ errors += 1;
454
+ continue;
455
+ }
456
+ const relativeFilePath = path
457
+ .relative(projectRoot, filePath)
458
+ .split(path.sep)
459
+ .join("/") as `public/val/${string}`;
460
+ if (!relativeFilePath.startsWith("public/val/")) {
461
+ console.log(
462
+ picocolors.red("✘"),
463
+ `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`,
464
+ );
465
+ errors += 1;
466
+ continue;
467
+ }
468
+ const remoteFileUpload = await uploadRemoteFile(
469
+ valRemoteHost,
470
+ fileBuffer,
471
+ publicProjectId,
472
+ bucket,
473
+ relativeFilePath,
474
+ resolveRemoteFileSchema as
475
+ | SerializedFileSchema
476
+ | SerializedImageSchema,
477
+ fileSourceMetadata,
478
+ { pat },
479
+ );
480
+ if (!remoteFileUpload.success) {
481
+ console.log(
482
+ picocolors.red("✘"),
483
+ `Error uploading remote file: ${remoteFileUpload.error}`,
484
+ );
485
+ errors += 1;
486
+ continue;
487
+ }
488
+ console.log(
489
+ picocolors.yellow("⚠"),
490
+ `Uploaded remote file ${filePath}`,
491
+ );
492
+ remoteFiles[sourcePath as SourcePath] = {
493
+ ref: remoteFileUpload.ref,
494
+ metadata: fileSourceMetadata,
495
+ };
496
+ }
497
+ } else if (
498
+ v.fixes.includes("image:download-remote") ||
499
+ v.fixes.includes("file:download-remote")
500
+ ) {
501
+ if (fix) {
502
+ console.log(
503
+ picocolors.yellow("⚠"),
504
+ `Downloading remote file in ${sourcePath}...`,
505
+ );
506
+ } else {
242
507
  console.log(
243
508
  picocolors.red("✘"),
244
- "Found error in",
245
- `${sourcePath}`,
509
+ `Remote file ${sourcePath} needs to be downloaded (use --fix to download)`,
246
510
  );
511
+ errors += 1;
512
+ continue;
247
513
  }
514
+ } else if (
515
+ v.fixes.includes("image:check-remote") ||
516
+ v.fixes.includes("file:check-remote")
517
+ ) {
518
+ // skip
248
519
  } else {
249
520
  console.log(
250
521
  picocolors.red("✘"),
251
- "Found error in",
252
- `${sourcePath}:`,
253
- v.message,
522
+ "Unknown fix",
523
+ v.fixes,
524
+ "for",
525
+ sourcePath,
254
526
  );
255
527
  errors += 1;
528
+ continue;
256
529
  }
257
530
  const fixPatch = await createFixPatch(
258
- { projectRoot },
531
+ { projectRoot, remoteHost: valRemoteHost },
259
532
  !!fix,
260
533
  sourcePath as SourcePath,
261
534
  v,
535
+ remoteFiles,
536
+ valModule.source,
537
+ valModule.schema,
262
538
  );
263
539
  if (fix && fixPatch?.patch && fixPatch?.patch.length > 0) {
264
540
  await service.patch(moduleFilePath, fixPatch.patch);
265
541
  didFix = true;
542
+ fixedErrors += 1;
266
543
  console.log(
267
544
  picocolors.yellow("⚠"),
268
545
  "Applied fix for",
@@ -272,8 +549,10 @@ export async function validate({
272
549
  fixPatch?.remainingErrors?.forEach((e) => {
273
550
  errors += 1;
274
551
  console.log(
275
- v.fixes ? picocolors.yellow("⚠") : picocolors.red("✘"),
276
- `Found ${v.fixes ? "fixable " : ""}error in`,
552
+ e.fixes && e.fixes.length
553
+ ? picocolors.yellow("")
554
+ : picocolors.red("✘"),
555
+ `Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`,
277
556
  `${sourcePath}:`,
278
557
  e.message,
279
558
  );
@@ -282,7 +561,7 @@ export async function validate({
282
561
  errors += 1;
283
562
  console.log(
284
563
  picocolors.red("✘"),
285
- "Found error in",
564
+ "Got error in",
286
565
  `${sourcePath}:`,
287
566
  v.message,
288
567
  );
@@ -290,6 +569,16 @@ export async function validate({
290
569
  }
291
570
  }
292
571
  }
572
+ if (
573
+ fixedErrors === errors &&
574
+ (!valModule.errors.fatal || valModule.errors.fatal.length == 0)
575
+ ) {
576
+ console.log(
577
+ picocolors.green("✔"),
578
+ moduleFilePath,
579
+ "is valid (" + (Date.now() - start) + "ms)",
580
+ );
581
+ }
293
582
  for (const fatalError of valModule.errors.fatal || []) {
294
583
  errors += 1;
295
584
  console.log(
@@ -306,6 +595,13 @@ export async function validate({
306
595
  "is valid (" + (Date.now() - start) + "ms)",
307
596
  );
308
597
  }
598
+ if (errors > 0) {
599
+ console.log(
600
+ picocolors.red("✘"),
601
+ `${`/${file}`} contains ${errors} error${errors > 1 ? "s" : ""}`,
602
+ " (" + (Date.now() - start) + "ms)",
603
+ );
604
+ }
309
605
  return errors;
310
606
  }
311
607
  }
@@ -325,9 +621,9 @@ export async function validate({
325
621
  if (errors > 0) {
326
622
  console.log(
327
623
  picocolors.red("✘"),
328
- "Found",
624
+ "Got",
329
625
  errors,
330
- "validation error" + (errors > 1 ? "s" : ""),
626
+ "error" + (errors > 1 ? "s" : ""),
331
627
  );
332
628
  process.exit(1);
333
629
  } else {