@rhseung/ps-cli 1.7.5 → 1.8.0

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
@@ -188,7 +188,7 @@ ps submit --language python # Python으로 제출
188
188
 
189
189
  ### `archive` - 아카이빙
190
190
 
191
- solving 디렉토리의 문제를 archive 디렉토리로 이동하고 Git 커밋을 생성합니다.
191
+ solving 디렉토리의 문제를 archive 디렉토리로 이동하고 (선택적으로) Git 커밋을 생성합니다.
192
192
 
193
193
  **사용법:**
194
194
 
@@ -206,31 +206,42 @@ ps archive # 현재 디렉토리에서 문제 번호 자동
206
206
  **설명:**
207
207
 
208
208
  - solving 디렉토리에서 문제를 찾아 archive 디렉토리로 이동
209
- - Git add 및 commit 실행
210
- - 커밋 메시지: "solve: {문제번호} - {문제이름}"
209
+ - 설정에 따라 Git add 및 commit 실행
210
+ - 기본 커밋 메시지: "solve: {id} - {title}"
211
+ - `archive-auto-commit` 이 `true` 인 경우, **archive 디렉토리로 이동 후** Git 커밋을 시도하며, 커밋 실패 시 다시 원래 위치로 되돌림 (롤백)
212
+ - `archive-auto-commit` 이 `false` 인 경우, Git 커밋 없이 디렉토리만 이동
211
213
 
212
214
  ---
213
215
 
214
216
  ### `open` - 문제 페이지 열기
215
217
 
216
- 백준 문제 페이지를 브라우저로 엽니다.
218
+ 백준 문제 페이지 또는 문제집 페이지를 브라우저로 엽니다.
217
219
 
218
220
  **사용법:**
219
221
 
220
222
  ```bash
221
223
  ps open [문제번호]
224
+ ps open --workbook <문제집ID>
225
+ ps open -w <문제집ID>
222
226
  ```
223
227
 
228
+ **옵션:**
229
+
230
+ - `--workbook`, `-w`: 문제집 ID를 지정하여 해당 문제집 페이지를 엽니다
231
+
224
232
  **예제:**
225
233
 
226
234
  ```bash
227
235
  ps open 1000 # 1000번 문제 열기
228
236
  ps open # 문제 디렉토리에서 실행 시 자동 추론
237
+ ps open --workbook 25052 # 문제집 25052 열기
238
+ ps open -w 25052 # 문제집 25052 열기 (단축 옵션)
229
239
  ```
230
240
 
231
241
  **설명:**
232
242
 
233
243
  - 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
244
+ - `--workbook` 또는 `-w` 옵션으로 문제집 페이지를 열 수 있습니다
234
245
 
235
246
  ---
236
247
 
@@ -347,6 +358,8 @@ ps config clear # .ps-cli.json 파일 삭제
347
358
  - `archive-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
348
359
  - `solving-dir`: 푸는 중인 문제 디렉토리 (기본값: solving)
349
360
  - `archive-strategy`: 아카이빙 전략
361
+ - `archive-auto-commit`: archive 실행 시 Git 커밋 자동 실행 여부 (true/false, 기본값: true)
362
+ - `archive-commit-message`: archive 시 사용할 Git 커밋 메시지 템플릿 (`{id}`, `{title}` 사용 가능, 기본값: `solve: {id} - {title}`)
350
363
 
351
364
  ### 아카이빙 전략
352
365
 
@@ -416,7 +429,7 @@ node dist/index.js init
416
429
  ```bash
417
430
  # 프로젝트 디렉토리에서
418
431
  bun run build
419
- npm link
432
+ bun link
420
433
 
421
434
  # 외부 폴더에서 테스트
422
435
  cd /path/to/test-project
@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
8
8
  var BOJ_BASE_URL = "https://www.acmicpc.net";
