@screepts/screeps-api 1.0.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.
package/dist/cli.mjs ADDED
@@ -0,0 +1,978 @@
1
+ import { Command } from "commander";
2
+ import { EventEmitter } from "eventemitter3";
3
+ import Debug from "debug";
4
+ import { inflate } from "pako";
5
+ import yaml from "js-yaml";
6
+ import { readFile, writeFile } from "fs/promises";
7
+ import { join, parse } from "path";
8
+ //#region src/Socket.ts
9
+ const debug = Debug("screepsapi:socket");
10
+ const DEFAULTS$1 = {
11
+ reconnect: true,
12
+ resubscribe: true,
13
+ keepAlive: true,
14
+ maxRetries: 10,
15
+ maxRetryDelay: 60 * 1e3
16
+ };
17
+ var Socket = class extends EventEmitter {
18
+ ws;
19
+ api;
20
+ opts;
21
+ authed = false;
22
+ connected = false;
23
+ reconnecting = false;
24
+ keepAliveInter = 0;
25
+ __queue = [];
26
+ __subQueue = [];
27
+ __subs = {};
28
+ constructor(api) {
29
+ super();
30
+ this.api = api;
31
+ this.opts = Object.assign({}, DEFAULTS$1);
32
+ this.on("error", () => {});
33
+ this.reset();
34
+ this.on("auth", (ev) => {
35
+ if (ev.data.status === "ok") {
36
+ while (this.__queue.length) this.ws?.send(this.__queue.shift());
37
+ clearInterval(this.keepAliveInter);
38
+ if (this.opts.keepAlive) this.keepAliveInter = setInterval(() => this.ws && this.ws.send(""), 1e4);
39
+ }
40
+ });
41
+ }
42
+ reset() {
43
+ this.authed = false;
44
+ this.connected = false;
45
+ this.reconnecting = false;
46
+ clearInterval(this.keepAliveInter);
47
+ this.keepAliveInter = 0;
48
+ this.__queue = [];
49
+ this.__subQueue = [];
50
+ this.__subs = {};
51
+ }
52
+ async connect(opts = {}) {
53
+ Object.assign(this.opts, opts);
54
+ if (!this.api.token) throw new Error("No token! Call api.auth() before connecting the socket!");
55
+ return new Promise((resolve, reject) => {
56
+ const baseURL = this.api.opts.url.replace("http", "ws");
57
+ const wsurl = new URL("socket/websocket", baseURL);
58
+ this.ws = new WebSocket(wsurl);
59
+ this.ws.addEventListener("open", () => {
60
+ this.connected = true;
61
+ this.reconnecting = false;
62
+ if (this.opts.resubscribe) this.__subQueue.push(...Object.keys(this.__subs));
63
+ debug("connected");
64
+ this.emit("connected");
65
+ resolve(this.auth(this.api.token));
66
+ });
67
+ this.ws.onclose = () => {
68
+ clearInterval(this.keepAliveInter);
69
+ this.authed = false;
70
+ this.connected = false;
71
+ debug("disconnected");
72
+ this.emit("disconnected");
73
+ if (this.opts.reconnect) this.reconnect().catch(() => {});
74
+ };
75
+ this.ws.addEventListener("error", (err) => {
76
+ this.ws?.close();
77
+ this.emit("error", err);
78
+ debug(`error ${err.error || err}`);
79
+ if (!this.connected) reject(err);
80
+ });
81
+ this.ws.addEventListener("message", (data) => this.handleMessage(data));
82
+ });
83
+ }
84
+ async reconnect() {
85
+ if (this.reconnecting) return;
86
+ this.reconnecting = true;
87
+ let retries = 0;
88
+ let retry;
89
+ do {
90
+ let time = Math.pow(2, retries) * 100;
91
+ if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay;
92
+ await this.sleep(time);
93
+ if (!this.reconnecting) return;
94
+ try {
95
+ await this.connect();
96
+ retry = false;
97
+ } catch {
98
+ retry = true;
99
+ }
100
+ retries++;
101
+ debug(`reconnect ${retries}/${this.opts.maxRetries}`);
102
+ } while (retry && retries < this.opts.maxRetries);
103
+ if (retry) {
104
+ const err = /* @__PURE__ */ new Error(`Reconnection failed after ${this.opts.maxRetries} retries`);
105
+ this.reconnecting = false;
106
+ debug("reconnect failed");
107
+ this.emit("error", err);
108
+ throw err;
109
+ } else Object.keys(this.__subs).forEach((sub) => this.subscribe(sub));
110
+ }
111
+ disconnect() {
112
+ debug("disconnect");
113
+ clearInterval(this.keepAliveInter);
114
+ if (this.ws) {
115
+ this.ws.onclose = null;
116
+ this.ws.close();
117
+ }
118
+ this.reset();
119
+ this.emit("disconnected");
120
+ }
121
+ sleep(time) {
122
+ return new Promise((resolve) => setTimeout(resolve, time));
123
+ }
124
+ handleMessage(ev) {
125
+ const msg = this.api.gz(ev.data || ev);
126
+ debug(`message ${msg}`);
127
+ if (msg[0] === "[") {
128
+ const arr = JSON.parse(msg);
129
+ let [, type, id, channel] = arr[0].match(/^(.+):(.+?)(?:\/(.+))?$/);
130
+ channel = channel || type;
131
+ const event = {
132
+ channel,
133
+ id,
134
+ type,
135
+ data: arr[1]
136
+ };
137
+ this.emit(msg[0], event);
138
+ this.emit(event.channel, event);
139
+ this.emit("message", event);
140
+ } else {
141
+ const [channel, ...data] = msg.split(" ");
142
+ const event = {
143
+ type: "server",
144
+ channel,
145
+ data
146
+ };
147
+ if (channel === "auth") event.data = {
148
+ status: data[0],
149
+ token: data[1]
150
+ };
151
+ if ([
152
+ "protocol",
153
+ "time",
154
+ "package"
155
+ ].includes(channel)) event.data = { [channel]: data[0] };
156
+ this.emit(channel, event);
157
+ this.emit("message", event);
158
+ }
159
+ }
160
+ async gzip(bool) {
161
+ await this.send(`gzip ${bool ? "on" : "off"}`);
162
+ }
163
+ async send(data) {
164
+ if (!this.connected) this.__queue.push(data);
165
+ else this.ws.send(data);
166
+ }
167
+ async auth(token) {
168
+ const waitAuth = new Promise((resolve) => this.once("auth", resolve));
169
+ await this.send(`auth ${token}`);
170
+ const { data } = await waitAuth;
171
+ if (data.status !== "ok") throw new Error("socket auth failed");
172
+ this.authed = true;
173
+ this.emit("token", data.token);
174
+ this.emit("authed");
175
+ while (this.__subQueue.length) await this.send(this.__subQueue.shift());
176
+ }
177
+ async subscribe(path, cb) {
178
+ if (!path) return;
179
+ const userID = await this.api.userID();
180
+ if (!path.match(/^(\w+):(.+?)$/)) path = `user:${userID}/${path}`;
181
+ if (this.authed) await this.send(`subscribe ${path}`);
182
+ else this.__subQueue.push(`subscribe ${path}`);
183
+ this.emit("subscribe", path);
184
+ this.__subs[path] = this.__subs[path] || 0;
185
+ this.__subs[path]++;
186
+ if (cb) this.on(path, cb);
187
+ }
188
+ async unsubscribe(path) {
189
+ if (!path) return;
190
+ const userID = await this.api.userID();
191
+ if (!path.match(/^(\w+):(.+?)$/)) path = `user:${userID}/${path}`;
192
+ await this.send(`unsubscribe ${path}`);
193
+ this.emit("unsubscribe", path);
194
+ if (this.__subs[path]) this.__subs[path]--;
195
+ }
196
+ };
197
+ //#endregion
198
+ //#region src/RawAPI.ts
199
+ const debugHttp = Debug("screepsapi:http");
200
+ const debugRateLimit = Debug("screepsapi:ratelimit");
201
+ const DEFAULT_SHARD = "shard0";
202
+ const OFFICIAL_HISTORY_INTERVAL = 100;
203
+ const PRIVATE_HISTORY_INTERVAL = 20;
204
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
205
+ const mapToShard = (res) => {
206
+ if (!res.shards) res.shards = { privSrv: res.list || res.rooms };
207
+ return res;
208
+ };
209
+ var RawAPI = class extends EventEmitter {
210
+ opts;
211
+ token;
212
+ __authed = false;
213
+ constructor(opts = {}) {
214
+ super();
215
+ this.opts = {};
216
+ this.setServer(opts);
217
+ }
218
+ raw = {
219
+ token: void 0,
220
+ version: () => this.req("GET", "/api/version"),
221
+ authmod: () => {
222
+ if (this.isOfficialServer()) return Promise.resolve({
223
+ ok: 1,
224
+ name: "official"
225
+ });
226
+ return this.req("GET", "/api/authmod");
227
+ },
228
+ history: (room, tick, shard = DEFAULT_SHARD) => {
229
+ if (this.isOfficialServer()) {
230
+ tick -= tick % OFFICIAL_HISTORY_INTERVAL;
231
+ return this.req("GET", `/room-history/${shard}/${room}/${tick}.json`);
232
+ } else {
233
+ tick -= tick % PRIVATE_HISTORY_INTERVAL;
234
+ return this.req("GET", "/room-history", {
235
+ room,
236
+ time: tick
237
+ });
238
+ }
239
+ },
240
+ servers: { list: () => this.req("POST", "/api/servers/list", {}) },
241
+ auth: {
242
+ signin: (email, password) => this.req("POST", "/api/auth/signin", {
243
+ email,
244
+ password
245
+ }),
246
+ steamTicket: (ticket, useNativeAuth = false) => this.req("POST", "/api/auth/steam-ticket", {
247
+ ticket,
248
+ useNativeAuth
249
+ }),
250
+ me: () => this.req("GET", "/api/auth/me"),
251
+ queryToken: (token) => this.req("GET", "/api/auth/query-token", { token })
252
+ },
253
+ register: {
254
+ checkEmail: (email) => this.req("GET", "/api/register/check-email", { email }),
255
+ checkUsername: (username) => this.req("GET", "/api/register/check-username", { username }),
256
+ setUsername: (username) => this.req("POST", "/api/register/set-username", { username }),
257
+ submit: (username, email, password, modules) => this.req("POST", "/api/register/submit", {
258
+ username,
259
+ email,
260
+ password,
261
+ modules
262
+ })
263
+ },
264
+ userMessages: {
265
+ list: (respondent) => this.req("GET", "/api/user/messages/list", { respondent }),
266
+ index: () => this.req("GET", "/api/user/messages/index"),
267
+ unreadCount: () => this.req("GET", "/api/user/messages/unread-count"),
268
+ send: (respondent, text) => this.req("POST", "/api/user/messages/send", {
269
+ respondent,
270
+ text
271
+ }),
272
+ markRead: (id) => this.req("POST", "/api/user/messages/mark-read", { id })
273
+ },
274
+ game: {
275
+ mapStats: (rooms, statName, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/map-stats", {
276
+ rooms,
277
+ statName,
278
+ shard
279
+ }),
280
+ genUniqueObjectName: (type, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/gen-unique-object-name", {
281
+ type,
282
+ shard
283
+ }),
284
+ checkUniqueObjectName: (type, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/check-unique-object-name", {
285
+ type,
286
+ name,
287
+ shard
288
+ }),
289
+ placeSpawn: (room, x, y, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/place-spawn", {
290
+ name,
291
+ room,
292
+ x,
293
+ y,
294
+ shard
295
+ }),
296
+ createFlag: (room, x, y, name, color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/create-flag", {
297
+ name,
298
+ room,
299
+ x,
300
+ y,
301
+ color,
302
+ secondaryColor,
303
+ shard
304
+ }),
305
+ genUniqueFlagName: (shard = DEFAULT_SHARD) => this.req("POST", "/api/game/gen-unique-flag-name", { shard }),
306
+ checkUniqueFlagName: (name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/check-unique-flag-name", {
307
+ name,
308
+ shard
309
+ }),
310
+ changeFlagColor: (color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/change-flag-color", {
311
+ color,
312
+ secondaryColor,
313
+ shard
314
+ }),
315
+ removeFlag: (room, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/remove-flag", {
316
+ name,
317
+ room,
318
+ shard
319
+ }),
320
+ addObjectIntent: (room, name, intent, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/add-object-intent", {
321
+ room,
322
+ name,
323
+ intent,
324
+ shard
325
+ }),
326
+ createConstruction: (room, x, y, structureType, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/create-construction", {
327
+ room,
328
+ x,
329
+ y,
330
+ structureType,
331
+ name,
332
+ shard
333
+ }),
334
+ setNotifyWhenAttacked: (_id, enabled = true, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/set-notify-when-attacked", {
335
+ _id,
336
+ enabled,
337
+ shard
338
+ }),
339
+ createInvader: (room, x, y, size, type, boosted = false, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/create-invader", {
340
+ room,
341
+ x,
342
+ y,
343
+ size,
344
+ type,
345
+ boosted,
346
+ shard
347
+ }),
348
+ removeInvader: (_id, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/remove-invader", {
349
+ _id,
350
+ shard
351
+ }),
352
+ time: (shard = DEFAULT_SHARD) => this.req("GET", "/api/game/time", { shard }),
353
+ worldSize: (shard = DEFAULT_SHARD) => this.req("GET", "/api/game/world-size", { shard }),
354
+ roomDecorations: (room, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-decorations", {
355
+ room,
356
+ shard
357
+ }),
358
+ roomObjects: (room, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-objects", {
359
+ room,
360
+ shard
361
+ }),
362
+ roomTerrain: (room, encoded = 1, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-terrain", {
363
+ room,
364
+ encoded,
365
+ shard
366
+ }),
367
+ roomStatus: (room, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-status", {
368
+ room,
369
+ shard
370
+ }),
371
+ roomOverview: (room, interval = 8, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-overview", {
372
+ room,
373
+ interval,
374
+ shard
375
+ }),
376
+ market: {
377
+ ordersIndex: (shard = DEFAULT_SHARD) => this.req("GET", "/api/game/market/orders-index", { shard }),
378
+ myOrders: () => this.req("GET", "/api/game/market/my-orders").then(mapToShard),
379
+ orders: (resourceType, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/market/orders", {
380
+ resourceType,
381
+ shard
382
+ }),
383
+ stats: (resourceType, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/market/stats", {
384
+ resourceType,
385
+ shard
386
+ })
387
+ },
388
+ shards: { info: () => this.req("GET", "/api/game/shards/info") }
389
+ },
390
+ leaderboard: {
391
+ list: (limit = 10, mode = "world", offset = 0, season) => {
392
+ if (mode !== "world" && mode !== "power") throw new Error("incorrect mode parameter");
393
+ if (!season) season = this.currentSeason();
394
+ return this.req("GET", "/api/leaderboard/list", {
395
+ limit,
396
+ mode,
397
+ offset,
398
+ season
399
+ });
400
+ },
401
+ find: (username, mode = "world", season = "") => this.req("GET", "/api/leaderboard/find", {
402
+ season,
403
+ mode,
404
+ username
405
+ }),
406
+ seasons: () => this.req("GET", "/api/leaderboard/seasons")
407
+ },
408
+ user: {
409
+ badge: (badge) => this.req("POST", "/api/user/badge", { badge }),
410
+ respawn: () => this.req("POST", "/api/user/respawn"),
411
+ setActiveBranch: (branch, activeName) => this.req("POST", "/api/user/set-active-branch", {
412
+ branch,
413
+ activeName
414
+ }),
415
+ cloneBranch: (branch = "", newName, defaultModules) => this.req("POST", "/api/user/clone-branch", {
416
+ branch,
417
+ newName,
418
+ defaultModules
419
+ }),
420
+ deleteBranch: (branch) => this.req("POST", "/api/user/delete-branch", { branch }),
421
+ notifyPrefs: (prefs) => this.req("POST", "/api/user/notify-prefs", prefs),
422
+ tutorialDone: () => this.req("POST", "/api/user/tutorial-done"),
423
+ email: (email) => this.req("POST", "/api/user/email", { email }),
424
+ worldStartRoom: (shard) => this.req("GET", "/api/user/world-start-room", { shard }),
425
+ worldStatus: () => this.req("GET", "/api/user/world-status"),
426
+ branches: () => this.req("GET", "/api/user/branches"),
427
+ code: {
428
+ get: (branch) => this.req("GET", "/api/user/code", { branch }),
429
+ set: (branch, modules, _hash) => {
430
+ if (!_hash) _hash = Date.now();
431
+ return this.req("POST", "/api/user/code", {
432
+ branch,
433
+ modules,
434
+ _hash
435
+ });
436
+ }
437
+ },
438
+ decorations: {
439
+ inventory: () => this.req("GET", "/api/user/decorations/inventory"),
440
+ themes: () => this.req("GET", "/api/user/decorations/themes"),
441
+ convert: (decorations) => this.req("POST", "/api/user/decorations/convert", { decorations }),
442
+ pixelize: (count, theme = "") => this.req("POST", "/api/user/decorations/pixelize", {
443
+ count,
444
+ theme
445
+ }),
446
+ activate: (_id, active) => this.req("POST", "/api/user/decorations/activate", {
447
+ _id,
448
+ active
449
+ }),
450
+ deactivate: (decorations) => this.req("POST", "/api/user/decorations/deactivate", { decorations })
451
+ },
452
+ respawnProhibitedRooms: () => this.req("GET", "/api/user/respawn-prohibited-rooms"),
453
+ memory: {
454
+ get: (path = "", shard = DEFAULT_SHARD) => this.req("GET", "/api/user/memory", {
455
+ path,
456
+ shard
457
+ }),
458
+ set: (path, value, shard = DEFAULT_SHARD) => this.req("POST", "/api/user/memory", {
459
+ path,
460
+ value,
461
+ shard
462
+ }),
463
+ segment: {
464
+ get: (segment, shard = DEFAULT_SHARD) => this.req("GET", "/api/user/memory-segment", {
465
+ segment,
466
+ shard
467
+ }),
468
+ set: (segment, data, shard = DEFAULT_SHARD) => this.req("POST", "/api/user/memory-segment", {
469
+ segment,
470
+ data,
471
+ shard
472
+ })
473
+ }
474
+ },
475
+ find: (username) => this.req("GET", "/api/user/find", { username }),
476
+ findById: (id) => this.req("GET", "/api/user/find", { id }),
477
+ stats: (interval) => this.req("GET", "/api/user/stats", { interval }),
478
+ rooms: (id) => this.req("GET", "/api/user/rooms", { id }).then(mapToShard),
479
+ overview: (interval, statName) => this.req("GET", "/api/user/overview", {
480
+ interval,
481
+ statName
482
+ }),
483
+ moneyHistory: (page = 0) => this.req("GET", "/api/user/money-history", { page }),
484
+ console: (expression, shard = DEFAULT_SHARD) => this.req("POST", "/api/user/console", {
485
+ expression,
486
+ shard
487
+ }),
488
+ name: () => this.req("GET", "/api/user/name")
489
+ },
490
+ experimental: {
491
+ pvp: (interval = 100) => this.req("GET", "/api/experimental/pvp", { interval }).then(mapToShard),
492
+ nukes: () => this.req("GET", "/api/experimental/nukes").then(mapToShard)
493
+ },
494
+ warpath: { battles: (interval = 100) => this.req("GET", "/api/warpath/battles", { interval }) },
495
+ scoreboard: { list: (limit = 20, offset = 0) => this.req("GET", "/api/scoreboard/list", {
496
+ limit,
497
+ offset
498
+ }) }
499
+ };
500
+ currentSeason() {
501
+ const now = /* @__PURE__ */ new Date();
502
+ const year = now.getFullYear();
503
+ let month = (now.getUTCMonth() + 1).toString();
504
+ if (month.length === 1) month = `0${month}`;
505
+ return `${year}-${month}`;
506
+ }
507
+ isOfficialServer() {
508
+ return this.opts.url.match(/screeps\.com/) !== null;
509
+ }
510
+ mapToShard = mapToShard;
511
+ setServer(opts) {
512
+ if (!this.opts) this.opts = {};
513
+ Object.assign(this.opts, opts);
514
+ if (opts.path && !opts.pathname) this.opts.pathname = opts.path;
515
+ if (opts.port) {
516
+ this.opts.port = String(opts.port);
517
+ if (opts.hostname) this.opts.host = `${opts.hostname}:${opts.port}`;
518
+ }
519
+ if (!opts.url) {
520
+ this.opts.url = urlFormat(this.opts);
521
+ if (!this.opts.url.endsWith("/")) this.opts.url += "/";
522
+ }
523
+ if (opts.token) this.token = opts.token;
524
+ }
525
+ async auth(email, password, opts = {}) {
526
+ this.setServer(opts);
527
+ if (email && password) Object.assign(this.opts, {
528
+ email,
529
+ password
530
+ });
531
+ const res = await this.raw.auth.signin(this.opts.email, this.opts.password);
532
+ this.emit("token", res.token);
533
+ this.emit("auth");
534
+ this.__authed = true;
535
+ return res;
536
+ }
537
+ async req(method, path, body = {}) {
538
+ let url = new URL(path, this.opts.url);
539
+ const opts = {
540
+ method,
541
+ headers: {}
542
+ };
543
+ if (debugHttp.enabled) debugHttp(`${method} ${path}`);
544
+ if (this.token) Object.assign(opts.headers, {
545
+ "X-Token": this.token,
546
+ "X-Username": this.token
547
+ });
548
+ if (method === "GET") Object.entries(body).forEach(([key, value]) => {
549
+ if (value !== void 0) url.searchParams.append(key, String(value));
550
+ });
551
+ else {
552
+ opts.body = body;
553
+ if (typeof body === "object") {
554
+ opts.body = JSON.stringify(body);
555
+ opts.headers["Content-Type"] = "application/json";
556
+ }
557
+ }
558
+ const res = await fetch(url, opts);
559
+ const token = res.headers.get("x-token");
560
+ if (token) this.emit("token", token);
561
+ const rateLimit = this.buildRateLimit(method, path, res);
562
+ this.emit("rateLimit", rateLimit);
563
+ debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`);
564
+ if (res.headers.get("content-type")?.includes("application/json")) res.data = await res.json();
565
+ else res.data = await res.text();
566
+ if (!res.ok) {
567
+ if (res.status === 401) if (this.__authed && this.opts.email && this.opts.password) {
568
+ this.__authed = false;
569
+ await this.auth(this.opts.email, this.opts.password);
570
+ return this.req(method, path, body);
571
+ } else throw new Error("Not Authorized");
572
+ else if (res.status === 429 && !res.headers.get("x-ratelimit-limit") && this.opts.experimentalRetry429) {
573
+ await sleep(Math.floor(Math.random() * 500) + 200);
574
+ return this.req(method, path, body);
575
+ }
576
+ throw new Error(res.data);
577
+ }
578
+ this.emit("response", res);
579
+ return res.data;
580
+ }
581
+ gz(data) {
582
+ if (!data.startsWith("gz:")) return data;
583
+ return inflate(Buffer.from(data.slice(3), "base64"), { to: "string" });
584
+ }
585
+ inflate(data) {
586
+ return JSON.parse(this.gz(data));
587
+ }
588
+ buildRateLimit(method, path, res) {
589
+ const limit = Number(res.headers.get("x-ratelimit-limit"));
590
+ const remaining = Number(res.headers.get("x-ratelimit-remaining"));
591
+ const reset = Number(res.headers.get("x-ratelimit-reset"));
592
+ return {
593
+ method,
594
+ path,
595
+ limit,
596
+ remaining,
597
+ reset,
598
+ toReset: reset - Math.floor(Date.now() / 1e3)
599
+ };
600
+ }
601
+ };
602
+ /** Based on Node.js built-in URL format */
603
+ function urlFormat(obj) {
604
+ let protocol = obj.protocol || "";
605
+ if (protocol && !protocol.endsWith(":")) protocol += ":";
606
+ let pathname = obj.pathname || "";
607
+ let host = "";
608
+ if (obj.host) host = obj.host;
609
+ else if (obj.hostname) {
610
+ host = obj.hostname.includes(":") && (obj.hostname[0] !== "[" || obj.hostname[obj.hostname.length - 1] !== "]") ? "[" + obj.hostname + "]" : obj.hostname;
611
+ if (obj.port) host += ":" + obj.port;
612
+ }
613
+ if (pathname.includes("#") || pathname.includes("?")) {
614
+ let newPathname = "";
615
+ let lastPos = 0;
616
+ const len = pathname.length;
617
+ for (let i = 0; i < len; i++) {
618
+ const code = pathname.charAt(i);
619
+ if (code === "#" || code === "?") {
620
+ if (i > lastPos) newPathname += pathname.slice(lastPos, i);
621
+ newPathname += code === "#" ? "%23" : "%3F";
622
+ lastPos = i + 1;
623
+ }
624
+ }
625
+ if (lastPos < len) newPathname += pathname.slice(lastPos);
626
+ pathname = newPathname;
627
+ }
628
+ if (host) {
629
+ if (pathname && pathname[0] !== "/") pathname = "/" + pathname;
630
+ host = "//" + host;
631
+ }
632
+ return protocol + host + pathname;
633
+ }
634
+ //#endregion
635
+ //#region src/ConfigManager.ts
636
+ /**
637
+ * Utility class for loading Unified Credentials Files from disk, environment variables, or other sources.
638
+ * Only platforms without filesystem access, {@link ConfigManager.setConfig}, must be used to provide the config data directly.
639
+ */
640
+ var ConfigManager = class {
641
+ path;
642
+ _config = null;
643
+ /** Custom environment variables, useful for platforms without process.env */
644
+ env = globalThis.process?.env || {};
645
+ /** Custom file reading function, useful for platforms with a virtual filesystem */
646
+ readFile;
647
+ setConfig(data, path) {
648
+ if (!data || typeof data !== "object" || !("servers" in data)) throw new Error(`Invalid config: 'servers' object does not exist in '${path}'`);
649
+ this._config = data;
650
+ this.path = path;
651
+ return this._config;
652
+ }
653
+ async refresh() {
654
+ this._config = null;
655
+ await this.getConfig();
656
+ }
657
+ async getServers() {
658
+ const conf = await this.getConfig();
659
+ return conf ? Object.keys(conf.servers) : [];
660
+ }
661
+ async getConfig() {
662
+ if (this._config) return this._config;
663
+ const paths = [];
664
+ if (this.env.SCREEPS_CONFIG) paths.push(this.env.SCREEPS_CONFIG);
665
+ const dirs = ["", import.meta.dirname];
666
+ for (const dir of dirs) {
667
+ paths.push(join$1(dir, ".screeps.yaml"));
668
+ paths.push(join$1(dir, ".screeps.yml"));
669
+ }
670
+ if (process.platform === "win32" && this.env.APPDATA) {
671
+ paths.push(join$1(this.env.APPDATA, "screeps/config.yaml"));
672
+ paths.push(join$1(this.env.APPDATA, "screeps/config.yml"));
673
+ } else {
674
+ if (this.env.XDG_CONFIG_HOME) {
675
+ paths.push(join$1(this.env.XDG_CONFIG_HOME, "screeps/config.yaml"));
676
+ paths.push(join$1(this.env.XDG_CONFIG_HOME, "screeps/config.yml"));
677
+ }
678
+ if (this.env.HOME) {
679
+ paths.push(join$1(this.env.HOME, ".config/screeps/config.yaml"));
680
+ paths.push(join$1(this.env.HOME, ".config/screeps/config.yml"));
681
+ paths.push(join$1(this.env.HOME, ".screeps.yaml"));
682
+ paths.push(join$1(this.env.HOME, ".screeps.yml"));
683
+ }
684
+ }
685
+ for (const path of paths) {
686
+ const data = await this.loadConfig(path);
687
+ if (data) return data;
688
+ }
689
+ return null;
690
+ }
691
+ async loadConfig(path) {
692
+ if (!this.readFile) {
693
+ const { readFile } = await import("fs/promises");
694
+ this.readFile = readFile;
695
+ }
696
+ let contents;
697
+ try {
698
+ contents = await this.readFile(path, { encoding: "utf8" });
699
+ } catch (e) {
700
+ if (e.code === "ENOENT") return null;
701
+ else throw e;
702
+ }
703
+ const data = yaml.load(contents);
704
+ return this.setConfig(data, path);
705
+ }
706
+ };
707
+ function join$1(a, b) {
708
+ return a + (a.endsWith("/") ? "" : "/") + b;
709
+ }
710
+ //#endregion
711
+ //#region src/ScreepsAPI.ts
712
+ const DEFAULTS = {
713
+ protocol: "https",
714
+ hostname: "screeps.com",
715
+ port: 443,
716
+ path: "/"
717
+ };
718
+ const configManager = new ConfigManager();
719
+ var ScreepsAPI = class ScreepsAPI extends RawAPI {
720
+ socket;
721
+ appConfig = {};
722
+ rateLimits;
723
+ _user;
724
+ _tokenInfo;
725
+ static async fromConfig(server = "main", config = false, opts = {}) {
726
+ const data = await configManager.getConfig();
727
+ if (!data) throw new Error("No valid config found");
728
+ if (!server && process.stdin.isTTY && process.stdout.isTTY) {
729
+ const { select, isCancel } = await import("@clack/prompts");
730
+ const selectedServer = await select({
731
+ message: "Select a server:",
732
+ withGuide: false,
733
+ options: Object.entries(data.servers).map(([value, args]) => ({
734
+ value,
735
+ hint: args.host
736
+ }))
737
+ });
738
+ if (isCancel(selectedServer)) throw new Error("Server selection cancelled");
739
+ server = selectedServer;
740
+ }
741
+ const conf = data.servers[server];
742
+ if (!conf) throw new Error(`Server '${server}' does not exist in '${configManager.path}'`);
743
+ if (conf.ptr) conf.path = "/ptr";
744
+ if (conf.season) conf.path = "/season";
745
+ const api = new ScreepsAPI(Object.assign({
746
+ hostname: conf.host,
747
+ protocol: conf.secure ? "https" : "http",
748
+ path: "/"
749
+ }, conf, opts));
750
+ api.appConfig = data.configs && data.configs[config] || {};
751
+ if (!conf.token && conf.username && conf.password) await api.auth(conf.username, conf.password);
752
+ return api;
753
+ }
754
+ constructor(opts = {}) {
755
+ opts = Object.assign({}, DEFAULTS, opts);
756
+ super(opts);
757
+ this.on("token", (token) => {
758
+ this.token = token;
759
+ this.raw.token = token;
760
+ });
761
+ const defaultLimit = (limit, period) => ({
762
+ limit,
763
+ period,
764
+ remaining: limit,
765
+ reset: 0,
766
+ toReset: 0
767
+ });
768
+ this.rateLimits = {
769
+ global: defaultLimit(120, "minute"),
770
+ GET: {
771
+ "/api/game/room-terrain": defaultLimit(360, "hour"),
772
+ "/api/user/code": defaultLimit(60, "hour"),
773
+ "/api/user/memory": defaultLimit(1440, "day"),
774
+ "/api/user/memory-segment": defaultLimit(360, "hour"),
775
+ "/api/game/market/orders-index": defaultLimit(60, "hour"),
776
+ "/api/game/market/orders": defaultLimit(60, "hour"),
777
+ "/api/game/market/my-orders": defaultLimit(60, "hour"),
778
+ "/api/game/market/stats": defaultLimit(60, "hour"),
779
+ "/api/game/user/money-history": defaultLimit(60, "hour")
780
+ },
781
+ POST: {
782
+ "/api/user/console": defaultLimit(360, "hour"),
783
+ "/api/game/map-stats": defaultLimit(60, "hour"),
784
+ "/api/user/code": defaultLimit(240, "day"),
785
+ "/api/user/set-active-branch": defaultLimit(240, "day"),
786
+ "/api/user/memory": defaultLimit(240, "day"),
787
+ "/api/user/memory-segment": defaultLimit(60, "hour")
788
+ }
789
+ };
790
+ this.on("rateLimit", (limits) => {
791
+ const rate = this.rateLimits[limits.method]?.[limits.path] || this.rateLimits.global;
792
+ const copy = Object.assign({}, limits);
793
+ delete copy.path;
794
+ delete copy.method;
795
+ Object.assign(rate, copy);
796
+ });
797
+ this.socket = new Socket(this);
798
+ }
799
+ getRateLimit(method, path) {
800
+ return this.rateLimits[method][path] || this.rateLimits.global;
801
+ }
802
+ get rateLimitResetUrl() {
803
+ return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${this.token.slice(0, 8)}`;
804
+ }
805
+ async me() {
806
+ if (this._user) return this._user;
807
+ if ((await this.tokenInfo()).full) this._user = await this.raw.auth.me();
808
+ else {
809
+ const { username } = await this.raw.user.name();
810
+ const { ok, user } = await this.raw.user.find(username);
811
+ this._user = {
812
+ ...user,
813
+ ok
814
+ };
815
+ }
816
+ return this._user;
817
+ }
818
+ async tokenInfo() {
819
+ if (this._tokenInfo) return this._tokenInfo;
820
+ if ("token" in this.opts) {
821
+ const { token } = await this.raw.auth.queryToken(this.token);
822
+ this._tokenInfo = token;
823
+ } else this._tokenInfo = { full: true };
824
+ return this._tokenInfo;
825
+ }
826
+ async userID() {
827
+ return (await this.me())._id;
828
+ }
829
+ registerUser = this.raw.register.submit;
830
+ history = this.raw.history;
831
+ authmod = this.raw.authmod;
832
+ version = this.raw.version;
833
+ time = this.raw.game.time;
834
+ leaderboard = this.raw.leaderboard;
835
+ market = this.raw.game.market;
836
+ console = this.raw.user.console;
837
+ code = codeRepository(this);
838
+ memory = {
839
+ ...this.raw.user.memory,
840
+ get: async (path, shard) => {
841
+ const { data } = await this.raw.user.memory.get(path, shard);
842
+ return this.gz(data);
843
+ }
844
+ };
845
+ segment = this.raw.user.memory.segment;
846
+ };
847
+ const codeRepository = ({ raw: { user } }) => ({
848
+ get: user.code.get,
849
+ set: async (branch, code) => {
850
+ const { list } = await user.branches();
851
+ if (list.some((b) => b.branch == branch)) return user.code.set(branch, code);
852
+ else return user.cloneBranch("", branch, code);
853
+ },
854
+ branches: user.branches,
855
+ cloneBranch: user.cloneBranch,
856
+ deleteBranch: user.deleteBranch,
857
+ setActiveBranch: user.setActiveBranch
858
+ });
859
+ //#endregion
860
+ //#region package.json
861
+ var version = "1.0.0";
862
+ //#endregion
863
+ //#region src/cli.ts
864
+ async function init(opts) {
865
+ return ScreepsAPI.fromConfig(opts.server);
866
+ }
867
+ async function out(data) {
868
+ data = await data;
869
+ data = data && data.data || data;
870
+ if (process.stdout.isTTY) console.log(data);
871
+ else process.stdout.write(JSON.stringify(data));
872
+ }
873
+ async function run() {
874
+ const program = new Command();
875
+ /** @param {string} name */
876
+ const commandBase = (name, args = "") => {
877
+ const command = new Command(name);
878
+ command.arguments(args).option("--server <server>", "Server config to use", "");
879
+ program.addCommand(command);
880
+ return command;
881
+ };
882
+ program.version(version);
883
+ commandBase("raw", "<cmd> [args...]").description("Execute raw API call").action(async function(cmd, args, opts) {
884
+ try {
885
+ const api = await init(opts);
886
+ const path = cmd.split(".");
887
+ let fn = api.raw;
888
+ for (const part of path) fn = fn[part];
889
+ if (!fn || typeof fn !== "function") {
890
+ console.log("Invalid cmd");
891
+ return;
892
+ }
893
+ await out(fn.apply(api, args));
894
+ } catch (e) {
895
+ console.error(e);
896
+ }
897
+ });
898
+ commandBase("memory", "[path]").description(`Get Memory contents`).option("--set <file>", "Sets the memory path to the contents of file").option("--allow-root", "Allows writing without path").option("-s --shard <shard>", "Shard to read from", "shard0").option("-f --file <file>", "File to write data to").action(async function(fpath, opts) {
899
+ try {
900
+ const api = await init(opts);
901
+ if (opts.set) {
902
+ if (!fpath && !opts.allowRoot) throw new Error("Refusing to write to root! Use --allow-root if you really want this.");
903
+ const data = await readFile(opts.set, "utf8");
904
+ await api.memory.set(fpath, data, opts.shard);
905
+ await out("Memory written");
906
+ } else {
907
+ const data = await api.memory.get(fpath, opts.shard);
908
+ if (opts.file) await writeFile(opts.file, data);
909
+ else await out(data);
910
+ }
911
+ } catch (e) {
912
+ console.error(e);
913
+ }
914
+ });
915
+ commandBase("segment", "<segment>").description(`Get segment contents. Use 'all' to get all)`).option("--set <file>", "Sets the segment content to the contents of file").option("-s --shard <shard>", "Shard to read from", "shard0").option("-d --dir <dir>", "Directory to save in. Empty files are not written. (defaults to outputing in console)").action(async function(segment, opts) {
916
+ try {
917
+ const api = await init(opts);
918
+ if (opts.set) {
919
+ const data = await readFile(opts.set, "utf8");
920
+ await api.memory.segment.set(segment, data, opts.shard);
921
+ await out("Segment Set");
922
+ } else {
923
+ if (segment === "all") segment = Array.from({ length: 100 }, (v, k) => k).join(",");
924
+ const { data } = await api.memory.segment.get(segment, opts.shard);
925
+ const dir = opts.dir;
926
+ const segments = data;
927
+ if (dir) {
928
+ if (Array.isArray(segments)) await Promise.all(segments.map((d, i) => d && writeFile(join(dir, `segment_${i}`), d)));
929
+ else await writeFile(join(dir, `segment_${segment}`), data);
930
+ await out("Segments Saved");
931
+ } else await out(segments);
932
+ }
933
+ } catch (e) {
934
+ console.error(e);
935
+ }
936
+ });
937
+ commandBase("download").description(`Download code`).option("-b --branch <branch>", "Code branch", "default").option("-d --dir <dir>", "Directory to save in (defaults to outputing in console)").action(async function(opts) {
938
+ try {
939
+ const api = await init(opts);
940
+ const dir = opts.dir;
941
+ const { modules } = await api.code.get(opts.branch);
942
+ if (dir) await Promise.all(Object.keys(modules).map(async (fn) => {
943
+ const data = modules[fn];
944
+ if (typeof data === "string") await writeFile(join(dir, `${fn}.js`), data);
945
+ else await writeFile(join(dir, `${fn}.wasm`), Buffer.from(data.binary, "base64"));
946
+ console.log(`Saved ${fn}`);
947
+ }));
948
+ else await out(modules);
949
+ } catch (e) {
950
+ console.error(e);
951
+ }
952
+ });
953
+ commandBase("upload", "<files...>").description(`Upload code`).option("-b --branch <branch>", "Code branch", "default").action(async function(files, opts) {
954
+ try {
955
+ const api = await init(opts);
956
+ const modules = {};
957
+ const ps = [];
958
+ for (const file of files) ps.push((async (file) => {
959
+ const { name, ext } = parse(file);
960
+ const data = await readFile(file);
961
+ if (ext === ".js") modules[name] = data.toString("utf8");
962
+ if (ext === ".wasm") modules[name] = { binary: data.toString("base64") };
963
+ })(file));
964
+ await Promise.all(ps);
965
+ await out(api.code.set(opts.branch, modules));
966
+ } catch (e) {
967
+ console.error(e);
968
+ }
969
+ });
970
+ if (!process.argv.slice(2).length) program.outputHelp();
971
+ await program.parseAsync();
972
+ }
973
+ run().then((data) => {
974
+ if (!data) return;
975
+ console.log(JSON.stringify(data.data || data, null, 2));
976
+ }).catch(console.error);
977
+ //#endregion
978
+ export {};