@rhseung/ps-cli 1.8.0 → 1.9.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.
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- source_default
4
- } from "../chunk-ASMT3CRD.js";
5
- import {
3
+ getUserProblemStats,
6
4
  getUserStats,
7
- getUserTop100
8
- } from "../chunk-QB2R47PW.js";
9
- import {
10
- defineFlags
11
- } from "../chunk-PY6GW22W.js";
5
+ getUserTagRatings,
6
+ getUserTop100,
7
+ scrapeUserStats
8
+ } from "../chunk-LR64BN3N.js";
12
9
  import {
13
10
  Command,
14
11
  CommandBuilder,
15
12
  CommandDef,
13
+ TIER_COLORS,
14
+ __decorateClass,
16
15
  calculateTierProgress,
16
+ defineFlags,
17
17
  findProjectRoot,
18
18
  getArchiveDir,
19
19
  getNextTierMinRating,
@@ -21,16 +21,16 @@ import {
21
21
  getSolvingDir,
22
22
  getTierColor,
23
23
  getTierName,
24
- getTierShortName
25
- } from "../chunk-JPDN34C7.js";
26
- import {
27
- __decorateClass
28
- } from "../chunk-7MQMPJ3X.js";
24
+ getTierShortName,
25
+ icons,
26
+ logger,
27
+ source_default
28
+ } from "../chunk-Q5NECGFA.js";
29
29
 
30
30
  // src/commands/stats.tsx
31
- import { Alert } from "@inkjs/ui";
32
- import { Spinner } from "@inkjs/ui";
33
- import { Box, Text, Transform } from "ink";
31
+ import { Alert, Spinner } from "@inkjs/ui";
32
+ import { BarChart, StackedBarChart } from "@pppp606/ink-chart";
33
+ import { Box, Text } from "ink";
34
34
 
35
35
  // src/hooks/use-user-stats.ts
36
36
  import { existsSync } from "fs";
