@mauricode/token-derby 2.13.0 → 3.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.
Files changed (3) hide show
  1. package/dist/bin.js +1040 -353
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -36,7 +36,9 @@ var CLI_VERSION_HEADER = "x-cli-version";
36
36
  var USER_ID_HEADER = "x-user-id";
37
37
  var USER_TOKEN_HEADER = "x-user-token";
38
38
  var USER_NAME_MAX_LENGTH = 40;
39
+ var SECRET_TOKEN_BYTES = 32;
39
40
  var ORG_NAME_PATTERN = /^[A-Za-z0-9]{1,12}$/;
41
+ var DEVICE_LABEL_MAX_LENGTH = 40;
40
42
 
41
43
  // ../shared/dist/version-match.js
42
44
  var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
@@ -152,48 +154,52 @@ function hatById(id) {
152
154
  }
153
155
  var HATS = [
154
156
  // ── COMMON (18) ────────────────────────────────────────────────────────
155
- { id: "flat_cap", name: "Flat Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...........", "....AAAA...", "...AAAAAA..", "..AAAAAAA.."], variants: [{ A: "#8B6914" }, { A: "#12301b" }, { A: "#aab5cb" }, { A: "#bdcbaa" }] },
156
- { id: "bucket_hat", name: "Bucket Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...QQQQQ...", "...AAAAA...", "...AAAAA...", ".AAAAAAAAA."], variants: [{ A: "#CC2200", Q: "#FFFFFF" }, { A: "#0007cc", Q: "#FFFFFF" }, { A: "#becc00", Q: "#4d3dc7" }, { A: "#cc00c5", Q: "#d6d6dc" }] },
157
- { id: "beanie", name: "Beanie", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#4A7C59" }, { A: "#b21f35" }, { A: "#ec9418" }, { A: "#3a17e6" }] },
158
- { id: "stetson", name: "Stetson", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...AA.AA...", "...AAAAA...", "...AAAAA...", "A..AAAAA..A", "AAAAAAAAAAA", "AAAAAAAAAAA"], variants: [{ A: "#C49A00" }, { A: "#272004" }, { A: "#040404" }, { A: "#373606" }] },
159
- { id: "party_hat", name: "Party Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "....AQA....", "...AQAQA...", "...AAAAA..."], variants: [{ A: "#FF69B4", Q: "#FFD700" }, { A: "#c46bff", Q: "#ff000d" }, { A: "#fdff6b", Q: "#0400ff" }, { A: "#6bff97", Q: "#05050a" }] },
160
- { id: "fez", name: "Fez", rarity: "common", width: 11, anchor_x: 22, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....Q......", "....AAA....", "....AAA....", "....AAA...."], variants: [{ A: "#CC0000", Q: "#8B0000" }, { A: "#CC0000", Q: "#f5e5e5" }, { A: "#CC0000", Q: "#f1e209" }, { A: "#CC0000", Q: "#0956f1" }] },
161
- { id: "sailor_hat", name: "Sailor Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...QQQQQ...", "..AAAAAAA..", "..QQQQQQQ..", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#000080" }, { A: "#FFFFFF", Q: "#c0c0ed" }, { A: "#FFFFFF", Q: "#00000a" }, { A: "#FFFFFF", Q: "#0505ef" }] },
162
- { id: "newsboy_cap", name: "Newsboy Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "..AAAQAAA..", "..AAAAAAA..", "..AAAAAA..."], variants: [{ A: "#5C4033", Q: "#3D2B1F" }, { A: "#e96020", Q: "#131211" }, { A: "#342c28", Q: "#131211" }, { A: "#0a0300", Q: "#ddab78" }] },
163
- { id: "tam_o_shanter", name: "Tam O'Shanter", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....Q.....", "....AAA....", "..AAAAAAA..", "..AAAAAAA..", "...AAAAA..."], variants: [{ A: "#006400", Q: "#FF0000" }, { A: "#ff0000", Q: "#016400" }] },
164
- { id: "trucker_cap", name: "Trucker Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAQQ....", "...AAQQ....", "..AAQQQAA..", "..AAQQQAA.."], variants: [{ A: "#2196F3", Q: "#FFFFFF" }, { A: "#f41fe3", Q: "#FFFFFF" }, { A: "#1ff45f", Q: "#FFFFFF" }] },
165
- { id: "hard_hat", name: "Hard Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#FFD600" }, { A: "#d74c1d" }] },
166
- { id: "chef_toque", name: "Chef's Toque", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...AAAAA...", "..AAQAQAA..", "...AQAQA...", "...AQAQA...", "...AQAQA...", "...AAAAA..."], variants: [{ A: "#FFFFFF", Q: "#f9f9f9" }] },
167
- { id: "bobble_hat", name: "Bobble Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....Q.....", "...AAAAA...", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#C62828", Q: "#FFFFFF" }, { A: "#293bc7", Q: "#FFFFFF" }, { A: "#29c775", Q: "#231f1f" }, { A: "#a529c7", Q: "#d50707" }] },
168
- { id: "cowboy_hat", name: "Cowboy Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "...AAAAA...", "A.AAAAAAA.A", ".AAAAAAAAA."], variants: [{ A: "#8B4513" }, { A: "#0a0400" }, { A: "#956c50" }] },
169
- { id: "baseball_cap", name: "Baseball Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "..AAAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#1565C0" }, { A: "#8515c1" }, { A: "#15c157" }, { A: "#ef2d0b" }] },
170
- { id: "tinfoil_hat", name: "Tinfoil Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#B0BEC5" }, { A: "#6a6c6c" }] },
171
- { id: "dunce_cap", name: "Dunce Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", ".....Q.....", "....QAQ....", "...QAAAQ...", "...QAAAQ...", "..QAAAAAQ..", "..AAAAAAA.."], variants: [{ A: "#F8F8F8", Q: "#F44336" }] },
172
- { id: "mini_top_hat", name: "Mini Top Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...AAAAA...", "...AAAAA...", "...AAAAA...", "..AAAAAAA..", "..AAAAAAA.."], variants: [{ A: "#212121" }, { A: "#626060" }] },
157
+ { id: "flat_cap", name: "Flat Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...........", "....AAAA...", "...AAAAAA..", "..AAAAAAA.."], variants: [{ A: "#8B6914" }, { A: "#12301b" }, { A: "#aab5cb" }, { A: "#bdcbaa" }], rollable: true },
158
+ { id: "bucket_hat", name: "Bucket Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...QQQQQ...", "...AAAAA...", "...AAAAA...", ".AAAAAAAAA."], variants: [{ A: "#CC2200", Q: "#FFFFFF" }, { A: "#0007cc", Q: "#FFFFFF" }, { A: "#becc00", Q: "#4d3dc7" }, { A: "#cc00c5", Q: "#d6d6dc" }], rollable: true },
159
+ { id: "beanie", name: "Beanie", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#4A7C59" }, { A: "#b21f35" }, { A: "#ec9418" }, { A: "#3a17e6" }], rollable: true },
160
+ { id: "stetson", name: "Stetson", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...AA.AA...", "...AAAAA...", "...AAAAA...", "A..AAAAA..A", "AAAAAAAAAAA", "AAAAAAAAAAA"], variants: [{ A: "#C49A00" }, { A: "#272004" }, { A: "#040404" }, { A: "#373606" }], rollable: true },
161
+ { id: "party_hat", name: "Party Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "....AQA....", "...AQAQA...", "...AAAAA..."], variants: [{ A: "#FF69B4", Q: "#FFD700" }, { A: "#c46bff", Q: "#ff000d" }, { A: "#fdff6b", Q: "#0400ff" }, { A: "#6bff97", Q: "#05050a" }], rollable: true },
162
+ { id: "fez", name: "Fez", rarity: "common", width: 11, anchor_x: 22, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....Q......", "....AAA....", "....AAA....", "....AAA...."], variants: [{ A: "#CC0000", Q: "#8B0000" }, { A: "#CC0000", Q: "#f5e5e5" }, { A: "#CC0000", Q: "#f1e209" }, { A: "#CC0000", Q: "#0956f1" }], rollable: true },
163
+ { id: "sailor_hat", name: "Sailor Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...QQQQQ...", "..AAAAAAA..", "..QQQQQQQ..", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#000080" }, { A: "#FFFFFF", Q: "#c0c0ed" }, { A: "#FFFFFF", Q: "#00000a" }, { A: "#FFFFFF", Q: "#0505ef" }], rollable: true },
164
+ { id: "newsboy_cap", name: "Newsboy Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "..AAAQAAA..", "..AAAAAAA..", "..AAAAAA..."], variants: [{ A: "#5C4033", Q: "#3D2B1F" }, { A: "#e96020", Q: "#131211" }, { A: "#342c28", Q: "#131211" }, { A: "#0a0300", Q: "#ddab78" }], rollable: true },
165
+ { id: "tam_o_shanter", name: "Tam O'Shanter", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....Q.....", "....AAA....", "..AAAAAAA..", "..AAAAAAA..", "...AAAAA..."], variants: [{ A: "#006400", Q: "#FF0000" }, { A: "#ff0000", Q: "#016400" }], rollable: true },
166
+ { id: "trucker_cap", name: "Trucker Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAQQ....", "...AAQQ....", "..AAQQQAA..", "..AAQQQAA.."], variants: [{ A: "#2196F3", Q: "#FFFFFF" }, { A: "#f41fe3", Q: "#FFFFFF" }, { A: "#1ff45f", Q: "#FFFFFF" }], rollable: true },
167
+ { id: "hard_hat", name: "Hard Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#FFD600" }, { A: "#d74c1d" }], rollable: true },
168
+ { id: "chef_toque", name: "Chef's Toque", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...AAAAA...", "..AAQAQAA..", "...AQAQA...", "...AQAQA...", "...AQAQA...", "...AAAAA..."], variants: [{ A: "#FFFFFF", Q: "#f9f9f9" }], rollable: true },
169
+ { id: "bobble_hat", name: "Bobble Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....Q.....", "...AAAAA...", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#C62828", Q: "#FFFFFF" }, { A: "#293bc7", Q: "#FFFFFF" }, { A: "#29c775", Q: "#231f1f" }, { A: "#a529c7", Q: "#d50707" }], rollable: true },
170
+ { id: "cowboy_hat", name: "Cowboy Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "...AAAAA...", "A.AAAAAAA.A", ".AAAAAAAAA."], variants: [{ A: "#8B4513" }, { A: "#0a0400" }, { A: "#956c50" }], rollable: true },
171
+ { id: "baseball_cap", name: "Baseball Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "..AAAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#1565C0" }, { A: "#8515c1" }, { A: "#15c157" }, { A: "#ef2d0b" }], rollable: true },
172
+ { id: "tinfoil_hat", name: "Tinfoil Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#B0BEC5" }, { A: "#6a6c6c" }], rollable: true },
173
+ { id: "dunce_cap", name: "Dunce Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", ".....Q.....", "....QAQ....", "...QAAAQ...", "...QAAAQ...", "..QAAAAAQ..", "..AAAAAAA.."], variants: [{ A: "#F8F8F8", Q: "#F44336" }], rollable: true },
174
+ { id: "mini_top_hat", name: "Mini Top Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...AAAAA...", "...AAAAA...", "...AAAAA...", "..AAAAAAA..", "..AAAAAAA.."], variants: [{ A: "#212121" }, { A: "#626060" }], rollable: true },
173
175
  // ── RARE (10) ──────────────────────────────────────────────────────────
174
- { id: "bicorne", name: "Bicorne", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AAAQQQQ..", "..AAAAAAA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#1A237E", Q: "#FFD700" }, { A: "#01052d", Q: "#544803" }] },
175
- { id: "viking_helmet", name: "Viking Helmet", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..A.AAA.A..", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#9E9E9E", Q: "#8D6E63" }, { A: "#dcdbdb", Q: "#bf3908" }] },
176
- { id: "jesters_cap", name: "Jester's Cap", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "...AAAAA...", "...AAAAA..."], variants: [{ A: "#E53935", Q: "#FFD600" }, { A: "#e5a434", Q: "#0040ff" }, { A: "#34e537", Q: "#eeff00" }, { A: "#e234e5", Q: "#00ff40" }] },
177
- { id: "plague_doctor", name: "Plague Doctor Beak", rarity: "rare", width: 11, anchor_x: 24, rows: ["...........", "...........", "...........", "...........", "....QQQ....", "...QAAAQQ..", "...QQQQQQQ.", "..QQQQQ.QQQ", "..QQQQ....Q", "..QQQQ....."], variants: [{ A: "#F5F5F5", Q: "#795548" }, { A: "#e1dbdb", Q: "#080707" }] },
178
- { id: "morion", name: "Conquistador Morion", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....Q.....", "...QQQQQ...", "...QAAAQ...", ".A..AAA..A.", "..AAQQQAA.."], variants: [{ A: "#B0BEC5", Q: "#FFD600" }, { A: "#0a4768", Q: "#e6c10a" }] },
179
- { id: "shako", name: "Shako", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "........Q..", "........Q..", "........Q..", "........Q..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#45464f", Q: "#00ff2a" }, { A: "#45464f", Q: "#ff4d00" }, { A: "#45464f", Q: "#ff0019" }, { A: "#45464f", Q: "#fff700" }] },
180
- { id: "centurion_helm", name: "Centurion Helm", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "....QQQ....", "...QQQQQQ..", "..QQQQQQQQ.", ".QQ..A...Q.", ".Q..AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAA.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#2f3131", Q: "#C62828" }] },
181
- { id: "papal_mitre", name: "Papal Mitre", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....QQQ....", "...QQQQQ...", "...QAAAQ...", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }] },
182
- { id: "headdress", name: "Headdress", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#FF8F00", Q: "#1565C0" }] },
183
- { id: "sombrero", name: "Sombrero", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "...AQAQA...", "AAAAAAAAAAA"], variants: [{ A: "#F57F17", Q: "#BF360C" }, { A: "#f8b377", Q: "#3e1204" }, { A: "#d6f877", Q: "#04133e" }, { A: "#d6f877", Q: "#04133e" }] },
176
+ { id: "bicorne", name: "Bicorne", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AAAQQQQ..", "..AAAAAAA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#1A237E", Q: "#FFD700" }, { A: "#01052d", Q: "#544803" }], rollable: true },
177
+ { id: "viking_helmet", name: "Viking Helmet", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..A.AAA.A..", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#9E9E9E", Q: "#8D6E63" }, { A: "#dcdbdb", Q: "#bf3908" }], rollable: true },
178
+ { id: "jesters_cap", name: "Jester's Cap", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "...AAAAA...", "...AAAAA..."], variants: [{ A: "#E53935", Q: "#FFD600" }, { A: "#e5a434", Q: "#0040ff" }, { A: "#34e537", Q: "#eeff00" }, { A: "#e234e5", Q: "#00ff40" }], rollable: true },
179
+ { id: "plague_doctor", name: "Plague Doctor Beak", rarity: "rare", width: 11, anchor_x: 24, rows: ["...........", "...........", "...........", "...........", "....QQQ....", "...QAAAQQ..", "...QQQQQQQ.", "..QQQQQ.QQQ", "..QQQQ....Q", "..QQQQ....."], variants: [{ A: "#F5F5F5", Q: "#795548" }, { A: "#e1dbdb", Q: "#080707" }], rollable: true },
180
+ { id: "morion", name: "Conquistador Morion", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....Q.....", "...QQQQQ...", "...QAAAQ...", ".A..AAA..A.", "..AAQQQAA.."], variants: [{ A: "#B0BEC5", Q: "#FFD600" }, { A: "#0a4768", Q: "#e6c10a" }], rollable: true },
181
+ { id: "shako", name: "Shako", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "........Q..", "........Q..", "........Q..", "........Q..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#45464f", Q: "#00ff2a" }, { A: "#45464f", Q: "#ff4d00" }, { A: "#45464f", Q: "#ff0019" }, { A: "#45464f", Q: "#fff700" }], rollable: true },
182
+ { id: "centurion_helm", name: "Centurion Helm", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "....QQQ....", "...QQQQQQ..", "..QQQQQQQQ.", ".QQ..A...Q.", ".Q..AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAA.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#2f3131", Q: "#C62828" }], rollable: true },
183
+ { id: "papal_mitre", name: "Papal Mitre", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....QQQ....", "...QQQQQ...", "...QAAAQ...", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }], rollable: true },
184
+ { id: "headdress", name: "Headdress", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#FF8F00", Q: "#1565C0" }], rollable: true },
185
+ { id: "sombrero", name: "Sombrero", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "...AQAQA...", "AAAAAAAAAAA"], variants: [{ A: "#F57F17", Q: "#BF360C" }, { A: "#f8b377", Q: "#3e1204" }, { A: "#d6f877", Q: "#04133e" }, { A: "#d6f877", Q: "#04133e" }], rollable: true },
184
186
  // ── EPIC (6) ───────────────────────────────────────────────────────────
185
- { id: "papal_tiara", name: "Papal Tiara", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "....QQ.....", "...QQQQQ...", "...QAAQQ...", "...QQQQQ...", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }, { A: "#b18e10", Q: "#f3f3f1" }] },
186
- { id: "samurai_kabuto", name: "Samurai Kabuto", rarity: "epic", width: 11, anchor_x: 23, rows: ["....Q...Q..", "....QQ.QQ..", ".....QQQ...", ".......Q...", "...AAAAQ...", "..AAAAAQQ..", "..AAAAAQQ..", ".AAAAAAAQ..", "AAAAAAAAQ..", "AAAAAAAAQ.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#050505", Q: "#C62828" }, { A: "#675f5f", Q: "#c4c729" }] },
187
- { id: "gladiator_galea", name: "Gladiator Galea", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "....QQQQQQ.", "...QQQQQ...", "...QQQQQ...", "...QAAA....", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#ffe907", Q: "#C62828" }] },
188
- { id: "pharaoh_nemes", name: "Pharaoh Nemes", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "..AAAAAAA..", "..AAQAQAA..", "..QAQAQAQ..", "..QAQAQAQ..", "..QQAAAQQ..", "..QQAAAAA..", "...QAQA...."], variants: [{ A: "#FFD700", Q: "#1565C0" }, { A: "#8d342a", Q: "#63686e" }] },
189
- { id: "spartan_helmet", name: "Spartan Helmet", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "........Q..", ".......QQ..", ".....QQQQ..", "....QQQQQ..", "..QQQQQQ...", ".QQQAA.....", "QQAAAAAAA..", "Q.AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#B0BEC5", Q: "#B71C1C" }, { A: "#B0BEC5", Q: "#b2b51c" }, { A: "#B0BEC5", Q: "#b5611c" }] },
190
- { id: "conquistador_full", name: "Conquistador Helm", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA..", "...AQQQA...", "...AQQQA..."], variants: [{ A: "#B0BEC5", Q: "#FFD700" }, { A: "#B0BEC5", Q: "#0040ff" }, { A: "#B0BEC5", Q: "#36123b" }] },
187
+ { id: "papal_tiara", name: "Papal Tiara", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "....QQ.....", "...QQQQQ...", "...QAAQQ...", "...QQQQQ...", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }, { A: "#b18e10", Q: "#f3f3f1" }], rollable: true },
188
+ { id: "samurai_kabuto", name: "Samurai Kabuto", rarity: "epic", width: 11, anchor_x: 23, rows: ["....Q...Q..", "....QQ.QQ..", ".....QQQ...", ".......Q...", "...AAAAQ...", "..AAAAAQQ..", "..AAAAAQQ..", ".AAAAAAAQ..", "AAAAAAAAQ..", "AAAAAAAAQ.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#050505", Q: "#C62828" }, { A: "#675f5f", Q: "#c4c729" }], rollable: true },
189
+ { id: "gladiator_galea", name: "Gladiator Galea", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "....QQQQQQ.", "...QQQQQ...", "...QQQQQ...", "...QAAA....", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#ffe907", Q: "#C62828" }], rollable: true },
190
+ { id: "pharaoh_nemes", name: "Pharaoh Nemes", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "..AAAAAAA..", "..AAQAQAA..", "..QAQAQAQ..", "..QAQAQAQ..", "..QQAAAQQ..", "..QQAAAAA..", "...QAQA...."], variants: [{ A: "#FFD700", Q: "#1565C0" }, { A: "#8d342a", Q: "#63686e" }], rollable: true },
191
+ { id: "spartan_helmet", name: "Spartan Helmet", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "........Q..", ".......QQ..", ".....QQQQ..", "....QQQQQ..", "..QQQQQQ...", ".QQQAA.....", "QQAAAAAAA..", "Q.AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#B0BEC5", Q: "#B71C1C" }, { A: "#B0BEC5", Q: "#b2b51c" }, { A: "#B0BEC5", Q: "#b5611c" }], rollable: true },
192
+ { id: "conquistador_full", name: "Conquistador Helm", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA..", "...AQQQA...", "...AQQQA..."], variants: [{ A: "#B0BEC5", Q: "#FFD700" }, { A: "#B0BEC5", Q: "#0040ff" }, { A: "#B0BEC5", Q: "#36123b" }], rollable: true },
191
193
  // ── LEGENDARY (5) ──────────────────────────────────────────────────────
192
- { id: "rainbow_crown", name: "Rainbow Crown", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", ".....A.....", "....AAA....", "....AQA....", "....AAA....", "...AAQAA...", "...AAAAA...", "..AAAQAAA..", "..AAAAAAA.."], colors: { A: "#FFD700", Q: "#553f3f" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF"], fps: 8 } },
193
- { id: "inferno_cap", name: "Inferno Cap", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "....AAA....", "...AAAAA...", "....AAA....", ".....Q.....", "A....Q....A", "A...QQQ...A", ".A..QQQ..A.", "..AAAAAAA..", "..AAAAAAA.."], colors: { A: "#FF4500", Q: "#FFD700" }, animation: { type: "cycle", frames: ["#FF0000", "#FF2200", "#FF4500", "#FF6600", "#FF8C00", "#FFA500"], fps: 12 } },
194
- { id: "void_hood", name: "Void Hood", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...AAA.....", "..AAAAAA...", ".AAAAAAA...", ".AAAAAAAA..", "AAAAAAAAA..", "AAAAQQQAA..", "AAAAQQQAA.."], colors: { A: "#1A0033", Q: "#d9cfe3" }, animation: { type: "cycle", frames: ["#0D0019", "#1A0033", "#2D004D", "#3D0066", "#2D004D", "#1A0033"], fps: 3 } },
195
- { id: "prismatic_jester", name: "Prismatic Jester", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "..Q.Q.Q.Q..", "Q..A.A.A..Q", ".Q.A.A.A.Q.", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA.."], colors: { A: "#FF0000", Q: "#0000FF" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF", "#FF00FF", "#00FFFF"], fps: 15 } },
196
- { id: "aurora_helm", name: "Aurora Helm", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...QQQQQ...", "...QQQQQ...", "..AAAAAAA..", "..AAAAAAA..", "..AQAAAQA..", "..AQAAAQA.."], colors: { A: "#00CED1", Q: "#00FF7F" }, animation: { type: "cycle", frames: ["#0000FF", "#0066FF", "#00BFFF", "#00CED1", "#00FF7F", "#7CFC00", "#00FF7F", "#00CED1"], fps: 4 } }
194
+ { id: "rainbow_crown", name: "Rainbow Crown", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", ".....A.....", "....AAA....", "....AQA....", "....AAA....", "...AAQAA...", "...AAAAA...", "..AAAQAAA..", "..AAAAAAA.."], colors: { A: "#FFD700", Q: "#553f3f" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF"], fps: 8 }, rollable: true },
195
+ { id: "inferno_cap", name: "Inferno Cap", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "....AAA....", "...AAAAA...", "....AAA....", ".....Q.....", "A....Q....A", "A...QQQ...A", ".A..QQQ..A.", "..AAAAAAA..", "..AAAAAAA.."], colors: { A: "#FF4500", Q: "#FFD700" }, animation: { type: "cycle", frames: ["#FF0000", "#FF2200", "#FF4500", "#FF6600", "#FF8C00", "#FFA500"], fps: 12 }, rollable: true },
196
+ { id: "void_hood", name: "Void Hood", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...AAA.....", "..AAAAAA...", ".AAAAAAA...", ".AAAAAAAA..", "AAAAAAAAA..", "AAAAQQQAA..", "AAAAQQQAA.."], colors: { A: "#1A0033", Q: "#d9cfe3" }, animation: { type: "cycle", frames: ["#0D0019", "#1A0033", "#2D004D", "#3D0066", "#2D004D", "#1A0033"], fps: 3 }, rollable: true },
197
+ { id: "prismatic_jester", name: "Prismatic Jester", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "..Q.Q.Q.Q..", "Q..A.A.A..Q", ".Q.A.A.A.Q.", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA.."], colors: { A: "#FF0000", Q: "#0000FF" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF", "#FF00FF", "#00FFFF"], fps: 15 }, rollable: true },
198
+ { id: "aurora_helm", name: "Aurora Helm", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...QQQQQ...", "...QQQQQ...", "..AAAAAAA..", "..AAAAAAA..", "..AQAAAQA..", "..AQAAAQA.."], colors: { A: "#00CED1", Q: "#00FF7F" }, animation: { type: "cycle", frames: ["#0000FF", "#0066FF", "#00BFFF", "#00CED1", "#00FF7F", "#7CFC00", "#00FF7F", "#00CED1"], fps: 4 }, rollable: true },
199
+ // Claim-only: awarded by an admin, never from a roll. Only the A channel cycles,
200
+ // so the inset motif shifts colour while the cap body stays a static rust brown.
201
+ // The body colour is kept out of the frame list so the motif never merges into it.
202
+ { id: "contributor_cap", name: "Contributor Cap", rarity: "legendary", width: 11, anchor_x: 23, rows: [".....Q.....", "....QQQ....", "...QQQQQ...", "..QAAAQQQ..", "..QQAQQQQ..", ".QQQAQAAQQ.", ".QQQAQAQAQ.", ".QQQQQAQAQ.", ".QQQQQAAQQ.", ".QQQQQQQQQ."], colors: { A: "#21138b", Q: "#6f2801" }, animation: { type: "cycle", frames: ["#FF8C42", "#E0457B", "#8B5CF6", "#3B82F6", "#22D3EE", "#34D399", "#A3E635", "#FBBF24"], fps: 6 }, rollable: false }
197
203
  ];
198
204
 
199
205
  // ../shared/dist/series-transform.js
@@ -285,6 +291,22 @@ var STAMINA_PARAM_BOUNDS = {
285
291
  tired_multiplier: { min: 0.2, max: 0.9, step: 0.05, default: STAMINA.TIRED_MULTIPLIER }
286
292
  };
287
293
 
294
+ // ../shared/dist/devices.js
295
+ var DEVICE_CODE_LENGTH = Math.ceil(SECRET_TOKEN_BYTES * 4 / 3);
296
+ var UNSAFE_LABEL_CHARS = /[\p{Cc}\p{Cf}]/u;
297
+ function validateDeviceLabel(raw) {
298
+ if (typeof raw !== "string")
299
+ return { ok: false, message: "label is required" };
300
+ const label = raw.trim();
301
+ if (label.length < 1 || label.length > DEVICE_LABEL_MAX_LENGTH) {
302
+ return { ok: false, message: `label must be 1\u2013${DEVICE_LABEL_MAX_LENGTH} characters` };
303
+ }
304
+ if (UNSAFE_LABEL_CHARS.test(label)) {
305
+ return { ok: false, message: "label may not contain control or invisible characters" };
306
+ }
307
+ return { ok: true, label };
308
+ }
309
+
288
310
  // src/ui/HorseSprite.tsx
289
311
  import { Box, Text } from "ink";
290
312
 
@@ -821,8 +843,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
821
843
  // src/version.ts
822
844
  import { createRequire } from "module";
823
845
  function readVersion() {
824
- if ("2.13.0".length > 0) {
825
- return "2.13.0";
846
+ if ("3.0.0".length > 0) {
847
+ return "3.0.0";
826
848
  }
827
849
  try {
828
850
  const req = createRequire(import.meta.url);
@@ -867,18 +889,37 @@ function geminiTmpDir() {
867
889
  }
868
890
 
869
891
  // src/identity/identity.ts
870
- async function loadIdentity() {
892
+ async function readIdentityFile() {
893
+ let raw;
871
894
  try {
872
- const raw = await fs.readFile(identityFile(), "utf8");
873
- const parsed = JSON.parse(raw);
874
- if (typeof parsed.user_id === "string" && typeof parsed.display_name === "string" && typeof parsed.secret_token === "string" && typeof parsed.created_at === "string") {
875
- return parsed;
876
- }
877
- return null;
895
+ raw = await fs.readFile(identityFile(), "utf8");
878
896
  } catch (e) {
879
- if (e?.code === "ENOENT") return null;
880
- return null;
897
+ if (e?.code === "ENOENT") return { kind: "missing" };
898
+ return { kind: "unreadable", reason: `could not be read (${e?.code ?? "read error"})` };
899
+ }
900
+ let parsed;
901
+ try {
902
+ parsed = JSON.parse(raw);
903
+ } catch {
904
+ return { kind: "unreadable", reason: "is not valid JSON" };
905
+ }
906
+ if (!isPlainObject(parsed)) {
907
+ return { kind: "unreadable", reason: "does not contain an identity object" };
908
+ }
909
+ if (typeof parsed.user_id === "string" && typeof parsed.display_name === "string" && typeof parsed.secret_token === "string" && typeof parsed.created_at === "string") {
910
+ return { kind: "ok", identity: parsed };
911
+ }
912
+ if (!("secret_token" in parsed)) {
913
+ return { kind: "no-credential", reason: "holds no credential this version can use" };
881
914
  }
915
+ return { kind: "unreadable", reason: "is missing fields this version expects" };
916
+ }
917
+ function isPlainObject(value) {
918
+ return typeof value === "object" && value !== null && !Array.isArray(value);
919
+ }
920
+ async function loadIdentity() {
921
+ const state = await readIdentityFile();
922
+ return state.kind === "ok" ? state.identity : null;
882
923
  }
883
924
  async function saveIdentity(identity) {
884
925
  await fs.mkdir(homeDir(), { recursive: true });
@@ -919,12 +960,12 @@ function getIdentity() {
919
960
  function _resetIdentityCacheForTests() {
920
961
  identityCache = null;
921
962
  }
922
- async function request(method, path9, body, horseAuthToken, fetchImpl = fetch) {
963
+ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, identityOverride) {
923
964
  const url = path9.startsWith("http") ? path9 : `${apiBase()}${path9}`;
924
965
  const headers = {};
925
966
  headers[CLI_VERSION_HEADER] = CLI_VERSION;
926
967
  headers["user-agent"] = `token-derby/${CLI_VERSION}`;
927
- const identity = await getIdentity();
968
+ const identity = identityOverride ?? await getIdentity();
928
969
  if (identity) {
929
970
  headers[USER_ID_HEADER] = identity.user_id;
930
971
  headers[USER_TOKEN_HEADER] = identity.secret_token;
@@ -990,6 +1031,9 @@ function listOrganisations() {
990
1031
  function initJockey(body) {
991
1032
  return request("POST", "/jockey/init", body, void 0);
992
1033
  }
1034
+ function getJockey() {
1035
+ return request("GET", "/jockey/me", void 0, void 0);
1036
+ }
993
1037
  function updateJockey(body) {
994
1038
  return request("PUT", "/jockey/me", body, void 0);
995
1039
  }
@@ -1024,6 +1068,34 @@ function equipHat(stableHorseId, body) {
1024
1068
  function createWebSession() {
1025
1069
  return request("POST", "/web-sessions", void 0, void 0);
1026
1070
  }
1071
+ function probeClaim(code) {
1072
+ return request("GET", `/claims/${encodeURIComponent(code)}`, void 0, void 0);
1073
+ }
1074
+ function redeemClaim(code, body) {
1075
+ return request("POST", `/claims/${encodeURIComponent(code)}/redeem`, body, void 0);
1076
+ }
1077
+ function cliAuthStart(body) {
1078
+ return request("POST", "/auth/cli/start", body, void 0);
1079
+ }
1080
+ function cliAuthPoll(body) {
1081
+ return request("POST", "/auth/cli/poll", body, void 0);
1082
+ }
1083
+ function revokeDevice(deviceId, auth) {
1084
+ return request(
1085
+ "DELETE",
1086
+ `/devices/${encodeURIComponent(deviceId)}`,
1087
+ void 0,
1088
+ void 0,
1089
+ void 0,
1090
+ auth
1091
+ );
1092
+ }
1093
+ function registerDevice(body) {
1094
+ return request("POST", "/devices", body, void 0);
1095
+ }
1096
+ function logoutDevice() {
1097
+ return request("DELETE", "/devices/me", void 0, void 0);
1098
+ }
1027
1099
 
1028
1100
  // src/commands/stable-create.ts
1029
1101
  async function stableCreateCommand() {
@@ -1163,7 +1235,7 @@ import { render as render3 } from "ink";
1163
1235
  import { useState as useState3 } from "react";
1164
1236
  import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
1165
1237
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1166
- function HorsePicker({ horses, onPick, onCancel }) {
1238
+ function HorsePicker({ horses, onPick, onCancel, prompt = "Pick a horse to race:" }) {
1167
1239
  const [idx, setIdx] = useState3(0);
1168
1240
  useInput2((input, key) => {
1169
1241
  if (key.escape) {
@@ -1191,7 +1263,7 @@ function HorsePicker({ horses, onPick, onCancel }) {
1191
1263
  ] });
1192
1264
  }
1193
1265
  return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
1194
- /* @__PURE__ */ jsx5(Text5, { children: "Pick a horse to race:" }),
1266
+ /* @__PURE__ */ jsx5(Text5, { children: prompt }),
1195
1267
  horses.map((h, i) => /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
1196
1268
  /* @__PURE__ */ jsx5(Box5, { flexDirection: "row", children: /* @__PURE__ */ jsxs3(Text5, { children: [
1197
1269
  i === idx ? "\u25BA" : " ",
@@ -2451,7 +2523,7 @@ async function joinCommand(joinCode, argv = []) {
2451
2523
  }
2452
2524
  const identity = await loadIdentity();
2453
2525
  if (!identity) {
2454
- console.error("Run `token-derby init` to set up your identity.");
2526
+ console.error("Run `token-derby login` to set up your identity.");
2455
2527
  return 1;
2456
2528
  }
2457
2529
  let race;
@@ -2624,210 +2696,814 @@ async function endCommand(adminCode) {
2624
2696
  // src/commands/init.ts
2625
2697
  import * as readline4 from "readline/promises";
2626
2698
  import { stdin as stdin4, stdout as stdout4 } from "process";
2627
- async function initCommand(reset = false) {
2628
- if (reset) {
2629
- await deleteIdentity();
2630
- _resetIdentityCacheForTests();
2631
- console.log("Removed local identity. Creating a new one\u2026");
2632
- }
2633
- const existing = await loadIdentity();
2699
+ async function defaultPromptText(question) {
2634
2700
  const rl = readline4.createInterface({ input: stdin4, output: stdout4 });
2635
2701
  try {
2636
- if (existing) {
2637
- console.log(`Current jockey name: ${existing.display_name}`);
2638
- const raw2 = (await rl.question("New jockey name (use your real name please) [keep]: ")).trim();
2639
- if (!raw2) {
2640
- console.log("Kept existing name.");
2641
- return 0;
2702
+ return await rl.question(question);
2703
+ } finally {
2704
+ rl.close();
2705
+ }
2706
+ }
2707
+ async function initCommand(reset = false, deps = {}) {
2708
+ const loadIdentity2 = deps.loadIdentity ?? loadIdentity;
2709
+ const saveIdentity2 = deps.saveIdentity ?? saveIdentity;
2710
+ const deleteIdentity2 = deps.deleteIdentity ?? deleteIdentity;
2711
+ const readIdentityFile2 = deps.readIdentityFile ?? readIdentityFile;
2712
+ const initJockey2 = deps.initJockey ?? initJockey;
2713
+ const updateJockey2 = deps.updateJockey ?? updateJockey;
2714
+ const apiGetJockey = deps.apiGetJockey ?? getJockey;
2715
+ const promptText = deps.promptText ?? defaultPromptText;
2716
+ const isTTY = deps.isTTY ?? Boolean(process.stdin.isTTY);
2717
+ console.log("");
2718
+ console.log(
2719
+ "Heads up: `token-derby login` is becoming the only way to manage accounts on this machine. `init` still works today, but plan to move over."
2720
+ );
2721
+ console.log("");
2722
+ if (reset) {
2723
+ const current = await readIdentityFile2();
2724
+ if (current.kind !== "missing") {
2725
+ if (current.kind === "ok") {
2726
+ console.log(`About to abandon jockey: ${current.identity.display_name}`);
2727
+ } else if (current.kind === "unreadable") {
2728
+ console.log(`About to delete identity.json, which ${current.reason}.`);
2729
+ console.log("The credential inside cannot be read, so there is no way to tell which jockey");
2730
+ console.log("this is, or whether anything else can still recover it.");
2731
+ } else {
2732
+ console.log(`About to delete identity.json, which ${current.reason}.`);
2733
+ }
2734
+ console.log("This deletes the local identity only \u2014 the account itself stays on the server.");
2735
+ if (current.kind === "ok") {
2736
+ try {
2737
+ const me = await apiGetJockey();
2738
+ if (me.device_label) {
2739
+ console.log(` This machine's credential, "${me.device_label}", will stay live on the`);
2740
+ console.log(" abandoned account and can only be revoked from the Account view in the org manager.");
2741
+ }
2742
+ if (me.email) {
2743
+ console.log(" This account has a Google account linked \u2014 `token-derby login` would recover");
2744
+ console.log(" this same jockey instead of abandoning it.");
2745
+ }
2746
+ } catch {
2747
+ }
2642
2748
  }
2643
- const v2 = validateDisplayName(raw2);
2644
- if (!v2.ok) {
2645
- console.error(v2.error);
2749
+ if (!isTTY) {
2750
+ console.error("");
2751
+ console.error("Refusing to reset without a terminal to confirm \u2014 this is irreversible.");
2752
+ console.error(
2753
+ "Run `token-derby init --reset` interactively, or use `token-derby login` to recover this jockey instead."
2754
+ );
2646
2755
  return 1;
2647
2756
  }
2648
- try {
2649
- const resp = await updateJockey({ display_name: v2.name });
2650
- const updated = { ...existing, display_name: resp.display_name };
2651
- await saveIdentity(updated);
2652
- console.log(`Updated jockey name to: ${updated.display_name}`);
2757
+ const answer = (await promptText('Type "yes" to abandon this jockey [no]: ')).trim().toLowerCase();
2758
+ if (answer !== "yes") {
2759
+ console.log("Reset cancelled. Nothing was deleted.");
2653
2760
  return 0;
2654
- } catch (e) {
2655
- if (e instanceof ApiError) {
2656
- if (e.code === "UNAUTHENTICATED") {
2657
- console.error(
2658
- "Server does not recognise this identity. Your account may have been wiped. Run `token-derby init --reset` to start fresh."
2659
- );
2660
- } else {
2661
- console.error(`Error: ${e.code} ${e.message}`);
2662
- }
2663
- return 1;
2664
- }
2665
- throw e;
2666
2761
  }
2667
2762
  }
2668
- const raw = (await rl.question("Jockey Name (use your real name please): ")).trim();
2669
- const v = validateDisplayName(raw);
2670
- if (!v.ok) {
2671
- console.error(v.error);
2763
+ await deleteIdentity2();
2764
+ _resetIdentityCacheForTests();
2765
+ console.log("Removed local identity. Creating a new one\u2026");
2766
+ }
2767
+ if (!reset) {
2768
+ const state = await readIdentityFile2();
2769
+ if (state.kind === "unreadable") {
2770
+ console.error(`Your local identity file ${state.reason}.`);
2771
+ console.error("It may hold a credential that cannot be recovered, so this will not overwrite it.");
2772
+ console.error("Run `token-derby login` to sign in fresh, or `token-derby init --reset` to discard it.");
2773
+ return 1;
2774
+ }
2775
+ if (state.kind === "no-credential") {
2776
+ console.log(`Your local identity file ${state.reason}, so this will replace it.`);
2777
+ }
2778
+ }
2779
+ const existing = await loadIdentity2();
2780
+ if (existing) {
2781
+ console.log(`Current jockey name: ${existing.display_name}`);
2782
+ const raw2 = (await promptText("New jockey name (use your real name please) [keep]: ")).trim();
2783
+ if (!raw2) {
2784
+ console.log("Kept existing name.");
2785
+ return 0;
2786
+ }
2787
+ const v2 = validateDisplayName(raw2);
2788
+ if (!v2.ok) {
2789
+ console.error(v2.error);
2672
2790
  return 1;
2673
2791
  }
2674
2792
  try {
2675
- const resp = await initJockey({ display_name: v.name });
2676
- const identity = {
2677
- user_id: resp.user_id,
2678
- display_name: resp.display_name,
2679
- secret_token: resp.secret_token,
2680
- created_at: (/* @__PURE__ */ new Date()).toISOString()
2681
- };
2682
- await saveIdentity(identity);
2683
- _resetIdentityCacheForTests();
2684
- console.log("");
2685
- console.log(`Welcome, ${identity.display_name}!`);
2686
- console.log("Your identity has been created on the server.");
2687
- console.log("You can now create a stable and join races.");
2688
- console.log("");
2689
- console.log(" \u26A0 Your secret token is stored locally in identity.json.");
2690
- console.log(" If you lose it, you cannot recover this account \u2014 you would");
2691
- console.log(" need to run `token-derby init --reset` and rebuild your stable.");
2793
+ const resp = await updateJockey2({ display_name: v2.name });
2794
+ const updated = { ...existing, display_name: resp.display_name };
2795
+ await saveIdentity2(updated);
2796
+ console.log(`Updated jockey name to: ${updated.display_name}`);
2692
2797
  return 0;
2693
2798
  } catch (e) {
2694
2799
  if (e instanceof ApiError) {
2695
- console.error(`Error: ${e.code} ${e.message}`);
2800
+ if (e.code === "UNAUTHENTICATED") {
2801
+ console.error(
2802
+ "Server does not recognise this identity. Your account may have been wiped. Run `token-derby init --reset` to start fresh."
2803
+ );
2804
+ } else {
2805
+ console.error(`Error: ${e.code} ${e.message}`);
2806
+ }
2696
2807
  return 1;
2697
2808
  }
2698
2809
  throw e;
2699
2810
  }
2700
- } finally {
2701
- rl.close();
2811
+ }
2812
+ const raw = (await promptText("Jockey Name (use your real name please): ")).trim();
2813
+ const v = validateDisplayName(raw);
2814
+ if (!v.ok) {
2815
+ console.error(v.error);
2816
+ return 1;
2817
+ }
2818
+ try {
2819
+ const resp = await initJockey2({ display_name: v.name });
2820
+ const identity = {
2821
+ user_id: resp.user_id,
2822
+ display_name: resp.display_name,
2823
+ secret_token: resp.secret_token,
2824
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2825
+ };
2826
+ await saveIdentity2(identity);
2827
+ _resetIdentityCacheForTests();
2828
+ console.log("");
2829
+ console.log(`Welcome, ${identity.display_name}!`);
2830
+ console.log("Your identity has been created on the server.");
2831
+ console.log("You can now create a stable and join races.");
2832
+ console.log("");
2833
+ console.log(" \u26A0 Your secret token is stored locally in identity.json.");
2834
+ console.log(" If you lose it, you cannot recover this account \u2014 you would");
2835
+ console.log(" need to run `token-derby init --reset` and rebuild your stable.");
2836
+ return 0;
2837
+ } catch (e) {
2838
+ if (e instanceof ApiError) {
2839
+ console.error(`Error: ${e.code} ${e.message}`);
2840
+ return 1;
2841
+ }
2842
+ throw e;
2702
2843
  }
2703
2844
  }
2704
2845
 
2705
- // src/commands/update.ts
2706
- import * as readline5 from "readline/promises";
2707
- import { stdin as stdin5, stdout as stdout5 } from "process";
2846
+ // src/commands/login.ts
2847
+ import { spawn as spawn2 } from "child_process";
2848
+ import * as os3 from "os";
2849
+
2850
+ // src/commands/web.ts
2708
2851
  import { spawn } from "child_process";
2709
- var REGISTRY_URL = "https://registry.npmjs.org/@mauricode/token-derby/latest";
2710
- var UPGRADE_CMD = "npm install -g @mauricode/token-derby@latest";
2711
- var FETCH_TIMEOUT_MS = 5e3;
2712
- async function updateCommand(deps = {}) {
2713
- const fetchImpl = deps.fetchImpl ?? fetch;
2852
+ function webOrigin() {
2853
+ return apiBase().replace(/\/api\/?$/, "");
2854
+ }
2855
+ function opener() {
2856
+ if (process.platform === "darwin") return "open";
2857
+ if (process.platform === "win32") return "start";
2858
+ if (process.platform === "linux") return "xdg-open";
2859
+ return null;
2860
+ }
2861
+ async function webCommand(deps = {}) {
2714
2862
  const spawnImpl = deps.spawnImpl ?? spawn;
2715
- const promptYesNo2 = deps.promptYesNo ?? defaultPromptYesNo;
2716
- let latest;
2863
+ let code;
2717
2864
  try {
2718
- latest = await fetchLatestVersion(fetchImpl);
2865
+ ({ code } = await createWebSession());
2719
2866
  } catch (e) {
2720
- console.error(`Could not reach the npm registry${e?.message ? ` (${e.message})` : ""}.`);
2721
- console.error(`To upgrade manually: ${UPGRADE_CMD}`);
2722
- return 1;
2867
+ if (e instanceof ApiError) {
2868
+ console.error(`Error: ${e.code} ${e.message}`);
2869
+ return 1;
2870
+ }
2871
+ throw e;
2723
2872
  }
2724
- if (gteSemver(CLI_VERSION, latest)) {
2725
- console.log(`You're on the latest version (${CLI_VERSION}).`);
2726
- return 0;
2873
+ const url = `${webOrigin()}/org-manager#code=${code}`;
2874
+ console.log("");
2875
+ console.log(" Opening the Token Derby org manager in your browser...");
2876
+ console.log(` ${url}`);
2877
+ console.log("");
2878
+ console.log(" If it doesn't open, copy the link above. It expires in 60 seconds.");
2879
+ const cmd = opener();
2880
+ if (cmd) {
2881
+ try {
2882
+ const child = spawnImpl(cmd, [url], { stdio: "ignore", detached: true });
2883
+ child.on("error", () => {
2884
+ });
2885
+ child.unref();
2886
+ } catch {
2887
+ }
2727
2888
  }
2728
- console.log(`Current: ${CLI_VERSION} Latest: ${latest}`);
2729
- const yes = await promptYesNo2("Run upgrade now? [y/N]: ");
2730
- if (!yes) {
2731
- console.log(`To upgrade manually: ${UPGRADE_CMD}`);
2732
- return 0;
2889
+ return 0;
2890
+ }
2891
+
2892
+ // src/ui/prompt.ts
2893
+ async function promptYesNo(question) {
2894
+ resetStdinAfterInk();
2895
+ const readline6 = await import("readline/promises");
2896
+ const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
2897
+ const a = (await rl.question(question)).trim().toLowerCase();
2898
+ rl.close();
2899
+ return a === "" || a === "y" || a === "yes";
2900
+ }
2901
+ function resetStdinAfterInk() {
2902
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
2903
+ process.stdin.setRawMode(false);
2733
2904
  }
2734
- const code = await runNpmUpgrade(spawnImpl);
2735
- if (code === 0) {
2736
- console.log(`
2737
- Upgraded to ${latest}.`);
2738
- console.log(`If \`token-derby --version\` still shows ${CLI_VERSION}, your shell has`);
2739
- console.log(`the old path cached. Drop the cache by running ONE of:`);
2740
- console.log(` exec $SHELL (any shell \u2014 replaces this shell with a fresh one)`);
2741
- console.log(` rehash (zsh)`);
2742
- console.log(` hash -r (bash)`);
2743
- console.log(`Or just open a new terminal tab.`);
2744
- console.log(`If --version still doesn't match after that, you may have multiple`);
2745
- console.log(`installs \u2014 run \`which -a token-derby\` to compare against \`npm bin -g\`.`);
2905
+ while (process.stdin.read() !== null) {
2746
2906
  }
2747
- return code;
2907
+ process.stdin.pause();
2908
+ process.stdin.ref();
2748
2909
  }
2749
- async function fetchLatestVersion(fetchImpl) {
2750
- const controller = new AbortController();
2751
- const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2752
- try {
2753
- const res = await fetchImpl(REGISTRY_URL, { signal: controller.signal });
2754
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
2755
- const body = await res.json();
2756
- if (typeof body.version !== "string" || !/^\d+\.\d+\.\d+/.test(body.version)) {
2757
- throw new Error("unexpected registry response");
2758
- }
2759
- return body.version;
2760
- } finally {
2761
- clearTimeout(t);
2910
+
2911
+ // src/commands/login.ts
2912
+ function parseDeviceNameFlag(argv) {
2913
+ for (let i = 0; i < argv.length; i++) {
2914
+ const a = argv[i];
2915
+ if (a === "--device-name") return argv[i + 1] ?? null;
2916
+ if (a.startsWith("--device-name=")) return a.slice("--device-name=".length);
2762
2917
  }
2918
+ return null;
2763
2919
  }
2764
- async function defaultPromptYesNo(question) {
2765
- const rl = readline5.createInterface({ input: stdin5, output: stdout5 });
2920
+ async function defaultPromptText2(question) {
2921
+ const readline6 = await import("readline/promises");
2922
+ const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
2766
2923
  try {
2767
- const answer = (await rl.question(question)).trim().toLowerCase();
2768
- return answer === "y" || answer === "yes";
2924
+ return await rl.question(question);
2769
2925
  } finally {
2770
2926
  rl.close();
2771
2927
  }
2772
2928
  }
2773
- function runNpmUpgrade(spawnImpl) {
2774
- return new Promise((resolve) => {
2775
- const child = spawnImpl("npm", ["install", "-g", "@mauricode/token-derby@latest"], {
2776
- stdio: "inherit"
2777
- });
2778
- child.on("error", (e) => {
2779
- if (e.code === "ENOENT") {
2780
- console.error("Could not find `npm` on PATH.");
2781
- console.error(`To upgrade manually: ${UPGRADE_CMD}`);
2782
- } else {
2783
- console.error(`npm failed to start: ${e.message}`);
2929
+ function defaultSleep(ms) {
2930
+ return new Promise((resolve) => setTimeout(resolve, ms));
2931
+ }
2932
+ async function resolveDeviceName(flagName, isTTY, hostname3, promptText) {
2933
+ if (flagName) return flagName;
2934
+ if (!isTTY) return hostname3;
2935
+ const answer = (await promptText(`Device name [${hostname3}]: `)).trim();
2936
+ return answer || hostname3;
2937
+ }
2938
+ async function loginCommand(argv = [], deps = {}) {
2939
+ const apiStart = deps.apiStart ?? cliAuthStart;
2940
+ const apiPoll = deps.apiPoll ?? cliAuthPoll;
2941
+ const apiRevokeDevice = deps.apiRevokeDevice ?? revokeDevice;
2942
+ const apiCreateWebSession = deps.apiCreateWebSession ?? createWebSession;
2943
+ const saveIdentity2 = deps.saveIdentity ?? saveIdentity;
2944
+ const loadIdentity2 = deps.loadIdentity ?? loadIdentity;
2945
+ const promptText = deps.promptText ?? defaultPromptText2;
2946
+ const promptYesNo2 = deps.promptYesNo ?? promptYesNo;
2947
+ const sleepImpl = deps.sleepImpl ?? defaultSleep;
2948
+ const isTTY = deps.isTTY ?? Boolean(process.stdin.isTTY);
2949
+ const spawnImpl = deps.spawnImpl ?? spawn2;
2950
+ const hostname3 = deps.hostname ?? (() => os3.hostname());
2951
+ const existing = await loadIdentity2();
2952
+ if (existing) {
2953
+ console.log("");
2954
+ console.log(` This machine is already signed in as ${existing.display_name}.`);
2955
+ console.log(" Signing in again adds a second credential; the current one stays active");
2956
+ console.log(" until you revoke it under Account in the org manager (`token-derby web`).");
2957
+ if (isTTY) {
2958
+ if (!await promptYesNo2(" Sign in again anyway? [Y/n] ")) {
2959
+ console.log("Login cancelled.");
2960
+ return 0;
2784
2961
  }
2785
- resolve(1);
2786
- });
2787
- child.on("exit", (code) => resolve(code ?? 1));
2788
- });
2789
- }
2790
-
2791
- // src/commands/roll.ts
2792
- import React13 from "react";
2793
- import { render as render5 } from "ink";
2794
-
2795
- // src/ui/RollReveal.tsx
2796
- import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
2797
- import { Box as Box10, Text as Text10 } from "ink";
2798
-
2799
- // src/ui/HatSprite.tsx
2800
- import { useEffect as useEffect3, useState as useState6 } from "react";
2801
- import { Box as Box9, Text as Text9 } from "ink";
2802
- import { jsx as jsx9 } from "react/jsx-runtime";
2803
- function HatSprite({ hat, variant, centerIn }) {
2804
- const colors = hatColorsFor2(hat, variant ?? 0);
2805
- const grid = makeHatGrid(hat, colors, centerIn);
2806
- const lines = hexGridToHalfBlocks(grid);
2807
- return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
2808
- }
2809
- function AnimatedHatSprite({ hat, variant, centerIn }) {
2810
- if (hat.rarity !== "legendary") {
2811
- return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
2962
+ } else {
2963
+ console.log(" No TTY to confirm \u2014 continuing (equivalent to answering Y).");
2964
+ }
2812
2965
  }
2813
- const frames = hat.animation.frames;
2814
- const fps = hat.animation.fps;
2815
- const [idx, setIdx] = useState6(0);
2816
- useEffect3(() => {
2817
- if (frames.length <= 1) return;
2818
- const interval = setInterval(
2819
- () => setIdx((i) => (i + 1) % frames.length),
2820
- Math.max(1, Math.round(1e3 / fps))
2821
- );
2822
- return () => clearInterval(interval);
2823
- }, [frames.length, fps]);
2824
- const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
2825
- return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
2826
- }
2827
- function hatColorsFor2(hat, variantIdx) {
2828
- if (hat.rarity === "legendary") return hat.colors;
2829
- return hat.variants[variantIdx] ?? hat.variants[0];
2830
- }
2966
+ let label = await resolveDeviceName(parseDeviceNameFlag(argv), isTTY, hostname3(), promptText);
2967
+ let start;
2968
+ for (; ; ) {
2969
+ try {
2970
+ start = await apiStart({ label });
2971
+ break;
2972
+ } catch (e) {
2973
+ if (e instanceof ApiError && e.code === "BAD_REQUEST") {
2974
+ console.error(`Error: ${e.message}`);
2975
+ if (!isTTY) return 1;
2976
+ const retry = (await promptText("Enter a different device name: ")).trim();
2977
+ if (!retry) {
2978
+ console.error("Device name cannot be empty.");
2979
+ return 1;
2980
+ }
2981
+ label = retry;
2982
+ continue;
2983
+ }
2984
+ if (e instanceof ApiError) {
2985
+ console.error(`Error: ${e.code} ${e.message}`);
2986
+ return 1;
2987
+ }
2988
+ throw e;
2989
+ }
2990
+ }
2991
+ let verificationUrl = start.verification_uri;
2992
+ let hasGrant = false;
2993
+ if (existing) {
2994
+ try {
2995
+ const grant = await apiCreateWebSession();
2996
+ verificationUrl = `${start.verification_uri}#code=${grant.code}`;
2997
+ hasGrant = true;
2998
+ } catch (e) {
2999
+ if (e instanceof ApiError && e.code === "UNAUTHENTICATED") {
3000
+ console.log("");
3001
+ console.log(" The credential stored on this machine is no longer valid (it may have been");
3002
+ console.log(" revoked), so this will sign you in fresh in the browser.");
3003
+ } else if (e instanceof ApiError) {
3004
+ console.error(`Error: ${e.code} ${e.message}`);
3005
+ return 1;
3006
+ } else {
3007
+ throw e;
3008
+ }
3009
+ }
3010
+ }
3011
+ console.log("");
3012
+ console.log(" To finish signing in, visit:");
3013
+ console.log(` ${verificationUrl}`);
3014
+ if (hasGrant) {
3015
+ console.log(" That link signs the browser in for you and expires in 60 seconds \u2014 if it");
3016
+ console.log(" does, run `token-derby login` again for a fresh one.");
3017
+ }
3018
+ console.log("");
3019
+ console.log(" And enter this code:");
3020
+ console.log(` ${start.user_code}`);
3021
+ console.log("");
3022
+ console.log(" Waiting for approval...");
3023
+ const cmd = opener();
3024
+ if (cmd) {
3025
+ try {
3026
+ const child = spawnImpl(cmd, [verificationUrl], { stdio: "ignore", detached: true });
3027
+ child.on("error", () => {
3028
+ });
3029
+ child.unref();
3030
+ } catch {
3031
+ }
3032
+ }
3033
+ const deadline = Date.now() + start.expires_in * 1e3;
3034
+ let approved = null;
3035
+ while (!approved) {
3036
+ let poll;
3037
+ try {
3038
+ poll = await apiPoll({ device_code: start.device_code });
3039
+ } catch (e) {
3040
+ if (e instanceof ApiError) {
3041
+ console.error(`Error: ${e.code} ${e.message}`);
3042
+ return 1;
3043
+ }
3044
+ throw e;
3045
+ }
3046
+ if (poll.status === "approved") {
3047
+ approved = poll;
3048
+ break;
3049
+ }
3050
+ if (Date.now() >= deadline) {
3051
+ console.error("Login timed out waiting for approval. Run `token-derby login` again.");
3052
+ return 1;
3053
+ }
3054
+ await sleepImpl(start.interval * 1e3);
3055
+ }
3056
+ console.log("");
3057
+ if (approved.email) console.log(` \u2713 Signed in as ${approved.email}`);
3058
+ console.log(
3059
+ ` Linking to your existing jockey: ${approved.display_name} (${approved.horses} horses, ${approved.orgs} orgs)`
3060
+ );
3061
+ let proceed;
3062
+ if (isTTY) {
3063
+ proceed = await promptYesNo2(" Link this account? [Y/n] ");
3064
+ } else {
3065
+ console.log(" No TTY to confirm \u2014 proceeding automatically (equivalent to answering Y).");
3066
+ proceed = true;
3067
+ }
3068
+ if (!proceed) {
3069
+ try {
3070
+ await apiRevokeDevice(approved.device_id, { user_id: approved.user_id, secret_token: approved.secret_token });
3071
+ } catch (e) {
3072
+ console.error("Declined, but could not revoke the device credential on the server.");
3073
+ if (e instanceof ApiError) console.error(`Error: ${e.code} ${e.message}`);
3074
+ return 1;
3075
+ }
3076
+ console.log("Login cancelled.");
3077
+ return 0;
3078
+ }
3079
+ const identity = {
3080
+ user_id: approved.user_id,
3081
+ display_name: approved.display_name,
3082
+ secret_token: approved.secret_token,
3083
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3084
+ };
3085
+ await saveIdentity2(identity);
3086
+ console.log("");
3087
+ console.log(`Welcome back, ${identity.display_name}!`);
3088
+ return 0;
3089
+ }
3090
+
3091
+ // src/commands/logout.ts
3092
+ async function logoutCommand(deps = {}) {
3093
+ const apiLogoutDevice = deps.apiLogoutDevice ?? logoutDevice;
3094
+ const deleteIdentity2 = deps.deleteIdentity ?? deleteIdentity;
3095
+ try {
3096
+ const result = await apiLogoutDevice();
3097
+ if (result.revoked) {
3098
+ console.log("Revoked this device's credential on the server.");
3099
+ } else {
3100
+ console.log(
3101
+ "This machine is signed in with a legacy account credential (never linked via `token-derby login`), so there is no separate device credential to revoke."
3102
+ );
3103
+ }
3104
+ } catch (e) {
3105
+ if (e instanceof ApiError && e.code === "UNAUTHENTICATED") {
3106
+ console.error("This device credential was already invalid on the server \u2014 clearing it locally too.");
3107
+ await deleteIdentity2();
3108
+ console.log("Removed local identity. You are signed out.");
3109
+ return 0;
3110
+ }
3111
+ if (e instanceof ApiError) {
3112
+ console.error(`Error: ${e.code} ${e.message}`);
3113
+ console.error("Local identity left in place \u2014 run `token-derby logout` again to retry.");
3114
+ return 1;
3115
+ }
3116
+ throw e;
3117
+ }
3118
+ await deleteIdentity2();
3119
+ console.log("Removed local identity. You are signed out.");
3120
+ return 0;
3121
+ }
3122
+
3123
+ // src/commands/link.ts
3124
+ import { spawn as spawn3 } from "child_process";
3125
+ import * as os4 from "os";
3126
+
3127
+ // src/ui/messages.ts
3128
+ var CREDENTIAL_DEAD_MESSAGE = "This machine's credential is no longer valid on the server \u2014 it may have been revoked.\nRun `token-derby login` to sign this machine in again.";
3129
+
3130
+ // src/commands/link.ts
3131
+ function defaultSleep2(ms) {
3132
+ return new Promise((resolve) => setTimeout(resolve, ms));
3133
+ }
3134
+ async function defaultPromptText3(question) {
3135
+ const readline6 = await import("readline/promises");
3136
+ const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
3137
+ try {
3138
+ return await rl.question(question);
3139
+ } finally {
3140
+ rl.close();
3141
+ }
3142
+ }
3143
+ var POLL_INTERVAL_MS = 2e3;
3144
+ var WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
3145
+ async function linkCommand(argv = [], deps = {}) {
3146
+ const apiGetJockey = deps.apiGetJockey ?? getJockey;
3147
+ const apiCreateWebSession = deps.apiCreateWebSession ?? createWebSession;
3148
+ const apiRegisterDevice = deps.apiRegisterDevice ?? registerDevice;
3149
+ const loadIdentity2 = deps.loadIdentity ?? loadIdentity;
3150
+ const saveIdentity2 = deps.saveIdentity ?? saveIdentity;
3151
+ const spawnImpl = deps.spawnImpl ?? spawn3;
3152
+ const sleepImpl = deps.sleepImpl ?? defaultSleep2;
3153
+ const promptText = deps.promptText ?? defaultPromptText3;
3154
+ const isTTY = deps.isTTY ?? Boolean(process.stdin.isTTY);
3155
+ const hostname3 = deps.hostname ?? (() => os4.hostname());
3156
+ const flagName = parseDeviceNameFlag(argv);
3157
+ if (flagName !== null) {
3158
+ const checked = validateDeviceLabel(flagName);
3159
+ if (!checked.ok) {
3160
+ console.error(`Error: invalid --device-name: ${checked.message}`);
3161
+ return 1;
3162
+ }
3163
+ }
3164
+ const syncLocalName = async (serverName) => {
3165
+ const local = await loadIdentity2();
3166
+ if (!local || local.display_name === serverName) return;
3167
+ await saveIdentity2({ ...local, display_name: serverName });
3168
+ };
3169
+ const registerThisMachine = async (linked) => {
3170
+ const serverName = linked.display_name;
3171
+ if (linked.device_label) {
3172
+ await syncLocalName(serverName);
3173
+ console.log(` This machine already has its own credential, "${linked.device_label}", so it`);
3174
+ console.log(" was not registered again \u2014 a second one would leave the first live with");
3175
+ console.log(" nothing on this machine holding it.");
3176
+ return;
3177
+ }
3178
+ const local = await loadIdentity2();
3179
+ if (!local) {
3180
+ console.log(" This machine has no local identity file, so it was not registered as a");
3181
+ console.log(" device. Run `token-derby login` to give it its own credential.");
3182
+ return;
3183
+ }
3184
+ const named = local.display_name === serverName ? local : { ...local, display_name: serverName };
3185
+ let label;
3186
+ try {
3187
+ label = await resolveDeviceName(flagName, isTTY, hostname3(), promptText);
3188
+ for (; ; ) {
3189
+ try {
3190
+ const registered = await apiRegisterDevice({ label });
3191
+ await saveIdentity2({ ...named, secret_token: registered.secret_token });
3192
+ break;
3193
+ } catch (e) {
3194
+ if (!(e instanceof ApiError && e.code === "BAD_REQUEST" && isTTY)) throw e;
3195
+ console.error(` Error: ${e.message}`);
3196
+ const retry = (await promptText(" Enter a different device name: ")).trim();
3197
+ if (!retry) throw e;
3198
+ label = retry;
3199
+ }
3200
+ }
3201
+ } catch (e) {
3202
+ try {
3203
+ if (named !== local) await saveIdentity2(named);
3204
+ } catch {
3205
+ }
3206
+ console.log("");
3207
+ console.log(" Your account is linked \u2014 that part is done and does not need repeating.");
3208
+ console.log(" Registering this machine as a device did not complete, so it is still");
3209
+ console.log(" using the shared account credential. Run `token-derby login` to finish.");
3210
+ console.error(` Error: ${e instanceof ApiError ? `${e.code} ${e.message}` : String(e)}`);
3211
+ return;
3212
+ }
3213
+ console.log(` This machine is now registered as "${label}", with a credential of its own`);
3214
+ console.log(" that you can revoke separately under Account in the org manager.");
3215
+ };
3216
+ let me;
3217
+ try {
3218
+ me = await apiGetJockey();
3219
+ } catch (e) {
3220
+ if (e instanceof ApiError) {
3221
+ if (e.code === "UNAUTHENTICATED") {
3222
+ console.error(CREDENTIAL_DEAD_MESSAGE);
3223
+ return 1;
3224
+ }
3225
+ console.error(`Error: ${e.code} ${e.message}`);
3226
+ return 1;
3227
+ }
3228
+ throw e;
3229
+ }
3230
+ if (me.email) {
3231
+ console.log(`Already linked to ${me.email}.`);
3232
+ await syncLocalName(me.display_name);
3233
+ return 0;
3234
+ }
3235
+ let code;
3236
+ try {
3237
+ ({ code } = await apiCreateWebSession());
3238
+ } catch (e) {
3239
+ if (e instanceof ApiError) {
3240
+ console.error(`Error: ${e.code} ${e.message}`);
3241
+ return 1;
3242
+ }
3243
+ throw e;
3244
+ }
3245
+ const url = `${webOrigin()}/link#code=${code}`;
3246
+ console.log("");
3247
+ console.log(" Opening the Google link page in your browser...");
3248
+ console.log(` ${url}`);
3249
+ console.log("");
3250
+ console.log(" If it doesn't open, copy the link above. It expires in 60 seconds.");
3251
+ const cmd = opener();
3252
+ if (cmd) {
3253
+ try {
3254
+ const child = spawnImpl(cmd, [url], { stdio: "ignore", detached: true });
3255
+ child.on("error", () => {
3256
+ });
3257
+ child.unref();
3258
+ } catch {
3259
+ }
3260
+ }
3261
+ console.log("");
3262
+ console.log(" Waiting for you to finish connecting with Google...");
3263
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
3264
+ for (; ; ) {
3265
+ let poll;
3266
+ try {
3267
+ poll = await apiGetJockey();
3268
+ } catch (e) {
3269
+ if (e instanceof ApiError) {
3270
+ if (e.code === "UNAUTHENTICATED") {
3271
+ console.error(CREDENTIAL_DEAD_MESSAGE);
3272
+ return 1;
3273
+ }
3274
+ console.error(`Error: ${e.code} ${e.message}`);
3275
+ return 1;
3276
+ }
3277
+ throw e;
3278
+ }
3279
+ if (poll.email) {
3280
+ console.log(`\u2713 Linked to ${poll.email}.`);
3281
+ if (poll.display_name !== me.display_name) {
3282
+ console.log(` Your jockey is now named ${poll.display_name} \u2014 a first link renames it to`);
3283
+ console.log(" the first name on the Google account. `token-derby init` renames it again");
3284
+ console.log(" if you would rather it said something else.");
3285
+ }
3286
+ await registerThisMachine(poll);
3287
+ return 0;
3288
+ }
3289
+ if (Date.now() >= deadline) {
3290
+ console.error("Timed out waiting for the Google link to complete. Run `token-derby link` again.");
3291
+ return 1;
3292
+ }
3293
+ await sleepImpl(POLL_INTERVAL_MS);
3294
+ }
3295
+ }
3296
+
3297
+ // src/commands/whoami.ts
3298
+ async function whoamiCommand(deps = {}) {
3299
+ const apiGetJockey = deps.apiGetJockey ?? getJockey;
3300
+ let me;
3301
+ try {
3302
+ me = await apiGetJockey();
3303
+ } catch (e) {
3304
+ if (e instanceof ApiError) {
3305
+ if (e.code === "UNAUTHENTICATED") {
3306
+ console.error(CREDENTIAL_DEAD_MESSAGE);
3307
+ return 1;
3308
+ }
3309
+ console.error(`Error: ${e.code} ${e.message}`);
3310
+ return 1;
3311
+ }
3312
+ throw e;
3313
+ }
3314
+ console.log("");
3315
+ console.log(` ${me.display_name}`);
3316
+ if (me.email) console.log(` ${me.email}`);
3317
+ if (me.device_label) console.log(` this machine: ${me.device_label}`);
3318
+ console.log("");
3319
+ return 0;
3320
+ }
3321
+
3322
+ // src/commands/update.ts
3323
+ import * as readline5 from "readline/promises";
3324
+ import { stdin as stdin5, stdout as stdout5 } from "process";
3325
+ import { spawn as spawn4 } from "child_process";
3326
+ var REGISTRY_URL = "https://registry.npmjs.org/@mauricode/token-derby/latest";
3327
+ var UPGRADE_CMD = "npm install -g @mauricode/token-derby@latest";
3328
+ var FETCH_TIMEOUT_MS = 5e3;
3329
+ async function updateCommand(deps = {}) {
3330
+ const fetchImpl = deps.fetchImpl ?? fetch;
3331
+ const spawnImpl = deps.spawnImpl ?? spawn4;
3332
+ const promptYesNo2 = deps.promptYesNo ?? defaultPromptYesNo;
3333
+ let latest;
3334
+ try {
3335
+ latest = await fetchLatestVersion(fetchImpl);
3336
+ } catch (e) {
3337
+ console.error(`Could not reach the npm registry${e?.message ? ` (${e.message})` : ""}.`);
3338
+ console.error(`To upgrade manually: ${UPGRADE_CMD}`);
3339
+ return 1;
3340
+ }
3341
+ if (gteSemver(CLI_VERSION, latest)) {
3342
+ console.log(`You're on the latest version (${CLI_VERSION}).`);
3343
+ return 0;
3344
+ }
3345
+ console.log(`Current: ${CLI_VERSION} Latest: ${latest}`);
3346
+ const yes = await promptYesNo2("Run upgrade now? [y/N]: ");
3347
+ if (!yes) {
3348
+ console.log(`To upgrade manually: ${UPGRADE_CMD}`);
3349
+ return 0;
3350
+ }
3351
+ const code = await runNpmUpgrade(spawnImpl);
3352
+ if (code === 0) {
3353
+ console.log(`
3354
+ Upgraded to ${latest}.`);
3355
+ console.log(`If \`token-derby --version\` still shows ${CLI_VERSION}, your shell has`);
3356
+ console.log(`the old path cached. Drop the cache by running ONE of:`);
3357
+ console.log(` exec $SHELL (any shell \u2014 replaces this shell with a fresh one)`);
3358
+ console.log(` rehash (zsh)`);
3359
+ console.log(` hash -r (bash)`);
3360
+ console.log(`Or just open a new terminal tab.`);
3361
+ console.log(`If --version still doesn't match after that, you may have multiple`);
3362
+ console.log(`installs \u2014 run \`which -a token-derby\` to compare against \`npm bin -g\`.`);
3363
+ }
3364
+ return code;
3365
+ }
3366
+ async function fetchLatestVersion(fetchImpl) {
3367
+ const controller = new AbortController();
3368
+ const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
3369
+ try {
3370
+ const res = await fetchImpl(REGISTRY_URL, { signal: controller.signal });
3371
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3372
+ const body = await res.json();
3373
+ if (typeof body.version !== "string" || !/^\d+\.\d+\.\d+/.test(body.version)) {
3374
+ throw new Error("unexpected registry response");
3375
+ }
3376
+ return body.version;
3377
+ } finally {
3378
+ clearTimeout(t);
3379
+ }
3380
+ }
3381
+ async function defaultPromptYesNo(question) {
3382
+ const rl = readline5.createInterface({ input: stdin5, output: stdout5 });
3383
+ try {
3384
+ const answer = (await rl.question(question)).trim().toLowerCase();
3385
+ return answer === "y" || answer === "yes";
3386
+ } finally {
3387
+ rl.close();
3388
+ }
3389
+ }
3390
+ function runNpmUpgrade(spawnImpl) {
3391
+ return new Promise((resolve) => {
3392
+ const child = spawnImpl("npm", ["install", "-g", "@mauricode/token-derby@latest"], {
3393
+ stdio: "inherit"
3394
+ });
3395
+ child.on("error", (e) => {
3396
+ if (e.code === "ENOENT") {
3397
+ console.error("Could not find `npm` on PATH.");
3398
+ console.error(`To upgrade manually: ${UPGRADE_CMD}`);
3399
+ } else {
3400
+ console.error(`npm failed to start: ${e.message}`);
3401
+ }
3402
+ resolve(1);
3403
+ });
3404
+ child.on("exit", (code) => resolve(code ?? 1));
3405
+ });
3406
+ }
3407
+
3408
+ // src/commands/roll.ts
3409
+ import React14 from "react";
3410
+ import { render as render6 } from "ink";
3411
+
3412
+ // src/ui/RollHorsePicker.tsx
3413
+ import { useState as useState6 } from "react";
3414
+ import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
3415
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3416
+ function RollHorsePicker({ horses, onPick, onCancel }) {
3417
+ const [idx, setIdx] = useState6(0);
3418
+ useInput4((input, key) => {
3419
+ if (key.escape) {
3420
+ onCancel();
3421
+ return;
3422
+ }
3423
+ if (horses.length === 0) return;
3424
+ if (key.upArrow) {
3425
+ setIdx((idx - 1 + horses.length) % horses.length);
3426
+ return;
3427
+ }
3428
+ if (key.downArrow) {
3429
+ setIdx((idx + 1) % horses.length);
3430
+ return;
3431
+ }
3432
+ if (key.return) {
3433
+ onPick(horses[idx]);
3434
+ return;
3435
+ }
3436
+ });
3437
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", children: [
3438
+ /* @__PURE__ */ jsx9(Text9, { children: "Pick a horse to roll for:" }),
3439
+ horses.map((h, i) => /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", children: [
3440
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "row", children: /* @__PURE__ */ jsxs7(Text9, { children: [
3441
+ i === idx ? "\u25BA" : " ",
3442
+ " ",
3443
+ h.name,
3444
+ " ",
3445
+ /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
3446
+ "[Lvl. ",
3447
+ levelFromXp(h.xp),
3448
+ "]"
3449
+ ] }),
3450
+ " ",
3451
+ /* @__PURE__ */ jsxs7(Text9, { color: "yellow", children: [
3452
+ "\u2014 ",
3453
+ h.pending,
3454
+ " roll",
3455
+ h.pending === 1 ? "" : "s"
3456
+ ] })
3457
+ ] }) }),
3458
+ /* @__PURE__ */ jsxs7(Box9, { flexDirection: "row", children: [
3459
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3460
+ /* @__PURE__ */ jsx9(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
3461
+ ] })
3462
+ ] }, h.stable_horse_id)),
3463
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
3464
+ ] });
3465
+ }
3466
+
3467
+ // src/ui/reveal.ts
3468
+ import React13 from "react";
3469
+ import { render as render5 } from "ink";
3470
+
3471
+ // src/ui/RollReveal.tsx
3472
+ import { useState as useState8, useEffect as useEffect4, useMemo } from "react";
3473
+ import { Box as Box11, Text as Text11 } from "ink";
3474
+
3475
+ // src/ui/HatSprite.tsx
3476
+ import { useEffect as useEffect3, useState as useState7 } from "react";
3477
+ import { Box as Box10, Text as Text10 } from "ink";
3478
+ import { jsx as jsx10 } from "react/jsx-runtime";
3479
+ function HatSprite({ hat, variant, centerIn }) {
3480
+ const colors = hatColorsFor2(hat, variant ?? 0);
3481
+ const grid = makeHatGrid(hat, colors, centerIn);
3482
+ const lines = hexGridToHalfBlocks(grid);
3483
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line }, i)) });
3484
+ }
3485
+ function AnimatedHatSprite({ hat, variant, centerIn }) {
3486
+ if (hat.rarity !== "legendary") {
3487
+ return /* @__PURE__ */ jsx10(HatSprite, { hat, variant, centerIn });
3488
+ }
3489
+ const frames = hat.animation.frames;
3490
+ const fps = hat.animation.fps;
3491
+ const [idx, setIdx] = useState7(0);
3492
+ useEffect3(() => {
3493
+ if (frames.length <= 1) return;
3494
+ const interval = setInterval(
3495
+ () => setIdx((i) => (i + 1) % frames.length),
3496
+ Math.max(1, Math.round(1e3 / fps))
3497
+ );
3498
+ return () => clearInterval(interval);
3499
+ }, [frames.length, fps]);
3500
+ const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
3501
+ return /* @__PURE__ */ jsx10(HatSprite, { hat: framed, variant, centerIn });
3502
+ }
3503
+ function hatColorsFor2(hat, variantIdx) {
3504
+ if (hat.rarity === "legendary") return hat.colors;
3505
+ return hat.variants[variantIdx] ?? hat.variants[0];
3506
+ }
2831
3507
  function makeHatGrid(hat, colors, centerIn) {
2832
3508
  const w = centerIn?.w ?? hat.width;
2833
3509
  const h = centerIn?.h ?? hat.rows.length;
@@ -2849,7 +3525,7 @@ function makeHatGrid(hat, colors, centerIn) {
2849
3525
  }
2850
3526
 
2851
3527
  // src/ui/RollReveal.tsx
2852
- import { jsx as jsx10 } from "react/jsx-runtime";
3528
+ import { jsx as jsx11 } from "react/jsx-runtime";
2853
3529
  var RESET3 = "\x1B[0m";
2854
3530
  var BOX_COLOR = "#E5C76B";
2855
3531
  var TIER_PALETTE = {
@@ -2918,7 +3594,7 @@ var BOX_EMPTY = [
2918
3594
  ""
2919
3595
  ].map(pad);
2920
3596
  function GiftBox({ frame, color }) {
2921
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
3597
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx11(Text11, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
2922
3598
  }
2923
3599
  function spawnParticles(tier, count, cx, cy) {
2924
3600
  const palette = TIER_PALETTE[tier];
@@ -2941,7 +3617,7 @@ function ConfettiBurst({ tier }) {
2941
3617
  const cx = Math.floor(SCENE_W / 2);
2942
3618
  const cy = Math.floor(SCENE_H / 2);
2943
3619
  const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
2944
- const [tick, setTick] = useState7(0);
3620
+ const [tick, setTick] = useState8(0);
2945
3621
  useEffect4(() => {
2946
3622
  const i = setInterval(() => setTick((t) => t + 1), 70);
2947
3623
  return () => clearInterval(i);
@@ -2954,13 +3630,13 @@ function ConfettiBurst({ tier }) {
2954
3630
  grid[y][x] = ansiFg(p.color) + p.char + RESET3;
2955
3631
  }
2956
3632
  }
2957
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
3633
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx11(Text11, { children: row.join("") }, y)) });
2958
3634
  }
2959
3635
  var CLOSED_HOLD_MS = 3e3;
2960
3636
  function RollReveal({ outcome, onDone }) {
2961
3637
  const isNoHat = outcome.kind === "no_hat";
2962
3638
  const isLegendary = outcome.kind !== "no_hat" && outcome.hat.rarity === "legendary";
2963
- const [phase, setPhase] = useState7("closed");
3639
+ const [phase, setPhase] = useState8("closed");
2964
3640
  useEffect4(() => {
2965
3641
  const timers = [];
2966
3642
  timers.push(setTimeout(() => setPhase("open1"), CLOSED_HOLD_MS));
@@ -2975,95 +3651,18 @@ function RollReveal({ outcome, onDone }) {
2975
3651
  }
2976
3652
  return () => timers.forEach(clearTimeout);
2977
3653
  }, [isNoHat, isLegendary, onDone]);
2978
- if (phase === "closed") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
2979
- if (phase === "open1") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
2980
- if (phase === "open2") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
2981
- if (phase === "empty") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
3654
+ if (phase === "closed") return /* @__PURE__ */ jsx11(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
3655
+ if (phase === "open1") return /* @__PURE__ */ jsx11(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
3656
+ if (phase === "open2") return /* @__PURE__ */ jsx11(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
3657
+ if (phase === "empty") return /* @__PURE__ */ jsx11(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2982
3658
  if (phase === "burst" && outcome.kind !== "no_hat") {
2983
- return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
3659
+ return /* @__PURE__ */ jsx11(ConfettiBurst, { tier: outcome.hat.rarity });
2984
3660
  }
2985
- if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2986
- return outcome.hat.rarity === "legendary" ? /* @__PURE__ */ jsx10(AnimatedHatSprite, { hat: outcome.hat, centerIn: { w: SCENE_W, h: SCENE_H } }) : /* @__PURE__ */ jsx10(HatSprite, { hat: outcome.hat, variant: outcome.variant, centerIn: { w: SCENE_W, h: SCENE_H } });
3661
+ if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx11(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
3662
+ return outcome.hat.rarity === "legendary" ? /* @__PURE__ */ jsx11(AnimatedHatSprite, { hat: outcome.hat, centerIn: { w: SCENE_W, h: SCENE_H } }) : /* @__PURE__ */ jsx11(HatSprite, { hat: outcome.hat, variant: outcome.variant, centerIn: { w: SCENE_W, h: SCENE_H } });
2987
3663
  }
2988
3664
 
2989
- // src/ui/RollHorsePicker.tsx
2990
- import { useState as useState8 } from "react";
2991
- import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
2992
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2993
- function RollHorsePicker({ horses, onPick, onCancel }) {
2994
- const [idx, setIdx] = useState8(0);
2995
- useInput4((input, key) => {
2996
- if (key.escape) {
2997
- onCancel();
2998
- return;
2999
- }
3000
- if (horses.length === 0) return;
3001
- if (key.upArrow) {
3002
- setIdx((idx - 1 + horses.length) % horses.length);
3003
- return;
3004
- }
3005
- if (key.downArrow) {
3006
- setIdx((idx + 1) % horses.length);
3007
- return;
3008
- }
3009
- if (key.return) {
3010
- onPick(horses[idx]);
3011
- return;
3012
- }
3013
- });
3014
- return /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
3015
- /* @__PURE__ */ jsx11(Text11, { children: "Pick a horse to roll for:" }),
3016
- horses.map((h, i) => /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
3017
- /* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs7(Text11, { children: [
3018
- i === idx ? "\u25BA" : " ",
3019
- " ",
3020
- h.name,
3021
- " ",
3022
- /* @__PURE__ */ jsxs7(Text11, { color: "cyan", children: [
3023
- "[Lvl. ",
3024
- levelFromXp(h.xp),
3025
- "]"
3026
- ] }),
3027
- " ",
3028
- /* @__PURE__ */ jsxs7(Text11, { color: "yellow", children: [
3029
- "\u2014 ",
3030
- h.pending,
3031
- " roll",
3032
- h.pending === 1 ? "" : "s"
3033
- ] })
3034
- ] }) }),
3035
- /* @__PURE__ */ jsxs7(Box11, { flexDirection: "row", children: [
3036
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
3037
- /* @__PURE__ */ jsx11(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
3038
- ] })
3039
- ] }, h.stable_horse_id)),
3040
- /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
3041
- ] });
3042
- }
3043
-
3044
- // src/commands/roll.ts
3045
- function pendingFor(horse) {
3046
- const level = levelFromXp(horse.xp);
3047
- const lastRolled = horse.last_rolled_level ?? Math.max(1, level - 1);
3048
- return level - lastRolled;
3049
- }
3050
- async function promptYesNo(question) {
3051
- resetStdinAfterInk();
3052
- const readline6 = await import("readline/promises");
3053
- const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
3054
- const a = (await rl.question(question)).trim().toLowerCase();
3055
- rl.close();
3056
- return a === "" || a === "y" || a === "yes";
3057
- }
3058
- function resetStdinAfterInk() {
3059
- if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
3060
- process.stdin.setRawMode(false);
3061
- }
3062
- while (process.stdin.read() !== null) {
3063
- }
3064
- process.stdin.pause();
3065
- process.stdin.ref();
3066
- }
3665
+ // src/ui/reveal.ts
3067
3666
  async function runReveal(outcome) {
3068
3667
  await new Promise((resolve) => {
3069
3668
  const app = render5(React13.createElement(RollReveal, {
@@ -3075,6 +3674,13 @@ async function runReveal(outcome) {
3075
3674
  }));
3076
3675
  });
3077
3676
  }
3677
+
3678
+ // src/commands/roll.ts
3679
+ function pendingFor(horse) {
3680
+ const level = levelFromXp(horse.xp);
3681
+ const lastRolled = horse.last_rolled_level ?? Math.max(1, level - 1);
3682
+ return level - lastRolled;
3683
+ }
3078
3684
  async function rollCommand() {
3079
3685
  let stable;
3080
3686
  try {
@@ -3092,7 +3698,7 @@ async function rollCommand() {
3092
3698
  return 0;
3093
3699
  }
3094
3700
  const picked = await new Promise((resolve) => {
3095
- const app = render5(React13.createElement(RollHorsePicker, {
3701
+ const app = render6(React14.createElement(RollHorsePicker, {
3096
3702
  horses: eligible,
3097
3703
  onPick: (h) => {
3098
3704
  app.unmount();
@@ -3174,16 +3780,16 @@ No hat this time. +${result.xp_awarded} XP toward your next level.
3174
3780
  }
3175
3781
  }
3176
3782
 
3177
- // src/commands/org-join.ts
3178
- async function orgJoinCommand(token) {
3783
+ // src/commands/claim.ts
3784
+ import React15 from "react";
3785
+ import { render as render7 } from "ink";
3786
+ async function claimCommand(token) {
3179
3787
  if (!token) {
3180
- console.error("Usage: token-derby organisation join <join-token>");
3788
+ console.error("Usage: token-derby claim <token>");
3181
3789
  return 2;
3182
3790
  }
3183
3791
  try {
3184
- const resp = await joinOrganisation({ join_token: token });
3185
- console.log(`Joined organisation: ${resp.org_name}`);
3186
- return 0;
3792
+ await probeClaim(token);
3187
3793
  } catch (e) {
3188
3794
  if (e instanceof ApiError) {
3189
3795
  console.error(`Error: ${e.code} ${e.message}`);
@@ -3191,24 +3797,9 @@ async function orgJoinCommand(token) {
3191
3797
  }
3192
3798
  throw e;
3193
3799
  }
3194
- }
3195
-
3196
- // src/commands/web.ts
3197
- import { spawn as spawn2 } from "child_process";
3198
- function webOrigin() {
3199
- return apiBase().replace(/\/api\/?$/, "");
3200
- }
3201
- function opener() {
3202
- if (process.platform === "darwin") return "open";
3203
- if (process.platform === "win32") return "start";
3204
- if (process.platform === "linux") return "xdg-open";
3205
- return null;
3206
- }
3207
- async function webCommand(deps = {}) {
3208
- const spawnImpl = deps.spawnImpl ?? spawn2;
3209
- let code;
3800
+ let stable;
3210
3801
  try {
3211
- ({ code } = await createWebSession());
3802
+ stable = await listStable();
3212
3803
  } catch (e) {
3213
3804
  if (e instanceof ApiError) {
3214
3805
  console.error(`Error: ${e.code} ${e.message}`);
@@ -3216,25 +3807,97 @@ async function webCommand(deps = {}) {
3216
3807
  }
3217
3808
  throw e;
3218
3809
  }
3219
- const url = `${webOrigin()}/org-manager#code=${code}`;
3220
- console.log("");
3221
- console.log(" Opening the Token Derby org manager in your browser...");
3222
- console.log(` ${url}`);
3223
- console.log("");
3224
- console.log(" If it doesn't open, copy the link above. It expires in 60 seconds.");
3225
- const cmd = opener();
3226
- if (cmd) {
3227
- try {
3228
- const child = spawnImpl(cmd, [url], { stdio: "ignore", detached: true });
3229
- child.on("error", () => {
3230
- });
3231
- child.unref();
3232
- } catch {
3810
+ if (stable.horses.length === 0) {
3811
+ console.error("No horses in your stable. Run `token-derby stable create` first.");
3812
+ return 1;
3813
+ }
3814
+ console.log("\n\u{1F381} A cosmetic has been awarded to you.\n");
3815
+ const picked = await new Promise((resolve) => {
3816
+ const app = render7(React15.createElement(HorsePicker, {
3817
+ horses: stable.horses,
3818
+ prompt: "Which horse should receive it?",
3819
+ onPick: (h) => {
3820
+ app.unmount();
3821
+ resolve(h);
3822
+ },
3823
+ onCancel: () => {
3824
+ app.unmount();
3825
+ resolve(null);
3826
+ }
3827
+ }));
3828
+ });
3829
+ if (!picked) {
3830
+ console.log("Cancelled. Your token is unspent.");
3831
+ return 0;
3832
+ }
3833
+ let result;
3834
+ try {
3835
+ result = await redeemClaim(token, { stable_horse_id: picked.stable_horse_id });
3836
+ } catch (e) {
3837
+ if (e instanceof ApiError) {
3838
+ console.error(`Error: ${e.code} ${e.message}`);
3839
+ return 1;
3840
+ }
3841
+ throw e;
3842
+ }
3843
+ const outcome = (() => {
3844
+ if (result.result === "hat") {
3845
+ const hat3 = hatById(result.collected.id);
3846
+ return hat3 ? { kind: "hat", hat: hat3, variant: result.collected.variant } : null;
3847
+ }
3848
+ const hat2 = hatById(result.hat_id);
3849
+ return hat2 ? { kind: "duplicate", hat: hat2, variant: result.variant } : null;
3850
+ })();
3851
+ if (!outcome) {
3852
+ console.error("Server returned an unknown hat id \u2014 catalog mismatch.");
3853
+ return 1;
3854
+ }
3855
+ await runReveal(outcome);
3856
+ if (result.result === "hat") {
3857
+ const hat2 = hatById(result.collected.id);
3858
+ const variantSuffix2 = hat2.rarity !== "legendary" && result.collected.variant !== void 0 ? ` #${result.collected.variant + 1}` : "";
3859
+ console.log(`
3860
+ \u2728 ${hat2.name}${variantSuffix2} [${hat2.rarity.toUpperCase()}]
3861
+ `);
3862
+ if (await promptYesNo("Equip now? [Y/n] ")) {
3863
+ try {
3864
+ await equipHat(picked.stable_horse_id, { hat_index: result.hat_index });
3865
+ console.log(`Equipped on ${picked.name}.`);
3866
+ } catch (e) {
3867
+ if (e instanceof ApiError) {
3868
+ console.error(`Equip failed: ${e.code} ${e.message}`);
3869
+ } else throw e;
3870
+ }
3233
3871
  }
3872
+ return 0;
3234
3873
  }
3874
+ const hat = hatById(result.hat_id);
3875
+ const variantSuffix = result.variant !== void 0 ? ` #${result.variant + 1}` : "";
3876
+ console.log(`
3877
+ ${picked.name} already has ${hat?.name ?? result.hat_id}${variantSuffix}. +${result.xp_awarded} XP.
3878
+ `);
3235
3879
  return 0;
3236
3880
  }
3237
3881
 
3882
+ // src/commands/org-join.ts
3883
+ async function orgJoinCommand(token) {
3884
+ if (!token) {
3885
+ console.error("Usage: token-derby organisation join <join-token>");
3886
+ return 2;
3887
+ }
3888
+ try {
3889
+ const resp = await joinOrganisation({ join_token: token });
3890
+ console.log(`Joined organisation: ${resp.org_name}`);
3891
+ return 0;
3892
+ } catch (e) {
3893
+ if (e instanceof ApiError) {
3894
+ console.error(`Error: ${e.code} ${e.message}`);
3895
+ return 1;
3896
+ }
3897
+ throw e;
3898
+ }
3899
+ }
3900
+
3238
3901
  // src/commands/env.ts
3239
3902
  function warnOverrides() {
3240
3903
  if (process.env.TOKEN_DERBY_HOME) {
@@ -3269,10 +3932,27 @@ function envCommand(arg) {
3269
3932
  var HELP = `token-derby v${CLI_VERSION}
3270
3933
 
3271
3934
  Identity:
3272
- token-derby init Set up your jockey identity (run this first)
3273
- Re-running renames you on the server.
3274
- token-derby init --reset Wipe local identity and create a fresh account.
3275
- Your previous stable is abandoned on the server.
3935
+ token-derby init Deprecated \u2014 creates or renames your jockey identity;
3936
+ prefer \`login\`, which also links your account.
3937
+ token-derby init --reset Deprecated \u2014 wipes local identity and starts a fresh
3938
+ account (previous stable abandoned); \`login\` recovers
3939
+ your existing account instead of abandoning it.
3940
+ token-derby login [--device-name <name>]
3941
+ About THIS MACHINE: sign in with Google to give
3942
+ it a credential of its own. The way in for a
3943
+ machine that has none yet. Adds no email to
3944
+ your account \u2014 see \`link\`.
3945
+ token-derby logout Retire this machine's credential and clear
3946
+ local identity.
3947
+ token-derby link [--device-name <name>]
3948
+ About YOUR JOCKEY: connect it to a Google
3949
+ account \u2014 adds your email on file, and
3950
+ renames your jockey to the first name on
3951
+ that account. Then registers this machine
3952
+ as a device too, if it is still using the
3953
+ shared account credential.
3954
+ token-derby whoami Show your jockey name, linked email (if any),
3955
+ and this machine's device name (if any).
3276
3956
 
3277
3957
  Maintenance:
3278
3958
  token-derby update Check for and install the latest CLI version
@@ -3299,6 +3979,8 @@ Races:
3299
3979
  Cosmetics:
3300
3980
  token-derby roll Spend a pending roll to try for a hat.
3301
3981
  Earn rolls by leveling up horses.
3982
+ token-derby claim <token> Redeem a claim token for a cosmetic
3983
+ awarded to you by an admin.
3302
3984
 
3303
3985
  Environment:
3304
3986
  token-derby env Show the active environment (prod|staging)
@@ -3323,11 +4005,12 @@ async function main() {
3323
4005
  const reset = argv.slice(1).includes("--reset");
3324
4006
  return initCommand(reset);
3325
4007
  }
4008
+ if (cmd === "login") return loginCommand(argv.slice(1));
3326
4009
  if (cmd === "update") return updateCommand();
3327
4010
  if (cmd === "env") return envCommand(argv[1]);
3328
4011
  const identity = await loadIdentity();
3329
4012
  if (!identity) {
3330
- console.error("Run `token-derby init` to set up your identity before using any other command.");
4013
+ console.error("Run `token-derby login` to set up your identity before using any other command.");
3331
4014
  return 1;
3332
4015
  }
3333
4016
  if (cmd === "stable") {
@@ -3351,9 +4034,13 @@ async function main() {
3351
4034
  const orgName = parseFlag(argv.slice(1), "--organisation");
3352
4035
  return createRaceCommand(orgName);
3353
4036
  }
4037
+ if (cmd === "logout") return logoutCommand();
4038
+ if (cmd === "link") return linkCommand(argv.slice(1));
4039
+ if (cmd === "whoami") return whoamiCommand();
3354
4040
  if (cmd === "join") return joinCommand(argv[1], argv.slice(2));
3355
4041
  if (cmd === "end") return endCommand(argv[1]);
3356
4042
  if (cmd === "roll") return rollCommand();
4043
+ if (cmd === "claim") return claimCommand(argv[1]);
3357
4044
  if (cmd === "web") return webCommand();
3358
4045
  console.error(`Unknown command: ${cmd}`);
3359
4046
  console.error(HELP);