buddy-reroll 0.2.0 → 0.3.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/README.md +78 -7
- package/index.js +256 -311
- package/lib/companion.js +183 -0
- package/lib/companion.test.js +134 -0
- package/lib/doctor.js +73 -0
- package/lib/estimator.js +43 -0
- package/lib/estimator.test.js +97 -0
- package/lib/finder.js +104 -0
- package/lib/finder.test.js +35 -0
- package/lib/hooks.js +96 -0
- package/lib/hooks.test.js +240 -0
- package/lib/runtime.js +184 -0
- package/lib/runtime.test.js +110 -0
- package/lib/wyhash.js +88 -0
- package/lib/wyhash.test.js +50 -0
- package/package.json +13 -2
- package/scripts/verify-runtime.js +34 -0
- package/scripts/worker.js +40 -0
- package/ui-fallback.js +181 -0
- package/ui.jsx +134 -64
package/ui.jsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { render, Box, Text, useApp, useInput } from "ink";
|
|
3
|
-
|
|
3
|
+
import { renderSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
4
|
+
import { existsSync, copyFileSync } from "fs";
|
|
5
|
+
|
|
4
6
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
5
7
|
function Spinner({ label }) {
|
|
6
8
|
const [frame, setFrame] = useState(0);
|
|
@@ -10,10 +12,8 @@ function Spinner({ label }) {
|
|
|
10
12
|
}, []);
|
|
11
13
|
return <Text><Text color="cyan">{SPINNER_FRAMES[frame]}</Text> {label}</Text>;
|
|
12
14
|
}
|
|
13
|
-
import { renderSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
14
|
-
import { existsSync, copyFileSync } from "fs";
|
|
15
15
|
|
|
16
|
-
// ──
|
|
16
|
+
// ── Components ──────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
18
|
function KeyHint({ children }) {
|
|
19
19
|
return <Text italic dimColor>{children}</Text>;
|
|
@@ -86,8 +86,6 @@ function ConfirmSelect({ label, onConfirm, onCancel, onBack, isActive }) {
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// ── PreviewCard ─────────────────────────────────────────────────────────
|
|
90
|
-
|
|
91
89
|
function PreviewCard({ species, rarity, eye, hat, shiny, stats }) {
|
|
92
90
|
const color = RARITY_COLORS[rarity] ?? "white";
|
|
93
91
|
const stars = RARITY_STARS[rarity] ?? "";
|
|
@@ -97,8 +95,8 @@ function PreviewCard({ species, rarity, eye, hat, shiny, stats }) {
|
|
|
97
95
|
<Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1}>
|
|
98
96
|
<Box>
|
|
99
97
|
<Box flexDirection="column">
|
|
100
|
-
{sprite.map((line,
|
|
101
|
-
<Text key={
|
|
98
|
+
{sprite.map((line, lineIdx) => (
|
|
99
|
+
<Text key={`sprite-${lineIdx}-${line.trim()}`} color={color}>{line}</Text>
|
|
102
100
|
))}
|
|
103
101
|
</Box>
|
|
104
102
|
<Box flexDirection="column" marginLeft={2}>
|
|
@@ -127,8 +125,6 @@ function PreviewCard({ species, rarity, eye, hat, shiny, stats }) {
|
|
|
127
125
|
);
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
// ── Step Components ─────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
128
|
function ShowCurrentStep({ isActive }) {
|
|
133
129
|
const { exit } = useApp();
|
|
134
130
|
|
|
@@ -172,8 +168,8 @@ function SpeciesStep({ speciesList, current, onChange, onSubmit, onBack, isActiv
|
|
|
172
168
|
|
|
173
169
|
function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
174
170
|
const [progress, setProgress] = useState("");
|
|
175
|
-
const [error, setError] = useState(null);
|
|
176
171
|
const cancelRef = useRef(false);
|
|
172
|
+
const hasStarted = useRef(false);
|
|
177
173
|
const { exit } = useApp();
|
|
178
174
|
|
|
179
175
|
useInput((input, key) => {
|
|
@@ -184,6 +180,8 @@ function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
|
184
180
|
}, { isActive });
|
|
185
181
|
|
|
186
182
|
useEffect(() => {
|
|
183
|
+
if (hasStarted.current) return;
|
|
184
|
+
hasStarted.current = true;
|
|
187
185
|
(async () => {
|
|
188
186
|
const found = await bruteForce(userId, target, (checked, elapsed) => {
|
|
189
187
|
if (!cancelRef.current) {
|
|
@@ -192,22 +190,13 @@ function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
|
192
190
|
});
|
|
193
191
|
if (cancelRef.current) return;
|
|
194
192
|
if (found) onFound(found);
|
|
195
|
-
else
|
|
193
|
+
else onFail();
|
|
196
194
|
})();
|
|
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
|
-
}
|
|
195
|
+
}, [bruteForce, userId, target, onFound, onFail]);
|
|
207
196
|
|
|
208
197
|
return (
|
|
209
198
|
<Box flexDirection="column">
|
|
210
|
-
<Spinner label={progress || "
|
|
199
|
+
<Spinner label={progress || "Looking for your buddy..."} />
|
|
211
200
|
<KeyHint>esc to cancel</KeyHint>
|
|
212
201
|
</Box>
|
|
213
202
|
);
|
|
@@ -215,6 +204,7 @@ function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
|
215
204
|
|
|
216
205
|
function DoneStep({ messages, isActive }) {
|
|
217
206
|
const { exit } = useApp();
|
|
207
|
+
const hasErrors = messages.some((msg) => msg.type === "error");
|
|
218
208
|
|
|
219
209
|
useInput(() => {
|
|
220
210
|
exit();
|
|
@@ -222,38 +212,41 @@ function DoneStep({ messages, isActive }) {
|
|
|
222
212
|
|
|
223
213
|
return (
|
|
224
214
|
<Box flexDirection="column">
|
|
225
|
-
{messages.map((msg
|
|
226
|
-
<Text key={
|
|
215
|
+
{messages.map((msg) => (
|
|
216
|
+
<Text key={`${msg.type}-${msg.text}`} color={msg.type === "error" ? "red" : "green"}>
|
|
217
|
+
{msg.type === "error" ? "✗ " : "✓ "}{msg.text}
|
|
218
|
+
</Text>
|
|
227
219
|
))}
|
|
228
220
|
<Box marginTop={1}>
|
|
229
|
-
<Text bold>
|
|
221
|
+
<Text bold>
|
|
222
|
+
{hasErrors
|
|
223
|
+
? "Something went wrong — check the issue above and try again."
|
|
224
|
+
: "All set! Restart Claude Code and say /buddy to meet your new friend."}
|
|
225
|
+
</Text>
|
|
230
226
|
</Box>
|
|
231
227
|
<KeyHint>Press any key to exit</KeyHint>
|
|
232
228
|
</Box>
|
|
233
229
|
);
|
|
234
230
|
}
|
|
235
231
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const STEP_ORDER = ["action", "species", "rarity", "eye", "hat", "shiny", "confirm"];
|
|
232
|
+
const STEP_ORDER = ["action", "species", "rarity", "eye", "hat", "shiny", "peak", "dump", "confirm"];
|
|
239
233
|
|
|
240
|
-
function getPrevStep(current, rarity) {
|
|
234
|
+
function getPrevStep(current, rarity, peak) {
|
|
241
235
|
const idx = STEP_ORDER.indexOf(current);
|
|
242
236
|
if (idx <= 0) return null;
|
|
243
237
|
let prev = STEP_ORDER[idx - 1];
|
|
244
|
-
// Skip hat when going back if common
|
|
245
238
|
if (prev === "hat" && rarity === "common") prev = "eye";
|
|
239
|
+
if (prev === "dump" && peak === null) prev = "shiny";
|
|
240
|
+
if (prev === "peak") prev = "shiny";
|
|
246
241
|
return prev;
|
|
247
242
|
}
|
|
248
243
|
|
|
249
|
-
// ── Main App ────────────────────────────────────────────────────────────
|
|
250
|
-
|
|
251
244
|
function App({ opts }) {
|
|
252
245
|
const { exit } = useApp();
|
|
253
246
|
const {
|
|
254
247
|
currentRoll, currentSalt, binaryPath, configPath, userId,
|
|
255
|
-
bruteForce, patchBinary, resignBinary, clearCompanion, isClaudeRunning,
|
|
256
|
-
rollFrom, matches, SPECIES, RARITIES, RARITY_LABELS, EYES, HATS,
|
|
248
|
+
bruteForce, patchBinary, resignBinary, clearCompanion, getPatchability, isClaudeRunning,
|
|
249
|
+
rollFrom, matches, SPECIES, RARITIES, RARITY_LABELS, EYES, HATS, STAT_NAMES,
|
|
257
250
|
} = opts;
|
|
258
251
|
|
|
259
252
|
const [step, setStep] = useState("action");
|
|
@@ -262,14 +255,23 @@ function App({ opts }) {
|
|
|
262
255
|
const [eye, setEye] = useState(currentRoll.eye);
|
|
263
256
|
const [hat, setHat] = useState(currentRoll.hat);
|
|
264
257
|
const [shiny, setShiny] = useState(currentRoll.shiny);
|
|
258
|
+
const [peak, setPeak] = useState(null);
|
|
259
|
+
const [dump, setDump] = useState(null);
|
|
265
260
|
const [found, setFound] = useState(null);
|
|
266
261
|
const [doneMessages, setDoneMessages] = useState([]);
|
|
267
262
|
|
|
268
263
|
const showStats = step === "showCurrent" || step === "result" || step === "done";
|
|
269
264
|
const displayRoll = found ? found.result : { species, rarity, eye, hat, shiny, stats: currentRoll.stats };
|
|
265
|
+
const effectiveHat = rarity === "common" ? "none" : hat;
|
|
266
|
+
const buildTarget = (s = shiny) => {
|
|
267
|
+
const t = { species, rarity, eye, hat: effectiveHat, shiny: s };
|
|
268
|
+
if (peak) t.peak = peak;
|
|
269
|
+
if (dump) t.dump = dump;
|
|
270
|
+
return t;
|
|
271
|
+
};
|
|
270
272
|
|
|
271
273
|
const goBack = (toStep) => {
|
|
272
|
-
const prev = toStep || getPrevStep(step, rarity);
|
|
274
|
+
const prev = toStep || getPrevStep(step, rarity, peak);
|
|
273
275
|
if (prev) setStep(prev);
|
|
274
276
|
else exit();
|
|
275
277
|
};
|
|
@@ -301,16 +303,29 @@ function App({ opts }) {
|
|
|
301
303
|
if (action === "current") {
|
|
302
304
|
setStep("showCurrent");
|
|
303
305
|
} else if (action === "restore") {
|
|
304
|
-
const
|
|
306
|
+
const patchability = getPatchability(binaryPath);
|
|
307
|
+
if (!patchability.ok) {
|
|
308
|
+
setDoneMessages([{ type: "error", text: patchability.message }]);
|
|
309
|
+
setStep("done");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { backupPath } = patchability;
|
|
305
314
|
if (!existsSync(backupPath)) {
|
|
306
|
-
setDoneMessages(["No backup found
|
|
315
|
+
setDoneMessages([{ type: "info", text: "No backup found — nothing to undo." }]);
|
|
307
316
|
setStep("done");
|
|
308
317
|
return;
|
|
309
318
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
copyFileSync(backupPath, binaryPath);
|
|
322
|
+
resignBinary(binaryPath);
|
|
323
|
+
clearCompanion(configPath);
|
|
324
|
+
setDoneMessages([{ type: "success", text: "Restored! Restart Claude Code and say /buddy to see your original friend." }]);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
setDoneMessages([{ type: "error", text: err.message }]);
|
|
327
|
+
}
|
|
328
|
+
|
|
314
329
|
setStep("done");
|
|
315
330
|
} else {
|
|
316
331
|
setStep("species");
|
|
@@ -379,36 +394,80 @@ function App({ opts }) {
|
|
|
379
394
|
isActive={step === "shiny"}
|
|
380
395
|
onConfirm={() => {
|
|
381
396
|
setShiny(true);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
setDoneMessages(["Already matching! No changes needed."]);
|
|
397
|
+
if (matches(currentRoll, buildTarget(true))) {
|
|
398
|
+
setDoneMessages([{ type: "success", text: "Your buddy already looks like that!" }]);
|
|
385
399
|
setStep("done");
|
|
386
400
|
} else {
|
|
387
|
-
setStep("
|
|
401
|
+
setStep("peak");
|
|
388
402
|
}
|
|
389
403
|
}}
|
|
390
404
|
onCancel={() => {
|
|
391
405
|
setShiny(false);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
setDoneMessages(["Already matching! No changes needed."]);
|
|
406
|
+
if (matches(currentRoll, buildTarget(false))) {
|
|
407
|
+
setDoneMessages([{ type: "success", text: "Your buddy already looks like that!" }]);
|
|
395
408
|
setStep("done");
|
|
396
409
|
} else {
|
|
410
|
+
setStep("peak");
|
|
411
|
+
}
|
|
412
|
+
}}
|
|
413
|
+
onBack={() => goBack()}
|
|
414
|
+
/>
|
|
415
|
+
)}
|
|
416
|
+
|
|
417
|
+
{step === "peak" && (
|
|
418
|
+
<ListSelect
|
|
419
|
+
label="Peak stat (highest)"
|
|
420
|
+
options={[
|
|
421
|
+
{ label: "Any (random)", value: "any" },
|
|
422
|
+
...(STAT_NAMES || []).map(s => ({ label: s, value: s })),
|
|
423
|
+
]}
|
|
424
|
+
defaultValue="any"
|
|
425
|
+
onSubmit={(v) => {
|
|
426
|
+
setPeak(v === "any" ? null : v);
|
|
427
|
+
if (v === "any") {
|
|
397
428
|
setStep("confirm");
|
|
429
|
+
} else {
|
|
430
|
+
setStep("dump");
|
|
398
431
|
}
|
|
399
432
|
}}
|
|
400
433
|
onBack={() => goBack()}
|
|
434
|
+
isActive={step === "peak"}
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{step === "dump" && (
|
|
439
|
+
<ListSelect
|
|
440
|
+
label="Dump stat (lowest)"
|
|
441
|
+
options={[
|
|
442
|
+
{ label: "Any (random)", value: "any" },
|
|
443
|
+
...(STAT_NAMES || []).filter(s => s !== peak).map(s => ({ label: s, value: s })),
|
|
444
|
+
]}
|
|
445
|
+
defaultValue="any"
|
|
446
|
+
onSubmit={(v) => {
|
|
447
|
+
setDump(v === "any" ? null : v);
|
|
448
|
+
setStep("confirm");
|
|
449
|
+
}}
|
|
450
|
+
onBack={() => goBack()}
|
|
451
|
+
isActive={step === "dump"}
|
|
401
452
|
/>
|
|
402
453
|
)}
|
|
403
454
|
|
|
404
455
|
{step === "confirm" && (
|
|
405
456
|
<Box flexDirection="column">
|
|
406
|
-
<Text>Target: <Text bold>{species}</Text> / <Text bold>{rarity}</Text> / eye:{eye} / hat:{
|
|
457
|
+
<Text>Target: <Text bold>{species}</Text> / <Text bold>{rarity}</Text> / eye:{eye} / hat:{effectiveHat}{shiny ? " / shiny" : ""}{peak ? ` / peak:${peak}` : ""}{dump ? ` / dump:${dump}` : ""}</Text>
|
|
407
458
|
{isClaudeRunning() && <Text color="yellow">⚠ Claude Code appears to be running. Quit it before patching.</Text>}
|
|
408
459
|
<ConfirmSelect
|
|
409
460
|
label="Search and apply?"
|
|
410
461
|
isActive={step === "confirm"}
|
|
411
|
-
onConfirm={() =>
|
|
462
|
+
onConfirm={() => {
|
|
463
|
+
const patchability = getPatchability(binaryPath);
|
|
464
|
+
if (!patchability.ok) {
|
|
465
|
+
setDoneMessages([{ type: "error", text: patchability.message }]);
|
|
466
|
+
setStep("done");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
setStep("search");
|
|
470
|
+
}}
|
|
412
471
|
onCancel={() => exit()}
|
|
413
472
|
onBack={() => goBack()}
|
|
414
473
|
/>
|
|
@@ -418,11 +477,11 @@ function App({ opts }) {
|
|
|
418
477
|
{step === "search" && (
|
|
419
478
|
<SearchStep
|
|
420
479
|
userId={userId}
|
|
421
|
-
target={
|
|
480
|
+
target={buildTarget()}
|
|
422
481
|
bruteForce={bruteForce}
|
|
423
482
|
onFound={(f) => { setFound(f); setStep("result"); }}
|
|
424
483
|
onFail={() => {
|
|
425
|
-
setDoneMessages(["
|
|
484
|
+
setDoneMessages([{ type: "error", text: "Couldn't find a match. Try being less picky!" }]);
|
|
426
485
|
setStep("done");
|
|
427
486
|
}}
|
|
428
487
|
isActive={step === "search"}
|
|
@@ -436,17 +495,30 @@ function App({ opts }) {
|
|
|
436
495
|
label="Apply patch?"
|
|
437
496
|
isActive={step === "result"}
|
|
438
497
|
onConfirm={() => {
|
|
498
|
+
const patchability = getPatchability(binaryPath);
|
|
499
|
+
if (!patchability.ok) {
|
|
500
|
+
setDoneMessages([{ type: "error", text: patchability.message }]);
|
|
501
|
+
setStep("done");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
439
505
|
const msgs = [];
|
|
440
|
-
const backupPath =
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
506
|
+
const { backupPath } = patchability;
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
if (!existsSync(backupPath)) {
|
|
510
|
+
copyFileSync(binaryPath, backupPath);
|
|
511
|
+
msgs.push({ type: "success", text: `Saved a backup just in case` });
|
|
512
|
+
}
|
|
513
|
+
const count = patchBinary(binaryPath, currentSalt, found.salt);
|
|
514
|
+
msgs.push({ type: "success", text: "Applied!" });
|
|
515
|
+
if (resignBinary(binaryPath)) msgs.push({ type: "success", text: "Re-signed for macOS" });
|
|
516
|
+
clearCompanion(configPath);
|
|
517
|
+
msgs.push({ type: "success", text: "Cleaned up old buddy data" });
|
|
518
|
+
} catch (err) {
|
|
519
|
+
msgs.push({ type: "error", text: err.message });
|
|
444
520
|
}
|
|
445
|
-
|
|
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");
|
|
521
|
+
|
|
450
522
|
setDoneMessages(msgs);
|
|
451
523
|
setStep("done");
|
|
452
524
|
}}
|
|
@@ -461,8 +533,6 @@ function App({ opts }) {
|
|
|
461
533
|
);
|
|
462
534
|
}
|
|
463
535
|
|
|
464
|
-
// ── Export ───────────────────────────────────────────────────────────────
|
|
465
|
-
|
|
466
536
|
export async function runInteractiveUI(opts) {
|
|
467
537
|
const { waitUntilExit } = render(<App opts={opts} />);
|
|
468
538
|
await waitUntilExit();
|