arcadible 0.1.43 → 0.1.45

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/bin/arc-build.js CHANGED
@@ -36,6 +36,7 @@ const relative = path.relative;
36
36
  const resolve = path.resolve;
37
37
  const SpinnerWheel = spinner.SpinnerWheel;
38
38
  const warn = output.warn;
39
+ const warning = output.warning;
39
40
 
40
41
  // [declare]
41
42
 
@@ -59,6 +60,8 @@ const start = function (argv) {
59
60
  const root = resolve(process.cwd(), argv.cwd);
60
61
  // the project, parsed once for both steps.
61
62
  let parsed = null;
63
+ // what --json lists under `warnings`.
64
+ let warnings = [];
62
65
  let sw = new SpinnerWheel("Validating... %s");
63
66
  sw.start(json || quiet);
64
67
  return project(root)
@@ -66,13 +69,14 @@ const start = function (argv) {
66
69
  parsed = proj;
67
70
  return validate({ project: parsed, root });
68
71
  })
69
- .then(({ issues, warnings }) => {
70
- const count = issues.length;
72
+ .then((validated) => {
73
+ const count = validated.issues.length;
74
+ warnings = validated.warnings;
71
75
  if (count) {
72
76
  throw new ArcError(
73
77
  cfgcode.VALIDATION_FAILED,
74
78
  phrase.issuesFound(count),
75
- { data: { issues } },
79
+ { data: { issues: validated.issues } },
76
80
  );
77
81
  }
78
82
  sw.stop(true);
@@ -83,7 +87,16 @@ const start = function (argv) {
83
87
  })
84
88
  .then((artifact) => {
85
89
  if (json) {
86
- print({ artifact });
90
+ const missing = artifact?.missing
91
+ ? [
92
+ warning(
93
+ cfgcode.WARNING.DEPLOY_DIR_NOT_BUILT,
94
+ phrase.buildOutputMissing(relative(root, artifact.missing)),
95
+ "workflow.deploy_dir",
96
+ ),
97
+ ]
98
+ : [];
99
+ print({ artifact, warnings: [...warnings, ...missing] });
87
100
  return;
88
101
  }
89
102
  if (!quiet) {
@@ -103,7 +116,9 @@ const start = function (argv) {
103
116
  log.hs("Nothing to do.");
104
117
  }
105
118
  })
106
- .catch((ex) => abort("Build aborted.", ex, { json, sw }));
119
+ .catch((ex) =>
120
+ abort("Build aborted.", ex, { data: { warnings }, json, sw }),
121
+ );
107
122
  };
108
123
 
109
124
  // [function]
package/bin/arc-deploy.js CHANGED
@@ -42,6 +42,7 @@ const ProgressBar = progress.ProgressBar;
42
42
  const resolve = path.resolve;
43
43
  const SpinnerWheel = spinner.SpinnerWheel;
44
44
  const warn = output.warn;
45
+ const warning = output.warning;
45
46
 
46
47
  // [declare]
47
48
 
@@ -65,7 +66,9 @@ const conf = {
65
66
 
66
67
  // [function]
67
68
 
68
- const handler = function ({ json, pb, quiet, sw }) {
69
+ // `warnings` collects what --json lists, such as a version that couldn't be
70
+ // checked.
71
+ const handler = function ({ json, pb, quiet, sw, warnings }) {
69
72
  return {
70
73
  onAccountRefresh(refresh) {
71
74
  sw.start(json || quiet);
@@ -76,7 +79,10 @@ const handler = function ({ json, pb, quiet, sw }) {
76
79
  pb.start();
77
80
  }
78
81
  },
79
- onLogInfo({ level, message }) {
82
+ onLogInfo({ code, level, message }) {
83
+ if (code) {
84
+ warnings.push(warning(code, message));
85
+ }
80
86
  if (!json && !quiet) {
81
87
  switch (level) {
82
88
  case "warn":
@@ -253,6 +259,8 @@ const start = function (argv) {
253
259
  // the project, parsed once and given to each step. A build can write files
254
260
  // the manifest reads, such as art, so it's parsed again after one runs.
255
261
  let parsed = null;
262
+ // what --json lists under `warnings`: arc validate's, then the deploy's.
263
+ const warnings = [];
256
264
  let sw = new SpinnerWheel("Validating... %s");
257
265
  sw.start(json || quiet);
258
266
  return checkTimeout(timeout)
@@ -261,26 +269,29 @@ const start = function (argv) {
261
269
  parsed = proj;
262
270
  return validate({ project: parsed, root });
263
271
  })
264
- .then(({ issues, warnings }) => {
265
- const count = issues.length;
272
+ .then((validated) => {
273
+ const count = validated.issues.length;
274
+ warnings.push(...validated.warnings);
266
275
  if (count) {
267
276
  throw new ArcError(
268
277
  cfgcode.VALIDATION_FAILED,
269
278
  phrase.issuesFound(count),
270
- { data: { issues } },
279
+ { data: { issues: validated.issues } },
271
280
  );
272
281
  }
273
282
  sw.stop(true);
274
- warn(warnings, { json, quiet });
283
+ warn(validated.warnings, { json, quiet });
275
284
  sw = new SpinnerWheel("Checking... %s");
276
285
  // git deploys need no login: anyone who can push to the repository can
277
286
  // release, and the Arcadible GitHub App publishes the release. Anyone
278
287
  // can wait for it too; the game's owner, logged in, waits among their
279
288
  // own releases, which also say when the repository isn't connected.
280
289
  if (git) {
281
- return whoamiIfAny({ handler: handler({ json, pb, quiet, sw }) });
290
+ return whoamiIfAny({
291
+ handler: handler({ json, pb, quiet, sw, warnings }),
292
+ });
282
293
  }
283
- return whoami({ handler: handler({ json, pb, quiet, sw }) });
294
+ return whoami({ handler: handler({ json, pb, quiet, sw, warnings }) });
284
295
  })
285
296
  .then((current) => {
286
297
  if (!current && !git) {
@@ -295,7 +306,7 @@ const start = function (argv) {
295
306
  predeploy({
296
307
  current,
297
308
  git,
298
- handler: handler({ json, pb, quiet, sw }),
309
+ handler: handler({ json, pb, quiet, sw, warnings }),
299
310
  project: parsed,
300
311
  root,
301
312
  }),
@@ -359,7 +370,7 @@ const start = function (argv) {
359
370
  artifact,
360
371
  current,
361
372
  git,
362
- handler: handler({ json, pb, quiet, sw }),
373
+ handler: handler({ json, pb, quiet, sw, warnings }),
363
374
  project: parsed,
364
375
  root,
365
376
  }),
@@ -376,7 +387,7 @@ const start = function (argv) {
376
387
  const shown = { ...deployed };
377
388
  delete shown.game;
378
389
  delete shown.release;
379
- const result = git ? shown : { built, ...shown };
390
+ const result = { ...(git ? shown : { built, ...shown }), warnings };
380
391
  sw.stop(true);
381
392
  if (argv.wait) {
382
393
  return waitAndReport(current, deployed, result, {
@@ -396,7 +407,9 @@ const start = function (argv) {
396
407
  }
397
408
  return null;
398
409
  })
399
- .catch((ex) => abort("Deploy aborted.", ex, { json, pb, sw }));
410
+ .catch((ex) =>
411
+ abort("Deploy aborted.", ex, { data: { warnings }, json, pb, sw }),
412
+ );
400
413
  };
401
414
 
402
415
  // [function]
package/bin/arc-pack.js CHANGED
@@ -60,6 +60,8 @@ const start = function (argv) {
60
60
  const root = resolve(process.cwd(), argv.cwd);
61
61
  // the project, parsed once for both steps.
62
62
  let parsed = null;
63
+ // what --json lists under `warnings`.
64
+ let warnings = [];
63
65
  let sw = new SpinnerWheel("Validating... %s");
64
66
  sw.start(json || quiet);
65
67
  return project(root)
@@ -67,13 +69,14 @@ const start = function (argv) {
67
69
  parsed = proj;
68
70
  return validate({ project: parsed, root });
69
71
  })
70
- .then(({ issues, warnings }) => {
71
- const count = issues.length;
72
+ .then((validated) => {
73
+ const count = validated.issues.length;
74
+ warnings = validated.warnings;
72
75
  if (count) {
73
76
  throw new ArcError(
74
77
  cfgcode.VALIDATION_FAILED,
75
78
  phrase.issuesFound(count),
76
- { data: { issues } },
79
+ { data: { issues: validated.issues } },
77
80
  );
78
81
  }
79
82
  sw.stop(true);
@@ -89,7 +92,7 @@ const start = function (argv) {
89
92
  })
90
93
  .then((artifact) => {
91
94
  if (json) {
92
- print({ artifact });
95
+ print({ artifact, warnings });
93
96
  return;
94
97
  }
95
98
  if (!quiet) {
@@ -98,7 +101,9 @@ const start = function (argv) {
98
101
  print(artifact, { nl: true, toml: true });
99
102
  }
100
103
  })
101
- .catch((ex) => abort("Pack aborted.", ex, { json, sw }));
104
+ .catch((ex) =>
105
+ abort("Pack aborted.", ex, { data: { warnings }, json, sw }),
106
+ );
102
107
  };
103
108
 
104
109
  // [function]
package/package.json CHANGED
@@ -75,5 +75,5 @@
75
75
  "scripts": {
76
76
  "test": "node --test \"test/**/*.test.js\""
77
77
  },
78
- "version": "0.1.43"
78
+ "version": "0.1.45"
79
79
  }
@@ -28,7 +28,7 @@ const POLL = {
28
28
  interval: time.s(15),
29
29
  maxDelay: time.s(5),
30
30
  timeoutError: () =>
31
- new ArcError(cfgcode.LOGIN_EXPIRED, phrase.loginExpired()),
31
+ new ArcError(cfgcode.LOGIN_CODE_EXPIRED, phrase.loginCodeExpired()),
32
32
  };
33
33
 
34
34
  // [function]
@@ -73,7 +73,10 @@ const pollFlow = function (value, onLoginPoll) {
73
73
  case "failed":
74
74
  throw new ArcError(cfgcode.LOGIN_FAILED, phrase.loginFailed());
75
75
  default:
76
- throw new ArcError(cfgcode.LOGIN_EXPIRED, phrase.loginExpired());
76
+ throw new ArcError(
77
+ cfgcode.LOGIN_CODE_EXPIRED,
78
+ phrase.loginCodeExpired(),
79
+ );
77
80
  }
78
81
  });
79
82
  };
@@ -3,7 +3,7 @@
3
3
  // [import]
4
4
 
5
5
  const links = require("../../util/links");
6
- const project = require("../../parser/project");
6
+ const self = require("../../parser/self");
7
7
 
8
8
  // [function]
9
9
 
@@ -20,7 +20,7 @@ const open = function (id) {
20
20
  // [function]
21
21
 
22
22
  const openSelf = function (root) {
23
- return project(root).then(({ game }) => openUrl(game?.handle ?? game?.id));
23
+ return self(root).then((game) => openUrl(game?.handle ?? game?.id));
24
24
  };
25
25
 
26
26
  // [function]
@@ -44,6 +44,8 @@ const SIZE_PARTIAL = size.mb(64);
44
44
 
45
45
  // [function]
46
46
 
47
+ // the files' own limits come first, since they name what to fix, such as
48
+ // the one file over 64 MB that makes the zip too large.
47
49
  const verifyPartial = function (zip) {
48
50
  if (!zip.has(MAIN)) {
49
51
  throw new ArcError(
@@ -51,6 +53,7 @@ const verifyPartial = function (zip) {
51
53
  phrase.packageFileMissing(MAIN),
52
54
  );
53
55
  }
56
+ limits.check(zip.entries());
54
57
  if (zip.stat().size > SIZE_PARTIAL) {
55
58
  throw new ArcError(
56
59
  cfgcode.PACKAGE_TOO_LARGE,
@@ -69,15 +72,26 @@ const verifyTotal = function (zip) {
69
72
 
70
73
  // [function]
71
74
 
72
- const pack = function (name, game, { inputPath, mediaPath, outputPath }) {
75
+ // `deployDir` when the input is deploy_dir, rather than a folder --input
76
+ // named: a missing deploy_dir means the build hasn't written it.
77
+ const pack = function (
78
+ name,
79
+ game,
80
+ { deployDir, inputPath, mediaPath, outputPath },
81
+ ) {
73
82
  const zip = new Zip(name);
74
83
  return pathExists(inputPath)
75
84
  .then((existsInput) => {
76
85
  if (!existsInput) {
77
- throw new ArcError(
78
- cfgcode.PATH_NOT_FOUND,
79
- phrase.unableToFind(inputPath),
80
- );
86
+ throw deployDir
87
+ ? new ArcError(
88
+ cfgcode.DEPLOY_DIR_MISSING,
89
+ phrase.deployDirMissing(inputPath),
90
+ )
91
+ : new ArcError(
92
+ cfgcode.PATH_NOT_FOUND,
93
+ phrase.unableToFind(inputPath),
94
+ );
81
95
  }
82
96
  zip.addDir(inputPath);
83
97
  return zip;
@@ -105,8 +119,11 @@ const main = function ({ input, output, ...conf }) {
105
119
  return given(conf).then(({ game, workflow }) => {
106
120
  const name = game.handle ?? game.id;
107
121
  const file = output ?? `${OUTPUT}/${name}-v${game.version}.zip`;
122
+ const deployDir = resolve(root, workflow.deploy_dir);
123
+ const inputPath = resolve(root, input ?? workflow.deploy_dir);
108
124
  return pack(parse(file).name, game, {
109
- inputPath: resolve(root, input ?? workflow.deploy_dir),
125
+ deployDir: inputPath === deployDir,
126
+ inputPath,
110
127
  mediaPath: resolve(root, MEDIA),
111
128
  outputPath: resolve(root, file),
112
129
  });
@@ -7,7 +7,7 @@ const cfgcode = require("../../config/code");
7
7
  const error = require("../../util/error");
8
8
  const links = require("../../util/links");
9
9
  const phrase = require("../../phrase");
10
- const project = require("../../parser/project");
10
+ const self = require("../../parser/self");
11
11
 
12
12
  // [use]
13
13
 
@@ -78,8 +78,8 @@ const play = function (id, release) {
78
78
  // this project's game: its orientation comes from the manifest. With a
79
79
  // chosen release, it makes no API call.
80
80
  const playSelf = function (root, release) {
81
- return project(root)
82
- .then(({ game }) => {
81
+ return self(root)
82
+ .then((game) => {
83
83
  const id = game?.handle ?? game?.id;
84
84
  if (!id) {
85
85
  return [null, null];
@@ -14,16 +14,19 @@ const ArcError = error.ArcError;
14
14
 
15
15
  // [function]
16
16
 
17
- // fails when `idOrHandle` is another game's, or this game's on another
18
- // account. A game that isn't on Arcadible yet passes.
19
- const ensureGame = function (current, idOrHandle, id) {
20
- return api.discoverGame(idOrHandle).then(
17
+ // fails when the game is on Arcadible on another account: it was deleted
18
+ // (the API doesn't find it, and the upload is refused), or it's someone
19
+ // else's. A game that isn't on Arcadible yet passes.
20
+ const ensureOwned = function (current, id) {
21
+ return api.discoverGame(id).then(
21
22
  (game) => {
22
- // a token's account isn't known yet, so only the id is compared; the
23
- // server checks ownership when it authorizes the upload.
24
- const owned = current?.token || game.account === current?.user?.id;
25
- if (!owned || game.id !== id) {
26
- throw new ArcError(cfgcode.GAME_EXISTS, phrase.gameExists(idOrHandle));
23
+ // a token's account isn't known yet; the server checks ownership when
24
+ // it authorizes the upload.
25
+ if (!current?.token && game.account !== current?.user?.id) {
26
+ throw new ArcError(
27
+ cfgcode.GAME_UNAVAILABLE,
28
+ phrase.gameUnavailable(id),
29
+ );
27
30
  }
28
31
  },
29
32
  (err) => {
@@ -36,12 +39,27 @@ const ensureGame = function (current, idOrHandle, id) {
36
39
 
37
40
  // [function]
38
41
 
39
- // a game without a handle has none to check.
40
- const ensureHandle = function (current, { handle, id }) {
42
+ // fails when the handle is another game's, whoever owns it. A game without a
43
+ // handle has none to check.
44
+ const ensureHandle = function ({ handle, id }) {
41
45
  if ((handle ?? null) === null) {
42
46
  return Promise.resolve(null);
43
47
  }
44
- return ensureGame(current, handle, id);
48
+ return api.discoverGame(handle).then(
49
+ (game) => {
50
+ if (game.id !== id) {
51
+ throw new ArcError(
52
+ cfgcode.GAME_HANDLE_TAKEN,
53
+ phrase.gameHandleTaken(handle),
54
+ );
55
+ }
56
+ },
57
+ (err) => {
58
+ if (err.status !== 404) {
59
+ throw err;
60
+ }
61
+ },
62
+ );
45
63
  };
46
64
 
47
65
  // [function]
@@ -49,8 +67,8 @@ const ensureHandle = function (current, { handle, id }) {
49
67
  // resolves with the game once its id, and its handle if it has one, are
50
68
  // free or already its own.
51
69
  const ensureId = function (current, game) {
52
- return ensureGame(current, game.id, game.id)
53
- .then(() => ensureHandle(current, game))
70
+ return ensureOwned(current, game.id)
71
+ .then(() => ensureHandle(game))
54
72
  .then(() => game);
55
73
  };
56
74
 
@@ -77,11 +77,28 @@ const ensureClean = function (root) {
77
77
 
78
78
  // [function]
79
79
 
80
+ // fails with git_remote_missing when there's no origin, and with
81
+ // git_remote_unreachable when it's there but can't be reached.
80
82
  const ensureRemote = function (root) {
81
- return git(root, ["ls-remote", "origin"]).then(
82
- () => null,
83
- failWith(cfgcode.GIT_REMOTE_UNAVAILABLE, phrase.gitRemoteMissing()),
84
- );
83
+ return git(root, ["remote", "get-url", "origin"])
84
+ .then(
85
+ () => null,
86
+ failWith(cfgcode.GIT_REMOTE_MISSING, phrase.gitRemoteMissing()),
87
+ )
88
+ .then(() => git(root, ["ls-remote", "origin"]))
89
+ .then(
90
+ () => null,
91
+ // git_remote_missing passes through.
92
+ (err) => {
93
+ if (err instanceof ArcError) {
94
+ throw err;
95
+ }
96
+ failWith(
97
+ cfgcode.GIT_REMOTE_UNREACHABLE,
98
+ phrase.gitRemoteUnreachable(),
99
+ )(err);
100
+ },
101
+ );
85
102
  };
86
103
 
87
104
  // [function]
@@ -168,7 +185,7 @@ const hasTagLocal = function (root, tag) {
168
185
  const hasTagRemote = function (root, tag) {
169
186
  return git(root, ["ls-remote", "--tags", "origin", `refs/tags/${tag}`]).then(
170
187
  (stdout) => Boolean(stdout.trim()),
171
- failWith(cfgcode.GIT_REMOTE_UNAVAILABLE, phrase.gitRemoteMissing()),
188
+ failWith(cfgcode.GIT_REMOTE_UNREACHABLE, phrase.gitRemoteUnreachable()),
172
189
  );
173
190
  };
174
191
 
@@ -57,6 +57,7 @@ const ensurePublishable = function ({ handle, id, version }, handler) {
57
57
  if (game === null || build === null) {
58
58
  if (handler.onLogInfo) {
59
59
  handler.onLogInfo({
60
+ code: cfgcode.WARNING.VERSION_UNCHECKED,
60
61
  level: "warn",
61
62
  message: phrase.versionUnchecked(version),
62
63
  });
@@ -11,7 +11,7 @@ const workflow = require("../../schema/workflow");
11
11
  // [use]
12
12
 
13
13
  const closest = strs.closest;
14
- const ISSUE = cfgcode.ISSUE;
14
+ const WARNING = cfgcode.WARNING;
15
15
 
16
16
  // [function]
17
17
 
@@ -29,7 +29,7 @@ const known = function (root) {
29
29
  // it's reported without failing, with the name it might have meant.
30
30
  const warning = function (path, message, near) {
31
31
  return {
32
- code: ISSUE.FIELD_UNKNOWN,
32
+ code: WARNING.FIELD_UNKNOWN,
33
33
  details: near ? [`Did you mean: ${near}?`] : [],
34
34
  link: null,
35
35
  message,
@@ -8,24 +8,26 @@
8
8
 
9
9
  // the CLI's own codes.
10
10
  const CLI = {
11
- API_ERROR: "api_error",
11
+ API_REFUSED: "api_refused",
12
+ API_UNREACHABLE: "api_unreachable",
12
13
  ARGUMENT_INVALID: "argument_invalid",
13
14
  BUILD_FAILED: "build_failed",
14
- GAME_EXISTS: "game_exists",
15
+ DEPLOY_DIR_MISSING: "deploy_dir_missing",
16
+ GAME_HANDLE_TAKEN: "game_handle_taken",
15
17
  GAME_ID_MISSING: "game_id_missing",
16
18
  GAME_NOT_DEPLOYED: "game_not_deployed",
17
19
  GAME_NOT_FOUND: "game_not_found",
18
- GIT_REMOTE_UNAVAILABLE: "git_remote_unavailable",
20
+ GIT_REMOTE_MISSING: "git_remote_missing",
21
+ GIT_REMOTE_UNREACHABLE: "git_remote_unreachable",
19
22
  GIT_REPO_MISSING: "git_repo_missing",
20
23
  GIT_TAG_CREATE_FAILED: "git_tag_create_failed",
21
24
  GIT_TAG_EXISTS: "git_tag_exists",
22
25
  GIT_TAG_PUSH_FAILED: "git_tag_push_failed",
23
26
  GIT_UNCOMMITTED: "git_uncommitted",
24
27
  LOCKFILE_MISSING: "lockfile_missing",
25
- LOGIN_EXPIRED: "login_expired",
28
+ LOGIN_CODE_EXPIRED: "login_code_expired",
26
29
  LOGIN_FAILED: "login_failed",
27
30
  MANIFEST_MISSING: "manifest_missing",
28
- NETWORK_ERROR: "network_error",
29
31
  NOT_LOGGED_IN: "not_logged_in",
30
32
  PACKAGE_FILE_TOO_LARGE: "package_file_too_large",
31
33
  PACKAGE_INDEX_MISSING: "package_index_missing",
@@ -48,24 +50,26 @@ const CLI = {
48
50
  // thing wherever it appears: GAME_UNAVAILABLE, INTERNAL, REPO_NOT_CONNECTED,
49
51
  // and VERSION_EXISTS are the CLI's too.
50
52
  const RELEASE = {
51
- APP_NOT_INSTALLED: "app_not_installed",
52
- EXPIRED: "expired",
53
+ BUILD_RATE_LIMITED: "build_rate_limited",
53
54
  GAME_LIMIT_REACHED: "game_limit_reached",
54
55
  GAME_MISMATCH: "game_mismatch",
55
56
  GAME_UNAVAILABLE: "game_unavailable",
57
+ GITHUB_APP_NOT_INSTALLED: "github_app_not_installed",
56
58
  INTERNAL: "internal",
57
59
  MANIFEST_INVALID: "manifest_invalid",
58
- RATE_LIMITED: "rate_limited",
60
+ RELEASE_EXPIRED: "release_expired",
59
61
  REPO_NOT_CONNECTED: "repo_not_connected",
60
62
  VERSION_EXISTS: "version_exists",
63
+ VERSION_MISMATCH: "version_mismatch",
61
64
  ZIP_INVALID: "zip_invalid",
65
+ ZIP_MISSING: "zip_missing",
62
66
  ZIP_TOO_LARGE: "zip_too_large",
63
67
  };
64
68
  // the Arcadible check's own codes on GitHub, for a `git` release that fails
65
- // before there's a release (the queue publisher Lambda). `arc deploy git`
69
+ // before there's a release (the queue publisher and subscriber Lambdas). `arc deploy git`
66
70
  // checks for workflow_missing before it pushes.
67
71
  const GITHUB = {
68
- TAG_INVALID: "tag_invalid",
72
+ GIT_TAG_INVALID: "git_tag_invalid",
69
73
  WORKFLOW_MISSING: "workflow_missing",
70
74
  };
71
75
  // an `arc validate` issue's codes, named for the rule it breaks; the field
@@ -76,7 +80,6 @@ const ISSUE = {
76
80
  DESCRIPTION_RULES_MISSING: "description_rules_missing",
77
81
  DUPLICATE: "duplicate",
78
82
  EMPTY: "empty",
79
- FIELD_UNKNOWN: "field_unknown",
80
83
  INDEX_MISSING: "index_missing",
81
84
  INVALID: "invalid",
82
85
  MARKDOWN_INVALID: "markdown_invalid",
@@ -93,6 +96,14 @@ const ISSUE = {
93
96
  UUID_INVALID: "uuid_invalid",
94
97
  VERSION_INVALID: "version_invalid",
95
98
  };
99
+ // a warning's codes: what's reported without failing, in an issue's shape.
100
+ // they follow the same rules, and share the Errors page, so no warning code
101
+ // is also an error or issue code.
102
+ const WARNING = {
103
+ DEPLOY_DIR_NOT_BUILT: "deploy_dir_not_built",
104
+ FIELD_UNKNOWN: "field_unknown",
105
+ VERSION_UNCHECKED: "version_unchecked",
106
+ };
96
107
  // every error code, for checking one exists.
97
108
  const ALL = new Set([
98
109
  ...Object.values(CLI),
@@ -101,6 +112,8 @@ const ALL = new Set([
101
112
  ]);
102
113
  // every issue code.
103
114
  const ISSUES = new Set(Object.values(ISSUE));
115
+ // every warning code.
116
+ const WARNINGS = new Set(Object.values(WARNING));
104
117
 
105
118
  // [export]
106
119
 
@@ -113,4 +126,6 @@ module.exports = {
113
126
  ISSUE,
114
127
  ISSUES,
115
128
  RELEASE,
129
+ WARNING,
130
+ WARNINGS,
116
131
  };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const cfgcode = require("../config/code");
6
+ const project = require("./project");
7
+
8
+ // [function]
9
+
10
+ // the game of the project at `root`, for `arc play` and `arc open` with no
11
+ // game named; null outside a project, so they ask for a game
12
+ // (game_id_missing) rather than for a project.
13
+ const main = function (root) {
14
+ return project(root).then(
15
+ ({ game }) => game,
16
+ (ex) => {
17
+ if (ex.code === cfgcode.MANIFEST_MISSING) {
18
+ return null;
19
+ }
20
+ throw ex;
21
+ },
22
+ );
23
+ };
24
+
25
+ // [export]
26
+
27
+ module.exports = main;
package/src/phrase.js CHANGED
@@ -101,6 +101,15 @@ const cmdValidate = function () {
101
101
 
102
102
  // [function]
103
103
 
104
+ const deployDirMissing = function (dir) {
105
+ return blocks(
106
+ `Unable to find deploy_dir: ${dir}`,
107
+ "Run `arc build`, or set deploy_dir to the folder your build writes.",
108
+ );
109
+ };
110
+
111
+ // [function]
112
+
104
113
  const docAttrInvalid = function (tag, attr, value) {
105
114
  return `Value '${value}' is not valid for the '${attr}' attribute on the '${tag}' tag.`;
106
115
  };
@@ -156,8 +165,11 @@ const errorCode = function (code) {
156
165
 
157
166
  // [function]
158
167
 
159
- const gameExists = function (idOrHandle) {
160
- return `Game exists: ${idOrHandle}`;
168
+ const gameHandleTaken = function (handle) {
169
+ return blocks(
170
+ `Handle taken: ${handle}`,
171
+ "Choose another handle in arcadible.toml.",
172
+ );
161
173
  };
162
174
 
163
175
  // [function]
@@ -207,8 +219,17 @@ const gitChangesPresent = function () {
207
219
 
208
220
  const gitRemoteMissing = function () {
209
221
  return blocks(
210
- "Unable to access Git remote 'origin'.",
211
- "Check that it exists and that you can access it.",
222
+ "No Git remote 'origin'.",
223
+ "Add the game's GitHub repository as origin.",
224
+ );
225
+ };
226
+
227
+ // [function]
228
+
229
+ const gitRemoteUnreachable = function () {
230
+ return blocks(
231
+ "Unable to reach Git remote 'origin'.",
232
+ "Check your connection, and that you can access it.",
212
233
  );
213
234
  };
214
235
 
@@ -357,8 +378,8 @@ const issuesFound = function (count) {
357
378
 
358
379
  // [function]
359
380
 
360
- const loginExpired = function () {
361
- return "Login request expired.";
381
+ const loginCodeExpired = function () {
382
+ return "Login code expired.";
362
383
  };
363
384
 
364
385
  // [function]
@@ -372,7 +393,7 @@ const loginFailed = function () {
372
393
  const noGameId = function () {
373
394
  return blocks(
374
395
  "No game handle or ID.",
375
- `Pass one, or run ${cmdInit()} to add an ID.`,
396
+ `Pass one, or run it in a project after ${cmdInit()} has added an ID.`,
376
397
  );
377
398
  };
378
399
 
@@ -469,10 +490,9 @@ const packageUncompressedTooLarge = function (size) {
469
490
  // and where to see it.
470
491
  // `studio` is the game's builds in Studio, for its owner; anyone else can't
471
492
  // open it.
472
- const releaseFailed = function ({ message, reason }, studio) {
493
+ const releaseFailed = function ({ message }, studio) {
473
494
  return blocks(
474
495
  (message ?? "").split(/(?<=\.)\s+/u).join("\n"),
475
- `Reason: ${reason}.`,
476
496
  ...(studio ? [[`${TITLE} Studio:`, studio]] : []),
477
497
  );
478
498
  };
@@ -767,6 +787,7 @@ module.exports = {
767
787
  buildSubmitted,
768
788
  cmdInit,
769
789
  cmdInspect,
790
+ deployDirMissing,
770
791
  docAttrInvalid,
771
792
  docAttrUnallowed,
772
793
  docCommentUnallowed,
@@ -776,13 +797,14 @@ module.exports = {
776
797
  docTagUnallowed,
777
798
  docUnsupported,
778
799
  errorCode,
779
- gameExists,
800
+ gameHandleTaken,
780
801
  gameNotDeployed,
781
802
  gameNotFound,
782
803
  gameRepoMissing,
783
804
  gameUnavailable,
784
805
  gitChangesPresent,
785
806
  gitRemoteMissing,
807
+ gitRemoteUnreachable,
786
808
  gitRepoMissing,
787
809
  gitTagCreateFailed,
788
810
  gitTagPushFailed,
@@ -799,7 +821,7 @@ module.exports = {
799
821
  imageWidthMin,
800
822
  issuesAfterBuild,
801
823
  issuesFound,
802
- loginExpired,
824
+ loginCodeExpired,
803
825
  loginFailed,
804
826
  noGameId,
805
827
  noLockFile,
package/src/util/error.js CHANGED
@@ -32,14 +32,15 @@ const ArcError = class extends Error {
32
32
 
33
33
  // [function]
34
34
 
35
- // the code a failure reports: its own, api_error for a request the API
36
- // refused, or internal for anything else, which is a bug in the CLI.
35
+ // the code a failure reports: its own, api_refused for a request the API
36
+ // refused, or internal for a failure on Arcadible's side (a 5xx) and for
37
+ // anything else, which is a bug in the CLI.
37
38
  const codeOf = function (ex) {
38
39
  if (ex instanceof ArcError) {
39
40
  return ex.code;
40
41
  }
41
- if (ex instanceof HttpError) {
42
- return cfgcode.API_ERROR;
42
+ if (ex instanceof HttpError && !(ex.status >= 500)) {
43
+ return cfgcode.API_REFUSED;
43
44
  }
44
45
  return cfgcode.INTERNAL;
45
46
  };
@@ -66,7 +66,7 @@ const print = function (data, { key, nl, toml } = {}) {
66
66
  // release that failed. `nl` puts a blank line before the heading, after
67
67
  // output the command has already printed, such as arc login's link.
68
68
  // `code` is a failed release's reason, which the API may add to, so it isn't
69
- // checked, and its message already says it.
69
+ // checked.
70
70
  const abort = function (
71
71
  heading,
72
72
  ex,
@@ -74,12 +74,15 @@ const abort = function (
74
74
  ) {
75
75
  const message = ex.message;
76
76
  const status = ex.status;
77
+ const shown = code ?? codeOf(ex);
78
+ const link = `${cfgurl.ERRORS}#${shown}`;
77
79
  if (json) {
78
80
  print({
79
81
  ...ex.data,
80
82
  ...data,
81
83
  error: {
82
- code: code ?? codeOf(ex),
84
+ code: shown,
85
+ link,
83
86
  message,
84
87
  ...(status ? { status } : {}),
85
88
  },
@@ -97,12 +100,7 @@ const abort = function (
97
100
  log.hd(heading);
98
101
  }
99
102
  log.pd(message);
100
- if (!code) {
101
- log.nlpd([
102
- phrase.errorCode(codeOf(ex)),
103
- chalk.cyan.underline(`${cfgurl.ERRORS}#${codeOf(ex)}`),
104
- ]);
105
- }
103
+ log.nlpd([phrase.errorCode(shown), chalk.cyan.underline(link)]);
106
104
  }
107
105
  process.exit(1);
108
106
  };
@@ -119,6 +117,21 @@ const warn = function (warnings, { json, quiet } = {}) {
119
117
  warnings.forEach((each) => log.pw(`⚠ ${schema.print(each)}`));
120
118
  };
121
119
 
120
+ // [function]
121
+
122
+ // a warning of the command's own, in an issue's shape, as --json lists it
123
+ // under `warnings` beside arc validate's: its code, and the link to it on
124
+ // the Errors page. `path` is the field it's about, if any.
125
+ const warning = function (code, message, path = null) {
126
+ return {
127
+ code,
128
+ details: [],
129
+ link: `${cfgurl.ERRORS}#${code}`,
130
+ message,
131
+ path,
132
+ };
133
+ };
134
+
122
135
  // [export]
123
136
 
124
- module.exports = { abort, print, printJson, printToml, warn };
137
+ module.exports = { abort, print, printJson, printToml, warn, warning };
@@ -53,7 +53,7 @@ const parse = function (resp) {
53
53
 
54
54
  // sends a request to a full URL. `headers` are added to the client's own, and
55
55
  // a form is sent as JSON. A request that gets no response, such as when
56
- // offline, rejects as network_error.
56
+ // offline, rejects as api_unreachable.
57
57
  const send = function (method, url, form, headers = {}) {
58
58
  const all = {
59
59
  [`X-${CLIENT}-Client-Id`]: ID,
@@ -66,7 +66,7 @@ const send = function (method, url, form, headers = {}) {
66
66
  headers: all,
67
67
  method,
68
68
  }).then(parse, (ex) => {
69
- throw new ArcError(cfgcode.NETWORK_ERROR, phrase.unableToReach(), {
69
+ throw new ArcError(cfgcode.API_UNREACHABLE, phrase.unableToReach(), {
70
70
  cause: ex,
71
71
  });
72
72
  });
package/src/util/s3.js CHANGED
@@ -32,7 +32,7 @@ const requestOpts = function (size) {
32
32
  // [function]
33
33
 
34
34
  // puts the file to the upload link, and resolves once S3 accepts it. A
35
- // request that gets no response rejects as network_error; a file that can't
35
+ // request that gets no response rejects as api_unreachable; a file that can't
36
36
  // be read rejects with its own error.
37
37
  const upload = function (url, { path, size }, onProgress) {
38
38
  return new Promise((resolve, reject) => {
@@ -49,7 +49,7 @@ const upload = function (url, { path, size }, onProgress) {
49
49
  req.on("error", (ex) => {
50
50
  reject(
51
51
  readError ??
52
- new ArcError(cfgcode.NETWORK_ERROR, phrase.unableToReach(), {
52
+ new ArcError(cfgcode.API_UNREACHABLE, phrase.unableToReach(), {
53
53
  cause: ex,
54
54
  }),
55
55
  );