buddy-reroll 0.3.0 → 0.3.2
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 +90 -21
- package/lib/estimator.js +1 -1
- package/lib/finder.js +13 -5
- package/package.json +1 -1
- package/scripts/worker.js +7 -1
- package/ui-fallback.js +17 -4
- package/ui.jsx +27 -23
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, copyFileSync, renameSync, unlinkSync } from "fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, renameSync, unlinkSync, statSync, chmodSync } from "fs";
|
|
4
4
|
import { platform } from "os";
|
|
5
|
-
import { execFileSync } from "child_process";
|
|
5
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { renderSprite, colorizeSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
@@ -27,8 +27,16 @@ import { installHook, removeHook, storeSalt, readStoredSalt } from "./lib/hooks.
|
|
|
27
27
|
|
|
28
28
|
const IS_BUN = typeof Bun !== "undefined";
|
|
29
29
|
const IS_APPLY_HOOK = process.argv.includes("--apply-hook");
|
|
30
|
+
|
|
30
31
|
if (!IS_BUN && !IS_APPLY_HOOK) {
|
|
31
|
-
|
|
32
|
+
try {
|
|
33
|
+
const cmd = platform() === "win32" ? "where.exe" : "which";
|
|
34
|
+
const bunPath = execFileSync(cmd, ["bun"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n")[0];
|
|
35
|
+
if (bunPath) {
|
|
36
|
+
const result = spawnSync(bunPath, process.argv.slice(1), { stdio: "inherit" });
|
|
37
|
+
process.exit(result.status ?? 0);
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
function getUserId(configPath) {
|
|
@@ -67,6 +75,7 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
|
|
|
67
75
|
throw new Error(`Salt length mismatch: "${oldSalt}" (${oldSalt.length}) vs "${newSalt}" (${newSalt.length})`);
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
const originalMode = statSync(binaryPath).mode;
|
|
70
79
|
const data = readFileSync(binaryPath);
|
|
71
80
|
const oldBuf = Buffer.from(oldSalt);
|
|
72
81
|
const newBuf = Buffer.from(newSalt);
|
|
@@ -88,8 +97,22 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
|
|
|
88
97
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
89
98
|
try {
|
|
90
99
|
const tmpPath = binaryPath + ".tmp";
|
|
91
|
-
writeFileSync(tmpPath, data);
|
|
92
|
-
|
|
100
|
+
writeFileSync(tmpPath, data, { mode: originalMode });
|
|
101
|
+
try {
|
|
102
|
+
renameSync(tmpPath, binaryPath);
|
|
103
|
+
} catch {
|
|
104
|
+
if (existsSync(binaryPath + ".backup")) {
|
|
105
|
+
try { unlinkSync(binaryPath); } catch {}
|
|
106
|
+
}
|
|
107
|
+
renameSync(tmpPath, binaryPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
chmodSync(binaryPath, originalMode);
|
|
111
|
+
|
|
112
|
+
const verify = readFileSync(binaryPath);
|
|
113
|
+
const found = verify.indexOf(Buffer.from(newSalt));
|
|
114
|
+
if (found === -1) throw new Error("Patch verification failed — new salt not found after write");
|
|
115
|
+
|
|
93
116
|
return count;
|
|
94
117
|
} catch (err) {
|
|
95
118
|
try { unlinkSync(binaryPath + ".tmp"); } catch {}
|
|
@@ -97,7 +120,10 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
|
|
|
97
120
|
sleepMs(2000);
|
|
98
121
|
continue;
|
|
99
122
|
}
|
|
100
|
-
|
|
123
|
+
if (isWin && (err.code === "EPERM" || err.code === "EBUSY")) {
|
|
124
|
+
throw new Error("Can't write — Claude Code might still be running. Close it and try again.");
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Failed to write: ${err.message}`);
|
|
101
127
|
}
|
|
102
128
|
}
|
|
103
129
|
}
|
|
@@ -106,10 +132,12 @@ function resignBinary(binaryPath) {
|
|
|
106
132
|
if (platform() !== "darwin") return false;
|
|
107
133
|
try {
|
|
108
134
|
execFileSync("codesign", ["-s", "-", "--force", binaryPath], {
|
|
109
|
-
stdio: "ignore",
|
|
135
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
+
timeout: 30000,
|
|
110
137
|
});
|
|
111
138
|
return true;
|
|
112
|
-
} catch {
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn(` ⚠ Code signing failed: ${err.message}\n Try manually: codesign --force --sign - "${binaryPath}"`);
|
|
113
141
|
return false;
|
|
114
142
|
}
|
|
115
143
|
}
|
|
@@ -130,7 +158,32 @@ function fail(message) {
|
|
|
130
158
|
|
|
131
159
|
function readCurrentCompanion(binaryPath, userId) {
|
|
132
160
|
const binaryData = readFileSync(binaryPath);
|
|
133
|
-
|
|
161
|
+
let currentSalt = findCurrentSalt(binaryData);
|
|
162
|
+
|
|
163
|
+
if (!currentSalt) {
|
|
164
|
+
const stored = readStoredSalt();
|
|
165
|
+
if (stored) {
|
|
166
|
+
const storedBuf = Buffer.from(stored.salt);
|
|
167
|
+
if (binaryData.includes(storedBuf)) {
|
|
168
|
+
currentSalt = stored.salt;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!currentSalt) {
|
|
174
|
+
const backupPath = binaryPath + ".backup";
|
|
175
|
+
if (existsSync(backupPath)) {
|
|
176
|
+
console.log(" ⚠ Can't find salt in binary — restoring from backup...");
|
|
177
|
+
try {
|
|
178
|
+
copyFileSync(backupPath, binaryPath);
|
|
179
|
+
resignBinary(binaryPath);
|
|
180
|
+
const restored = readFileSync(binaryPath);
|
|
181
|
+
currentSalt = findCurrentSalt(restored);
|
|
182
|
+
if (currentSalt) console.log(" ✓ Restored successfully.");
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
134
187
|
if (!currentSalt) fail(" ✗ Couldn't read your current buddy from the Claude binary.");
|
|
135
188
|
return { currentSalt, currentRoll: rollFrom(currentSalt, userId) };
|
|
136
189
|
}
|
|
@@ -216,7 +269,7 @@ async function interactiveMode(binaryPath, configPath, userId) {
|
|
|
216
269
|
binaryPath,
|
|
217
270
|
configPath,
|
|
218
271
|
userId,
|
|
219
|
-
bruteForce,
|
|
272
|
+
bruteForce: parallelBruteForce,
|
|
220
273
|
patchBinary,
|
|
221
274
|
resignBinary,
|
|
222
275
|
clearCompanion,
|
|
@@ -230,6 +283,8 @@ async function interactiveMode(binaryPath, configPath, userId) {
|
|
|
230
283
|
EYES,
|
|
231
284
|
HATS,
|
|
232
285
|
STAT_NAMES,
|
|
286
|
+
storeSalt,
|
|
287
|
+
installHook,
|
|
233
288
|
};
|
|
234
289
|
|
|
235
290
|
try {
|
|
@@ -277,25 +332,30 @@ async function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
277
332
|
const target = buildTargetFromArgs(args);
|
|
278
333
|
if (Object.keys(target).length === 0) fail(" ✗ Tell me what kind of buddy you want! Use --help to see options.");
|
|
279
334
|
|
|
280
|
-
const
|
|
281
|
-
console.log(` Target: ${Object.entries(target).map(([k, v]) => `${k}=${v}`).join(" ")}`);
|
|
282
|
-
console.log(` This might take ~${expected.toLocaleString()} tries\n`);
|
|
335
|
+
const patchability = assertPatchable(binaryPath);
|
|
283
336
|
|
|
284
337
|
if (matches(currentRoll, target)) {
|
|
285
338
|
console.log(" ✓ Your buddy already looks like that!\n" + formatCompanionCard(currentRoll));
|
|
286
339
|
return;
|
|
287
340
|
}
|
|
288
341
|
|
|
289
|
-
const
|
|
342
|
+
const expected = estimateAttempts(target);
|
|
343
|
+
console.log(` Target: ${Object.entries(target).map(([k, v]) => `${k}=${v}`).join(" ")}`);
|
|
344
|
+
console.log(` This might take ~${expected.toLocaleString()} tries\n`);
|
|
290
345
|
|
|
291
346
|
if (isClaudeRunning()) {
|
|
292
347
|
console.warn(" ⚠ Claude Code is still running — close it first so the changes stick.");
|
|
293
348
|
}
|
|
294
349
|
|
|
295
350
|
console.log(" Looking for your buddy...");
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
351
|
+
let found;
|
|
352
|
+
try {
|
|
353
|
+
found = await parallelBruteForce(userId, target, (attempts, elapsed, est, workers) => {
|
|
354
|
+
process.stdout.write(`\r ${formatProgress(attempts, elapsed, est, workers)}`);
|
|
355
|
+
});
|
|
356
|
+
} catch (err) {
|
|
357
|
+
fail(`\n ✗ ${err.message}`);
|
|
358
|
+
}
|
|
299
359
|
if (!found) fail("\n ✗ Couldn't find a match. Try being less picky!");
|
|
300
360
|
console.log(`\n ✓ Found it! (${found.checked.toLocaleString()} tries, ${(found.elapsed / 1000).toFixed(1)}s)`);
|
|
301
361
|
console.log(formatCompanionCard(found.result));
|
|
@@ -316,8 +376,9 @@ async function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
316
376
|
if (resignBinary(binaryPath)) console.log(" Re-signed for macOS ✓");
|
|
317
377
|
clearCompanion(configPath);
|
|
318
378
|
storeSalt(found.salt);
|
|
379
|
+
try { installHook(); } catch {}
|
|
319
380
|
console.log(" Cleaned up old buddy data ✓");
|
|
320
|
-
console.log("\n All set! Restart Claude Code and say /buddy to meet your new friend.\n");
|
|
381
|
+
console.log("\n All set! Your buddy will stick around even after Claude updates.\n Restart Claude Code and say /buddy to meet your new friend.\n");
|
|
321
382
|
} catch (err) {
|
|
322
383
|
fail(` ✗ ${err.message}`);
|
|
323
384
|
}
|
|
@@ -365,8 +426,7 @@ async function main() {
|
|
|
365
426
|
buddy-reroll --current Show current buddy
|
|
366
427
|
buddy-reroll --doctor Check setup
|
|
367
428
|
buddy-reroll --restore Undo changes
|
|
368
|
-
buddy-reroll --
|
|
369
|
-
buddy-reroll --unhook Stop keeping after updates
|
|
429
|
+
buddy-reroll --unhook Stop auto-keeping after updates
|
|
370
430
|
|
|
371
431
|
Appearance (all optional — skip to leave random):
|
|
372
432
|
--species <name> ${SPECIES.join(", ")}
|
|
@@ -413,8 +473,17 @@ async function main() {
|
|
|
413
473
|
if (currentSalt === stored.salt) process.exit(0);
|
|
414
474
|
const patchability = getPatchability(bp);
|
|
415
475
|
if (!patchability.ok) process.exit(0);
|
|
476
|
+
const backupPath = patchability.backupPath;
|
|
477
|
+
if (!existsSync(backupPath)) copyFileSync(bp, backupPath);
|
|
416
478
|
patchBinary(bp, currentSalt, stored.salt);
|
|
417
|
-
|
|
479
|
+
if (platform() === "darwin") {
|
|
480
|
+
try {
|
|
481
|
+
execFileSync("codesign", ["-s", "-", "--force", bp], { stdio: "ignore", timeout: 30000 });
|
|
482
|
+
} catch {
|
|
483
|
+
copyFileSync(backupPath, bp);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
418
487
|
clearCompanion(cp);
|
|
419
488
|
} catch {}
|
|
420
489
|
process.exit(0);
|
package/lib/estimator.js
CHANGED
|
@@ -8,7 +8,7 @@ export function estimateAttempts(target) {
|
|
|
8
8
|
if (target.species) probability *= 1 / 18;
|
|
9
9
|
if (target.rarity) probability *= RARITY_WEIGHTS[target.rarity] / 100;
|
|
10
10
|
if (target.eye) probability *= 1 / 6;
|
|
11
|
-
if (target.hat && target.rarity !== "common") probability *= 1 / 8;
|
|
11
|
+
if (target.hat && target.hat !== "none" && target.rarity !== "common") probability *= 1 / 8;
|
|
12
12
|
if (target.shiny === true) probability *= 0.01;
|
|
13
13
|
if (target.peak) probability *= 1 / 5;
|
|
14
14
|
if (target.dump) probability *= 1 / 4;
|
package/lib/finder.js
CHANGED
|
@@ -14,6 +14,7 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
14
14
|
return new Promise((resolve, reject) => {
|
|
15
15
|
const children = [];
|
|
16
16
|
const workerStdout = [];
|
|
17
|
+
const workerStderr = [];
|
|
17
18
|
const workerAttempts = [];
|
|
18
19
|
let resolved = false;
|
|
19
20
|
let exited = 0;
|
|
@@ -26,6 +27,7 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
26
27
|
|
|
27
28
|
for (let i = 0; i < numWorkers; i++) {
|
|
28
29
|
workerStdout[i] = "";
|
|
30
|
+
workerStderr[i] = "";
|
|
29
31
|
workerAttempts[i] = 0;
|
|
30
32
|
|
|
31
33
|
const child = spawn(process.execPath, [WORKER_SCRIPT, userId, JSON.stringify(target)], {
|
|
@@ -38,7 +40,9 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
child.stderr.on("data", (chunk) => {
|
|
41
|
-
const
|
|
43
|
+
const text = chunk.toString();
|
|
44
|
+
workerStderr[i] += text;
|
|
45
|
+
const lines = text.split("\n").filter(Boolean);
|
|
42
46
|
for (const line of lines) {
|
|
43
47
|
try {
|
|
44
48
|
const progress = JSON.parse(line);
|
|
@@ -80,14 +84,17 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
if (exited === numWorkers && !resolved) {
|
|
83
|
-
|
|
87
|
+
const totalAttempts = workerAttempts.reduce((a, b) => a + b, 0);
|
|
88
|
+
const stderr = workerStderr.filter(Boolean).join("\n").trim();
|
|
89
|
+
const detail = stderr ? `\n Worker output: ${stderr.slice(0, 200)}` : "";
|
|
90
|
+
reject(new Error(`All ${numWorkers} workers exited without finding a match (${totalAttempts.toLocaleString()} tries).${detail}`));
|
|
84
91
|
}
|
|
85
92
|
});
|
|
86
93
|
|
|
87
|
-
child.on("error", () => {
|
|
94
|
+
child.on("error", (err) => {
|
|
88
95
|
exited++;
|
|
89
96
|
if (exited === numWorkers && !resolved) {
|
|
90
|
-
|
|
97
|
+
reject(new Error(`Worker failed to start: ${err.message}`));
|
|
91
98
|
}
|
|
92
99
|
});
|
|
93
100
|
}
|
|
@@ -97,7 +104,8 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
97
104
|
if (!resolved) {
|
|
98
105
|
resolved = true;
|
|
99
106
|
killAll();
|
|
100
|
-
|
|
107
|
+
const totalAttempts = workerAttempts.reduce((a, b) => a + b, 0);
|
|
108
|
+
reject(new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s (${totalAttempts.toLocaleString()} tries). This combination might be extremely rare — try fewer constraints.`));
|
|
101
109
|
}
|
|
102
110
|
}, timeoutMs);
|
|
103
111
|
});
|
package/package.json
CHANGED
package/scripts/worker.js
CHANGED
|
@@ -14,7 +14,13 @@ function randomSalt() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const userId = process.argv[2];
|
|
17
|
-
|
|
17
|
+
let target;
|
|
18
|
+
try {
|
|
19
|
+
target = JSON.parse(process.argv[3]);
|
|
20
|
+
} catch {
|
|
21
|
+
process.stderr.write("Invalid target JSON\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
if (!userId || !target) {
|
|
20
26
|
process.stderr.write("Usage: worker.js <userId> '<targetJSON>'\n");
|
package/ui-fallback.js
CHANGED
|
@@ -30,6 +30,7 @@ export async function runInteractiveUI(opts) {
|
|
|
30
30
|
currentRoll, currentSalt, binaryPath, configPath, userId,
|
|
31
31
|
bruteForce, patchBinary, resignBinary, clearCompanion, getPatchability, isClaudeRunning,
|
|
32
32
|
rollFrom, matches, SPECIES, RARITIES, RARITY_LABELS, EYES, HATS, STAT_NAMES,
|
|
33
|
+
storeSalt, installHook,
|
|
33
34
|
} = opts;
|
|
34
35
|
|
|
35
36
|
console.log(chalk.bold.dim("\n buddy-reroll\n"));
|
|
@@ -151,9 +152,19 @@ export async function runInteractiveUI(opts) {
|
|
|
151
152
|
if (!proceed) return;
|
|
152
153
|
|
|
153
154
|
console.log(" Looking for your buddy...");
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
let found;
|
|
156
|
+
try {
|
|
157
|
+
found = await bruteForce(userId, target, (attempts, elapsed, expected, workers) => {
|
|
158
|
+
const pct = Math.min(100, Math.round((attempts / expected) * 100));
|
|
159
|
+
const rate = attempts / (elapsed / 1000);
|
|
160
|
+
const rateStr = rate >= 1e6 ? `${(rate / 1e6).toFixed(1)}M` : `${(rate / 1e3).toFixed(1)}k`;
|
|
161
|
+
const eta = Math.max(0, (expected - attempts) / rate);
|
|
162
|
+
process.stdout.write(`\r ${pct}% | ${rateStr} tries/s | ~${Math.round(eta)}s left | ${workers} cores`);
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.log(chalk.red(`\n✗ ${err.message}`));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
157
168
|
|
|
158
169
|
if (!found) {
|
|
159
170
|
console.log(chalk.red("\n✗ Couldn't find a match. Try being less picky!"));
|
|
@@ -174,7 +185,9 @@ export async function runInteractiveUI(opts) {
|
|
|
174
185
|
if (resignBinary(binaryPath)) console.log(" Re-signed for macOS ✓");
|
|
175
186
|
clearCompanion(configPath);
|
|
176
187
|
console.log(" Cleaned up old buddy data ✓");
|
|
177
|
-
|
|
188
|
+
if (storeSalt) storeSalt(found.salt);
|
|
189
|
+
if (installHook) installHook();
|
|
190
|
+
console.log(chalk.bold("\n All set! Your buddy will stick around even after Claude updates.\n Restart Claude Code and say /buddy to meet your new friend.\n"));
|
|
178
191
|
} catch (err) {
|
|
179
192
|
console.log(chalk.red(`\n✗ ${err.message}`));
|
|
180
193
|
}
|
package/ui.jsx
CHANGED
|
@@ -130,6 +130,7 @@ function ShowCurrentStep({ isActive }) {
|
|
|
130
130
|
|
|
131
131
|
useInput(() => {
|
|
132
132
|
exit();
|
|
133
|
+
setTimeout(() => process.exit(0), 100);
|
|
133
134
|
}, { isActive });
|
|
134
135
|
|
|
135
136
|
return (
|
|
@@ -183,11 +184,21 @@ function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
|
183
184
|
if (hasStarted.current) return;
|
|
184
185
|
hasStarted.current = true;
|
|
185
186
|
(async () => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
let found;
|
|
188
|
+
try {
|
|
189
|
+
found = await bruteForce(userId, target, (attempts, elapsed, expected, workers) => {
|
|
190
|
+
if (!cancelRef.current) {
|
|
191
|
+
const pct = Math.min(100, Math.round((attempts / expected) * 100));
|
|
192
|
+
const rate = attempts / (elapsed / 1000);
|
|
193
|
+
const rateStr = rate >= 1e6 ? `${(rate / 1e6).toFixed(1)}M` : `${(rate / 1e3).toFixed(1)}k`;
|
|
194
|
+
const eta = Math.max(0, (expected - attempts) / rate);
|
|
195
|
+
setProgress(`${pct}% | ${rateStr} tries/s | ~${Math.round(eta)}s left | ${workers} cores`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
} catch {
|
|
199
|
+
if (!cancelRef.current) onFail();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
191
202
|
if (cancelRef.current) return;
|
|
192
203
|
if (found) onFound(found);
|
|
193
204
|
else onFail();
|
|
@@ -208,6 +219,7 @@ function DoneStep({ messages, isActive }) {
|
|
|
208
219
|
|
|
209
220
|
useInput(() => {
|
|
210
221
|
exit();
|
|
222
|
+
setTimeout(() => process.exit(0), 100);
|
|
211
223
|
}, { isActive });
|
|
212
224
|
|
|
213
225
|
return (
|
|
@@ -221,7 +233,7 @@ function DoneStep({ messages, isActive }) {
|
|
|
221
233
|
<Text bold>
|
|
222
234
|
{hasErrors
|
|
223
235
|
? "Something went wrong — check the issue above and try again."
|
|
224
|
-
: "All set! Restart Claude Code and say /buddy
|
|
236
|
+
: "All set! Your buddy will stick around even after updates. Restart Claude Code and say /buddy!"}
|
|
225
237
|
</Text>
|
|
226
238
|
</Box>
|
|
227
239
|
<KeyHint>Press any key to exit</KeyHint>
|
|
@@ -236,8 +248,7 @@ function getPrevStep(current, rarity, peak) {
|
|
|
236
248
|
if (idx <= 0) return null;
|
|
237
249
|
let prev = STEP_ORDER[idx - 1];
|
|
238
250
|
if (prev === "hat" && rarity === "common") prev = "eye";
|
|
239
|
-
if (prev === "dump" && peak
|
|
240
|
-
if (prev === "peak") prev = "shiny";
|
|
251
|
+
if (prev === "dump" && !peak) prev = "peak";
|
|
241
252
|
return prev;
|
|
242
253
|
}
|
|
243
254
|
|
|
@@ -247,6 +258,7 @@ function App({ opts }) {
|
|
|
247
258
|
currentRoll, currentSalt, binaryPath, configPath, userId,
|
|
248
259
|
bruteForce, patchBinary, resignBinary, clearCompanion, getPatchability, isClaudeRunning,
|
|
249
260
|
rollFrom, matches, SPECIES, RARITIES, RARITY_LABELS, EYES, HATS, STAT_NAMES,
|
|
261
|
+
storeSalt, installHook,
|
|
250
262
|
} = opts;
|
|
251
263
|
|
|
252
264
|
const [step, setStep] = useState("action");
|
|
@@ -394,21 +406,11 @@ function App({ opts }) {
|
|
|
394
406
|
isActive={step === "shiny"}
|
|
395
407
|
onConfirm={() => {
|
|
396
408
|
setShiny(true);
|
|
397
|
-
|
|
398
|
-
setDoneMessages([{ type: "success", text: "Your buddy already looks like that!" }]);
|
|
399
|
-
setStep("done");
|
|
400
|
-
} else {
|
|
401
|
-
setStep("peak");
|
|
402
|
-
}
|
|
409
|
+
setStep("peak");
|
|
403
410
|
}}
|
|
404
411
|
onCancel={() => {
|
|
405
412
|
setShiny(false);
|
|
406
|
-
|
|
407
|
-
setDoneMessages([{ type: "success", text: "Your buddy already looks like that!" }]);
|
|
408
|
-
setStep("done");
|
|
409
|
-
} else {
|
|
410
|
-
setStep("peak");
|
|
411
|
-
}
|
|
413
|
+
setStep("peak");
|
|
412
414
|
}}
|
|
413
415
|
onBack={() => goBack()}
|
|
414
416
|
/>
|
|
@@ -416,7 +418,7 @@ function App({ opts }) {
|
|
|
416
418
|
|
|
417
419
|
{step === "peak" && (
|
|
418
420
|
<ListSelect
|
|
419
|
-
label="
|
|
421
|
+
label="Best at"
|
|
420
422
|
options={[
|
|
421
423
|
{ label: "Any (random)", value: "any" },
|
|
422
424
|
...(STAT_NAMES || []).map(s => ({ label: s, value: s })),
|
|
@@ -437,7 +439,7 @@ function App({ opts }) {
|
|
|
437
439
|
|
|
438
440
|
{step === "dump" && (
|
|
439
441
|
<ListSelect
|
|
440
|
-
label="
|
|
442
|
+
label="Worst at"
|
|
441
443
|
options={[
|
|
442
444
|
{ label: "Any (random)", value: "any" },
|
|
443
445
|
...(STAT_NAMES || []).filter(s => s !== peak).map(s => ({ label: s, value: s })),
|
|
@@ -490,7 +492,7 @@ function App({ opts }) {
|
|
|
490
492
|
|
|
491
493
|
{step === "result" && (
|
|
492
494
|
<Box flexDirection="column">
|
|
493
|
-
<Text bold color="green">✓ Found
|
|
495
|
+
<Text bold color="green">✓ Found your buddy! ({found.checked.toLocaleString()} tries, {(found.elapsed / 1000).toFixed(1)}s)</Text>
|
|
494
496
|
<ConfirmSelect
|
|
495
497
|
label="Apply patch?"
|
|
496
498
|
isActive={step === "result"}
|
|
@@ -514,6 +516,8 @@ function App({ opts }) {
|
|
|
514
516
|
msgs.push({ type: "success", text: "Applied!" });
|
|
515
517
|
if (resignBinary(binaryPath)) msgs.push({ type: "success", text: "Re-signed for macOS" });
|
|
516
518
|
clearCompanion(configPath);
|
|
519
|
+
if (storeSalt) storeSalt(found.salt);
|
|
520
|
+
if (installHook) installHook();
|
|
517
521
|
msgs.push({ type: "success", text: "Cleaned up old buddy data" });
|
|
518
522
|
} catch (err) {
|
|
519
523
|
msgs.push({ type: "error", text: err.message });
|