9
9
  function useOpenBrowser({
10
10
  problemId,
11
+ workbookId,
11
12
  onComplete
12
13
  }) {
13
14
  const [status, setStatus] = useState(
@@ -18,9 +19,16 @@ function useOpenBrowser({
18
19
  useEffect(() => {
19
20
  async function handleOpenBrowser() {
20
21
  try {
21
- const problemUrl = `${BOJ_BASE_URL}/problem/${problemId}`;
22
- setUrl(problemUrl);
23
- await openBrowser(problemUrl);
22
+ let targetUrl;
23
+ if (workbookId !== void 0) {
24
+ targetUrl = `${BOJ_BASE_URL}/workbook/view/${workbookId}`;
25
+ } else if (problemId !== void 0) {
26
+ targetUrl = `${BOJ_BASE_URL}/problem/${problemId}`;
27
+ } else {
28
+ throw new Error("\uBB38\uC81C \uBC88\uD638 \uB610\uB294 \uBB38\uC81C\uC9D1 ID\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
29
+ }
30
+ setUrl(targetUrl);
31
+ await openBrowser(targetUrl);
24
32
  setStatus("success");
25
33
  setTimeout(() => {
26
34
  onComplete?.();
@@ -34,7 +42,7 @@ function useOpenBrowser({
34
42
  }
35
43
  }
36
44
  void handleOpenBrowser();
37
- }, [problemId, onComplete]);
45
+ }, [problemId, workbookId, onComplete]);
38
46
  return {
39
47
  status,
40
48
  error,
@@ -9926,8 +9926,9 @@ var config = new Conf({
9926
9926
  solvedAcHandle: void 0,
9927
9927
  archiveDir: "problems",
9928
9928
  // 기본값: problems 디렉토리
9929
- solvingDir: "solving"
9929
+ solvingDir: "solving",
9930
9930
  // 기본값: solving 디렉토리
9931
+ archiveAutoCommit: true
9931
9932
  }
9932
9933
  });
9933
9934
  var projectConfigCache = null;
@@ -10030,6 +10031,24 @@ function getArchiveStrategy() {
10030
10031
  }
10031
10032
  return config.get("archiveStrategy") ?? "flat";
10032
10033
  }
10034
+ function getArchiveAutoCommit() {
10035
+ const projectConfig = getProjectConfigSync();
10036
+ if (projectConfig?.archiveAutoCommit !== void 0) {
10037
+ return projectConfig.archiveAutoCommit;
10038
+ }
10039
+ const globalValue = config.get("archiveAutoCommit");
10040
+ if (globalValue !== void 0) {
10041
+ return globalValue;
10042
+ }
10043
+ return true;
10044
+ }
10045
+ function getArchiveCommitMessage() {
10046
+ const projectConfig = getProjectConfigSync();
10047
+ if (projectConfig?.archiveCommitMessage !== void 0) {
10048
+ return projectConfig.archiveCommitMessage;
10049
+ }
10050
+ return config.get("archiveCommitMessage");
10051
+ }
10033
10052
 
10034
10053
  // src/utils/tier.ts
10035
10054
  import gradient from "gradient-string";
@@ -10067,6 +10086,40 @@ var TIER_NAMES = [
10067
10086
  "Ruby I",
10068
10087
  "Master"
10069
10088
  ];
10089
+ var TIER_SHORT_NAMES = [
10090
+ void 0,
10091
+ "B5",
10092
+ "B4",
10093
+ "B3",
10094
+ "B2",
10095
+ "B1",
10096
+ "S5",
10097
+ "S4",
10098
+ "S3",
10099
+ "S2",
10100
+ "S1",
10101
+ "G5",
10102
+ "G4",
10103
+ "G3",
10104
+ "G2",
10105
+ "G1",
10106
+ "P5",
10107
+ "P4",
10108
+ "P3",
10109
+ "P2",
10110
+ "P1",
10111
+ "D5",
10112
+ "D4",
10113
+ "D3",
10114
+ "D2",
10115
+ "D1",
10116
+ "R5",
10117
+ "R4",
10118
+ "R3",
10119
+ "R2",
10120
+ "R1",
10121
+ "M"
10122
+ ];
10070
10123
  var TIER_COLORS = [
10071
10124
  void 0,
10072
10125
  "#9d4900",
@@ -10213,6 +10266,13 @@ function getTierName(level) {
10213
10266
  }
10214
10267
  return "Unrated";
10215
10268
  }
10269
+ function getTierShortName(level) {
10270
+ if (level === 0) return "UR";
10271
+ if (level >= 1 && level < TIER_SHORT_NAMES.length) {
10272
+ return TIER_SHORT_NAMES[level] || "UR";
10273
+ }
10274
+ return "UR";
10275
+ }
10216
10276
  function getTierColor(level) {
10217
10277
  if (level === 0) return "#2d2d2d";
10218
10278
  if (level === 31) {
@@ -10248,7 +10308,7 @@ function getArchiveSubPath(problemId, strategy = "flat", problem) {
10248
10308
  return String(range).padStart(5, "0");
10249
10309
  }
10250
10310
  case "by-tier": {
10251
- if (!problem) {
10311
+ if (!problem || problem.level === void 0) {
10252
10312
  return "";
10253
10313
  }
10254
10314
  return getTierDirName(problem.level);
@@ -10508,7 +10568,8 @@ var CommandBuilder = class {
10508
10568
  return {
10509
10569
  name: finalMetadata.name,
10510
10570
  help,
10511
- execute: wrappedExecute
10571
+ execute: wrappedExecute,
10572
+ metadata: finalMetadata
10512
10573
  };
10513
10574
  }
10514
10575
  /**
@@ -10597,9 +10658,12 @@ export {
10597
10658
  getArchiveDir,
10598
10659
  getSolvingDir,
10599
10660
  getArchiveStrategy,
10661
+ getArchiveAutoCommit,
10662
+ getArchiveCommitMessage,
10600
10663
  getNextTierMinRating,
10601
10664
  calculateTierProgress,
10602
10665
  getTierName,
10666
+ getTierShortName,
10603
10667
  getTierColor,
10604
10668
  getTierImageUrl,
10605
10669
  detectProblemIdFromPath,
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/types/command.ts
4
+ function defineFlags(schema) {
5
+ return Object.entries(schema).map(([name, options]) => ({
6
+ name,
7
+ options
8
+ }));
9
+ }
10
+
11
+ export {
12
+ defineFlags
13
+ };
@@ -47,8 +47,15 @@ async function getUserStats(handle) {
47
47
  const data = await response.json();
48
48
  return data;
49
49
  }
50
+ async function getUserTop100(handle) {
51
+ const url = `${BASE_URL}/user/top_100?handle=${handle}`;
52
+ const response = await fetchWithRetry(url);
53
+ const data = await response.json();
54
+ return data.items || [];
55
+ }
50
56
 
51
57
  export {
52
58
  getProblem,
53
- getUserStats
59
+ getUserStats,
60
+ getUserTop100
54
61
  };
@@ -4,11 +4,13 @@ import {
4
4
  CommandBuilder,
5
5
  CommandDef,
6
6
  findProjectRoot,
7
+ getArchiveAutoCommit,
8
+ getArchiveCommitMessage,
7
9
  getArchiveDirPath,
8
10
  getSolvingDir,
9
11
  getSolvingDirPath,
10
12
  resolveProblemContext
11
- } from "../chunk-F4LZ6ENP.js";
13
+ } from "../chunk-JPDN34C7.js";
12
14
  import {
13
15
  __decorateClass
14
16
  } from "../chunk-7MQMPJ3X.js";
@@ -82,11 +84,33 @@ function useArchive({
82
84
  throw err;
83
85
  }
84
86
  }
87
+ const autoCommit = getArchiveAutoCommit();
88
+ const template = getArchiveCommitMessage() ?? "solve: {id} - {title}";
89
+ const commitMessage = template.replace("{id}", String(problemId)).replace("{title}", problemTitle);
85
90
  const archiveDirParent = dirname(archiveDir);
86
91
  setMessage("\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC\uB97C \uC900\uBE44\uD558\uB294 \uC911...");
87
92
  await mkdir(archiveDirParent, { recursive: true });
88
93
  setMessage("\uBB38\uC81C\uB97C archive \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD558\uB294 \uC911...");
89
94
  await rename(solvingDir, archiveDir);
95
+ if (autoCommit) {
96
+ try {
97
+ setMessage("Git \uCEE4\uBC0B\uC744 \uC2E4\uD589\uD558\uB294 \uC911...");
98
+ await execa("git", ["add", archiveDir], { cwd: projectRoot });
99
+ await execa("git", ["commit", "-m", commitMessage], {
100
+ cwd: projectRoot
101
+ });
102
+ } catch (gitError) {
103
+ setMessage("\uCEE4\uBC0B \uC2E4\uD328\uB85C \uC778\uD574 \uB864\uBC31\uD558\uB294 \uC911...");
104
+ try {
105
+ await rename(archiveDir, solvingDir);
106
+ } catch (rollbackError) {
107
+ throw new Error(
108
+ `Git \uCEE4\uBC0B \uC2E4\uD328 \uBC0F \uB864\uBC31 \uC2E4\uD328: ${gitError instanceof Error ? gitError.message : String(gitError)}. \uB864\uBC31 \uC5D0\uB7EC: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}. \uC218\uB3D9\uC73C\uB85C \uD30C\uC77C\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.`
109
+ );
110
+ }
111
+ throw gitError;
112
+ }
113
+ }
90
114
  setMessage("\uBE48 \uB514\uB809\uD1A0\uB9AC \uC815\uB9AC \uC911...");
91
115
  try {
92
116
  const solvingDirConfig = getSolvingDir();
@@ -109,19 +133,6 @@ function useArchive({
109
133
  }
110
134
  } catch {
111
135
  }
112
- setMessage("Git \uCEE4\uBC0B\uC744 \uC2E4\uD589\uD558\uB294 \uC911...");
113
- try {
114
- await execa("git", ["add", archiveDir], { cwd: projectRoot });
115
- const commitMessage = `solve: ${problemId} - ${problemTitle}`;
116
- await execa("git", ["commit", "-m", commitMessage], {
117
- cwd: projectRoot
118
- });
119
- } catch (gitError) {
120
- console.warn(
121
- "Git \uCEE4\uBC0B \uC2E4\uD328:",
122
- gitError instanceof Error ? gitError.message : String(gitError)
123
- );
124
- }
125
136
  setStatus("success");
126
137
  setMessage(`\uBB38\uC81C ${problemId}\uB97C \uC544\uCE74\uC774\uBE0C\uD588\uC2B5\uB2C8\uB2E4: ${archiveDir}`);
127
138
  setTimeout(() => {
@@ -3,13 +3,15 @@ import {
3
3
  Command,
4
4
  CommandBuilder,
5
5
  CommandDef,
6
+ getArchiveAutoCommit,
7
+ getArchiveCommitMessage,
6
8
  getArchiveDir,
7
9
  getArchiveStrategy,
8
10
  getAutoOpenEditor,
9
11
  getDefaultLanguage,
10
12
  getEditor,
11
13
  getSolvedAcHandle
12
- } from "../chunk-F4LZ6ENP.js";
14
+ } from "../chunk-JPDN34C7.js";
13
15
  import {
14
16
  __decorateClass,
15
17
  getSupportedLanguages,
@@ -117,6 +119,19 @@ function useConfig({
117
119
  updatedConfig.archiveStrategy = value;
118
120
  break;
119
121
  }
122
+ case "archive-auto-commit": {
123
+ if (value !== "true" && value !== "false") {
124
+ console.error(
125
+ `archive-auto-commit \uAC12\uC740 true \uB610\uB294 false \uC5EC\uC57C \uD569\uB2C8\uB2E4: ${value}`
126
+ );
127
+ process.exit(1);
128
+ }
129
+ updatedConfig.archiveAutoCommit = value === "true";
130
+ break;
131
+ }
132
+ case "archive-commit-message":
133
+ updatedConfig.archiveCommitMessage = value;
134
+ break;
120
135
  default:
121
136
  console.error(`\uC54C \uC218 \uC5C6\uB294 \uC124\uC815 \uD0A4: ${configKey}`);
122
137
  process.exit(1);
@@ -161,6 +176,8 @@ function getConfigHelp() {
161
176
  solved-ac-handle Solved.ac \uD578\uB4E4 (stats \uBA85\uB839\uC5B4\uC6A9)
162
177
  archive-dir \uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C (\uAE30\uBCF8\uAC12: problems, "." \uB610\uB294 ""\uB294 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8)
163
178
  archive-strategy \uC544\uCE74\uC774\uBE59 \uC804\uB7B5 (flat, by-range, by-tier, by-tag)
179
+ archive-auto-commit archive \uC2DC Git \uCEE4\uBC0B \uC790\uB3D9 \uC2E4\uD589 \uC5EC\uBD80 (true/false)
180
+ archive-commit-message archive \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uD15C\uD50C\uB9BF ({id}, {title} \uC0AC\uC6A9 \uAC00\uB2A5)
164
181
 
165
182
  \uC635\uC158:
166
183
  --help, -h \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC
@@ -181,7 +198,9 @@ var CONFIG_KEYS = [
181
198
  { label: "auto-open-editor", value: "auto-open-editor" },
182
199
  { label: "solved-ac-handle", value: "solved-ac-handle" },
183
200
  { label: "archive-dir", value: "archive-dir" },
184
- { label: "archive-strategy", value: "archive-strategy" }
201
+ { label: "archive-strategy", value: "archive-strategy" },
202
+ { label: "archive-auto-commit", value: "archive-auto-commit" },
203
+ { label: "archive-commit-message", value: "archive-commit-message" }
185
204
  ];
186
205
  function ConfigView({
187
206
  configKey,
@@ -215,6 +234,8 @@ function ConfigView({
215
234
  const handle = config?.solvedAcHandle ?? getSolvedAcHandle();
216
235
  const archiveDir = config?.archiveDir ?? getArchiveDir();
217
236
  const archiveStrategy = config?.archiveStrategy ?? getArchiveStrategy();
237
+ const archiveAutoCommit = config?.archiveAutoCommit ?? getArchiveAutoCommit();
238
+ const archiveCommitMessage = config?.archiveCommitMessage ?? getArchiveCommitMessage();
218
239
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
219
240
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2699\uFE0F \uD604\uC7AC \uC124\uC815 (.ps-cli.json)" }) }),
220
241
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
@@ -247,6 +268,23 @@ function ConfigView({
247
268
  /* @__PURE__ */ jsx(Text, { color: "gray", children: "archive-strategy:" }),
248
269
  /* @__PURE__ */ jsx(Text, { children: " " }),
249
270
  /* @__PURE__ */ jsx(Text, { bold: true, children: archiveStrategy })
271
+ ] }),
272
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
273
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "archive-auto-commit:" }),
274
+ /* @__PURE__ */ jsx(Text, { children: " " }),
275
+ /* @__PURE__ */ jsx(Text, { bold: true, color: archiveAutoCommit ? "green" : "gray", children: archiveAutoCommit ? "true" : "false" })
276
+ ] }),
277
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
278
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "archive-commit-message:" }),
279
+ /* @__PURE__ */ jsx(Text, { children: " " }),
280
+ /* @__PURE__ */ jsx(
281
+ Text,
282
+ {
283
+ bold: true,
284
+ color: archiveCommitMessage && archiveCommitMessage.length > 0 ? "cyan" : "gray",
285
+ children: archiveCommitMessage || "(\uC124\uC815 \uC548 \uB428)"
286
+ }
287
+ )
250
288
  ] })
