arcadible 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <h1 align="center">Arcadible CLI</h1>
8
8
 
9
9
  <p align="center">
10
- Create, play, and share games made with AI.
10
+ Ship your game to players in one command.
11
11
  </p>
12
12
 
13
13
  <p align="center">
package/package.json CHANGED
@@ -65,5 +65,5 @@
65
65
  "scripts": {
66
66
  "test": "node --test \"test/**/*.test.js\""
67
67
  },
68
- "version": "1.0.4"
68
+ "version": "1.0.6"
69
69
  }
@@ -35,9 +35,13 @@ const execBuild = function (command, { cwdPath }) {
35
35
  if (err) {
36
36
  const { code, signal } = err;
37
37
  reject(
38
- new ArcError(cfgcode.BUILD_FAILED, phrase.buildFailed({ code, signal, stderr }), {
39
- cause: err,
40
- }),
38
+ new ArcError(
39
+ cfgcode.BUILD_FAILED,
40
+ phrase.buildFailed({ code, signal, stderr, stdout }),
41
+ {
42
+ cause: err,
43
+ },
44
+ ),
41
45
  );
42
46
  return;
43
47
  }
@@ -12,7 +12,6 @@ const repo = require("../predeploy/git");
12
12
  // [use]
13
13
 
14
14
  const ArcError = error.ArcError;
15
- const failWith = repo.failWith;
16
15
  const git = repo.git;
17
16
  const tagOf = repo.tagOf;
18
17
 
@@ -25,7 +24,13 @@ const createTag = function (root, version) {
25
24
  : phrase.versionPrerelease(version);
26
25
  return git(root, ["tag", "-a", tagOf(version), "-m", message]).then(
27
26
  () => null,
28
- failWith(cfgcode.GIT_TAG_CREATE_FAILED, phrase.gitTagCreateFailed(version)),
27
+ (err) => {
28
+ throw new ArcError(
29
+ cfgcode.GIT_TAG_CREATE_FAILED,
30
+ phrase.gitTagCreateFailed(version, err.stderr),
31
+ { cause: err },
32
+ );
33
+ },
29
34
  );
30
35
  };
31
36
 
@@ -52,7 +57,7 @@ const pushTag = function (root, version) {
52
57
  version,
53
58
  new ArcError(
54
59
  cfgcode.GIT_TAG_PUSH_FAILED,
55
- phrase.gitTagPushFailed(version),
60
+ phrase.gitTagPushFailed(version, err.stderr),
56
61
  {
57
62
  cause: err,
58
63
  },
@@ -15,6 +15,7 @@ const cfgcode = require("../../config/code");
15
15
  const error = require("../../util/error");
16
16
  const manifest = require("./manifest");
17
17
  const phrase = require("../../phrase");
18
+ const templates = require("../../config/template");
18
19
 
19
20
  // [use]
20
21
 
@@ -38,10 +39,14 @@ const removeLicense = function (dest) {
38
39
 
39
40
  // [function]
40
41
 
42
+ // a starter's name on its own, such as `blank`, suggests the starter.
41
43
  const unavailable = function (src, cause) {
44
+ const starter = templates.choices.find(
45
+ ({ value }) => value.split("/").pop() === src,
46
+ );
42
47
  return new ArcError(
43
48
  cfgcode.TEMPLATE_UNAVAILABLE,
44
- phrase.templateUnavailable(src),
49
+ phrase.templateUnavailable(src, starter?.value),
45
50
  { cause },
46
51
  );
47
52
  };
@@ -64,8 +69,11 @@ const scaffold = function (src, dest, { onInitBegin }) {
64
69
  if (onInitBegin) {
65
70
  onInitBegin({ dest, src });
66
71
  }
67
- return degit(src, { cache: false, force: true, verbose: false })
68
- .clone(dest)
72
+ // degit throws as it's made when it can't parse `src`.
73
+ return Promise.resolve()
74
+ .then(() =>
75
+ degit(src, { cache: false, force: true, verbose: false }).clone(dest),
76
+ )
69
77
  .catch((ex) => {
70
78
  throw unavailable(src, ex);
71
79
  })
@@ -48,13 +48,20 @@ const SIZE_PARTIAL = cfglimit.ZIP_SIZE;
48
48
 
49
49
  // [function]
50
50
 
51
- // the build's folder named like the reserved one, if it has one.
51
+ // the first path at the build's root that's Arcadible's, if it has one: a
52
+ // folder named like the reserved one, or the manifest or media that packing
53
+ // adds, as `arc validate` checks deploy_dir. A folder ends with "/".
52
54
  const findReserved = function (zip) {
53
- const folder = zip
55
+ return zip
54
56
  .entries()
55
57
  .map((entry) => entry.name.split("/"))
56
- .find((parts) => parts.length > 1 && parts[0].toLowerCase() === RESERVED);
57
- return folder?.[0];
58
+ .map((parts) => (parts.length > 1 ? `${parts[0]}/` : parts[0]))
59
+ .find(
60
+ (name) =>
61
+ name.toLowerCase() === `${RESERVED}/` ||
62
+ name === MANIFEST ||
63
+ name === `${MEDIA}/`,
64
+ );
58
65
  };
59
66
 
60
67
  // [function]
@@ -35,9 +35,9 @@ const NAME_REPO = /^[A-Za-z0-9._-]{1,100}$/u;
35
35
  // runs git in `root`, and resolves with its stdout.
36
36
  const git = function (root, args) {
37
37
  return new Promise((resolve, reject) => {
38
- execFile("git", ["-C", root, ...args], (err, stdout) => {
38
+ execFile("git", ["-C", root, ...args], (err, stdout, stderr) => {
39
39
  if (err) {
40
- reject(err);
40
+ reject(Object.assign(err, { stderr }));
41
41
  return;
42
42
  }
43
43
  resolve(stdout);
package/src/phrase.js CHANGED
@@ -6,8 +6,10 @@ const cfgproduct = require("./config/product");
6
6
 
7
7
  // [declare]
8
8
 
9
+ const HANDLE = cfgproduct.handle;
9
10
  const TITLE = cfgproduct.title;
10
11
  const TOOL = cfgproduct.tool;
12
+ const TAIL_LINES = 20;
11
13
 
12
14
  // [function]
13
15
 
@@ -21,6 +23,22 @@ const blocks = function (reason, ...details) {
21
23
 
22
24
  // [function]
23
25
 
26
+ // the last lines a tool printed, stdout's then stderr's, since some, such as
27
+ // tsc, print their errors on stdout, or "" when it printed nothing. Lines
28
+ // left out are marked with "…".
29
+ const tail = function (...outputs) {
30
+ const lines = outputs
31
+ .map((output) => output?.trim())
32
+ .filter(Boolean)
33
+ .join("\n")
34
+ .split("\n");
35
+ return lines.length > TAIL_LINES
36
+ ? ["…", ...lines.slice(-TAIL_LINES)].join("\n")
37
+ : lines.join("\n");
38
+ };
39
+
40
+ // [function]
41
+
24
42
  // a wait, in words: "5 minutes", "90 seconds".
25
43
  const waited = function (seconds) {
26
44
  if (seconds % 60 === 0) {
@@ -39,10 +57,10 @@ const aborted = function (command) {
39
57
 
40
58
  // [function]
41
59
 
42
- // a build that exited with `code`, or was stopped by `signal`, with what it
43
- // wrote to stderr.
44
- const buildFailed = function ({ code, signal, stderr }) {
45
- const output = stderr?.trim();
60
+ // a build that exited with `code`, or was stopped by `signal`, with the end
61
+ // of what it printed.
62
+ const buildFailed = function ({ code, signal, stderr, stdout }) {
63
+ const output = tail(stdout, stderr);
46
64
  return blocks(
47
65
  signal
48
66
  ? `Build stopped by ${signal}.`
@@ -500,18 +518,27 @@ const gitRepoMissing = function () {
500
518
 
501
519
  // [function]
502
520
 
503
- const gitTagCreateFailed = function (version) {
504
- return blocks(`Unable to create Git tag for v${version}.`, [
505
- "Check that the repository has a commit.",
506
- "Check that your Git name and email are set.",
507
- ]);
521
+ // with the end of what git printed, such as a lock file it found.
522
+ const gitTagCreateFailed = function (version, stderr) {
523
+ const output = tail(stderr);
524
+ return blocks(
525
+ `Unable to create Git tag for v${version}.`,
526
+ ...(output ? [output] : []),
527
+ [
528
+ "Check that the repository has a commit.",
529
+ "Check that your Git name and email are set.",
530
+ ],
531
+ );
508
532
  };
509
533
 
510
534
  // [function]
511
535
 
512
- const gitTagPushFailed = function (version) {
536
+ // with the end of what git printed, such as a hook's refusal.
537
+ const gitTagPushFailed = function (version, stderr) {
538
+ const output = tail(stderr);
513
539
  return blocks(
514
540
  `Unable to push Git tag for v${version}.`,
541
+ ...(output ? [output] : []),
515
542
  "Check that you can push to origin.",
516
543
  );
517
544
  };
@@ -701,10 +728,14 @@ const packageFileTooLarge = function (file, size) {
701
728
 
702
729
  // [function]
703
730
 
704
- const packagePathReserved = function (folder) {
731
+ // `name` is a folder when it ends with "/": the reserved folder, which a
732
+ // build may load from, or the media folder. A file is the manifest.
733
+ const packagePathReserved = function (name) {
705
734
  return blocks(
706
- `'${folder}/' is reserved for ${TITLE}.`,
707
- "Rename the folder, and the paths that load from it.",
735
+ `'${name}' is reserved for ${TITLE}.`,
736
+ name.toLowerCase() === `${HANDLE}/`
737
+ ? "Rename the folder, and the paths that load from it."
738
+ : `${TITLE} adds its own, so leave it out of the folder you pack.`,
708
739
  );
709
740
  };
710
741
 
@@ -858,10 +889,13 @@ const tagPushed = function (tag) {
858
889
 
859
890
  // [function]
860
891
 
861
- const templateUnavailable = function (template) {
892
+ // `starter` is the starter its name suggests.
893
+ const templateUnavailable = function (template, starter) {
862
894
  return blocks(
863
895
  `Unable to download template ${template}.`,
864
- "Check that it's a public GitHub repository, or a folder in one.",
896
+ starter
897
+ ? `Did you mean: ${starter}?`
898
+ : "Check that it's a public GitHub repository, or a folder in one.",
865
899
  );
866
900
  };
867
901
 
package/src/serve/page.js CHANGED
@@ -97,10 +97,10 @@ const html = function (game) {
97
97
  version: config.version,
98
98
  });
99
99
  } catch (err) {
100
- console.error("arcadible: no host for the game:", err);
100
+ console.error("Arcadible SDK: the play page couldn't host the game, so it stands alone.", err);
101
101
  }
102
102
  } else {
103
- console.error("arcadible: no host for the game: the play page's host didn't load.");
103
+ console.error("Arcadible SDK: the play page's host didn't load, so the game stands alone.");
104
104
  }
105
105
  frame.src = config.src;
106
106
  document.body.prepend(frame);
package/src/util/rc.js CHANGED
@@ -75,7 +75,22 @@ const files = function (cmd, argv) {
75
75
 
76
76
  // [function]
77
77
 
78
- // a file's sections, INI or JSON, or nothing when it can't be read. JSON
78
+ // ini parses anything, so a section header with no closing bracket, such as
79
+ // `[validate`, would become a key, and the options under it would be read
80
+ // outside their section. The first one's problem, or null.
81
+ const unclosedSection = function (str) {
82
+ const lines = str.split(/\r?\n/u).map((line) => line.trim());
83
+ const i = lines.findIndex(
84
+ (text) => text.startsWith("[") && !/^\[[^\]]*\]$/u.test(text),
85
+ );
86
+ return i === -1
87
+ ? null
88
+ : `Line ${i + 1}: section header isn't closed: ${lines[i]}`;
89
+ };
90
+
91
+ // [function]
92
+
93
+ // a file's sections, INI or JSON, or nothing when it can't be read. A file
79
94
  // that doesn't parse is `parse_failed`, naming the file.
80
95
  const read = function (file) {
81
96
  let str = null;
@@ -85,6 +100,13 @@ const read = function (file) {
85
100
  return {};
86
101
  }
87
102
  if (!/^\s*\{/u.test(str)) {
103
+ const unclosed = unclosedSection(str);
104
+ if (unclosed) {
105
+ throw new ArcError(
106
+ cfgcode.PARSE_FAILED,
107
+ phrase.unableToParse(file, unclosed),
108
+ );
109
+ }
88
110
  return ini.parse(str);
89
111
  }
90
112
  try {
package/src/util/s3.js CHANGED
@@ -17,13 +17,32 @@ const ArcError = error.ArcError;
17
17
  const createReadStream = fs.createReadStream;
18
18
  const request = https.request;
19
19
 
20
+ // [declare]
21
+
22
+ const CHECKSUM = "x-amz-checksum-sha256";
23
+
24
+ // [function]
25
+
26
+ // [function]
27
+
28
+ // whether the upload link signs `header`, which must then be sent.
29
+ const signs = function (url, header) {
30
+ const signed = new URL(url).searchParams.get("X-Amz-SignedHeaders");
31
+ return Boolean(signed?.split(";").includes(header));
32
+ };
33
+
20
34
  // [function]
21
35
 
22
- const requestOpts = function (size) {
36
+ // the zip's SHA-256 is sent when the link signs it, so S3 refuses a zip that
37
+ // isn't the one the upload was asked for.
38
+ const requestOpts = function (url, { sha256, size }) {
23
39
  return {
24
40
  headers: {
25
41
  "Content-Length": size,
26
42
  "Content-Type": "application/zip",
43
+ ...(signs(url, CHECKSUM)
44
+ ? { [CHECKSUM]: Buffer.from(sha256, "hex").toString("base64") }
45
+ : {}),
27
46
  },
28
47
  method: "PUT",
29
48
  };
@@ -34,9 +53,9 @@ const requestOpts = function (size) {
34
53
  // puts the file to the upload link, and resolves once S3 accepts it. A
35
54
  // request that gets no response rejects as api_unreachable; a file that can't
36
55
  // be read rejects with its own error.
37
- const upload = function (url, { path, size }, onProgress) {
56
+ const upload = function (url, { path, sha256, size }, onProgress) {
38
57
  return new Promise((resolve, reject) => {
39
- const req = request(url, requestOpts(size), (resp) => {
58
+ const req = request(url, requestOpts(url, { sha256, size }), (resp) => {
40
59
  resp.resume();
41
60
  const status = resp.statusCode;
42
61
  if (status >= 200 && status < 300) {