buddy-reroll 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +55 -147
- package/package.json +7 -3
- package/sprites.js +137 -0
- package/ui.jsx +469 -0
package/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { join } from "path";
|
|
|
5
5
|
import { homedir, platform } from "os";
|
|
6
6
|
import { execSync } from "child_process";
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
|
-
import
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { renderSprite, colorizeSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
9
10
|
|
|
10
11
|
if (typeof Bun === "undefined") {
|
|
11
12
|
console.error("buddy-reroll requires Bun runtime (uses Bun.hash).\nInstall: https://bun.sh");
|
|
@@ -112,7 +113,9 @@ function findBinaryPath() {
|
|
|
112
113
|
const versionsDir = join(homedir(), ".local", "share", "claude", "versions");
|
|
113
114
|
if (existsSync(versionsDir)) {
|
|
114
115
|
try {
|
|
115
|
-
const versions = readdirSync(versionsDir)
|
|
116
|
+
const versions = readdirSync(versionsDir)
|
|
117
|
+
.filter((f) => !f.includes(".backup"))
|
|
118
|
+
.sort();
|
|
116
119
|
if (versions.length > 0) return join(versionsDir, versions[versions.length - 1]);
|
|
117
120
|
} catch {}
|
|
118
121
|
}
|
|
@@ -181,7 +184,7 @@ function findCurrentSalt(binaryData, userId) {
|
|
|
181
184
|
|
|
182
185
|
// ── Brute-force ──────────────────────────────────────────────────────────
|
|
183
186
|
|
|
184
|
-
function bruteForce(userId, target,
|
|
187
|
+
async function bruteForce(userId, target, onProgress) {
|
|
185
188
|
const startTime = Date.now();
|
|
186
189
|
let checked = 0;
|
|
187
190
|
|
|
@@ -206,9 +209,11 @@ function bruteForce(userId, target, spinner) {
|
|
|
206
209
|
const r = rollFrom(salt, userId);
|
|
207
210
|
if (matches(r, target)) return { salt, result: r, checked, elapsed: Date.now() - startTime };
|
|
208
211
|
|
|
212
|
+
if (checked % 100_000 === 0) {
|
|
213
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
214
|
+
}
|
|
209
215
|
if (checked % 5_000_000 === 0) {
|
|
210
|
-
|
|
211
|
-
if (spinner) spinner.message(`${(checked / 1e6).toFixed(0)}M salts checked (${secs}s)`);
|
|
216
|
+
if (onProgress) onProgress(checked, Date.now() - startTime);
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
|
|
@@ -281,165 +286,68 @@ function clearCompanion(configPath) {
|
|
|
281
286
|
|
|
282
287
|
// ── Display ──────────────────────────────────────────────────────────────
|
|
283
288
|
|
|
284
|
-
function
|
|
289
|
+
function formatCompanionCard(result) {
|
|
290
|
+
const sprite = renderSprite({ species: result.species, eye: result.eye, hat: result.hat });
|
|
291
|
+
const colored = colorizeSprite(sprite, result.rarity);
|
|
292
|
+
const colorFn = chalk[RARITY_COLORS[result.rarity]] ?? chalk.white;
|
|
293
|
+
const stars = RARITY_STARS[result.rarity] ?? "";
|
|
294
|
+
|
|
295
|
+
const meta = [];
|
|
296
|
+
meta.push(`${result.species} / ${result.rarity}${result.shiny ? " / shiny" : ""}`);
|
|
297
|
+
meta.push(`eye:${result.eye} / hat:${result.hat}`);
|
|
298
|
+
meta.push(stars);
|
|
299
|
+
|
|
285
300
|
const lines = [];
|
|
286
|
-
|
|
301
|
+
const spriteWidth = 14;
|
|
302
|
+
for (let i = 0; i < colored.length; i++) {
|
|
303
|
+
const right = meta[i] ?? "";
|
|
304
|
+
lines.push(` ${colored[i]}${" ".repeat(Math.max(0, spriteWidth - sprite[i].length))}${right}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
287
307
|
for (const [k, v] of Object.entries(result.stats)) {
|
|
288
|
-
const bar = "█".repeat(Math.round(v / 10)) + "░".repeat(10 - Math.round(v / 10));
|
|
308
|
+
const bar = colorFn("█".repeat(Math.round(v / 10)) + "░".repeat(10 - Math.round(v / 10)));
|
|
289
309
|
lines.push(` ${k.padEnd(10)} ${bar} ${String(v).padStart(3)}`);
|
|
290
310
|
}
|
|
311
|
+
|
|
291
312
|
return lines.join("\n");
|
|
292
313
|
}
|
|
293
314
|
|
|
294
315
|
// ── Interactive mode ─────────────────────────────────────────────────────
|
|
295
316
|
|
|
296
317
|
async function interactiveMode(binaryPath, configPath, userId) {
|
|
297
|
-
p.intro("buddy-reroll");
|
|
298
|
-
|
|
299
318
|
const binaryData = readFileSync(binaryPath);
|
|
300
319
|
const currentSalt = findCurrentSalt(binaryData, userId);
|
|
301
320
|
if (!currentSalt) {
|
|
302
|
-
|
|
321
|
+
console.error("✗ Could not find companion salt in binary.");
|
|
303
322
|
process.exit(1);
|
|
304
323
|
}
|
|
305
324
|
const currentRoll = rollFrom(currentSalt, userId);
|
|
306
|
-
p.note(formatRoll(currentRoll), "Current companion");
|
|
307
|
-
|
|
308
|
-
const action = await p.select({
|
|
309
|
-
message: "What would you like to do?",
|
|
310
|
-
options: [
|
|
311
|
-
{ value: "reroll", label: "Reroll companion", hint: "pick species, rarity, etc." },
|
|
312
|
-
{ value: "restore", label: "Restore original", hint: "undo all patches" },
|
|
313
|
-
{ value: "current", label: "Show current", hint: "just display info" },
|
|
314
|
-
],
|
|
315
|
-
});
|
|
316
|
-
if (p.isCancel(action)) { p.cancel(); process.exit(0); }
|
|
317
|
-
|
|
318
|
-
if (action === "current") {
|
|
319
|
-
p.outro("Done!");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
325
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
})),
|
|
343
|
-
initialValue: currentRoll.species,
|
|
326
|
+
const { runInteractiveUI } = await import("./ui.jsx");
|
|
327
|
+
await runInteractiveUI({
|
|
328
|
+
currentRoll,
|
|
329
|
+
currentSalt,
|
|
330
|
+
binaryPath,
|
|
331
|
+
configPath,
|
|
332
|
+
userId,
|
|
333
|
+
bruteForce,
|
|
334
|
+
patchBinary,
|
|
335
|
+
resignBinary,
|
|
336
|
+
clearCompanion,
|
|
337
|
+
isClaudeRunning,
|
|
338
|
+
rollFrom,
|
|
339
|
+
matches,
|
|
340
|
+
SPECIES,
|
|
341
|
+
RARITIES,
|
|
342
|
+
RARITY_LABELS,
|
|
343
|
+
EYES,
|
|
344
|
+
HATS,
|
|
344
345
|
});
|
|
345
|
-
if (p.isCancel(species)) { p.cancel(); process.exit(0); }
|
|
346
|
-
|
|
347
|
-
const rarity = await p.select({
|
|
348
|
-
message: "Rarity",
|
|
349
|
-
options: RARITIES.map((r) => ({
|
|
350
|
-
value: r,
|
|
351
|
-
label: RARITY_LABELS[r],
|
|
352
|
-
hint: r === currentRoll.rarity ? "current" : undefined,
|
|
353
|
-
})),
|
|
354
|
-
initialValue: currentRoll.rarity,
|
|
355
|
-
});
|
|
356
|
-
if (p.isCancel(rarity)) { p.cancel(); process.exit(0); }
|
|
357
|
-
|
|
358
|
-
const eye = await p.select({
|
|
359
|
-
message: "Eye",
|
|
360
|
-
options: EYES.map((e) => ({
|
|
361
|
-
value: e,
|
|
362
|
-
label: e,
|
|
363
|
-
hint: e === currentRoll.eye ? "current" : undefined,
|
|
364
|
-
})),
|
|
365
|
-
initialValue: currentRoll.eye,
|
|
366
|
-
});
|
|
367
|
-
if (p.isCancel(eye)) { p.cancel(); process.exit(0); }
|
|
368
|
-
|
|
369
|
-
let hat = "none";
|
|
370
|
-
if (rarity === "common") {
|
|
371
|
-
p.log.info("Common rarity always gets hat=none");
|
|
372
|
-
} else {
|
|
373
|
-
hat = await p.select({
|
|
374
|
-
message: "Hat",
|
|
375
|
-
options: HATS.map((h) => ({
|
|
376
|
-
value: h,
|
|
377
|
-
label: h,
|
|
378
|
-
hint: h === currentRoll.hat ? "current" : undefined,
|
|
379
|
-
})),
|
|
380
|
-
initialValue: currentRoll.hat === "none" ? "crown" : currentRoll.hat,
|
|
381
|
-
});
|
|
382
|
-
if (p.isCancel(hat)) { p.cancel(); process.exit(0); }
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const shiny = await p.confirm({
|
|
386
|
-
message: "Shiny?",
|
|
387
|
-
initialValue: false,
|
|
388
|
-
});
|
|
389
|
-
if (p.isCancel(shiny)) { p.cancel(); process.exit(0); }
|
|
390
|
-
|
|
391
|
-
const target = { species, rarity, eye, hat, shiny };
|
|
392
|
-
|
|
393
|
-
if (matches(currentRoll, target)) {
|
|
394
|
-
p.outro("Already matching! No changes needed.");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
p.log.info(`Target: ${species} / ${rarity} / eye:${eye} / hat:${hat}${shiny ? " / shiny" : ""}`);
|
|
399
|
-
|
|
400
|
-
const confirm = await p.confirm({ message: "Search and apply?" });
|
|
401
|
-
if (p.isCancel(confirm) || !confirm) { p.cancel(); process.exit(0); }
|
|
402
|
-
|
|
403
|
-
if (isClaudeRunning()) {
|
|
404
|
-
p.log.warn("Claude Code appears to be running. Quit it before patching to avoid issues.");
|
|
405
|
-
const proceed = await p.confirm({ message: "Patch anyway?" });
|
|
406
|
-
if (p.isCancel(proceed) || !proceed) { p.cancel(); process.exit(0); }
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const spinner = p.spinner();
|
|
410
|
-
spinner.start("Searching for matching salt...");
|
|
411
|
-
|
|
412
|
-
const found = bruteForce(userId, target, spinner);
|
|
413
|
-
if (!found) {
|
|
414
|
-
spinner.stop("No matching salt found. Try relaxing constraints.");
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
spinner.stop(`Found in ${found.checked.toLocaleString()} attempts (${(found.elapsed / 1000).toFixed(1)}s)`);
|
|
418
|
-
|
|
419
|
-
p.note(formatRoll(found.result), "New companion");
|
|
420
|
-
|
|
421
|
-
const backupPath = binaryPath + ".backup";
|
|
422
|
-
if (!existsSync(backupPath)) {
|
|
423
|
-
copyFileSync(binaryPath, backupPath);
|
|
424
|
-
p.log.info(`Backup saved to ${backupPath}`);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const patchCount = patchBinary(binaryPath, currentSalt, found.salt);
|
|
428
|
-
p.log.success(`Patched ${patchCount} occurrence(s)`);
|
|
429
|
-
|
|
430
|
-
if (resignBinary(binaryPath)) {
|
|
431
|
-
p.log.success("Binary re-signed (ad-hoc codesign)");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
clearCompanion(configPath);
|
|
435
|
-
p.log.success("Companion data cleared");
|
|
436
|
-
|
|
437
|
-
p.outro("Done! Restart Claude Code and run /buddy to hatch your new companion.");
|
|
438
346
|
}
|
|
439
347
|
|
|
440
348
|
// ── Non-interactive mode ─────────────────────────────────────────────────
|
|
441
349
|
|
|
442
|
-
function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
350
|
+
async function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
443
351
|
console.log(` Binary: ${binaryPath}`);
|
|
444
352
|
console.log(` Config: ${configPath}`);
|
|
445
353
|
console.log(` User ID: ${userId.slice(0, 8)}...`);
|
|
@@ -467,7 +375,7 @@ function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
467
375
|
if (args.current) {
|
|
468
376
|
const result = rollFrom(currentSalt, userId);
|
|
469
377
|
console.log(`\n Current companion (salt: ${currentSalt}):`);
|
|
470
|
-
console.log(
|
|
378
|
+
console.log(formatCompanionCard(result));
|
|
471
379
|
console.log();
|
|
472
380
|
return;
|
|
473
381
|
}
|
|
@@ -500,7 +408,7 @@ function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
500
408
|
|
|
501
409
|
const currentRoll = rollFrom(currentSalt, userId);
|
|
502
410
|
if (matches(currentRoll, target)) {
|
|
503
|
-
console.log(" ✓ Already matches!\n" +
|
|
411
|
+
console.log(" ✓ Already matches!\n" + formatCompanionCard(currentRoll));
|
|
504
412
|
return;
|
|
505
413
|
}
|
|
506
414
|
|
|
@@ -509,13 +417,13 @@ function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
509
417
|
}
|
|
510
418
|
|
|
511
419
|
console.log(" Searching...");
|
|
512
|
-
const found = bruteForce(userId, target, null);
|
|
420
|
+
const found = await bruteForce(userId, target, null);
|
|
513
421
|
if (!found) {
|
|
514
422
|
console.error(" ✗ No matching salt found. Try relaxing constraints.");
|
|
515
423
|
process.exit(1);
|
|
516
424
|
}
|
|
517
425
|
console.log(` ✓ Found in ${found.checked.toLocaleString()} attempts (${(found.elapsed / 1000).toFixed(1)}s)`);
|
|
518
|
-
console.log(
|
|
426
|
+
console.log(formatCompanionCard(found.result));
|
|
519
427
|
|
|
520
428
|
const backupPath = binaryPath + ".backup";
|
|
521
429
|
if (!existsSync(backupPath)) {
|
|
@@ -603,7 +511,7 @@ async function main() {
|
|
|
603
511
|
if (!hasTargetFlags && !isCommand) {
|
|
604
512
|
await interactiveMode(binaryPath, configPath, userId);
|
|
605
513
|
} else {
|
|
606
|
-
nonInteractiveMode(args, binaryPath, configPath, userId);
|
|
514
|
+
await nonInteractiveMode(args, binaryPath, configPath, userId);
|
|
607
515
|
}
|
|
608
516
|
}
|
|
609
517
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-reroll",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reroll your Claude Code buddy companion to any species/rarity/eye/hat/shiny combo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"buddy-reroll": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"index.js"
|
|
10
|
+
"index.js",
|
|
11
|
+
"ui.jsx",
|
|
12
|
+
"sprites.js"
|
|
11
13
|
],
|
|
12
14
|
"engines": {
|
|
13
15
|
"bun": ">=1.0.0"
|
|
@@ -25,6 +27,8 @@
|
|
|
25
27
|
"reroll"
|
|
26
28
|
],
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"
|
|
30
|
+
"chalk": "^5.6.2",
|
|
31
|
+
"ink": "^6.8.0",
|
|
32
|
+
"react": "^19.2.4"
|
|
29
33
|
}
|
|
30
34
|
}
|
package/sprites.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export const RARITY_STARS = {
|
|
4
|
+
common: "★",
|
|
5
|
+
uncommon: "★★",
|
|
6
|
+
rare: "★★★",
|
|
7
|
+
epic: "★★★★",
|
|
8
|
+
legendary: "★★★★★",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const RARITY_COLORS = {
|
|
12
|
+
common: "white",
|
|
13
|
+
uncommon: "greenBright",
|
|
14
|
+
rare: "blueBright",
|
|
15
|
+
epic: "magentaBright",
|
|
16
|
+
legendary: "yellowBright",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const HAT_LINES = {
|
|
20
|
+
none: "",
|
|
21
|
+
crown: " \\^^^/ ",
|
|
22
|
+
tophat: " [___] ",
|
|
23
|
+
propeller: " -+- ",
|
|
24
|
+
halo: " ( ) ",
|
|
25
|
+
wizard: " /^\\ ",
|
|
26
|
+
beanie: " (___) ",
|
|
27
|
+
tinyduck: " ,> ",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const BODIES = {
|
|
31
|
+
duck: [
|
|
32
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--´ "],
|
|
33
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--´~ "],
|
|
34
|
+
[" ", " __ ", " <({E} )___ ", " ( .__> ", " `--´ "],
|
|
35
|
+
],
|
|
36
|
+
goose: [
|
|
37
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
38
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
39
|
+
[" ", " ({E}>> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
40
|
+
],
|
|
41
|
+
blob: [
|
|
42
|
+
[" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----´ "],
|
|
43
|
+
[" ", " .------. ", " ( {E} {E} ) ", " ( ) ", " `------´ "],
|
|
44
|
+
[" ", " .--. ", " ({E} {E}) ", " ( ) ", " `--´ "],
|
|
45
|
+
],
|
|
46
|
+
cat: [
|
|
47
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( ω ) ", ' (")_(") '],
|
|
48
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( ω ) ", ' (")_(")~ '],
|
|
49
|
+
[" ", " /\\-/\\ ", " ( {E} {E}) ", " ( ω ) ", ' (")_(") '],
|
|
50
|
+
],
|
|
51
|
+
dragon: [
|
|
52
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-´ "],
|
|
53
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ) ", " `-vvvv-´ "],
|
|
54
|
+
[" ~ ~ ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-´ "],
|
|
55
|
+
],
|
|
56
|
+
octopus: [
|
|
57
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
58
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " \\/\\/\\/\\/ "],
|
|
59
|
+
[" o ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
60
|
+
],
|
|
61
|
+
owl: [
|
|
62
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----´ "],
|
|
63
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " .----. "],
|
|
64
|
+
[" ", " /\\ /\\ ", " (({E})(-)) ", " ( >< ) ", " `----´ "],
|
|
65
|
+
],
|
|
66
|
+
penguin: [
|
|
67
|
+
[" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---´ "],
|
|
68
|
+
[" ", " .---. ", " ({E}>{E}) ", " |( )| ", " `---´ "],
|
|
69
|
+
[" .---. ", " ({E}>{E}) ", " /( )\\ ", " `---´ ", " ~ ~ "],
|
|
70
|
+
],
|
|
71
|
+
turtle: [
|
|
72
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
73
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
74
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[======]\\ ", " `` `` "],
|
|
75
|
+
],
|
|
76
|
+
snail: [
|
|
77
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
|
|
78
|
+
[" ", " {E} .--. ", " | ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
|
|
79
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~ "],
|
|
80
|
+
],
|
|
81
|
+
ghost: [
|
|
82
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
|
|
83
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " `~`~~`~` "],
|
|
84
|
+
[" ~ ~ ", " .----. ", " / {E} {E} \\ ", " | | ", " ~~`~~`~~ "],
|
|
85
|
+
],
|
|
86
|
+
axolotl: [
|
|
87
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
|
|
88
|
+
[" ", "~}(______){~", "~}({E} .. {E}){~", " ( .--. ) ", " (_/ \\_) "],
|
|
89
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( -- ) ", " ~_/ \\_~ "],
|
|
90
|
+
],
|
|
91
|
+
capybara: [
|
|
92
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------´ "],
|
|
93
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( Oo ) ", " `------´ "],
|
|
94
|
+
[" ~ ~ ", " u______n ", " ( {E} {E} ) ", " ( oo ) ", " `------´ "],
|
|
95
|
+
],
|
|
96
|
+
cactus: [
|
|
97
|
+
[" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
98
|
+
[" ", " ____ ", " n |{E} {E}| n ", " |_| |_| ", " | | "],
|
|
99
|
+
[" n n ", " | ____ | ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
100
|
+
],
|
|
101
|
+
robot: [
|
|
102
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------´ "],
|
|
103
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ -==- ] ", " `------´ "],
|
|
104
|
+
[" * ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------´ "],
|
|
105
|
+
],
|
|
106
|
+
rabbit: [
|
|
107
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", ' (")__(") '],
|
|
108
|
+
[" ", " (|__/) ", " ( {E} {E} ) ", " =( .. )= ", ' (")__(") '],
|
|
109
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( . . )= ", ' (")__(") '],
|
|
110
|
+
],
|
|
111
|
+
mushroom: [
|
|
112
|
+
[" ", " .-o-OO-o-. ", "(__________)", " |{E} {E}| ", " |____| "],
|
|
113
|
+
[" ", " .-O-oo-O-. ", "(__________)", " |{E} {E}| ", " |____| "],
|
|
114
|
+
[" . o . ", " .-o-OO-o-. ", "(__________)", " |{E} {E}| ", " |____| "],
|
|
115
|
+
],
|
|
116
|
+
chonk: [
|
|
117
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------´ "],
|
|
118
|
+
[" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------´ "],
|
|
119
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------´~ "],
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function renderSprite(bones, frame = 0) {
|
|
124
|
+
const frames = BODIES[bones.species];
|
|
125
|
+
const body = frames[frame % frames.length].map((line) => line.replaceAll("{E}", bones.eye));
|
|
126
|
+
const lines = [...body];
|
|
127
|
+
if (bones.hat !== "none" && !lines[0].trim()) {
|
|
128
|
+
lines[0] = HAT_LINES[bones.hat];
|
|
129
|
+
}
|
|
130
|
+
if (!lines[0].trim() && frames.every((f) => !f[0].trim())) lines.shift();
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function colorizeSprite(lines, rarity) {
|
|
135
|
+
const colorFn = chalk[RARITY_COLORS[rarity]] ?? chalk.white;
|
|
136
|
+
return lines.map((line) => colorFn(line));
|
|
137
|
+
}
|
package/ui.jsx
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { render, Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
// Simple spinner — avoids @inkjs/ui dependency for one component
|
|
4
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
5
|
+
function Spinner({ label }) {
|
|
6
|
+
const [frame, setFrame] = useState(0);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
9
|
+
return () => clearInterval(timer);
|
|
10
|
+
}, []);
|
|
11
|
+
return <Text><Text color="cyan">{SPINNER_FRAMES[frame]}</Text> {label}</Text>;
|
|
12
|
+
}
|
|
13
|
+
import { renderSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
14
|
+
import { existsSync, copyFileSync } from "fs";
|
|
15
|
+
|
|
16
|
+
// ── Shared Components ───────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function KeyHint({ children }) {
|
|
19
|
+
return <Text italic dimColor>{children}</Text>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ListSelect({ label, options, defaultValue, onChange, onSubmit, onBack, isActive }) {
|
|
23
|
+
const [idx, setIdx] = useState(() => Math.max(0, options.findIndex((o) => o.value === defaultValue)));
|
|
24
|
+
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (key.escape && onBack) { onBack(); return; }
|
|
27
|
+
if (key.upArrow || key.leftArrow) {
|
|
28
|
+
const next = (idx - 1 + options.length) % options.length;
|
|
29
|
+
setIdx(next);
|
|
30
|
+
if (onChange) onChange(options[next].value);
|
|
31
|
+
}
|
|
32
|
+
if (key.downArrow || key.rightArrow) {
|
|
33
|
+
const next = (idx + 1) % options.length;
|
|
34
|
+
setIdx(next);
|
|
35
|
+
if (onChange) onChange(options[next].value);
|
|
36
|
+
}
|
|
37
|
+
if (key.return) onSubmit(options[idx].value);
|
|
38
|
+
}, { isActive: isActive !== false });
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Box flexDirection="column">
|
|
42
|
+
{label && <Text bold>{label}</Text>}
|
|
43
|
+
{options.map((opt, i) => (
|
|
44
|
+
<Text key={opt.value}>
|
|
45
|
+
<Text color={i === idx ? "cyan" : undefined}>
|
|
46
|
+
{i === idx ? "❯ " : " "}{opt.label}
|
|
47
|
+
</Text>
|
|
48
|
+
{opt.hint && <Text dimColor> ({opt.hint})</Text>}
|
|
49
|
+
</Text>
|
|
50
|
+
))}
|
|
51
|
+
<KeyHint>{onBack ? "↑↓ select · enter confirm · esc back" : "↑↓ select · enter confirm"}</KeyHint>
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ConfirmSelect({ label, onConfirm, onCancel, onBack, isActive }) {
|
|
57
|
+
const [idx, setIdx] = useState(0);
|
|
58
|
+
const options = [
|
|
59
|
+
{ label: "Yes", value: true },
|
|
60
|
+
{ label: "No", value: false },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
useInput((input, key) => {
|
|
64
|
+
if (key.escape && onBack) { onBack(); return; }
|
|
65
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
66
|
+
setIdx(idx === 0 ? 1 : 0);
|
|
67
|
+
}
|
|
68
|
+
if (key.return) {
|
|
69
|
+
if (options[idx].value) onConfirm();
|
|
70
|
+
else onCancel();
|
|
71
|
+
}
|
|
72
|
+
}, { isActive: isActive !== false });
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Box flexDirection="column">
|
|
76
|
+
<Text bold>{label}</Text>
|
|
77
|
+
<Box gap={2}>
|
|
78
|
+
{options.map((opt, i) => (
|
|
79
|
+
<Text key={opt.label} color={i === idx ? "cyan" : undefined}>
|
|
80
|
+
{i === idx ? "❯ " : " "}{opt.label}
|
|
81
|
+
</Text>
|
|
82
|
+
))}
|
|
83
|
+
</Box>
|
|
84
|
+
<KeyHint>{onBack ? "←→ select · enter confirm · esc back" : "←→ select · enter confirm"}</KeyHint>
|
|
85
|
+
</Box>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── PreviewCard ─────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function PreviewCard({ species, rarity, eye, hat, shiny, stats }) {
|
|
92
|
+
const color = RARITY_COLORS[rarity] ?? "white";
|
|
93
|
+
const stars = RARITY_STARS[rarity] ?? "";
|
|
94
|
+
const sprite = renderSprite({ species, eye, hat });
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1}>
|
|
98
|
+
<Box>
|
|
99
|
+
<Box flexDirection="column">
|
|
100
|
+
{sprite.map((line, i) => (
|
|
101
|
+
<Text key={i} color={color}>{line}</Text>
|
|
102
|
+
))}
|
|
103
|
+
</Box>
|
|
104
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
105
|
+
<Text bold>{species}</Text>
|
|
106
|
+
<Text color={color}>{rarity}{shiny ? " ✦shiny" : ""}</Text>
|
|
107
|
+
<Text dimColor>eye:{eye} hat:{hat}</Text>
|
|
108
|
+
<Text>{stars}</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
</Box>
|
|
111
|
+
{stats && (
|
|
112
|
+
<Box flexDirection="column" marginTop={1}>
|
|
113
|
+
{Object.entries(stats).map(([k, v]) => {
|
|
114
|
+
const filled = Math.min(10, Math.max(0, Math.round(v / 10)));
|
|
115
|
+
return (
|
|
116
|
+
<Text key={k}>
|
|
117
|
+
<Text>{k.padEnd(10)} </Text>
|
|
118
|
+
<Text color={color}>{"█".repeat(filled)}</Text>
|
|
119
|
+
<Text dimColor>{"░".repeat(10 - filled)}</Text>
|
|
120
|
+
<Text> {String(v).padStart(3)}</Text>
|
|
121
|
+
</Text>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</Box>
|
|
125
|
+
)}
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Step Components ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function ShowCurrentStep({ isActive }) {
|
|
133
|
+
const { exit } = useApp();
|
|
134
|
+
|
|
135
|
+
useInput(() => {
|
|
136
|
+
exit();
|
|
137
|
+
}, { isActive });
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Box flexDirection="column">
|
|
141
|
+
<Text color="green">✓ Current companion shown above.</Text>
|
|
142
|
+
<KeyHint>Press any key to exit</KeyHint>
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function SpeciesStep({ speciesList, current, onChange, onSubmit, onBack, isActive }) {
|
|
148
|
+
const [idx, setIdx] = useState(Math.max(0, speciesList.indexOf(current)));
|
|
149
|
+
|
|
150
|
+
useInput((input, key) => {
|
|
151
|
+
if (key.escape && onBack) { onBack(); return; }
|
|
152
|
+
if (key.leftArrow || key.upArrow) {
|
|
153
|
+
const next = (idx - 1 + speciesList.length) % speciesList.length;
|
|
154
|
+
setIdx(next);
|
|
155
|
+
onChange(speciesList[next]);
|
|
156
|
+
}
|
|
157
|
+
if (key.rightArrow || key.downArrow) {
|
|
158
|
+
const next = (idx + 1) % speciesList.length;
|
|
159
|
+
setIdx(next);
|
|
160
|
+
onChange(speciesList[next]);
|
|
161
|
+
}
|
|
162
|
+
if (key.return) onSubmit();
|
|
163
|
+
}, { isActive });
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Box flexDirection="column">
|
|
167
|
+
<Text bold>Species: <Text color="cyan">{speciesList[idx]}</Text> <Text dimColor>({idx + 1}/{speciesList.length})</Text></Text>
|
|
168
|
+
<KeyHint>←→ browse · enter select · esc back</KeyHint>
|
|
169
|
+
</Box>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
174
|
+
const [progress, setProgress] = useState("");
|
|
175
|
+
const [error, setError] = useState(null);
|
|
176
|
+
const cancelRef = useRef(false);
|
|
177
|
+
const { exit } = useApp();
|
|
178
|
+
|
|
179
|
+
useInput((input, key) => {
|
|
180
|
+
if (key.escape) {
|
|
181
|
+
cancelRef.current = true;
|
|
182
|
+
exit();
|
|
183
|
+
}
|
|
184
|
+
}, { isActive });
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
(async () => {
|
|
188
|
+
const found = await bruteForce(userId, target, (checked, elapsed) => {
|
|
189
|
+
if (!cancelRef.current) {
|
|
190
|
+
setProgress(`${(checked / 1e6).toFixed(0)}M salts checked (${(elapsed / 1000).toFixed(1)}s)`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (cancelRef.current) return;
|
|
194
|
+
if (found) onFound(found);
|
|
195
|
+
else setError("No matching salt found. Try relaxing constraints.");
|
|
196
|
+
})();
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
if (error) {
|
|
200
|
+
return (
|
|
201
|
+
<Box flexDirection="column">
|
|
202
|
+
<Text color="red">✗ {error}</Text>
|
|
203
|
+
<KeyHint>Press esc to exit</KeyHint>
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Box flexDirection="column">
|
|
210
|
+
<Spinner label={progress || "Searching for matching salt..."} />
|
|
211
|
+
<KeyHint>esc to cancel</KeyHint>
|
|
212
|
+
</Box>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function DoneStep({ messages, isActive }) {
|
|
217
|
+
const { exit } = useApp();
|
|
218
|
+
|
|
219
|
+
useInput(() => {
|
|
220
|
+
exit();
|
|
221
|
+
}, { isActive });
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<Box flexDirection="column">
|
|
225
|
+
{messages.map((msg, i) => (
|
|
226
|
+
<Text key={i} color="green">✓ {msg}</Text>
|
|
227
|
+
))}
|
|
228
|
+
<Box marginTop={1}>
|
|
229
|
+
<Text bold>Done! Restart Claude Code and run /buddy to hatch your new companion.</Text>
|
|
230
|
+
</Box>
|
|
231
|
+
<KeyHint>Press any key to exit</KeyHint>
|
|
232
|
+
</Box>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Step Flow ───────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
const STEP_ORDER = ["action", "species", "rarity", "eye", "hat", "shiny", "confirm"];
|
|
239
|
+
|
|
240
|
+
function getPrevStep(current, rarity) {
|
|
241
|
+
const idx = STEP_ORDER.indexOf(current);
|
|
242
|
+
if (idx <= 0) return null;
|
|
243
|
+
let prev = STEP_ORDER[idx - 1];
|
|
244
|
+
// Skip hat when going back if common
|
|
245
|
+
if (prev === "hat" && rarity === "common") prev = "eye";
|
|
246
|
+
return prev;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Main App ────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function App({ opts }) {
|
|
252
|
+
const { exit } = useApp();
|
|
253
|
+
const {
|
|
254
|
+
currentRoll, currentSalt, binaryPath, configPath, userId,
|
|
255
|
+
bruteForce, patchBinary, resignBinary, clearCompanion, isClaudeRunning,
|
|
256
|
+
rollFrom, matches, SPECIES, RARITIES, RARITY_LABELS, EYES, HATS,
|
|
257
|
+
} = opts;
|
|
258
|
+
|
|
259
|
+
const [step, setStep] = useState("action");
|
|
260
|
+
const [species, setSpecies] = useState(currentRoll.species);
|
|
261
|
+
const [rarity, setRarity] = useState(currentRoll.rarity);
|
|
262
|
+
const [eye, setEye] = useState(currentRoll.eye);
|
|
263
|
+
const [hat, setHat] = useState(currentRoll.hat);
|
|
264
|
+
const [shiny, setShiny] = useState(currentRoll.shiny);
|
|
265
|
+
const [found, setFound] = useState(null);
|
|
266
|
+
const [doneMessages, setDoneMessages] = useState([]);
|
|
267
|
+
|
|
268
|
+
const showStats = step === "showCurrent" || step === "result" || step === "done";
|
|
269
|
+
const displayRoll = found ? found.result : { species, rarity, eye, hat, shiny, stats: currentRoll.stats };
|
|
270
|
+
|
|
271
|
+
const goBack = (toStep) => {
|
|
272
|
+
const prev = toStep || getPrevStep(step, rarity);
|
|
273
|
+
if (prev) setStep(prev);
|
|
274
|
+
else exit();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<Box flexDirection="column" padding={1}>
|
|
279
|
+
<Text bold dimColor>buddy-reroll</Text>
|
|
280
|
+
|
|
281
|
+
<PreviewCard
|
|
282
|
+
species={displayRoll.species}
|
|
283
|
+
rarity={displayRoll.rarity}
|
|
284
|
+
eye={displayRoll.eye}
|
|
285
|
+
hat={displayRoll.hat}
|
|
286
|
+
shiny={displayRoll.shiny}
|
|
287
|
+
stats={showStats ? displayRoll.stats : null}
|
|
288
|
+
/>
|
|
289
|
+
|
|
290
|
+
<Box marginTop={1}>
|
|
291
|
+
{step === "action" && (
|
|
292
|
+
<ListSelect
|
|
293
|
+
label="What would you like to do?"
|
|
294
|
+
options={[
|
|
295
|
+
{ label: "Reroll companion", value: "reroll" },
|
|
296
|
+
{ label: "Restore original", value: "restore" },
|
|
297
|
+
{ label: "Show current", value: "current" },
|
|
298
|
+
]}
|
|
299
|
+
isActive={step === "action"}
|
|
300
|
+
onSubmit={(action) => {
|
|
301
|
+
if (action === "current") {
|
|
302
|
+
setStep("showCurrent");
|
|
303
|
+
} else if (action === "restore") {
|
|
304
|
+
const backupPath = binaryPath + ".backup";
|
|
305
|
+
if (!existsSync(backupPath)) {
|
|
306
|
+
setDoneMessages(["No backup found. Nothing to restore."]);
|
|
307
|
+
setStep("done");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
copyFileSync(backupPath, binaryPath);
|
|
311
|
+
resignBinary(binaryPath);
|
|
312
|
+
clearCompanion(configPath);
|
|
313
|
+
setDoneMessages(["Restored! Restart Claude Code and run /buddy."]);
|
|
314
|
+
setStep("done");
|
|
315
|
+
} else {
|
|
316
|
+
setStep("species");
|
|
317
|
+
}
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{step === "showCurrent" && (
|
|
323
|
+
<ShowCurrentStep isActive={step === "showCurrent"} />
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
{step === "species" && (
|
|
327
|
+
<SpeciesStep
|
|
328
|
+
speciesList={SPECIES}
|
|
329
|
+
current={species}
|
|
330
|
+
onChange={setSpecies}
|
|
331
|
+
onSubmit={() => setStep("rarity")}
|
|
332
|
+
onBack={() => goBack("action")}
|
|
333
|
+
isActive={step === "species"}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{step === "rarity" && (
|
|
338
|
+
<ListSelect
|
|
339
|
+
label="Rarity"
|
|
340
|
+
options={RARITIES.map((r) => ({ label: RARITY_LABELS[r], value: r }))}
|
|
341
|
+
defaultValue={rarity}
|
|
342
|
+
onChange={(r) => {
|
|
343
|
+
setRarity(r);
|
|
344
|
+
if (r === "common") setHat("none");
|
|
345
|
+
}}
|
|
346
|
+
onSubmit={() => setStep("eye")}
|
|
347
|
+
onBack={() => goBack()}
|
|
348
|
+
isActive={step === "rarity"}
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{step === "eye" && (
|
|
353
|
+
<ListSelect
|
|
354
|
+
label="Eye"
|
|
355
|
+
options={EYES.map((e) => ({ label: e, value: e }))}
|
|
356
|
+
defaultValue={eye}
|
|
357
|
+
onChange={setEye}
|
|
358
|
+
onSubmit={() => setStep(rarity === "common" ? "shiny" : "hat")}
|
|
359
|
+
onBack={() => goBack()}
|
|
360
|
+
isActive={step === "eye"}
|
|
361
|
+
/>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{step === "hat" && (
|
|
365
|
+
<ListSelect
|
|
366
|
+
label="Hat"
|
|
367
|
+
options={HATS.map((h) => ({ label: h, value: h }))}
|
|
368
|
+
defaultValue={hat === "none" ? "crown" : hat}
|
|
369
|
+
onChange={setHat}
|
|
370
|
+
onSubmit={() => setStep("shiny")}
|
|
371
|
+
onBack={() => goBack()}
|
|
372
|
+
isActive={step === "hat"}
|
|
373
|
+
/>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{step === "shiny" && (
|
|
377
|
+
<ConfirmSelect
|
|
378
|
+
label="Shiny?"
|
|
379
|
+
isActive={step === "shiny"}
|
|
380
|
+
onConfirm={() => {
|
|
381
|
+
setShiny(true);
|
|
382
|
+
const target = { species, rarity, eye, hat: rarity === "common" ? "none" : hat, shiny: true };
|
|
383
|
+
if (matches(currentRoll, target)) {
|
|
384
|
+
setDoneMessages(["Already matching! No changes needed."]);
|
|
385
|
+
setStep("done");
|
|
386
|
+
} else {
|
|
387
|
+
setStep("confirm");
|
|
388
|
+
}
|
|
389
|
+
}}
|
|
390
|
+
onCancel={() => {
|
|
391
|
+
setShiny(false);
|
|
392
|
+
const target = { species, rarity, eye, hat: rarity === "common" ? "none" : hat, shiny: false };
|
|
393
|
+
if (matches(currentRoll, target)) {
|
|
394
|
+
setDoneMessages(["Already matching! No changes needed."]);
|
|
395
|
+
setStep("done");
|
|
396
|
+
} else {
|
|
397
|
+
setStep("confirm");
|
|
398
|
+
}
|
|
399
|
+
}}
|
|
400
|
+
onBack={() => goBack()}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{step === "confirm" && (
|
|
405
|
+
<Box flexDirection="column">
|
|
406
|
+
<Text>Target: <Text bold>{species}</Text> / <Text bold>{rarity}</Text> / eye:{eye} / hat:{rarity === "common" ? "none" : hat}{shiny ? " / shiny" : ""}</Text>
|
|
407
|
+
{isClaudeRunning() && <Text color="yellow">⚠ Claude Code appears to be running. Quit it before patching.</Text>}
|
|
408
|
+
<ConfirmSelect
|
|
409
|
+
label="Search and apply?"
|
|
410
|
+
isActive={step === "confirm"}
|
|
411
|
+
onConfirm={() => setStep("search")}
|
|
412
|
+
onCancel={() => exit()}
|
|
413
|
+
onBack={() => goBack()}
|
|
414
|
+
/>
|
|
415
|
+
</Box>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
{step === "search" && (
|
|
419
|
+
<SearchStep
|
|
420
|
+
userId={userId}
|
|
421
|
+
target={{ species, rarity, eye, hat: rarity === "common" ? "none" : hat, shiny }}
|
|
422
|
+
bruteForce={bruteForce}
|
|
423
|
+
onFound={(f) => { setFound(f); setStep("result"); }}
|
|
424
|
+
onFail={() => {
|
|
425
|
+
setDoneMessages(["No matching salt found. Try relaxing constraints."]);
|
|
426
|
+
setStep("done");
|
|
427
|
+
}}
|
|
428
|
+
isActive={step === "search"}
|
|
429
|
+
/>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{step === "result" && (
|
|
433
|
+
<Box flexDirection="column">
|
|
434
|
+
<Text bold color="green">✓ Found in {found.checked.toLocaleString()} attempts ({(found.elapsed / 1000).toFixed(1)}s)</Text>
|
|
435
|
+
<ConfirmSelect
|
|
436
|
+
label="Apply patch?"
|
|
437
|
+
isActive={step === "result"}
|
|
438
|
+
onConfirm={() => {
|
|
439
|
+
const msgs = [];
|
|
440
|
+
const backupPath = binaryPath + ".backup";
|
|
441
|
+
if (!existsSync(backupPath)) {
|
|
442
|
+
copyFileSync(binaryPath, backupPath);
|
|
443
|
+
msgs.push(`Backup saved to ${backupPath}`);
|
|
444
|
+
}
|
|
445
|
+
const count = patchBinary(binaryPath, currentSalt, found.salt);
|
|
446
|
+
msgs.push(`Patched ${count} occurrence(s)`);
|
|
447
|
+
if (resignBinary(binaryPath)) msgs.push("Binary re-signed (ad-hoc codesign)");
|
|
448
|
+
clearCompanion(configPath);
|
|
449
|
+
msgs.push("Companion data cleared");
|
|
450
|
+
setDoneMessages(msgs);
|
|
451
|
+
setStep("done");
|
|
452
|
+
}}
|
|
453
|
+
onCancel={() => exit()}
|
|
454
|
+
/>
|
|
455
|
+
</Box>
|
|
456
|
+
)}
|
|
457
|
+
|
|
458
|
+
{step === "done" && <DoneStep messages={doneMessages} isActive={step === "done"} />}
|
|
459
|
+
</Box>
|
|
460
|
+
</Box>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── Export ───────────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
export async function runInteractiveUI(opts) {
|
|
467
|
+
const { waitUntilExit } = render(<App opts={opts} />);
|
|
468
|
+
await waitUntilExit();
|
|
469
|
+
}
|