251
289
  ] })
252
290
  ] });
@@ -272,6 +310,12 @@ function ConfigView({
272
310
  case "archive-strategy":
273
311
  configValue = config?.archiveStrategy ?? getArchiveStrategy();
274
312
  break;
313
+ case "archive-auto-commit":
314
+ configValue = config?.archiveAutoCommit !== void 0 ? String(config.archiveAutoCommit) : String(getArchiveAutoCommit());
315
+ break;
316
+ case "archive-commit-message":
317
+ configValue = config?.archiveCommitMessage ?? getArchiveCommitMessage();
318
+ break;
275
319
  default:
276
320
  console.error(`\uC54C \uC218 \uC5C6\uB294 \uC124\uC815 \uD0A4: ${configKey}`);
277
321
  process.exit(1);
@@ -444,6 +488,10 @@ var ConfigCommand = class extends Command {
444
488
  return "\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C \uC785\uB825";
445
489
  case "archive-strategy":
446
490
  return "\uC544\uCE74\uC774\uBE59 \uC804\uB7B5 \uC785\uB825 (flat, by-range, by-tier, by-tag)";
491
+ case "archive-auto-commit":
492
+ return "true \uB610\uB294 false \uC785\uB825";
493
+ case "archive-commit-message":
494
+ return "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uD15C\uD50C\uB9BF \uC785\uB825 (\uC608: solve: {id} - {title})";
447
495
  default:
448
496
  return "\uAC12 \uC785\uB825";
449
497
  }
@@ -462,6 +510,10 @@ var ConfigCommand = class extends Command {
462
510
  return '\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C (\uAE30\uBCF8\uAC12: "problems", \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8: ".")';
463
511
  case "archive-strategy":
464
512
  return "\uC544\uCE74\uC774\uBE59 \uC804\uB7B5: flat (\uD3C9\uBA74), by-range (1000\uBC88\uB300 \uBB36\uAE30), by-tier (\uD2F0\uC5B4\uBCC4), by-tag (\uD0DC\uADF8\uBCC4)";
513
+ case "archive-auto-commit":
514
+ return "archive \uBA85\uB839 \uC2E4\uD589 \uC2DC Git \uCEE4\uBC0B\uC744 \uC790\uB3D9\uC73C\uB85C \uC218\uD589\uD560\uC9C0 \uC5EC\uBD80";
515
+ case "archive-commit-message":
516
+ return "archive \uC2DC \uC0AC\uC6A9\uD560 Git \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uD15C\uD50C\uB9BF ({id}, {title} \uC0AC\uC6A9 \uAC00\uB2A5)";
465
517
  default:
466
518
  return "";
467
519
  }
@@ -478,6 +530,8 @@ var ConfigCommand = class extends Command {
478
530
  return ["problems", ".", ""];
479
531
  case "archive-strategy":
480
532
  return ["flat", "by-range", "by-tier", "by-tag"];
533
+ case "archive-auto-commit":
534
+ return ["true", "false"];
481
535
  default:
482
536
  return [];
483
537
  }
@@ -4,7 +4,10 @@ import {
4
4
  } from "../chunk-4ISG24GW.js";
5
5
  import {
6
6
  getProblem
7
- } from "../chunk-A6STXEAE.js";
7
+ } from "../chunk-QB2R47PW.js";
8
+ import {
9
+ defineFlags
10
+ } from "../chunk-PY6GW22W.js";
8
11
  import {
9
12
  Command,
10
13
  CommandBuilder,
@@ -16,7 +19,7 @@ import {
16
19
  getTierImageUrl,
17
20
  getTierName,
18
21
  resolveProblemContext
19
- } from "../chunk-F4LZ6ENP.js";
22
+ } from "../chunk-JPDN34C7.js";
20
23
  import {
21
24
  __decorateClass,
22
25
  getLanguageConfig,
@@ -315,6 +318,14 @@ ${editor}\uB85C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.`
315
318
 
316
319
  // src/commands/fetch.tsx
317
320
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
321
+ var fetchFlagsSchema = {
322
+ language: {
323
+ type: "string",
324
+ shortFlag: "l",
325
+ description: `\uC5B8\uC5B4 \uC120\uD0DD (${getSupportedLanguagesString()})
326
+ \uAE30\uBCF8\uAC12: python`
327
+ }
328
+ };
318
329
  function FetchView({
319
330
  problemId,
320
331
  language = "python",
@@ -378,16 +389,7 @@ FetchCommand = __decorateClass([
378
389
  - \uBB38\uC81C \uC124\uBA85, \uC785\uCD9C\uB825 \uD615\uC2DD, \uC608\uC81C \uC785\uCD9C\uB825 \uD30C\uC77C \uC790\uB3D9 \uC0DD\uC131
379
390
  - \uC120\uD0DD\uD55C \uC5B8\uC5B4\uC758 \uC194\uB8E8\uC158 \uD15C\uD50C\uB9BF \uD30C\uC77C \uC0DD\uC131
380
391
  - README.md\uC5D0 \uBB38\uC81C \uC815\uBCF4, \uD1B5\uACC4, \uD0DC\uADF8 \uB4F1 \uD3EC\uD568`,
381
- flags: [
382
- {
383
- name: "language",
384
- options: {
385
- shortFlag: "l",
386
- description: `\uC5B8\uC5B4 \uC120\uD0DD (${getSupportedLanguagesString()})
387
- \uAE30\uBCF8\uAC12: python`
388
- }
389
- }
390
- ],
392
+ flags: defineFlags(fetchFlagsSchema),
391
393
  autoDetectProblemId: false,
392
394
  requireProblemId: true,
393
395
  examples: ["fetch 1000", "fetch 1000 --language python", "fetch 1000 -l cpp"]
@@ -10,7 +10,7 @@ import {
10
10
  getEditor,
11
11
  getSolvedAcHandle,
12
12
  getSolvingDir
13
- } from "../chunk-F4LZ6ENP.js";
13
+ } from "../chunk-JPDN34C7.js";
14
14
  import {
15
15
  __decorateClass,
16
16
  getSupportedLanguages
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  useOpenBrowser
4
- } from "../chunk-GCOFFYJ3.js";
4
+ } from "../chunk-3LR2NGRC.js";
5
5
  import "../chunk-QGMWUOJ3.js";