@@ -68,17 +68,48 @@ function useUserStats({
68
68
  );
69
69
  const [user, setUser] = useState(null);
70
70
  const [top100, setTop100] = useState(null);
71
+ const [problemStats, setProblemStats] = useState(null);
72
+ const [tagRatings, setTagRatings] = useState(
73
+ null
74
+ );
75
+ const [bojStats, setBojStats] = useState(null);
71
76
  const [localSolvedCount, setLocalSolvedCount] = useState(null);
72
77
  const [error, setError] = useState(null);
73
78
  useEffect(() => {
74
79
  async function fetchData() {
75
80
  try {
76
- const [userData, top100Data] = await Promise.all([
77
- getUserStats(handle),
78
- getUserTop100(handle)
79
- ]);
81
+ const userData = await getUserStats(handle).catch((err) => {
82
+ if (err instanceof Error && err.message.includes("404")) {
83
+ throw new Error(`\uC0AC\uC6A9\uC790 '${handle}'\uC744(\uB97C) \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
84
+ }
85
+ throw err;
86
+ });
87
+ if (!userData) {
88
+ throw new Error(`\uC0AC\uC6A9\uC790 '${handle}'\uC744(\uB97C) \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
89
+ }
80
90
  setUser(userData);
91
+ const [top100Data, problemStatsData, tagRatingsData, bojStatsData] = await Promise.all([
92
+ getUserTop100(handle).catch((err) => {
93
+ console.error("Error fetching top 100:", err);
94
+ return null;
95
+ }),
96
+ getUserProblemStats(handle).catch((err) => {
97
+ console.error("Error fetching problem stats:", err);
98
+ return null;
99
+ }),
100
+ getUserTagRatings(handle).catch((err) => {
101
+ console.error("Error fetching tag ratings:", err);
102
+ return null;
103
+ }),
104
+ scrapeUserStats(handle).catch((err) => {
105
+ console.error("Error scraping BOJ stats:", err);
106
+ return null;
107
+ })
108
+ ]);
81
109
  setTop100(top100Data);
110
+ setProblemStats(problemStatsData);
111
+ setTagRatings(tagRatingsData);
112
+ setBojStats(bojStatsData);
82
113
  if (fetchLocalCount) {
83
114
  const projectRoot = findProjectRoot();
84
115
  if (projectRoot) {
@@ -106,11 +137,14 @@ function useUserStats({
106
137
  }
107
138
  }
108
139
  void fetchData();
109
- }, [handle, onComplete]);
140
+ }, [fetchLocalCount, handle, onComplete]);
110
141
  return {
111
142
  status,
112
143
  user,
113
144
  top100,
145
+ problemStats,
146
+ tagRatings,
147
+ bojStats,
114
148
  localSolvedCount,
115
149
  error
116
150
  };
@@ -125,27 +159,26 @@ var statsFlagsSchema = {
125
159
  description: "Solved.ac \uD578\uB4E4 (\uC124\uC815\uC5D0 \uC800\uC7A5\uB41C \uAC12 \uC0AC\uC6A9 \uAC00\uB2A5)"
126
160
  }
127
161
  };
128
- function ProgressBarWithColor({ value, colorFn }) {
129
- const width = process.stdout.columns || 40;
130
- const barWidth = Math.max(10, Math.min(30, width - 20));
131
- const filled = Math.round(value / 100 * barWidth);
132
- const empty = barWidth - filled;
133
- const filledBar = "\u2588".repeat(filled);
134
- const emptyBar = "\u2591".repeat(empty);
135
- const barText = filledBar + emptyBar;
136
- return /* @__PURE__ */ jsx(Transform, { transform: (output) => colorFn(output), children: /* @__PURE__ */ jsx(Text, { children: barText }) });
137
- }
138
162
  function StatsView({ handle, onComplete, showLocalStats }) {
139
- const { status, user, top100, localSolvedCount, error } = useUserStats({
163
+ const {
164
+ status,
165
+ user,
166
+ top100,
167
+ problemStats,
168
+ tagRatings,
169
+ bojStats,
170
+ localSolvedCount,
171
+ error
172
+ } = useUserStats({
140
173
  handle,
141
174
  onComplete,
142
175
  fetchLocalCount: showLocalStats
143
176
  });
144
177
  if (status === "loading") {
145
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Spinner, { label: "\uD1B5\uACC4\uB97C \uBD88\uB7EC\uC624\uB294 \uC911..." }) });
178
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingY: 1, children: /* @__PURE__ */ jsx(Spinner, { label: "\uD1B5\uACC4\uB97C \uBD88\uB7EC\uC624\uB294 \uC911..." }) });
146
179
  }
147
180
  if (status === "error") {
148
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Alert, { variant: "error", children: [
181
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingY: 1, children: /* @__PURE__ */ jsxs(Alert, { variant: "error", children: [
149
182
  "\uD1B5\uACC4\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ",
150
183
  error
151
184
  ] }) });
@@ -156,68 +189,206 @@ function StatsView({ handle, onComplete, showLocalStats }) {
156
189
  const tierColorFn = typeof tierColor === "string" ? source_default.hex(tierColor) : tierColor.multiline;
157
190
  const nextTierMin = getNextTierMinRating(user.tier);
158
191
  const progress = user.tier === 31 ? 100 : calculateTierProgress(user.rating, user.tier);
159
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
160
- /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
161
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
162
- "\u2728 ",
163
- user.handle
164
- ] }),
165
- /* @__PURE__ */ jsxs(Text, { color: "blue", underline: true, children: [
166
- "https://solved.ac/profile/",
167
- user.handle
168
- ] })
169
- ] }),
170
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, flexDirection: "row", gap: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
171
- tierColorFn(tierName),
172
- " ",
173
- /* @__PURE__ */ jsx(Text, { bold: true, children: tierColorFn(user.rating.toLocaleString()) }),
174
- nextTierMin !== null && /* @__PURE__ */ jsx(Text, { bold: true, children: " / " + nextTierMin.toLocaleString() })
175
- ] }) }),
176
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx(ProgressBarWithColor, { value: progress, colorFn: tierColorFn }) }),
192
+ const tierDistData = problemStats ? [
193
+ {
194
+ label: "Bronze",
195
+ value: problemStats.filter((s) => s.level >= 1 && s.level <= 5).reduce((a, b) => a + b.solved, 0),
196
+ color: TIER_COLORS[3]
197
+ // Bronze III
198
+ },
199
+ {
200
+ label: "Silver",
201
+ value: problemStats.filter((s) => s.level >= 6 && s.level <= 10).reduce((a, b) => a + b.solved, 0),
202
+ color: TIER_COLORS[8]
203
+ // Silver III
204
+ },
205
+ {
206
+ label: "Gold",
207
+ value: problemStats.filter((s) => s.level >= 11 && s.level <= 15).reduce((a, b) => a + b.solved, 0),
208
+ color: TIER_COLORS[13]
209
+ // Gold III
210
+ },
211
+ {
212
+ label: "Platinum",
213
+ value: problemStats.filter((s) => s.level >= 16 && s.level <= 20).reduce((a, b) => a + b.solved, 0),
214
+ color: TIER_COLORS[18]
215
+ // Platinum III
216
+ },
217
+ {
218
+ label: "Diamond",
219
+ value: problemStats.filter((s) => s.level >= 21 && s.level <= 25).reduce((a, b) => a + b.solved, 0),
220
+ color: TIER_COLORS[23]
221
+ // Diamond III
222
+ },
223
+ {
224
+ label: "Ruby",
225
+ value: problemStats.filter((s) => s.level >= 26 && s.level <= 30).reduce((a, b) => a + b.solved, 0),
226
+ color: TIER_COLORS[28]
227
+ // Ruby III
228
+ },
229
+ {
230
+ label: "Master",
231
+ value: problemStats.filter((s) => s.level === 31).reduce((a, b) => a + b.solved, 0),
232
+ color: TIER_COLORS[31]
233
+ // Master
234
+ }
235
+ ].filter((d) => d.value > 0) : [];
236
+ const tagChartData = tagRatings ? tagRatings.sort((a, b) => b.rating - a.rating).slice(0, 8).map((tr) => ({
237
+ label: tr.tag.displayNames.find((dn) => dn.language === "ko")?.name || tr.tag.key,
238
+ value: tr.rating
239
+ })) : [];
240
+ const bojSummaryData = bojStats ? [
241
+ { label: "\uC815\uB2F5", value: bojStats.accepted, color: "green" },
242
+ { label: "\uC624\uB2F5", value: bojStats.wrong, color: "red" },
243
+ {
244
+ label: "TLE/MLE",
245
+ value: bojStats.timeout + bojStats.memory,
246
+ color: "yellow"
247
+ },
248
+ {
249
+ label: "\uAE30\uD0C0",
250
+ value: bojStats.runtimeError + bojStats.compileError,
251
+ color: "gray"
252
+ }
253
+ ].filter((d) => d.value > 0) : [];
254
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
177
255
  /* @__PURE__ */ jsx(
256
+ Box,
257
+ {
258
+ flexDirection: "row",
259
+ justifyContent: "space-between",
260
+ alignItems: "flex-end",
261
+ children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
262
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
263
+ icons.user,
264
+ " ",
265
+ source_default.cyan(user.handle)
266
+ ] }),
267
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
268
+ "https://solved.ac/profile/",
269
+ user.handle
270
+ ] })
271
+ ] })
272
+ }
273
+ ),
274
+ /* @__PURE__ */ jsxs(
178
275
  Box,
179
276
  {
180
277
  flexDirection: "column",
181
278
  borderStyle: "round",
182
279
  borderColor: "gray",
183
- alignSelf: "flex-start",
184
- children: /* @__PURE__ */ jsxs(Box, { paddingX: 1, paddingY: 0, flexDirection: "column", children: [
280
+ paddingX: 1,
281
+ children: [
282
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [
283
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
284
+ /* @__PURE__ */ jsx(Text, { bold: true, children: tierColorFn(tierName) }),
285
+ /* @__PURE__ */ jsx(Text, { bold: true, children: tierColorFn(user.rating.toLocaleString()) }),
286
+ nextTierMin !== null && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
287
+ " / ",
288
+ nextTierMin.toLocaleString()
289
+ ] })
290
+ ] }),
291
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
292
+ icons.trophy,
293
+ " Rank #",
294
+ user.rank.toLocaleString()
295
+ ] })
296
+ ] }),
297
+ /* @__PURE__ */ jsx(
298
+ StackedBarChart,
299
+ {
300
+ data: [
301
+ {
302
+ label: "Progress",
303
+ value: progress,
304
+ color: typeof tierColor === "string" ? tierColor : "#ff7ca8"
305
+ },
306
+ ...progress < 100 ? [{ label: "Remaining", value: 100 - progress, color: "#333" }] : []
307
+ ],
308
+ showLabels: false,
309
+ showValues: false,
310
+ width: "full"
311
+ }
312
+ )
313
+ ]
314
+ }
315
+ ),
316
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
317
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
318
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[ \uC0C1\uC138 \uD1B5\uACC4 ]" }) }),
319
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 0, children: [
185
320
  /* @__PURE__ */ jsxs(Text, { children: [
186
321
  "\uD574\uACB0\uD55C \uBB38\uC81C:",
187
322
  " ",
188
323
  /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: user.solvedCount.toLocaleString() }),
189
- "\uAC1C",
190
- localSolvedCount !== null && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
191
- " (\uB85C\uCEEC: ",
192
- localSolvedCount,
193
- "\uAC1C)"
194
- ] })
324
+ " ",
325
+ "\uAC1C"
326
+ ] }),
327
+ localSolvedCount !== null && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
328
+ " ",
329
+ "\u2514 \uB85C\uCEEC \uAD00\uB9AC: ",
330
+ /* @__PURE__ */ jsx(Text, { bold: true, children: localSolvedCount }),
331
+ " \uAC1C"
195
332
  ] }),
