buddy-reroll 0.3.3 → 0.3.4
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 +36 -26
- package/lib/finder.js +23 -20
- package/lib/hooks.js +21 -8
- package/lib/hooks.test.js +3 -3
- package/package.json +1 -1
- package/ui.jsx +57 -35
package/index.js
CHANGED
|
@@ -40,8 +40,12 @@ if (!IS_BUN && !IS_APPLY_HOOK) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function getUserId(configPath) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
try {
|
|
44
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
45
|
+
return config.oauthAccount?.accountUuid ?? config.userID ?? "anon";
|
|
46
|
+
} catch {
|
|
47
|
+
return "anon";
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
// ── Binary patch ─────────────────────────────────────────────────────────
|
|
@@ -93,29 +97,24 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
|
|
|
93
97
|
if (count === 0) throw new Error(`Salt "${oldSalt}" not found in binary`);
|
|
94
98
|
|
|
95
99
|
const isWin = platform() === "win32";
|
|
100
|
+
const tmpPath = binaryPath + ".tmp";
|
|
96
101
|
const maxRetries = isWin ? 3 : 1;
|
|
102
|
+
|
|
97
103
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
98
104
|
try {
|
|
99
|
-
const tmpPath = binaryPath + ".tmp";
|
|
100
105
|
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
106
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
const verify = readFileSync(tmpPath);
|
|
108
|
+
if (verify.indexOf(Buffer.from(newSalt)) === -1) {
|
|
109
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
110
|
+
throw new Error("Patch verification failed — new salt not found in temp file");
|
|
111
|
+
}
|
|
115
112
|
|
|
113
|
+
renameSync(tmpPath, binaryPath);
|
|
114
|
+
try { chmodSync(binaryPath, originalMode); } catch {}
|
|
116
115
|
return count;
|
|
117
116
|
} catch (err) {
|
|
118
|
-
try { unlinkSync(
|
|
117
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
119
118
|
if (isWin && (err.code === "EACCES" || err.code === "EPERM" || err.code === "EBUSY") && attempt < maxRetries - 1) {
|
|
120
119
|
sleepMs(2000);
|
|
121
120
|
continue;
|
|
@@ -123,7 +122,7 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
|
|
|
123
122
|
if (isWin && (err.code === "EPERM" || err.code === "EBUSY")) {
|
|
124
123
|
throw new Error("Can't write — Claude Code might still be running. Close it and try again.");
|
|
125
124
|
}
|
|
126
|
-
throw
|
|
125
|
+
throw err;
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
}
|
|
@@ -143,12 +142,18 @@ function resignBinary(binaryPath) {
|
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
function clearCompanion(configPath) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
try {
|
|
146
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
147
|
+
const config = JSON.parse(raw);
|
|
148
|
+
if (!config.companion && !config.companionMuted) return;
|
|
149
|
+
delete config.companion;
|
|
150
|
+
delete config.companionMuted;
|
|
151
|
+
const indent = raw.match(/^(\s+)"/m)?.[1] ?? " ";
|
|
152
|
+
const mode = statSync(configPath).mode;
|
|
153
|
+
const tmpPath = configPath + ".tmp";
|
|
154
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, indent) + "\n", { mode });
|
|
155
|
+
renameSync(tmpPath, configPath);
|
|
156
|
+
} catch {}
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
function fail(message) {
|
|
@@ -460,13 +465,13 @@ async function main() {
|
|
|
460
465
|
}
|
|
461
466
|
|
|
462
467
|
if (args["apply-hook"]) {
|
|
468
|
+
let mutated = false;
|
|
463
469
|
try {
|
|
464
470
|
const stored = readStoredSalt();
|
|
465
471
|
if (!stored) process.exit(0);
|
|
466
472
|
const bp = findBinaryPath();
|
|
467
473
|
const cp = findConfigPath();
|
|
468
474
|
if (!bp || !cp) process.exit(0);
|
|
469
|
-
const uid = getUserId(cp);
|
|
470
475
|
const binaryData = readFileSync(bp);
|
|
471
476
|
const currentSalt = findCurrentSalt(binaryData);
|
|
472
477
|
if (!currentSalt) process.exit(0);
|
|
@@ -476,16 +481,21 @@ async function main() {
|
|
|
476
481
|
const backupPath = patchability.backupPath;
|
|
477
482
|
if (!existsSync(backupPath)) copyFileSync(bp, backupPath);
|
|
478
483
|
patchBinary(bp, currentSalt, stored.salt);
|
|
484
|
+
mutated = true;
|
|
479
485
|
if (platform() === "darwin") {
|
|
480
486
|
try {
|
|
481
487
|
execFileSync("codesign", ["-s", "-", "--force", bp], { stdio: "ignore", timeout: 30000 });
|
|
482
488
|
} catch {
|
|
483
489
|
copyFileSync(backupPath, bp);
|
|
490
|
+
try { chmodSync(bp, statSync(backupPath).mode); } catch {}
|
|
484
491
|
process.exit(1);
|
|
485
492
|
}
|
|
486
493
|
}
|
|
487
494
|
clearCompanion(cp);
|
|
488
|
-
} catch {
|
|
495
|
+
} catch (err) {
|
|
496
|
+
process.stderr.write(`buddy-reroll --apply-hook failed: ${err.message}\n`);
|
|
497
|
+
process.exit(mutated ? 1 : 0);
|
|
498
|
+
}
|
|
489
499
|
process.exit(0);
|
|
490
500
|
}
|
|
491
501
|
|
package/lib/finder.js
CHANGED
|
@@ -7,7 +7,7 @@ import { rollFrom } from "./companion.js";
|
|
|
7
7
|
|
|
8
8
|
const WORKER_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "worker.js");
|
|
9
9
|
|
|
10
|
-
export async function parallelBruteForce(userId, target, onProgress) {
|
|
10
|
+
export async function parallelBruteForce(userId, target, onProgress, signal) {
|
|
11
11
|
const numWorkers = Math.max(1, Math.min(cpus().length, 8));
|
|
12
12
|
const expected = estimateAttempts(target);
|
|
13
13
|
|
|
@@ -18,13 +18,25 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
18
18
|
const workerAttempts = [];
|
|
19
19
|
let resolved = false;
|
|
20
20
|
let exited = 0;
|
|
21
|
+
let timer;
|
|
22
|
+
|
|
23
|
+
function finish(fn, val) {
|
|
24
|
+
if (resolved) return;
|
|
25
|
+
resolved = true;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
killAll();
|
|
28
|
+
fn(val);
|
|
29
|
+
}
|
|
21
30
|
|
|
22
31
|
function killAll() {
|
|
23
32
|
for (const child of children) {
|
|
24
|
-
try { child.kill(); } catch {}
|
|
33
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
if (signal?.aborted) { reject(new Error("Cancelled")); return; }
|
|
38
|
+
signal?.addEventListener("abort", () => finish(reject, new Error("Cancelled")), { once: true });
|
|
39
|
+
|
|
28
40
|
for (let i = 0; i < numWorkers; i++) {
|
|
29
41
|
workerStdout[i] = "";
|
|
30
42
|
workerStderr[i] = "";
|
|
@@ -50,9 +62,7 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
50
62
|
workerAttempts[i] = progress.attempts;
|
|
51
63
|
const totalAttempts = workerAttempts.reduce((a, b) => a + b, 0);
|
|
52
64
|
const elapsed = progress.elapsed || 0;
|
|
53
|
-
if (onProgress)
|
|
54
|
-
onProgress(totalAttempts, elapsed, expected, numWorkers);
|
|
55
|
-
}
|
|
65
|
+
if (onProgress) onProgress(totalAttempts, elapsed, expected, numWorkers);
|
|
56
66
|
}
|
|
57
67
|
} catch {}
|
|
58
68
|
}
|
|
@@ -63,14 +73,11 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
63
73
|
if (resolved) return;
|
|
64
74
|
|
|
65
75
|
if (code === 0 && workerStdout[i].trim()) {
|
|
66
|
-
resolved = true;
|
|
67
|
-
killAll();
|
|
68
|
-
|
|
69
76
|
try {
|
|
70
77
|
const result = JSON.parse(workerStdout[i].trim());
|
|
71
78
|
const totalAttempts = workerAttempts.reduce((a, b) => a + b, 0);
|
|
72
79
|
const roll = rollFrom(result.salt, userId);
|
|
73
|
-
resolve
|
|
80
|
+
finish(resolve, {
|
|
74
81
|
salt: result.salt,
|
|
75
82
|
result: roll,
|
|
76
83
|
checked: Math.max(totalAttempts, result.attempts),
|
|
@@ -78,35 +85,31 @@ export async function parallelBruteForce(userId, target, onProgress) {
|
|
|
78
85
|
workers: numWorkers,
|
|
79
86
|
});
|
|
80
87
|
} catch (err) {
|
|
81
|
-
reject
|
|
88
|
+
finish(reject, err);
|
|
82
89
|
}
|
|
83
90
|
return;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
if (exited === numWorkers
|
|
93
|
+
if (exited === numWorkers) {
|
|
87
94
|
const totalAttempts = workerAttempts.reduce((a, b) => a + b, 0);
|
|
88
95
|
const stderr = workerStderr.filter(Boolean).join("\n").trim();
|
|
89
96
|
const detail = stderr ? `\n Worker output: ${stderr.slice(0, 200)}` : "";
|
|
90
|
-
reject
|
|
97
|
+
finish(reject, new Error(`All ${numWorkers} workers exited without finding a match (${totalAttempts.toLocaleString()} tries).${detail}`));
|
|
91
98
|
}
|
|
92
99
|
});
|
|
93
100
|
|
|
94
101
|
child.on("error", (err) => {
|
|
95
102
|
exited++;
|
|
96
103
|
if (exited === numWorkers && !resolved) {
|
|
97
|
-
reject
|
|
104
|
+
finish(reject, new Error(`Worker failed to start: ${err.message}`));
|
|
98
105
|
}
|
|
99
106
|
});
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
const timeoutMs = Math.max(600_000, Math.ceil(expected / 50_000_000) * 60_000 + 600_000);
|
|
103
|
-
setTimeout(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
killAll();
|
|
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.`));
|
|
109
|
-
}
|
|
110
|
+
timer = setTimeout(() => {
|
|
111
|
+
const totalAttempts = workerAttempts.reduce((a, b) => a + b, 0);
|
|
112
|
+
finish(reject, new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s (${totalAttempts.toLocaleString()} tries). This combination might be extremely rare — try fewer constraints.`));
|
|
110
113
|
}, timeoutMs);
|
|
111
114
|
});
|
|
112
115
|
}
|
package/lib/hooks.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, statSync } from "fs";
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
|
|
5
5
|
const HOOK_COMMAND = "npx buddy-reroll --apply-hook";
|
|
6
6
|
|
|
7
7
|
const HOOK_ENTRY = {
|
|
8
|
-
matcher: "",
|
|
9
8
|
hooks: [{ type: "command", command: HOOK_COMMAND }],
|
|
10
9
|
};
|
|
11
10
|
|
|
@@ -24,18 +23,28 @@ export function readSettings(settingsPath) {
|
|
|
24
23
|
try {
|
|
25
24
|
return JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
26
25
|
} catch {
|
|
27
|
-
return
|
|
26
|
+
return null;
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
function writeSettingsAtomic(settingsPath, obj) {
|
|
32
31
|
const dir = dirname(settingsPath);
|
|
33
32
|
mkdirSync(dir, { recursive: true });
|
|
34
|
-
|
|
33
|
+
const tmpPath = settingsPath + ".tmp";
|
|
34
|
+
const content = JSON.stringify(obj, null, 2) + "\n";
|
|
35
|
+
let mode;
|
|
36
|
+
try { mode = statSync(settingsPath).mode; } catch {}
|
|
37
|
+
writeFileSync(tmpPath, content, mode ? { mode } : undefined);
|
|
38
|
+
renameSync(tmpPath, settingsPath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeSettings(settingsPath, obj) {
|
|
42
|
+
writeSettingsAtomic(settingsPath, obj);
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export function installHook(settingsPath = getSettingsPath()) {
|
|
38
46
|
const settings = readSettings(settingsPath);
|
|
47
|
+
if (!settings) return { installed: false, reason: "settings file is corrupted, not modifying" };
|
|
39
48
|
|
|
40
49
|
if (!settings.hooks) settings.hooks = {};
|
|
41
50
|
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
@@ -47,13 +56,14 @@ export function installHook(settingsPath = getSettingsPath()) {
|
|
|
47
56
|
|
|
48
57
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((e) => !isOurHook(e));
|
|
49
58
|
settings.hooks.SessionStart.push(HOOK_ENTRY);
|
|
50
|
-
|
|
59
|
+
writeSettingsAtomic(settingsPath, settings);
|
|
51
60
|
|
|
52
61
|
return { installed: true, path: settingsPath };
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export function removeHook(settingsPath = getSettingsPath()) {
|
|
56
65
|
const settings = readSettings(settingsPath);
|
|
66
|
+
if (!settings) return { removed: false, reason: "settings file is corrupted, not modifying" };
|
|
57
67
|
|
|
58
68
|
if (!settings.hooks || !Array.isArray(settings.hooks.SessionStart)) {
|
|
59
69
|
return { removed: false, reason: "not installed" };
|
|
@@ -69,12 +79,13 @@ export function removeHook(settingsPath = getSettingsPath()) {
|
|
|
69
79
|
if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
|
|
70
80
|
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
writeSettingsAtomic(settingsPath, settings);
|
|
73
83
|
return { removed: true, path: settingsPath };
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
export function isHookInstalled(settingsPath = getSettingsPath()) {
|
|
77
87
|
const settings = readSettings(settingsPath);
|
|
88
|
+
if (!settings) return false;
|
|
78
89
|
return Array.isArray(settings.hooks?.SessionStart) && settings.hooks.SessionStart.some(isOurHook);
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -86,7 +97,9 @@ export function storeSalt(salt) {
|
|
|
86
97
|
const path = getSaltStorePath();
|
|
87
98
|
const dir = dirname(path);
|
|
88
99
|
mkdirSync(dir, { recursive: true });
|
|
89
|
-
|
|
100
|
+
const tmpPath = path + ".tmp";
|
|
101
|
+
writeFileSync(tmpPath, JSON.stringify({ salt, timestamp: Date.now() }, null, 2) + "\n");
|
|
102
|
+
renameSync(tmpPath, path);
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
export function readStoredSalt() {
|
package/lib/hooks.test.js
CHANGED
|
@@ -45,10 +45,10 @@ describe("readSettings", () => {
|
|
|
45
45
|
expect(readSettings(p)).toEqual({ permissions: { allow: [] } });
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it("returns
|
|
48
|
+
it("returns null for corrupted JSON", () => {
|
|
49
49
|
const p = join(tempDir, "settings.json");
|
|
50
50
|
writeFileSync(p, "{ invalid json");
|
|
51
|
-
expect(readSettings(p)).
|
|
51
|
+
expect(readSettings(p)).toBeNull();
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -76,7 +76,7 @@ describe("installHook", () => {
|
|
|
76
76
|
const settings = readSettings(p);
|
|
77
77
|
const hook = findOurHook(settings);
|
|
78
78
|
expect(hook).toBeDefined();
|
|
79
|
-
expect(hook.matcher).
|
|
79
|
+
expect(hook.matcher).toBeUndefined();
|
|
80
80
|
expect(hook.hooks[0].type).toBe("command");
|
|
81
81
|
expect(hook.hooks[0].command).toBe(HOOK_COMMAND);
|
|
82
82
|
});
|
package/package.json
CHANGED
package/ui.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from "react";
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
2
2
|
import { render, Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import { renderSprite, RARITY_STARS, RARITY_COLORS } from "./sprites.js";
|
|
4
4
|
import { existsSync, copyFileSync } from "fs";
|
|
@@ -125,19 +125,41 @@ function PreviewCard({ species, rarity, eye, hat, shiny, stats }) {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function
|
|
128
|
+
function ExitOnKey({ isActive, children }) {
|
|
129
129
|
const { exit } = useApp();
|
|
130
|
+
const exiting = useRef(false);
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
const doExit = useCallback(() => {
|
|
133
|
+
if (exiting.current) return;
|
|
134
|
+
exiting.current = true;
|
|
132
135
|
exit();
|
|
133
|
-
setTimeout(() => process.exit(0),
|
|
134
|
-
},
|
|
136
|
+
setTimeout(() => process.exit(0), 200);
|
|
137
|
+
}, [exit]);
|
|
138
|
+
|
|
139
|
+
useInput(() => doExit(), { isActive });
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!isActive) return;
|
|
143
|
+
const fallback = () => doExit();
|
|
144
|
+
process.stdin.once("data", fallback);
|
|
145
|
+
const timer = setTimeout(() => doExit(), 30000);
|
|
146
|
+
return () => {
|
|
147
|
+
process.stdin.removeListener("data", fallback);
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
};
|
|
150
|
+
}, [isActive, doExit]);
|
|
151
|
+
|
|
152
|
+
return children;
|
|
153
|
+
}
|
|
135
154
|
|
|
155
|
+
function ShowCurrentStep({ isActive }) {
|
|
136
156
|
return (
|
|
137
|
-
<
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
<ExitOnKey isActive={isActive}>
|
|
158
|
+
<Box flexDirection="column">
|
|
159
|
+
<Text color="green">✓ Current companion shown above.</Text>
|
|
160
|
+
<KeyHint>Press any key to exit</KeyHint>
|
|
161
|
+
</Box>
|
|
162
|
+
</ExitOnKey>
|
|
141
163
|
);
|
|
142
164
|
}
|
|
143
165
|
|
|
@@ -169,40 +191,44 @@ function SpeciesStep({ speciesList, current, onChange, onSubmit, onBack, isActiv
|
|
|
169
191
|
|
|
170
192
|
function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
171
193
|
const [progress, setProgress] = useState("");
|
|
172
|
-
const
|
|
194
|
+
const abortRef = useRef(null);
|
|
173
195
|
const hasStarted = useRef(false);
|
|
174
196
|
const { exit } = useApp();
|
|
175
197
|
|
|
176
198
|
useInput((input, key) => {
|
|
177
199
|
if (key.escape) {
|
|
178
|
-
|
|
200
|
+
if (abortRef.current) abortRef.current.abort();
|
|
179
201
|
exit();
|
|
202
|
+
setTimeout(() => process.exit(0), 200);
|
|
180
203
|
}
|
|
181
204
|
}, { isActive });
|
|
182
205
|
|
|
183
206
|
useEffect(() => {
|
|
184
207
|
if (hasStarted.current) return;
|
|
185
208
|
hasStarted.current = true;
|
|
209
|
+
const ac = new AbortController();
|
|
210
|
+
abortRef.current = ac;
|
|
186
211
|
(async () => {
|
|
187
212
|
let found;
|
|
188
213
|
try {
|
|
189
214
|
found = await bruteForce(userId, target, (attempts, elapsed, expected, workers) => {
|
|
190
|
-
if (!
|
|
215
|
+
if (!ac.signal.aborted) {
|
|
191
216
|
const pct = Math.min(100, Math.round((attempts / expected) * 100));
|
|
192
217
|
const rate = attempts / (elapsed / 1000);
|
|
193
218
|
const rateStr = rate >= 1e6 ? `${(rate / 1e6).toFixed(1)}M` : `${(rate / 1e3).toFixed(1)}k`;
|
|
194
219
|
const eta = Math.max(0, (expected - attempts) / rate);
|
|
195
220
|
setProgress(`${pct}% | ${rateStr} tries/s | ~${Math.round(eta)}s left | ${workers} cores`);
|
|
196
221
|
}
|
|
197
|
-
});
|
|
222
|
+
}, ac.signal);
|
|
198
223
|
} catch {
|
|
199
|
-
if (!
|
|
224
|
+
if (!ac.signal.aborted) onFail();
|
|
200
225
|
return;
|
|
201
226
|
}
|
|
202
|
-
if (
|
|
227
|
+
if (ac.signal.aborted) return;
|
|
203
228
|
if (found) onFound(found);
|
|
204
229
|
else onFail();
|
|
205
230
|
})();
|
|
231
|
+
return () => ac.abort();
|
|
206
232
|
}, [bruteForce, userId, target, onFound, onFail]);
|
|
207
233
|
|
|
208
234
|
return (
|
|
@@ -214,30 +240,26 @@ function SearchStep({ userId, target, bruteForce, onFound, onFail, isActive }) {
|
|
|
214
240
|
}
|
|
215
241
|
|
|
216
242
|
function DoneStep({ messages, isActive }) {
|
|
217
|
-
const { exit } = useApp();
|
|
218
243
|
const hasErrors = messages.some((msg) => msg.type === "error");
|
|
219
244
|
|
|
220
|
-
useInput(() => {
|
|
221
|
-
exit();
|
|
222
|
-
setTimeout(() => process.exit(0), 100);
|
|
223
|
-
}, { isActive });
|
|
224
|
-
|
|
225
245
|
return (
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
{msg.type === "error" ? "
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
<ExitOnKey isActive={isActive}>
|
|
247
|
+
<Box flexDirection="column">
|
|
248
|
+
{messages.map((msg) => (
|
|
249
|
+
<Text key={`${msg.type}-${msg.text}`} color={msg.type === "error" ? "red" : "green"}>
|
|
250
|
+
{msg.type === "error" ? "✗ " : "✓ "}{msg.text}
|
|
251
|
+
</Text>
|
|
252
|
+
))}
|
|
253
|
+
<Box marginTop={1}>
|
|
254
|
+
<Text bold>
|
|
255
|
+
{hasErrors
|
|
256
|
+
? "Something went wrong — check the issue above and try again."
|
|
257
|
+
: "All set! Your buddy will stick around even after updates. Restart Claude Code and say /buddy!"}
|
|
258
|
+
</Text>
|
|
259
|
+
</Box>
|
|
260
|
+
<KeyHint>Press any key to exit</KeyHint>
|
|
238
261
|
</Box>
|
|
239
|
-
|
|
240
|
-
</Box>
|
|
262
|
+
</ExitOnKey>
|
|
241
263
|
);
|
|
242
264
|
}
|
|
243
265
|
|