updateday 0.0.0-stage → 0.2.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -2
  3. package/assets/hourglass/10m.png +0 -0
  4. package/assets/hourglass/12h.png +0 -0
  5. package/assets/hourglass/15m.png +0 -0
  6. package/assets/hourglass/1h.png +0 -0
  7. package/assets/hourglass/24h.png +0 -0
  8. package/assets/hourglass/2h.png +0 -0
  9. package/assets/hourglass/30m.png +0 -0
  10. package/assets/hourglass/3h.png +0 -0
  11. package/assets/hourglass/45m.png +0 -0
  12. package/assets/hourglass/4h.png +0 -0
  13. package/assets/hourglass/5h.png +0 -0
  14. package/assets/hourglass/5m.png +0 -0
  15. package/assets/hourglass/6h.png +0 -0
  16. package/assets/hourglass/default.png +0 -0
  17. package/assets/hourglass/now.png +0 -0
  18. package/assets/hourglass-styles/blocky.png +0 -0
  19. package/assets/hourglass-styles/classic.png +0 -0
  20. package/assets/hourglass-styles/glow.png +0 -0
  21. package/assets/hourglass-styles/outline.png +0 -0
  22. package/assets/hourglass-styles/pixel.png +0 -0
  23. package/assets/hourglass-styles/rounded.png +0 -0
  24. package/bin/updateday.js +5 -0
  25. package/dist/bundle.js +54 -0
  26. package/dist/cli.js +254 -0
  27. package/dist/config.js +261 -0
  28. package/dist/doctor.js +119 -0
  29. package/dist/experiments.js +349 -0
  30. package/dist/gamePage.js +143 -0
  31. package/dist/gifts.js +55 -0
  32. package/dist/history.js +68 -0
  33. package/dist/icons.js +216 -0
  34. package/dist/labels.js +64 -0
  35. package/dist/mcp.js +302 -0
  36. package/dist/moderation.js +86 -0
  37. package/dist/notify.js +72 -0
  38. package/dist/openCloud.js +211 -0
  39. package/dist/playerData.js +109 -0
  40. package/dist/preflight.js +202 -0
  41. package/dist/runner.js +360 -0
  42. package/dist/schedule.js +162 -0
  43. package/dist/server.js +929 -0
  44. package/dist/state.js +29 -0
  45. package/dist/swap.js +196 -0
  46. package/dist/zip.js +82 -0
  47. package/docs/AGENTS.md +123 -0
  48. package/docs/COVERAGE.md +50 -0
  49. package/docs/GUIDE.md +288 -0
  50. package/docs/hourglass-styles.png +0 -0
  51. package/docs/screenshot.png +0 -0
  52. package/example/multi-game.json +16 -0
  53. package/example/updateday.json +37 -0
  54. package/package.json +63 -5
  55. package/roblox/UpdateDayBanner.client.luau +159 -0
  56. package/roblox/UpdateDayCountdown.server.luau +48 -0
  57. package/roblox/UpdateDayGifts.luau +119 -0
  58. package/roblox/UpdateDayListener.luau +43 -0
  59. package/templates/github-actions/updateday.yml +35 -0
  60. package/ui/core.js +325 -0
  61. package/ui/countdown.js +775 -0
  62. package/ui/events.js +249 -0
  63. package/ui/gifts.js +215 -0
  64. package/ui/history.js +156 -0
  65. package/ui/index.html +530 -0
  66. package/ui/jsonEditor.js +417 -0
  67. package/ui/page.js +94 -0
  68. package/ui/players.js +256 -0
  69. package/ui/run.js +391 -0
  70. package/ui/sale.js +366 -0
  71. package/ui/settings.js +414 -0
  72. package/ui/shell.js +193 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UpdateDay contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,110 @@
