claude-buddy-reroll 1.8.13 → 1.8.15

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 CHANGED
@@ -19,6 +19,7 @@ npm i -g claude-buddy-reroll
19
19
 
20
20
  기본 화면은 스피키 스타일의 리치 터미널 홈으로 시작하고, `dex`는 발견한 개체를 다시 적용할 수 있게 설계돼 있어요.
21
21
  지금은 `fullscreen dex`, `animated preview`, `form collection`, `one-by-one gacha reveal`까지 들어가 있어요.
22
+ 여러 Claude 계정이 있어도 현재 계정을 직접 읽어서 계정별 프로필 상태를 따로 저장해요.
22
23
 
23
24
  처음 설치할 때 런타임 셋업이 자동으로 Bun과 Claude 연동을 준비해요. 자동 셋업이 실패했거나 `Bun is required` 에러가 보이면 아래를 한 번 실행하세요.
24
25
 
@@ -27,6 +28,7 @@ bdy setup
27
28
  ```
28
29
 
29
30
  `bdy update`도 업데이트 후 같은 셋업을 자동으로 다시 확인해요.
31
+ 그리고 현재 계정 기준으로 도감 정합성도 다시 맞춰요.
30
32
 
31
33
  ---
32
34
 
@@ -51,6 +53,7 @@ bdy setup # Bun / 런타임 셋업 복구
51
53
  - species progress와 form collection을 분리한 도감
52
54
  - 10연차 one-by-one reveal + highlight-aware skip
53
55
  - 오른쪽 상세 패널에 animated preview / flavor text / rarity track / form gallery
56
+ - 현재 Claude 계정을 매번 직접 읽고 계정별 프로필 상태를 분리 저장
54
57
  - JSON 모드는 유지해서 에이전트/스크립트 자동화도 가능
55
58
 
56
59
  ### Agent DX (JSON 모드)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-buddy-reroll",
3
- "version": "1.8.13",
3
+ "version": "1.8.15",
4
4
  "description": "Speaki-styled npm terminal for buddy rerolls, dex replay, and SALT patching.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -18,7 +18,7 @@ import { roll, multiRoll, ORIGINAL_SALT, EYES, HATS, STATS, RARITY_STARS, SPECIE
18
18
  import { patchSalt, clearSoul, restoreOriginal } from './patcher.mjs';
19
19
  import { MIN_SUPPORTED_CLAUDE_VERSION, resolveClaudeContext, updatePatchedSalt } from './context.mjs';
20
20
  import { renderCard, renderMiniCard } from './display.mjs';
21
- import { addBatchToCollection, getCollection, getLatestVariant, getPreferredVariant, getRarityCompletion, getShinyVariant, renderCollection } from './collection.mjs';
21
+ import { addBatchToCollection, getCollection, getLatestVariant, getPreferredVariant, getRarityCompletion, getShinyVariant, renderCollection, resyncCollection } from './collection.mjs';
22
22
  import { select } from './selector.mjs';
23
23
  import { playHatchAnimation } from './animation.mjs';
24
24
  import { createInterface, emitKeypressEvents } from 'readline';
@@ -30,7 +30,7 @@ import { renderSprite } from './sprites.mjs';
30
30
  import { formatEye, formatStars, toTerminalSafeText } from './terminal.mjs';
31
31
  import { findNextHighlightIndex, getRevealAction } from './gacha-reveal.mjs';
32
32
  import { BUDDY_LORE, wrapLore } from './buddy-lore.mjs';
33
- import { loadProfileState, saveProfileState } from './profile-state.mjs';
33
+ import { listKnownProfiles, loadProfileState, saveProfileState } from './profile-state.mjs';
34
34
 
35
35
  const BOLD = '\x1b[1m';
36
36
  const DIM = '\x1b[2m';
@@ -119,6 +119,10 @@ function mapResult(result, index = null) {
119
119
  };
120
120
  }
121
121
 
122
+ function formatProfileBadge(userId) {
123
+ return userId === 'anon' ? 'anonymous' : `${userId.slice(0, 8)}...`;
124
+ }
125
+
122
126
  // ─── Star check ─────────────────────────────────────
123
127
 
124
128
  const _starCache = new Map();
@@ -366,14 +370,15 @@ function buildTamagotchiPreview(species, entry, tick = 0) {
366
370
  ...lines.map((line) => ` | ${toTerminalSafeText(line).padEnd(width)} |`),
367
371
  ' +----------------------+',
368
372
  ];
369
- const padSlot = (value) => value.padEnd(10);
373
+ const fit = (value) => toTerminalSafeText(String(value || '').slice(0, width)).padEnd(width);
374
+ const formLine = (label, value) => fit(`${label} ${value}`);
370
375
 
371
376
  const variant = entry ? getPreferredVariant(entry) : null;
372
377
  if (!variant) {
373
378
  return [
374
379
  `${MAGENTA}${BOLD} PIXEL PREVIEW${RESET}`,
375
380
  ` ${DIM}undiscovered buddy${RESET}`,
376
- ...panel('PROFILE', ['best --', 'latest --', 'shiny --', 'forms 0']),
381
+ ...panel('PROFILE', ['best --', 'latest --', 'shiny no', 'forms 0']),
377
382
  ...panel('PREVIEW', [
378
383
  ' .------. ',
379
384
  ' / ?? ?? \\ ',
@@ -383,24 +388,28 @@ function buildTamagotchiPreview(species, entry, tick = 0) {
383
388
  '',
384
389
  ]),
385
390
  ...panel('FLAVOR TEXT', ['Find this species', 'in gacha first,', 'then come back to', 'unlock its card.']),
386
- ...panel('RARITY TRACK', ['-- common -- uncommon', '-- rare -- epic', '-- legendary']),
387
- ...panel('FORMS', [`${padSlot('--')} ${padSlot('--')}`, `${padSlot('--')} ${padSlot('--')}`]),
391
+ ...panel('RARITY TRACK', ['[ ] common', '[ ] uncommon', '[ ] rare', '[ ] epic', '[ ] legendary']),
392
+ ...panel('FORMS', ['1. --', '2. --', '3. --', '4. --']),
388
393
  ].join('\n');
389
394
  }
390
395
 
391
396
  const latestVariant = getLatestVariant(entry);
392
397
  const shinyVariant = getShinyVariant(entry);
393
- const rarityCompletion = getRarityCompletion(entry)
394
- .map(({ rarity, found }) => `${found ? formatStars(RARITY_STARS[rarity]) : '--'} ${rarity}`)
395
- .join(' ');
398
+ const rarityTrack = getRarityCompletion(entry).map(({ rarity, found }) =>
399
+ fit(`${found ? '[x]' : '[ ]'} ${rarity.padEnd(9)} ${formatStars(RARITY_STARS[rarity])}`),
400
+ );
396
401
  const gallery = entry.variants
397
402
  .slice(0, 4)
398
- .map((item) => {
399
- const shinyMark = item.bones.shiny ? ' ✨' : '';
400
- return `${formatStars(RARITY_STARS[item.bones.rarity])} ${formatEye(item.bones.eye)} ${toTerminalSafeText(item.bones.hat)}${shinyMark}`.slice(0, 10);
403
+ .map((item, index) => {
404
+ const tags = [
405
+ item.salt === variant.salt ? 'BEST' : null,
406
+ latestVariant && item.salt === latestVariant.salt ? 'NEW' : null,
407
+ item.bones.shiny ? 'SHN' : null,
408
+ ].filter(Boolean).join('/');
409
+ const core = `${formatStars(RARITY_STARS[item.bones.rarity])} ${formatEye(item.bones.eye)} ${toTerminalSafeText(item.bones.hat)}`;
410
+ return fit(`${index + 1}. ${core}${tags ? ` ${tags}` : ''}`);
401
411
  });
402
- while (gallery.length < 4) gallery.push('--');
403
- const rarityTrack = getRarityCompletion(entry).map(({ rarity, found }) => `${found ? formatStars(RARITY_STARS[rarity]) : '--'} ${rarity}`);
412
+ while (gallery.length < 4) gallery.push(fit('--'));
404
413
 
405
414
  const stars = formatStars(RARITY_STARS[variant.bones.rarity]);
406
415
  const loreLines = wrapLore(BUDDY_LORE[species] || '', 20, 4);
@@ -423,14 +432,14 @@ function buildTamagotchiPreview(species, entry, tick = 0) {
423
432
  ` ${DIM}${variant.bones.rarity} • x${entry.count}${variant.bones.shiny ? ' • shiny' : ''}${RESET}`,
424
433
  ...panel('PREVIEW', sprite),
425
434
  ...panel('PROFILE', [
426
- `eye ${formatEye(variant.bones.eye)} • ${toTerminalSafeText(variant.bones.hat)}`,
427
- `best ${formatStars(RARITY_STARS[variant.bones.rarity])}`,
428
- `latest ${latestVariant ? formatStars(RARITY_STARS[latestVariant.bones.rarity]) : '--'}`,
429
- `shiny ${shinyVariant ? 'yes' : 'no'} • forms ${entry.variants.length}`,
435
+ formLine('eye', `${formatEye(variant.bones.eye)} • ${toTerminalSafeText(variant.bones.hat)}`),
436
+ formLine('best', `${formatStars(RARITY_STARS[variant.bones.rarity])} ${variant.bones.rarity}`),
437
+ formLine('latest', latestVariant ? `${formatStars(RARITY_STARS[latestVariant.bones.rarity])} ${latestVariant.bones.rarity}` : '--'),
438
+ formLine('shiny', `${shinyVariant ? 'yes' : 'no'} • forms ${entry.variants.length}`),
430
439
  ]),
431
440
  ...panel('FLAVOR TEXT', loreLines),
432
- ...panel('RARITY TRACK', [`${rarityTrack[0]} ${rarityTrack[1]}`, `${rarityTrack[2]} ${rarityTrack[3]}`, `${rarityTrack[4]}`]),
433
- ...panel('FORMS', [`${padSlot(gallery[0])} ${padSlot(gallery[1])}`, `${padSlot(gallery[2])} ${padSlot(gallery[3])}`]),
441
+ ...panel('RARITY TRACK', rarityTrack),
442
+ ...panel('FORMS', gallery),
434
443
  ].join('\n');
435
444
  }
436
445
 
@@ -583,6 +592,7 @@ async function cmdCheck() {
583
592
  output({
584
593
  command: 'check',
585
594
  account: userId.slice(0, 8),
595
+ profiles: listKnownProfiles(),
586
596
  install: install ? { type: install.type, path: install.path } : null,
587
597
  salt: salt,
588
598
  patched: salt !== ORIGINAL_SALT,
@@ -602,6 +612,7 @@ async function cmdCheck() {
602
612
  quotaLines: renderQuotaSummary({ used, limit, starred, eventRemaining }),
603
613
  buddyCard: renderCard(buddy, { showSalt: true }),
604
614
  }));
615
+ console.log(`${DIM}Profile: ${formatProfileBadge(userId)} | Known profiles: ${listKnownProfiles().length}${RESET}\n`);
605
616
  }
606
617
 
607
618
  async function cmdGacha(count = 10) {
@@ -931,6 +942,7 @@ async function cmdDex() {
931
942
 
932
943
  // Show collection view
933
944
  console.log(renderCollection(previewContext.userId));
945
+ console.log(`${DIM} Current profile: ${formatProfileBadge(previewContext.userId)} | Known profiles: ${listKnownProfiles().length}${RESET}`);
934
946
 
935
947
  // Also show game info
936
948
  console.log(`${BOLD} Eyes:${RESET} ${EYES.join(' ')}`);
@@ -1128,6 +1140,19 @@ function cmdSchema(subCmd) {
1128
1140
  },
1129
1141
  },
1130
1142
  },
1143
+ doctor: {
1144
+ command: 'doctor',
1145
+ description: 'Inspect current profile or compare dex preview vs apply result',
1146
+ args: [{ name: 'scope', type: 'string', optional: true }, { name: 'pick', type: 'number', optional: true }],
1147
+ flags: ['--json'],
1148
+ response: {
1149
+ type: 'object',
1150
+ properties: {
1151
+ command: { type: 'string', const: 'doctor' },
1152
+ scope: { type: 'string' },
1153
+ },
1154
+ },
1155
+ },
1131
1156
  };
1132
1157
 
1133
1158
  const definitions = {
@@ -1177,6 +1202,7 @@ function cmdSchema(subCmd) {
1177
1202
 
1178
1203
  async function cmdUpdate() {
1179
1204
  const { execSync } = await import('child_process');
1205
+ const context = resolveClaudeContext();
1180
1206
 
1181
1207
  if (flags.json) {
1182
1208
  try {
@@ -1184,8 +1210,9 @@ async function cmdUpdate() {
1184
1210
  const current = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
1185
1211
  execSync('npm install -g claude-buddy-reroll@latest', { stdio: 'pipe' });
1186
1212
  runRuntimeSetup({ stdio: 'pipe' });
1213
+ resyncCollection(context.userId);
1187
1214
  const updated = execSync('npm info claude-buddy-reroll version', { encoding: 'utf-8' }).trim();
1188
- output({ command: 'update', previous: current, current: updated, updated: current !== updated, setup: true });
1215
+ output({ command: 'update', previous: current, current: updated, updated: current !== updated, setup: true, resynced: true });
1189
1216
  } catch (e) {
1190
1217
  errorJson('UPDATE_FAILED', e.message);
1191
1218
  }
@@ -1200,11 +1227,13 @@ async function cmdUpdate() {
1200
1227
  execSync('npm install -g claude-buddy-reroll@latest', { stdio: 'inherit' });
1201
1228
  console.log(`\n${DIM} 런타임 셋업 확인 중...${RESET}`);
1202
1229
  runRuntimeSetup({ stdio: 'inherit' });
1230
+ console.log(`${DIM} 현재 계정 도감 정합성 재검증 중...${RESET}`);
1231
+ resyncCollection(context.userId);
1203
1232
  const newVer = execSync('npm info claude-buddy-reroll version', { encoding: 'utf-8' }).trim();
1204
1233
  if (newVer !== pkg.version) {
1205
- console.log(`\n${GREEN} ✓ v${pkg.version} → v${newVer} 업데이트 완료! 쪼아요~!${RESET}\n`);
1234
+ console.log(`\n${GREEN} ✓ v${pkg.version} → v${newVer} 업데이트 완료! 도감도 갱신했어요.${RESET}\n`);
1206
1235
  } else {
1207
- console.log(`\n${GREEN} ✓ 이미 최신이에요! (v${pkg.version}) 쪼아요~${RESET}\n`);
1236
+ console.log(`\n${GREEN} ✓ 이미 최신이에요! (v${pkg.version}) 도감 정합성만 다시 맞췄어요.${RESET}\n`);
1208
1237
  }
1209
1238
  } catch (e) {
1210
1239
  console.log(`\n${RED} ✗ 업데이트 실패: ${e.message}${RESET}`);
@@ -1218,10 +1247,12 @@ function runRuntimeSetup({ stdio = 'inherit' } = {}) {
1218
1247
  }
1219
1248
 
1220
1249
  async function cmdSetup() {
1250
+ const context = resolveClaudeContext();
1221
1251
  if (flags.json) {
1222
1252
  try {
1223
1253
  runRuntimeSetup({ stdio: 'pipe' });
1224
- output({ command: 'setup', success: true });
1254
+ resyncCollection(context.userId);
1255
+ output({ command: 'setup', success: true, resynced: true });
1225
1256
  } catch (error) {
1226
1257
  errorJson('SETUP_FAILED', error?.message || String(error));
1227
1258
  }
@@ -1231,12 +1262,95 @@ async function cmdSetup() {
1231
1262
  console.log(`\n${MAGENTA}${BOLD} 쪼아요~! 런타임 셋업 확인할게요.${RESET}\n`);
1232
1263
  try {
1233
1264
  runRuntimeSetup({ stdio: 'inherit' });
1265
+ console.log(`${DIM} 현재 계정 도감 정합성 재검증 중...${RESET}`);
1266
+ resyncCollection(context.userId);
1234
1267
  console.log(`\n${GREEN} ✓ setup complete${RESET}\n`);
1235
1268
  } catch (error) {
1236
1269
  console.log(`\n${RED} ✗ setup failed: ${error?.message || String(error)}${RESET}\n`);
1237
1270
  }
1238
1271
  }
1239
1272
 
1273
+ async function cmdDoctor(subCmd, subArg) {
1274
+ const context = requireClaudeContext();
1275
+ if (!context) return;
1276
+ const { userId, install, currentSalt, installVersion } = context;
1277
+ const profile = loadState(userId);
1278
+
1279
+ if (subCmd === 'dex') {
1280
+ const idx = Math.max(1, parseInt(subArg || '0', 10)) - 1;
1281
+ if (idx < 0 || idx >= SPECIES.length) {
1282
+ if (flags.json) errorJson('INVALID_PICK', `Pick must be 1-${SPECIES.length}`);
1283
+ console.log(`\n${RED} ✗ Pick must be 1-${SPECIES.length}${RESET}\n`);
1284
+ return;
1285
+ }
1286
+
1287
+ const targetSpecies = SPECIES[idx];
1288
+ const collection = getCollection(userId);
1289
+ const entry = collection[targetSpecies] || null;
1290
+ const preferred = entry ? getPreferredVariant(entry, userId) : null;
1291
+ const search = entry ? findDexBuddy({ userId, targetSpecies, entry }) : null;
1292
+
1293
+ const payload = {
1294
+ command: 'doctor',
1295
+ scope: 'dex',
1296
+ account: userId,
1297
+ install: install ? { type: install.type, path: install.path, version: installVersion } : null,
1298
+ current: { salt: currentSalt, buddy: roll(userId, currentSalt).bones },
1299
+ target: targetSpecies,
1300
+ profile: { knownProfiles: listKnownProfiles(), currentProfile: userId },
1301
+ stored: preferred ? mapResult(preferred) : null,
1302
+ apply: search?.found ? mapResult(search.found) : null,
1303
+ criteria: search?.criteria || null,
1304
+ };
1305
+
1306
+ if (flags.json) {
1307
+ output(payload);
1308
+ return;
1309
+ }
1310
+
1311
+ console.log(`\n${BOLD} DEX DOCTOR${RESET}`);
1312
+ console.log(`${DIM} profile: ${formatProfileBadge(userId)} | install: ${installVersion || 'unknown'}${RESET}`);
1313
+ console.log(`${DIM} target: ${targetSpecies}${RESET}\n`);
1314
+ console.log(`${BOLD} Current buddy${RESET}`);
1315
+ console.log(renderCard({ salt: currentSalt, ...roll(userId, currentSalt) }, { showSalt: true }));
1316
+ console.log(`${BOLD} Stored preview${RESET}`);
1317
+ if (preferred) console.log(renderCard(preferred, { showSalt: true }));
1318
+ else console.log(`${DIM} No stored form for this species in the current profile.${RESET}\n`);
1319
+ console.log(`${BOLD} Apply candidate${RESET}`);
1320
+ if (search?.found) console.log(renderCard(search.found, { showSalt: true }));
1321
+ else console.log(`${DIM} No apply candidate found.${RESET}\n`);
1322
+ return;
1323
+ }
1324
+
1325
+ const payload = {
1326
+ command: 'doctor',
1327
+ scope: 'profile',
1328
+ account: userId,
1329
+ install: install ? { type: install.type, path: install.path, version: installVersion } : null,
1330
+ profile: {
1331
+ currentProfile: userId,
1332
+ knownProfiles: listKnownProfiles(),
1333
+ bestRarity: profile.bestRarity,
1334
+ rollsToday: getRollsToday(profile),
1335
+ eventRemaining: getApologyEventRemaining(profile),
1336
+ collectedSpecies: Object.keys(profile.collection || {}).length,
1337
+ },
1338
+ };
1339
+
1340
+ if (flags.json) {
1341
+ output(payload);
1342
+ return;
1343
+ }
1344
+
1345
+ console.log(`\n${BOLD} PROFILE DOCTOR${RESET}`);
1346
+ console.log(`${DIM} profile: ${formatProfileBadge(userId)}${RESET}`);
1347
+ console.log(`${DIM} known profiles: ${listKnownProfiles().join(', ') || '(none)'}${RESET}`);
1348
+ console.log(`${DIM} install: ${installVersion || 'unknown'}${RESET}`);
1349
+ console.log(`${DIM} best rarity: ${profile.bestRarity}${RESET}`);
1350
+ console.log(`${DIM} rolls today: ${getRollsToday(profile)} | event remaining: ${getApologyEventRemaining(profile)}${RESET}`);
1351
+ console.log(`${DIM} collected species: ${Object.keys(profile.collection || {}).length}${RESET}\n`);
1352
+ }
1353
+
1240
1354
  async function promptReturnToHome() {
1241
1355
  const answer = await ask(` ${DIM}Press Enter to go back to home, or type q to quit: ${RESET}`);
1242
1356
  return answer.toLowerCase() !== 'q';
@@ -1250,6 +1364,8 @@ async function cmdHome() {
1250
1364
 
1251
1365
  while (true) {
1252
1366
  console.log(renderHomeScreen());
1367
+ const currentContext = resolveClaudeContext();
1368
+ console.log(`${DIM}Current profile: ${formatProfileBadge(currentContext.userId)} | Known profiles: ${listKnownProfiles().length}${RESET}\n`);
1253
1369
  const choice = await select({
1254
1370
  title: 'Speaki Quick Actions',
1255
1371
  columns: 2,
@@ -1317,6 +1433,7 @@ function showHelp() {
1317
1433
  console.log(`${BOLD}Setup${RESET}`);
1318
1434
  console.log(` ${GREEN}bdy setup${RESET} install Bun/runtime hook support if missing`);
1319
1435
  console.log(` ${GREEN}bdy update${RESET} update package and rerun runtime setup`);
1436
+ console.log(` ${GREEN}bdy doctor${RESET} inspect current profile or dex apply candidate`);
1320
1437
  console.log();
1321
1438
  console.log(`${DIM}Daily quota: ${BASE_LIMIT} (+1 with GitHub star) | Event bonus: ${APOLOGY_EVENT.pullsPerRun}-pull x${APOLOGY_EVENT.bonusRuns}${RESET}\n`);
1322
1439
  }
@@ -1330,6 +1447,7 @@ switch (cmd) {
1330
1447
  case 'restore': await cmdRestore(); break;
1331
1448
  case 'dex': await cmdDex(); break;
1332
1449
  case 'setup': await cmdSetup(); break;
1450
+ case 'doctor': await cmdDoctor(args[0], args[1]); break;
1333
1451
  case 'schema': cmdSchema(args[0]); break;
1334
1452
  case 'update': await cmdUpdate(); break;
1335
1453
  case '--help': case '-h': case 'help': showHelp(); break;
@@ -187,6 +187,13 @@ export function getCollection(ownerId = null) {
187
187
  return ingestCollectionResults(profile.collection || {}, [], ownerId);
188
188
  }
189
189
 
190
+ export function resyncCollection(ownerId = null) {
191
+ const profile = loadProfileState(ownerId || 'anon');
192
+ profile.collection = ingestCollectionResults(profile.collection || {}, [], ownerId);
193
+ saveProfileState(ownerId || 'anon', profile);
194
+ return profile.collection;
195
+ }
196
+
190
197
  export function getCollectionStats(ownerId = null) {
191
198
  const col = getCollection(ownerId);
192
199
  const collected = Object.keys(col).length;
package/src/display.mjs CHANGED
@@ -79,12 +79,13 @@ export function renderMiniCard(result, index) {
79
79
  const { rarity, species, eye, shiny } = bones;
80
80
  const style = RARITY_STYLE[rarity];
81
81
  const stars = formatStars(RARITY_STARS[rarity]);
82
+ const badge = style.badge.padEnd(9);
82
83
  const shinyTag = shiny
83
84
  ? (isAsciiOnlyTerminal() ? '[SHINY]' : formatShinyTag('✨'))
84
85
  : '';
85
86
 
86
87
  const idx = String(index + 1).padStart(2, ' ');
87
- return `${style.color}${idx}. ${formatEye(eye)} ${species.padEnd(10)} ${stars.padEnd(5)} ${shinyTag.padEnd(8)}${RESET}`;
88
+ return `${style.color}${idx}. ${formatEye(eye)} ${species.padEnd(10)} ${stars.padEnd(5)} ${badge} ${shinyTag.padEnd(8)}${RESET}`;
88
89
  }
89
90
 
90
91
  // Dex rendering is handled directly in cli.mjs
@@ -25,6 +25,11 @@ function readRootState() {
25
25
  return {};
26
26
  }
27
27
 
28
+ export function listKnownProfiles() {
29
+ const root = readRootState();
30
+ return Object.keys(root.profiles || {}).sort();
31
+ }
32
+
28
33
  function migrateLegacyIntoProfile(root, userId) {
29
34
  const hasLegacy = LEGACY_KEYS.some((key) => key in root);
30
35
  if (!hasLegacy) return;
package/src/ui.mjs CHANGED
@@ -9,6 +9,15 @@ const MAGENTA = '\x1b[35m';
9
9
  const YELLOW = '\x1b[33m';
10
10
  const RED = '\x1b[31m';
11
11
  const WHITE = '\x1b[37m';
12
+ const GRAY = '\x1b[90m';
13
+
14
+ const RARITY_TONE = {
15
+ common: WHITE,
16
+ uncommon: GREEN,
17
+ rare: CYAN,
18
+ epic: MAGENTA,
19
+ legendary: '\x1b[33;1m',
20
+ };
12
21
 
13
22
  export function renderBanner(title = 'SPEAKI BUDDY LAB', subtitle = 'npm-only buddy reroll terminal') {
14
23
  const border = '═'.repeat(54);
@@ -24,11 +33,14 @@ export function renderBanner(title = 'SPEAKI BUDDY LAB', subtitle = 'npm-only bu
24
33
  }
25
34
 
26
35
  export function renderPanel(title, lines = [], tone = CYAN) {
27
- const content = lines.map((line) => ` ${line}`);
36
+ const width = Math.max(title.length + 6, ...lines.map((line) => String(line).length), 28);
37
+ const content = lines.map((line) => ` ${tone}│${RESET} ${String(line).padEnd(width - 4)} ${tone}│${RESET}`);
28
38
  return [
29
- `${tone}${BOLD}${title}${RESET}`,
30
- `${tone}${''.repeat(Math.max(18, title.length + 6))}${RESET}`,
39
+ `${tone}┌${'─'.repeat(width - 2)}┐${RESET}`,
40
+ `${tone}│${RESET} ${tone}${BOLD}${title}${RESET}${' '.repeat(Math.max(0, width - title.length - 4))}${tone}│${RESET}`,
41
+ `${tone}├${'─'.repeat(width - 2)}┤${RESET}`,
31
42
  ...content,
43
+ `${tone}└${'─'.repeat(width - 2)}┘${RESET}`,
32
44
  '',
33
45
  ].join('\n');
34
46
  }
@@ -45,7 +57,8 @@ export function renderQuotaSummary({ used = 0, limit = 0, starred = false, event
45
57
  export function renderRarityOverview() {
46
58
  return RARITIES.map((rarity) => {
47
59
  const pct = `${RARITY_WEIGHTS[rarity]}%`.padStart(3);
48
- return `${WHITE}${RARITY_STARS[rarity].padEnd(6)}${RESET} ${rarity.padEnd(10)} ${DIM}${pct}${RESET}`;
60
+ const color = RARITY_TONE[rarity] || WHITE;
61
+ return `${color}${RARITY_STARS[rarity].padEnd(6)}${RESET} ${color}${rarity.padEnd(10)}${RESET} ${GRAY}${pct}${RESET}`;
49
62
  });
50
63
  }
51
64