arcadible 1.0.1 → 1.0.4

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/src/api.js CHANGED
@@ -11,6 +11,18 @@ const VERSION = "v1";
11
11
 
12
12
  // [function]
13
13
 
14
+ // an API path with each value encoded as one segment, so a handle or version
15
+ // can't add a segment or a query: pathOf`/games/${id}`.
16
+ const pathOf = function (strings, ...values) {
17
+ return strings.reduce(
18
+ (out, each, i) =>
19
+ out + each + (i < values.length ? encodeURIComponent(values[i]) : ""),
20
+ "",
21
+ );
22
+ };
23
+
24
+ // [function]
25
+
14
26
  // the full URL of an API path, with `params` as its query. Undefined and null
15
27
  // params are left out.
16
28
  const endpoint = function (path, params) {
@@ -45,7 +57,7 @@ const postFlow = function (form, params, headers) {
45
57
  // [function]
46
58
 
47
59
  const flow = function (id, params, headers) {
48
- return request.get(endpoint(`/flows/${id}`, params), headers);
60
+ return request.get(endpoint(pathOf`/flows/${id}`, params), headers);
49
61
  };
50
62
 
51
63
  // [function]
@@ -57,20 +69,23 @@ const postUpload = function (form, params, headers) {
57
69
  // [function]
58
70
 
59
71
  const discoverGame = function (id, params, headers) {
60
- return request.get(endpoint(`/discover/games/${id}`, params), headers);
72
+ return request.get(endpoint(pathOf`/discover/games/${id}`, params), headers);
61
73
  };
62
74
 
63
75
  // [function]
64
76
 
65
77
  const discoverGameBuilds = function (id, params, headers) {
66
- return request.get(endpoint(`/discover/games/${id}/builds`, params), headers);
78
+ return request.get(
79
+ endpoint(pathOf`/discover/games/${id}/builds`, params),
80
+ headers,
81
+ );
67
82
  };
68
83
 
69
84
  // [function]
70
85
 
71
86
  const discoverGameBuild = function (id, version, params, headers) {
72
87
  return request.get(
73
- endpoint(`/discover/games/${id}/builds/${version}`, params),
88
+ endpoint(pathOf`/discover/games/${id}/builds/${version}`, params),
74
89
  headers,
75
90
  );
76
91
  };
@@ -80,7 +95,7 @@ const discoverGameBuild = function (id, version, params, headers) {
80
95
  // a game's releases, which anyone can read: what each says about itself.
81
96
  const discoverGameReleases = function (id, params, headers) {
82
97
  return request.get(
83
- endpoint(`/discover/games/${id}/releases`, params),
98
+ endpoint(pathOf`/discover/games/${id}/releases`, params),
84
99
  headers,
85
100
  );
86
101
  };
@@ -90,10 +105,7 @@ const discoverGameReleases = function (id, params, headers) {
90
105
  // the game's connected repository, if it's `owner/name`; otherwise 404.
91
106
  const discoverGameRepo = function (id, owner, name, params, headers) {
92
107
  return request.get(
93
- endpoint(
94
- `/discover/games/${id}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
95
- params,
96
- ),
108
+ endpoint(pathOf`/discover/games/${id}/repos/${owner}/${name}`, params),
97
109
  headers,
98
110
  );
99
111
  };
@@ -102,7 +114,18 @@ const discoverGameRepo = function (id, owner, name, params, headers) {
102
114
 
103
115
  const gameBuild = function (id, version, params, headers) {
104
116
  return request.get(
105
- endpoint(`/games/${id}/builds/${version}`, params),
117
+ endpoint(pathOf`/games/${id}/builds/${version}`, params),
118
+ headers,
119
+ );
120
+ };
121
+
122
+ // [function]
123
+
124
+ // whether the caller's game could take a handle on a deploy to `channel`:
125
+ // its status is available, taken, limit, or lapsed.
126
+ const gameHandle = function (id, handle, params, headers) {
127
+ return request.get(
128
+ endpoint(pathOf`/games/${id}/handles/${handle}`, params),
106
129
  headers,
107
130
  );
108
131
  };
@@ -110,13 +133,16 @@ const gameBuild = function (id, version, params, headers) {
110
133
  // [function]
111
134
 
112
135
  const release = function (id, params, headers) {
113
- return request.get(endpoint(`/releases/${id}`, params), headers);
136
+ return request.get(endpoint(pathOf`/releases/${id}`, params), headers);
114
137
  };
115
138
 
116
139
  // [function]
117
140
 
118
141
  const accountReleases = function (id, params, headers) {
119
- return request.get(endpoint(`/accounts/${id}/releases`, params), headers);
142
+ return request.get(
143
+ endpoint(pathOf`/accounts/${id}/releases`, params),
144
+ headers,
145
+ );
120
146
  };
121
147
 
122
148
  // [export]
@@ -131,6 +157,7 @@ module.exports = {
131
157
  discoverGameRepo,
132
158
  flow,
133
159
  gameBuild,
160
+ gameHandle,
134
161
  postAuthRefresh,
135
162
  postFlow,
136
163
  postUpload,
@@ -13,6 +13,7 @@ const CLI = {
13
13
  ARGUMENT_INVALID: "argument_invalid",
14
14
  BUILD_FAILED: "build_failed",
15
15
  DEPLOY_DIR_MISSING: "deploy_dir_missing",
16
+ DEPLOY_RATE_LIMITED: "deploy_rate_limited",
16
17
  GAME_HANDLE_TAKEN: "game_handle_taken",
17
18
  GAME_ID_MISSING: "game_id_missing",
18
19
  GAME_NOT_DEPLOYED: "game_not_deployed",
@@ -43,7 +44,6 @@ const CLI = {
43
44
  TEMPLATE_MISSING: "template_missing",
44
45
  TEMPLATE_UNAVAILABLE: "template_unavailable",
45
46
  UPLOAD_FAILED: "upload_failed",
46
- UPLOAD_RATE_LIMITED: "upload_rate_limited",
47
47
  VALIDATION_FAILED: "validation_failed",
48
48
  WAIT_TIMED_OUT: "wait_timed_out",
49
49
  };
@@ -104,6 +104,8 @@ const ISSUE = {
104
104
  const WARNING = {
105
105
  DEPLOY_DIR_NOT_BUILT: "deploy_dir_not_built",
106
106
  FIELD_UNKNOWN: "field_unknown",
107
+ GAME_HANDLE_LAPSED: "game_handle_lapsed",
108
+ GAME_HANDLE_LIMIT_REACHED: "game_handle_limit_reached",
107
109
  VERSION_UNCHECKED: "version_unchecked",
108
110
  };
109
111
  // every error code, for checking one exists.
@@ -12,7 +12,7 @@ const strs = require("../util/strs");
12
12
  const deepCompact = objs.deepCompact;
13
13
  const trimWhitespace = strs.trimWhitespace;
14
14
 
15
- // [define]
15
+ // [declare]
16
16
 
17
17
  const COMMENT = 8;
18
18
 
package/src/phrase.js CHANGED
@@ -142,19 +142,19 @@ const didYouMean = function (near) {
142
142
  // [function]
143
143
 
144
144
  const docAttrInvalid = function (tag, attr, value) {
145
- return `Value '${value}' is not valid for the '${attr}' attribute on the '${tag}' tag.`;
145
+ return `Value '${value}' isn't allowed for ${attr} on <${tag}>.`;
146
146
  };
147
147
 
148
148
  // [function]
149
149
 
150
150
  const docAttrUnallowed = function (tag, attr) {
151
- return `Attribute '${attr}' is not permitted on the '${tag}' tag.`;
151
+ return `Attribute ${attr} isn't allowed on <${tag}>.`;
152
152
  };
153
153
 
154
154
  // [function]
155
155
 
156
156
  const docCommentUnallowed = function () {
157
- return "HTML comments are not permitted.";
157
+ return "HTML comments aren't allowed.";
158
158
  };
159
159
 
160
160
  // [function]
@@ -186,7 +186,7 @@ const docMediaType = function (src) {
186
186
  // [function]
187
187
 
188
188
  const docTagUnallowed = function (tag) {
189
- return `Tag '${tag}' is not permitted.`;
189
+ return `Tag <${tag}> isn't allowed.`;
190
190
  };
191
191
 
192
192
  // [function]
@@ -207,6 +207,18 @@ const gameHandleTaken = function (handle) {
207
207
 
208
208
  // [function]
209
209
 
210
+ const gameHandleLapsed = function (handle) {
211
+ return `Handle ${handle} lapsed, so deploying without it.\nA production release takes it back.`;
212
+ };
213
+
214
+ // [function]
215
+
216
+ const gameHandleLimit = function (handle) {
217
+ return `Handle ${handle} not applied, so deploying without it.\nYour unreleased games already hold 5 handles.`;
218
+ };
219
+
220
+ // [function]
221
+
210
222
  const gameNotDeployed = function (game) {
211
223
  return blocks(`${game} isn't on ${TITLE} yet.`, [
212
224
  `Deploy it once with ${cmdDeploy()}.`,
@@ -273,7 +285,7 @@ const packed = function () {
273
285
  // [function]
274
286
 
275
287
  const portInvalid = function () {
276
- return "The port must be a whole number from 1 to 65535.";
288
+ return "`--port` must be a whole number from 1 to 65535.";
277
289
  };
278
290
 
279
291
  // [function]
@@ -299,13 +311,13 @@ const remoteName = function (name) {
299
311
 
300
312
  // arc serve's copy of one of Arcadible's files, kept from `fetchedAt`.
301
313
  const remoteKept = function (name, fetchedAt) {
302
- return `${TITLE} can't be reached, so serving ${remoteName(name)} saved on ${fetchedAt.slice(0, 10)}.`;
314
+ return `Unable to reach ${TITLE}.\nServing ${remoteName(name)} saved on ${fetchedAt.slice(0, 10)}.`;
303
315
  };
304
316
 
305
317
  // [function]
306
318
 
307
319
  const remoteUnreachable = function (name) {
308
- return `${TITLE} can't be reached, and ${remoteName(name)} isn't saved yet. The game runs without it until ${TITLE} is back.`;
320
+ return `Unable to reach ${TITLE}, and ${remoteName(name)} isn't saved.\nThe game runs without it until ${TITLE} is back.`;
309
321
  };
310
322
 
311
323
  // [function]
@@ -322,6 +334,20 @@ const serveAlone = function () {
322
334
 
323
335
  // [function]
324
336
 
337
+ // the label before a play link in a result.
338
+ const playLabel = function () {
339
+ return "Play:";
340
+ };
341
+
342
+ // [function]
343
+
344
+ // the label before a store page's link in a result.
345
+ const storeLabel = function () {
346
+ return "Store:";
347
+ };
348
+
349
+ // [function]
350
+
325
351
  const serveNext = function (dir) {
326
352
  return `Reloads when ${dir}/ changes. Press Ctrl+C to stop.`;
327
353
  };
@@ -347,7 +373,7 @@ const serveNextUrl = function () {
347
373
  // [function]
348
374
 
349
375
  const urlInvalid = function () {
350
- return "The URL must start with http:// or https://, such as http://localhost:5173.";
376
+ return "`--url` must start with http:// or https://.";
351
377
  };
352
378
 
353
379
  // [function]
@@ -356,8 +382,12 @@ const urlInvalid = function () {
356
382
  // says how to add them.
357
383
  const urlNotFrameable = function (url, link) {
358
384
  return blocks(
359
- `⚠ The play page can't frame ${url}.`,
360
- "Send Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Resource-Policy: cross-origin.",
385
+ `The play page can't frame ${url}.`,
386
+ [
387
+ "Its server doesn't send the headers the play page needs:",
388
+ "Cross-Origin-Embedder-Policy: require-corp",
389
+ "Cross-Origin-Resource-Policy: cross-origin",
390
+ ],
361
391
  ["Add them to your server, as the starters' vite.config.js does:", link],
362
392
  );
363
393
  };
@@ -365,7 +395,7 @@ const urlNotFrameable = function (url, link) {
365
395
  // [function]
366
396
 
367
397
  const urlUnreachable = function (url) {
368
- return `⚠ Nothing answers at ${url} yet. Start your server, then reload the play page.`;
398
+ return `Nothing answers at ${url}.\nStart your server, then reload the play page.`;
369
399
  };
370
400
 
371
401
  // [function]
@@ -384,10 +414,11 @@ const releaseFailedHeading = function () {
384
414
 
385
415
  // origin isn't the game's connected repository: none is, or another is.
386
416
  const repoNotConnected = function (game, repo, url) {
387
- return blocks(`${repo} isn't ${game}'s repository on ${TITLE}.`, [
388
- `Connect it in ${TITLE} Studio, on the game's Repository tab (again if it was renamed on GitHub):`,
389
- url,
390
- ]);
417
+ return blocks(
418
+ `${repo} isn't ${game}'s repository on ${TITLE}.`,
419
+ [`Connect it on the game's Repository tab in ${TITLE} Studio:`, url],
420
+ "If it was renamed on GitHub, connect it again.",
421
+ );
391
422
  };
392
423
 
393
424
  // [function]
@@ -399,10 +430,10 @@ const gameNotFound = function (idOrHandle) {
399
430
  // [function]
400
431
 
401
432
  const gameUnavailable = function (id) {
402
- return blocks(`Game unavailable: ${id}`, [
403
- "It was deleted, or belongs to another account.",
433
+ return blocks(
434
+ `Game unavailable: ${id}\nIt was deleted, or belongs to another account.`,
404
435
  "Give game.id a new UUID to deploy it as a new game.",
405
- ]);
436
+ );
406
437
  };
407
438
 
408
439
  // [function]
@@ -418,7 +449,7 @@ const gitUncommitted = function () {
418
449
 
419
450
  const gitRemoteMissing = function () {
420
451
  return blocks(
421
- "No Git remote 'origin'.",
452
+ "No Git remote origin.",
422
453
  "Add the game's GitHub repository as origin.",
423
454
  );
424
455
  };
@@ -427,8 +458,8 @@ const gitRemoteMissing = function () {
427
458
 
428
459
  const gitRemoteUnreachable = function () {
429
460
  return blocks(
430
- "Unable to reach Git remote 'origin'.",
431
- "Check your connection, and that you can access it.",
461
+ "Unable to reach Git remote origin.",
462
+ "Check your internet connection, and that you can access it.",
432
463
  );
433
464
  };
434
465
 
@@ -438,8 +469,14 @@ const manifestMissing = function (file, dir, found) {
438
469
  return blocks(
439
470
  dir === "." ? `No ${file} in this directory.` : `No ${file} in ${dir}.`,
440
471
  found
441
- ? `The project is in \`${found}\`: run ${TOOL} there, or pass \`-C ${found}\`.`
442
- : `Run ${TOOL} in the project's directory, or pass \`-C\`, or start a project with ${cmdInit()}.`,
472
+ ? [
473
+ `Found a project in ${found}.`,
474
+ `Run \`${TOOL}\` there, or pass \`-C ${found}\`.`,
475
+ ]
476
+ : [
477
+ `Run \`${TOOL}\` in a project's directory, or pass \`-C <dir>\`.`,
478
+ `To start a project, run ${cmdInit()}.`,
479
+ ],
443
480
  );
444
481
  };
445
482
 
@@ -447,7 +484,7 @@ const manifestMissing = function (file, dir, found) {
447
484
 
448
485
  const projectNotRepoRoot = function (prefix) {
449
486
  return blocks(
450
- `${cmdDeployGit()} releases the project at the repository's root, and this one is in ${prefix}.`,
487
+ `Project is in ${prefix}, not the repository's root.\n${cmdDeployGit()} releases only a project at the root.`,
451
488
  `Deploy it with ${cmdDeploy()}, or give it a repository of its own.`,
452
489
  );
453
490
  };
@@ -475,7 +512,7 @@ const gitTagCreateFailed = function (version) {
475
512
  const gitTagPushFailed = function (version) {
476
513
  return blocks(
477
514
  `Unable to push Git tag for v${version}.`,
478
- "Check that you can push to 'origin'.",
515
+ "Check that you can push to origin.",
479
516
  );
480
517
  };
481
518
 
@@ -552,7 +589,7 @@ const imageWidthMin = function (width) {
552
589
  // [function]
553
590
 
554
591
  const timeoutInvalid = function () {
555
- return "The timeout must be a number of seconds above 0.";
592
+ return "`--timeout` must be a number of seconds above 0.";
556
593
  };
557
594
 
558
595
  // [function]
@@ -561,8 +598,8 @@ const timeoutInvalid = function () {
561
598
  // parsed the project again.
562
599
  const issuesAfterBuild = function (count) {
563
600
  return blocks(
564
- `${count} ${count === 1 ? "issue" : "issues"} found after the build.`,
565
- `The build writes files the manifest reads. Run ${cmdValidate()} for details.`,
601
+ `${count} ${count === 1 ? "issue" : "issues"} found after the build.\nThe build writes files the manifest reads.`,
602
+ `Run ${cmdValidate()} for details.`,
566
603
  );
567
604
  };
568
605
 
@@ -578,13 +615,13 @@ const issuesFound = function (count) {
578
615
  // [function]
579
616
 
580
617
  const loginCodeExpired = function () {
581
- return "Login code expired.";
618
+ return blocks("Login code expired.", `Run ${cmdLogin()} again.`);
582
619
  };
583
620
 
584
621
  // [function]
585
622
 
586
623
  const loginFailed = function () {
587
- return "Login request failed.";
624
+ return blocks("Login request failed.", `Run ${cmdLogin()} again.`);
588
625
  };
589
626
 
590
627
  // [function]
@@ -599,10 +636,10 @@ const gameIdMissing = function () {
599
636
  // [function]
600
637
 
601
638
  const lockfileMissing = function () {
602
- return blocks("No lockfile found.", [
603
- "Install your dependencies to create one.",
604
- "Then commit it.",
605
- ]);
639
+ return blocks(
640
+ "No lockfile found.",
641
+ "Install your dependencies to create one, and commit it.",
642
+ );
606
643
  };
607
644
 
608
645
  // [function]
@@ -676,7 +713,7 @@ const packagePathReserved = function (folder) {
676
713
  const packageTooLarge = function (size) {
677
714
  return blocks(
678
715
  `Package is larger than ${size} MB.`,
679
- "Store assets don't count toward it.",
716
+ ".arcadible/ doesn't count toward it.",
680
717
  );
681
718
  };
682
719
 
@@ -700,7 +737,7 @@ const packageUncompressedTooLarge = function (size) {
700
737
  // open it.
701
738
  const releaseFailed = function ({ message }, studio) {
702
739
  return blocks(
703
- (message ?? "").split(/(?<=\.)\s+/u).join("\n"),
740
+ (message ?? "No reason given.").split(/(?<=\.)\s+/u).join("\n"),
704
741
  ...(studio ? [[`${TITLE} Studio:`, studio]] : []),
705
742
  );
706
743
  };
@@ -730,10 +767,10 @@ const releaseTimedOut = function (
730
767
  ]);
731
768
  }
732
769
  if (found) {
733
- return blocks(
734
- head,
735
- `It may still finish. The ${TITLE} check on the tagged commit says how it went.`,
736
- );
770
+ return blocks(head, [
771
+ "It may still finish.",
772
+ `The ${TITLE} check on the tagged commit says how it went.`,
773
+ ]);
737
774
  }
738
775
  return blocks(
739
776
  head,
@@ -741,7 +778,7 @@ const releaseTimedOut = function (
741
778
  "GitHub Actions hasn't released it yet, or its workflow failed:",
742
779
  actions ?? "See the repository's Actions tab.",
743
780
  ],
744
- `If the workflow passed, the ${TITLE} check on the tagged commit says why.`,
781
+ `If the workflow passed, see the ${TITLE} check on the tagged commit.`,
745
782
  );
746
783
  };
747
784
 
@@ -750,7 +787,7 @@ const releaseTimedOut = function (
750
787
  // the spinner while `--wait` waits: on GitHub Actions until the release
751
788
  // exists, then on Arcadible.
752
789
  const releaseWaiting = function (building) {
753
- return building ? "Waiting for GitHub Actions... %s" : "Publishing... %s";
790
+ return building ? "Waiting for GitHub Actions… %s" : "Publishing… %s";
754
791
  };
755
792
 
756
793
  // [function]
@@ -875,9 +912,9 @@ const uploadFailed = function (status) {
875
912
 
876
913
  // [function]
877
914
 
878
- const uploadRateLimited = function (version) {
915
+ const deployRateLimited = function (version) {
879
916
  return blocks(
880
- `v${version} was uploaded in the last 5 minutes.`,
917
+ `v${version} was deployed in the last 5 minutes.`,
881
918
  "Wait, then deploy again.",
882
919
  );
883
920
  };
@@ -1010,6 +1047,7 @@ module.exports = {
1010
1047
  cmdInit,
1011
1048
  cmdInspect,
1012
1049
  deployDirMissing,
1050
+ deployRateLimited,
1013
1051
  didYouMean,
1014
1052
  docAttrInvalid,
1015
1053
  docAttrUnallowed,
@@ -1020,6 +1058,8 @@ module.exports = {
1020
1058
  docMediaType,
1021
1059
  docTagUnallowed,
1022
1060
  errorCode,
1061
+ gameHandleLapsed,
1062
+ gameHandleLimit,
1023
1063
  gameHandleTaken,
1024
1064
  gameIdMissing,
1025
1065
  gameNotDeployed,
@@ -1069,6 +1109,7 @@ module.exports = {
1069
1109
  packageTooManyFiles,
1070
1110
  packageUncompressedTooLarge,
1071
1111
  packed,
1112
+ playLabel,
1072
1113
  portInUse,
1073
1114
  portInvalid,
1074
1115
  pressEnterToOpen,
@@ -1096,6 +1137,7 @@ module.exports = {
1096
1137
  starterPhaser,
1097
1138
  starterPixiJs,
1098
1139
  starterThreeJs,
1140
+ storeLabel,
1099
1141
  tagPushed,
1100
1142
  templateMissing,
1101
1143
  templateUnavailable,
@@ -1107,7 +1149,6 @@ module.exports = {
1107
1149
  unknownField,
1108
1150
  unknownTable,
1109
1151
  uploadFailed,
1110
- uploadRateLimited,
1111
1152
  urlInvalid,
1112
1153
  urlNotFrameable,
1113
1154
  urlUnreachable,
@@ -123,16 +123,20 @@ const GALLERY_DIMENSIONS = {
123
123
  // leaving a field out is how to unset it, so an empty one is always a mistake.
124
124
  const atLeast = function (field, min, unit) {
125
125
  return min === 1
126
- ? message(field, ["Must not be empty. Leave the field out instead."])
127
- : message(field, [`Must be at least: ${min} ${unit}`]);
126
+ ? message(field, ["Must not be empty.", "Leave the field out instead."])
127
+ : message(field, [
128
+ unit === "character"
129
+ ? `Must be at least ${min} characters.`
130
+ : `Must have at least ${min} ${unit}s.`,
131
+ ]);
128
132
  };
129
133
 
130
134
  // [function]
131
135
 
132
- // a required string's error: "Must be defined." and `lines` when it's missing,
136
+ // a required string's error: "Must be set." and `lines` when it's missing,
133
137
  // and zod's own type message (null) when it's there but isn't a string.
134
138
  const required = function (field, lines) {
135
- const missing = message(field, ["Must be defined.", ...lines]).message;
139
+ const missing = message(field, ["Must be set.", ...lines]).message;
136
140
  return {
137
141
  error(issue) {
138
142
  return typeof issue.input === "undefined" ? missing : null;
@@ -144,14 +148,14 @@ const required = function (field, lines) {
144
148
 
145
149
  // a list of distinct values. Without a max, its length is up to the values.
146
150
  const zSet = function (field, schema, { max, min }) {
147
- const list = z.array(schema).min(min, atLeast(field, min, "element"));
151
+ const list = z.array(schema).min(min, atLeast(field, min, "item"));
148
152
  return (
149
153
  max
150
- ? list.max(max, message(field, [`Must not exceed: ${max} elements`]))
154
+ ? list.max(max, message(field, [`Must have at most ${max} items.`]))
151
155
  : list
152
156
  ).refine(
153
157
  (items) => new Set(items).size === items.length,
154
- message(field, ["Must not contain duplicate elements."], ISSUE.DUPLICATE),
158
+ message(field, ["Must not repeat an item."], ISSUE.DUPLICATE),
155
159
  );
156
160
  };
157
161
 
@@ -161,12 +165,12 @@ const zSetBasefile = function (field, schema, { max, min }) {
161
165
  return z
162
166
  .array(schema)
163
167
  .min(min, atLeast(field, min, "file"))
164
- .max(max, message(field, [`Must not exceed: ${max} files`]))
168
+ .max(max, message(field, [`Must have at most ${max} files.`]))
165
169
  .refine(
166
170
  (items) => new Set(items.map(basefile)).size === items.length,
167
171
  message(
168
172
  field,
169
- ["Must not contain duplicate filenames (ignoring extensions)."],
173
+ ["Must not have two files of one name, whatever their extensions."],
170
174
  ISSUE.DUPLICATE,
171
175
  ),
172
176
  );
@@ -183,7 +187,7 @@ const zString = function (field, char, { max, min }) {
183
187
  .min(min, { ...atLeast(field, min, "character"), abort: min === 1 })
184
188
  .refine(
185
189
  (str) => [...str].length <= max,
186
- message(field, [`Must not exceed: ${max} characters`], ISSUE.TOO_LONG),
190
+ message(field, [`Must be at most ${max} characters.`], ISSUE.TOO_LONG),
187
191
  )
188
192
  .superRefine((str, ctx) => {
189
193
  const invalid = invalidChars(str, char);
@@ -458,7 +462,7 @@ const zHandle = function () {
458
462
  const field = "handle";
459
463
  return z.optional(
460
464
  zString(field, /[a-z0-9-]/u, { max: 24, min: 1 })
461
- .min(3, atLeast(field, 3, "characters"))
465
+ .min(3, atLeast(field, 3, "character"))
462
466
  .refine(
463
467
  (str) => !str.startsWith("-") && !str.endsWith("-"),
464
468
  message(
@@ -487,7 +491,7 @@ const zId = function () {
487
491
  required(field, [`Run ${phrase.cmdInit()} to generate a new game ID.`]),
488
492
  )
489
493
  .min(1, { ...atLeast(field, 1), abort: true })
490
- .uuid(message(field, ["Must be a valid UUID."]))
494
+ .uuid(message(field, ["Must be a UUID."]))
491
495
  .refine(
492
496
  (value) => value === value.toLowerCase(),
493
497
  message(field, ["Must be lowercase."], ISSUE.UUID_INVALID),
@@ -514,8 +518,8 @@ const zPrice = function () {
514
518
  return z.optional(
515
519
  z
516
520
  .int()
517
- .min(0, message(field, ["Must not be below: 0"]))
518
- .max(25, message(field, ["Must not be above: 25"])),
521
+ .min(0, message(field, ["Must be 0 or more."]))
522
+ .max(25, message(field, ["Must be 25 or less."])),
519
523
  );
520
524
  };
521
525
 
@@ -547,7 +551,7 @@ const zTrailer = function () {
547
551
  return z
548
552
  .string()
549
553
  .min(1, { ...atLeast(field, 1), abort: true })
550
- .max(255, message(field, ["Must not exceed: 255 characters"]))
554
+ .max(255, message(field, ["Must be at most 255 characters."]))
551
555
  .refine(
552
556
  (url) =>
553
557
  zVimeo().safeParse(url).success || zYouTube().safeParse(url).success,
@@ -561,18 +565,26 @@ const zTrailer = function () {
561
565
  const zVersion = function () {
562
566
  const field = "version";
563
567
  return z
564
- .string(
565
- required(field, ["A version is required unless one can be derived."]),
566
- )
568
+ .string(required(field, ["Set it here, or in package.json."]))
567
569
  .min(1, { ...atLeast(field, 1), abort: true })
568
570
  .refine((value) => validSemver(value) !== null, {
569
- ...message(field, ["Must be valid semver."], ISSUE.VERSION_INVALID),
571
+ ...message(
572
+ field,
573
+ ["Must be a version, such as 1.2.0."],
574
+ ISSUE.VERSION_INVALID,
575
+ ),
570
576
  abort: true,
571
577
  })
572
578
  .refine(
573
579
  // lowercase too, since the database refuses an uppercase prerelease.
574
580
  (value) => cleanSemver(value) === value && value === value.toLowerCase(),
575
- message(field, ["Must be normalized semver."], ISSUE.VERSION_INVALID),
581
+ message(
582
+ field,
583
+ [
584
+ "Must be written as 1.2.0 or 1.2.0-beta.1: lowercase, with no v, spaces, or build metadata.",
585
+ ],
586
+ ISSUE.VERSION_INVALID,
587
+ ),
576
588
  );
577
589
  };
578
590
 
@@ -29,7 +29,8 @@ const main = function (value, { file }) {
29
29
  ...message(
30
30
  "description",
31
31
  [
32
- `${file} has content after its title, tagline, and bio, so it needs two --- rules around the description.`,
32
+ "Needs two --- rules around the description.",
33
+ `${file} has content after its title, tagline, and bio.`,
33
34
  ],
34
35
  cfgcode.ISSUE.DESCRIPTION_RULES_MISSING,
35
36
  ),