6
+ import {
7
+ defineFlags
8
+ } from "../chunk-PY6GW22W.js";
6
9
  import {
7
10
  Command,
8
11
  CommandBuilder,
9
12
  CommandDef,
10
13
  resolveProblemContext
11
- } from "../chunk-F4LZ6ENP.js";
14
+ } from "../chunk-JPDN34C7.js";
12
15
  import {
13
16
  __decorateClass
14
17
  } from "../chunk-7MQMPJ3X.js";
@@ -18,17 +21,30 @@ import { StatusMessage, Alert } from "@inkjs/ui";
18
21
  import { Spinner } from "@inkjs/ui";
19
22
  import { Text, Box } from "ink";
20
23
  import { jsx, jsxs } from "react/jsx-runtime";
21
- function OpenView({ problemId, onComplete }) {
24
+ var openFlagsSchema = {
25
+ workbook: {
26
+ type: "number",
27
+ shortFlag: "w",
28
+ description: "\uBB38\uC81C\uC9D1 ID\uB97C \uC9C0\uC815\uD558\uC5EC \uD574\uB2F9 \uBB38\uC81C\uC9D1 \uD398\uC774\uC9C0\uB97C \uC5FD\uB2C8\uB2E4"
29
+ }
30
+ };
31
+ function OpenView({ problemId, workbookId, onComplete }) {
22
32
  const { status, error, url } = useOpenBrowser({
23
33
  problemId,
34
+ workbookId,
24
35
  onComplete
25
36
  });
37
+ const displayId = workbookId !== void 0 ? workbookId : problemId;
38
+ const displayType = workbookId !== void 0 ? "\uBB38\uC81C\uC9D1" : "\uBB38\uC81C";
26
39
  if (status === "loading") {
27
40
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
28
41
  /* @__PURE__ */ jsx(Spinner, { label: "\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5EC\uB294 \uC911..." }),
29
42
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
30
- "\uBB38\uC81C #",
31
- problemId
43
+ displayType,
44
+ " ",
45
+ workbookId !== void 0 ? "ID" : "#",
46
+ ": ",
47
+ displayId
32
48
  ] }) })
33
49
  ] });
34
50
  }
@@ -45,12 +61,21 @@ function OpenView({ problemId, onComplete }) {
45
61
  ] });
46
62
  }
47
63
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
48
- /* @__PURE__ */ jsx(StatusMessage, { variant: "success", children: "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uBB38\uC81C \uD398\uC774\uC9C0\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4!" }),
64
+ /* @__PURE__ */ jsxs(StatusMessage, { variant: "success", children: [
65
+ "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C ",
66
+ displayType,
67
+ " \uD398\uC774\uC9C0\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4!"
68
+ ] }),
49
69
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
50
70
  /* @__PURE__ */ jsxs(Text, { children: [
51
- /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\uBB38\uC81C \uBC88\uD638:" }),
71
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
72
+ displayType,
73
+ " ",
74
+ workbookId !== void 0 ? "ID" : "\uBC88\uD638",
75
+ ":"
76
+ ] }),
52
77
  " ",
53
- problemId
78
+ displayId
54
79
  ] }),
55
80
  /* @__PURE__ */ jsxs(Text, { children: [
56
81
  /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "URL:" }),
@@ -61,11 +86,26 @@ function OpenView({ problemId, onComplete }) {
61
86
  ] });
62
87
  }
