@latticexyz/cli 0.11.1 → 0.12.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.
@@ -0,0 +1,483 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
3
+ import { constants, ethers } from "ethers";
4
+ import inquirer from "inquirer";
5
+ import { v4 } from "uuid";
6
+ import { Listr, Logger } from "listr2";
7
+ import { exit } from "process";
8
+ import fs from "fs";
9
+ import openurl from "openurl";
10
+ import ips from "inquirer-prompt-suggest";
11
+ import { Arguments, CommandBuilder } from "yargs";
12
+ inquirer.registerPrompt("suggest", ips);
13
+
14
+ // Workaround to prevent tsc to transpile dynamic imports with require, which causes an error upstream
15
+ // https://github.com/microsoft/TypeScript/issues/43329#issuecomment-922544562
16
+ const importNetlify = eval('import("netlify")') as Promise<typeof import("netlify")>;
17
+ const importChalk = eval('import("chalk")') as Promise<typeof import("chalk")>;
18
+ const importExeca = eval('import("execa")') as Promise<typeof import("execa")>;
19
+ const importFetch = eval('import("node-fetch")') as Promise<typeof import("node-fetch")>;
20
+
21
+ interface Options {
22
+ i?: boolean;
23
+ chainSpec?: string;
24
+ chainId?: number;
25
+ rpc?: string;
26
+ wsRpc?: string;
27
+ world?: string;
28
+ reuseComponents?: boolean;
29
+ deployerPrivateKey?: string;
30
+ deployClient?: boolean;
31
+ clientUrl?: string;
32
+ netlifySlug?: string;
33
+ netlifyPersonalToken?: string;
34
+ upgradeSystems?: boolean;
35
+ codespace?: boolean;
36
+ dry?: boolean;
37
+ }
38
+
39
+ export const command = "deploy";
40
+ export const desc = "Deploys the local mud contracts and optionally the client";
41
+
42
+ export const builder: CommandBuilder<Options, Options> = (yargs) =>
43
+ yargs.options({
44
+ i: { type: "boolean" },
45
+ chainSpec: { type: "string" },
46
+ chainId: { type: "number" },
47
+ rpc: { type: "string" },
48
+ wsRpc: { type: "string" },
49
+ world: { type: "string" },
50
+ reuseComponents: { type: "boolean" },
51
+ deployerPrivateKey: { type: "string" },
52
+ deployClient: { type: "boolean" },
53
+ clientUrl: { type: "string" },
54
+ netlifySlug: { type: "string" },
55
+ netlifyPersonalToken: { type: "string" },
56
+ upgradeSystems: { type: "boolean" },
57
+ codespace: { type: "boolean" },
58
+ dry: { type: "boolean" },
59
+ });
60
+
61
+ export const handler = async (args: Arguments<Options>): Promise<void> => {
62
+ const info = await getDeployInfo(args);
63
+ await deploy(info);
64
+ };
65
+
66
+ function isValidHttpUrl(s: string): boolean {
67
+ let url: URL | undefined;
68
+
69
+ try {
70
+ url = new URL(s);
71
+ } catch (_) {
72
+ return false;
73
+ }
74
+
75
+ return url.protocol === "http:" || url.protocol === "https:";
76
+ }
77
+
78
+ function findLog(deployLogLines: string[], log: string): string {
79
+ for (const logLine of deployLogLines) {
80
+ if (logLine.includes(log)) {
81
+ return logLine.split(log)[1].trim();
82
+ }
83
+ }
84
+ throw new Error("Can not find log");
85
+ }
86
+
87
+ const getDeployInfo: (args: Arguments<Options>) => Promise<Options> = async (args) => {
88
+ const { default: chalk } = await importChalk;
89
+ console.log();
90
+ console.log(chalk.bgWhite.black.bold(" == Mud Deployer == "));
91
+ console.log();
92
+
93
+ let config: Options = {};
94
+ try {
95
+ config = JSON.parse(fs.readFileSync("mud.config.json", "utf8"));
96
+ } catch (e) {
97
+ console.log("No mud.config.json found, using command line args");
98
+ }
99
+
100
+ const getNetlifyAccounts = async (token: string) => {
101
+ const { NetlifyAPI: netlify } = await importNetlify;
102
+ const netlifyAPI = new netlify(token);
103
+ console.log("Netlify api");
104
+ const accounts = await netlifyAPI.listAccountsForUser();
105
+ console.log("Accounts");
106
+ return accounts.map((a: { slug: string }) => a.slug);
107
+ };
108
+
109
+ const defaultOptions: Options = {
110
+ chainSpec: "chainSpec.json",
111
+ chainId: 31337,
112
+ rpc: "http://localhost:8545",
113
+ wsRpc: "ws://localhost:8545",
114
+ reuseComponents: false,
115
+ deployClient: false,
116
+ clientUrl: "http://localhost:3000",
117
+ upgradeSystems: false,
118
+ };
119
+
120
+ const { default: fetch } = await importFetch;
121
+
122
+ // Fetch deployed lattice chains
123
+ const latticeChains = args.i
124
+ ? ((await (await fetch("https://registry.lattice.xyz/api?update=true")).json()) as
125
+ | { specUrl: string }[]
126
+ | undefined)
127
+ : [];
128
+
129
+ const chainSpecs = latticeChains?.map((e) => e.specUrl) || [];
130
+ console.log("Available Lattice chains");
131
+ console.log(JSON.stringify(latticeChains, null, 2));
132
+
133
+ const answers: Options =
134
+ args.upgradeSystems && !args.world
135
+ ? await inquirer.prompt([
136
+ {
137
+ type: "input",
138
+ name: "world",
139
+ message: "Provide the address of the World contract to upgrade the systems on.",
140
+ when: () => args.world == null && config.world == null,
141
+ validate: (i) => {
142
+ if (!i || (i[0] == "0" && i[1] == "x" && i.length === 42)) return true;
143
+ return "Invalid address";
144
+ },
145
+ },
146
+ ])
147
+ : args.i
148
+ ? await inquirer.prompt([
149
+ {
150
+ type: "suggest",
151
+ name: "chainSpec",
152
+ message: "Provide a chainSpec.json location (local or remote)",
153
+ suggestions: chainSpecs,
154
+ when: () => args.chainSpec == null && config.chainSpec == null,
155
+ },
156
+ {
157
+ type: "number",
158
+ name: "chainId",
159
+ default: defaultOptions.chainId,
160
+ message: "Provide a chainId for the deployment",
161
+ when: (answers) => answers.chainSpec == null && args.chainId == null && config.chainSpec == null,
162
+ },
163
+ {
164
+ type: "input",
165
+ name: "wsRpc",
166
+ default: defaultOptions.wsRpc,
167
+ message: "Provide a WebSocket RPC endpoint for your deployment",
168
+ when: (answers) => answers.chainSpec == null && args.wsRpc == null && config.wsRpc == null,
169
+ },
170
+ {
171
+ type: "input",
172
+ name: "rpc",
173
+ default: defaultOptions.rpc,
174
+ message: "Provide a JSON RPC endpoint for your deployment",
175
+ when: (answers) => answers.chainSpec == null && args.rpc == null && config.rpc == null,
176
+ validate: (i) => {
177
+ if (isValidHttpUrl(i)) return true;
178
+ return "Invalid URL";
179
+ },
180
+ },
181
+ {
182
+ type: "input",
183
+ name: "world",
184
+ message:
185
+ "Provide the address of an existing World contract. (If none is given, a new World will be deployed.)",
186
+ when: () => args.world == null && config.world == null,
187
+ validate: (i) => {
188
+ if (!i || (i[0] == "0" && i[1] == "x" && i.length === 42)) return true;
189
+ return "Invalid address";
190
+ },
191
+ },
192
+ {
193
+ type: "list",
194
+ name: "upgradeSystems",
195
+ message: "Only upgrade systems?",
196
+ choices: [
197
+ { name: "Yes", value: true },
198
+ { name: "No", value: false },
199
+ ],
200
+ default: defaultOptions.upgradeSystems,
201
+ when: (answers) =>
202
+ (args.world || config.world || answers.world) &&
203
+ args.upgradeSystems == null &&
204
+ config.upgradeSystems == null,
205
+ },
206
+ {
207
+ type: "list",
208
+ name: "reuseComponents",
209
+ message: "Reuse existing components?",
210
+ choices: [
211
+ { name: "Yes", value: true },
212
+ { name: "No", value: false },
213
+ ],
214
+ default: defaultOptions.reuseComponents,
215
+ when: (answers) =>
216
+ !answers.upgradeSystems &&
217
+ !args.upgradeSystems &&
218
+ !config.upgradeSystems &&
219
+ args.reuseComponents == null &&
220
+ config.reuseComponents == null,
221
+ },
222
+ {
223
+ type: "input",
224
+ name: "deployerPrivateKey",
225
+ message: "Enter private key of the deployer account:",
226
+ when: () => !args.deployerPrivateKey && !config.deployerPrivateKey,
227
+ validate: (i) => {
228
+ if (i[0] == "0" && i[1] == "x" && i.length === 66) return true;
229
+ return "Invalid private key";
230
+ },
231
+ },
232
+ {
233
+ type: "list",
234
+ message: "Deploy the client?",
235
+ choices: [
236
+ { name: "Yes", value: true },
237
+ { name: "No", value: false },
238
+ ],
239
+ default: defaultOptions.deployClient,
240
+ name: "deployClient",
241
+ when: () => args.deployClient == null && config.deployClient == null,
242
+ },
243
+ {
244
+ type: "input",
245
+ name: "netlifyPersonalToken",
246
+ message: "Enter a netlify personal token for deploying the client:",
247
+ when: (answers) => answers.deployClient && !args.netlifyPersonalToken && !config.netlifyPersonalToken,
248
+ },
249
+ {
250
+ type: "list",
251
+ message: "From which netlify account?",
252
+ choices: async (answers) =>
253
+ await getNetlifyAccounts(
254
+ args.netlifyPersonalToken ?? config.netlifyPersonalToken ?? answers.netlifyPersonalToken!
255
+ ),
256
+ name: "netlifySlug",
257
+ when: (answers) => answers.deployClient && !args.netlifySlug && !config.netlifySlug,
258
+ },
259
+ {
260
+ type: "input",
261
+ name: "clientUrl",
262
+ message: "Enter URL of an already deployed client:",
263
+ when: (answers) => !answers.deployClient && !args.clientUrl && !config.clientUrl,
264
+ default: "http://localhost:3000",
265
+ validate: (i) => {
266
+ if (isValidHttpUrl(i)) {
267
+ if (i[i.length - 1] === "/") {
268
+ return "No trailing slash";
269
+ }
270
+ return true;
271
+ } else {
272
+ return "Not a valid URL";
273
+ }
274
+ },
275
+ },
276
+ ])
277
+ : ({} as Options);
278
+
279
+ const chainSpecUrl = args.chainSpec ?? config.chainSpec ?? answers.chainSpec;
280
+ const chainSpec = !chainSpecUrl
281
+ ? null
282
+ : isValidHttpUrl(chainSpecUrl)
283
+ ? await (await fetch(chainSpecUrl)).json()
284
+ : JSON.parse(fs.readFileSync(chainSpecUrl, "utf8"));
285
+
286
+ // Priority of config source: command line args >> chainSpec >> local config >> interactive answers >> defaults
287
+ // -> Command line args can override every other config, interactive questions are only asked if no other config given for this option
288
+
289
+ return {
290
+ chainSpec: args.chainSpec ?? config.chainSpec ?? answers.chainSpec ?? defaultOptions.chainSpec,
291
+ chainId: args.chainId ?? chainSpec?.chainId ?? config.chainId ?? answers.chainId ?? defaultOptions.chainId,
292
+ rpc: args.rpc ?? chainSpec?.rpc ?? config.rpc ?? answers.rpc ?? defaultOptions.rpc,
293
+ wsRpc: args.wsRpc ?? chainSpec?.wsRpc ?? config.wsRpc ?? answers.wsRpc ?? defaultOptions.wsRpc,
294
+ world: args.world ?? chainSpec?.world ?? config.world ?? answers.world,
295
+ upgradeSystems: args.upgradeSystems ?? config.upgradeSystems ?? answers.upgradeSystems,
296
+ reuseComponents:
297
+ args.reuseComponents ?? config.reuseComponents ?? answers.reuseComponents ?? defaultOptions.reuseComponents,
298
+ deployerPrivateKey: args.deployerPrivateKey ?? config.deployerPrivateKey ?? answers.deployerPrivateKey,
299
+ deployClient: args.deployClient ?? config.deployClient ?? answers.deployClient ?? defaultOptions.deployClient,
300
+ clientUrl: args.clientUrl ?? config.clientUrl ?? answers.clientUrl ?? defaultOptions.clientUrl,
301
+ netlifySlug: args.netlifySlug ?? config.netlifySlug ?? answers.netlifySlug,
302
+ netlifyPersonalToken: args.netlifyPersonalToken ?? config.netlifyPersonalToken ?? answers.netlifyPersonalToken,
303
+ codespace: args.codespace,
304
+ dry: args.dry,
305
+ };
306
+ };
307
+
308
+ export const deploy = async (options: Options) => {
309
+ const { default: chalk } = await importChalk;
310
+ const { execa } = await importExeca;
311
+ console.log();
312
+ console.log(chalk.yellow(`>> Deploying contracts <<`));
313
+
314
+ console.log("Options");
315
+ console.log(options);
316
+
317
+ const wallet = new ethers.Wallet(options.deployerPrivateKey!);
318
+ console.log(chalk.red(`>> Deployer address: ${chalk.bgYellow.black.bold(" " + wallet.address + " ")} <<`));
319
+ console.log();
320
+
321
+ const logger = new Logger({ useIcons: true });
322
+
323
+ const { NetlifyAPI: netlify } = await importNetlify;
324
+
325
+ const netlifyAPI = options.deployClient && new netlify(options.netlifyPersonalToken);
326
+ const id = v4().substring(0, 6);
327
+
328
+ let launcherUrl;
329
+ let worldAddress;
330
+
331
+ const cmdArgs = options.upgradeSystems
332
+ ? [
333
+ "workspace",
334
+ "contracts",
335
+ "forge:deploy",
336
+ ...(options.dry ? [] : ["--broadcast", "--private-keys", wallet.privateKey]),
337
+ "--sig",
338
+ "upgradeSystems(address,address)",
339
+ wallet.address,
340
+ options.world || constants.AddressZero,
341
+ "--fork-url",
342
+ options.rpc!,
343
+ ]
344
+ : [
345
+ "workspace",
346
+ "contracts",
347
+ "forge:deploy",
348
+ ...(options.dry ? [] : ["--broadcast", "--private-keys", wallet.privateKey]),
349
+ "--sig",
350
+ "deploy(address,address,bool)",
351
+ wallet.address,
352
+ options.world || constants.AddressZero,
353
+ options.reuseComponents ? "true" : "false",
354
+ "--fork-url",
355
+ options.rpc!,
356
+ ];
357
+
358
+ try {
359
+ const tasks = new Listr([
360
+ {
361
+ title: "Deploying",
362
+ task: () => {
363
+ return new Listr(
364
+ [
365
+ {
366
+ title: "Contracts",
367
+ task: async (ctx, task) => {
368
+ const child = execa("yarn", cmdArgs);
369
+ child.stdout?.pipe(task.stdout());
370
+ const { stdout } = await child;
371
+ const lines = stdout.split("\n");
372
+
373
+ ctx.worldAddress = worldAddress = findLog(lines, "world: address");
374
+ ctx.initialBlockNumber = findLog(lines, "initialBlockNumber: uint256");
375
+ task.output = chalk.yellow(`World deployed at: ${chalk.bgYellow.black(ctx.worldAddress)}`);
376
+ },
377
+ options: { bottomBar: 3 },
378
+ },
379
+ {
380
+ title: "Client",
381
+ task: () => {
382
+ return new Listr(
383
+ [
384
+ {
385
+ title: "Building",
386
+ task: async (_, task) => {
387
+ const time = Date.now();
388
+ task.output = "Building local client...";
389
+ const child = execa("yarn", ["workspace", "ri-client", "build"]);
390
+ await child;
391
+ const duration = Date.now() - time;
392
+ task.output = "Client built in " + Math.round(duration / 1000) + "s";
393
+ },
394
+ skip: () => !options.deployClient,
395
+ options: { bottomBar: 3 },
396
+ },
397
+ {
398
+ title: "Creating",
399
+ task: async (ctx, task) => {
400
+ const site = await netlifyAPI.createSite({
401
+ body: {
402
+ name: `mud-deployment-${wallet.address.substring(2, 8)}-${id}`,
403
+ account_slug: options.netlifySlug,
404
+ ssl: true,
405
+ force_ssl: true,
406
+ },
407
+ });
408
+ ctx.siteId = site.id;
409
+ ctx.clientUrl = site.ssl_url;
410
+ task.output = "Netlify site created with id: " + chalk.bgYellow.black(site.id);
411
+ },
412
+ skip: () => !options.deployClient,
413
+ options: { bottomBar: 1 },
414
+ },
415
+ {
416
+ title: "Deploying",
417
+ task: async (ctx, task) => {
418
+ const child = execa(
419
+ "yarn",
420
+ ["workspace", "ri-client", "run", "netlify", "deploy", "--prod", "--dir", "dist"],
421
+ {
422
+ env: {
423
+ NETLIFY_AUTH_TOKEN: options.netlifyPersonalToken,
424
+ NETLIFY_SITE_ID: ctx.siteId,
425
+ },
426
+ }
427
+ );
428
+ child.stdout?.pipe(task.stdout());
429
+ await child;
430
+ task.output = chalk.yellow("Netlify site deployed!");
431
+ },
432
+ skip: () => !options.deployClient,
433
+ options: { bottomBar: 3 },
434
+ },
435
+ ],
436
+ { concurrent: false }
437
+ );
438
+ },
439
+ },
440
+ {
441
+ title: "Open Launcher",
442
+ task: async (ctx) => {
443
+ function getCodespaceUrl(port: number, protocol = "https") {
444
+ return `${protocol}://${process.env["CODESPACE_NAME"]}-${port}.app.online.visualstudio.com`;
445
+ }
446
+
447
+ let clientUrl = options.deployClient ? ctx.clientUrl : options.clientUrl;
448
+
449
+ if (options.codespace) {
450
+ clientUrl = getCodespaceUrl(3000);
451
+ options.rpc = getCodespaceUrl(8545);
452
+ }
453
+
454
+ launcherUrl = `${clientUrl}?chainId=${options.chainId}&worldAddress=${ctx.worldAddress}&rpc=${options.rpc}&wsRpc=${options.wsRpc}&checkpoint=&initialBlockNumber=${ctx.initialBlockNumber}&dev=true`;
455
+
456
+ // Launcher version:
457
+ // `https://play.lattice.xyz?worldAddress=${ctx.worldAddress || ""}&client=${
458
+ // clientUrl || ""
459
+ // }&rpc=${options.rpc || ""}&wsRpc=${options.wsRpc || ""}&chainId=${options.chainId || ""}&dev=${
460
+ // options.chainId === 31337 || ""
461
+ // }&initialBlockNumber=${ctx.initialBlockNumber}`;
462
+
463
+ if (!options.upgradeSystems) openurl.open(launcherUrl);
464
+ },
465
+ options: { bottomBar: 3 },
466
+ },
467
+ ],
468
+ { concurrent: false }
469
+ );
470
+ },
471
+ },
472
+ ]);
473
+ await tasks.run();
474
+ console.log(chalk.bgGreen.black.bold(" Congratulations! Deployment successful"));
475
+ console.log();
476
+ console.log(chalk.green(`World address ${worldAddress}`));
477
+ console.log(chalk.green(`Open launcher at ${launcherUrl}`));
478
+ console.log();
479
+ } catch (e) {
480
+ logger.fail((e as Error).message);
481
+ }
482
+ exit(0);
483
+ };
@@ -0,0 +1,58 @@
1
+ import fs from "fs";
2
+ import glob from "glob";
3
+ import type { Arguments, CommandBuilder } from "yargs";
4
+ import { deferred } from "../utils";
5
+
6
+ type Options = {
7
+ include: (string | number)[] | undefined;
8
+ exclude: (string | number)[] | undefined;
9
+ out: string | undefined;
10
+ };
11
+
12
+ export const command = "diamond-abi";
13
+ export const desc = "Merges the abis of different facets of a diamond to a single diamond abi";
14
+
15
+ export const builder: CommandBuilder<Options, Options> = (yargs) =>
16
+ yargs.options({
17
+ include: { type: "array" },
18
+ exclude: { type: "array" },
19
+ out: { type: "string" },
20
+ });
21
+
22
+ export const handler = async (argv: Arguments<Options>): Promise<void> => {
23
+ const { include: _include, exclude: _exclude, out: _out } = argv;
24
+ const wd = process.cwd();
25
+ console.log("Current working directory:", wd);
26
+
27
+ const include = (_include as string[]) || [`${wd}/abi/*Facet.json`];
28
+ const exclude =
29
+ (_exclude as string[]) ||
30
+ ["DiamondCutFacet", "DiamondLoupeFacet", "OwnershipFacet"].map((file) => `./abi/${file}.json`);
31
+ const out = _out || `${wd}/abi/CombinedFacets.json`;
32
+
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ const abi: any[] = [];
35
+
36
+ for (const path of include) {
37
+ const [resolve, , promise] = deferred<void>();
38
+ glob(path, {}, (_, facets) => {
39
+ // Merge all abis matching the path glob
40
+ const pathAbi = facets
41
+ .filter((facet) => !exclude.includes(facet))
42
+ .map((facet) => require(facet))
43
+ .map((abis) => abis.abi)
44
+ .flat(1);
45
+
46
+ abi.push(...pathAbi);
47
+ resolve();
48
+ });
49
+
50
+ // Make the callback syncronous
51
+ await promise;
52
+ }
53
+
54
+ fs.writeFileSync(out, JSON.stringify({ abi }));
55
+
56
+ console.log(`Created diamond abi at ${out}`);
57
+ process.exit(0);
58
+ };
@@ -0,0 +1,23 @@
1
+ import type { Arguments, CommandBuilder } from "yargs";
2
+
3
+ type Options = {
4
+ name: string;
5
+ upper: boolean | undefined;
6
+ };
7
+
8
+ export const command = "hello <name>";
9
+ export const desc = "Greet <name> with Hello";
10
+
11
+ export const builder: CommandBuilder<Options, Options> = (yargs) =>
12
+ yargs
13
+ .options({
14
+ upper: { type: "boolean" },
15
+ })
16
+ .positional("name", { type: "string", demandOption: true });
17
+
18
+ export const handler = (argv: Arguments<Options>): void => {
19
+ const { name } = argv;
20
+ const greeting = `Gm, ${name}!`;
21
+ console.log(greeting);
22
+ process.exit(0);
23
+ };
@@ -0,0 +1,47 @@
1
+ import { Arguments, CommandBuilder } from "yargs";
2
+ import { exec } from "../utils";
3
+
4
+ type Options = {
5
+ repo: string;
6
+ commitHash?: string;
7
+ };
8
+
9
+ export const command = "sync-art <repo>";
10
+ export const desc = "Syncs art from a MUD-compatible art repo, found in <repo>";
11
+
12
+ export const builder: CommandBuilder<Options, Options> = (yargs) =>
13
+ yargs.positional("repo", { type: "string", demandOption: true }).options({
14
+ commitHash: { type: "string" },
15
+ });
16
+
17
+ export const handler = async (argv: Arguments<Options>): Promise<void> => {
18
+ const { repo, commitHash } = argv;
19
+ console.log("Syncing art repo from", repo);
20
+ const clean = await exec(`git diff --quiet --exit-code`);
21
+ if (clean !== 0) {
22
+ console.log("Directory is not clean! Please git add and commit");
23
+ process.exit(0);
24
+ }
25
+
26
+ console.log("Cloning...");
27
+ await exec(`git clone ${repo} _artmudtemp`);
28
+ if (commitHash) {
29
+ await exec(`cd _artmudtemp && git reset --hard ${commitHash} && cd -`);
30
+ }
31
+
32
+ console.log("Moving atlases...");
33
+ await exec(`cp -r _artmudtemp/atlases src/public`);
34
+
35
+ console.log("Moving tilesets...");
36
+ await exec(`cp -r _artmudtemp/tilesets src/layers/Renderer/assets`);
37
+
38
+ console.log("Moving tileset types...");
39
+ await exec(`cp -r _artmudtemp/types/ src/layers/Renderer/Phaser/tilesets/`);
40
+
41
+ console.log("Cleaning up...");
42
+ await exec(`rm -rf _artmudtemp`);
43
+
44
+ console.log("Committing...");
45
+ await exec(`git add src/public && git add src/layers/Renderer && git commit -m "feat(art): adding art from ${repo}"`);
46
+ process.exit(0);
47
+ };