arcadible 0.1.39 → 0.1.41

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 with the world.
10
+ Create, play, and share games made with AI.
11
11
  </p>
12
12
 
13
13
  <p align="center">
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.39"
78
+ "version": "0.1.41"
79
79
  }
@@ -95,6 +95,23 @@ const ensureRepo = function (root) {
95
95
 
96
96
  // [function]
97
97
 
98
+ // fails when the project isn't the repository's top level: the release
99
+ // workflow builds from the root, so a project in a directory below it would
100
+ // fail on GitHub, or release the root's game.
101
+ const ensureRoot = function (root) {
102
+ return git(root, ["rev-parse", "--show-prefix"]).then((stdout) => {
103
+ const prefix = stdout.trim();
104
+ if (prefix) {
105
+ throw new ArcError(
106
+ cfgcode.PROJECT_NOT_REPO_ROOT,
107
+ phrase.projectNotRepoRoot(prefix),
108
+ );
109
+ }
110
+ });
111
+ };
112
+
113
+ // [function]
114
+
98
115
  // fails when HEAD has no release workflow, which Arcadible checks for when the
99
116
  // tag arrives (workflow_missing).
100
117
  const ensureWorkflow = function (root) {
@@ -178,6 +195,7 @@ module.exports = {
178
195
  ensureClean,
179
196
  ensureRemote,
180
197
  ensureRepo,
198
+ ensureRoot,
181
199
  ensureTagFree,
182
200
  ensureWorkflow,
183
201
  failWith,
@@ -19,6 +19,7 @@ const ensureClean = git.ensureClean;
19
19
  const ensureLockFile = pm.ensureLockFile;
20
20
  const ensureRemote = git.ensureRemote;
21
21
  const ensureRepo = git.ensureRepo;
22
+ const ensureRoot = git.ensureRoot;
22
23
  const ensureTagFree = git.ensureTagFree;
23
24
  const ensureWorkflow = git.ensureWorkflow;
24
25
  const originRepo = git.originRepo;
@@ -71,10 +72,12 @@ const ensurePublishable = function ({ handle, id, version }, handler) {
71
72
  // [function]
72
73
 
73
74
  // the project's checks: a lockfile when there's a package.json, and a clean
74
- // repository with the release workflow and a reachable origin.
75
+ // repository, whose root is the project, with the release workflow and a
76
+ // reachable origin.
75
77
  const ensureProject = function (root) {
76
78
  return ensureLockFile(root)
77
79
  .then(() => ensureRepo(root))
80
+ .then(() => ensureRoot(root))
78
81
  .then(() => ensureClean(root))
79
82
  .then(() => ensureWorkflow(root))
80
83
  .then(() => ensureRemote(root));
@@ -24,6 +24,7 @@ const CLI = {
24
24
  LOCKFILE_MISSING: "lockfile_missing",
25
25
  LOGIN_EXPIRED: "login_expired",
26
26
  LOGIN_FAILED: "login_failed",
27
+ MANIFEST_MISSING: "manifest_missing",
27
28
  NETWORK_ERROR: "network_error",
28
29
  NOT_LOGGED_IN: "not_logged_in",
29
30
  PACKAGE_FILE_TOO_LARGE: "package_file_too_large",
@@ -33,6 +34,7 @@ const CLI = {
33
34
  PACKAGE_UNCOMPRESSED_TOO_LARGE: "package_uncompressed_too_large",
34
35
  PARSE_FAILED: "parse_failed",
35
36
  PATH_NOT_FOUND: "path_not_found",
37
+ PROJECT_NOT_REPO_ROOT: "project_not_repo_root",
36
38
  SESSION_EXPIRED: "session_expired",
37
39
  TEMPLATE_MISSING: "template_missing",
38
40
  TEMPLATE_UNAVAILABLE: "template_unavailable",
@@ -48,6 +50,7 @@ const CLI = {
48
50
  const RELEASE = {
49
51
  APP_NOT_INSTALLED: "app_not_installed",
50
52
  EXPIRED: "expired",
53
+ GAME_LIMIT_REACHED: "game_limit_reached",
51
54
  GAME_MISMATCH: "game_mismatch",
52
55
  GAME_UNAVAILABLE: "game_unavailable",
53
56
  INTERNAL: "internal",
@@ -2,11 +2,16 @@
2
2
 
3
3
  // [import]
4
4
 
5
+ const path = require("node:path");
6
+
7
+ // [import]
8
+
5
9
  const fs = require("fs-extra");
6
10
 
7
11
  // [import]
8
12
 
9
13
  const cfgcode = require("../config/code");
14
+ const cfgproduct = require("../config/product");
10
15
  const cfgworkflow = require("../config/workflow");
11
16
  const env = require("./env");
12
17
  const error = require("../util/error");
@@ -19,7 +24,16 @@ const readme = require("./readme");
19
24
  // [use]
20
25
 
21
26
  const ArcError = error.ArcError;
27
+ const dirname = path.dirname;
28
+ const isAbsolute = path.isAbsolute;
29
+ const join = path.join;
22
30
  const pathExists = fs.pathExists;
31
+ const relative = path.relative;
32
+ const sep = path.sep;
33
+
34
+ // [declare]
35
+
36
+ const MANIFEST = `${cfgproduct.handle}.toml`;
23
37
 
24
38
  // [function]
25
39
 
@@ -44,6 +58,51 @@ const configWorkflow = function (envWorkflow, manifestWorkflow) {
44
58
 
45
59
  // [function]
46
60
 
61
+ // a directory as the developer would type it: relative to where arc runs
62
+ // when it's there, below it, or above it (`..`), or else its full path.
63
+ const shown = function (dir) {
64
+ const rel = relative(process.cwd(), dir);
65
+ const above = rel.split(sep).every((part) => part === "..");
66
+ return isAbsolute(rel) || (rel.startsWith("..") && !above) ? dir : rel || ".";
67
+ };
68
+
69
+ // [function]
70
+
71
+ // the nearest directory above `dir` with a manifest, or null. It's only
72
+ // named in the error's hint: arc never reads a project it wasn't given.
73
+ const nearestAbove = function (dir) {
74
+ const parent = dirname(dir);
75
+ if (parent === dir) {
76
+ return Promise.resolve(null);
77
+ }
78
+ return pathExists(join(parent, MANIFEST)).then((exists) => {
79
+ if (exists) {
80
+ return parent;
81
+ }
82
+ return nearestAbove(parent);
83
+ });
84
+ };
85
+
86
+ // [function]
87
+
88
+ // fails when `root` has no manifest. A game's id is only ever in it, so a
89
+ // project without one can't be deployed, and the likeliest cause is running
90
+ // arc from inside a project: the hint names the project above, if any.
91
+ const ensureManifest = function (root) {
92
+ return pathExists(join(root, MANIFEST))
93
+ .then((exists) => (exists ? false : nearestAbove(root)))
94
+ .then((found) => {
95
+ if (found !== false) {
96
+ throw new ArcError(
97
+ cfgcode.MANIFEST_MISSING,
98
+ phrase.noManifest(MANIFEST, shown(root), found && shown(found)),
99
+ );
100
+ }
101
+ });
102
+ };
103
+
104
+ // [function]
105
+
47
106
  // the other sources, once arcadible.toml is read: the README's title is read
48
107
  // word for word when arcadible.toml sets the short title.
49
108
  const parseSources = function (root, manifestData) {
@@ -66,16 +125,17 @@ const parseSources = function (root, manifestData) {
66
125
 
67
126
  // [function]
68
127
 
69
- // the project at `root`, which must exist: a mistyped -C would otherwise
70
- // read as an empty project.
128
+ // the project at `root`, which must exist, with its manifest: a mistyped
129
+ // -C would otherwise read as an empty project.
71
130
  const main = function (root) {
72
131
  return pathExists(root)
73
132
  .then((exists) => {
74
133
  if (!exists) {
75
134
  throw new ArcError(cfgcode.PATH_NOT_FOUND, phrase.unableToFind(root));
76
135
  }
77
- return manifest(root);
136
+ return ensureManifest(root);
78
137
  })
138
+ .then(() => manifest(root))
79
139
  .then((manifestData) => parseSources(root, manifestData));
80
140
  };
81
141
 
package/src/phrase.js CHANGED
@@ -214,6 +214,26 @@ const gitRemoteMissing = function () {
214
214
 
215
215
  // [function]
216
216
 
217
+ const noManifest = function (file, dir, found) {
218
+ return blocks(
219
+ dir === "." ? `No ${file} in this directory.` : `No ${file} in ${dir}`,
220
+ found
221
+ ? `The project is in \`${found}\`: run arc there, or pass \`-C ${found}\`.`
222
+ : "Run arc in the project's directory, or pass -C, or start a project with `arc init`.",
223
+ );
224
+ };
225
+
226
+ // [function]
227
+
228
+ const projectNotRepoRoot = function (prefix) {
229
+ return blocks(
230
+ `arc deploy git releases the project at the repository's root, and this one is in ${prefix}`,
231
+ "Deploy it with `arc deploy`, or give it a repository of its own.",
232
+ );
233
+ };
234
+
235
+ // [function]
236
+
217
237
  const gitRepoMissing = function () {
218
238
  return blocks(
219
239
  "No Git repository found.",
@@ -771,6 +791,7 @@ module.exports = {
771
791
  loginFailed,
772
792
  noGameId,
773
793
  noLockFile,
794
+ noManifest,
774
795
  noProductionRelease,
775
796
  noTemplate,
776
797
  noVideoMetadata,
@@ -783,6 +804,7 @@ module.exports = {
783
804
  packageTooLarge,
784
805
  packageTooManyFiles,
785
806
  packageUncompressedTooLarge,
807
+ projectNotRepoRoot,
786
808
  releaseFailed,
787
809
  releasePublished,
788
810
  releaseTimedOut,
@@ -43,45 +43,57 @@ const hasBuild = function (build) {
43
43
 
44
44
  // [function]
45
45
 
46
+ // a value of neither type says what the matching kind expected, a list's for
47
+ // a list and a string's otherwise, not the union's bare "Invalid input".
48
+ const typeMessage = function (issue) {
49
+ const branch = Array.isArray(issue.input) ? 1 : 0;
50
+ return issue.errors?.[branch]?.[0]?.message;
51
+ };
52
+
53
+ // [function]
54
+
46
55
  const zBuild = function () {
47
- // const field = "build";
48
- return z.union([z.string(), z.array(z.string())]);
56
+ return z.union([z.string(), z.array(z.string())], { error: typeMessage });
49
57
  };
50
58
 
51
59
  // [function]
52
60
 
61
+ // a directory inside the project with an index.html. Each check aborts the
62
+ // rest when it fails, so one bad path gets one issue.
53
63
  const zDeployDir = function (root, { build }) {
54
64
  const field = "deploy_dir";
55
65
  return z
56
66
  .string()
57
- .min(1, message(field, ["Must not be empty. Leave the field out instead."]))
58
- .refine(
59
- (file) => isRelativeCheck(root, file),
60
- message(field, ["Must be a relative path."], ISSUE.PATH_INVALID),
61
- )
62
- .refine(
63
- (file) => isInsideCheck(root, file),
64
- message(field, ["Must be within the project."], ISSUE.PATH_INVALID),
65
- )
66
- .refine(
67
- (file) => !isRootCheck(root, file),
68
- message(
67
+ .min(1, {
68
+ ...message(field, ["Must not be empty. Leave the field out instead."]),
69
+ abort: true,
70
+ })
71
+ .refine((file) => isRelativeCheck(root, file), {
72
+ ...message(field, ["Must be a relative path."], ISSUE.PATH_INVALID),
73
+ abort: true,
74
+ })
75
+ .refine((file) => isInsideCheck(root, file), {
76
+ ...message(field, ["Must be within the project."], ISSUE.PATH_INVALID),
77
+ abort: true,
78
+ })
79
+ .refine((file) => !isRootCheck(root, file), {
80
+ ...message(
69
81
  field,
70
82
  ["Must be a directory inside the project, not the project root."],
71
83
  ISSUE.PATH_INVALID,
72
84
  ),
73
- )
74
- .refine(
75
- (file) => avoidsCheck(root, file, MEDIA),
76
- message(
85
+ abort: true,
86
+ })
87
+ .refine((file) => avoidsCheck(root, file, MEDIA), {
88
+ ...message(
77
89
  field,
78
90
  [`Must not contain ${MEDIA} in path.`],
79
91
  ISSUE.PATH_INVALID,
80
92
  ),
81
- )
82
- .refine(
83
- (file) => hasBuild(build) || existsCheck(root, file),
84
- message(
93
+ abort: true,
94
+ })
95
+ .refine((file) => hasBuild(build) || existsCheck(root, file), {
96
+ ...message(
85
97
  field,
86
98
  [
87
99
  "Must exist when no build step is defined.",
@@ -89,25 +101,26 @@ const zDeployDir = function (root, { build }) {
89
101
  ],
90
102
  ISSUE.PATH_INVALID,
91
103
  ),
92
- )
93
- .refine(
94
- (file) => hasFileCheck(root, file, INDEX),
95
- message(
104
+ abort: true,
105
+ })
106
+ .refine((file) => hasFileCheck(root, file, INDEX), {
107
+ ...message(
96
108
  field,
97
109
  [`Must have an ${INDEX} at its root.`],
98
110
  ISSUE.INDEX_MISSING,
99
111
  ),
100
- )
101
- .refine(
102
- (file) => lacksFilesCheck(root, file, RESERVED),
103
- message(
112
+ abort: true,
113
+ })
114
+ .refine((file) => lacksFilesCheck(root, file, RESERVED), {
115
+ ...message(
104
116
  field,
105
117
  [
106
118
  `Must not have ${RESERVED.join(" or ")} at its root. They're reserved for the files ${cfgproduct.title} adds.`,
107
119
  ],
108
120
  ISSUE.PATH_INVALID,
109
121
  ),
110
- );
122
+ abort: true,
123
+ });
111
124
  };
112
125
 
113
126
  // [function]
@@ -39,6 +39,13 @@ const codeOf = function (issue) {
39
39
  return typeof issue.input === "undefined"
40
40
  ? ISSUE.REQUIRED
41
41
  : ISSUE.TYPE_INVALID;
42
+ case "invalid_union":
43
+ // every branch refused the value's type, e.g. `build = 3`
44
+ return issue.errors.every((branch) =>
45
+ branch.every((each) => each.code === "invalid_type"),
46
+ )
47
+ ? ISSUE.TYPE_INVALID
48
+ : ISSUE.INVALID;
42
49
  case "invalid_value":
43
50
  return ISSUE.OPTION_INVALID;
44
51
  case "too_big":