63
88
  var OpenCommand = class extends Command {
64
- async execute(args, _flags) {
89
+ async execute(args, flags) {
90
+ const workbookId = flags.workbook ? parseInt(String(flags.workbook), 10) : null;
91
+ if (workbookId !== null) {
92
+ if (isNaN(workbookId) || workbookId <= 0) {
93
+ console.error("\uC624\uB958: \uC720\uD6A8\uD55C \uBB38\uC81C\uC9D1 ID\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
94
+ console.error(`\uC0AC\uC6A9\uBC95: ps open --workbook <\uBB38\uC81C\uC9D1ID>`);
95
+ console.error(`\uB3C4\uC6C0\uB9D0: ps open --help`);
96
+ process.exit(1);
97
+ return;
98
+ }
99
+ await this.renderView(OpenView, {
100
+ workbookId
101
+ });
102
+ return;
103
+ }
65
104
  const context = await resolveProblemContext(args, { requireId: true });
66
105
  if (context.problemId === null) {
67
106
  console.error("\uC624\uB958: \uBB38\uC81C \uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
68
107
  console.error(`\uC0AC\uC6A9\uBC95: ps open <\uBB38\uC81C\uBC88\uD638>`);
108
+ console.error(` ps open --workbook <\uBB38\uC81C\uC9D1ID>`);
69
109
  console.error(`\uB3C4\uC6C0\uB9D0: ps open --help`);
70
110
  console.error(
71
111
  `\uD78C\uD2B8: problems/{\uBB38\uC81C\uBC88\uD638} \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uBB38\uC81C \uBC88\uD638\uB97C \uCD94\uB860\uD569\uB2C8\uB2E4.`
@@ -81,14 +121,18 @@ var OpenCommand = class extends Command {
81
121
  OpenCommand = __decorateClass([
82
122
  CommandDef({
83
123
  name: "open",
84
- description: `\uBC31\uC900 \uBB38\uC81C \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5FD\uB2C8\uB2E4.
124
+ description: `\uBC31\uC900 \uBB38\uC81C \uD398\uC774\uC9C0 \uB610\uB294 \uBB38\uC81C\uC9D1 \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5FD\uB2C8\uB2E4.
85
125
  - \uBB38\uC81C \uBC88\uD638\uB97C \uC778\uC790\uB85C \uC804\uB2EC\uD558\uAC70\uB098
86
- - \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uBB38\uC81C \uBC88\uD638\uB97C \uCD94\uB860\uD569\uB2C8\uB2E4.`,
126
+ - \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uBB38\uC81C \uBC88\uD638\uB97C \uCD94\uB860\uD569\uB2C8\uB2E4.
127
+ - --workbook \uC635\uC158\uC73C\uB85C \uBB38\uC81C\uC9D1 ID\uB97C \uC9C0\uC815\uD558\uBA74 \uBB38\uC81C\uC9D1 \uD398\uC774\uC9C0\uB97C \uC5FD\uB2C8\uB2E4.`,
87
128
  autoDetectProblemId: true,
88
- requireProblemId: true,
129
+ requireProblemId: false,
130
+ flags: defineFlags(openFlagsSchema),
89
131
  examples: [
90
132
  "open 1000 # 1000\uBC88 \uBB38\uC81C \uC5F4\uAE30",
91
133
  "open # \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589 \uC2DC \uC790\uB3D9 \uCD94\uB860",
134
+ "open --workbook 25052 # \uBB38\uC81C\uC9D1 25052 \uC5F4\uAE30",
135
+ "open -w 25052 # \uBB38\uC81C\uC9D1 25052 \uC5F4\uAE30 (\uB2E8\uCD95 \uC635\uC158)",
92
136
  "open --help # \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC"
93
137
  ]
94
138
  })
@@ -2,13 +2,16 @@
2
2
  import {
3
3
  runSolution
4
4
  } from "../chunk-VIHXBCOZ.js";
5
+ import {
6
+ defineFlags
7
+ } from "../chunk-PY6GW22W.js";
5
8
  import {
6
9
  Command,
7
10
  CommandBuilder,
8
11
  CommandDef,
9
12
  resolveLanguage,
10
13
  resolveProblemContext
11
- } from "../chunk-F4LZ6ENP.js";
14
+ } from "../chunk-JPDN34C7.js";
12
15
  import {
13
16
  __decorateClass,
14
17
  getSupportedLanguagesString
@@ -63,6 +66,19 @@ function useRunSolution({
63
66
 
64
67
  // src/commands/run.tsx
65
68
  import { jsx, jsxs } from "react/jsx-runtime";
69
+ var runFlagsSchema = {
70
+ language: {
71
+ type: "string",
72
+ shortFlag: "l",
73
+ description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
74
+ \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
75
+ },
76
+ input: {
77
+ type: "string",
78
+ shortFlag: "i",
79
+ description: "\uC785\uB825 \uD30C\uC77C \uC9C0\uC815 (\uC608: 1 \uB610\uB294 testcases/1/input.txt)"
80
+ }
81
+ };
66
82
  function RunView({
67
83
  problemDir,
68
84
  language,
@@ -171,23 +187,7 @@ RunCommand = __decorateClass([
171
187
  - --input \uC635\uC158\uC73C\uB85C \uC785\uB825 \uD30C\uC77C \uC9C0\uC815 \uAC00\uB2A5 (\uC608: testcases/1/input.txt)
172
188
  - \uC635\uC158 \uC5C6\uC774 \uC2E4\uD589 \uC2DC \uD45C\uC900 \uC785\uB825\uC73C\uB85C \uC785\uB825 \uBC1B\uAE30
173
189
  - \uD14C\uC2A4\uD2B8 \uCF00\uC774\uC2A4 \uAC80\uC99D \uC5C6\uC774 \uB2E8\uC21C \uC2E4\uD589`,
174
- flags: [
175
- {
176
- name: "language",
177
- options: {
178
- shortFlag: "l",
179
- description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
180
- \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
181
- }
182
- },
183
- {
184
- name: "input",
185
- options: {
186
- shortFlag: "i",
187
- description: "\uC785\uB825 \uD30C\uC77C \uC9C0\uC815 (\uC608: 1 \uB610\uB294 testcases/1/input.txt)"
188
- }
189
- }
190
- ],
190
+ flags: defineFlags(runFlagsSchema),
191
191
  autoDetectProblemId: true,
192
192
  autoDetectLanguage: true,
193
193
  examples: [
@@ -7,11 +7,14 @@ import {
7
7
  } from "../chunk-4ISG24GW.js";
8
8
  import {
9
9
  getProblem
10
- } from "../chunk-A6STXEAE.js";
10
+ } from "../chunk-QB2R47PW.js";
11
11
  import {
12
12
  useOpenBrowser
13
- } from "../chunk-GCOFFYJ3.js";
13
+ } from "../chunk-3LR2NGRC.js";
14
14
  import "../chunk-QGMWUOJ3.js";
15
+ import {
16
+ defineFlags
17
+ } from "../chunk-PY6GW22W.js";
15
18
  import {
16
19
  Command,
17
20
  CommandBuilder,
@@ -19,7 +22,7 @@ import {
19
22
  getArchiveDirPath,
20
23
  getTierColor,
21
24
  getTierName
22
- } from "../chunk-F4LZ6ENP.js";
25
+ } from "../chunk-JPDN34C7.js";
23
26
  import {
24
27
  __decorateClass
25
28
  } from "../chunk-7MQMPJ3X.js";
@@ -48,20 +51,20 @@ function ProblemSelector({
48
51
  const solvedText = problem.solvedCount ? ` (${problem.solvedCount.toLocaleString()}\uBA85` : "";
49
52
  const triesText = problem.averageTries ? `, \uD3C9\uADE0 ${problem.averageTries}\uD68C` : "";
50
53
  const suffix = solvedText + triesText + (solvedText ? ")" : "");
51
- const solvedMark = problem.isSolved ? " \u2713" : "";
54
+ const solvedMark = problem.isSolved ? ` ${source_default.bold.green("\u2713")}` : "";
52
55
  let tierText = "";
53
56
  if (problem.level) {
54
57
  const tierName = getTierName(problem.level);
55
58
  const tierColor = getTierColor(problem.level);
56
59
  if (typeof tierColor === "string") {
57
- tierText = ` ${source_default.bold.hex(tierColor)(tierName)}`;
60
+ tierText = `${source_default.bold.hex(tierColor)(tierName)} `;
58
61
  } else {
59
- tierText = ` ${tierColor(source_default.bold(tierName))}`;
62
+ tierText = `${tierColor(source_default.bold(tierName))} `;
60
63
  }
61
64
  }
62
65
  const problemText = `${problem.problemId} - ${problem.title}`;
63
66
  options.push({
64
- label: `${tierText} ${problemText}${solvedMark}${suffix}`,
67
+ label: `${tierText}${problemText}${suffix}${solvedMark}`,
65
68
  value: `problem:${problem.problemId}`
66
69
  });
67
70
  });
@@ -180,6 +183,13 @@ async function scrapeWorkbook(workbookId) {
180
183
 
181
184
  // src/commands/search.tsx
182
185
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
186
+ var searchFlagsSchema = {
187
+ workbook: {
188
+ type: "number",
189
+ shortFlag: "w",
190
+ description: "\uBB38\uC81C\uC9D1 ID\uB97C \uC9C0\uC815\uD558\uC5EC \uD574\uB2F9 \uBB38\uC81C\uC9D1\uC758 \uBB38\uC81C \uBAA9\uB85D\uC744 \uD45C\uC2DC"
191
+ }
192
+ };
183
193
  async function enrichProblemsWithTiers(problems) {
184
194
  const BATCH_SIZE = 10;
185
195
  const DELAY_MS = 200;
@@ -191,7 +201,10 @@ async function enrichProblemsWithTiers(problems) {
191
201
  const solvedAcData = await getProblem(problem.problemId);
192
202
  return {
193
203
  ...problem,
194
- level: solvedAcData.level
204
+ level: solvedAcData.level,
205
+ tags: solvedAcData.tags.map(
206
+ (t) => t.displayNames.find((n) => n.language === "ko")?.name || t.key
207
+ )
195
208
  };
196
209
  } catch (error) {
197
210
  console.warn(
@@ -270,7 +283,10 @@ function WorkbookSearchView({
270
283
  ] });
271
284
  }
272
285
  const problemsWithSolvedStatus = problems.map((problem) => {
273
- const problemDirPath = getArchiveDirPath(problem.problemId);
286
+ const problemDirPath = getArchiveDirPath(problem.problemId, process.cwd(), {
287
+ level: problem.level,
288
+ tags: problem.tags
289
+ });
274
290
  const isSolved = existsSync(problemDirPath);
275
291
  return {
276
292
  problemId: problem.problemId,
@@ -360,12 +376,34 @@ function SearchView({ query, onComplete }) {
360
376
  setLoading(true);
361
377
  setError(null);
362
378
  const searchResults = await searchProblems(query, currentPage);
379
+ const enrichedProblems = await enrichProblemsWithTiers(
380
+ searchResults.problems.map((p) => ({
381
+ problemId: p.problemId,
382
+ title: p.title,
383
+ order: 0
384
+ // SearchResult에는 order가 없으므로 0으로 설정
385
+ }))
386
+ );
363
387
  const resultsWithSolvedStatus = searchResults.problems.map(
364
388
  (problem) => {
365
- const problemDirPath = getArchiveDirPath(problem.problemId);
389
+ const enriched = enrichedProblems.find(
390
+ (ep) => ep.problemId === problem.problemId
391
+ );
392
+ const level = enriched?.level ?? problem.level;
393
+ const tags = enriched?.tags ?? problem.tags;
394
+ const problemDirPath = getArchiveDirPath(
395
+ problem.problemId,
396
+ process.cwd(),
397
+ {
398
+ level,
399
+ tags
400
+ }
401
+ );
366
402
  const isSolved = existsSync(problemDirPath);
367
403
  return {
368
404
  ...problem,
405
+ level,
406
+ tags,
369
407
  isSolved
370
408
  };
371
409
  }
@@ -483,14 +521,7 @@ SearchCommand = __decorateClass([
483
521
  - \uBB38\uC81C \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uBB38\uC81C \uD398\uC774\uC9C0\uB97C \uC5FD\uB2C8\uB2E4.
484
522
  - \uD398\uC774\uC9C0\uB124\uC774\uC158\uC744 \uD1B5\uD574 \uC5EC\uB7EC \uD398\uC774\uC9C0\uC758 \uACB0\uACFC\uB97C \uD0D0\uC0C9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
485
523
  - --workbook \uC635\uC158\uC73C\uB85C \uBC31\uC900 \uBB38\uC81C\uC9D1\uC758 \uBB38\uC81C \uBAA9\uB85D\uC744 \uBCFC \uC218 \uC788\uC2B5\uB2C8\uB2E4.`,
486
- flags: [
487
- {
488
- name: "workbook",
489
- options: {
490
- description: "\uBB38\uC81C\uC9D1 ID\uB97C \uC9C0\uC815\uD558\uC5EC \uD574\uB2F9 \uBB38\uC81C\uC9D1\uC758 \uBB38\uC81C \uBAA9\uB85D\uC744 \uD45C\uC2DC"
491
- }
492
- }
493
- ],
524
+ flags: defineFlags(searchFlagsSchema),
494
525
  autoDetectProblemId: false,
495
526
  requireProblemId: false,
496
527
  examples: [
@@ -499,7 +530,8 @@ SearchCommand = __decorateClass([
499
530
  'search "#dp" # DP \uD0DC\uADF8 \uBB38\uC81C \uAC80\uC0C9',
500
531
  'search "tag:dp" # DP \uD0DC\uADF8 \uBB38\uC81C \uAC80\uC0C9 (tag: \uBB38\uBC95)',
501
532
  'search "*g1...g5 #dp" # Gold 1-5 \uD2F0\uC5B4\uC758 DP \uD0DC\uADF8 \uBB38\uC81C \uAC80\uC0C9',
502
- "search --workbook 25052 # \uBB38\uC81C\uC9D1 25052\uC758 \uBB38\uC81C \uBAA9\uB85D \uD45C\uC2DC"
533
+ "search --workbook 25052 # \uBB38\uC81C\uC9D1 25052\uC758 \uBB38\uC81C \uBAA9\uB85D \uD45C\uC2DC",
534
+ "search -w 25052 # \uBB38\uC81C\uC9D1 25052\uC758 \uBB38\uC81C \uBAA9\uB85D \uD45C\uC2DC (\uB2E8\uCD95 \uC635\uC158)"
503
535
  ]
504
536
  })
505
537
  ], SearchCommand);
@@ -3,18 +3,26 @@ import {
3
3
  source_default
4
4
  } from "../chunk-ASMT3CRD.js";
5
5
  import {
6
- getUserStats
7
- } from "../chunk-A6STXEAE.js";
6
+ getUserStats,
7
+ getUserTop100
8
+ } from "../chunk-QB2R47PW.js";
9
+ import {
10
+ defineFlags
11
+ } from "../chunk-PY6GW22W.js";
8
12
  import {
9
13
  Command,
10
14
  CommandBuilder,
11
15
  CommandDef,
12
16
  calculateTierProgress,
17
+ findProjectRoot,
18
+ getArchiveDir,
13
19
  getNextTierMinRating,
14
20
  getSolvedAcHandle,
21
+ getSolvingDir,
15
22
  getTierColor,
16
- getTierName
17
- } from "../chunk-F4LZ6ENP.js";
23
+ getTierName,
24
+ getTierShortName
25
+ } from "../chunk-JPDN34C7.js";
18
26
  import {
19
27
  __decorateClass
20
28
  } from "../chunk-7MQMPJ3X.js";
@@ -25,40 +33,98 @@ import { Spinner } from "@inkjs/ui";
25
33
  import { Box, Text, Transform } from "ink";
26
34
 
27
35
  // src/hooks/use-user-stats.ts
36
+ import { existsSync } from "fs";
37
+ import { readdir, stat } from "fs/promises";
38
+ import { join } from "path";
28
39
  import { useEffect, useState } from "react";
40
+ async function countProblems(dir) {
41
+ let count = 0;
42
+ try {
43
+ if (!existsSync(dir)) return 0;
44
+ const entries = await readdir(dir);
45
+ for (const entry of entries) {
46
+ if (entry.startsWith(".")) continue;
47
+ const fullPath = join(dir, entry);
48
+ const s = await stat(fullPath);
49
+ if (s.isDirectory()) {
50
+ if (existsSync(join(fullPath, "meta.json"))) {
51
+ count++;
52
+ } else {
53
+ count += await countProblems(fullPath);
54
+ }
55
+ }
56
+ }
57
+ } catch {
58
+ }
59
+ return count;
60
+ }
29
61
  function useUserStats({
30
62
  handle,
31
- onComplete
63
+ onComplete,
64
+ fetchLocalCount = false
32
65
  }) {
33
66
  const [status, setStatus] = useState(
34
67
  "loading"
35
68
  );
36
69
  const [user, setUser] = useState(null);
70
+ const [top100, setTop100] = useState(null);
71
+ const [localSolvedCount, setLocalSolvedCount] = useState(null);
37
72
  const [error, setError] = useState(null);
38
73
  useEffect(() => {
39
- void getUserStats(handle).then((userData) => {
40
- setUser(userData);
41
- setStatus("success");
42
- setTimeout(() => {
43
- onComplete();
44
- }, 5e3);
45
- }).catch((err) => {
46
- setError(err instanceof Error ? err.message : String(err));
47
- setStatus("error");
48
- setTimeout(() => {
49
- onComplete();
50
- }, 3e3);
51
- });
74
+ async function fetchData() {
75
+ try {
76
+ const [userData, top100Data] = await Promise.all([
77
+ getUserStats(handle),
78
+ getUserTop100(handle)
79
+ ]);
80
+ setUser(userData);
81
+ setTop100(top100Data);
82
+ if (fetchLocalCount) {
83
+ const projectRoot = findProjectRoot();
84
+ if (projectRoot) {
85
+ const archiveDir = getArchiveDir();
86
+ const solvingDir = getSolvingDir();
87
+ const archivePath = join(projectRoot, archiveDir);
88
+ const solvingPath = join(projectRoot, solvingDir);
89
+ const [archiveCount, solvingCount] = await Promise.all([
90
+ countProblems(archivePath),
91
+ countProblems(solvingPath)
92
+ ]);
93
+ setLocalSolvedCount(archiveCount + solvingCount);
94
+ }
95
+ }
96
+ setStatus("success");
97
+ setTimeout(() => {
98
+ onComplete();
99
+ }, 5e3);
100
+ } catch (err) {
101
+ setError(err instanceof Error ? err.message : String(err));
102
+ setStatus("error");
103
+ setTimeout(() => {
104
+ onComplete();
105
+ }, 3e3);
106
+ }
107
+ }
108
+ void fetchData();
52
109
  }, [handle, onComplete]);
53
110
  return {
54
111
  status,
55
112
  user,
113
+ top100,
114
+ localSolvedCount,
56
115
  error
57
116
  };
58
117
  }
59
118
 
60
119
  // src/commands/stats.tsx
61
120
  import { jsx, jsxs } from "react/jsx-runtime";
121
+ var statsFlagsSchema = {
122
+ handle: {
123
+ type: "string",
124
+ shortFlag: "h",
125
+ description: "Solved.ac \uD578\uB4E4 (\uC124\uC815\uC5D0 \uC800\uC7A5\uB41C \uAC12 \uC0AC\uC6A9 \uAC00\uB2A5)"
126
+ }
127
+ };
62
128
  function ProgressBarWithColor({ value, colorFn }) {
63
129
  const width = process.stdout.columns || 40;
64
130
  const barWidth = Math.max(10, Math.min(30, width - 20));
@@ -69,10 +135,11 @@ function ProgressBarWithColor({ value, colorFn }) {
69
135
  const barText = filledBar + emptyBar;
70
136
  return /* @__PURE__ */ jsx(Transform, { transform: (output) => colorFn(output), children: /* @__PURE__ */ jsx(Text, { children: barText }) });
71
137
  }
72
- function StatsView({ handle, onComplete }) {
73
- const { status, user, error } = useUserStats({
138
+ function StatsView({ handle, onComplete, showLocalStats }) {
139
+ const { status, user, top100, localSolvedCount, error } = useUserStats({
74
140
  handle,
75
- onComplete
141
+ onComplete,
142
+ fetchLocalCount: showLocalStats
76
143
  });
77
144
  if (status === "loading") {
78
145
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Spinner, { label: "\uD1B5\uACC4\uB97C \uBD88\uB7EC\uC624\uB294 \uC911..." }) });
@@ -90,10 +157,16 @@ function StatsView({ handle, onComplete }) {
90
157
  const nextTierMin = getNextTierMinRating(user.tier);
91
158
  const progress = user.tier === 31 ? 100 : calculateTierProgress(user.rating, user.tier);
92
159
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
93
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
94
- "\u2728 ",
95
- user.handle
96
- ] }) }),
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
+ ] }),
97
170
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, flexDirection: "row", gap: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
98
171
  tierColorFn(tierName),
99
172
  " ",
@@ -113,7 +186,12 @@ function StatsView({ handle, onComplete }) {
113
186
  "\uD574\uACB0\uD55C \uBB38\uC81C:",
114
187
  " ",
115
188
  /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: user.solvedCount.toLocaleString() }),
116
- "\uAC1C"
189
+ "\uAC1C",
190
+ localSolvedCount !== null && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
191
+ " (\uB85C\uCEEC: ",
192
+ localSolvedCount,
193
+ "\uAC1C)"
194
+ ] })
117
195
  ] }),
118
196
  /* @__PURE__ */ jsxs(Text, { children: [
119
197
  "\uD074\uB798\uC2A4: ",
@@ -132,7 +210,17 @@ function StatsView({ handle, onComplete }) {
132
210
  ] })
133
211
  ] })
