@mauricode/token-derby 1.1.0 → 2.0.1
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 +639 -367
- 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.1".length > 0) {
|
|
341
|
+
return "2.0.1";
|
|
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 };
|
|
389
447
|
}
|
|
390
|
-
|
|
391
|
-
|
|
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;
|
|
464
|
+
}
|
|
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
663
|
console.error("Usage: token-derby stable delete <name>");
|
|
505
664
|
return 2;
|
|
506
665
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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.`);
|
|
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}`);
|
|
518
672
|
return 1;
|
|
519
673
|
}
|
|
674
|
+
throw e;
|
|
520
675
|
}
|
|
521
|
-
const
|
|
522
|
-
|
|
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) {
|
|
676
|
+
const horse = horses.find((h) => h.name === name);
|
|
677
|
+
if (!horse) {
|
|
544
678
|
console.error(`No horse named "${name}" in your stable.`);
|
|
545
679
|
return 1;
|
|
546
680
|
}
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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;
|
|
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;
|
|
666
687
|
}
|
|
667
|
-
if (authToken) headers["authorization"] = `Bearer ${authToken}`;
|
|
668
|
-
if (body !== void 0) headers["content-type"] = "application/json";
|
|
669
|
-
let res;
|
|
670
688
|
try {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
675
|
-
});
|
|
689
|
+
await deleteStableHorse(horse.stable_horse_id);
|
|
690
|
+
console.log(`\u2713 Deleted "${name}".`);
|
|
691
|
+
return 0;
|
|
676
692
|
} catch (e) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
681
|
-
let parsed = null;
|
|
682
|
-
if (contentType.includes("application/json") && text.length > 0) {
|
|
683
|
-
try {
|
|
684
|
-
parsed = JSON.parse(text);
|
|
685
|
-
} catch {
|
|
686
|
-
parsed = null;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
if (!res.ok) {
|
|
690
|
-
if (parsed && typeof parsed.code === "string") {
|
|
691
|
-
throw new ApiError(parsed.code, parsed.message ?? "API error", res.status);
|
|
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,17 @@ 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
|
-
|
|
1110
|
+
let entries;
|
|
1111
|
+
try {
|
|
1112
|
+
entries = await fs3.readdir(projectDir);
|
|
1113
|
+
} catch {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1034
1116
|
for (const entry of entries) {
|
|
1035
1117
|
if (entry.endsWith(".jsonl")) out.push(path4.join(projectDir, entry));
|
|
1036
1118
|
}
|
|
@@ -1043,7 +1125,7 @@ function addNum(value) {
|
|
|
1043
1125
|
async function sumFile(file) {
|
|
1044
1126
|
let raw;
|
|
1045
1127
|
try {
|
|
1046
|
-
raw = await
|
|
1128
|
+
raw = await fs3.readFile(file, "utf8");
|
|
1047
1129
|
} catch {
|
|
1048
1130
|
return { input: 0, output: 0 };
|
|
1049
1131
|
}
|
|
@@ -1096,14 +1178,6 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1096
1178
|
}
|
|
1097
1179
|
}, [race?.status]);
|
|
1098
1180
|
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
1181
|
runHeartbeatLoop({
|
|
1108
1182
|
sendHeartbeat: async (currentTokens) => {
|
|
1109
1183
|
const resp = await heartbeat(
|
|
@@ -1129,6 +1203,7 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1129
1203
|
onSuccess: (resp) => {
|
|
1130
1204
|
setLastHbAt(/* @__PURE__ */ new Date());
|
|
1131
1205
|
setLastHbOk(true);
|
|
1206
|
+
setRace(raceViewFrom(resp));
|
|
1132
1207
|
if (resp.race_status === "finished") exit();
|
|
1133
1208
|
},
|
|
1134
1209
|
onError: (err) => {
|
|
@@ -1146,13 +1221,13 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1146
1221
|
const sampler = setInterval(async () => {
|
|
1147
1222
|
try {
|
|
1148
1223
|
lastTokenSampleRef.current = await sumOutputTokens();
|
|
1149
|
-
} catch {
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
console.error("[token-derby] token sampler failed:", e);
|
|
1150
1226
|
}
|
|
1151
1227
|
}, 5e3);
|
|
1152
1228
|
sumOutputTokens().then((t) => {
|
|
1153
1229
|
lastTokenSampleRef.current = t;
|
|
1154
|
-
}).catch(() =>
|
|
1155
|
-
});
|
|
1230
|
+
}).catch((e) => console.error("[token-derby] token sampler prime failed:", e));
|
|
1156
1231
|
const controller = ctrl.current;
|
|
1157
1232
|
return () => {
|
|
1158
1233
|
clearInterval(sampler);
|
|
@@ -1179,6 +1254,15 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1179
1254
|
}
|
|
1180
1255
|
);
|
|
1181
1256
|
}
|
|
1257
|
+
function raceViewFrom(resp) {
|
|
1258
|
+
return {
|
|
1259
|
+
...resp.race,
|
|
1260
|
+
status: resp.race_status,
|
|
1261
|
+
horses: resp.horses,
|
|
1262
|
+
server_time: resp.server_time,
|
|
1263
|
+
time_left_seconds: resp.time_left_seconds
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1182
1266
|
async function buildInitialState(args) {
|
|
1183
1267
|
const runningTotal = await sumOutputTokens();
|
|
1184
1268
|
if (args.rejoin) {
|
|
@@ -1231,12 +1315,21 @@ async function joinCommand(joinCode) {
|
|
|
1231
1315
|
chosenColors = ownHorse.colors;
|
|
1232
1316
|
isResume = true;
|
|
1233
1317
|
} else {
|
|
1234
|
-
|
|
1235
|
-
|
|
1318
|
+
let horses;
|
|
1319
|
+
try {
|
|
1320
|
+
horses = (await listStable()).horses;
|
|
1321
|
+
} catch (e) {
|
|
1322
|
+
if (e instanceof ApiError) {
|
|
1323
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1324
|
+
return 1;
|
|
1325
|
+
}
|
|
1326
|
+
throw e;
|
|
1327
|
+
}
|
|
1328
|
+
if (horses.length === 0) {
|
|
1236
1329
|
console.error("Your stable is empty. Run `token-derby stable create` first.");
|
|
1237
1330
|
return 1;
|
|
1238
1331
|
}
|
|
1239
|
-
const picked = await pickHorse(
|
|
1332
|
+
const picked = await pickHorse(horses);
|
|
1240
1333
|
if (!picked) {
|
|
1241
1334
|
console.log("Cancelled.");
|
|
1242
1335
|
return 1;
|
|
@@ -1248,9 +1341,7 @@ async function joinCommand(joinCode) {
|
|
|
1248
1341
|
}
|
|
1249
1342
|
let joinResp;
|
|
1250
1343
|
try {
|
|
1251
|
-
joinResp = await joinRace(code, {
|
|
1252
|
-
horse: { stable_horse_id: chosenStableHorseId, name: chosenName, colors: chosenColors }
|
|
1253
|
-
});
|
|
1344
|
+
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId });
|
|
1254
1345
|
} catch (e) {
|
|
1255
1346
|
if (e instanceof ApiError) {
|
|
1256
1347
|
if (e.code === "RACE_FULL") console.error("This race is full.");
|
|
@@ -1258,7 +1349,9 @@ async function joinCommand(joinCode) {
|
|
|
1258
1349
|
else if (e.code === "RACE_NOT_FOUND") console.error(`No race with join code ${code}.`);
|
|
1259
1350
|
else if (e.code === "VERSION_MISMATCH") console.error(e.message);
|
|
1260
1351
|
else if (e.code === "DUPLICATE_HORSE") console.error(e.message);
|
|
1261
|
-
else if (e.code === "
|
|
1352
|
+
else if (e.code === "STABLE_HORSE_NOT_FOUND") {
|
|
1353
|
+
console.error("That horse no longer exists in your stable. Try again.");
|
|
1354
|
+
} else if (e.code === "NOT_ORG_MEMBER") console.error(e.message);
|
|
1262
1355
|
else console.error(`Error: ${e.code} ${e.message}`);
|
|
1263
1356
|
return 1;
|
|
1264
1357
|
}
|
|
@@ -1303,14 +1396,14 @@ async function pickHorse(horses) {
|
|
|
1303
1396
|
}
|
|
1304
1397
|
|
|
1305
1398
|
// src/commands/end.ts
|
|
1306
|
-
import * as
|
|
1307
|
-
import { stdin as
|
|
1399
|
+
import * as readline3 from "readline/promises";
|
|
1400
|
+
import { stdin as stdin3, stdout as stdout3 } from "process";
|
|
1308
1401
|
async function endCommand(adminCode) {
|
|
1309
1402
|
if (!adminCode) {
|
|
1310
1403
|
console.error("Usage: token-derby end <admin-code>");
|
|
1311
1404
|
return 2;
|
|
1312
1405
|
}
|
|
1313
|
-
const rl =
|
|
1406
|
+
const rl = readline3.createInterface({ input: stdin3, output: stdout3 });
|
|
1314
1407
|
const answer = (await rl.question("End the race now and freeze final tokens? [y/N] ")).trim().toLowerCase();
|
|
1315
1408
|
rl.close();
|
|
1316
1409
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -1332,11 +1425,16 @@ async function endCommand(adminCode) {
|
|
|
1332
1425
|
}
|
|
1333
1426
|
|
|
1334
1427
|
// src/commands/init.ts
|
|
1335
|
-
import * as
|
|
1336
|
-
import { stdin as
|
|
1337
|
-
async function initCommand() {
|
|
1428
|
+
import * as readline4 from "readline/promises";
|
|
1429
|
+
import { stdin as stdin4, stdout as stdout4 } from "process";
|
|
1430
|
+
async function initCommand(reset = false) {
|
|
1431
|
+
if (reset) {
|
|
1432
|
+
await deleteIdentity();
|
|
1433
|
+
_resetIdentityCacheForTests();
|
|
1434
|
+
console.log("Removed local identity. Creating a new one\u2026");
|
|
1435
|
+
}
|
|
1338
1436
|
const existing = await loadIdentity();
|
|
1339
|
-
const rl =
|
|
1437
|
+
const rl = readline4.createInterface({ input: stdin4, output: stdout4 });
|
|
1340
1438
|
try {
|
|
1341
1439
|
if (existing) {
|
|
1342
1440
|
console.log(`Current jockey name: ${existing.display_name}`);
|
|
@@ -1350,10 +1448,25 @@ async function initCommand() {
|
|
|
1350
1448
|
console.error(v2.error);
|
|
1351
1449
|
return 1;
|
|
1352
1450
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1451
|
+
try {
|
|
1452
|
+
const resp = await updateJockey({ display_name: v2.name });
|
|
1453
|
+
const updated = { ...existing, display_name: resp.display_name };
|
|
1454
|
+
await saveIdentity(updated);
|
|
1455
|
+
console.log(`Updated jockey name to: ${updated.display_name}`);
|
|
1456
|
+
return 0;
|
|
1457
|
+
} catch (e) {
|
|
1458
|
+
if (e instanceof ApiError) {
|
|
1459
|
+
if (e.code === "UNAUTHENTICATED") {
|
|
1460
|
+
console.error(
|
|
1461
|
+
"Server does not recognise this identity. Your account may have been wiped. Run `token-derby init --reset` to start fresh."
|
|
1462
|
+
);
|
|
1463
|
+
} else {
|
|
1464
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1465
|
+
}
|
|
1466
|
+
return 1;
|
|
1467
|
+
}
|
|
1468
|
+
throw e;
|
|
1469
|
+
}
|
|
1357
1470
|
}
|
|
1358
1471
|
const raw = (await rl.question("Jockey Name (use your real name please): ")).trim();
|
|
1359
1472
|
const v = validateDisplayName(raw);
|
|
@@ -1361,26 +1474,152 @@ async function initCommand() {
|
|
|
1361
1474
|
console.error(v.error);
|
|
1362
1475
|
return 1;
|
|
1363
1476
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
const resp = await initJockey({ display_name: v.name });
|
|
1479
|
+
const identity = {
|
|
1480
|
+
user_id: resp.user_id,
|
|
1481
|
+
display_name: resp.display_name,
|
|
1482
|
+
secret_token: resp.secret_token,
|
|
1483
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1484
|
+
};
|
|
1485
|
+
await saveIdentity(identity);
|
|
1486
|
+
_resetIdentityCacheForTests();
|
|
1487
|
+
console.log("");
|
|
1488
|
+
console.log(`Welcome, ${identity.display_name}!`);
|
|
1489
|
+
console.log("Your identity has been created on the server.");
|
|
1490
|
+
console.log("You can now create a stable and join races.");
|
|
1491
|
+
console.log("");
|
|
1492
|
+
console.log(" \u26A0 Your secret token is stored locally in identity.json.");
|
|
1493
|
+
console.log(" If you lose it, you cannot recover this account \u2014 you would");
|
|
1494
|
+
console.log(" need to run `token-derby init --reset` and rebuild your stable.");
|
|
1495
|
+
return 0;
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
if (e instanceof ApiError) {
|
|
1498
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1499
|
+
return 1;
|
|
1500
|
+
}
|
|
1501
|
+
throw e;
|
|
1502
|
+
}
|
|
1503
|
+
} finally {
|
|
1504
|
+
rl.close();
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/commands/org-create.ts
|
|
1509
|
+
import * as readline5 from "readline/promises";
|
|
1510
|
+
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
1511
|
+
async function orgCreateCommand() {
|
|
1512
|
+
const rl = readline5.createInterface({ input: stdin5, output: stdout5 });
|
|
1513
|
+
try {
|
|
1514
|
+
const name = (await rl.question(`Organisation name (1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric chars): `)).trim();
|
|
1515
|
+
if (!ORG_NAME_PATTERN.test(name)) {
|
|
1516
|
+
console.error(`Name must be 1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric characters (no spaces or symbols).`);
|
|
1517
|
+
return 1;
|
|
1518
|
+
}
|
|
1519
|
+
const resp = await createOrganisation({ name });
|
|
1370
1520
|
console.log("");
|
|
1371
|
-
console.log(`
|
|
1372
|
-
console.log(
|
|
1521
|
+
console.log(` Organisation created: ${resp.org_name}`);
|
|
1522
|
+
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");
|
|
1523
|
+
console.log(` \u2551 JOIN TOKEN: ${resp.org_join_token.padEnd(43)}\u2551`);
|
|
1524
|
+
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");
|
|
1525
|
+
console.log(" \u26A0 Share this token to invite members. Treat it as a secret.");
|
|
1526
|
+
console.log("");
|
|
1527
|
+
console.log(` Members join with: token-derby organisation join ${resp.org_join_token}`);
|
|
1528
|
+
console.log(` Create org races: token-derby create --organisation ${resp.org_name}`);
|
|
1373
1529
|
return 0;
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
if (e instanceof ApiError) {
|
|
1532
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1533
|
+
return 1;
|
|
1534
|
+
}
|
|
1535
|
+
throw e;
|
|
1374
1536
|
} finally {
|
|
1375
1537
|
rl.close();
|
|
1376
1538
|
}
|
|
1377
1539
|
}
|
|
1378
1540
|
|
|
1541
|
+
// src/commands/org-join.ts
|
|
1542
|
+
async function orgJoinCommand(token) {
|
|
1543
|
+
if (!token) {
|
|
1544
|
+
console.error("Usage: token-derby organisation join <join-token>");
|
|
1545
|
+
return 2;
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
const resp = await joinOrganisation({ join_token: token });
|
|
1549
|
+
console.log(`Joined organisation: ${resp.org_name}`);
|
|
1550
|
+
return 0;
|
|
1551
|
+
} catch (e) {
|
|
1552
|
+
if (e instanceof ApiError) {
|
|
1553
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1554
|
+
return 1;
|
|
1555
|
+
}
|
|
1556
|
+
throw e;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/commands/org-list.ts
|
|
1561
|
+
async function orgListCommand() {
|
|
1562
|
+
try {
|
|
1563
|
+
const resp = await listOrganisations();
|
|
1564
|
+
if (resp.organisations.length === 0) {
|
|
1565
|
+
console.log("You are not in any organisations.");
|
|
1566
|
+
console.log("Create one with: token-derby organisation create");
|
|
1567
|
+
console.log("Or join one with: token-derby organisation join <token>");
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1570
|
+
console.log(`Your organisations (${resp.organisations.length}):`);
|
|
1571
|
+
for (const o of resp.organisations) {
|
|
1572
|
+
console.log(` \u2022 ${o.org_name}`);
|
|
1573
|
+
}
|
|
1574
|
+
return 0;
|
|
1575
|
+
} catch (e) {
|
|
1576
|
+
if (e instanceof ApiError) {
|
|
1577
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1578
|
+
return 1;
|
|
1579
|
+
}
|
|
1580
|
+
throw e;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/commands/org-info.ts
|
|
1585
|
+
async function orgInfoCommand(name) {
|
|
1586
|
+
if (!name) {
|
|
1587
|
+
console.error("Usage: token-derby organisation info <name>");
|
|
1588
|
+
return 2;
|
|
1589
|
+
}
|
|
1590
|
+
if (!ORG_NAME_PATTERN.test(name)) {
|
|
1591
|
+
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
1592
|
+
return 2;
|
|
1593
|
+
}
|
|
1594
|
+
try {
|
|
1595
|
+
const resp = await getOrganisation(name);
|
|
1596
|
+
console.log(`Organisation: ${resp.org_name}`);
|
|
1597
|
+
console.log(`Created: ${resp.created_at} by ${resp.creator_user_name}`);
|
|
1598
|
+
console.log("");
|
|
1599
|
+
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");
|
|
1600
|
+
console.log(` \u2551 JOIN TOKEN: ${resp.org_join_token.padEnd(43)}\u2551`);
|
|
1601
|
+
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");
|
|
1602
|
+
console.log(" \u26A0 Treat the token as a secret \u2014 anyone with it can join.");
|
|
1603
|
+
console.log("");
|
|
1604
|
+
console.log(` Members join with: token-derby organisation join ${resp.org_join_token}`);
|
|
1605
|
+
return 0;
|
|
1606
|
+
} catch (e) {
|
|
1607
|
+
if (e instanceof ApiError) {
|
|
1608
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
1609
|
+
return 1;
|
|
1610
|
+
}
|
|
1611
|
+
throw e;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1379
1615
|
// src/bin.ts
|
|
1380
1616
|
var HELP = `token-derby v${CLI_VERSION}
|
|
1381
1617
|
|
|
1382
1618
|
Identity:
|
|
1383
1619
|
token-derby init Set up your jockey identity (run this first)
|
|
1620
|
+
Re-running renames you on the server.
|
|
1621
|
+
token-derby init --reset Wipe local identity and create a fresh account.
|
|
1622
|
+
Your previous stable is abandoned on the server.
|
|
1384
1623
|
|
|
1385
1624
|
Stable management:
|
|
1386
1625
|
token-derby stable create Make a new horse (interactive)
|
|
@@ -1388,8 +1627,17 @@ Stable management:
|
|
|
1388
1627
|
token-derby stable edit <name> Edit an existing horse's colors
|
|
1389
1628
|
token-derby stable delete <name> Remove a horse from your stable
|
|
1390
1629
|
|
|
1630
|
+
Organisations:
|
|
1631
|
+
token-derby organisation create Create a new organisation (interactive)
|
|
1632
|
+
token-derby organisation join <token> Join an organisation with a join token
|
|
1633
|
+
token-derby organisation info <name> Show an org's join token (members only)
|
|
1634
|
+
token-derby organisation list Show organisations you're a member of
|
|
1635
|
+
|
|
1391
1636
|
Races:
|
|
1392
|
-
token-derby create
|
|
1637
|
+
token-derby create [--organisation <name>]
|
|
1638
|
+
Create a new race (interactive). When
|
|
1639
|
+
--organisation is set, only members of
|
|
1640
|
+
that org can join.
|
|
1393
1641
|
token-derby join <join-code> Join (or resume) a race
|
|
1394
1642
|
token-derby end <admin-code> End a race early
|
|
1395
1643
|
|
|
@@ -1408,7 +1656,10 @@ async function main() {
|
|
|
1408
1656
|
console.log(CLI_VERSION);
|
|
1409
1657
|
return 0;
|
|
1410
1658
|
}
|
|
1411
|
-
if (cmd === "init")
|
|
1659
|
+
if (cmd === "init") {
|
|
1660
|
+
const reset = argv.slice(1).includes("--reset");
|
|
1661
|
+
return initCommand(reset);
|
|
1662
|
+
}
|
|
1412
1663
|
const identity = await loadIdentity();
|
|
1413
1664
|
if (!identity) {
|
|
1414
1665
|
console.error("Run `token-derby init` to set up your identity before using any other command.");
|
|
@@ -1424,13 +1675,34 @@ async function main() {
|
|
|
1424
1675
|
console.error("Try: stable create | stable list | stable edit <name> | stable delete <name>");
|
|
1425
1676
|
return 2;
|
|
1426
1677
|
}
|
|
1427
|
-
if (cmd === "
|
|
1678
|
+
if (cmd === "organisation" || cmd === "org") {
|
|
1679
|
+
const sub = argv[1];
|
|
1680
|
+
if (sub === "create") return orgCreateCommand();
|
|
1681
|
+
if (sub === "join") return orgJoinCommand(argv[2]);
|
|
1682
|
+
if (sub === "info") return orgInfoCommand(argv[2]);
|
|
1683
|
+
if (sub === "list") return orgListCommand();
|
|
1684
|
+
console.error(`Unknown organisation subcommand: ${sub ?? "(none)"}`);
|
|
1685
|
+
console.error("Try: organisation create | organisation join <token> | organisation info <name> | organisation list");
|
|
1686
|
+
return 2;
|
|
1687
|
+
}
|
|
1688
|
+
if (cmd === "create") {
|
|
1689
|
+
const orgName = parseFlag(argv.slice(1), "--organisation");
|
|
1690
|
+
return createRaceCommand(orgName);
|
|
1691
|
+
}
|
|
1428
1692
|
if (cmd === "join") return joinCommand(argv[1]);
|
|
1429
1693
|
if (cmd === "end") return endCommand(argv[1]);
|
|
1430
1694
|
console.error(`Unknown command: ${cmd}`);
|
|
1431
1695
|
console.error(HELP);
|
|
1432
1696
|
return 2;
|
|
1433
1697
|
}
|
|
1698
|
+
function parseFlag(args, flag) {
|
|
1699
|
+
for (let i = 0; i < args.length; i++) {
|
|
1700
|
+
if (args[i] === flag) return args[i + 1];
|
|
1701
|
+
const eq = `${flag}=`;
|
|
1702
|
+
if (args[i]?.startsWith(eq)) return args[i].slice(eq.length);
|
|
1703
|
+
}
|
|
1704
|
+
return void 0;
|
|
1705
|
+
}
|
|
1434
1706
|
main().then(
|
|
1435
1707
|
(code) => process.exit(code),
|
|
1436
1708
|
(err) => {
|