196
333
  /* @__PURE__ */ jsxs(Text, { children: [
197
- "\uD074\uB798\uC2A4: ",
198
- /* @__PURE__ */ jsx(Text, { bold: true, children: user.class })
334
+ "\uD074\uB798\uC2A4:",
335
+ " ",
336
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "magenta", children: [
337
+ user.class,
338
+ user.classDecoration === "gold" ? "++" : user.classDecoration === "silver" ? "+" : ""
339
+ ] })
199
340
  ] }),
200
- user.maxStreak > 0 && /* @__PURE__ */ jsxs(Text, { children: [
201
- "\uCD5C\uB300 \uC5F0\uC18D \uD574\uACB0:",
341
+ /* @__PURE__ */ jsxs(Text, { children: [
342
+ "\uCD5C\uB300 \uC2A4\uD2B8\uB9AD:",
343
+ " ",
344
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "orange", children: user.maxStreak }),
202
345
  " ",
203
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: user.maxStreak }),
204
346
  "\uC77C"
205
347
  ] }),
206
348
  /* @__PURE__ */ jsxs(Text, { children: [
207
- "\uC21C\uC704: ",
208
- /* @__PURE__ */ jsx(Text, { bold: true, children: user.rank.toLocaleString() }),
209
- "\uC704"
349
+ "\uAE30\uC5EC \uD69F\uC218:",
350
+ " ",
351
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "blue", children: user.voteCount }),
352
+ " ",
353
+ "\uD68C"
210
354
  ] })