134
212
  }
135
- )
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) => {
218
+ const tierColor2 = getTierColor(p.level);
219
+ 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);
221
+ }) }, rowIndex)
222
+ ) })
223
+ ] })
136
224
  ] });
137
225
  }
138
226
  return null;
@@ -153,8 +241,10 @@ var StatsCommand = class extends Command {
153
241
  process.exit(1);
154
242
  return;
155
243
  }
244
+ const showLocalStats = !args[0] && !flags.handle;
156
245
  await this.renderView(StatsView, {
157
- handle
246
+ handle,
247
+ showLocalStats
158
248
  });
159
249
  }
160
250
  };
@@ -164,15 +254,7 @@ StatsCommand = __decorateClass([
164
254
  description: `Solved.ac\uC5D0\uC11C \uC0AC\uC6A9\uC790 \uD1B5\uACC4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.
165
255
  - \uD2F0\uC5B4, \uB808\uC774\uD305, \uD574\uACB0\uD55C \uBB38\uC81C \uC218 \uB4F1 \uD45C\uC2DC
166
256
  - \uADF8\uB77C\uB370\uC774\uC158\uC73C\uB85C \uC2DC\uAC01\uC801\uC73C\uB85C \uD45C\uC2DC`,
167
- flags: [
168
- {
169
- name: "handle",
170
- options: {
171
- shortFlag: "h",
172
- description: "Solved.ac \uD578\uB4E4 (\uC124\uC815\uC5D0 \uC800\uC7A5\uB41C \uAC12 \uC0AC\uC6A9 \uAC00\uB2A5)"
173
- }
174
- }
175
- ],
257
+ flags: defineFlags(statsFlagsSchema),
176
258
  autoDetectProblemId: false,
177
259
  examples: ["stats myhandle", "stats --handle myhandle"]
178
260
  })
