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 +3 -0
- package/package.json +1 -1
- package/src/cli.mjs +142 -24
- package/src/collection.mjs +7 -0
- package/src/display.mjs +2 -1
- package/src/profile-state.mjs +5 -0
- package/src/ui.mjs +17 -4
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
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
|
|
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
|
|
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', ['
|
|
387
|
-
...panel('FORMS', [
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
400
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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',
|
|
433
|
-
...panel('FORMS',
|
|
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} 업데이트 완료!
|
|
1234
|
+
console.log(`\n${GREEN} ✓ v${pkg.version} → v${newVer} 업데이트 완료! 도감도 갱신했어요.${RESET}\n`);
|
|
1206
1235
|
} else {
|
|
1207
|
-
console.log(`\n${GREEN} ✓ 이미 최신이에요! (v${pkg.version})
|
|
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
|
-
|
|
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;
|
package/src/collection.mjs
CHANGED
|
@@ -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
|
package/src/profile-state.mjs
CHANGED
|
@@ -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
|
|
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}
|
|
30
|
-
`${tone}${'
|
|
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
|
-
|
|
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
|
|