@mauricode/token-derby 1.1.0 → 2.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/README.md +1 -0
- package/dist/bin.js +640 -373
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -250,7 +250,7 @@ function defaultColors() {
|
|
|
250
250
|
|
|
251
251
|
// src/ui/HorseCreator.tsx
|
|
252
252
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
253
|
-
function HorseCreator({ onSubmit, onCancel, initialColors, initialName, lockName }) {
|
|
253
|
+
function HorseCreator({ onSubmit, onCancel, initialColors, initialName, lockName, initialLevel }) {
|
|
254
254
|
const [colors, setColors] = useState(initialColors ?? defaultColors());
|
|
255
255
|
const [slotIdx, setSlotIdx] = useState(0);
|
|
256
256
|
const [namingMode, setNamingMode] = useState(false);
|
|
@@ -296,6 +296,14 @@ function HorseCreator({ onSubmit, onCancel, initialColors, initialName, lockName
|
|
|
296
296
|
onSubmit(value.trim(), colors);
|
|
297
297
|
};
|
|
298
298
|
return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
|
|
299
|
+
lockName && initialName && /* @__PURE__ */ jsxs(Box2, { marginBottom: 1, children: [
|
|
300
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: initialName }),
|
|
301
|
+
typeof initialLevel === "number" && /* @__PURE__ */ jsxs(Text2, { color: "cyan", children: [
|
|
302
|
+
" [Lvl. ",
|
|
303
|
+
initialLevel,
|
|
304
|
+
"]"
|
|
305
|
+
] })
|
|
306
|
+
] }),
|
|
299
307
|
/* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(HorseSprite, { sprite: MAIN_SPRITE, colors }) }),
|
|
300
308
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: SLOTS.map((s, i) => /* @__PURE__ */ jsxs(Text2, { children: [
|
|
301
309
|
i === slotIdx ? "\u25BA" : " ",
|
|
@@ -318,9 +326,72 @@ function HorseCreator({ onSubmit, onCancel, initialColors, initialName, lockName
|
|
|
318
326
|
] });
|
|
319
327
|
}
|
|
320
328
|
|
|
321
|
-
// src/
|
|
322
|
-
|
|
323
|
-
|
|
329
|
+
// src/config.ts
|
|
330
|
+
var DEFAULT_API_BASE = "https://token-derby.mauricode.co.uk/api";
|
|
331
|
+
function apiBase() {
|
|
332
|
+
return process.env.TOKEN_DERBY_API_BASE ?? DEFAULT_API_BASE;
|
|
333
|
+
}
|
|
334
|
+
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
335
|
+
var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
336
|
+
|
|
337
|
+
// src/version.ts
|
|
338
|
+
import { createRequire } from "module";
|
|
339
|
+
function readVersion() {
|
|
340
|
+
if ("2.0.0".length > 0) {
|
|
341
|
+
return "2.0.0";
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const req = createRequire(import.meta.url);
|
|
345
|
+
const pkg = req("../package.json");
|
|
346
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
return "0.0.0-dev";
|
|
350
|
+
}
|
|
351
|
+
var CLI_VERSION = readVersion();
|
|
352
|
+
|
|
353
|
+
// ../shared/dist/constants.js
|
|
354
|
+
var CLI_VERSION_HEADER = "x-cli-version";
|
|
355
|
+
var USER_ID_HEADER = "x-user-id";
|
|
356
|
+
var USER_TOKEN_HEADER = "x-user-token";
|
|
357
|
+
var USER_NAME_MAX_LENGTH = 40;
|
|
358
|
+
var ORG_NAME_MAX_LENGTH = 12;
|
|
359
|
+
var ORG_NAME_PATTERN = /^[A-Za-z0-9]{1,12}$/;
|
|
360
|
+
|
|
361
|
+
// ../shared/dist/levels.js
|
|
362
|
+
var MAX_LEVEL = 30;
|
|
363
|
+
function xpForLevel(n) {
|
|
364
|
+
return 2.5 * n ** 3 + 20 * n ** 2 + 50 * n - 22.5;
|
|
365
|
+
}
|
|
366
|
+
function thresholdForLevel(level) {
|
|
367
|
+
if (level <= 1)
|
|
368
|
+
return 0;
|
|
369
|
+
return Math.round(xpForLevel(level - 1));
|
|
370
|
+
}
|
|
371
|
+
var XP_THRESHOLDS = Array.from({ length: MAX_LEVEL }, (_, i) => thresholdForLevel(i + 1));
|
|
372
|
+
function levelFromXp(xp) {
|
|
373
|
+
const v = Math.max(0, Math.floor(xp));
|
|
374
|
+
let level = 1;
|
|
375
|
+
while (level < MAX_LEVEL && v >= thresholdForLevel(level + 1)) {
|
|
376
|
+
level++;
|
|
377
|
+
}
|
|
378
|
+
return level;
|
|
379
|
+
}
|
|
380
|
+
function levelInfo(xp) {
|
|
381
|
+
const v = Math.max(0, Math.floor(xp));
|
|
382
|
+
const level = levelFromXp(v);
|
|
383
|
+
const level_start_xp = thresholdForLevel(level);
|
|
384
|
+
const isMax = level >= MAX_LEVEL;
|
|
385
|
+
const next_level_xp = isMax ? null : thresholdForLevel(level + 1);
|
|
386
|
+
const xp_into_level = v - level_start_xp;
|
|
387
|
+
const xp_for_level = isMax ? null : next_level_xp - level_start_xp;
|
|
388
|
+
const progress = isMax ? 1 : Math.min(1, xp_into_level / Math.max(1, xp_for_level));
|
|
389
|
+
return { level, xp: v, level_start_xp, next_level_xp, xp_into_level, xp_for_level, progress };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/identity/identity.ts
|
|
393
|
+
import { promises as fs } from "fs";
|
|
394
|
+
import * as path2 from "path";
|
|
324
395
|
|
|
325
396
|
// src/paths.ts
|
|
326
397
|
import * as os from "os";
|
|
@@ -328,9 +399,6 @@ import * as path from "path";
|
|
|
328
399
|
function homeDir() {
|
|
329
400
|
return process.env.TOKEN_DERBY_HOME ?? path.join(os.homedir(), ".token-derby");
|
|
330
401
|
}
|
|
331
|
-
function stableFile() {
|
|
332
|
-
return path.join(homeDir(), "stable.json");
|
|
333
|
-
}
|
|
334
402
|
function identityFile() {
|
|
335
403
|
return path.join(homeDir(), "identity.json");
|
|
336
404
|
}
|
|
@@ -344,78 +412,185 @@ function claudeProjectsDir() {
|
|
|
344
412
|
return process.env.TOKEN_DERBY_CLAUDE_DIR ?? path.join(os.homedir(), ".claude", "projects");
|
|
345
413
|
}
|
|
346
414
|
|
|
347
|
-
// src/
|
|
348
|
-
function
|
|
349
|
-
return randomUUID();
|
|
350
|
-
}
|
|
351
|
-
async function loadStable() {
|
|
415
|
+
// src/identity/identity.ts
|
|
416
|
+
async function loadIdentity() {
|
|
352
417
|
try {
|
|
353
|
-
const raw = await fs.readFile(
|
|
418
|
+
const raw = await fs.readFile(identityFile(), "utf8");
|
|
354
419
|
const parsed = JSON.parse(raw);
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (typeof h.stable_horse_id === "string" && h.stable_horse_id.length > 0) {
|
|
360
|
-
return h;
|
|
361
|
-
}
|
|
362
|
-
mutated = true;
|
|
363
|
-
return { ...h, stable_horse_id: newStableHorseId() };
|
|
364
|
-
});
|
|
365
|
-
const result = { horses };
|
|
366
|
-
if (mutated) await saveStable(result);
|
|
367
|
-
return result;
|
|
420
|
+
if (typeof parsed.user_id === "string" && typeof parsed.display_name === "string" && typeof parsed.secret_token === "string" && typeof parsed.created_at === "string") {
|
|
421
|
+
return parsed;
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
368
424
|
} catch (e) {
|
|
369
|
-
if (e?.code === "ENOENT") return
|
|
370
|
-
|
|
371
|
-
throw e;
|
|
425
|
+
if (e?.code === "ENOENT") return null;
|
|
426
|
+
return null;
|
|
372
427
|
}
|
|
373
428
|
}
|
|
374
|
-
async function
|
|
429
|
+
async function saveIdentity(identity) {
|
|
375
430
|
await fs.mkdir(homeDir(), { recursive: true });
|
|
376
|
-
await fs.writeFile(
|
|
431
|
+
await fs.writeFile(identityFile(), JSON.stringify(identity, null, 2) + "\n", "utf8");
|
|
377
432
|
}
|
|
378
|
-
async function
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
433
|
+
async function deleteIdentity() {
|
|
434
|
+
try {
|
|
435
|
+
await fs.unlink(identityFile());
|
|
436
|
+
} catch (e) {
|
|
437
|
+
if (e?.code !== "ENOENT") throw e;
|
|
438
|
+
}
|
|
384
439
|
}
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
440
|
+
function validateDisplayName(name) {
|
|
441
|
+
const trimmed = name.trim();
|
|
442
|
+
if (trimmed.length < 1) return { ok: false, error: "Name cannot be empty." };
|
|
443
|
+
if (trimmed.length > USER_NAME_MAX_LENGTH) {
|
|
444
|
+
return { ok: false, error: `Name must be ${USER_NAME_MAX_LENGTH} characters or fewer.` };
|
|
445
|
+
}
|
|
446
|
+
return { ok: true, name: trimmed };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/api/client.ts
|
|
450
|
+
var ApiError = class extends Error {
|
|
451
|
+
constructor(code, message, status) {
|
|
452
|
+
super(message);
|
|
453
|
+
this.code = code;
|
|
454
|
+
this.status = status;
|
|
455
|
+
this.name = "ApiError";
|
|
456
|
+
}
|
|
457
|
+
code;
|
|
458
|
+
status;
|
|
459
|
+
};
|
|
460
|
+
var identityCache = null;
|
|
461
|
+
function getIdentity() {
|
|
462
|
+
if (!identityCache) identityCache = loadIdentity();
|
|
463
|
+
return identityCache;
|
|
389
464
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
465
|
+
function _resetIdentityCacheForTests() {
|
|
466
|
+
identityCache = null;
|
|
467
|
+
}
|
|
468
|
+
async function request(method, path5, body, horseAuthToken, fetchImpl = fetch) {
|
|
469
|
+
const url = path5.startsWith("http") ? path5 : `${apiBase()}${path5}`;
|
|
470
|
+
const headers = {};
|
|
471
|
+
headers[CLI_VERSION_HEADER] = CLI_VERSION;
|
|
472
|
+
headers["user-agent"] = `token-derby/${CLI_VERSION}`;
|
|
473
|
+
const identity = await getIdentity();
|
|
474
|
+
if (identity) {
|
|
475
|
+
headers[USER_ID_HEADER] = identity.user_id;
|
|
476
|
+
headers[USER_TOKEN_HEADER] = identity.secret_token;
|
|
477
|
+
}
|
|
478
|
+
if (horseAuthToken) headers["authorization"] = `Bearer ${horseAuthToken}`;
|
|
479
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
480
|
+
let res;
|
|
481
|
+
try {
|
|
482
|
+
res = await fetchImpl(url, {
|
|
483
|
+
method,
|
|
484
|
+
headers,
|
|
485
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
486
|
+
});
|
|
487
|
+
} catch (e) {
|
|
488
|
+
throw new ApiError("NETWORK_ERROR", e?.message ?? "fetch failed", 0);
|
|
489
|
+
}
|
|
490
|
+
const text = await res.text();
|
|
491
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
492
|
+
let parsed = null;
|
|
493
|
+
if (contentType.includes("application/json") && text.length > 0) {
|
|
494
|
+
try {
|
|
495
|
+
parsed = JSON.parse(text);
|
|
496
|
+
} catch {
|
|
497
|
+
parsed = null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!res.ok) {
|
|
501
|
+
if (parsed && typeof parsed.code === "string") {
|
|
502
|
+
throw new ApiError(parsed.code, parsed.message ?? "API error", res.status);
|
|
503
|
+
}
|
|
504
|
+
throw new ApiError("NETWORK_ERROR", `HTTP ${res.status}`, res.status);
|
|
505
|
+
}
|
|
506
|
+
return parsed;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/api/endpoints.ts
|
|
510
|
+
function createRace(body) {
|
|
511
|
+
return request("POST", "/races", body, void 0);
|
|
512
|
+
}
|
|
513
|
+
function getRace(joinCode) {
|
|
514
|
+
return request("GET", `/races/${encodeURIComponent(joinCode)}`, void 0, void 0);
|
|
515
|
+
}
|
|
516
|
+
function joinRace(joinCode, body) {
|
|
517
|
+
return request("POST", `/races/${encodeURIComponent(joinCode)}/join`, body, void 0);
|
|
518
|
+
}
|
|
519
|
+
function heartbeat(joinCode, horseId, token, body) {
|
|
520
|
+
return request(
|
|
521
|
+
"POST",
|
|
522
|
+
`/races/${encodeURIComponent(joinCode)}/horses/${encodeURIComponent(horseId)}/heartbeat`,
|
|
523
|
+
body,
|
|
524
|
+
token
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
function endRace(adminCode) {
|
|
528
|
+
return request("DELETE", `/races/admin/${encodeURIComponent(adminCode)}`, void 0, void 0);
|
|
529
|
+
}
|
|
530
|
+
function createOrganisation(body) {
|
|
531
|
+
return request("POST", "/organisations", body, void 0);
|
|
532
|
+
}
|
|
533
|
+
function joinOrganisation(body) {
|
|
534
|
+
return request("POST", "/organisations/join", body, void 0);
|
|
535
|
+
}
|
|
536
|
+
function listOrganisations() {
|
|
537
|
+
return request("GET", "/organisations", void 0, void 0);
|
|
538
|
+
}
|
|
539
|
+
function getOrganisation(name) {
|
|
540
|
+
return request("GET", `/organisations/${encodeURIComponent(name)}`, void 0, void 0);
|
|
541
|
+
}
|
|
542
|
+
function initJockey(body) {
|
|
543
|
+
return request("POST", "/jockey/init", body, void 0);
|
|
544
|
+
}
|
|
545
|
+
function updateJockey(body) {
|
|
546
|
+
return request("PUT", "/jockey/me", body, void 0);
|
|
547
|
+
}
|
|
548
|
+
function listStable() {
|
|
549
|
+
return request("GET", "/jockey/me/horses", void 0, void 0);
|
|
550
|
+
}
|
|
551
|
+
function createStableHorse(body) {
|
|
552
|
+
return request("POST", "/jockey/me/horses", body, void 0);
|
|
553
|
+
}
|
|
554
|
+
function updateStableHorse(stableHorseId, body) {
|
|
555
|
+
return request(
|
|
556
|
+
"PUT",
|
|
557
|
+
`/jockey/me/horses/${encodeURIComponent(stableHorseId)}`,
|
|
558
|
+
body,
|
|
559
|
+
void 0
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function deleteStableHorse(stableHorseId) {
|
|
563
|
+
return request(
|
|
564
|
+
"DELETE",
|
|
565
|
+
`/jockey/me/horses/${encodeURIComponent(stableHorseId)}`,
|
|
566
|
+
void 0,
|
|
567
|
+
void 0
|
|
568
|
+
);
|
|
392
569
|
}
|
|
393
570
|
|
|
394
571
|
// src/commands/stable-create.ts
|
|
395
|
-
import * as readline from "readline/promises";
|
|
396
|
-
import { stdin, stdout } from "process";
|
|
397
572
|
async function stableCreateCommand() {
|
|
398
573
|
let exitCode = 0;
|
|
399
574
|
const app = render(
|
|
400
575
|
React2.createElement(HorseCreator, {
|
|
401
576
|
onSubmit: async (name, colors) => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (existing) {
|
|
577
|
+
try {
|
|
578
|
+
await createStableHorse({ name, colors });
|
|
405
579
|
app.unmount();
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
580
|
+
console.log(`\u2713 Saved "${name}" to your stable.`);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
app.unmount();
|
|
583
|
+
if (e instanceof ApiError) {
|
|
584
|
+
if (e.code === "STABLE_HORSE_NAME_TAKEN") {
|
|
585
|
+
console.error(`A horse named "${name}" already exists. Pick a different name or use \`token-derby stable edit ${name}\` to modify it.`);
|
|
586
|
+
} else {
|
|
587
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
588
|
+
}
|
|
411
589
|
exitCode = 1;
|
|
412
590
|
return;
|
|
413
591
|
}
|
|
592
|
+
throw e;
|
|
414
593
|
}
|
|
415
|
-
const stable_horse_id = existing?.stable_horse_id ?? newStableHorseId();
|
|
416
|
-
await upsertHorse({ stable_horse_id, name, colors, created_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
417
|
-
app.unmount();
|
|
418
|
-
console.log(`\u2713 Saved "${name}" to your stable.`);
|
|
419
594
|
},
|
|
420
595
|
onCancel: () => {
|
|
421
596
|
app.unmount();
|
|
@@ -433,13 +608,23 @@ import React3 from "react";
|
|
|
433
608
|
import { render as render2, Box as Box3, Text as Text3 } from "ink";
|
|
434
609
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
435
610
|
async function stableListCommand() {
|
|
436
|
-
|
|
437
|
-
|
|
611
|
+
let horses;
|
|
612
|
+
try {
|
|
613
|
+
const resp = await listStable();
|
|
614
|
+
horses = resp.horses;
|
|
615
|
+
} catch (e) {
|
|
616
|
+
if (e instanceof ApiError) {
|
|
617
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
618
|
+
return 1;
|
|
619
|
+
}
|
|
620
|
+
throw e;
|
|
621
|
+
}
|
|
622
|
+
if (horses.length === 0) {
|
|
438
623
|
console.log("Your stable is empty. Run `token-derby stable create` to add a horse.");
|
|
439
624
|
return 0;
|
|
440
625
|
}
|
|
441
626
|
const app = render2(
|
|
442
|
-
React3.createElement(StableList, { horses
|
|
627
|
+
React3.createElement(StableList, { horses })
|
|
443
628
|
);
|
|
444
629
|
await app.waitUntilExit();
|
|
445
630
|
return 0;
|
|
@@ -458,269 +643,127 @@ function StableList({ horses }) {
|
|
|
458
643
|
/* @__PURE__ */ jsx3(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors }),
|
|
459
644
|
/* @__PURE__ */ jsxs2(Text3, { children: [
|
|
460
645
|
" ",
|
|
461
|
-
h.name
|
|
646
|
+
h.name,
|
|
647
|
+
" ",
|
|
648
|
+
/* @__PURE__ */ jsxs2(Text3, { color: "cyan", children: [
|
|
649
|
+
"[Lvl. ",
|
|
650
|
+
levelFromXp(h.xp),
|
|
651
|
+
"]"
|
|
652
|
+
] })
|
|
462
653
|
] })
|
|
463
|
-
] }, h.
|
|
654
|
+
] }, h.stable_horse_id))
|
|
464
655
|
] });
|
|
465
656
|
}
|
|
466
657
|
|
|
467
658
|
// src/commands/stable-delete.ts
|
|
468
|
-
import * as
|
|
469
|
-
import { stdin
|
|
470
|
-
|
|
471
|
-
// src/stable/active-race.ts
|
|
472
|
-
import * as fs2 from "fs/promises";
|
|
473
|
-
import * as path2 from "path";
|
|
474
|
-
async function loadActiveRace(joinCode) {
|
|
475
|
-
try {
|
|
476
|
-
const raw = await fs2.readFile(activeRaceFile(joinCode), "utf8");
|
|
477
|
-
return JSON.parse(raw);
|
|
478
|
-
} catch (e) {
|
|
479
|
-
if (e?.code === "ENOENT") return null;
|
|
480
|
-
throw e;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
async function saveActiveRace(active) {
|
|
484
|
-
await fs2.mkdir(activeRacesDir(), { recursive: true });
|
|
485
|
-
await fs2.writeFile(
|
|
486
|
-
activeRaceFile(active.join_code),
|
|
487
|
-
JSON.stringify(active, null, 2) + "\n",
|
|
488
|
-
"utf8"
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
async function listActiveRaces() {
|
|
492
|
-
try {
|
|
493
|
-
const entries = await fs2.readdir(activeRacesDir());
|
|
494
|
-
return entries.filter((f) => f.endsWith(".json")).map((f) => path2.basename(f, ".json"));
|
|
495
|
-
} catch (e) {
|
|
496
|
-
if (e?.code === "ENOENT") return [];
|
|
497
|
-
throw e;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// src/commands/stable-delete.ts
|
|
659
|
+
import * as readline from "readline/promises";
|
|
660
|
+
import { stdin, stdout } from "process";
|
|
502
661
|
async function stableDeleteCommand(name) {
|
|
503
662
|
if (!name) {
|
|
504
|
-
console.error("Usage: token-derby stable delete <name>");
|
|
505
|
-
return 2;
|
|
506
|
-
}
|
|
507
|
-
const stable = await loadStable();
|
|
508
|
-
const horse = findHorse(stable, name);
|
|
509
|
-
if (!horse) {
|
|
510
|
-
console.error(`No horse named "${name}" in your stable.`);
|
|
511
|
-
return 1;
|
|
512
|
-
}
|
|
513
|
-
const codes = await listActiveRaces();
|
|
514
|
-
for (const code of codes) {
|
|
515
|
-
const active = await loadActiveRace(code);
|
|
516
|
-
if (active?.horse_name === name) {
|
|
517
|
-
console.error(`"${name}" is currently running in race ${code}. Close that terminal first.`);
|
|
518
|
-
return 1;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
const rl = readline2.createInterface({ input: stdin2, output: stdout2 });
|
|
522
|
-
const answer = (await rl.question(`Delete "${name}" from your stable? [y/N] `)).trim().toLowerCase();
|
|
523
|
-
rl.close();
|
|
524
|
-
if (answer !== "y" && answer !== "yes") {
|
|
525
|
-
console.log("Cancelled.");
|
|
526
|
-
return 1;
|
|
527
|
-
}
|
|
528
|
-
await removeHorse(name);
|
|
529
|
-
console.log(`\u2713 Deleted "${name}".`);
|
|
530
|
-
return 0;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// src/commands/stable-edit.ts
|
|
534
|
-
import React4 from "react";
|
|
535
|
-
import { render as render3 } from "ink";
|
|
536
|
-
async function stableEditCommand(name) {
|
|
537
|
-
if (!name) {
|
|
538
|
-
console.error("Usage: token-derby stable edit <name>");
|
|
539
|
-
return 2;
|
|
540
|
-
}
|
|
541
|
-
const stable = await loadStable();
|
|
542
|
-
const existing = findHorse(stable, name);
|
|
543
|
-
if (!existing) {
|
|
544
|
-
console.error(`No horse named "${name}" in your stable.`);
|
|
545
|
-
return 1;
|
|
546
|
-
}
|
|
547
|
-
let exitCode = 0;
|
|
548
|
-
const app = render3(
|
|
549
|
-
React4.createElement(HorseCreator, {
|
|
550
|
-
initialColors: existing.colors,
|
|
551
|
-
initialName: existing.name,
|
|
552
|
-
lockName: true,
|
|
553
|
-
onSubmit: async (_name, colors) => {
|
|
554
|
-
await upsertHorse({
|
|
555
|
-
stable_horse_id: existing.stable_horse_id,
|
|
556
|
-
name: existing.name,
|
|
557
|
-
colors,
|
|
558
|
-
created_at: existing.created_at
|
|
559
|
-
});
|
|
560
|
-
app.unmount();
|
|
561
|
-
console.log(`\u2713 Updated "${existing.name}".`);
|
|
562
|
-
},
|
|
563
|
-
onCancel: () => {
|
|
564
|
-
app.unmount();
|
|
565
|
-
console.log("Cancelled.");
|
|
566
|
-
exitCode = 1;
|
|
567
|
-
}
|
|
568
|
-
})
|
|
569
|
-
);
|
|
570
|
-
await app.waitUntilExit();
|
|
571
|
-
return exitCode;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// src/commands/create.ts
|
|
575
|
-
import * as readline3 from "readline/promises";
|
|
576
|
-
import { stdin as stdin3, stdout as stdout3 } from "process";
|
|
577
|
-
|
|
578
|
-
// src/config.ts
|
|
579
|
-
var DEFAULT_API_BASE = "https://token-derby.mauricode.co.uk/api";
|
|
580
|
-
function apiBase() {
|
|
581
|
-
return process.env.TOKEN_DERBY_API_BASE ?? DEFAULT_API_BASE;
|
|
582
|
-
}
|
|
583
|
-
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
584
|
-
var POLL_INTERVAL_MS = 3e3;
|
|
585
|
-
var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
586
|
-
|
|
587
|
-
// src/version.ts
|
|
588
|
-
import { createRequire } from "module";
|
|
589
|
-
function readVersion() {
|
|
590
|
-
if ("1.1.0".length > 0) {
|
|
591
|
-
return "1.1.0";
|
|
592
|
-
}
|
|
593
|
-
try {
|
|
594
|
-
const req = createRequire(import.meta.url);
|
|
595
|
-
const pkg = req("../package.json");
|
|
596
|
-
if (typeof pkg.version === "string") return pkg.version;
|
|
597
|
-
} catch {
|
|
598
|
-
}
|
|
599
|
-
return "0.0.0-dev";
|
|
600
|
-
}
|
|
601
|
-
var CLI_VERSION = readVersion();
|
|
602
|
-
|
|
603
|
-
// ../shared/dist/constants.js
|
|
604
|
-
var CLI_VERSION_HEADER = "x-cli-version";
|
|
605
|
-
var USER_ID_HEADER = "x-user-id";
|
|
606
|
-
var USER_NAME_HEADER = "x-user-name";
|
|
607
|
-
var USER_NAME_MAX_LENGTH = 40;
|
|
608
|
-
|
|
609
|
-
// src/identity/identity.ts
|
|
610
|
-
import { promises as fs3 } from "fs";
|
|
611
|
-
import * as path3 from "path";
|
|
612
|
-
import * as crypto from "crypto";
|
|
613
|
-
async function loadIdentity() {
|
|
614
|
-
try {
|
|
615
|
-
const raw = await fs3.readFile(identityFile(), "utf8");
|
|
616
|
-
const parsed = JSON.parse(raw);
|
|
617
|
-
if (typeof parsed.user_id === "string" && typeof parsed.display_name === "string" && typeof parsed.created_at === "string") {
|
|
618
|
-
return parsed;
|
|
619
|
-
}
|
|
620
|
-
return null;
|
|
621
|
-
} catch (e) {
|
|
622
|
-
if (e?.code === "ENOENT") return null;
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
async function saveIdentity(identity) {
|
|
627
|
-
await fs3.mkdir(homeDir(), { recursive: true });
|
|
628
|
-
await fs3.writeFile(identityFile(), JSON.stringify(identity, null, 2) + "\n", "utf8");
|
|
629
|
-
}
|
|
630
|
-
function generateUserId() {
|
|
631
|
-
return crypto.randomUUID();
|
|
632
|
-
}
|
|
633
|
-
function validateDisplayName(name) {
|
|
634
|
-
const trimmed = name.trim();
|
|
635
|
-
if (trimmed.length < 1) return { ok: false, error: "Name cannot be empty." };
|
|
636
|
-
if (trimmed.length > USER_NAME_MAX_LENGTH) {
|
|
637
|
-
return { ok: false, error: `Name must be ${USER_NAME_MAX_LENGTH} characters or fewer.` };
|
|
638
|
-
}
|
|
639
|
-
return { ok: true, name: trimmed };
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// src/api/client.ts
|
|
643
|
-
var ApiError = class extends Error {
|
|
644
|
-
constructor(code, message, status) {
|
|
645
|
-
super(message);
|
|
646
|
-
this.code = code;
|
|
647
|
-
this.status = status;
|
|
648
|
-
this.name = "ApiError";
|
|
649
|
-
}
|
|
650
|
-
code;
|
|
651
|
-
status;
|
|
652
|
-
};
|
|
653
|
-
var identityCache = null;
|
|
654
|
-
function getIdentity() {
|
|
655
|
-
if (!identityCache) identityCache = loadIdentity();
|
|
656
|
-
return identityCache;
|
|
657
|
-
}
|
|
658
|
-
async function request(method, path5, body, authToken, fetchImpl = fetch) {
|
|
659
|
-
const url = path5.startsWith("http") ? path5 : `${apiBase()}${path5}`;
|
|
660
|
-
const headers = {};
|
|
661
|
-
headers[CLI_VERSION_HEADER] = CLI_VERSION;
|
|
662
|
-
const identity = await getIdentity();
|
|
663
|
-
if (identity) {
|
|
664
|
-
headers[USER_ID_HEADER] = identity.user_id;
|
|
665
|
-
headers[USER_NAME_HEADER] = identity.display_name;
|
|
666
|
-
}
|
|
667
|
-
if (authToken) headers["authorization"] = `Bearer ${authToken}`;
|
|
668
|
-
if (body !== void 0) headers["content-type"] = "application/json";
|
|
669
|
-
let res;
|
|
670
|
-
try {
|
|
671
|
-
res = await fetchImpl(url, {
|
|
672
|
-
method,
|
|
673
|
-
headers,
|
|
674
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
675
|
-
});
|
|
676
|
-
} catch (e) {
|
|
677
|
-
throw new ApiError("NETWORK_ERROR", e?.message ?? "fetch failed", 0);
|
|
663
|
+
console.error("Usage: token-derby stable delete <name>");
|
|
664
|
+
return 2;
|
|
678
665
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
parsed = null;
|
|
666
|
+
let horses;
|
|
667
|
+
try {
|
|
668
|
+
horses = (await listStable()).horses;
|
|
669
|
+
} catch (e) {
|
|
670
|
+
if (e instanceof ApiError) {
|
|
671
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
672
|
+
return 1;
|
|
687
673
|
}
|
|
674
|
+
throw e;
|
|
688
675
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
676
|
+
const horse = horses.find((h) => h.name === name);
|
|
677
|
+
if (!horse) {
|
|
678
|
+
console.error(`No horse named "${name}" in your stable.`);
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
682
|
+
const answer = (await rl.question(`Delete "${name}" from your stable? [y/N] `)).trim().toLowerCase();
|
|
683
|
+
rl.close();
|
|
684
|
+
if (answer !== "y" && answer !== "yes") {
|
|
685
|
+
console.log("Cancelled.");
|
|
686
|
+
return 1;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
await deleteStableHorse(horse.stable_horse_id);
|
|
690
|
+
console.log(`\u2713 Deleted "${name}".`);
|
|
691
|
+
return 0;
|
|
692
|
+
} catch (e) {
|
|
693
|
+
if (e instanceof ApiError) {
|
|
694
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
695
|
+
return 1;
|
|
692
696
|
}
|
|
693
|
-
throw
|
|
697
|
+
throw e;
|
|
694
698
|
}
|
|
695
|
-
return parsed;
|
|
696
699
|
}
|
|
697
700
|
|
|
698
|
-
// src/
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
"
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
701
|
+
// src/commands/stable-edit.ts
|
|
702
|
+
import React4 from "react";
|
|
703
|
+
import { render as render3 } from "ink";
|
|
704
|
+
async function stableEditCommand(name) {
|
|
705
|
+
if (!name) {
|
|
706
|
+
console.error("Usage: token-derby stable edit <name>");
|
|
707
|
+
return 2;
|
|
708
|
+
}
|
|
709
|
+
const horses = await fetchStable();
|
|
710
|
+
if (!horses) return 1;
|
|
711
|
+
const existing = horses.find((h) => h.name === name);
|
|
712
|
+
if (!existing) {
|
|
713
|
+
console.error(`No horse named "${name}" in your stable.`);
|
|
714
|
+
return 1;
|
|
715
|
+
}
|
|
716
|
+
let exitCode = 0;
|
|
717
|
+
const app = render3(
|
|
718
|
+
React4.createElement(HorseCreator, {
|
|
719
|
+
initialColors: existing.colors,
|
|
720
|
+
initialName: existing.name,
|
|
721
|
+
lockName: true,
|
|
722
|
+
initialLevel: levelFromXp(existing.xp),
|
|
723
|
+
onSubmit: async (_name, colors) => {
|
|
724
|
+
try {
|
|
725
|
+
await updateStableHorse(existing.stable_horse_id, { colors });
|
|
726
|
+
app.unmount();
|
|
727
|
+
console.log(`\u2713 Updated "${existing.name}".`);
|
|
728
|
+
} catch (e) {
|
|
729
|
+
app.unmount();
|
|
730
|
+
if (e instanceof ApiError) {
|
|
731
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
732
|
+
exitCode = 1;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
throw e;
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
onCancel: () => {
|
|
739
|
+
app.unmount();
|
|
740
|
+
console.log("Cancelled.");
|
|
741
|
+
exitCode = 1;
|
|
742
|
+
}
|
|
743
|
+
})
|
|
714
744
|
);
|
|
745
|
+
await app.waitUntilExit();
|
|
746
|
+
return exitCode;
|
|
715
747
|
}
|
|
716
|
-
function
|
|
717
|
-
|
|
748
|
+
async function fetchStable() {
|
|
749
|
+
try {
|
|
750
|
+
const resp = await listStable();
|
|
751
|
+
return resp.horses;
|
|
752
|
+
} catch (e) {
|
|
753
|
+
if (e instanceof ApiError) {
|
|
754
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
throw e;
|
|
758
|
+
}
|
|
718
759
|
}
|
|
719
760
|
|
|
720
761
|
// src/commands/create.ts
|
|
762
|
+
import * as readline2 from "readline/promises";
|
|
763
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
721
764
|
var DEFAULT_TZ = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
722
|
-
async function createRaceCommand() {
|
|
723
|
-
const rl =
|
|
765
|
+
async function createRaceCommand(organisationName) {
|
|
766
|
+
const rl = readline2.createInterface({ input: stdin2, output: stdout2 });
|
|
724
767
|
try {
|
|
725
768
|
const name = (await rl.question("Race name: ")).trim();
|
|
726
769
|
if (!name) {
|
|
@@ -747,12 +790,22 @@ async function createRaceCommand() {
|
|
|
747
790
|
console.error("Max participants must be a positive number.");
|
|
748
791
|
return 1;
|
|
749
792
|
}
|
|
793
|
+
let org = organisationName;
|
|
794
|
+
if (org === void 0) {
|
|
795
|
+
const raw = (await rl.question("Organisation (blank for none): ")).trim();
|
|
796
|
+
if (raw) org = raw;
|
|
797
|
+
}
|
|
798
|
+
if (org !== void 0 && !ORG_NAME_PATTERN.test(org)) {
|
|
799
|
+
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
750
802
|
const resp = await createRace({
|
|
751
803
|
name,
|
|
752
804
|
start_time: start,
|
|
753
805
|
end_time: end,
|
|
754
806
|
tz,
|
|
755
|
-
...max !== void 0 ? { max_participants: max } : {}
|
|
807
|
+
...max !== void 0 ? { max_participants: max } : {},
|
|
808
|
+
...org ? { organisation_name: org } : {}
|
|
756
809
|
});
|
|
757
810
|
console.log("");
|
|
758
811
|
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
@@ -762,6 +815,9 @@ async function createRaceCommand() {
|
|
|
762
815
|
console.log(` Admin code: ${resp.admin_code}`);
|
|
763
816
|
console.log(" \u26A0 Save the admin code \u2014 you need it to end the race early.");
|
|
764
817
|
console.log("");
|
|
818
|
+
if (org) {
|
|
819
|
+
console.log(` Restricted to organisation: ${org}`);
|
|
820
|
+
}
|
|
765
821
|
console.log(` Share with participants: token-derby join ${resp.join_code}`);
|
|
766
822
|
return 0;
|
|
767
823
|
} catch (e) {
|
|
@@ -821,17 +877,44 @@ function HorsePicker({ horses, onPick, onCancel }) {
|
|
|
821
877
|
/* @__PURE__ */ jsx4(Box4, { flexDirection: "row", children: /* @__PURE__ */ jsxs3(Text4, { children: [
|
|
822
878
|
i === idx ? "\u25BA" : " ",
|
|
823
879
|
" ",
|
|
824
|
-
h.name
|
|
880
|
+
h.name,
|
|
881
|
+
" ",
|
|
882
|
+
/* @__PURE__ */ jsxs3(Text4, { color: "cyan", children: [
|
|
883
|
+
"[Lvl. ",
|
|
884
|
+
levelFromXp(h.xp),
|
|
885
|
+
"]"
|
|
886
|
+
] })
|
|
825
887
|
] }) }),
|
|
826
888
|
/* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", children: [
|
|
827
889
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
828
890
|
/* @__PURE__ */ jsx4(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
|
|
829
891
|
] })
|
|
830
|
-
] }, h.
|
|
892
|
+
] }, h.stable_horse_id)),
|
|
831
893
|
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
832
894
|
] });
|
|
833
895
|
}
|
|
834
896
|
|
|
897
|
+
// src/stable/active-race.ts
|
|
898
|
+
import * as fs2 from "fs/promises";
|
|
899
|
+
import * as path3 from "path";
|
|
900
|
+
async function loadActiveRace(joinCode) {
|
|
901
|
+
try {
|
|
902
|
+
const raw = await fs2.readFile(activeRaceFile(joinCode), "utf8");
|
|
903
|
+
return JSON.parse(raw);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
if (e?.code === "ENOENT") return null;
|
|
906
|
+
throw e;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async function saveActiveRace(active) {
|
|
910
|
+
await fs2.mkdir(activeRacesDir(), { recursive: true });
|
|
911
|
+
await fs2.writeFile(
|
|
912
|
+
activeRaceFile(active.join_code),
|
|
913
|
+
JSON.stringify(active, null, 2) + "\n",
|
|
914
|
+
"utf8"
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
835
918
|
// src/runtime/run-race.tsx
|
|
836
919
|
import { useEffect, useRef, useState as useState3 } from "react";
|
|
837
920
|
import { Box as Box6, Text as Text6, useApp } from "ink";
|
|
@@ -848,6 +931,7 @@ function StatusScreen(props) {
|
|
|
848
931
|
const leader = race.horses[0];
|
|
849
932
|
const elapsedPct = elapsed(race);
|
|
850
933
|
const timeLeft = formatDuration(race.time_left_seconds);
|
|
934
|
+
const lvl = levelInfo(own?.xp ?? 0);
|
|
851
935
|
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
852
936
|
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
853
937
|
"\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
|
|
@@ -860,7 +944,13 @@ function StatusScreen(props) {
|
|
|
860
944
|
/* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
|
|
861
945
|
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
862
946
|
" ",
|
|
863
|
-
ownHorseName
|
|
947
|
+
ownHorseName,
|
|
948
|
+
" ",
|
|
949
|
+
/* @__PURE__ */ jsxs4(Text5, { color: "cyan", children: [
|
|
950
|
+
"[Lvl. ",
|
|
951
|
+
lvl.level,
|
|
952
|
+
"]"
|
|
953
|
+
] })
|
|
864
954
|
] }),
|
|
865
955
|
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
866
956
|
" ",
|
|
@@ -897,6 +987,10 @@ function StatusScreen(props) {
|
|
|
897
987
|
"Time left: ",
|
|
898
988
|
timeLeft
|
|
899
989
|
] }),
|
|
990
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
991
|
+
"XP: ",
|
|
992
|
+
lvl.next_level_xp === null ? `${lvl.xp} (max level) ${bar(1, 20)}` : `${lvl.xp_into_level}/${lvl.xp_for_level} \u2192 Lvl. ${lvl.level + 1} ${bar(lvl.progress, 20)}`
|
|
993
|
+
] }),
|
|
900
994
|
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
901
995
|
"Last heartbeat: ",
|
|
902
996
|
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
@@ -970,31 +1064,8 @@ function runHeartbeatLoop(opts) {
|
|
|
970
1064
|
schedule(0);
|
|
971
1065
|
}
|
|
972
1066
|
|
|
973
|
-
// src/runtime/poll-loop.ts
|
|
974
|
-
function runPollLoop(opts) {
|
|
975
|
-
let timer = null;
|
|
976
|
-
let stopped = false;
|
|
977
|
-
const stop = () => {
|
|
978
|
-
stopped = true;
|
|
979
|
-
if (timer) clearTimeout(timer);
|
|
980
|
-
timer = null;
|
|
981
|
-
};
|
|
982
|
-
opts.abortSignal.addEventListener("abort", stop, { once: true });
|
|
983
|
-
const tick = async () => {
|
|
984
|
-
if (stopped) return;
|
|
985
|
-
try {
|
|
986
|
-
const race = await opts.fetchRace();
|
|
987
|
-
if (!stopped) opts.onSnapshot(race);
|
|
988
|
-
} catch (err) {
|
|
989
|
-
if (!stopped) opts.onError(err);
|
|
990
|
-
}
|
|
991
|
-
if (!stopped) timer = setTimeout(tick, opts.intervalMs);
|
|
992
|
-
};
|
|
993
|
-
timer = setTimeout(tick, 0);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
1067
|
// src/tokens/transcripts.ts
|
|
997
|
-
import * as
|
|
1068
|
+
import * as fs3 from "fs/promises";
|
|
998
1069
|
import * as path4 from "path";
|
|
999
1070
|
async function sumTokens() {
|
|
1000
1071
|
const root = claudeProjectsDir();
|
|
@@ -1010,12 +1081,18 @@ async function sumTokens() {
|
|
|
1010
1081
|
}
|
|
1011
1082
|
async function sumOutputTokens() {
|
|
1012
1083
|
const { input, output } = await sumTokens();
|
|
1013
|
-
return input + output;
|
|
1084
|
+
return countInputTokens() ? input + output : output;
|
|
1085
|
+
}
|
|
1086
|
+
function countInputTokens() {
|
|
1087
|
+
const v = process.env.TOKEN_DERBY_COUNT_INPUT_TOKENS;
|
|
1088
|
+
if (!v) return false;
|
|
1089
|
+
const s = v.toLowerCase();
|
|
1090
|
+
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
1014
1091
|
}
|
|
1015
1092
|
async function listJsonlFiles(root) {
|
|
1016
1093
|
let projects;
|
|
1017
1094
|
try {
|
|
1018
|
-
projects = await
|
|
1095
|
+
projects = await fs3.readdir(root);
|
|
1019
1096
|
} catch (e) {
|
|
1020
1097
|
if (e?.code === "ENOENT") return [];
|
|
1021
1098
|
throw e;
|
|
@@ -1025,12 +1102,12 @@ async function listJsonlFiles(root) {
|
|
|
1025
1102
|
const projectDir = path4.join(root, project);
|
|
1026
1103
|
let stat2;
|
|
1027
1104
|
try {
|
|
1028
|
-
stat2 = await
|
|
1105
|
+
stat2 = await fs3.stat(projectDir);
|
|
1029
1106
|
} catch {
|
|
1030
1107
|
continue;
|
|
1031
1108
|
}
|
|
1032
1109
|
if (!stat2.isDirectory()) continue;
|
|
1033
|
-
const entries = await
|
|
1110
|
+
const entries = await fs3.readdir(projectDir);
|
|
1034
1111
|
for (const entry of entries) {
|
|
1035
1112
|
if (entry.endsWith(".jsonl")) out.push(path4.join(projectDir, entry));
|
|
1036
1113
|
}
|
|
@@ -1043,7 +1120,7 @@ function addNum(value) {
|
|
|
1043
1120
|
async function sumFile(file) {
|
|
1044
1121
|
let raw;
|
|
1045
1122
|
try {
|
|
1046
|
-
raw = await
|
|
1123
|
+
raw = await fs3.readFile(file, "utf8");
|
|
1047
1124
|
} catch {
|
|
1048
1125
|
return { input: 0, output: 0 };
|
|
1049
1126
|
}
|
|
@@ -1096,14 +1173,6 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1096
1173
|
}
|
|
1097
1174
|
}, [race?.status]);
|
|
1098
1175
|
useEffect(() => {
|
|
1099
|
-
runPollLoop({
|
|
1100
|
-
fetchRace: () => getRace(active.join_code),
|
|
1101
|
-
intervalMs: POLL_INTERVAL_MS,
|
|
1102
|
-
onSnapshot: (r) => setRace(r),
|
|
1103
|
-
onError: () => {
|
|
1104
|
-
},
|
|
1105
|
-
abortSignal: ctrl.current.signal
|
|
1106
|
-
});
|
|
1107
1176
|
runHeartbeatLoop({
|
|
1108
1177
|
sendHeartbeat: async (currentTokens) => {
|
|
1109
1178
|
const resp = await heartbeat(
|
|
@@ -1129,6 +1198,7 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1129
1198
|
onSuccess: (resp) => {
|
|
1130
1199
|
setLastHbAt(/* @__PURE__ */ new Date());
|
|
1131
1200
|
setLastHbOk(true);
|
|
1201
|
+
setRace(raceViewFrom(resp));
|
|
1132
1202
|
if (resp.race_status === "finished") exit();
|
|
1133
1203
|
},
|
|
1134
1204
|
onError: (err) => {
|
|
@@ -1179,6 +1249,15 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1179
1249
|
}
|
|
1180
1250
|
);
|
|
1181
1251
|
}
|
|
1252
|
+
function raceViewFrom(resp) {
|
|
1253
|
+
return {
|
|
1254
|
+
...resp.race,
|
|
1255
|
+
status: resp.race_status,
|
|
1256
|
+
horses: resp.horses,
|
|
1257
|
+
server_time: resp.server_time,
|
|
1258
|
+
time_left_seconds: resp.time_left_seconds
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1182
1261
|
async function buildInitialState(args) {
|
|
1183
1262
|
const runningTotal = await sumOutputTokens();
|
|
1184
1263
|
if (args.rejoin) {
|
|
@@ -1231,12 +1310,21 @@ async function joinCommand(joinCode) {
|
|
|
1231
1310
|
chosenColors = ownHorse.colors;
|
|
1232
1311
|
isResume = true;
|
|
1233
1312
|
} else {
|
|
1234
|
-
|
|
1235
|
-
|
|
1313
|
+
let horses;
|
|
1314
|
+
try {
|
|
1315
|
+
horses = (await listStable()).horses;
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
if (e instanceof ApiError) {
|
|
1318
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1319
|
+
return 1;
|
|
1320
|
+
}
|
|
1321
|
+
throw e;
|
|
1322
|
+
}
|
|
1323
|
+
if (horses.length === 0) {
|
|
1236
1324
|
console.error("Your stable is empty. Run `token-derby stable create` first.");
|
|
1237
1325
|
return 1;
|
|
1238
1326
|
}
|
|
1239
|
-
const picked = await pickHorse(
|
|
1327
|
+
const picked = await pickHorse(horses);
|
|
1240
1328
|
if (!picked) {
|
|
1241
1329
|
console.log("Cancelled.");
|
|
1242
1330
|
return 1;
|
|
@@ -1248,9 +1336,7 @@ async function joinCommand(joinCode) {
|
|
|
1248
1336
|
}
|
|
1249
1337
|
let joinResp;
|
|
1250
1338
|
try {
|
|
1251
|
-
joinResp = await joinRace(code, {
|
|
1252
|
-
horse: { stable_horse_id: chosenStableHorseId, name: chosenName, colors: chosenColors }
|
|
1253
|
-
});
|
|
1339
|
+
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId });
|
|
1254
1340
|
} catch (e) {
|
|
1255
1341
|
if (e instanceof ApiError) {
|
|
1256
1342
|
if (e.code === "RACE_FULL") console.error("This race is full.");
|
|
@@ -1258,7 +1344,9 @@ async function joinCommand(joinCode) {
|
|
|
1258
1344
|
else if (e.code === "RACE_NOT_FOUND") console.error(`No race with join code ${code}.`);
|
|
1259
1345
|
else if (e.code === "VERSION_MISMATCH") console.error(e.message);
|
|
1260
1346
|
else if (e.code === "DUPLICATE_HORSE") console.error(e.message);
|
|
1261
|
-
else if (e.code === "
|
|
1347
|
+
else if (e.code === "STABLE_HORSE_NOT_FOUND") {
|
|
1348
|
+
console.error("That horse no longer exists in your stable. Try again.");
|
|
1349
|
+
} else if (e.code === "NOT_ORG_MEMBER") console.error(e.message);
|
|
1262
1350
|
else console.error(`Error: ${e.code} ${e.message}`);
|
|
1263
1351
|
return 1;
|
|
1264
1352
|
}
|
|
@@ -1303,14 +1391,14 @@ async function pickHorse(horses) {
|
|
|
1303
1391
|
}
|
|
1304
1392
|
|
|
1305
1393
|
// src/commands/end.ts
|
|
1306
|
-
import * as
|
|
1307
|
-
import { stdin as
|
|
1394
|
+
import * as readline3 from "readline/promises";
|
|
1395
|
+
import { stdin as stdin3, stdout as stdout3 } from "process";
|
|
1308
1396
|
async function endCommand(adminCode) {
|
|
1309
1397
|
if (!adminCode) {
|
|
1310
1398
|
console.error("Usage: token-derby end <admin-code>");
|
|
1311
1399
|
return 2;
|
|
1312
1400
|
}
|
|
1313
|
-
const rl =
|
|
1401
|
+
const rl = readline3.createInterface({ input: stdin3, output: stdout3 });
|
|
1314
1402
|
const answer = (await rl.question("End the race now and freeze final tokens? [y/N] ")).trim().toLowerCase();
|
|
1315
1403
|
rl.close();
|
|
1316
1404
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -1332,11 +1420,16 @@ async function endCommand(adminCode) {
|
|
|
1332
1420
|
}
|
|
1333
1421
|
|
|
1334
1422
|
// src/commands/init.ts
|
|
1335
|
-
import * as
|
|
1336
|
-
import { stdin as
|
|
1337
|
-
async function initCommand() {
|
|
1423
|
+
import * as readline4 from "readline/promises";
|
|
1424
|
+
import { stdin as stdin4, stdout as stdout4 } from "process";
|
|
1425
|
+
async function initCommand(reset = false) {
|
|
1426
|
+
if (reset) {
|
|
1427
|
+
await deleteIdentity();
|
|
1428
|
+
_resetIdentityCacheForTests();
|
|
1429
|
+
console.log("Removed local identity. Creating a new one\u2026");
|
|
1430
|
+
}
|
|
1338
1431
|
const existing = await loadIdentity();
|
|
1339
|
-
const rl =
|
|
1432
|
+
const rl = readline4.createInterface({ input: stdin4, output: stdout4 });
|
|
1340
1433
|
try {
|
|
1341
1434
|
if (existing) {
|
|
1342
1435
|
console.log(`Current jockey name: ${existing.display_name}`);
|
|
@@ -1350,10 +1443,25 @@ async function initCommand() {
|
|
|
1350
1443
|
console.error(v2.error);
|
|
1351
1444
|
return 1;
|
|
1352
1445
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1446
|
+
try {
|
|
1447
|
+
const resp = await updateJockey({ display_name: v2.name });
|
|
1448
|
+
const updated = { ...existing, display_name: resp.display_name };
|
|
1449
|
+
await saveIdentity(updated);
|
|
1450
|
+
console.log(`Updated jockey name to: ${updated.display_name}`);
|
|
1451
|
+
return 0;
|
|
1452
|
+
} catch (e) {
|
|
1453
|
+
if (e instanceof ApiError) {
|
|
1454
|
+
if (e.code === "UNAUTHENTICATED") {
|
|
1455
|
+
console.error(
|
|
1456
|
+
"Server does not recognise this identity. Your account may have been wiped. Run `token-derby init --reset` to start fresh."
|
|
1457
|
+
);
|
|
1458
|
+
} else {
|
|
1459
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1460
|
+
}
|
|
1461
|
+
return 1;
|
|
1462
|
+
}
|
|
1463
|
+
throw e;
|
|
1464
|
+
}
|
|
1357
1465
|
}
|
|
1358
1466
|
const raw = (await rl.question("Jockey Name (use your real name please): ")).trim();
|
|
1359
1467
|
const v = validateDisplayName(raw);
|
|
@@ -1361,26 +1469,152 @@ async function initCommand() {
|
|
|
1361
1469
|
console.error(v.error);
|
|
1362
1470
|
return 1;
|
|
1363
1471
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1472
|
+
try {
|
|
1473
|
+
const resp = await initJockey({ display_name: v.name });
|
|
1474
|
+
const identity = {
|
|
1475
|
+
user_id: resp.user_id,
|
|
1476
|
+
display_name: resp.display_name,
|
|
1477
|
+
secret_token: resp.secret_token,
|
|
1478
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1479
|
+
};
|
|
1480
|
+
await saveIdentity(identity);
|
|
1481
|
+
_resetIdentityCacheForTests();
|
|
1482
|
+
console.log("");
|
|
1483
|
+
console.log(`Welcome, ${identity.display_name}!`);
|
|
1484
|
+
console.log("Your identity has been created on the server.");
|
|
1485
|
+
console.log("You can now create a stable and join races.");
|
|
1486
|
+
console.log("");
|
|
1487
|
+
console.log(" \u26A0 Your secret token is stored locally in identity.json.");
|
|
1488
|
+
console.log(" If you lose it, you cannot recover this account \u2014 you would");
|
|
1489
|
+
console.log(" need to run `token-derby init --reset` and rebuild your stable.");
|
|
1490
|
+
return 0;
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
if (e instanceof ApiError) {
|
|
1493
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1494
|
+
return 1;
|
|
1495
|
+
}
|
|
1496
|
+
throw e;
|
|
1497
|
+
}
|
|
1498
|
+
} finally {
|
|
1499
|
+
rl.close();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/commands/org-create.ts
|
|
1504
|
+
import * as readline5 from "readline/promises";
|
|
1505
|
+
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
1506
|
+
async function orgCreateCommand() {
|
|
1507
|
+
const rl = readline5.createInterface({ input: stdin5, output: stdout5 });
|
|
1508
|
+
try {
|
|
1509
|
+
const name = (await rl.question(`Organisation name (1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric chars): `)).trim();
|
|
1510
|
+
if (!ORG_NAME_PATTERN.test(name)) {
|
|
1511
|
+
console.error(`Name must be 1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric characters (no spaces or symbols).`);
|
|
1512
|
+
return 1;
|
|
1513
|
+
}
|
|
1514
|
+
const resp = await createOrganisation({ name });
|
|
1370
1515
|
console.log("");
|
|
1371
|
-
console.log(`
|
|
1372
|
-
console.log(
|
|
1516
|
+
console.log(` Organisation created: ${resp.org_name}`);
|
|
1517
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
1518
|
+
console.log(` \u2551 JOIN TOKEN: ${resp.org_join_token.padEnd(43)}\u2551`);
|
|
1519
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
1520
|
+
console.log(" \u26A0 Share this token to invite members. Treat it as a secret.");
|
|
1521
|
+
console.log("");
|
|
1522
|
+
console.log(` Members join with: token-derby organisation join ${resp.org_join_token}`);
|
|
1523
|
+
console.log(` Create org races: token-derby create --organisation ${resp.org_name}`);
|
|
1373
1524
|
return 0;
|
|
1525
|
+
} catch (e) {
|
|
1526
|
+
if (e instanceof ApiError) {
|
|
1527
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1528
|
+
return 1;
|
|
1529
|
+
}
|
|
1530
|
+
throw e;
|
|
1374
1531
|
} finally {
|
|
1375
1532
|
rl.close();
|
|
1376
1533
|
}
|
|
1377
1534
|
}
|
|
1378
1535
|
|
|
1536
|
+
// src/commands/org-join.ts
|
|
1537
|
+
async function orgJoinCommand(token) {
|
|
1538
|
+
if (!token) {
|
|
1539
|
+
console.error("Usage: token-derby organisation join <join-token>");
|
|
1540
|
+
return 2;
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const resp = await joinOrganisation({ join_token: token });
|
|
1544
|
+
console.log(`Joined organisation: ${resp.org_name}`);
|
|
1545
|
+
return 0;
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
if (e instanceof ApiError) {
|
|
1548
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1549
|
+
return 1;
|
|
1550
|
+
}
|
|
1551
|
+
throw e;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/commands/org-list.ts
|
|
1556
|
+
async function orgListCommand() {
|
|
1557
|
+
try {
|
|
1558
|
+
const resp = await listOrganisations();
|
|
1559
|
+
if (resp.organisations.length === 0) {
|
|
1560
|
+
console.log("You are not in any organisations.");
|
|
1561
|
+
console.log("Create one with: token-derby organisation create");
|
|
1562
|
+
console.log("Or join one with: token-derby organisation join <token>");
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
console.log(`Your organisations (${resp.organisations.length}):`);
|
|
1566
|
+
for (const o of resp.organisations) {
|
|
1567
|
+
console.log(` \u2022 ${o.org_name}`);
|
|
1568
|
+
}
|
|
1569
|
+
return 0;
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
if (e instanceof ApiError) {
|
|
1572
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1573
|
+
return 1;
|
|
1574
|
+
}
|
|
1575
|
+
throw e;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/commands/org-info.ts
|
|
1580
|
+
async function orgInfoCommand(name) {
|
|
1581
|
+
if (!name) {
|
|
1582
|
+
console.error("Usage: token-derby organisation info <name>");
|
|
1583
|
+
return 2;
|
|
1584
|
+
}
|
|
1585
|
+
if (!ORG_NAME_PATTERN.test(name)) {
|
|
1586
|
+
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
1587
|
+
return 2;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const resp = await getOrganisation(name);
|
|
1591
|
+
console.log(`Organisation: ${resp.org_name}`);
|
|
1592
|
+
console.log(`Created: ${resp.created_at} by ${resp.creator_user_name}`);
|
|
1593
|
+
console.log("");
|
|
1594
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
1595
|
+
console.log(` \u2551 JOIN TOKEN: ${resp.org_join_token.padEnd(43)}\u2551`);
|
|
1596
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
1597
|
+
console.log(" \u26A0 Treat the token as a secret \u2014 anyone with it can join.");
|
|
1598
|
+
console.log("");
|
|
1599
|
+
console.log(` Members join with: token-derby organisation join ${resp.org_join_token}`);
|
|
1600
|
+
return 0;
|
|
1601
|
+
} catch (e) {
|
|
1602
|
+
if (e instanceof ApiError) {
|
|
1603
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1604
|
+
return 1;
|
|
1605
|
+
}
|
|
1606
|
+
throw e;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1379
1610
|
// src/bin.ts
|
|
1380
1611
|
var HELP = `token-derby v${CLI_VERSION}
|
|
1381
1612
|
|
|
1382
1613
|
Identity:
|
|
1383
1614
|
token-derby init Set up your jockey identity (run this first)
|
|
1615
|
+
Re-running renames you on the server.
|
|
1616
|
+
token-derby init --reset Wipe local identity and create a fresh account.
|
|
1617
|
+
Your previous stable is abandoned on the server.
|
|
1384
1618
|
|
|
1385
1619
|
Stable management:
|
|
1386
1620
|
token-derby stable create Make a new horse (interactive)
|
|
@@ -1388,8 +1622,17 @@ Stable management:
|
|
|
1388
1622
|
token-derby stable edit <name> Edit an existing horse's colors
|
|
1389
1623
|
token-derby stable delete <name> Remove a horse from your stable
|
|
1390
1624
|
|
|
1625
|
+
Organisations:
|
|
1626
|
+
token-derby organisation create Create a new organisation (interactive)
|
|
1627
|
+
token-derby organisation join <token> Join an organisation with a join token
|
|
1628
|
+
token-derby organisation info <name> Show an org's join token (members only)
|
|
1629
|
+
token-derby organisation list Show organisations you're a member of
|
|
1630
|
+
|
|
1391
1631
|
Races:
|
|
1392
|
-
token-derby create
|
|
1632
|
+
token-derby create [--organisation <name>]
|
|
1633
|
+
Create a new race (interactive). When
|
|
1634
|
+
--organisation is set, only members of
|
|
1635
|
+
that org can join.
|
|
1393
1636
|
token-derby join <join-code> Join (or resume) a race
|
|
1394
1637
|
token-derby end <admin-code> End a race early
|
|
1395
1638
|
|
|
@@ -1408,7 +1651,10 @@ async function main() {
|
|
|
1408
1651
|
console.log(CLI_VERSION);
|
|
1409
1652
|
return 0;
|
|
1410
1653
|
}
|
|
1411
|
-
if (cmd === "init")
|
|
1654
|
+
if (cmd === "init") {
|
|
1655
|
+
const reset = argv.slice(1).includes("--reset");
|
|
1656
|
+
return initCommand(reset);
|
|
1657
|
+
}
|
|
1412
1658
|
const identity = await loadIdentity();
|
|
1413
1659
|
if (!identity) {
|
|
1414
1660
|
console.error("Run `token-derby init` to set up your identity before using any other command.");
|
|
@@ -1424,13 +1670,34 @@ async function main() {
|
|
|
1424
1670
|
console.error("Try: stable create | stable list | stable edit <name> | stable delete <name>");
|
|
1425
1671
|
return 2;
|
|
1426
1672
|
}
|
|
1427
|
-
if (cmd === "
|
|
1673
|
+
if (cmd === "organisation" || cmd === "org") {
|
|
1674
|
+
const sub = argv[1];
|
|
1675
|
+
if (sub === "create") return orgCreateCommand();
|
|
1676
|
+
if (sub === "join") return orgJoinCommand(argv[2]);
|
|
1677
|
+
if (sub === "info") return orgInfoCommand(argv[2]);
|
|
1678
|
+
if (sub === "list") return orgListCommand();
|
|
1679
|
+
console.error(`Unknown organisation subcommand: ${sub ?? "(none)"}`);
|
|
1680
|
+
console.error("Try: organisation create | organisation join <token> | organisation info <name> | organisation list");
|
|
1681
|
+
return 2;
|
|
1682
|
+
}
|
|
1683
|
+
if (cmd === "create") {
|
|
1684
|
+
const orgName = parseFlag(argv.slice(1), "--organisation");
|
|
1685
|
+
return createRaceCommand(orgName);
|
|
1686
|
+
}
|
|
1428
1687
|
if (cmd === "join") return joinCommand(argv[1]);
|
|
1429
1688
|
if (cmd === "end") return endCommand(argv[1]);
|
|
1430
1689
|
console.error(`Unknown command: ${cmd}`);
|
|
1431
1690
|
console.error(HELP);
|
|
1432
1691
|
return 2;
|
|
1433
1692
|
}
|
|
1693
|
+
function parseFlag(args, flag) {
|
|
1694
|
+
for (let i = 0; i < args.length; i++) {
|
|
1695
|
+
if (args[i] === flag) return args[i + 1];
|
|
1696
|
+
const eq = `${flag}=`;
|
|
1697
|
+
if (args[i]?.startsWith(eq)) return args[i].slice(eq.length);
|
|
1698
|
+
}
|
|
1699
|
+
return void 0;
|
|
1700
|
+
}
|
|
1434
1701
|
main().then(
|
|
1435
1702
|
(code) => process.exit(code),
|
|
1436
1703
|
(err) => {
|