@valbuild/cli 0.97.7 → 0.99.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.
@@ -4,16 +4,13 @@ var meow = require('meow');
4
4
  var chalk = require('chalk');
5
5
  var path = require('path');
6
6
  var pc = require('picocolors');
7
- var fs = require('fs/promises');
7
+ var fs$1 = require('fs/promises');
8
8
  var fastGlob = require('fast-glob');
9
9
  var core = require('@valbuild/core');
10
10
  var server = require('@valbuild/server');
11
- var vm = require('node:vm');
12
- var ts = require('typescript');
13
- var z = require('zod');
14
- var node_module = require('node:module');
15
11
  var internal = require('@valbuild/shared/internal');
16
- var fs$1 = require('fs');
12
+ var ts = require('typescript');
13
+ var fs = require('fs');
17
14
  var JSZip = require('jszip');
18
15
  var readline = require('readline');
19
16
 
@@ -41,11 +38,9 @@ var meow__default = /*#__PURE__*/_interopDefault(meow);
41
38
  var chalk__default = /*#__PURE__*/_interopDefault(chalk);
42
39
  var path__default = /*#__PURE__*/_interopDefault(path);
43
40
  var pc__default = /*#__PURE__*/_interopDefault(pc);
44
- var fs__default = /*#__PURE__*/_interopDefault(fs);
45
- var vm__default = /*#__PURE__*/_interopDefault(vm);
46
- var ts__default = /*#__PURE__*/_interopDefault(ts);
47
- var z__default = /*#__PURE__*/_interopDefault(z);
48
41
  var fs__default$1 = /*#__PURE__*/_interopDefault(fs$1);
42
+ var ts__default = /*#__PURE__*/_interopDefault(ts);
43
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
49
44
  var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
50
45
  var readline__default = /*#__PURE__*/_interopDefault(readline);
51
46
 
@@ -64,76 +59,6 @@ function info(message, opts = {}) {
64
59
  console.log(chalk__default["default"].blue("️ℹ️ : ") + message);
65
60
  }
66
61
 
67
- const ValConfigSchema = z__default["default"].object({
68
- project: z__default["default"].string().optional(),
69
- root: z__default["default"].string().optional(),
70
- files: z__default["default"].object({
71
- directory: z__default["default"].string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
72
- // Reject path traversal so the directory cannot escape /public
73
- !val.split("/").some(segment => segment === "." || segment === ".."), {
74
- message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
75
- })
76
- }).optional(),
77
- gitCommit: z__default["default"].string().optional(),
78
- gitBranch: z__default["default"].string().optional(),
79
- defaultTheme: z__default["default"].union([z__default["default"].literal("light"), z__default["default"].literal("dark")]).optional(),
80
- ai: z__default["default"].object({
81
- commitMessages: z__default["default"].object({
82
- disabled: z__default["default"].boolean().optional()
83
- }).optional()
84
- }).optional()
85
- });
86
- async function evalValConfigFile(projectRoot, configFileName) {
87
- const valConfigPath = path__default["default"].join(projectRoot, configFileName);
88
- let code = null;
89
- try {
90
- code = await fs__default["default"].readFile(valConfigPath, "utf-8");
91
- } catch {
92
- //
93
- }
94
- if (!code) {
95
- return null;
96
- }
97
- const transpiled = ts__default["default"].transpileModule(code, {
98
- compilerOptions: {
99
- target: ts__default["default"].ScriptTarget.ES2020,
100
- module: ts__default["default"].ModuleKind.CommonJS,
101
- esModuleInterop: true
102
- },
103
- fileName: valConfigPath
104
- });
105
- const projectRootRequire = node_module.createRequire(valConfigPath);
106
- const exportsObj = {};
107
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
- const sandbox = {
109
- exports: exportsObj,
110
- module: {
111
- exports: exportsObj
112
- },
113
- require: projectRootRequire,
114
- // NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
115
- __filename: valConfigPath,
116
- __dirname: projectRoot,
117
- console,
118
- process
119
- };
120
- sandbox.global = sandbox;
121
- const context = vm__default["default"].createContext(sandbox);
122
- const script = new vm__default["default"].Script(transpiled.outputText, {
123
- filename: valConfigPath
124
- });
125
- script.runInContext(context);
126
- const valConfig = sandbox.module.exports.config;
127
- if (!valConfig) {
128
- throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
129
- }
130
- const result = ValConfigSchema.safeParse(valConfig);
131
- if (!result.success) {
132
- throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
133
- }
134
- return result.data;
135
- }
136
-
137
62
  function getFileExt(filePath) {
138
63
  // 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).
139
64
  return filePath.split(".").pop() || "";
@@ -175,32 +100,13 @@ async function handleFileMetadata(ctx) {
175
100
  shouldApplyPatch: true
176
101
  };
177
102
  }