@@ -2,6 +2,9 @@
2
2
  import {
3
3
  openBrowser
4
4
  } from "../chunk-QGMWUOJ3.js";
5
+ import {
6
+ defineFlags
7
+ } from "../chunk-PY6GW22W.js";
5
8
  import {
6
9
  Command,
7
10
  CommandBuilder,
@@ -10,7 +13,7 @@ import {
10
13
  findSolutionFile,
11
14
  resolveLanguage,
12
15
  resolveProblemContext
13
- } from "../chunk-F4LZ6ENP.js";
16
+ } from "../chunk-JPDN34C7.js";
14
17
  import {
15
18
  __decorateClass,
16
19
  getSupportedLanguagesString
@@ -117,6 +120,14 @@ function useSubmit({
117
120
 
118
121
  // src/commands/submit.tsx
119
122
  import { jsx, jsxs } from "react/jsx-runtime";
123
+ var submitFlagsSchema = {
124
+ language: {
125
+ type: "string",
126
+ shortFlag: "l",
127
+ description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
128
+ \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
129
+ }
130
+ };
120
131
  function SubmitView({
121
132
  problemId,
122
133
  language,
@@ -224,16 +235,7 @@ SubmitCommand = __decorateClass([
224
235
  - solution.* \uD30C\uC77C\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uC5B8\uC5B4 \uAC10\uC9C0
225
236
  - \uC18C\uC2A4 \uCF54\uB4DC\uB97C \uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uC790\uB3D9 \uBCF5\uC0AC
226
237
  - \uC81C\uCD9C \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC790\uB3D9 \uC5F4\uAE30`,
227
- flags: [
228
- {
229
- name: "language",
230
- options: {
231
- shortFlag: "l",
232
- description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
233
- \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
234
- }
235
- }
236
- ],
238
+ flags: defineFlags(submitFlagsSchema),
237
239
  autoDetectProblemId: true,
238
240
  autoDetectLanguage: true,
239
241
  requireProblemId: true,
@@ -2,13 +2,16 @@
2
2
  import {
3
3
  runSolution
4
4
  } from "../chunk-VIHXBCOZ.js";
5
+ import {
6
+ defineFlags
7
+ } from "../chunk-PY6GW22W.js";
5
8
  import {
6
9
  Command,
7
10
  CommandBuilder,
8
11
  CommandDef,
9
12
  resolveLanguage,
10
13
  resolveProblemContext
11
- } from "../chunk-F4LZ6ENP.js";
14
+ } from "../chunk-JPDN34C7.js";
12
15
  import {
13
16
  __decorateClass,
14
17
  getSupportedLanguagesString
@@ -281,6 +284,20 @@ function useTestRunner({
281
284
 
282
285
  // src/commands/test.tsx
283
286
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
287
+ var testFlagsSchema = {
288
+ language: {
289
+ type: "string",
290
+ shortFlag: "l",
291
+ description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
292
+ \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
293
+ },
294
+ watch: {
295
+ type: "boolean",
296
+ shortFlag: "w",
297
+ description: `watch \uBAA8\uB4DC (\uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8)
298
+ solution.*, testcases/**/*.txt \uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0`
299
+ }
300
+ };
284
301
  function TestView({
285
302
  problemDir,
286
303
  language,
@@ -342,24 +359,7 @@ TestCommand = __decorateClass([
342
359
  - testcases/{\uBC88\uD638}/input.txt\uC640 testcases/{\uBC88\uD638}/output.txt \uD30C\uC77C\uC744 \uAE30\uBC18\uC73C\uB85C \uD14C\uC2A4\uD2B8
343
360
  - \uBB38\uC81C\uC758 \uC2DC\uAC04 \uC81C\uD55C\uC744 \uC790\uB3D9\uC73C\uB85C \uC801\uC6A9
344
361
  - --watch \uC635\uC158\uC73C\uB85C \uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8`,
345
- flags: [
346
- {
347
- name: "language",
348
- options: {
349
- shortFlag: "l",
350
- description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
351
- \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
352
- }
353
- },
354
- {
355
- name: "watch",
356
- options: {
357
- shortFlag: "w",
358
- description: `watch \uBAA8\uB4DC (\uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8)
359
- solution.*, testcases/**/*.txt \uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0`
360
- }
361
- }
362
- ],
362
+ flags: defineFlags(testFlagsSchema),
363
363
  autoDetectProblemId: true,
364
364
  autoDetectLanguage: true,
365
365
  examples: [
package/dist/index.js CHANGED
@@ -9471,7 +9471,8 @@ async function main() {
9471
9471
  if (!commands) {
9472
9472
  commands = await loadCommands();
9473
9473
  }
9474
- const [command, ...args] = cli.input;
9474
+ const [command, ...initialArgs] = cli.input;
9475
+ let rawArgs = initialArgs;
9475
9476
  if (command === "help" || !command && cli.flags.help) {
9476
9477
  const helpText = generateHelpText(commands);
9477
9478
  console.log(helpText.trim());
@@ -9493,6 +9494,80 @@ async function main() {
9493
9494
  process.exit(1);
9494
9495
  return;
9495
9496
  }
9497
+ let finalFlags = cli.flags;
9498
+ const commandIndex = process.argv.findIndex((arg) => arg === command);
9499
+ const commandArgs = commandIndex >= 0 ? process.argv.slice(commandIndex + 1) : process.argv.slice(2);
9500
+ if (commandDef.metadata?.flags) {
9501
+ const commandFlags = {};
9502
+ for (const flagDef of commandDef.metadata.flags) {
9503
+ const flagConfig = {
9504
+ type: flagDef.options?.type || "string"
9505
+ };
9506
+ if (flagDef.options?.shortFlag) {
9507
+ flagConfig.shortFlag = flagDef.options.shortFlag;
9508
+ }
9509
+ if (flagDef.options?.default !== void 0) {
9510
+ flagConfig.default = flagDef.options.default;
9511
+ }
9512
+ commandFlags[flagDef.name] = flagConfig;
9513
+ }
9514
+ const commandCli = meow("", {
9515
+ importMeta: import.meta,
9516
+ argv: commandArgs,
9517
+ autoHelp: false,
9518
+ // 기본 help 출력 비활성화 (우리가 직접 처리)
9519
+ flags: {
9520
+ help: {
9521
+ type: "boolean",
9522
+ shortFlag: "h",
9523
+ default: false
9524
+ },
9525
+ ...commandFlags
9526
+ }
9527
+ });
9528
+ if (commandCli.flags.help) {
9529
+ if (commandDef.help && commandDef.help.trim()) {
9530
+ console.log(commandDef.help.trim());
9531
+ } else {
9532
+ console.log(`\uBA85\uB839\uC5B4: ${commandDef.name}`);
9533
+ if (commandDef.metadata?.description) {
9534
+ console.log(`\uC124\uBA85: ${commandDef.metadata.description}`);
9535
+ }
9536
+ }
9537
+ process.exit(0);
9538
+ return;
9539
+ }
9540
+ rawArgs = commandCli.input;
9541
+ finalFlags = { ...cli.flags, ...commandCli.flags };
9542
+ } else {
9543
+ const commandCli = meow("", {
9544
+ importMeta: import.meta,
9545
+ argv: commandArgs,
9546
+ autoHelp: false,
9547
+ // 기본 help 출력 비활성화 (우리가 직접 처리)
9548
+ flags: {
9549
+ help: {
9550
+ type: "boolean",
9551
+ shortFlag: "h",
9552
+ default: false
9553
+ }
9554
+ }
9555
+ });
9556
+ if (commandCli.flags.help) {
9557
+ if (commandDef.help && commandDef.help.trim()) {
9558
+ console.log(commandDef.help.trim());
9559
+ } else {
9560
+ console.log(`\uBA85\uB839\uC5B4: ${commandDef.name}`);
9561
+ if (commandDef.metadata?.description) {
9562
+ console.log(`\uC124\uBA85: ${commandDef.metadata.description}`);
9563
+ }
9564
+ }
9565
+ process.exit(0);
9566
+ return;
9567
+ }
9568
+ rawArgs = commandCli.input;
9569
+ finalFlags = { ...cli.flags, ...commandCli.flags };
9570
+ }
9496
9571
  if (command !== "init") {
9497
9572
  let currentDir = process.cwd();
9498
9573
  let found = false;
@@ -9517,7 +9592,7 @@ async function main() {
9517
9592
  return;
9518
9593
  }
9519
9594
  }
9520
- await commandDef.execute(args, cli.flags);
9595
+ await commandDef.execute(rawArgs, finalFlags);
9521
9596
  }
9522
9597
  main().catch((error) => {
9523
9598
  console.error("\uC624\uB958:", error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhseung/ps-cli",
3
- "version": "1.7.5",
3
+ "version": "1.8.0",
4
4
  "description": "백준(BOJ) 문제 해결을 위한 통합 CLI 도구",
5
5
  "type": "module",
6
6
  "bin": {