1
- # Temporary Holding Version
1
+ # UpdateDay
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Free, open-source update-day automation for Roblox games.**
4
+
5
+ Pick an update time and UpdateDay runs the whole day for you: countdown icons and titles, a sale, a Roblox event,
6
+ in-game warnings and server restarts. Everything is put back exactly as it was afterwards. It runs on your own
7
+ computer with an Open Cloud API key, needs no logins or cookies, and there's no UpdateDay server.
8
+
9
+ ![The UpdateDay setup page](docs/screenshot.png)
10
+
11
+ ## What it can do
12
+
13
+ Everything is optional and off until you turn it on.
14
+
15
+ - **Countdown icons** built from your game's own icon: 24 hours … 5 minutes, then NOW. Pick a style (hand-drawn,
16
+ classic, pixel, rounded, glow, blocky, outline) or upload your own art, and move and resize the hourglass and the text.
17
+ - **Title countdown**: "[6H] Your Game" → "[LIVE] Your Game" → back to normal, for players in your game's main language.
18
+ - **Moderation check**: Roblox reviews every icon days ahead, so nothing gets rejected on the day.
19
+ - **Sale**: X% off game passes and products, with "[30% OFF]" name tags. Every price and name goes back afterwards.
20
+ - **Events**: create and edit Roblox events, or get one made automatically for every update.
21
+ - **In-game**: warnings before the update, a live "UPDATE IN 04:59" banner, and old servers restarted onto the new version.
22
+ - **Alerts**: Discord messages (with the icon to upload attached) and desktop pop-ups.
23
+ - **Extras**: several games at once, repeating updates, a game page checklist, a history of everything it did,
24
+ a player-data editor with backups, gifts, translated labels in 15 languages, and an [AI agent hook](docs/AGENTS.md).
25
+
26
+ ## Start in 3 minutes
27
+
28
+ **1. Install [Node.js](https://nodejs.org) 22.18 or newer.**
29
+
30
+ **2. Open the setup page** in any folder you like:
31
+
32
+ ```bash
33
+ npx updateday ui updateday.json
34
+ ```
35
+
36
+ It opens on `http://127.0.0.1:4747`, which only your own computer can reach.
37
+
38
+ **3. Add your game**: paste its Roblox link (`https://www.roblox.com/games/...`) in **Settings**.
39
+
40
+ **4. Make an API key**: Creator Hub → **Open Cloud → API Keys → Create API Key**. Add your experience, then add the
41
+ permissions for the features you want:
42
+
43
+ | Feature | Permission to add |
44
+ |---|---|
45
+ | Always | Universe: read |
46
+ | Countdown icons | Legacy universe: manage |
47
+ | Title countdown | Places: read + write |
48
+ | Moderation check | Assets: read + write |
49
+ | Sale | Game passes: read + write, Developer products: read + write |
50
+ | Events | Universe events: read + write |
51
+ | In-game warnings and banner | Messaging service: publish |
52
+ | Server restarts | Universe: write |
53
+ | Player data and gifts | DataStore: list stores, read, create and update entries |
54
+
55
+ Paste the key in the page (the **▴** at the bottom right). It's kept in memory only and never saved to disk.
56
+ **Check plan** tells you if a permission is missing.
57
+
58
+ **5. Plan it**: set the update time, choose what you want in each tab, and press **Save**.
59
+
60
+ **6. Press Start** and leave the page open. **Check plan** runs first and tells you if anything needs fixing.
61
+ Changed your mind? **Undo all** puts every price, name, title and icon back.
62
+
63
+ ## Good to know
64
+
65
+ - **The main game icon.** Roblox doesn't let API keys change a game's main icon. UpdateDay either reminds you to
66
+ upload each finished icon (with a download button and a Discord alert), or does it automatically with a
67
+ *language swap*: it briefly makes another language the game's main one and sets the countdown for every
68
+ other language. Players whose Roblox language is that temporary one see your normal icon.
69
+ [More in the guide](docs/GUIDE.md#how-the-icon-changes-on-roblox).
70
+ - **Safe by design.** Original prices, names and titles are saved before anything changes, and every change is
71
+ read back. If UpdateDay is closed or crashes part-way, it picks up where it left off.
72
+ - **PC off?** Run it from GitHub Actions instead. [See the guide](docs/GUIDE.md#with-your-pc-off-github-actions).
73
+ - **In your game:** download the Roblox kit from **Settings** for the countdown banner and warning pop-ups.
74
+ [See the guide](docs/GUIDE.md#in-game-countdown).
75
+
76
+ ## Using the terminal instead
77
+
78
+ The page and these commands share the same `updateday.json`, so you can mix them.
79
+
80
+ ```bash
81
+ npx updateday check updateday.json # pre-flight check
82
+ npx updateday plan updateday.json # the full schedule
83
+ npx updateday preview updateday.json # every countdown icon into ./preview
84
+ npx updateday watch updateday.json # run it
85
+ npx updateday restore updateday.json # undo everything now
86
+ ```
87
+
88
+ The key comes from the `UPDATEDAY_API_KEY` environment variable. Every setting is in the [guide](docs/GUIDE.md).
89
+
90
+ ## For AI agents
91
+
92
+ `npx updateday mcp updateday.json` runs UpdateDay as an MCP server. Claude, Cursor and other agents can then plan
93
+ and check update days for you. Anything that changes your live game needs an explicit confirmation.
94
+ See [docs/AGENTS.md](docs/AGENTS.md).
95
+
96
+ ## Contributing
97
+
98
+ ```bash
99
+ git clone https://github.com/hiyouwhatsapp/UpdateDay && cd UpdateDay
100
+ npm install
101
+ npm test # unit tests, a full update day against a fake Roblox, edge cases, security
102
+ npm run test:ui # every tab at desktop and phone size, light and dark (needs Chrome)
103
+ node src/cli.ts ui example/updateday.json
104
+ ```
105
+
106
+ Issues and pull requests are welcome. [docs/COVERAGE.md](docs/COVERAGE.md) lists every Roblox API UpdateDay uses.
107
+
108
+ ## License
109
+
110
+ MIT. Free for everyone, forever.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+
4
+ const built = new URL("../dist/cli.js", import.meta.url);
5
+ await import(existsSync(built) ? built.href : new URL("../src/cli.ts", import.meta.url).href);
package/dist/bundle.js ADDED
@@ -0,0 +1,54 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { activeMarks, showsNow } from "./config.js";
5
+ import { renderIcon } from "./icons.js";
6
+ import { LABEL_LANGUAGES } from "./labels.js";
7
+ import { zip } from "./zip.js";
8
+ const KIT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../roblox");
9
+ const KIT_README = `UpdateDay in-game kit
10
+
11
+ UpdateDayListener.luau ModuleScript (ServerScriptService). Receives UpdateDay's warnings.
12
+ UpdateDayCountdown.server.luau Script (ServerScriptService). Starts the listener and shares the update time with players.
13
+ UpdateDayBanner.client.luau LocalScript (StarterPlayerScripts). Shows the countdown banner and warnings.
14
+ UpdateDayGifts.luau ModuleScript (ServerScriptService). Optional: the gift box used by the Gifts tab.
15
+
16
+ Every option sits at the top of each file. Turn on "Warn players in live servers"
17
+ in UpdateDay so the game hears about the update.
18
+ `;
19
+ export function iconMarks(countdown) {
20
+ const marks = activeMarks(countdown);
21
+ if (showsNow(countdown)) {
22
+ marks.push("now");
23
+ }
24
+ marks.push("final");
25
+ return marks;
26
+ }
27
+ function fileName(mark, index) {
28
+ return `${String(index + 1).padStart(2, "0")}-${mark}.png`;
29
+ }
30
+ export async function iconZip(config, base, translated) {
31
+ const countdown = config.countdown;
32
+ const marks = iconMarks(countdown);
33
+ const entries = [];
34
+ for (const [index, mark] of marks.entries()) {
35
+ entries.push({ name: fileName(mark, index), data: await renderIcon(base, mark, countdown) });
36
+ }
37
+ if (translated) {
38
+ for (const language of LABEL_LANGUAGES) {
39
+ for (const [index, mark] of marks.entries()) {
40
+ if (mark === "final") {
41
+ continue;
42
+ }
43
+ entries.push({ name: `languages/${language}/${fileName(mark, index)}`, data: await renderIcon(base, mark, countdown, language) });
44
+ }
45
+ }
46
+ }
47
+ return zip(entries);
48
+ }
49
+ export function kitZip() {
50
+ const files = readdirSync(KIT_DIR).filter((name) => name.endsWith(".luau"));
51
+ const entries = files.map((name) => ({ name, data: readFileSync(join(KIT_DIR, name)) }));
52
+ entries.push({ name: "README.txt", data: Buffer.from(KIT_README) });
53
+ return zip(entries);
54
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,254 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { iconMarks } from "./bundle.js";
4
+ import { loadConfigs, nextOccurrence } from "./config.js";
5
+ import { recording } from "./history.js";
6
+ import { fetchCurrentIcon, renderIcon } from "./icons.js";
7
+ import { startMcp } from "./mcp.js";
8
+ import { discordNotifier } from "./notify.js";
9
+ import { preflight } from "./preflight.js";
10
+ import { OpenCloud } from "./openCloud.js";
11
+ import { endCountdown, restorePrices, tick } from "./runner.js";
12
+ import { checkKey } from "./doctor.js";
13
+ import { startServer } from "./server.js";
14
+ import { buildSchedule } from "./schedule.js";
15
+ import { loadState, saveState } from "./state.js";
16
+ const DEFAULT_CONFIG = "updateday.json";
17
+ const WATCH_INTERVAL_MS = 30 * 1000;
18
+ const PREVIEW_DIR = "preview";
19
+ const KEY_COMMANDS = ["tick", "watch", "restore"];
20
+ const DEFAULT_UI_PORT = 4747;
21
+ const USAGE = `UpdateDay - schedule a Roblox update day with an Open Cloud API key
22
+
23
+ Usage: updateday <command> [config.json] [--game <name or universeId>] [--dry-run]
24
+
25
+ Commands:
26
+ ui Open the setup page in your browser (http://127.0.0.1:4747)
27
+ doctor Check the API key has every permission this config needs
28
+ plan Show the full schedule and what has already run
29
+ check Pre-flight: check the key, times, files, icons, sale items and alerts
30
+ preview Build every countdown icon from the live game icon into ./preview (no key needed)
31
+ tick Run every step that is due now, then exit (for cron / GitHub Actions)
32
+ watch Keep running and tick every 30 seconds until every schedule is done
33
+ restore Put prices, names and the countdown icon back to normal now
34
+ mcp Run as an MCP server so AI agents can plan and run update days
35
+
36
+ A config can hold one game, or several under "games". Commands run for every
37
+ game unless --game picks one. The API key is read from UPDATEDAY_API_KEY, or
38
+ from the variable a game names in "apiKeyEnv".`;
39
+ function formatTime(date) {
40
+ return date.getTime() === 0 ? "immediately" : date.toLocaleString();
41
+ }
42
+ function header(config, multi) {
43
+ if (multi) {
44
+ console.log(`\n=== ${config.name} (${config.universeId}) ===`);
45
+ }
46
+ }
47
+ function hasDueSteps(game) {
48
+ const now = new Date();
49
+ return game.steps.some((step) => step.at <= now && !game.ctx.state.done[step.id]);
50
+ }
51
+ function printPlan(game) {
52
+ console.log(`Update at ${new Date(game.config.updateAt).toLocaleString()}\n`);
53
+ for (const step of game.steps) {
54
+ const status = game.ctx.state.done[step.id] ? "done" : step.at <= game.ctx.now ? "DUE " : " ";
55
+ console.log(`[${status}] ${formatTime(step.at).padEnd(24)} ${step.label}`);
56
+ }
57
+ }
58
+ const CHECK_MARKS = { ok: "OK ", warn: "WARN", bad: "FIX ", info: " " };
59
+ function printPreflight(report) {
60
+ for (const check of report.checks) {
61
+ console.log(`[${CHECK_MARKS[check.level]}] ${check.title}`);
62
+ if (check.detail) {
63
+ console.log(` ${check.detail}`);
64
+ }
65
+ }
66
+ console.log(report.ready ? "Ready." : "Fix the lines marked FIX before the update.");
67
+ }
68
+ async function preview(config, multi) {
69
+ if (!config.countdown) {
70
+ console.log("Countdown icons are turned off for this game.");
71
+ return;
72
+ }
73
+ const folder = multi ? join(PREVIEW_DIR, config.name.replace(/[^\w-]+/g, "_")) : PREVIEW_DIR;
74
+ const outDir = resolve(config.baseDir, folder);
75
+ mkdirSync(outDir, { recursive: true });
76
+ const base = await fetchCurrentIcon(config.universeId);
77
+ for (const mark of iconMarks(config.countdown)) {
78
+ const path = join(outDir, `${mark}.png`);
79
+ writeFileSync(path, await renderIcon(base, mark, config.countdown));
80
+ console.log(`wrote ${path}`);
81
+ }
82
+ }
83
+ function webhookFor(config) {
84
+ return config.notify ? process.env[config.notify.discordWebhookEnv] : undefined;
85
+ }
86
+ function makeGame(config, needsKey) {
87
+ const apiKey = process.env[config.apiKeyEnv];
88
+ if (needsKey && !apiKey) {
89
+ throw new Error(`${config.name}: set ${config.apiKeyEnv} to an Open Cloud API key for this game`);
90
+ }
91
+ const ctx = {
92
+ config,
93
+ state: loadState(config.statePath, config.updateAt),
94
+ cloud: apiKey ? new OpenCloud(apiKey, config.universeId) : null,
95
+ now: new Date(),
96
+ notify: discordNotifier(config, webhookFor(config)),
97
+ };
98
+ return { config, steps: buildSchedule(config), ctx };
99
+ }
100
+ function pickGames(configs, filter) {
101
+ if (!filter) {
102
+ return configs;
103
+ }
104
+ const picked = configs.filter((config) => config.name === filter || String(config.universeId) === filter);
105
+ if (picked.length === 0) {
106
+ throw new Error(`No game named "${filter}" in this config`);
107
+ }
108
+ return picked;
109
+ }
110
+ async function tickGame(game, dryRun) {
111
+ try {
112
+ game.ctx.now = new Date();
113
+ const run = () => tick(game.steps, game.ctx, dryRun);
114
+ return dryRun ? await run() : await recording(game.config, run);
115
+ }
116
+ catch (error) {
117
+ console.error(`${game.config.name}: ${error instanceof Error ? error.message : error}`);
118
+ process.exitCode = 1;
119
+ return -1;
120
+ }
121
+ }
122
+ async function watch(load, multi) {
123
+ const finished = new Set();
124
+ while (true) {
125
+ let waiting = 0;
126
+ for (const config of load()) {
127
+ const id = `${config.universeId}|${config.updateAt}`;
128
+ if (finished.has(id)) {
129
+ waiting += nextOccurrence(config) ? 1 : 0;
130
+ continue;
131
+ }
132
+ const game = makeGame(config, true);
133
+ header(game.config, multi && hasDueSteps(game));
134
+ const remaining = await tickGame(game, false);
135
+ if (remaining !== 0) {
136
+ waiting++;
137
+ continue;
138
+ }
139
+ finished.add(id);
140
+ const next = nextOccurrence(config);
141
+ console.log(next ? `${config.name}: done. Next update day ${next.toLocaleString()}.` : `${config.name}: schedule complete.`);
142
+ waiting += next ? 1 : 0;
143
+ }
144
+ if (waiting === 0) {
145
+ return;
146
+ }
147
+ await new Promise((done) => setTimeout(done, WATCH_INTERVAL_MS));
148
+ }
149
+ }
150
+ const DOCTOR_MARKS = { ok: "OK ", missing: "MISS", error: "ERR ", untested: "-- " };
151
+ async function printDoctor(game) {
152
+ const probes = await checkKey(game.ctx.cloud, game.config);
153
+ for (const probe of probes) {
154
+ const flag = probe.used ? "" : " (not used by this config)";
155
+ console.log(`[${DOCTOR_MARKS[probe.status]}] ${probe.feature.padEnd(26)} ${probe.permission}${flag}`);
156
+ if (probe.detail && probe.status !== "ok") {
157
+ console.log(` ${probe.detail}`);
158
+ }
159
+ }
160
+ const blocking = probes.filter((probe) => probe.used && (probe.status === "missing" || probe.status === "error"));
161
+ if (blocking.length > 0) {
162
+ process.exitCode = 1;
163
+ }
164
+ }
165
+ function readFlag(args, flag) {
166
+ const index = args.indexOf(flag);
167
+ return index >= 0 ? args[index + 1] : undefined;
168
+ }
169
+ function positional(args) {
170
+ return args.filter((arg, index) => !arg.startsWith("--") && !["--game", "--port"].includes(args[index - 1]));
171
+ }
172
+ async function main() {
173
+ const args = process.argv.slice(2);
174
+ const dryRun = args.includes("--dry-run");
175
+ const [command, configPath = DEFAULT_CONFIG] = positional(args);
176
+ if (!command || command === "help") {
177
+ console.log(USAGE);
178
+ return;
179
+ }
180
+ if (command === "mcp") {
181
+ await startMcp(configPath);
182
+ return;
183
+ }
184
+ if (command === "ui") {
185
+ const port = Number(readFlag(args, "--port")) || DEFAULT_UI_PORT;
186
+ const { url } = await startServer(configPath, port);
187
+ console.log(`UpdateDay is open at ${url} (Ctrl+C to quit)`);
188
+ return;
189
+ }
190
+ const allConfigs = loadConfigs(configPath);
191
+ const configs = pickGames(allConfigs, readFlag(args, "--game"));
192
+ const multi = allConfigs.length > 1;
193
+ const needsKey = !dryRun && [...KEY_COMMANDS, "doctor"].includes(command);
194
+ if (command === "check") {
195
+ let ok = true;
196
+ for (const config of configs) {
197
+ header(config, multi);
198
+ const apiKey = process.env[config.apiKeyEnv];
199
+ const report = await preflight(config, apiKey ? new OpenCloud(apiKey, config.universeId) : null, webhookFor(config));
200
+ printPreflight(report);
201
+ ok = report.ready && ok;
202
+ }
203
+ process.exitCode = ok ? 0 : 1;
204
+ return;
205
+ }
206
+ if (command === "preview") {
207
+ for (const config of configs) {
208
+ header(config, multi);
209
+ await preview(config, multi);
210
+ }
211
+ return;
212
+ }
213
+ const games = configs.map((config) => makeGame(config, needsKey));
214
+ switch (command) {
215
+ case "doctor":
216
+ for (const game of games) {
217
+ header(game.config, multi);
218
+ await printDoctor(game);
219
+ }
220
+ return;
221
+ case "plan":
222
+ for (const game of games) {
223
+ header(game.config, multi);
224
+ printPlan(game);
225
+ }
226
+ return;
227
+ case "tick":
228
+ for (const game of games) {
229
+ header(game.config, multi);
230
+ await tickGame(game, dryRun);
231
+ }
232
+ return;
233
+ case "watch":
234
+ await watch(() => pickGames(loadConfigs(configPath), readFlag(args, "--game")), multi);
235
+ return;
236
+ case "restore":
237
+ for (const game of games) {
238
+ header(game.config, multi);
239
+ await recording(game.config, async () => {
240
+ await restorePrices(game.ctx);
241
+ await endCountdown(game.ctx);
242
+ });
243
+ saveState(game.config.statePath, game.ctx.state);
244
+ }
245
+ return;
246
+ default:
247
+ console.log(USAGE);
248
+ process.exitCode = 1;
249
+ }
250
+ }
251
+ main().catch((error) => {
252
+ console.error(error instanceof Error ? error.message : error);
253
+ process.exitCode = 1;
254
+ });