355
+ ] }),
356
+ bojSummaryData.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
357
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[ \uBC31\uC900 \uC81C\uCD9C \uC694\uC57D ]" }) }),
358
+ /* @__PURE__ */ jsx(
359
+ StackedBarChart,
360
+ {
361
+ data: bojSummaryData,
362
+ mode: "absolute",
363
+ width: 30
364
+ }
365
+ )
211
366
  ] })
212
- }
213
- ),
214
- top100 && top100.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
215
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u{1F3C6} \uC0C1\uC704 100\uBB38\uC81C \uD2F0\uC5B4 \uBD84\uD3EC" }) }),
216
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: Array.from({ length: Math.ceil(top100.length / 10) }).map(
217
- (_, rowIndex) => /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: top100.slice(rowIndex * 10, (rowIndex + 1) * 10).map((p, colIndex) => {
367
+ ] }),
368
+ tagChartData.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: 40, children: [
369
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[ \uC54C\uACE0\uB9AC\uC998 \uAC15\uC810 (Tag Rating) ]" }) }),
370
+ /* @__PURE__ */ jsx(
371
+ BarChart,
372
+ {
373
+ data: tagChartData,
374
+ showValue: "right",
375
+ barChar: "\u2588",
376
+ color: "cyan"
377
+ }
378
+ )
379
+ ] })
380
+ ] }),
381
+ tierDistData.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
382
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[ \uD2F0\uC5B4\uBCC4 \uD574\uACB0 \uBB38\uC81C \uBD84\uD3EC ]" }) }),
383
+ /* @__PURE__ */ jsx(StackedBarChart, { data: tierDistData, width: "full" })
384
+ ] }),
385
+ top100 && top100.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
386
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[ \uC0C1\uC704 100\uBB38\uC81C \uBD84\uD3EC ]" }) }),
387
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: Array.from({ length: Math.ceil(top100.length / 20) }).map(
388
+ (_, rowIndex) => /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: top100.slice(rowIndex * 20, (rowIndex + 1) * 20).map((p, colIndex) => {
218
389
  const tierColor2 = getTierColor(p.level);
219
390
  const tierColorFn2 = typeof tierColor2 === "string" ? source_default.hex(tierColor2) : tierColor2.multiline;
220
- return /* @__PURE__ */ jsx(Box, { width: 4, children: /* @__PURE__ */ jsx(Text, { children: tierColorFn2(getTierShortName(p.level)) }) }, colIndex);
391
+ return /* @__PURE__ */ jsx(Box, { width: 3, children: /* @__PURE__ */ jsx(Text, { children: tierColorFn2(getTierShortName(p.level)) }) }, colIndex);
221
392
  }) }, rowIndex)