178
- async function handleRemoteFileUpload(ctx) {
179
- var _resolvedRemoteFileAt;
180
- if (!ctx.fix) {
181
- return {
182
- success: false,
183
- errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`
184
- };
185
- }
186
- const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
187
- if (!ctx.valModule.source || !ctx.valModule.schema) {
188
- return {
189
- success: false,
190
- errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
191
- };
192
- }
193
- const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
194
103
 
195
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
- const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
197
- if (!fileRefProp) {
198
- return {
199
- success: false,
200
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
201
- };
202
- }
203
- const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
104
+ // Shared upload core used by both the single-field (handleRemoteFileUpload)
105
+ // and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
106
+ // how they derive the local file ref, metadata and serialized image/file
107
+ // schema; everything from auth through upload is identical.
108
+ async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
109
+ const filePath = path__default["default"].join(ctx.projectRoot, fileRef);
204
110
  if (!ctx.fs.fileExists(filePath)) {
205
111
  return {
206
112
  success: false,
@@ -240,21 +146,6 @@ async function handleRemoteFileUpload(ctx) {
240
146
  }]
241
147
  };
242
148
  }
243
- if (!resolvedRemoteFileAtSourcePath.schema) {
244
- return {
245
- success: false,
246
- errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
247
- };
248
- }
249
- const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
250
- const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
251
- const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
252
- if (!resolveRemoteFileSchema) {
253
- return {
254
- success: false,
255
- errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
256
- };
257
- }
258
149
  const projectName = ctx.project;
259
150
  let publicProjectId = ctx.publicProjectId;
260
151
  let remoteFileBuckets = ctx.remoteFileBuckets;
@@ -290,12 +181,6 @@ async function handleRemoteFileUpload(ctx) {
290
181
  errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
291
182
  };
292
183
  }
293
- if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
294
- return {
295
- success: false,
296
- errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
297
- };
298
- }
299
184
  remoteFilesCounter += 1;
300
185
  const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
301
186
  if (!bucket) {
@@ -321,8 +206,6 @@ async function handleRemoteFileUpload(ctx) {
321
206
  const fileHash = core.Internal.remote.getFileHash(fileBuffer);
322
207
  const coreVersion = core.Internal.VERSION.core || "unknown";
323
208
  const fileExt = getFileExt(filePath);
324
- const schema = resolveRemoteFileSchema;
325
- const metadata = fileSourceMetadata;
326
209
  const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
327
210
  publicProjectId,
328
211
  coreVersion,
@@ -342,7 +225,7 @@ async function handleRemoteFileUpload(ctx) {
342
225
  }
343
226
  ctx.remoteFiles[ctx.sourcePath] = {
344
227
  ref,
345
- metadata: fileSourceMetadata
228
+ metadata
346
229
  };
347
230
  return {
348
231
  success: true,
@@ -359,6 +242,131 @@ async function handleRemoteFileUpload(ctx) {
359
242
  }]
360
243
  };
361
244
  }
245
+ async function handleRemoteFileUpload(ctx) {
246
+ var _resolvedRemoteFileAt;
247
+ if (!ctx.fix) {
248
+ return {
249
+ success: false,
250
+ // No sourcePath in the message: the reported location already points at it.
251
+ errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
252
+ };
253
+ }
254
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
255
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
256
+ return {
257
+ success: false,
258
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
259
+ };
260
+ }
261
+ const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
262
+
263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
+ const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
265
+ if (!fileRefProp) {
266
+ return {
267
+ success: false,
268
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
269
+ };
270
+ }
271
+ const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
272
+ if (!resolveRemoteFileSchema) {
273
+ return {
274
+ success: false,
275
+ errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
276
+ };
277
+ }
278
+ if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
279
+ return {
280
+ success: false,
281
+ errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
282
+ };
283
+ }
284
+ const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
285
+ const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
286
+ return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
287
+ }
288
+
289
+ // Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
290
+ // Unlike a single image/file field, a gallery entry is keyed by its local file
291
+ // path and the value is bare metadata (no FileSource), so we derive the file
292
+ // ref from the key and synthesize the image/file schema from the record's
293
+ // media options.
294
+ async function handleRemoteGalleryFileUpload(ctx) {
295
+ var _ctx$validationError$;
296
+ if (!ctx.fix) {
297
+ return {
298
+ success: false,
299
+ // No sourcePath in the message: the reported location already points at it.
300
+ errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
301
+ };
302
+ }
303
+ const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
304
+ const mediaType = fix === "files:upload-remote" ? "file" : "image";
305
+
306
+ // The gallery entry is keyed by its local file path; validateMediaKey carries
307
+ // that key as the error value.
308
+ const fileRef = ctx.validationError.value;
309
+ if (typeof fileRef !== "string") {
310
+ return {
311
+ success: false,
312
+ errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
313
+ };
314
+ }
315
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
316
+ if (!ctx.valModule.source || !ctx.valModule.schema) {
317
+ return {
318
+ success: false,
319
+ errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
320
+ };
321
+ }
322
+ const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
323
+ const entrySource = resolved.source;
324
+ const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
325
+
326
+ // The gallery item schema is an ObjectSchema, not an image/file schema, so
327
+ // synthesize the serialized image/file schema (matching how single fields
328
+ // serialize) for the remote ref's validation hash.
329
+ //
330
+ // `accept`/`directory` come from the RECORD that holds the entry, resolved
331
+ // from the entry's PARENT path - not from the module root, which is only the
332
+ // record when the gallery is the whole module. A nested gallery
333
+ // (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
334
+ // with no options and bake a validation hash into the remote ref that can
335
+ // never validate, so a mismatch fails fast instead of uploading.
336
+ const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
337
+ const recordSchema = core.Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
338
+ if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
339
+ return {
340
+ success: false,
341
+ errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
342
+ };
343
+ }
344
+ const {
345
+ accept,
346
+ directory
347
+ } = recordSchema;
348
+ const schema = mediaType === "image" ? {
349
+ type: "image",
350
+ opt: false,
351
+ options: {
352
+ ...(accept ? {
353
+ accept
354
+ } : {}),
355
+ ...(directory ? {
356
+ directory
357
+ } : {})
358
+ }
359
+ } : {
360
+ type: "file",
361
+ opt: false,
362
+ options: {
363
+ ...(accept ? {
364
+ accept
365
+ } : {})
366
+ }
367
+ };
368
+ return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
369
+ }
362
370
  async function handleRemoteFileDownload(ctx) {
363
371
  if (ctx.fix) {
364
372
  return {
@@ -416,6 +424,17 @@ async function handleUniqueFolderCheck(ctx) {
416
424
  success: true
417
425
  };
418
426
  }
427
+
428
+ // Maps a gallery key to its on-disk local path. Remote galleries key uploaded
429
+ // entries by a remote URL while keeping the file on disk; everything else is
430
+ // already a local path and is returned unchanged.
431
+ function remoteKeyToLocalPath(key) {
432
+ const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
433
+ if (remoteRefRes.status === "success") {
434
+ return `/${remoteRefRes.filePath}`;
435
+ }
436
+ return key;
437
+ }
419
438
  async function handleCheckAllFiles(ctx) {
420
439
  const value = ctx.validationError.value;
421
440
  if (!value || typeof value.directory !== "string") {
@@ -434,7 +453,10 @@ async function handleCheckAllFiles(ctx) {
434
453
  errorMessage: `Could not get source for ${ctx.sourcePath}`
435
454
  };
436
455
  }
437
- const trackedFiles = new Set(Object.keys(source));
456
+ // Gallery entries are keyed by their file path. Remote galleries key uploaded
457
+ // entries by a remote URL, but the file is kept on disk at its local path, so
458
+ // normalize remote-URL keys back to that local path for the on-disk checks.
459
+ const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
438
460
 
439
461
  // Check that all tracked files exist on disk
440
462
  const missingTrackedFiles = [...trackedFiles].filter(f => {
@@ -489,6 +511,8 @@ const currentFixHandlers = {
489
511
  "file:add-metadata": handleFileMetadata,
490
512
  "image:upload-remote": handleRemoteFileUpload,
491
513
  "file:upload-remote": handleRemoteFileUpload,
514
+ "images:upload-remote": handleRemoteGalleryFileUpload,
515
+ "files:upload-remote": handleRemoteGalleryFileUpload,
492
516
  "image:download-remote": handleRemoteFileDownload,
493
517
  "file:download-remote": handleRemoteFileDownload,
494
518
  "image:check-remote": handleRemoteFileCheck,
@@ -511,15 +535,15 @@ function createDefaultValFSHost() {
511
535
  return {
512
536
  ...ts__default["default"].sys,
513
537
  writeFile: (fileName, data, encoding) => {
514
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(fileName), {
538
+ fs__default["default"].mkdirSync(path__default["default"].dirname(fileName), {
515
539
  recursive: true
516
540
  });
517
- fs__default$1["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
541
+ fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
518
542
  },
519
- rmFile: fs__default$1["default"].rmSync,
543
+ rmFile: fs__default["default"].rmSync,
520
544
  readBuffer: fileName => {
521
545
  try {
522
- return fs__default$1["default"].readFileSync(fileName);
546
+ return fs__default["default"].readFileSync(fileName);
523
547
  } catch {
524
548
  return undefined;
525
549
  }
@@ -860,7 +884,7 @@ function getCachedFile(moduleFilePath, projectRoot, cache) {
860
884
  const filePath = path__default["default"].join(projectRoot, moduleFilePath);
861
885
  let fileContent;
862
886
  try {
863
- fileContent = fs__default$1["default"].readFileSync(filePath, "utf-8");
887
+ fileContent = fs__default["default"].readFileSync(filePath, "utf-8");
864
888
  } catch {
865
889
  cache.set(moduleFilePath, null);
866
890
  return null;
@@ -891,7 +915,7 @@ async function validate({
891
915
  // errors found. Re-reads config and val files each call so it always reflects
892
916
  // the latest state on disk (used both for one-shot and watch mode).
893
917
  async function runOnce() {
894
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
918
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
895
919
  const resolvedValConfigFile = valConfigFile ? {
896
920
  ...valConfigFile,
897
921
  project: process.env.VAL_PROJECT || valConfigFile.project
@@ -1071,11 +1095,11 @@ async function validate({
1071
1095
  if (prettier) {
1072
1096
  for (const file of fixedFiles) {
1073
1097
  const filePath = path__default["default"].join(projectRoot, file);
1074
- const fileContent = await fs__default["default"].readFile(filePath, "utf-8");
1098
+ const fileContent = await fs__default$1["default"].readFile(filePath, "utf-8");
1075
1099
  const formattedContent = await prettier.format(fileContent, {
1076
1100
  filepath: filePath
1077
1101
  });
1078
- await fs__default["default"].writeFile(filePath, formattedContent);
1102
+ await fs__default$1["default"].writeFile(filePath, formattedContent);
1079
1103
  }
1080
1104
  }
1081
1105
 
@@ -1196,7 +1220,7 @@ async function listUnusedFiles({
1196
1220
  }) {
1197
1221
  var _valConfigFile$files;
1198
1222
  const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
1199
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
1223
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
1200
1224
  // Strip the leading "/" so it is relative to the project root (e.g. "public/val").
1201
1225
  const managedDir = ((valConfigFile === null || valConfigFile === void 0 || (_valConfigFile$files = valConfigFile.files) === null || _valConfigFile$files === void 0 ? void 0 : _valConfigFile$files.directory) ?? "/public/val").replace(/^\//, "");
1202
1226
  const service = await server.createService(projectRoot);
@@ -1285,7 +1309,7 @@ const getVersions = () => {
1285
1309
  };
1286
1310
  };
1287
1311
 
1288
- const host$1 = process.env.VAL_BUILD_URL || "https://admin.val.build";
1312
+ const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1289
1313
  async function connect(options) {
1290
1314
  const {
1291
1315
  root
@@ -1303,11 +1327,11 @@ async function connect(options) {
1303
1327
  if (maybeGitOwnerAndRepo) {
1304
1328
  params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
1305
1329
  }
1306
- const url = `${host$1}/connect?${params.toString()}`;
1330
+ const url = `${host}/connect?${params.toString()}`;
1307
1331
  console.log(pc__default["default"].dim(`\nFollow the instructions in your browser to complete the setup:\n${pc__default["default"].cyan(url)}\n`));
1308
1332
  }
1309
1333
  async function tryGetProject(projectRoot) {
1310
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
1334
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
1311
1335
  if (valConfigFile && valConfigFile.project) {
1312
1336
  const parts = valConfigFile.project.split("/");
1313
1337
  if (parts.length === 2) {
@@ -1360,8 +1384,8 @@ function tryGetGitConfig(root) {
1360
1384
  let lastDir = null;
1361
1385
  while (currentDir !== lastDir) {
1362
1386
  const gitConfigPath = path__default["default"].join(currentDir, ".git", "config");
1363
- if (fs__default$1["default"].existsSync(gitConfigPath)) {
1364
- return fs__default$1["default"].readFileSync(gitConfigPath, "utf-8");
1387
+ if (fs__default["default"].existsSync(gitConfigPath)) {
1388
+ return fs__default["default"].readFileSync(gitConfigPath, "utf-8");
1365
1389
  }
1366
1390
  lastDir = currentDir;
1367
1391
  currentDir = path__default["default"].dirname(currentDir);
@@ -1372,83 +1396,54 @@ function tryGetGitConfig(root) {
1372
1396
  return null;
1373
1397
  }
1374
1398
 
1375
- const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1399
+ /**
1400
+ * Terminal wrapper around the login device flow in `@valbuild/server`.
1401
+ * The flow itself (polling, token persistence) is shared with
1402
+ * `@valbuild/language-server`.
1403
+ */
1376
1404
  async function login(options) {
1377
1405
  try {
1378
- var _response$headers$get;
1379
1406
  console.log(pc__default["default"].cyan("\nStarting login process...\n"));
1380
-
1381
- // Step 1: Initiate login and get token and URL
1382
- const response = await fetch(`${host}/api/login`, {
1383
- method: "POST",
1384
- headers: {
1385
- "Content-Type": "application/json"
1386
- }
1387
- });
1388
- let token;
1389
- let url;
1390
- if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
1391
- const text = await response.text();
1392
- console.error(pc__default["default"].red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
1393
- process.exit(1);
1394
- }
1395
- const json = await response.json();
1396
- if (json) {
1397
- token = json.nonce;
1398
- url = json.url;
1399
- }
1400
- if (!token || !url) {
1401
- console.error(pc__default["default"].red("Unexpected response from the server."), json);
1402
- process.exit(1);
1403
- }
1407
+ const {
1408
+ nonce,
1409
+ url
1410
+ } = await server.startValLogin();
1404
1411
  console.log(pc__default["default"].green("Open the following URL in your browser to log in:"));
1405
1412
  console.log(pc__default["default"].underline(pc__default["default"].blue(url)));
1406
1413
  console.log(pc__default["default"].dim("\nWaiting for login confirmation...\n"));
1407
-
1408
- // Step 2: Poll for login confirmation
1409
- const result = await pollForConfirmation(token);
1410
-
1411
- // Step 3: Save the token
1412
- const filePath = server.getPersonalAccessTokenPath(options.root || process.cwd());
1413
- saveToken(result, filePath);
1414
+ const result = await server.awaitValLoginConfirmation(nonce);
1415
+ const filePath = server.persistPersonalAccessToken(options.root || process.cwd(), result);
1416
+ console.log(pc__default["default"].green(`Token for ${pc__default["default"].cyan(result.profile.email)} saved to ${pc__default["default"].cyan(filePath)}`));
1414
1417
  } catch (error) {
1415
- console.error(pc__default["default"].red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
1416
- process.exit(1);
1417
- }
1418
- }
1419
- const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
1420
- async function pollForConfirmation(token) {
1421
- const start = Date.now();
1422
- while (Date.now() - start < MAX_DURATION) {
1423
- await new Promise(resolve => setTimeout(resolve, 1000));
1424
- const response = await fetch(`${host}/api/login?token=${token}&consume=true`, {
1425
- method: "POST"
1426
- });
1427
- if (response.status === 500) {
1428
- console.error(pc__default["default"].red("An error occurred on the server."));
1418
+ if (error instanceof server.ValLoginError) {
1419
+ console.error(pc__default["default"].red(error.message), error.details ?? "");
1429
1420
  process.exit(1);
1430
1421
  }
1431
- if (response.status === 200) {
1432
- const json = await response.json();
1433
- if (json) {
1434
- if (typeof json.profile.email === "string" && typeof json.pat === "string") {
1435
- return json;
1436
- } else {
1437
- console.error(pc__default["default"].red("Unexpected response from the server."));
1438
- process.exit(1);
1439
- }
1440
- }
1441
- }
1422
+ console.error(pc__default["default"].red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
1423
+ process.exit(1);
1442
1424
  }
1443
- console.error(pc__default["default"].red("Login confirmation timed out."));
1444
- process.exit(1);
1445
1425
  }
1446
- function saveToken(result, filePath) {
1447
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(filePath), {
1448
- recursive: true
1449
- });
1450
- fs__default$1["default"].writeFileSync(filePath, JSON.stringify(result, null, 2));
1451
- console.log(pc__default["default"].green(`Token for ${pc__default["default"].cyan(result.profile.email)} saved to ${pc__default["default"].cyan(filePath)}`));
1426
+
1427
+ /**
1428
+ * Start the Val language server.
1429
+ *
1430
+ * Editors normally resolve `@valbuild/language-server` from the project's
1431
+ * `node_modules` and launch its `val-language-server` binary directly. This
1432
+ * subcommand exists so the server is discoverable from the CLI and easy to
1433
+ * start by hand when debugging an editor integration:
1434
+ *
1435
+ * npx val lsp --stdio
1436
+ *
1437
+ * The import is dynamic so that `vscode-languageserver` is not loaded on every
1438
+ * `val` invocation.
1439
+ */
1440
+ async function lsp() {
1441
+ const {
1442
+ main
1443
+ } = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('@valbuild/language-server')); });
1444
+ // Transport (`--stdio`, `--node-ipc`, `--socket=`) is read from argv by the
1445
+ // server itself, so the flags pass straight through.
1446
+ main();
1452
1447
  }
1453
1448
 
1454
1449
  /**
@@ -1465,10 +1460,10 @@ class DebugContextError extends Error {}
1465
1460
  async function createDebugContext(options) {
1466
1461
  var _config$files;
1467
1462
  const projectRoot = options.root ? path__default["default"].resolve(options.root) : process.cwd();
1468
- if (!fs__default$1["default"].existsSync(projectRoot)) {
1463
+ if (!fs__default["default"].existsSync(projectRoot)) {
1469
1464
  throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
1470
1465
  }
1471
- const config = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
1466
+ const config = await server.findAndEvalValConfigFile(projectRoot);
1472
1467
  if (!config) {
1473
1468
  throw new DebugContextError(`Could not find val.config.ts nor val.config.js in: ${projectRoot}`);
1474
1469
  }
@@ -1535,8 +1530,8 @@ async function createDebugContext(options) {
1535
1530
  */
1536
1531
  function readAuth(projectRoot) {
1537
1532
  const patFile = server.getPersonalAccessTokenPath(projectRoot);
1538
- if (fs__default$1["default"].existsSync(patFile)) {
1539
- const contents = fs__default$1["default"].readFileSync(patFile, "utf-8");
1533
+ if (fs__default["default"].existsSync(patFile)) {
1534
+ const contents = fs__default["default"].readFileSync(patFile, "utf-8");
1540
1535
  const parsed = server.parsePersonalAccessTokenFile(contents);
1541
1536
  if (parsed.success) {
1542
1537
  return {
@@ -2083,7 +2078,7 @@ function readProjectValVersions(projectRoot) {
2083
2078
  function readLocalFile(projectRoot, projectRelativePath) {
2084
2079
  const absPath = path__default["default"].join(projectRoot, projectRelativePath);
2085
2080
  try {
2086
- return fs__default$1["default"].readFileSync(absPath, "utf-8");
2081
+ return fs__default["default"].readFileSync(absPath, "utf-8");
2087
2082
  } catch {
2088
2083
  return null;
2089
2084
  }
@@ -2225,10 +2220,10 @@ async function writeZip(outPath, entries) {
2225
2220
  type: "nodebuffer",
2226
2221
  compression: "DEFLATE"
2227
2222
  });
2228
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(outPath), {
2223
+ fs__default["default"].mkdirSync(path__default["default"].dirname(outPath), {
2229
2224
  recursive: true
2230
2225
  });
2231
- fs__default$1["default"].writeFileSync(outPath, buffer);
2226
+ fs__default["default"].writeFileSync(outPath, buffer);
2232
2227
  }
2233
2228
  function timestamp() {
2234
2229
  return new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
@@ -2365,6 +2360,7 @@ async function main() {
2365
2360
  files
2366
2361
  connect
2367
2362
  versions
2363
+ lsp
2368
2364
  debug
2369
2365
  delete-unappliable-patches
2370
2366
 
@@ -2394,6 +2390,13 @@ async function main() {
2394
2390
  Options:
2395
2391
  --root [root], -r [root] Set project root directory (default process.cwd())
2396
2392
 
2393
+ Command: lsp
2394
+ Description: start the Val language server (used by editor integrations)
2395
+ Options:
2396
+ --stdio Communicate over stdin/stdout
2397
+ --node-ipc Communicate over Node IPC
2398
+ --socket=[port] Communicate over a socket
2399
+
2397
2400
  Command: debug
2398
2401
  Description: Create a self-contained snapshot (the pending patches plus the modules they
2399
2402
  touch or reference) that Val developers can replay to reproduce validation and save
@@ -2423,11 +2426,6 @@ async function main() {
2423
2426
  --verbose List every pending patch, not just the failing ones
2424
2427
  `, {
2425
2428
  flags: {
2426
- port: {
2427
- type: "number",
2428
- alias: "p",
2429
- default: 4123
2430
- },
2431
2429
  root: {
2432
2430
  type: "string",
2433
2431
  alias: "r"
@@ -2489,6 +2487,8 @@ async function main() {
2489
2487
  });
2490
2488
  case "versions":
2491
2489
  return versions();
2490
+ case "lsp":
2491
+ return lsp();
2492
2492
  case "debug":
2493
2493
  return debug({
2494
2494
  root: flags.root,