buddy-reroll 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +92 -156
- package/package.json +7 -3
- package/sprites.js +137 -0
- package/ui.jsx +458 -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");
|
|
@@ -99,8 +100,11 @@ function getClaudeConfigDir() {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
function findBinaryPath() {
|
|
103
|
+
const isWin = platform() === "win32";
|
|
104
|
+
|
|
102
105
|
try {
|
|
103
|
-
const
|
|
106
|
+
const cmd = isWin ? "where.exe claude 2>nul" : "which -a claude 2>/dev/null";
|
|
107
|
+
const allPaths = execSync(cmd, { encoding: "utf-8" }).trim().split("\n");
|
|
104
108
|
for (const entry of allPaths) {
|
|
105
109
|
try {
|
|
106
110
|
const resolved = realpathSync(entry.trim());
|
|
@@ -109,10 +113,16 @@ function findBinaryPath() {
|
|
|
109
113
|
}
|
|
110
114
|
} catch {}
|
|
111
115
|
|
|
112
|
-
const
|
|
113
|
-
|
|
116
|
+
const versionsDirs = [
|
|
117
|
+
join(homedir(), ".local", "share", "claude", "versions"),
|
|
118
|
+
...(isWin ? [join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Claude", "versions")] : []),
|
|
119
|
+
];
|
|
120
|
+
for (const versionsDir of versionsDirs) {
|
|
121
|
+
if (!existsSync(versionsDir)) continue;
|
|
114
122
|
try {
|
|
115
|
-
const versions = readdirSync(versionsDir)
|
|
123
|
+
const versions = readdirSync(versionsDir)
|
|
124
|
+
.filter((f) => !f.includes(".backup"))
|
|
125
|
+
.sort();
|
|
116
126
|
if (versions.length > 0) return join(versionsDir, versions[versions.length - 1]);
|
|
117
127
|
} catch {}
|
|
118
128
|
}
|
|
@@ -130,6 +140,12 @@ function findConfigPath() {
|
|
|
130
140
|
const defaultPath = join(home, ".claude.json");
|
|
131
141
|
if (existsSync(defaultPath)) return defaultPath;
|
|
132
142
|
|
|
143
|
+
// Windows: check AppData\Roaming\Claude
|
|
144
|
+
if (platform() === "win32" && process.env.APPDATA) {
|
|
145
|
+
const appDataPath = join(process.env.APPDATA, "Claude", "config.json");
|
|
146
|
+
if (existsSync(appDataPath)) return appDataPath;
|
|
147
|
+
}
|
|
148
|
+
|
|
133
149
|
return null;
|
|
134
150
|
}
|
|
135
151
|
|
|
@@ -143,9 +159,9 @@ function getUserId(configPath) {
|
|
|
143
159
|
function findCurrentSalt(binaryData, userId) {
|
|
144
160
|
if (binaryData.includes(Buffer.from(ORIGINAL_SALT))) return ORIGINAL_SALT;
|
|
145
161
|
|
|
146
|
-
const text = binaryData.toString("
|
|
162
|
+
const text = binaryData.toString("latin1");
|
|
147
163
|
|
|
148
|
-
// Scan for previously patched salts
|
|
164
|
+
// Scan for previously patched salts
|
|
149
165
|
const patterns = [
|
|
150
166
|
new RegExp(`x{${SALT_LEN - 8}}\\d{8}`, "g"),
|
|
151
167
|
new RegExp(`friend-\\d{4}-.{${SALT_LEN - 12}}`, "g"),
|
|
@@ -157,7 +173,6 @@ function findCurrentSalt(binaryData, userId) {
|
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
175
|
|
|
160
|
-
// Contextual scan near companion code markers
|
|
161
176
|
const saltRegex = new RegExp(`"([a-zA-Z0-9_-]{${SALT_LEN}})"`, "g");
|
|
162
177
|
const candidates = new Set();
|
|
163
178
|
const markers = ["rollRarity", "CompanionBones", "inspirationSeed", "companionUserId"];
|
|
@@ -171,7 +186,6 @@ function findCurrentSalt(binaryData, userId) {
|
|
|
171
186
|
}
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
// Filter: real salts contain digits or hyphens (rules out "projectSettings" etc.)
|
|
175
189
|
for (const c of candidates) {
|
|
176
190
|
if (/[\d-]/.test(c)) return c;
|
|
177
191
|
}
|
|
@@ -181,7 +195,7 @@ function findCurrentSalt(binaryData, userId) {
|
|
|
181
195
|
|
|
182
196
|
// ── Brute-force ──────────────────────────────────────────────────────────
|
|
183
197
|
|
|
184
|
-
function bruteForce(userId, target,
|
|
198
|
+
async function bruteForce(userId, target, onProgress) {
|
|
185
199
|
const startTime = Date.now();
|
|
186
200
|
let checked = 0;
|
|
187
201
|
|
|
@@ -206,9 +220,11 @@ function bruteForce(userId, target, spinner) {
|
|
|
206
220
|
const r = rollFrom(salt, userId);
|
|
207
221
|
if (matches(r, target)) return { salt, result: r, checked, elapsed: Date.now() - startTime };
|
|
208
222
|
|
|
223
|
+
if (checked % 100_000 === 0) {
|
|
224
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
225
|
+
}
|
|
209
226
|
if (checked % 5_000_000 === 0) {
|
|
210
|
-
|
|
211
|
-
if (spinner) spinner.message(`${(checked / 1e6).toFixed(0)}M salts checked (${secs}s)`);
|
|
227
|
+
if (onProgress) onProgress(checked, Date.now() - startTime);
|
|
212
228
|
}
|
|
213
229
|
}
|
|
214
230
|
|
|
@@ -228,6 +244,10 @@ function matches(roll, target) {
|
|
|
228
244
|
|
|
229
245
|
function isClaudeRunning() {
|
|
230
246
|
try {
|
|
247
|
+
if (platform() === "win32") {
|
|
248
|
+
const out = execSync('tasklist /FI "IMAGENAME eq claude.exe" /FO CSV 2>nul', { encoding: "utf-8" });
|
|
249
|
+
return out.toLowerCase().includes("claude.exe");
|
|
250
|
+
}
|
|
231
251
|
const out = execSync("pgrep -af claude 2>/dev/null", { encoding: "utf-8" });
|
|
232
252
|
return out.split("\n").some((line) => !line.includes("buddy-reroll") && line.trim().length > 0);
|
|
233
253
|
} catch {
|
|
@@ -256,8 +276,20 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
|
|
|
256
276
|
|
|
257
277
|
if (count === 0) throw new Error(`Salt "${oldSalt}" not found in binary`);
|
|
258
278
|
|
|
259
|
-
|
|
260
|
-
|
|
279
|
+
const isWin = platform() === "win32";
|
|
280
|
+
const maxRetries = isWin ? 3 : 1;
|
|
281
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
282
|
+
try {
|
|
283
|
+
writeFileSync(binaryPath, data);
|
|
284
|
+
return count;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (isWin && (err.code === "EACCES" || err.code === "EPERM" || err.code === "EBUSY") && attempt < maxRetries - 1) {
|
|
287
|
+
execSync("timeout /t 2 /nobreak >nul 2>&1", { shell: true, stdio: "ignore" });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
throw new Error(`Failed to write binary: ${err.message}${isWin ? " (ensure Claude Code is fully closed)" : ""}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
261
293
|
}
|
|
262
294
|
|
|
263
295
|
function resignBinary(binaryPath) {
|
|
@@ -281,165 +313,69 @@ function clearCompanion(configPath) {
|
|
|
281
313
|
|
|
282
314
|
// ── Display ──────────────────────────────────────────────────────────────
|
|
283
315
|
|
|
284
|
-
function
|
|
316
|
+
function formatCompanionCard(result) {
|
|
317
|
+
const sprite = renderSprite({ species: result.species, eye: result.eye, hat: result.hat });
|
|
318
|
+
const colored = colorizeSprite(sprite, result.rarity);
|
|
319
|
+
const colorFn = chalk[RARITY_COLORS[result.rarity]] ?? chalk.white;
|
|
320
|
+
const stars = RARITY_STARS[result.rarity] ?? "";
|
|
321
|
+
|
|
322
|
+
const meta = [];
|
|
323
|
+
meta.push(`${result.species} / ${result.rarity}${result.shiny ? " / shiny" : ""}`);
|
|
324
|
+
meta.push(`eye:${result.eye} / hat:${result.hat}`);
|
|
325
|
+
meta.push(stars);
|
|
326
|
+
|
|
285
327
|
const lines = [];
|
|
286
|
-
|
|
328
|
+
const spriteWidth = 14;
|
|
329
|
+
for (let i = 0; i < colored.length; i++) {
|
|
330
|
+
const right = meta[i] ?? "";
|
|
331
|
+
lines.push(` ${colored[i]}${" ".repeat(Math.max(0, spriteWidth - sprite[i].length))}${right}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
287
334
|
for (const [k, v] of Object.entries(result.stats)) {
|
|
288
|
-
const
|
|
335
|
+
const filled = Math.min(10, Math.max(0, Math.round(v / 10)));
|
|
336
|
+
const bar = colorFn("█".repeat(filled) + "░".repeat(10 - filled));
|
|
289
337
|
lines.push(` ${k.padEnd(10)} ${bar} ${String(v).padStart(3)}`);
|
|
290
338
|
}
|
|
339
|
+
|
|
291
340
|
return lines.join("\n");
|
|
292
341
|
}
|
|
293
342
|
|
|
294
343
|
// ── Interactive mode ─────────────────────────────────────────────────────
|
|
295
344
|
|
|
296
345
|
async function interactiveMode(binaryPath, configPath, userId) {
|
|
297
|
-
p.intro("buddy-reroll");
|
|
298
|
-
|
|
299
346
|
const binaryData = readFileSync(binaryPath);
|
|
300
347
|
const currentSalt = findCurrentSalt(binaryData, userId);
|
|
301
348
|
if (!currentSalt) {
|
|
302
|
-
|
|
349
|
+
console.error("✗ Could not find companion salt in binary.");
|
|
303
350
|
process.exit(1);
|
|
304
351
|
}
|
|
305
352
|
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
|
-
|
|
323
|
-
if (action === "restore") {
|
|
324
|
-
const backupPath = binaryPath + ".backup";
|
|
325
|
-
if (!existsSync(backupPath)) {
|
|
326
|
-
p.cancel("No backup found. Nothing to restore.");
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
copyFileSync(backupPath, binaryPath);
|
|
330
|
-
resignBinary(binaryPath);
|
|
331
|
-
clearCompanion(configPath);
|
|
332
|
-
p.outro("Restored! Restart Claude Code and run /buddy.");
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const species = await p.select({
|
|
337
|
-
message: "Species",
|
|
338
|
-
options: SPECIES.map((s) => ({
|
|
339
|
-
value: s,
|
|
340
|
-
label: s,
|
|
341
|
-
hint: s === currentRoll.species ? "current" : undefined,
|
|
342
|
-
})),
|
|
343
|
-
initialValue: currentRoll.species,
|
|
344
|
-
});
|
|
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
353
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
354
|
+
const { runInteractiveUI } = await import("./ui.jsx");
|
|
355
|
+
await runInteractiveUI({
|
|
356
|
+
currentRoll,
|
|
357
|
+
currentSalt,
|
|
358
|
+
binaryPath,
|
|
359
|
+
configPath,
|
|
360
|
+
userId,
|
|
361
|
+
bruteForce,
|
|
362
|
+
patchBinary,
|
|
363
|
+
resignBinary,
|
|
364
|
+
clearCompanion,
|
|
365
|
+
isClaudeRunning,
|
|
366
|
+
rollFrom,
|
|
367
|
+
matches,
|
|
368
|
+
SPECIES,
|
|
369
|
+
RARITIES,
|
|
370
|
+
RARITY_LABELS,
|
|
371
|
+
EYES,
|
|
372
|
+
HATS,
|
|
388
373
|
});
|
|
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
374
|
}
|
|
439
375
|
|
|
440
376
|
// ── Non-interactive mode ─────────────────────────────────────────────────
|
|
441
377
|
|
|
442
|
-
function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
378
|
+
async function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
443
379
|
console.log(` Binary: ${binaryPath}`);
|
|
444
380
|
console.log(` Config: ${configPath}`);
|
|
445
381
|
console.log(` User ID: ${userId.slice(0, 8)}...`);
|
|
@@ -467,7 +403,7 @@ function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
467
403
|
if (args.current) {
|
|
468
404
|
const result = rollFrom(currentSalt, userId);
|
|
469
405
|
console.log(`\n Current companion (salt: ${currentSalt}):`);
|
|
470
|
-
console.log(
|
|
406
|
+
console.log(formatCompanionCard(result));
|
|
471
407
|
console.log();
|
|
472
408
|
return;
|
|
473
409
|
}
|
|
@@ -500,7 +436,7 @@ function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
500
436
|
|
|
501
437
|
const currentRoll = rollFrom(currentSalt, userId);
|
|
502
438
|
if (matches(currentRoll, target)) {
|
|
503
|
-
console.log(" ✓ Already matches!\n" +
|
|
439
|
+
console.log(" ✓ Already matches!\n" + formatCompanionCard(currentRoll));
|
|
504
440
|
return;
|
|
505
441
|
}
|
|
506
442
|
|
|
@@ -509,13 +445,13 @@ function nonInteractiveMode(args, binaryPath, configPath, userId) {
|
|
|
509
445
|
}
|
|
510
446
|
|
|
511
447
|
console.log(" Searching...");
|
|
512
|
-
const found = bruteForce(userId, target, null);
|
|
448
|
+
const found = await bruteForce(userId, target, null);
|
|
513
449
|
if (!found) {
|
|
514
450
|
console.error(" ✗ No matching salt found. Try relaxing constraints.");
|
|
515
451
|
process.exit(1);
|
|
516
452
|
}
|
|
517
453
|
console.log(` ✓ Found in ${found.checked.toLocaleString()} attempts (${(found.elapsed / 1000).toFixed(1)}s)`);
|
|
518
|
-
console.log(
|
|
454
|
+
console.log(formatCompanionCard(found.result));
|
|
519
455
|
|
|
520
456
|
const backupPath = binaryPath + ".backup";
|
|
521
457
|
if (!existsSync(backupPath)) {
|
|
@@ -603,7 +539,7 @@ async function main() {
|
|
|
603
539
|
if (!hasTargetFlags && !isCommand) {
|
|
604
540
|
await interactiveMode(binaryPath, configPath, userId);
|
|
605
541
|
} else {
|
|
606
|
-
nonInteractiveMode(args, binaryPath, configPath, userId);
|
|
542
|
+
await nonInteractiveMode(args, binaryPath, configPath, userId);
|
|
607
543
|
}
|
|
608
544
|
}
|
|
609
545
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-reroll",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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,458 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { render, Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { renderSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
4
|
+
import { existsSync, copyFileSync } from "fs";
|
|
5
|
+
|
|
6
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
+
function Spinner({ label }) {
|
|
8
|
+
const [frame, setFrame] = useState(0);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
11
|
+
return () => clearInterval(timer);
|
|
12
|
+
}, []);
|
|
13
|
+
return <Text><Text color="cyan">{SPINNER_FRAMES[frame]}</Text> {label}</Text>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── 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
|
+
function PreviewCard({ species, rarity, eye, hat, shiny, stats }) {
|
|
90
|
+
const color = RARITY_COLORS[rarity] ?? "white";
|
|
91
|
+
const stars = RARITY_STARS[rarity] ?? "";
|
|
92
|
+
const sprite = renderSprite({ species, eye, hat });
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1}>
|
|
96
|
+
<Box>
|
|
97
|
+
<Box flexDirection="column">
|
|
98
|
+
{sprite.map((line, i) => (
|
|
99
|
+
<Text key={i} color={color}>{line}</Text>
|
|
100
|
+
))}
|
|
101
|
+
</Box>
|
|
102
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
103
|
+
<Text bold>{species}</Text>
|
|
104
|
+
<Text color={color}>{rarity}{shiny ? " ✦shiny" : ""}</Text>
|
|
105
|
+
<Text dimColor>eye:{eye} hat:{hat}</Text>
|
|
106
|
+
<Text>{stars}</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
</Box>
|
|
109
|
+
{stats && (
|
|
110
|
+
<Box flexDirection="column" marginTop={1}>
|
|
111
|
+
{Object.entries(stats).map(([k, v]) => {
|
|
112
|
+
const filled = Math.min(10, Math.max(0, Math.round(v / 10)));
|
|
113
|
+
return (
|
|
114
|
+
<Text key={k}>
|
|
115
|
+
<Text>{k.padEnd(10)} </Text>
|
|
116
|
+
<Text color={color}>{"█".repeat(filled)}</Text>
|
|
117
|
+
<Text dimColor>{"░".repeat(10 - filled)}</Text>
|
|
118
|
+
<Text> {String(v).padStart(3)}</Text>
|
|
119
|
+
</Text>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</Box>
|
|
123
|
+
)}
|
|
124
|
+
</Box>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ShowCurrentStep({ isActive }) {
|
|
129
|
+
const { exit } = useApp();
|
|
130
|
+
|
|
131
|
+
useInput(() => {
|
|
132
|
+
exit();
|
|
133
|
+
}, { isActive });
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Box flexDirection="column">
|
|
137
|
+
<Text color="green">✓ Current companion shown above.</Text>
|
|
138
|
+
<KeyHint>Press any key to exit</KeyHint>
|
|
139
|
+
</Box>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function SpeciesStep({ speciesList, current, onChange, onSubmit, onBack, isActive }) {
|
|
144
|
+
const [idx, setIdx] = useState(Math.max(0, speciesList.indexOf(current)));
|
|
145
|
+
|
|
146
|
+
useInput((input, key) => {
|
|
147
|
+
if (key.escape && onBack) { onBack(); return; }
|
|
148
|
+
if (key.leftArrow || key.upArrow) {
|
|
149
|
+
const next = (idx - 1 + speciesList.length) % speciesList.length;
|
|
150
|
+
setIdx(next);
|
|
151
|
+
onChange(speciesList[next]);
|
|
152
|
+
}
|
|
153
|
+
if (key.rightArrow || key.downArrow) {
|
|
154
|
+
const next = (idx + 1) % speciesList.length;
|
|
155
|
+
setIdx(next);
|
|
156
|
+
onChange(speciesList[next]);
|
|
157
|
+
}
|
|
158
|
+
if (key.return) onSubmit();
|
|
159
|
+
}, { isActive });
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Box flexDirection="column">
|
|
163
|
+
<Text bold>Species: <Text color="cyan">{speciesList[idx]}</Text> <Text dimColor>({idx + 1}/{speciesList.length})</Text></Text>
|
|
164
|
+
<KeyHint>←→ browse · enter select · esc back</KeyHint>
|
|
165
|
+
</Box>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
170
|
+
const [progress, setProgress] = useState("");
|
|
171
|
+
const [error, setError] = useState(null);
|
|
172
|
+
const cancelRef = useRef(false);
|
|
173
|
+
const { exit } = useApp();
|
|
174
|
+
|
|
175
|
+
useInput((input, key) => {
|
|
176
|
+
if (key.escape) {
|
|
177
|
+
cancelRef.current = true;
|
|
178
|
+
exit();
|
|
179
|
+
}
|
|
180
|
+
}, { isActive });
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
(async () => {
|
|
184
|
+
const found = await bruteForce(userId, target, (checked, elapsed) => {
|
|
185
|
+
if (!cancelRef.current) {
|
|
186
|
+
setProgress(`${(checked / 1e6).toFixed(0)}M salts checked (${(elapsed / 1000).toFixed(1)}s)`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (cancelRef.current) return;
|
|
190
|
+
if (found) onFound(found);
|
|
191
|
+
else setError("No matching salt found. Try relaxing constraints.");
|
|
192
|
+
})();
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
if (error) {
|
|
196
|
+
return (
|
|
197
|
+
<Box flexDirection="column">
|
|
198
|
+
<Text color="red">✗ {error}</Text>
|
|
199
|
+
<KeyHint>Press esc to exit</KeyHint>
|
|
200
|
+
</Box>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<Box flexDirection="column">
|
|
206
|
+
<Spinner label={progress || "Searching for matching salt..."} />
|
|
207
|
+
<KeyHint>esc to cancel</KeyHint>
|
|
208
|
+
</Box>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function DoneStep({ messages, isActive }) {
|
|
213
|
+
const { exit } = useApp();
|
|
214
|
+
|
|
215
|
+
useInput(() => {
|
|
216
|
+
exit();
|
|
217
|
+
}, { isActive });
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<Box flexDirection="column">
|
|
221
|
+
{messages.map((msg, i) => (
|
|
222
|
+
<Text key={i} color="green">✓ {msg}</Text>
|
|
223
|
+
))}
|
|
224
|
+
<Box marginTop={1}>
|
|
225
|
+
<Text bold>Done! Restart Claude Code and run /buddy to hatch your new companion.</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
<KeyHint>Press any key to exit</KeyHint>
|
|
228
|
+
</Box>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const STEP_ORDER = ["action", "species", "rarity", "eye", "hat", "shiny", "confirm"];
|
|
233
|
+
|
|
234
|
+
function getPrevStep(current, rarity) {
|
|
235
|
+
const idx = STEP_ORDER.indexOf(current);
|
|
236
|
+
if (idx <= 0) return null;
|
|
237
|
+
let prev = STEP_ORDER[idx - 1];
|
|
238
|
+
if (prev === "hat" && rarity === "common") prev = "eye";
|
|
239
|
+
return prev;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function App({ opts }) {
|
|
243
|
+
const { exit } = useApp();
|
|
244
|
+
const {
|
|
245
|
+
currentRoll, currentSalt, binaryPath, configPath, userId,
|
|
246
|
+
bruteForce, patchBinary, resignBinary, clearCompanion, isClaudeRunning,
|
|
247
|
+
rollFrom, matches, SPECIES, RARITIES, RARITY_LABELS, EYES, HATS,
|
|
248
|
+
} = opts;
|
|
249
|
+
|
|
250
|
+
const [step, setStep] = useState("action");
|
|
251
|
+
const [species, setSpecies] = useState(currentRoll.species);
|
|
252
|
+
const [rarity, setRarity] = useState(currentRoll.rarity);
|
|
253
|
+
const [eye, setEye] = useState(currentRoll.eye);
|
|
254
|
+
const [hat, setHat] = useState(currentRoll.hat);
|
|
255
|
+
const [shiny, setShiny] = useState(currentRoll.shiny);
|
|
256
|
+
const [found, setFound] = useState(null);
|
|
257
|
+
const [doneMessages, setDoneMessages] = useState([]);
|
|
258
|
+
|
|
259
|
+
const showStats = step === "showCurrent" || step === "result" || step === "done";
|
|
260
|
+
const displayRoll = found ? found.result : { species, rarity, eye, hat, shiny, stats: currentRoll.stats };
|
|
261
|
+
const effectiveHat = rarity === "common" ? "none" : hat;
|
|
262
|
+
const buildTarget = (s = shiny) => ({ species, rarity, eye, hat: effectiveHat, shiny: s });
|
|
263
|
+
|
|
264
|
+
const goBack = (toStep) => {
|
|
265
|
+
const prev = toStep || getPrevStep(step, rarity);
|
|
266
|
+
if (prev) setStep(prev);
|
|
267
|
+
else exit();
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Box flexDirection="column" padding={1}>
|
|
272
|
+
<Text bold dimColor>buddy-reroll</Text>
|
|
273
|
+
|
|
274
|
+
<PreviewCard
|
|
275
|
+
species={displayRoll.species}
|
|
276
|
+
rarity={displayRoll.rarity}
|
|
277
|
+
eye={displayRoll.eye}
|
|
278
|
+
hat={displayRoll.hat}
|
|
279
|
+
shiny={displayRoll.shiny}
|
|
280
|
+
stats={showStats ? displayRoll.stats : null}
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
<Box marginTop={1}>
|
|
284
|
+
{step === "action" && (
|
|
285
|
+
<ListSelect
|
|
286
|
+
label="What would you like to do?"
|
|
287
|
+
options={[
|
|
288
|
+
{ label: "Reroll companion", value: "reroll" },
|
|
289
|
+
{ label: "Restore original", value: "restore" },
|
|
290
|
+
{ label: "Show current", value: "current" },
|
|
291
|
+
]}
|
|
292
|
+
isActive={step === "action"}
|
|
293
|
+
onSubmit={(action) => {
|
|
294
|
+
if (action === "current") {
|
|
295
|
+
setStep("showCurrent");
|
|
296
|
+
} else if (action === "restore") {
|
|
297
|
+
const backupPath = binaryPath + ".backup";
|
|
298
|
+
if (!existsSync(backupPath)) {
|
|
299
|
+
setDoneMessages(["No backup found. Nothing to restore."]);
|
|
300
|
+
setStep("done");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
copyFileSync(backupPath, binaryPath);
|
|
304
|
+
resignBinary(binaryPath);
|
|
305
|
+
clearCompanion(configPath);
|
|
306
|
+
setDoneMessages(["Restored! Restart Claude Code and run /buddy."]);
|
|
307
|
+
setStep("done");
|
|
308
|
+
} else {
|
|
309
|
+
setStep("species");
|
|
310
|
+
}
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{step === "showCurrent" && (
|
|
316
|
+
<ShowCurrentStep isActive={step === "showCurrent"} />
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{step === "species" && (
|
|
320
|
+
<SpeciesStep
|
|
321
|
+
speciesList={SPECIES}
|
|
322
|
+
current={species}
|
|
323
|
+
onChange={setSpecies}
|
|
324
|
+
onSubmit={() => setStep("rarity")}
|
|
325
|
+
onBack={() => goBack("action")}
|
|
326
|
+
isActive={step === "species"}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{step === "rarity" && (
|
|
331
|
+
<ListSelect
|
|
332
|
+
label="Rarity"
|
|
333
|
+
options={RARITIES.map((r) => ({ label: RARITY_LABELS[r], value: r }))}
|
|
334
|
+
defaultValue={rarity}
|
|
335
|
+
onChange={(r) => {
|
|
336
|
+
setRarity(r);
|
|
337
|
+
if (r === "common") setHat("none");
|
|
338
|
+
}}
|
|
339
|
+
onSubmit={() => setStep("eye")}
|
|
340
|
+
onBack={() => goBack()}
|
|
341
|
+
isActive={step === "rarity"}
|
|
342
|
+
/>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{step === "eye" && (
|
|
346
|
+
<ListSelect
|
|
347
|
+
label="Eye"
|
|
348
|
+
options={EYES.map((e) => ({ label: e, value: e }))}
|
|
349
|
+
defaultValue={eye}
|
|
350
|
+
onChange={setEye}
|
|
351
|
+
onSubmit={() => setStep(rarity === "common" ? "shiny" : "hat")}
|
|
352
|
+
onBack={() => goBack()}
|
|
353
|
+
isActive={step === "eye"}
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{step === "hat" && (
|
|
358
|
+
<ListSelect
|
|
359
|
+
label="Hat"
|
|
360
|
+
options={HATS.map((h) => ({ label: h, value: h }))}
|
|
361
|
+
defaultValue={hat === "none" ? "crown" : hat}
|
|
362
|
+
onChange={setHat}
|
|
363
|
+
onSubmit={() => setStep("shiny")}
|
|
364
|
+
onBack={() => goBack()}
|
|
365
|
+
isActive={step === "hat"}
|
|
366
|
+
/>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{step === "shiny" && (
|
|
370
|
+
<ConfirmSelect
|
|
371
|
+
label="Shiny?"
|
|
372
|
+
isActive={step === "shiny"}
|
|
373
|
+
onConfirm={() => {
|
|
374
|
+
setShiny(true);
|
|
375
|
+
if (matches(currentRoll, buildTarget(true))) {
|
|
376
|
+
setDoneMessages(["Already matching! No changes needed."]);
|
|
377
|
+
setStep("done");
|
|
378
|
+
} else {
|
|
379
|
+
setStep("confirm");
|
|
380
|
+
}
|
|
381
|
+
}}
|
|
382
|
+
onCancel={() => {
|
|
383
|
+
setShiny(false);
|
|
384
|
+
if (matches(currentRoll, buildTarget(false))) {
|
|
385
|
+
setDoneMessages(["Already matching! No changes needed."]);
|
|
386
|
+
setStep("done");
|
|
387
|
+
} else {
|
|
388
|
+
setStep("confirm");
|
|
389
|
+
}
|
|
390
|
+
}}
|
|
391
|
+
onBack={() => goBack()}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
{step === "confirm" && (
|
|
396
|
+
<Box flexDirection="column">
|
|
397
|
+
<Text>Target: <Text bold>{species}</Text> / <Text bold>{rarity}</Text> / eye:{eye} / hat:{effectiveHat}{shiny ? " / shiny" : ""}</Text>
|
|
398
|
+
{isClaudeRunning() && <Text color="yellow">⚠ Claude Code appears to be running. Quit it before patching.</Text>}
|
|
399
|
+
<ConfirmSelect
|
|
400
|
+
label="Search and apply?"
|
|
401
|
+
isActive={step === "confirm"}
|
|
402
|
+
onConfirm={() => setStep("search")}
|
|
403
|
+
onCancel={() => exit()}
|
|
404
|
+
onBack={() => goBack()}
|
|
405
|
+
/>
|
|
406
|
+
</Box>
|
|
407
|
+
)}
|
|
408
|
+
|
|
409
|
+
{step === "search" && (
|
|
410
|
+
<SearchStep
|
|
411
|
+
userId={userId}
|
|
412
|
+
target={buildTarget()}
|
|
413
|
+
bruteForce={bruteForce}
|
|
414
|
+
onFound={(f) => { setFound(f); setStep("result"); }}
|
|
415
|
+
onFail={() => {
|
|
416
|
+
setDoneMessages(["No matching salt found. Try relaxing constraints."]);
|
|
417
|
+
setStep("done");
|
|
418
|
+
}}
|
|
419
|
+
isActive={step === "search"}
|
|
420
|
+
/>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{step === "result" && (
|
|
424
|
+
<Box flexDirection="column">
|
|
425
|
+
<Text bold color="green">✓ Found in {found.checked.toLocaleString()} attempts ({(found.elapsed / 1000).toFixed(1)}s)</Text>
|
|
426
|
+
<ConfirmSelect
|
|
427
|
+
label="Apply patch?"
|
|
428
|
+
isActive={step === "result"}
|
|
429
|
+
onConfirm={() => {
|
|
430
|
+
const msgs = [];
|
|
431
|
+
const backupPath = binaryPath + ".backup";
|
|
432
|
+
if (!existsSync(backupPath)) {
|
|
433
|
+
copyFileSync(binaryPath, backupPath);
|
|
434
|
+
msgs.push(`Backup saved to ${backupPath}`);
|
|
435
|
+
}
|
|
436
|
+
const count = patchBinary(binaryPath, currentSalt, found.salt);
|
|
437
|
+
msgs.push(`Patched ${count} occurrence(s)`);
|
|
438
|
+
if (resignBinary(binaryPath)) msgs.push("Binary re-signed (ad-hoc codesign)");
|
|
439
|
+
clearCompanion(configPath);
|
|
440
|
+
msgs.push("Companion data cleared");
|
|
441
|
+
setDoneMessages(msgs);
|
|
442
|
+
setStep("done");
|
|
443
|
+
}}
|
|
444
|
+
onCancel={() => exit()}
|
|
445
|
+
/>
|
|
446
|
+
</Box>
|
|
447
|
+
)}
|
|
448
|
+
|
|
449
|
+
{step === "done" && <DoneStep messages={doneMessages} isActive={step === "done"} />}
|
|
450
|
+
</Box>
|
|
451
|
+
</Box>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function runInteractiveUI(opts) {
|
|
456
|
+
const { waitUntilExit } = render(<App opts={opts} />);
|
|
457
|
+
await waitUntilExit();
|
|
458
|
+
}
|