@valbuild/cli 0.97.6 → 0.98.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 +73 -172
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +73 -172
- package/cli/dist/valbuild-cli-cli.esm.js +72 -169
- package/package.json +5 -4
- package/src/cli.ts +11 -5
- package/src/connect.ts +2 -4
- package/src/debug/context.ts +2 -4
- package/src/listUnusedFiles.ts +2 -4
- package/src/login.ts +29 -94
- package/src/lsp.ts +19 -0
- package/src/utils/evalValConfigFile.ts +6 -100
- package/src/validate.ts +2 -4
|
@@ -2,16 +2,13 @@ import meow from 'meow';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import fs from 'fs/promises';
|
|
5
|
+
import fs$1 from 'fs/promises';
|
|
6
6
|
import { glob } from 'fast-glob';
|
|
7
7
|
import { Internal, FILE_REF_PROP, DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST, VAL_EXTENSION } from '@valbuild/core';
|
|
8
|
-
import { createService, createFixPatch, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getModulePathRange, createModulePathMap, getSettings, uploadRemoteFile, loadValModules, safeReadGit, ValOpsHttp, ValOpsFS, formatPatchSourceError } from '@valbuild/server';
|
|
9
|
-
import vm from 'node:vm';
|
|
10
|
-
import ts from 'typescript';
|
|
11
|
-
import z from 'zod';
|
|
12
|
-
import { createRequire } from 'node:module';
|
|
8
|
+
import { createService, createFixPatch, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getModulePathRange, createModulePathMap, findAndEvalValConfigFile, getSettings, uploadRemoteFile, startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, loadValModules, safeReadGit, ValOpsHttp, ValOpsFS, formatPatchSourceError } from '@valbuild/server';
|
|
13
9
|
import { resolveSchemaSourceFixes } from '@valbuild/shared/internal';
|
|
14
|
-
import
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
import fs from 'fs';
|
|
15
12
|
import JSZip from 'jszip';
|
|
16
13
|
import readline from 'readline';
|
|
17
14
|
|
|
@@ -30,76 +27,6 @@ function info(message, opts = {}) {
|
|
|
30
27
|
console.log(chalk.blue("️ℹ️ : ") + message);
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
const ValConfigSchema = z.object({
|
|
34
|
-
project: z.string().optional(),
|
|
35
|
-
root: z.string().optional(),
|
|
36
|
-
files: z.object({
|
|
37
|
-
directory: z.string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
|
|
38
|
-
// Reject path traversal so the directory cannot escape /public
|
|
39
|
-
!val.split("/").some(segment => segment === "." || segment === ".."), {
|
|
40
|
-
message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
|
|
41
|
-
})
|
|
42
|
-
}).optional(),
|
|
43
|
-
gitCommit: z.string().optional(),
|
|
44
|
-
gitBranch: z.string().optional(),
|
|
45
|
-
defaultTheme: z.union([z.literal("light"), z.literal("dark")]).optional(),
|
|
46
|
-
ai: z.object({
|
|
47
|
-
commitMessages: z.object({
|
|
48
|
-
disabled: z.boolean().optional()
|
|
49
|
-
}).optional()
|
|
50
|
-
}).optional()
|
|
51
|
-
});
|
|
52
|
-
async function evalValConfigFile(projectRoot, configFileName) {
|
|
53
|
-
const valConfigPath = path.join(projectRoot, configFileName);
|
|
54
|
-
let code = null;
|
|
55
|
-
try {
|
|
56
|
-
code = await fs.readFile(valConfigPath, "utf-8");
|
|
57
|
-
} catch {
|
|
58
|
-
//
|
|
59
|
-
}
|
|
60
|
-
if (!code) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const transpiled = ts.transpileModule(code, {
|
|
64
|
-
compilerOptions: {
|
|
65
|
-
target: ts.ScriptTarget.ES2020,
|
|
66
|
-
module: ts.ModuleKind.CommonJS,
|
|
67
|
-
esModuleInterop: true
|
|
68
|
-
},
|
|
69
|
-
fileName: valConfigPath
|
|
70
|
-
});
|
|
71
|
-
const projectRootRequire = createRequire(valConfigPath);
|
|
72
|
-
const exportsObj = {};
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
-
const sandbox = {
|
|
75
|
-
exports: exportsObj,
|
|
76
|
-
module: {
|
|
77
|
-
exports: exportsObj
|
|
78
|
-
},
|
|
79
|
-
require: projectRootRequire,
|
|
80
|
-
// NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
|
|
81
|
-
__filename: valConfigPath,
|
|
82
|
-
__dirname: projectRoot,
|
|
83
|
-
console,
|
|
84
|
-
process
|
|
85
|
-
};
|
|
86
|
-
sandbox.global = sandbox;
|
|
87
|
-
const context = vm.createContext(sandbox);
|
|
88
|
-
const script = new vm.Script(transpiled.outputText, {
|
|
89
|
-
filename: valConfigPath
|
|
90
|
-
});
|
|
91
|
-
script.runInContext(context);
|
|
92
|
-
const valConfig = sandbox.module.exports.config;
|
|
93
|
-
if (!valConfig) {
|
|
94
|
-
throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
|
|
95
|
-
}
|
|
96
|
-
const result = ValConfigSchema.safeParse(valConfig);
|
|
97
|
-
if (!result.success) {
|
|
98
|
-
throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
|
|
99
|
-
}
|
|
100
|
-
return result.data;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
30
|
function getFileExt(filePath) {
|
|
104
31
|
// 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).
|
|
105
32
|
return filePath.split(".").pop() || "";
|
|
@@ -477,15 +404,15 @@ function createDefaultValFSHost() {
|
|
|
477
404
|
return {
|
|
478
405
|
...ts.sys,
|
|
479
406
|
writeFile: (fileName, data, encoding) => {
|
|
480
|
-
fs
|
|
407
|
+
fs.mkdirSync(path.dirname(fileName), {
|
|
481
408
|
recursive: true
|
|
482
409
|
});
|
|
483
|
-
fs
|
|
410
|
+
fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
|
484
411
|
},
|
|
485
|
-
rmFile: fs
|
|
412
|
+
rmFile: fs.rmSync,
|
|
486
413
|
readBuffer: fileName => {
|
|
487
414
|
try {
|
|
488
|
-
return fs
|
|
415
|
+
return fs.readFileSync(fileName);
|
|
489
416
|
} catch {
|
|
490
417
|
return undefined;
|
|
491
418
|
}
|
|
@@ -826,7 +753,7 @@ function getCachedFile(moduleFilePath, projectRoot, cache) {
|
|
|
826
753
|
const filePath = path.join(projectRoot, moduleFilePath);
|
|
827
754
|
let fileContent;
|
|
828
755
|
try {
|
|
829
|
-
fileContent = fs
|
|
756
|
+
fileContent = fs.readFileSync(filePath, "utf-8");
|
|
830
757
|
} catch {
|
|
831
758
|
cache.set(moduleFilePath, null);
|
|
832
759
|
return null;
|
|
@@ -857,7 +784,7 @@ async function validate({
|
|
|
857
784
|
// errors found. Re-reads config and val files each call so it always reflects
|
|
858
785
|
// the latest state on disk (used both for one-shot and watch mode).
|
|
859
786
|
async function runOnce() {
|
|
860
|
-
const valConfigFile =
|
|
787
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
861
788
|
const resolvedValConfigFile = valConfigFile ? {
|
|
862
789
|
...valConfigFile,
|
|
863
790
|
project: process.env.VAL_PROJECT || valConfigFile.project
|
|
@@ -1037,11 +964,11 @@ async function validate({
|
|
|
1037
964
|
if (prettier) {
|
|
1038
965
|
for (const file of fixedFiles) {
|
|
1039
966
|
const filePath = path.join(projectRoot, file);
|
|
1040
|
-
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
967
|
+
const fileContent = await fs$1.readFile(filePath, "utf-8");
|
|
1041
968
|
const formattedContent = await prettier.format(fileContent, {
|
|
1042
969
|
filepath: filePath
|
|
1043
970
|
});
|
|
1044
|
-
await fs.writeFile(filePath, formattedContent);
|
|
971
|
+
await fs$1.writeFile(filePath, formattedContent);
|
|
1045
972
|
}
|
|
1046
973
|
}
|
|
1047
974
|
|
|
@@ -1162,7 +1089,7 @@ async function listUnusedFiles({
|
|
|
1162
1089
|
}) {
|
|
1163
1090
|
var _valConfigFile$files;
|
|
1164
1091
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
1165
|
-
const valConfigFile =
|
|
1092
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
1166
1093
|
// Strip the leading "/" so it is relative to the project root (e.g. "public/val").
|
|
1167
1094
|
const managedDir = ((valConfigFile === null || valConfigFile === void 0 || (_valConfigFile$files = valConfigFile.files) === null || _valConfigFile$files === void 0 ? void 0 : _valConfigFile$files.directory) ?? "/public/val").replace(/^\//, "");
|
|
1168
1095
|
const service = await createService(projectRoot);
|
|
@@ -1251,7 +1178,7 @@ const getVersions = () => {
|
|
|
1251
1178
|
};
|
|
1252
1179
|
};
|
|
1253
1180
|
|
|
1254
|
-
const host
|
|
1181
|
+
const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
|
|
1255
1182
|
async function connect(options) {
|
|
1256
1183
|
const {
|
|
1257
1184
|
root
|
|
@@ -1269,11 +1196,11 @@ async function connect(options) {
|
|
|
1269
1196
|
if (maybeGitOwnerAndRepo) {
|
|
1270
1197
|
params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
|
|
1271
1198
|
}
|
|
1272
|
-
const url = `${host
|
|
1199
|
+
const url = `${host}/connect?${params.toString()}`;
|
|
1273
1200
|
console.log(pc.dim(`\nFollow the instructions in your browser to complete the setup:\n${pc.cyan(url)}\n`));
|
|
1274
1201
|
}
|
|
1275
1202
|
async function tryGetProject(projectRoot) {
|
|
1276
|
-
const valConfigFile =
|
|
1203
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
1277
1204
|
if (valConfigFile && valConfigFile.project) {
|
|
1278
1205
|
const parts = valConfigFile.project.split("/");
|
|
1279
1206
|
if (parts.length === 2) {
|
|
@@ -1326,8 +1253,8 @@ function tryGetGitConfig(root) {
|
|
|
1326
1253
|
let lastDir = null;
|
|
1327
1254
|
while (currentDir !== lastDir) {
|
|
1328
1255
|
const gitConfigPath = path.join(currentDir, ".git", "config");
|
|
1329
|
-
if (fs
|
|
1330
|
-
return fs
|
|
1256
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
1257
|
+
return fs.readFileSync(gitConfigPath, "utf-8");
|
|
1331
1258
|
}
|
|
1332
1259
|
lastDir = currentDir;
|
|
1333
1260
|
currentDir = path.dirname(currentDir);
|
|
@@ -1338,83 +1265,54 @@ function tryGetGitConfig(root) {
|
|
|
1338
1265
|
return null;
|
|
1339
1266
|
}
|
|
1340
1267
|
|
|
1341
|
-
|
|
1268
|
+
/**
|
|
1269
|
+
* Terminal wrapper around the login device flow in `@valbuild/server`.
|
|
1270
|
+
* The flow itself (polling, token persistence) is shared with
|
|
1271
|
+
* `@valbuild/language-server`.
|
|
1272
|
+
*/
|
|
1342
1273
|
async function login(options) {
|
|
1343
1274
|
try {
|
|
1344
|
-
var _response$headers$get;
|
|
1345
1275
|
console.log(pc.cyan("\nStarting login process...\n"));
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
headers: {
|
|
1351
|
-
"Content-Type": "application/json"
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
let token;
|
|
1355
|
-
let url;
|
|
1356
|
-
if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
|
|
1357
|
-
const text = await response.text();
|
|
1358
|
-
console.error(pc.red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
1359
|
-
process.exit(1);
|
|
1360
|
-
}
|
|
1361
|
-
const json = await response.json();
|
|
1362
|
-
if (json) {
|
|
1363
|
-
token = json.nonce;
|
|
1364
|
-
url = json.url;
|
|
1365
|
-
}
|
|
1366
|
-
if (!token || !url) {
|
|
1367
|
-
console.error(pc.red("Unexpected response from the server."), json);
|
|
1368
|
-
process.exit(1);
|
|
1369
|
-
}
|
|
1276
|
+
const {
|
|
1277
|
+
nonce,
|
|
1278
|
+
url
|
|
1279
|
+
} = await startValLogin();
|
|
1370
1280
|
console.log(pc.green("Open the following URL in your browser to log in:"));
|
|
1371
1281
|
console.log(pc.underline(pc.blue(url)));
|
|
1372
1282
|
console.log(pc.dim("\nWaiting for login confirmation...\n"));
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
// Step 3: Save the token
|
|
1378
|
-
const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
|
|
1379
|
-
saveToken(result, filePath);
|
|
1283
|
+
const result = await awaitValLoginConfirmation(nonce);
|
|
1284
|
+
const filePath = persistPersonalAccessToken(options.root || process.cwd(), result);
|
|
1285
|
+
console.log(pc.green(`Token for ${pc.cyan(result.profile.email)} saved to ${pc.cyan(filePath)}`));
|
|
1380
1286
|
} catch (error) {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
1386
|
-
async function pollForConfirmation(token) {
|
|
1387
|
-
const start = Date.now();
|
|
1388
|
-
while (Date.now() - start < MAX_DURATION) {
|
|
1389
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1390
|
-
const response = await fetch(`${host}/api/login?token=${token}&consume=true`, {
|
|
1391
|
-
method: "POST"
|
|
1392
|
-
});
|
|
1393
|
-
if (response.status === 500) {
|
|
1394
|
-
console.error(pc.red("An error occurred on the server."));
|
|
1287
|
+
if (error instanceof ValLoginError) {
|
|
1288
|
+
console.error(pc.red(error.message), error.details ?? "");
|
|
1395
1289
|
process.exit(1);
|
|
1396
1290
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (json) {
|
|
1400
|
-
if (typeof json.profile.email === "string" && typeof json.pat === "string") {
|
|
1401
|
-
return json;
|
|
1402
|
-
} else {
|
|
1403
|
-
console.error(pc.red("Unexpected response from the server."));
|
|
1404
|
-
process.exit(1);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1291
|
+
console.error(pc.red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
|
|
1292
|
+
process.exit(1);
|
|
1408
1293
|
}
|
|
1409
|
-
console.error(pc.red("Login confirmation timed out."));
|
|
1410
|
-
process.exit(1);
|
|
1411
1294
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Start the Val language server.
|
|
1298
|
+
*
|
|
1299
|
+
* Editors normally resolve `@valbuild/language-server` from the project's
|
|
1300
|
+
* `node_modules` and launch its `val-language-server` binary directly. This
|
|
1301
|
+
* subcommand exists so the server is discoverable from the CLI and easy to
|
|
1302
|
+
* start by hand when debugging an editor integration:
|
|
1303
|
+
*
|
|
1304
|
+
* npx val lsp --stdio
|
|
1305
|
+
*
|
|
1306
|
+
* The import is dynamic so that `vscode-languageserver` is not loaded on every
|
|
1307
|
+
* `val` invocation.
|
|
1308
|
+
*/
|
|
1309
|
+
async function lsp() {
|
|
1310
|
+
const {
|
|
1311
|
+
main
|
|
1312
|
+
} = await import('@valbuild/language-server');
|
|
1313
|
+
// Transport (`--stdio`, `--node-ipc`, `--socket=`) is read from argv by the
|
|
1314
|
+
// server itself, so the flags pass straight through.
|
|
1315
|
+
main();
|
|
1418
1316
|
}
|
|
1419
1317
|
|
|
1420
1318
|
/**
|
|
@@ -1431,10 +1329,10 @@ class DebugContextError extends Error {}
|
|
|
1431
1329
|
async function createDebugContext(options) {
|
|
1432
1330
|
var _config$files;
|
|
1433
1331
|
const projectRoot = options.root ? path.resolve(options.root) : process.cwd();
|
|
1434
|
-
if (!fs
|
|
1332
|
+
if (!fs.existsSync(projectRoot)) {
|
|
1435
1333
|
throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
|
|
1436
1334
|
}
|
|
1437
|
-
const config =
|
|
1335
|
+
const config = await findAndEvalValConfigFile(projectRoot);
|
|
1438
1336
|
if (!config) {
|
|
1439
1337
|
throw new DebugContextError(`Could not find val.config.ts nor val.config.js in: ${projectRoot}`);
|
|
1440
1338
|
}
|
|
@@ -1501,8 +1399,8 @@ async function createDebugContext(options) {
|
|
|
1501
1399
|
*/
|
|
1502
1400
|
function readAuth(projectRoot) {
|
|
1503
1401
|
const patFile = getPersonalAccessTokenPath(projectRoot);
|
|
1504
|
-
if (fs
|
|
1505
|
-
const contents = fs
|
|
1402
|
+
if (fs.existsSync(patFile)) {
|
|
1403
|
+
const contents = fs.readFileSync(patFile, "utf-8");
|
|
1506
1404
|
const parsed = parsePersonalAccessTokenFile(contents);
|
|
1507
1405
|
if (parsed.success) {
|
|
1508
1406
|
return {
|
|
@@ -2049,7 +1947,7 @@ function readProjectValVersions(projectRoot) {
|
|
|
2049
1947
|
function readLocalFile(projectRoot, projectRelativePath) {
|
|
2050
1948
|
const absPath = path.join(projectRoot, projectRelativePath);
|
|
2051
1949
|
try {
|
|
2052
|
-
return fs
|
|
1950
|
+
return fs.readFileSync(absPath, "utf-8");
|
|
2053
1951
|
} catch {
|
|
2054
1952
|
return null;
|
|
2055
1953
|
}
|
|
@@ -2191,10 +2089,10 @@ async function writeZip(outPath, entries) {
|
|
|
2191
2089
|
type: "nodebuffer",
|
|
2192
2090
|
compression: "DEFLATE"
|
|
2193
2091
|
});
|
|
2194
|
-
fs
|
|
2092
|
+
fs.mkdirSync(path.dirname(outPath), {
|
|
2195
2093
|
recursive: true
|
|
2196
2094
|
});
|
|
2197
|
-
fs
|
|
2095
|
+
fs.writeFileSync(outPath, buffer);
|
|
2198
2096
|
}
|
|
2199
2097
|
function timestamp() {
|
|
2200
2098
|
return new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
|
|
@@ -2331,6 +2229,7 @@ async function main() {
|
|
|
2331
2229
|
files
|
|
2332
2230
|
connect
|
|
2333
2231
|
versions
|
|
2232
|
+
lsp
|
|
2334
2233
|
debug
|
|
2335
2234
|
delete-unappliable-patches
|
|
2336
2235
|
|
|
@@ -2360,6 +2259,13 @@ async function main() {
|
|
|
2360
2259
|
Options:
|
|
2361
2260
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
2362
2261
|
|
|
2262
|
+
Command: lsp
|
|
2263
|
+
Description: start the Val language server (used by editor integrations)
|
|
2264
|
+
Options:
|
|
2265
|
+
--stdio Communicate over stdin/stdout
|
|
2266
|
+
--node-ipc Communicate over Node IPC
|
|
2267
|
+
--socket=[port] Communicate over a socket
|
|
2268
|
+
|
|
2363
2269
|
Command: debug
|
|
2364
2270
|
Description: Create a self-contained snapshot (the pending patches plus the modules they
|
|
2365
2271
|
touch or reference) that Val developers can replay to reproduce validation and save
|
|
@@ -2389,11 +2295,6 @@ async function main() {
|
|
|
2389
2295
|
--verbose List every pending patch, not just the failing ones
|
|
2390
2296
|
`, {
|
|
2391
2297
|
flags: {
|
|
2392
|
-
port: {
|
|
2393
|
-
type: "number",
|
|
2394
|
-
alias: "p",
|
|
2395
|
-
default: 4123
|
|
2396
|
-
},
|
|
2397
2298
|
root: {
|
|
2398
2299
|
type: "string",
|
|
2399
2300
|
alias: "r"
|
|
@@ -2455,6 +2356,8 @@ async function main() {
|
|
|
2455
2356
|
});
|
|
2456
2357
|
case "versions":
|
|
2457
2358
|
return versions();
|
|
2359
|
+
case "lsp":
|
|
2360
|
+
return lsp();
|
|
2458
2361
|
case "debug":
|
|
2459
2362
|
return debug({
|
|
2460
2363
|
root: flags.root,
|
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.98.0",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
"open": "^9.1.0",
|
|
28
28
|
"picocolors": "^1.1.1",
|
|
29
29
|
"zod": "^4.3.5",
|
|
30
|
-
"@valbuild/core": "0.97.
|
|
30
|
+
"@valbuild/core": "0.97.7",
|
|
31
31
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
32
|
-
"@valbuild/server": "0.
|
|
33
|
-
"@valbuild/
|
|
32
|
+
"@valbuild/language-server": "0.98.0",
|
|
33
|
+
"@valbuild/server": "0.98.0",
|
|
34
|
+
"@valbuild/shared": "0.97.7"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"prettier": "*",
|
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getVersions } from "./getVersions";
|
|
|
6
6
|
import { connect } from "./connect";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { login } from "./login";
|
|
9
|
+
import { lsp } from "./lsp";
|
|
9
10
|
import { debug } from "./debug";
|
|
10
11
|
import { deleteUnappliablePatches } from "./deleteUnappliablePatches";
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ async function main(): Promise<void> {
|
|
|
24
25
|
files
|
|
25
26
|
connect
|
|
26
27
|
versions
|
|
28
|
+
lsp
|
|
27
29
|
debug
|
|
28
30
|
delete-unappliable-patches
|
|
29
31
|
|
|
@@ -53,6 +55,13 @@ async function main(): Promise<void> {
|
|
|
53
55
|
Options:
|
|
54
56
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
55
57
|
|
|
58
|
+
Command: lsp
|
|
59
|
+
Description: start the Val language server (used by editor integrations)
|
|
60
|
+
Options:
|
|
61
|
+
--stdio Communicate over stdin/stdout
|
|
62
|
+
--node-ipc Communicate over Node IPC
|
|
63
|
+
--socket=[port] Communicate over a socket
|
|
64
|
+
|
|
56
65
|
Command: debug
|
|
57
66
|
Description: Create a self-contained snapshot (the pending patches plus the modules they
|
|
58
67
|
touch or reference) that Val developers can replay to reproduce validation and save
|
|
@@ -83,11 +92,6 @@ async function main(): Promise<void> {
|
|
|
83
92
|
`,
|
|
84
93
|
{
|
|
85
94
|
flags: {
|
|
86
|
-
port: {
|
|
87
|
-
type: "number",
|
|
88
|
-
alias: "p",
|
|
89
|
-
default: 4123,
|
|
90
|
-
},
|
|
91
95
|
root: {
|
|
92
96
|
type: "string",
|
|
93
97
|
alias: "r",
|
|
@@ -155,6 +159,8 @@ async function main(): Promise<void> {
|
|
|
155
159
|
});
|
|
156
160
|
case "versions":
|
|
157
161
|
return versions();
|
|
162
|
+
case "lsp":
|
|
163
|
+
return lsp();
|
|
158
164
|
case "debug":
|
|
159
165
|
return debug({
|
|
160
166
|
root: flags.root,
|
package/src/connect.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { findAndEvalValConfigFile } from "./utils/evalValConfigFile";
|
|
5
5
|
|
|
6
6
|
const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
|
|
7
7
|
|
|
@@ -39,9 +39,7 @@ async function tryGetProject(projectRoot: string): Promise<{
|
|
|
39
39
|
orgName: string;
|
|
40
40
|
projectName: string;
|
|
41
41
|
} | null> {
|
|
42
|
-
const valConfigFile =
|
|
43
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
44
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
42
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
45
43
|
|
|
46
44
|
if (valConfigFile && valConfigFile.project) {
|
|
47
45
|
const parts = valConfigFile.project.split("/");
|
package/src/debug/context.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ValOpsFS,
|
|
10
10
|
ValOpsHttp,
|
|
11
11
|
} from "@valbuild/server";
|
|
12
|
-
import {
|
|
12
|
+
import { findAndEvalValConfigFile } from "../utils/evalValConfigFile";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Everything the debug commands need in order to talk to the same ops the
|
|
@@ -56,9 +56,7 @@ export async function createDebugContext(options: {
|
|
|
56
56
|
if (!fs.existsSync(projectRoot)) {
|
|
57
57
|
throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
|
|
58
58
|
}
|
|
59
|
-
const config =
|
|
60
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
61
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
59
|
+
const config = await findAndEvalValConfigFile(projectRoot);
|
|
62
60
|
if (!config) {
|
|
63
61
|
throw new DebugContextError(
|
|
64
62
|
`Could not find val.config.ts nor val.config.js in: ${projectRoot}`,
|
package/src/listUnusedFiles.ts
CHANGED
|
@@ -8,14 +8,12 @@ import {
|
|
|
8
8
|
import { createService } from "@valbuild/server";
|
|
9
9
|
import { glob } from "fast-glob";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import {
|
|
11
|
+
import { findAndEvalValConfigFile } from "./utils/evalValConfigFile";
|
|
12
12
|
|
|
13
13
|
export async function listUnusedFiles({ root }: { root?: string }) {
|
|
14
14
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
15
15
|
|
|
16
|
-
const valConfigFile =
|
|
17
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
18
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
16
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
19
17
|
// Strip the leading "/" so it is relative to the project root (e.g. "public/val").
|
|
20
18
|
const managedDir = (valConfigFile?.files?.directory ?? "/public/val").replace(
|
|
21
19
|
/^\//,
|
package/src/login.ts
CHANGED
|
@@ -1,56 +1,44 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
awaitValLoginConfirmation,
|
|
4
|
+
persistPersonalAccessToken,
|
|
5
|
+
startValLogin,
|
|
6
|
+
ValLoginError,
|
|
7
|
+
} from "@valbuild/server";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Terminal wrapper around the login device flow in `@valbuild/server`.
|
|
11
|
+
* The flow itself (polling, token persistence) is shared with
|
|
12
|
+
* `@valbuild/language-server`.
|
|
13
|
+
*/
|
|
8
14
|
export async function login(options: { root?: string }) {
|
|
9
15
|
try {
|
|
10
16
|
console.log(pc.cyan("\nStarting login process...\n"));
|
|
11
17
|
|
|
12
|
-
|
|
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). ",
|
|
26
|
-
),
|
|
27
|
-
text
|
|
28
|
-
? `Server response: ${text} (status: ${response.status})`
|
|
29
|
-
: `Status: ${response.status}`,
|
|
30
|
-
);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
const json = await response.json();
|
|
34
|
-
if (json) {
|
|
35
|
-
token = json.nonce;
|
|
36
|
-
url = json.url;
|
|
37
|
-
}
|
|
38
|
-
if (!token || !url) {
|
|
39
|
-
console.error(pc.red("Unexpected response from the server."), json);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
18
|
+
const { nonce, url } = await startValLogin();
|
|
42
19
|
|
|
43
20
|
console.log(pc.green("Open the following URL in your browser to log in:"));
|
|
44
21
|
console.log(pc.underline(pc.blue(url)));
|
|
45
22
|
console.log(pc.dim("\nWaiting for login confirmation...\n"));
|
|
46
23
|
|
|
47
|
-
|
|
48
|
-
const result = await pollForConfirmation(token);
|
|
24
|
+
const result = await awaitValLoginConfirmation(nonce);
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
26
|
+
const filePath = persistPersonalAccessToken(
|
|
27
|
+
options.root || process.cwd(),
|
|
28
|
+
result,
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
pc.green(
|
|
32
|
+
`Token for ${pc.cyan(
|
|
33
|
+
result.profile.email,
|
|
34
|
+
)} saved to ${pc.cyan(filePath)}`,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
53
37
|
} catch (error) {
|
|
38
|
+
if (error instanceof ValLoginError) {
|
|
39
|
+
console.error(pc.red(error.message), error.details ?? "");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
54
42
|
console.error(
|
|
55
43
|
pc.red(
|
|
56
44
|
"An error occurred during the login process. Check your internet connection. Details:",
|
|
@@ -60,56 +48,3 @@ export async function login(options: { root?: string }) {
|
|
|
60
48
|
process.exit(1);
|
|
61
49
|
}
|
|
62
50
|
}
|
|
63
|
-
|
|
64
|
-
const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
65
|
-
async function pollForConfirmation(token: string): Promise<{
|
|
66
|
-
profile: { email: string };
|
|
67
|
-
pat: string;
|
|
68
|
-
}> {
|
|
69
|
-
const start = Date.now();
|
|
70
|
-
while (Date.now() - start < MAX_DURATION) {
|
|
71
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
72
|
-
const response = await fetch(
|
|
73
|
-
`${host}/api/login?token=${token}&consume=true`,
|
|
74
|
-
{ method: "POST" },
|
|
75
|
-
);
|
|
76
|
-
if (response.status === 500) {
|
|
77
|
-
console.error(pc.red("An error occurred on the server."));
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
if (response.status === 200) {
|
|
81
|
-
const json = await response.json();
|
|
82
|
-
if (json) {
|
|
83
|
-
if (
|
|
84
|
-
typeof json.profile.email === "string" &&
|
|
85
|
-
typeof json.pat === "string"
|
|
86
|
-
) {
|
|
87
|
-
return json;
|
|
88
|
-
} else {
|
|
89
|
-
console.error(pc.red("Unexpected response from the server."));
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
console.error(pc.red("Login confirmation timed out."));
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function saveToken(
|
|
100
|
-
result: {
|
|
101
|
-
profile: { email: string };
|
|
102
|
-
pat: string;
|
|
103
|
-
},
|
|
104
|
-
filePath: string,
|
|
105
|
-
) {
|
|
106
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
107
|
-
fs.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
108
|
-
console.log(
|
|
109
|
-
pc.green(
|
|
110
|
-
`Token for ${pc.cyan(
|
|
111
|
-
result.profile.email,
|
|
112
|
-
)} saved to ${pc.cyan(filePath)}`,
|
|
113
|
-
),
|
|
114
|
-
);
|
|
115
|
-
}
|