@valbuild/cli 0.97.7 → 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.
@@ -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 fs$1 from 'fs';
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$1.mkdirSync(path.dirname(fileName), {
407
+ fs.mkdirSync(path.dirname(fileName), {
481
408
  recursive: true
482
409
  });
483
- fs$1.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
410
+ fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
484
411
  },
485
- rmFile: fs$1.rmSync,
412
+ rmFile: fs.rmSync,
486
413
  readBuffer: fileName => {
487
414
  try {
488
- return fs$1.readFileSync(fileName);
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$1.readFileSync(filePath, "utf-8");
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 = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
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 = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
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$1 = process.env.VAL_BUILD_URL || "https://admin.val.build";
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$1}/connect?${params.toString()}`;
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 = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
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$1.existsSync(gitConfigPath)) {
1330
- return fs$1.readFileSync(gitConfigPath, "utf-8");
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
- const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
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
- // Step 1: Initiate login and get token and URL
1348
- const response = await fetch(`${host}/api/login`, {
1349
- method: "POST",
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
- // Step 2: Poll for login confirmation
1375
- const result = await pollForConfirmation(token);
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
- 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));
1382
- process.exit(1);
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
- if (response.status === 200) {
1398
- const json = await response.json();
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
- function saveToken(result, filePath) {
1413
- fs$1.mkdirSync(path.dirname(filePath), {
1414
- recursive: true
1415
- });
1416
- fs$1.writeFileSync(filePath, JSON.stringify(result, null, 2));
1417
- console.log(pc.green(`Token for ${pc.cyan(result.profile.email)} saved to ${pc.cyan(filePath)}`));
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$1.existsSync(projectRoot)) {
1332
+ if (!fs.existsSync(projectRoot)) {
1435
1333
  throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
1436
1334
  }
1437
- const config = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
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$1.existsSync(patFile)) {
1505
- const contents = fs$1.readFileSync(patFile, "utf-8");
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$1.readFileSync(absPath, "utf-8");
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$1.mkdirSync(path.dirname(outPath), {
2092
+ fs.mkdirSync(path.dirname(outPath), {
2195
2093
  recursive: true
2196
2094
  });
2197
- fs$1.writeFileSync(outPath, buffer);
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.97.7",
4
+ "version": "0.98.0",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,7 +29,8 @@
29
29
  "zod": "^4.3.5",
30
30
  "@valbuild/core": "0.97.7",
31
31
  "@valbuild/eslint-plugin": "0.93.0",
32
- "@valbuild/server": "0.97.7",
32
+ "@valbuild/language-server": "0.98.0",
33
+ "@valbuild/server": "0.98.0",
33
34
  "@valbuild/shared": "0.97.7"
34
35
  },
35
36
  "peerDependencies": {
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 { evalValConfigFile } from "./utils/evalValConfigFile";
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("/");
@@ -9,7 +9,7 @@ import {
9
9
  ValOpsFS,
10
10
  ValOpsHttp,
11
11
  } from "@valbuild/server";
12
- import { evalValConfigFile } from "../utils/evalValConfigFile";
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}`,
@@ -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 { evalValConfigFile } from "./utils/evalValConfigFile";
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 fs from "fs";
3
- import path from "path";
4
- import { getPersonalAccessTokenPath } from "@valbuild/server";
5
-
6
- const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
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
- // 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). ",
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
- // Step 2: Poll for login confirmation
48
- const result = await pollForConfirmation(token);
24
+ const result = await awaitValLoginConfirmation(nonce);
49
25
 
50
- // Step 3: Save the token
51
- const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
52
- saveToken(result, filePath);
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
- }