buddy-reroll 0.2.0 → 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.
Files changed (3) hide show
  1. package/index.js +38 -10
  2. package/package.json +1 -1
  3. package/ui.jsx +10 -21
package/index.js CHANGED
@@ -100,8 +100,11 @@ function getClaudeConfigDir() {
100
100
  }
101
101
 
102
102
  function findBinaryPath() {
103
+ const isWin = platform() === "win32";
104
+
103
105
  try {
104
- const allPaths = execSync("which -a claude 2>/dev/null", { encoding: "utf-8" }).trim().split("\n");
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");
105
108
  for (const entry of allPaths) {
106
109
  try {
107
110
  const resolved = realpathSync(entry.trim());
@@ -110,8 +113,12 @@ function findBinaryPath() {
110
113
  }
111
114
  } catch {}
112
115
 
113
- const versionsDir = join(homedir(), ".local", "share", "claude", "versions");
114
- if (existsSync(versionsDir)) {
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;
115
122
  try {
116
123
  const versions = readdirSync(versionsDir)
117
124
  .filter((f) => !f.includes(".backup"))
@@ -133,6 +140,12 @@ function findConfigPath() {
133
140
  const defaultPath = join(home, ".claude.json");
134
141
  if (existsSync(defaultPath)) return defaultPath;
135
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
+
136
149
  return null;
137
150
  }
138
151
 
@@ -146,9 +159,9 @@ function getUserId(configPath) {
146
159
  function findCurrentSalt(binaryData, userId) {
147
160
  if (binaryData.includes(Buffer.from(ORIGINAL_SALT))) return ORIGINAL_SALT;
148
161
 
149
- const text = binaryData.toString("utf-8");
162
+ const text = binaryData.toString("latin1");
150
163
 
151
- // Scan for previously patched salts (our known patterns)
164
+ // Scan for previously patched salts
152
165
  const patterns = [
153
166
  new RegExp(`x{${SALT_LEN - 8}}\\d{8}`, "g"),
154
167
  new RegExp(`friend-\\d{4}-.{${SALT_LEN - 12}}`, "g"),
@@ -160,7 +173,6 @@ function findCurrentSalt(binaryData, userId) {
160
173
  }
161
174
  }
162
175
 
163
- // Contextual scan near companion code markers
164
176
  const saltRegex = new RegExp(`"([a-zA-Z0-9_-]{${SALT_LEN}})"`, "g");
165
177
  const candidates = new Set();
166
178
  const markers = ["rollRarity", "CompanionBones", "inspirationSeed", "companionUserId"];
@@ -174,7 +186,6 @@ function findCurrentSalt(binaryData, userId) {
174
186
  }
175
187
  }
176
188
 
177
- // Filter: real salts contain digits or hyphens (rules out "projectSettings" etc.)
178
189
  for (const c of candidates) {
179
190
  if (/[\d-]/.test(c)) return c;
180
191
  }
@@ -233,6 +244,10 @@ function matches(roll, target) {
233
244
 
234
245
  function isClaudeRunning() {
235
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
+ }
236
251
  const out = execSync("pgrep -af claude 2>/dev/null", { encoding: "utf-8" });
237
252
  return out.split("\n").some((line) => !line.includes("buddy-reroll") && line.trim().length > 0);
238
253
  } catch {
@@ -261,8 +276,20 @@ function patchBinary(binaryPath, oldSalt, newSalt) {
261
276
 
262
277
  if (count === 0) throw new Error(`Salt "${oldSalt}" not found in binary`);
263
278
 
264
- writeFileSync(binaryPath, data);
265
- return count;
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
+ }
266
293
  }
267
294
 
268
295
  function resignBinary(binaryPath) {
@@ -305,7 +332,8 @@ function formatCompanionCard(result) {
305
332
  }
306
333
 
307
334
  for (const [k, v] of Object.entries(result.stats)) {
308
- const bar = colorFn("█".repeat(Math.round(v / 10)) + "░".repeat(10 - Math.round(v / 10)));
335
+ const filled = Math.min(10, Math.max(0, Math.round(v / 10)));
336
+ const bar = colorFn("█".repeat(filled) + "░".repeat(10 - filled));
309
337
  lines.push(` ${k.padEnd(10)} ${bar} ${String(v).padStart(3)}`);
310
338
  }
311
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-reroll",
3
- "version": "0.2.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": {
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
- // Simple spinner avoids @inkjs/ui dependency for one component
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
- // ── Shared Components ───────────────────────────────────────────────────
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] ?? "";
@@ -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
 
@@ -233,21 +229,16 @@ function DoneStep({ messages, isActive }) {
233
229
  );
234
230
  }
235
231
 
236
- // ── Step Flow ───────────────────────────────────────────────────────────
237
-
238
232
  const STEP_ORDER = ["action", "species", "rarity", "eye", "hat", "shiny", "confirm"];
239
233
 
240
234
  function getPrevStep(current, rarity) {
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";
246
239
  return prev;
247
240
  }
248
241
 
249
- // ── Main App ────────────────────────────────────────────────────────────
250
-
251
242
  function App({ opts }) {
252
243
  const { exit } = useApp();
253
244
  const {
@@ -267,6 +258,8 @@ function App({ opts }) {
267
258
 
268
259
  const showStats = step === "showCurrent" || step === "result" || step === "done";
269
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 });
270
263
 
271
264
  const goBack = (toStep) => {
272
265
  const prev = toStep || getPrevStep(step, rarity);
@@ -379,8 +372,7 @@ function App({ opts }) {
379
372
  isActive={step === "shiny"}
380
373
  onConfirm={() => {
381
374
  setShiny(true);
382
- const target = { species, rarity, eye, hat: rarity === "common" ? "none" : hat, shiny: true };
383
- if (matches(currentRoll, target)) {
375
+ if (matches(currentRoll, buildTarget(true))) {
384
376
  setDoneMessages(["Already matching! No changes needed."]);
385
377
  setStep("done");
386
378
  } else {
@@ -389,8 +381,7 @@ function App({ opts }) {
389
381
  }}
390
382
  onCancel={() => {
391
383
  setShiny(false);
392
- const target = { species, rarity, eye, hat: rarity === "common" ? "none" : hat, shiny: false };
393
- if (matches(currentRoll, target)) {
384
+ if (matches(currentRoll, buildTarget(false))) {
394
385
  setDoneMessages(["Already matching! No changes needed."]);
395
386
  setStep("done");
396
387
  } else {
@@ -403,7 +394,7 @@ function App({ opts }) {
403
394
 
404
395
  {step === "confirm" && (
405
396
  <Box flexDirection="column">
406
- <Text>Target: <Text bold>{species}</Text> / <Text bold>{rarity}</Text> / eye:{eye} / hat:{rarity === "common" ? "none" : hat}{shiny ? " / shiny" : ""}</Text>
397
+ <Text>Target: <Text bold>{species}</Text> / <Text bold>{rarity}</Text> / eye:{eye} / hat:{effectiveHat}{shiny ? " / shiny" : ""}</Text>
407
398
  {isClaudeRunning() && <Text color="yellow">⚠ Claude Code appears to be running. Quit it before patching.</Text>}
408
399
  <ConfirmSelect
409
400
  label="Search and apply?"
@@ -418,7 +409,7 @@ function App({ opts }) {
418
409
  {step === "search" && (
419
410
  <SearchStep
420
411
  userId={userId}
421
- target={{ species, rarity, eye, hat: rarity === "common" ? "none" : hat, shiny }}
412
+ target={buildTarget()}
422
413
  bruteForce={bruteForce}
423
414
  onFound={(f) => { setFound(f); setStep("result"); }}
424
415
  onFail={() => {
@@ -461,8 +452,6 @@ function App({ opts }) {
461
452
  );
462
453
  }
463
454
 
464
- // ── Export ───────────────────────────────────────────────────────────────
465
-
466
455
  export async function runInteractiveUI(opts) {
467
456
  const { waitUntilExit } = render(<App opts={opts} />);
468
457
  await waitUntilExit();