@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.
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +282 -18
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +282 -18
- package/cli/dist/valbuild-cli-cli.esm.js +283 -20
- package/package.json +4 -4
- package/src/cli.ts +13 -1
- package/src/getVersions.ts +4 -4
- package/src/login.ts +112 -0
- package/src/utils/getFileExt.ts +4 -0
- package/src/utils/getValCoreVersion.ts +5 -0
- package/src/validate.ts +339 -23
@@ -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
|
-
|
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
|
-
|
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
|
-
|
162
|
-
|
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
|
-
|
165
|
-
|
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
|
-
|
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(
|
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("✘"), "
|
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("✘"), "
|
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 =
|
338
|
-
const coreVersion =
|
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 =
|
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 =
|
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.
|
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.
|
22
|
-
"@valbuild/server": "~0.
|
23
|
-
"@valbuild/eslint-plugin": "~0.
|
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 =
|
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
|
}
|
package/src/getVersions.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
export const getVersions =
|
1
|
+
export const getVersions = (): {
|
2
2
|
coreVersion?: string;
|
3
3
|
nextVersion?: string;
|
4
|
-
}
|
5
|
-
const coreVersion =
|
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 =
|
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
|
+
}
|