222
393
  ) })
223
394
  ] })
@@ -232,12 +403,8 @@ var StatsCommand = class extends Command {
232
403
  handle = getSolvedAcHandle();
233
404
  }
234
405
  if (!handle) {
235
- console.error("\uC624\uB958: Solved.ac \uD578\uB4E4\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694.");
236
- console.error(`\uC0AC\uC6A9\uBC95: ps stats <\uD578\uB4E4>`);
237
- console.error(`\uB3C4\uC6C0\uB9D0: ps stats --help`);
238
- console.error(
239
- `\uD78C\uD2B8: \uC124\uC815\uC5D0 \uD578\uB4E4\uC744 \uC800\uC7A5\uD558\uBA74 \uB9E4\uBC88 \uC785\uB825\uD560 \uD544\uC694\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`
240
- );
406
+ logger.error("Solved.ac \uD578\uB4E4\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694.");
407
+ console.log(`\uB3C4\uC6C0\uB9D0: ps stats --help`);
241
408
  process.exit(1);
242
409
  return;
243
410
  }
@@ -253,7 +420,7 @@ StatsCommand = __decorateClass([
253
420
  name: "stats",
254
421
  description: `Solved.ac\uC5D0\uC11C \uC0AC\uC6A9\uC790 \uD1B5\uACC4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.
255
422
  - \uD2F0\uC5B4, \uB808\uC774\uD305, \uD574\uACB0\uD55C \uBB38\uC81C \uC218 \uB4F1 \uD45C\uC2DC
256
- - \uADF8\uB77C\uB370\uC774\uC158\uC73C\uB85C \uC2DC\uAC01\uC801\uC73C\uB85C \uD45C\uC2DC`,
423
+ - \uAE30\uBCF8 Solved.ac \uD578\uB4E4\uC740 ps config\uC5D0\uC11C \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.`,
257
424
  flags: defineFlags(statsFlagsSchema),
258
425
  autoDetectProblemId: false,
259
426
  examples: ["stats myhandle", "stats --handle myhandle"]