@valbuild/cli 0.72.3 → 0.73.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.
@@ -1,12 +1,13 @@
1
1
  import meow from 'meow';
2
2
  import chalk from 'chalk';
3
3
  import path from 'path';
4
- import { createService, createFixPatch } from '@valbuild/server';
5
- import { Internal, FILE_REF_PROP, VAL_EXTENSION } from '@valbuild/core';
4
+ import { createService, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getSettings, uploadRemoteFile, createFixPatch } from '@valbuild/server';
5
+ import { DEFAULT_VAL_REMOTE_HOST, Internal, FILE_REF_PROP, VAL_EXTENSION } from '@valbuild/core';
6
6
  import { glob } from 'fast-glob';
7
7
  import picocolors from 'picocolors';
8
8
  import { ESLint } from 'eslint';
9
9
  import fs from 'fs/promises';
10
+ import fs$1 from 'fs';
10
11
 
11
12
  function error(message) {
12
13
  console.error(chalk.red("❌Error: ") + message);
@@ -17,6 +18,7 @@ async function validate({
17
18
  fix,
18
19
  noEslint
19
20
  }) {
21
+ const valRemoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
20
22
  const projectRoot = root ? path.resolve(root) : process.cwd();
21
23
  const eslint = new ESLint({
22
24
  cwd: projectRoot,
@@ -90,6 +92,7 @@ async function validate({
90
92
  console.log(errors === 0 ? picocolors.green("✔") : picocolors.red("✘"), "ESlint complete", lintFiles.length, "files");
91
93
  }
92
94
  console.log("Validating...", valFiles.length, "files");
95
+ let publicProjectId;
93
96
  let didFix = false; // TODO: ugly
94
97
  async function validateFile(file) {
95
98
  var _eslintResultsByFile;
@@ -102,6 +105,9 @@ async function validate({
102
105
  });
103
106
  const fileContent = await fs.readFile(path.join(projectRoot, file), "utf-8");
104
107
  const eslintResult = (_eslintResultsByFile = eslintResultsByFile) === null || _eslintResultsByFile === void 0 ? void 0 : _eslintResultsByFile[file];
108
+ const remoteFiles = {};
109
+ let remoteFileBuckets = null;
110
+ let remoteFilesCounter = 0;
105
111
  eslintResult === null || eslintResult === void 0 || eslintResult.messages.forEach(m => {
106
112
  // display surrounding code
107
113
  logEslintMessage(fileContent, moduleFilePath, m);
@@ -112,6 +118,7 @@ async function validate({
112
118
  } else {
113
119
  var _eslintResultsByFile2;
114
120
  let errors = ((_eslintResultsByFile2 = eslintResultsByFile) === null || _eslintResultsByFile2 === void 0 || (_eslintResultsByFile2 = _eslintResultsByFile2[file]) === null || _eslintResultsByFile2 === void 0 ? void 0 : _eslintResultsByFile2.messages.reduce((prev, m) => m.severity >= 2 ? prev + 1 : prev, 0)) || 0;
121
+ let fixedErrors = 0;
115
122
  if (valModule.errors) {
116
123
  if (valModule.errors.validation) {
117
124
  for (const [sourcePath, validationErrors] of Object.entries(valModule.errors.validation)) {
@@ -122,20 +129,24 @@ async function validate({
122
129
  ) || v.fixes.includes("image:check-metadata") || v.fixes.includes("image:add-metadata") || v.fixes.includes("file:check-metadata") || v.fixes.includes("file:add-metadata")) {
123
130
  const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
124
131
  if (valModule.source && valModule.schema) {
125
- var _fileSource$source;
126
132
  const fileSource = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
127
- const filePath = path.join(projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
- (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[FILE_REF_PROP]);
133
+ let filePath = null;
129
134
  try {
135
+ var _fileSource$source;
136
+ filePath = path.join(projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[FILE_REF_PROP]);
130
138
  await fs.access(filePath);
131
139
  } catch {
132
- console.log(picocolors.red("✘"), `File ${filePath} does not exist`);
140
+ if (filePath) {
141
+ console.log(picocolors.red("✘"), `File ${filePath} does not exist`);
142
+ } else {
143
+ console.log(picocolors.red("✘"), `Expected file to be defined at: ${sourcePath} but no file was found`);
144
+ }
133
145
  errors += 1;
134
146
  continue;
135
147
  }
136
148
  }
137
149
  } else if (v.fixes.includes("keyof:check-keys")) {
138
- const prevErrors = errors;
139
150
  if (v.value && typeof v.value === "object" && "key" in v.value && "sourcePath" in v.value) {
140
151
  const {
141
152
  key,
@@ -158,32 +169,193 @@ async function validate({
158
169
  console.log(picocolors.red("✘"), "Unexpected error in", `${sourcePath}:`, v.message, " (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)");
159
170
  errors += 1;
160
171
  }
161
- if (prevErrors < errors) {
162
- console.log(picocolors.red("✘"), "Found error in", `${sourcePath}`);
172
+ } else if (v.fixes.includes("image:upload-remote") || v.fixes.includes("file:upload-remote")) {
173
+ if (!fix) {
174
+ console.log(picocolors.red("✘"), `Remote file ${sourcePath} needs to be uploaded (use --fix to upload)`);
175
+ errors += 1;
176
+ continue;
163
177
  }
164
- } else {
165
- console.log(picocolors.red("✘"), "Found error in", `${sourcePath}:`, v.message);
178
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
179
+ if (valModule.source && valModule.schema) {
180
+ const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
181
+ let filePath = null;
182
+ console.log(sourcePath, resolvedRemoteFileAtSourcePath.source);
183
+ try {
184
+ var _resolvedRemoteFileAt;
185
+ filePath = path.join(projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[FILE_REF_PROP]);
187
+ await fs.access(filePath);
188
+ } catch {
189
+ if (filePath) {
190
+ console.log(picocolors.red("✘"), `File ${filePath} does not exist`);
191
+ } else {
192
+ console.log(picocolors.red("✘"), `Expected file to be defined at: ${sourcePath} but no file was found`);
193
+ }
194
+ errors += 1;
195
+ continue;
196
+ }
197
+ const patFile = getPersonalAccessTokenPath(projectRoot);
198
+ try {
199
+ await fs.access(patFile);
200
+ } catch {
201
+ // TODO: display this error only once:
202
+ console.log(picocolors.red("✘"), `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`);
203
+ errors += 1;
204
+ continue;
205
+ }
206
+ const parsedPatFile = parsePersonalAccessTokenFile(await fs.readFile(patFile, "utf-8"));
207
+ if (!parsedPatFile.success) {
208
+ console.log(picocolors.red("✘"), `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`);
209
+ errors += 1;
210
+ continue;
211
+ }
212
+ const {
213
+ pat
214
+ } = parsedPatFile.data;
215
+ if (remoteFiles[sourcePath]) {
216
+ console.log(picocolors.yellow("⚠"), `Remote file ${filePath} already uploaded`);
217
+ continue;
218
+ }
219
+ // TODO: parallelize this:
220
+ console.log(picocolors.yellow("⚠"), `Uploading remote file ${filePath}...`);
221
+ if (!resolvedRemoteFileAtSourcePath.schema) {
222
+ console.log(picocolors.red("✘"), `Cannot upload remote file: schema not found for ${sourcePath}`);
223
+ errors += 1;
224
+ continue;
225
+ }
226
+ const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
227
+ const fileSourceMetadata = Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
228
+ const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
229
+ if (!resolveRemoteFileSchema) {
230
+ console.log(picocolors.red("✘"), `Could not resolve schema for remote file: ${sourcePath}`);
231
+ errors += 1;
232
+ continue;
233
+ }
234
+ if (!publicProjectId || !remoteFileBuckets) {
235
+ let projectName = process.env.VAL_PROJECT;
236
+ if (!projectName) {
237
+ try {
238
+ var _require;
239
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
240
+ projectName = (_require = require(`${root}/val.config`)) === null || _require === void 0 || (_require = _require.config) === null || _require === void 0 ? void 0 : _require.project;
241
+ } catch {
242
+ // ignore
243
+ }
244
+ }
245
+ if (!projectName) {
246
+ try {
247
+ var _require2;
248
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
249
+ projectName = (_require2 = require(`${root}/val.config.ts`)) === null || _require2 === void 0 || (_require2 = _require2.config) === null || _require2 === void 0 ? void 0 : _require2.project;
250
+ } catch {
251
+ // ignore
252
+ }
253
+ }
254
+ if (!projectName) {
255
+ try {
256
+ var _require3;
257
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
258
+ projectName = (_require3 = require(`${root}/val.config.js`)) === null || _require3 === void 0 || (_require3 = _require3.config) === null || _require3 === void 0 ? void 0 : _require3.project;
259
+ } catch {
260
+ // ignore
261
+ }
262
+ }
263
+ if (!projectName) {
264
+ console.log(picocolors.red("✘"), "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config");
265
+ errors += 1;
266
+ continue;
267
+ }
268
+ const settingsRes = await getSettings(projectName, {
269
+ pat
270
+ });
271
+ if (!settingsRes.success) {
272
+ console.log(picocolors.red("✘"), `Could not get public project id: ${settingsRes.message}.`);
273
+ errors += 1;
274
+ continue;
275
+ }
276
+ publicProjectId = settingsRes.data.publicProjectId;
277
+ remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
278
+ }
279
+ if (!publicProjectId) {
280
+ console.log(picocolors.red("✘"), "Could not get public project id");
281
+ errors += 1;
282
+ continue;
283
+ }
284
+ if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
285
+ console.log(picocolors.red("✘"), `The schema is the remote is neither image nor file: ${sourcePath}`);
286
+ }
287
+ remoteFilesCounter += 1;
288
+ const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
289
+ if (!bucket) {
290
+ console.log(picocolors.red("✘"), `Internal error: could not allocate a bucket for the remote file located at ${sourcePath}`);
291
+ errors += 1;
292
+ continue;
293
+ }
294
+ let fileBuffer;
295
+ try {
296
+ fileBuffer = await fs.readFile(filePath);
297
+ } catch (e) {
298
+ console.log(picocolors.red("✘"), `Error reading file: ${e}`);
299
+ errors += 1;
300
+ continue;
301
+ }
302
+ const relativeFilePath = path.relative(projectRoot, filePath).split(path.sep).join("/");
303
+ if (!relativeFilePath.startsWith("public/val/")) {
304
+ console.log(picocolors.red("✘"), `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`);
305
+ errors += 1;
306
+ continue;
307
+ }
308
+ const remoteFileUpload = await uploadRemoteFile(valRemoteHost, fileBuffer, publicProjectId, bucket, relativeFilePath, resolveRemoteFileSchema, fileSourceMetadata, {
309
+ pat
310
+ });
311
+ if (!remoteFileUpload.success) {
312
+ console.log(picocolors.red("✘"), `Error uploading remote file: ${remoteFileUpload.error}`);
313
+ errors += 1;
314
+ continue;
315
+ }
316
+ console.log(picocolors.yellow("⚠"), `Uploaded remote file ${filePath}`);
317
+ remoteFiles[sourcePath] = {
318
+ ref: remoteFileUpload.ref,
319
+ metadata: fileSourceMetadata
320
+ };
321
+ }
322
+ } else if (v.fixes.includes("image:download-remote") || v.fixes.includes("file:download-remote")) {
323
+ if (fix) {
324
+ console.log(picocolors.yellow("⚠"), `Downloading remote file in ${sourcePath}...`);
325
+ } else {
326
+ console.log(picocolors.red("✘"), `Remote file ${sourcePath} needs to be downloaded (use --fix to download)`);
327
+ errors += 1;
328
+ continue;
329
+ }
330
+ } else if (v.fixes.includes("image:check-remote") || v.fixes.includes("file:check-remote")) ; else {
331
+ console.log(picocolors.red("✘"), "Unknown fix", v.fixes, "for", sourcePath);
166
332
  errors += 1;
333
+ continue;
167
334
  }
168
335
  const fixPatch = await createFixPatch({
169
- projectRoot
170
- }, !!fix, sourcePath, v);
336
+ projectRoot,
337
+ remoteHost: valRemoteHost
338
+ }, !!fix, sourcePath, v, remoteFiles, valModule.source, valModule.schema);
171
339
  if (fix && fixPatch !== null && fixPatch !== void 0 && fixPatch.patch && (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.patch.length) > 0) {
172
340
  await service.patch(moduleFilePath, fixPatch.patch);
173
341
  didFix = true;
342
+ fixedErrors += 1;
174
343
  console.log(picocolors.yellow("⚠"), "Applied fix for", sourcePath);
175
344
  }
176
345
  fixPatch === null || fixPatch === void 0 || (_fixPatch$remainingEr = fixPatch.remainingErrors) === null || _fixPatch$remainingEr === void 0 || _fixPatch$remainingEr.forEach(e => {
177
346
  errors += 1;
178
- console.log(v.fixes ? picocolors.yellow("⚠") : picocolors.red("✘"), `Found ${v.fixes ? "fixable " : ""}error in`, `${sourcePath}:`, e.message);
347
+ console.log(e.fixes && e.fixes.length ? picocolors.yellow("⚠") : picocolors.red("✘"), `Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`, `${sourcePath}:`, e.message);
179
348
  });
180
349
  } else {
181
350
  errors += 1;
182
- console.log(picocolors.red("✘"), "Found error in", `${sourcePath}:`, v.message);
351
+ console.log(picocolors.red("✘"), "Got error in", `${sourcePath}:`, v.message);
183
352
  }
184
353
  }
185
354
  }
186
355
  }
356
+ if (fixedErrors === errors && (!valModule.errors.fatal || valModule.errors.fatal.length == 0)) {
357
+ console.log(picocolors.green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
358
+ }
187
359
  for (const fatalError of valModule.errors.fatal || []) {
188
360
  errors += 1;
189
361
  console.log(picocolors.red("✘"), moduleFilePath, "is invalid:", fatalError.message);
@@ -191,6 +363,9 @@ async function validate({
191
363
  } else {
192
364
  console.log(picocolors.green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
193
365
  }
366
+ if (errors > 0) {
367
+ console.log(picocolors.red("✘"), `${`/${file}`} contains ${errors} error${errors > 1 ? "s" : ""}`, " (" + (Date.now() - start) + "ms)");
368
+ }
194
369
  return errors;
195
370
  }
196
371
  }
@@ -208,7 +383,7 @@ async function validate({
208
383
  }
209
384
  }
210
385
  if (errors > 0) {
211
- console.log(picocolors.red("✘"), "Found", errors, "validation error" + (errors > 1 ? "s" : ""));
386
+ console.log(picocolors.red("✘"), "Got", errors, "error" + (errors > 1 ? "s" : ""));
212
387
  process.exit(1);
213
388
  } else {
214
389
  console.log(picocolors.green("✔"), "No validation errors found");
@@ -334,8 +509,8 @@ function isFileRef(value) {
334
509
  return false;
335
510
  }
336
511
 
337
- const getVersions = async () => {
338
- const coreVersion = await (() => {
512
+ const getVersions = () => {
513
+ const coreVersion = (() => {
339
514
  try {
340
515
  var _require;
341
516
  // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
@@ -344,7 +519,7 @@ const getVersions = async () => {
344
519
  return null;
345
520
  }
346
521
  })();
347
- const nextVersion = await (() => {
522
+ const nextVersion = (() => {
348
523
  try {
349
524
  var _require2;
350
525
  // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
@@ -359,6 +534,83 @@ const getVersions = async () => {
359
534
  };
360
535
  };
361
536
 
537
+ const host = process.env.VAL_BUILD_URL || "https://app.val.build";
538
+ async function login(options) {
539
+ try {
540
+ var _response$headers$get;
541
+ console.log(picocolors.cyan("\nStarting login process...\n"));
542
+
543
+ // Step 1: Initiate login and get token and URL
544
+ const response = await fetch(`${host}/api/login`, {
545
+ method: "POST",
546
+ headers: {
547
+ "Content-Type": "application/json"
548
+ }
549
+ });
550
+ let token;
551
+ let url;
552
+ if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
553
+ const text = await response.text();
554
+ console.error(picocolors.red("Unexpected failure while trying to login (content type was not JSON). Server response:"), text || "<empty>");
555
+ process.exit(1);
556
+ }
557
+ const json = await response.json();
558
+ if (json) {
559
+ token = json.nonce;
560
+ url = json.url;
561
+ }
562
+ if (!token || !url) {
563
+ console.error(picocolors.red("Unexpected response from the server."), json);
564
+ process.exit(1);
565
+ }
566
+ console.log(picocolors.green("Open the following URL in your browser to log in:"));
567
+ console.log(picocolors.underline(picocolors.blue(url)));
568
+ console.log(picocolors.dim("\nWaiting for login confirmation...\n"));
569
+
570
+ // Step 2: Poll for login confirmation
571
+ const result = await pollForConfirmation(token);
572
+
573
+ // Step 3: Save the token
574
+ const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
575
+ saveToken(result, filePath);
576
+ } catch (error) {
577
+ console.error(picocolors.red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
578
+ process.exit(1);
579
+ }
580
+ }
581
+ const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
582
+ async function pollForConfirmation(token) {
583
+ const start = Date.now();
584
+ while (Date.now() - start < MAX_DURATION) {
585
+ await new Promise(resolve => setTimeout(resolve, 1000));
586
+ const response = await fetch(`${host}/api/login?token=${token}&consume=true`);
587
+ if (response.status === 500) {
588
+ console.error(picocolors.red("An error occurred on the server."));
589
+ process.exit(1);
590
+ }
591
+ if (response.status === 200) {
592
+ const json = await response.json();
593
+ if (json) {
594
+ if (typeof json.profile.username === "string" && typeof json.pat === "string") {
595
+ return json;
596
+ } else {
597
+ console.error(picocolors.red("Unexpected response from the server."));
598
+ process.exit(1);
599
+ }
600
+ }
601
+ }
602
+ }
603
+ console.error(picocolors.red("Login confirmation timed out."));
604
+ process.exit(1);
605
+ }
606
+ function saveToken(result, filePath) {
607
+ fs$1.mkdirSync(path.dirname(filePath), {
608
+ recursive: true
609
+ });
610
+ fs$1.writeFileSync(filePath, JSON.stringify(result, null, 2));
611
+ console.log(picocolors.green(`Token for ${picocolors.cyan(result.profile.username)} saved to ${picocolors.cyan(filePath)}`));
612
+ }
613
+
362
614
  async function main() {
363
615
  const {
364
616
  input,
@@ -373,6 +625,7 @@ async function main() {
373
625
 
374
626
  Commands:
375
627
  validate
628
+ login
376
629
  list-files
377
630
  versions
378
631
 
@@ -383,6 +636,12 @@ async function main() {
383
636
  --fix [fix] Attempt to fix validation errors
384
637
  --noEslint [noEslint] Disable eslint validation during validate
385
638
 
639
+
640
+ Command: login
641
+ Description: login to app.val.build and generate a Personal Access Token
642
+ Options:
643
+ --root [root], -r [root] Set project root directory (default process.cwd())
644
+
386
645
 
387
646
  Command: files
388
647
  Description: EXPERIMENTAL.
@@ -436,6 +695,10 @@ async function main() {
436
695
  });
437
696
  case "versions":
438
697
  return versions();
698
+ case "login":
699
+ return login({
700
+ root: flags.root
701
+ });
439
702
  case "validate":
440
703
  case "idate":
441
704
  if (flags.managedDir) {
@@ -455,7 +718,7 @@ void main().catch(err => {
455
718
  process.exitCode = 1;
456
719
  });
457
720
  async function versions() {
458
- const foundVersions = await getVersions();
721
+ const foundVersions = getVersions();
459
722
  console.log(`${chalk.cyan("@valbuild/core")}: ${foundVersions.coreVersion}`);
460
723
  console.log(`${chalk.cyan("@valbuild/next")}: ${foundVersions.nextVersion}`);
461
724
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.72.3",
4
+ "version": "0.73.0",
5
5
  "description": "Val CLI tools",
6
6
  "bin": {
7
7
  "val": "./bin.js"
@@ -18,9 +18,9 @@
18
18
  "typecheck": "tsc --noEmit"
19
19
  },
20
20
  "dependencies": {
21
- "@valbuild/core": "~0.72.3",
22
- "@valbuild/server": "~0.72.3",
23
- "@valbuild/eslint-plugin": "~0.72.3",
21
+ "@valbuild/core": "~0.73.0",
22
+ "@valbuild/server": "~0.73.0",
23
+ "@valbuild/eslint-plugin": "~0.73.0",
24
24
  "eslint": "^8.31.0",
25
25
  "@inquirer/confirm": "^2.0.15",
26
26
  "@inquirer/prompts": "^3.0.2",
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { validate } from "./validate";
4
4
  import { files as files } from "./files";
5
5
  import { getVersions } from "./getVersions";
6
6
  import chalk from "chalk";
7
+ import { login } from "./login";
7
8
 
8
9
  async function main(): Promise<void> {
9
10
  const { input, flags, showHelp } = meow(
@@ -16,6 +17,7 @@ async function main(): Promise<void> {
16
17
 
17
18
  Commands:
18
19
  validate
20
+ login
19
21
  list-files
20
22
  versions
21
23
 
@@ -26,6 +28,12 @@ async function main(): Promise<void> {
26
28
  --fix [fix] Attempt to fix validation errors
27
29
  --noEslint [noEslint] Disable eslint validation during validate
28
30
 
31
+
32
+ Command: login
33
+ Description: login to app.val.build and generate a Personal Access Token
34
+ Options:
35
+ --root [root], -r [root] Set project root directory (default process.cwd())
36
+
29
37
 
30
38
  Command: files
31
39
  Description: EXPERIMENTAL.
@@ -86,6 +94,10 @@ async function main(): Promise<void> {
86
94
  });
87
95
  case "versions":
88
96
  return versions();
97
+ case "login":
98
+ return login({
99
+ root: flags.root,
100
+ });
89
101
  case "validate":
90
102
  case "idate":
91
103
  if (flags.managedDir) {
@@ -113,7 +125,7 @@ void main().catch((err) => {
113
125
  });
114
126
 
115
127
  async function versions() {
116
- const foundVersions = await getVersions();
128
+ const foundVersions = getVersions();
117
129
  console.log(`${chalk.cyan("@valbuild/core")}: ${foundVersions.coreVersion}`);
118
130
  console.log(`${chalk.cyan("@valbuild/next")}: ${foundVersions.nextVersion}`);
119
131
  }
@@ -1,8 +1,8 @@
1
- export const getVersions = async (): Promise<{
1
+ export const getVersions = (): {
2
2
  coreVersion?: string;
3
3
  nextVersion?: string;
4
- }> => {
5
- const coreVersion = await (() => {
4
+ } => {
5
+ const coreVersion = (() => {
6
6
  try {
7
7
  // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
8
8
  return require("@valbuild/core")?.Internal?.VERSION?.core;
@@ -10,7 +10,7 @@ export const getVersions = async (): Promise<{
10
10
  return null;
11
11
  }
12
12
  })();
13
- const nextVersion = await (() => {
13
+ const nextVersion = (() => {
14
14
  try {
15
15
  // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
16
16
  return require("@valbuild/next")?.Internal?.VERSION?.next;
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,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
+ }