@rhseung/ps-cli 1.7.2 → 1.7.4

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
@@ -24,99 +24,313 @@ ps test
24
24
 
25
25
  # 4. 제출
26
26
  ps submit
27
+
28
+ # 5. 커밋 및 아카이빙
29
+ ps archive
27
30
  ```
28
31
 
29
32
  ## 명령어
30
33
 
31
34
  ### `init` - 프로젝트 초기화
32
35
 
33
- 프로젝트를 초기화하고 설정을 구성합니다.
36
+ 프로젝트를 대화형으로 초기화하고 설정을 구성합니다.
37
+
38
+ **사용법:**
34
39
 
35
40
  ```bash
36
41
  ps init
37
42
  ```
38
43
 
44
+ **설명:**
45
+
46
+ - 단계별로 설정을 물어봅니다
47
+ - 아카이브 디렉토리, solving 디렉토리, 아카이빙 전략, 기본 언어, 에디터 등을 설정할 수 있습니다
48
+
49
+ ---
50
+
39
51
  ### `fetch` - 문제 가져오기
40
52
 
41
53
  백준 문제를 가져와서 로컬에 파일을 생성합니다.
42
54
 
55
+ **사용법:**
56
+
57
+ ```bash
58
+ ps fetch <문제번호> [옵션]
59
+ ```
60
+
61
+ **옵션:**
62
+
63
+ - `--language`, `-l`: 언어 선택 (python, javascript, typescript, cpp)
64
+ - 기본값: python 또는 설정 파일의 `default-language`
65
+
66
+ **예제:**
67
+
43
68
  ```bash
44
69
  ps fetch 1000
45
70
  ps fetch 1000 --language python
71
+ ps fetch 1000 -l cpp
46
72
  ```
47
73
 
74
+ **설명:**
75
+
76
+ - Solved.ac API와 BOJ 크롤링을 통해 문제 정보 수집
77
+ - 문제 설명, 입출력 형식, 예제 입출력 파일 자동 생성
78
+ - 선택한 언어의 솔루션 템플릿 파일 생성
79
+ - README.md에 문제 정보, 통계, 태그 등 포함
80
+
81
+ ---
82
+
48
83
  ### `test` - 로컬 테스트
49
84
 
50
85
  예제 입출력으로 테스트를 실행합니다.
51
86
 
87
+ **사용법:**
88
+
52
89
  ```bash
53
- ps test
54
- ps test 1000
55
- ps test --watch # 파일 변경 시 자동 재테스트
90
+ ps test [문제번호] [옵션]
91
+ ```
92
+
93
+ **옵션:**
94
+
95
+ - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
96
+ - 지원 언어: python, javascript, typescript, cpp
97
+ - `--watch`, `-w`: watch 모드 (파일 변경 시 자동 재테스트)
98
+ - solution._, input_.txt, output\*.txt 파일 변경 감지
99
+
100
+ **예제:**
101
+
102
+ ```bash
103
+ ps test # 현재 디렉토리에서 테스트
104
+ ps test 1000 # 1000번 문제 테스트
105
+ ps test --watch # watch 모드로 테스트
106
+ ps test 1000 --watch # 1000번 문제를 watch 모드로 테스트
107
+ ps test --language python # Python으로 테스트
56
108
  ```
57
109
 
110
+ **설명:**
111
+
112
+ - 현재 디렉토리 또는 지정한 문제 번호의 테스트 실행
113
+ - solution.\* 파일을 자동으로 찾아 언어 감지
114
+ - input*.txt와 output*.txt 파일을 기반으로 테스트
115
+ - 문제의 시간 제한을 자동으로 적용
116
+
117
+ ---
118
+
58
119
  ### `run` - 코드 실행
59
120
 
60
121
  테스트 없이 코드를 실행합니다.
61
122
 
123
+ **사용법:**
124
+
125
+ ```bash
126
+ ps run [문제번호] [옵션]
127
+ ```
128
+
129
+ **옵션:**
130
+
131
+ - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
132
+ - 지원 언어: python, javascript, typescript, cpp
133
+ - `--input`, `-i`: 입력 파일 지정
134
+ - 기본값: input.txt 또는 input1.txt
135
+
136
+ **예제:**
137
+
62
138
  ```bash
63
- ps run
64
- ps run 1000
139
+ ps run # 현재 디렉토리에서 실행
140
+ ps run 1000 # 1000번 문제 실행
141
+ ps run --language python # Python으로 실행
142
+ ps run --input input2.txt # 특정 입력 파일 사용
65
143
  ```
66
144
 
145
+ **설명:**
146
+
147
+ - 현재 디렉토리 또는 지정한 문제 번호의 코드 실행
148
+ - solution.\* 파일을 자동으로 찾아 언어 감지
149
+ - input.txt 또는 input1.txt를 표준 입력으로 사용
150
+ - 테스트 케이스 검증 없이 단순 실행
151
+
152
+ ---
153
+
67
154
  ### `submit` - 제출
68
155
 
69
156
  백준 제출 페이지를 열고 소스 코드를 클립보드에 복사합니다.
70
157
 
158
+ **사용법:**
159
+
71
160
  ```bash
72
- ps submit
73
- ps submit 1000
161
+ ps submit [문제번호] [옵션]
162
+ ```
163
+
164
+ **옵션:**
165
+
166
+ - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
167
+ - 지원 언어: python, javascript, typescript, cpp
168
+
169
+ **예제:**
170
+
171
+ ```bash
172
+ ps submit # 현재 디렉토리에서 제출
173
+ ps submit 1000 # 1000번 문제 제출
174
+ ps submit --language python # Python으로 제출
74
175
  ```
75
176
 
76
- ### `solve` - 아카이빙
177
+ **설명:**
178
+
179
+ - 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
180
+ - solution.\* 파일을 자동으로 찾아 언어 감지
181
+ - 소스 코드를 클립보드에 자동 복사
182
+ - 제출 페이지를 브라우저로 자동 열기
183
+
184
+ ---
185
+
186
+ ### `archive` - 아카이빙
187
+
188
+ solving 디렉토리의 문제를 archive 디렉토리로 이동하고 Git 커밋을 생성합니다.
189
+
190
+ **사용법:**
77
191
 
78
- solving 디렉토리의 문제를 problem 디렉토리로 이동하고 Git 커밋을 생성합니다.
192
+ ```bash
193
+ ps archive [문제번호]
194
+ ```
195
+
196
+ **예제:**
79
197
 
80
198
  ```bash
81
- ps solve 1000
199
+ ps archive 1000 # 1000번 문제 아카이빙
200
+ ps archive # 현재 디렉토리에서 문제 번호 자동 감지
82
201
  ```
83
202
 
203
+ **설명:**
204
+
205
+ - solving 디렉토리에서 문제를 찾아 archive 디렉토리로 이동
206
+ - Git add 및 commit 실행
207
+ - 커밋 메시지: "solve: {문제번호} - {문제이름}"
208
+
209
+ ---
210
+
84
211
  ### `open` - 문제 페이지 열기
85
212
 
86
213
  백준 문제 페이지를 브라우저로 엽니다.
87
214
 
215
+ **사용법:**
216
+
88
217
  ```bash
89
- ps open 1000
218
+ ps open [문제번호]
90
219
  ```
91
220
 
221
+ **예제:**
222
+
223
+ ```bash
224
+ ps open 1000 # 1000번 문제 열기
225
+ ps open # 문제 디렉토리에서 실행 시 자동 추론
226
+ ```
227
+
228
+ **설명:**
229
+
230
+ - 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
231
+
232
+ ---
233
+
92
234
  ### `search` - 문제 검색
93
235
 
94
- solved.ac에서 문제를 검색합니다.
236
+ solved.ac에서 문제를 검색하거나 백준 문제집의 문제 목록을 표시합니다.
237
+
238
+ **사용법:**
239
+
240
+ ```bash
241
+ ps search <쿼리> [옵션]
242
+ ps search --workbook <문제집ID>
243
+ ```
244
+
245
+ **옵션:**
246
+
247
+ - `--workbook`: 문제집 ID를 지정하여 해당 문제집의 문제 목록을 표시
248
+
249
+ **예제:**
95
250
 
96
251
  ```bash
97
- ps search "*g1...g5"
98
- ps search --workbook 12345
252
+ ps search "*g1...g5" # Gold 1-5 문제 검색
253
+ ps search "tier:g1...g5" # Gold 1-5 문제 검색 (tier: 문법)
254
+ ps search "#dp" # DP 태그 문제 검색
255
+ ps search "tag:dp" # DP 태그 문제 검색 (tag: 문법)
256
+ ps search "*g1...g5 #dp" # Gold 1-5 티어의 DP 태그 문제 검색
257
+ ps search --workbook 25052 # 문제집 25052의 문제 목록 표시
99
258
  ```
100
259
 
260
+ **설명:**
261
+
262
+ - solved.ac 검색어 문법을 지원합니다
263
+ - 문제 목록에서 선택하면 자동으로 브라우저에서 문제 페이지를 엽니다
264
+ - 페이지네이션을 통해 여러 페이지의 결과를 탐색할 수 있습니다
265
+ - `--workbook` 옵션으로 백준 문제집의 문제 목록을 볼 수 있습니다
266
+
267
+ ---
268
+
101
269
  ### `stats` - 통계 조회
102
270
 
103
- Solved.ac 사용자 통계를 조회합니다.
271
+ Solved.ac에서 사용자 통계를 조회합니다.
272
+
273
+ **사용법:**
274
+
275
+ ```bash
276
+ ps stats [핸들] [옵션]
277
+ ```
278
+
279
+ **옵션:**
280
+
281
+ - `--handle`, `-h`: Solved.ac 핸들
282
+ - 설정에 저장된 값 사용 가능
283
+ - 인자로 전달하거나 플래그로 지정 가능
284
+
285
+ **예제:**
104
286
 
105
287
  ```bash
106
- ps stats
107
- ps stats myhandle
288
+ ps stats myhandle # myhandle의 통계 조회
289
+ ps stats --handle myhandle # 플래그로 핸들 지정
290
+ ps stats # 설정에 저장된 핸들 사용
108
291
  ```
109
292
 
293
+ **설명:**
294
+
295
+ - 티어, 레이팅, 해결한 문제 수 등 표시
296
+ - 그라데이션으로 시각적으로 표시
297
+ - 핸들 우선순위: 인자 > 플래그 > 설정 파일
298
+
299
+ ---
300
+
110
301
  ### `config` - 설정 관리
111
302
 
112
- 프로젝트 설정을 관리합니다.
303
+ 프로젝트 설정 파일(.ps-cli.json)을 관리합니다.
304
+
305
+ **사용법:**
113
306
 
114
307
  ```bash
115
- ps config list
116
- ps config set default-language python
117
- ps config get archive-strategy
308
+ ps config <명령어> [키] [값]
118
309
  ```
119
310
 
311
+ **명령어:**
312
+
313
+ - `get [키]`: 설정 값 조회 (키 없으면 대화형 선택)
314
+ - `set [키] [값]`: 설정 값 설정 (키/값 없으면 대화형 선택)
315
+ - `list`: 모든 설정 조회
316
+ - `clear`: .ps-cli.json 파일 삭제
317
+
318
+ **예제:**
319
+
320
+ ```bash
321
+ ps config get # 대화형으로 키 선택 후 값 조회
322
+ ps config get default-language # default-language 값 조회
323
+ ps config set # 대화형으로 키 선택 후 값 설정
324
+ ps config set editor cursor # editor를 cursor로 설정
325
+ ps config list # 모든 설정 조회
326
+ ps config clear # .ps-cli.json 파일 삭제
327
+ ```
328
+
329
+ **설명:**
330
+
331
+ - 설정은 현재 프로젝트의 .ps-cli.json 파일에 저장됩니다
332
+ - 대화형 모드로 키와 값을 선택할 수 있습니다
333
+
120
334
  ## 설정
121
335
 
122
336
  프로젝트 루트의 `.ps-cli.json` 파일에 저장됩니다.
@@ -127,7 +341,7 @@ ps config get archive-strategy
127
341
  - `editor`: 에디터 명령어 (code, cursor, vim 등)
128
342
  - `auto-open-editor`: fetch 후 자동으로 에디터 열기 (true/false)
129
343
  - `solved-ac-handle`: Solved.ac 핸들
130
- - `problem-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
344
+ - `archive-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
131
345
  - `solving-dir`: 푸는 중인 문제 디렉토리 (기본값: solving)
132
346
  - `archive-strategy`: 아카이빙 전략
133
347
 
@@ -173,7 +387,54 @@ ps config get archive-strategy
173
387
  3. **작성**: `solving/1000/`에서 코드 작성
174
388
  4. **테스트**: `ps test`로 로컬 테스트
175
389
  5. **제출**: `ps submit`으로 제출
176
- 6. **아카이빙**: `ps solve`로 problem 디렉토리로 이동
390
+ 6. **아카이빙**: `ps archive`로 archive 디렉토리로 이동
391
+
392
+ ## 개발
393
+
394
+ 로컬에서 개발하거나 테스트할 때는 글로벌로 설치된 `ps` 명령어와 충돌을 피하기 위해 다음 방법을 사용할 수 있습니다:
395
+
396
+ ### 방법 1: 절대 경로로 직접 실행 (외부 폴더 테스트 가능)
397
+
398
+ ```bash
399
+ # 빌드
400
+ bun run build
401
+
402
+ # 프로젝트 디렉토리에서 절대 경로로 실행
403
+ /path/to/ps-cli/dist/index.js init
404
+ /path/to/ps-cli/dist/index.js fetch 1000
405
+
406
+ # 또는 프로젝트 디렉토리로 이동 후
407
+ cd /path/to/ps-cli
408
+ node dist/index.js init
409
+ ```
410
+
411
+ ### 방법 2: npm link 사용 (주의 필요)
412
+
413
+ ```bash
414
+ # 프로젝트 디렉토리에서
415
+ bun run build
416
+ npm link
417
+
418
+ # 외부 폴더에서 테스트
419
+ cd /path/to/test-project
420
+ ps init # 로컬 버전이 사용됨
421
+
422
+ # 테스트 후 링크 해제
423
+ npm unlink -g @rhseung/ps-cli
424
+ ```
425
+
426
+ **주의:** `npm link`를 사용하면 글로벌 설치된 버전이 링크된 버전으로 대체됩니다.
427
+
428
+ ### 방법 3: 프로젝트 내에서만 테스트
429
+
430
+ ```bash
431
+ # 빌드
432
+ bun run build
433
+
434
+ # 프로젝트 디렉토리 내에서만
435
+ bun run ps init
436
+ bun run ps fetch 1000
437
+ ```
177
438
 
178
439
  ## 라이선스
179
440
 
@@ -9924,7 +9924,7 @@ var config = new Conf({
9924
9924
  autoOpenEditor: false,
9925
9925
  // 기본값: 자동 열기 비활성화
9926
9926
  solvedAcHandle: void 0,
9927
- problemDir: "problems",
9927
+ archiveDir: "problems",
9928
9928
  // 기본값: problems 디렉토리
9929
9929
  solvingDir: "solving"
9930
9930
  // 기본값: solving 디렉토리
@@ -10009,12 +10009,12 @@ function getSolvedAcHandle() {
10009
10009
  }
10010
10010
  return config.get("solvedAcHandle");
10011
10011
  }
10012
- function getProblemDir() {
10012
+ function getArchiveDir() {
10013
10013
  const projectConfig = getProjectConfigSync();
10014
- if (projectConfig?.problemDir !== void 0) {
10015
- return projectConfig.problemDir;
10014
+ if (projectConfig?.archiveDir !== void 0) {
10015
+ return projectConfig.archiveDir;
10016
10016
  }
10017
- return config.get("problemDir") ?? "problems";
10017
+ return config.get("archiveDir") ?? "problems";
10018
10018
  }
10019
10019
  function getSolvingDir() {
10020
10020
  const projectConfig = getProjectConfigSync();
@@ -10264,11 +10264,11 @@ function getArchiveSubPath(problemId, strategy = "flat", problem) {
10264
10264
  }
10265
10265
  }
10266
10266
  function detectProblemIdFromPath(cwd = process.cwd()) {
10267
- const problemDir = getProblemDir();
10267
+ const archiveDir = getArchiveDir();
10268
10268
  const solvingDir = getSolvingDir();
10269
10269
  const archiveStrategy = getArchiveStrategy();
10270
10270
  const normalizedPath = cwd.replace(/\\/g, "/");
10271
- const dirsToCheck = [problemDir, solvingDir].filter(
10271
+ const dirsToCheck = [archiveDir, solvingDir].filter(
10272
10272
  (dir) => dir && dir !== "." && dir !== ""
10273
10273
  );
10274
10274
  if (dirsToCheck.length === 0) {
@@ -10359,22 +10359,22 @@ function getProblemId(args, cwd = process.cwd()) {
10359
10359
  }
10360
10360
  return detectProblemIdFromPath(cwd);
10361
10361
  }
10362
- function getProblemDirPath(problemId, cwd = process.cwd(), problem) {
10363
- const problemDir = getProblemDir();
10362
+ function getArchiveDirPath(problemId, cwd = process.cwd(), problem) {
10363
+ const archiveDir = getArchiveDir();
10364
10364
  const archiveStrategy = getArchiveStrategy();
10365
10365
  const projectRoot = findProjectRoot(cwd);
10366
10366
  const baseDir = projectRoot || cwd;
10367
10367
  const subPath = getArchiveSubPath(problemId, archiveStrategy, problem);
10368
- if (problemDir === "." || problemDir === "") {
10368
+ if (archiveDir === "." || archiveDir === "") {
10369
10369
  if (subPath) {
10370
10370
  return join2(baseDir, subPath, problemId.toString());
10371
10371
  }
10372
10372
  return join2(baseDir, problemId.toString());
10373
10373
  }
10374
10374
  if (subPath) {
10375
- return join2(baseDir, problemDir, subPath, problemId.toString());
10375
+ return join2(baseDir, archiveDir, subPath, problemId.toString());
10376
10376
  }
10377
- return join2(baseDir, problemDir, problemId.toString());
10377
+ return join2(baseDir, archiveDir, problemId.toString());
10378
10378
  }
10379
10379
  function getSolvingDirPath(problemId, cwd = process.cwd(), _) {
10380
10380
  const solvingDir = getSolvingDir();
@@ -10408,27 +10408,27 @@ async function resolveProblemContext(args, options = {}) {
10408
10408
  );
10409
10409
  }
10410
10410
  const isCurrentDir = problemId === null || problemId !== null && currentPathProblemId === problemId;
10411
- let problemDir;
10411
+ let archiveDir;
10412
10412
  if (problemId && !isCurrentDir) {
10413
10413
  const solvingDirPath = getSolvingDirPath(problemId);
10414
- const problemDirPath = getProblemDirPath(problemId);
10414
+ const archiveDirPath = getArchiveDirPath(problemId);
10415
10415
  const solvingDirExists = await directoryExists(solvingDirPath);
10416
10416
  if (solvingDirExists) {
10417
- problemDir = solvingDirPath;
10417
+ archiveDir = solvingDirPath;
10418
10418
  } else {
10419
- const problemDirExists = await directoryExists(problemDirPath);
10420
- if (problemDirExists) {
10421
- problemDir = problemDirPath;
10419
+ const archiveDirExists = await directoryExists(archiveDirPath);
10420
+ if (archiveDirExists) {
10421
+ archiveDir = archiveDirPath;
10422
10422
  } else {
10423
- problemDir = solvingDirPath;
10423
+ archiveDir = solvingDirPath;
10424
10424
  }
10425
10425
  }
10426
10426
  } else {
10427
- problemDir = process.cwd();
10427
+ archiveDir = process.cwd();
10428
10428
  }
10429
10429
  return {
10430
10430
  problemId,
10431
- problemDir,
10431
+ archiveDir,
10432
10432
  isCurrentDir
10433
10433
  };
10434
10434
  }
@@ -10594,7 +10594,7 @@ export {
10594
10594
  getEditor,
10595
10595
  getAutoOpenEditor,
10596
10596
  getSolvedAcHandle,
10597
- getProblemDir,
10597
+ getArchiveDir,
10598
10598
  getSolvingDir,
10599
10599
  getArchiveStrategy,
10600
10600
  getNextTierMinRating,
@@ -10603,8 +10603,7 @@ export {
10603
10603
  getTierColor,
10604
10604
  getTierImageUrl,
10605
10605
  detectProblemIdFromPath,
10606
- getProblemId,
10607
- getProblemDirPath,
10606
+ getArchiveDirPath,
10608
10607
  getSolvingDirPath,
10609
10608
  resolveProblemContext,
10610
10609
  resolveLanguage,
@@ -4,26 +4,26 @@ import {
4
4
  CommandBuilder,
5
5
  CommandDef,
6
6
  findProjectRoot,
7
- getProblemDirPath,
8
- getProblemId,
7
+ getArchiveDirPath,
9
8
  getSolvingDir,
10
- getSolvingDirPath
11
- } from "../chunk-TNGUME4H.js";
9
+ getSolvingDirPath,
10
+ resolveProblemContext
11
+ } from "../chunk-F4LZ6ENP.js";
12
12
  import {
13
13
  __decorateClass
14
14
  } from "../chunk-7MQMPJ3X.js";
15
15
 
16
- // src/commands/solve.tsx
16
+ // src/commands/archive.tsx
17
17
  import { StatusMessage, Alert } from "@inkjs/ui";
18
18
  import { Spinner } from "@inkjs/ui";
19
19
  import { Box } from "ink";
20
20
 
21
- // src/hooks/use-solve.ts
21
+ // src/hooks/use-archive.ts
22
22
  import { access, readFile, rename, mkdir, readdir, rmdir } from "fs/promises";
23
23
  import { join, dirname } from "path";
24
24
  import { execa } from "execa";
25
25
  import { useEffect, useState } from "react";
26
- function useSolve({
26
+ function useArchive({
27
27
  problemId,
28
28
  onComplete
29
29
  }) {
@@ -33,7 +33,7 @@ function useSolve({
33
33
  const [message, setMessage] = useState("\uBB38\uC81C\uB97C \uC544\uCE74\uC774\uBE0C\uD558\uB294 \uC911...");
34
34
  const [error, setError] = useState(null);
35
35
  useEffect(() => {
36
- async function solve() {
36
+ async function archive() {
37
37
  try {
38
38
  const projectRoot = findProjectRoot();
39
39
  if (!projectRoot) {
@@ -71,22 +71,22 @@ function useSolve({
71
71
  }
72
72
  } catch {
73
73
  }
74
- const problemDir = getProblemDirPath(problemId, projectRoot, problem);
74
+ const archiveDir = getArchiveDirPath(problemId, projectRoot, problem);
75
75
  try {
76
- await access(problemDir);
76
+ await access(archiveDir);
77
77
  throw new Error(
78
- `problem \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC774\uBBF8 \uBB38\uC81C ${problemId}\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4.`
78
+ `archive \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC774\uBBF8 \uBB38\uC81C ${problemId}\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4.`
79
79
  );
80
80
  } catch (err) {
81
81
  if (err instanceof Error && err.message.includes("\uC774\uBBF8")) {
82
82
  throw err;
83
83
  }
84
84
  }
85
- const problemDirParent = dirname(problemDir);
85
+ const archiveDirParent = dirname(archiveDir);
86
86
  setMessage("\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC\uB97C \uC900\uBE44\uD558\uB294 \uC911...");
87
- await mkdir(problemDirParent, { recursive: true });
88
- setMessage("\uBB38\uC81C\uB97C problem \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD558\uB294 \uC911...");
89
- await rename(solvingDir, problemDir);
87
+ await mkdir(archiveDirParent, { recursive: true });
88
+ setMessage("\uBB38\uC81C\uB97C archive \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD558\uB294 \uC911...");
89
+ await rename(solvingDir, archiveDir);
90
90
  setMessage("\uBE48 \uB514\uB809\uD1A0\uB9AC \uC815\uB9AC \uC911...");
91
91
  try {
92
92
  const solvingDirConfig = getSolvingDir();
@@ -111,7 +111,7 @@ function useSolve({
111
111
  }
112
112
  setMessage("Git \uCEE4\uBC0B\uC744 \uC2E4\uD589\uD558\uB294 \uC911...");
113
113
  try {
114
- await execa("git", ["add", problemDir], { cwd: projectRoot });
114
+ await execa("git", ["add", archiveDir], { cwd: projectRoot });
115
115
  const commitMessage = `solve: ${problemId} - ${problemTitle}`;
116
116
  await execa("git", ["commit", "-m", commitMessage], {
117
117
  cwd: projectRoot
@@ -123,7 +123,7 @@ function useSolve({
123
123
  );
124
124
  }
125
125
  setStatus("success");
126
- setMessage(`\uBB38\uC81C ${problemId}\uB97C \uC544\uCE74\uC774\uBE0C\uD588\uC2B5\uB2C8\uB2E4: ${problemDir}`);
126
+ setMessage(`\uBB38\uC81C ${problemId}\uB97C \uC544\uCE74\uC774\uBE0C\uD588\uC2B5\uB2C8\uB2E4: ${archiveDir}`);
127
127
  setTimeout(() => {
128
128
  onComplete?.();
129
129
  }, 2e3);
@@ -135,7 +135,7 @@ function useSolve({
135
135
  }, 2e3);
136
136
  }
137
137
  }
138
- void solve();
138
+ void archive();
139
139
  }, [problemId, onComplete]);
140
140
  return {
141
141
  status,
@@ -144,10 +144,10 @@ function useSolve({
144
144
  };
145
145
  }
146
146
 
147
- // src/commands/solve.tsx
147
+ // src/commands/archive.tsx
148
148
  import { jsx, jsxs } from "react/jsx-runtime";
149
- function SolveView({ problemId, onComplete }) {
150
- const { status, message, error } = useSolve({
149
+ function ArchiveView({ problemId, onComplete }) {
150
+ const { status, message, error } = useArchive({
151
151
  problemId,
152
152
  onComplete
153
153
  });
@@ -162,40 +162,40 @@ function SolveView({ problemId, onComplete }) {
162
162
  }
163
163
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: "100%", children: /* @__PURE__ */ jsx(StatusMessage, { variant: "success", children: message }) });
164
164
  }
165
- var SolveCommand = class extends Command {
165
+ var ArchiveCommand = class extends Command {
166
166
  async execute(args, _) {
167
- const problemId = getProblemId(args);
168
- if (problemId === null) {
167
+ const context = await resolveProblemContext(args, { requireId: false });
168
+ if (context.problemId === null) {
169
169
  console.error("\uC624\uB958: \uBB38\uC81C \uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
170
- console.error(`\uC0AC\uC6A9\uBC95: ps solve <\uBB38\uC81C\uBC88\uD638>`);
171
- console.error(`\uB3C4\uC6C0\uB9D0: ps solve --help`);
170
+ console.error(`\uC0AC\uC6A9\uBC95: ps archive <\uBB38\uC81C\uBC88\uD638>`);
171
+ console.error(`\uB3C4\uC6C0\uB9D0: ps archive --help`);
172
172
  console.error(
173
173
  `\uD78C\uD2B8: solving/{\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.`
174
174
  );
175
175
  process.exit(1);
176
176
  return;
177
177
  }
178
- await this.renderView(SolveView, {
179
- problemId
178
+ await this.renderView(ArchiveView, {
179
+ problemId: context.problemId
180
180
  });
181
181
  }
182
182
  };
183
- SolveCommand = __decorateClass([
183
+ ArchiveCommand = __decorateClass([
184
184
  CommandDef({
185
- name: "solve",
185
+ name: "archive",
186
186
  description: `\uBB38\uC81C\uB97C \uC544\uCE74\uC774\uBE0C\uD558\uACE0 Git \uCEE4\uBC0B\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4.
187
- - solving \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C\uB97C \uCC3E\uC544 problem \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9
187
+ - solving \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C\uB97C \uCC3E\uC544 archive \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9
188
188
  - Git add \uBC0F commit \uC2E4\uD589 (\uCEE4\uBC0B \uBA54\uC2DC\uC9C0: "solve: {\uBB38\uC81C\uBC88\uD638} - {\uBB38\uC81C\uC774\uB984}")`,
189
189
  autoDetectProblemId: true,
190
190
  requireProblemId: false,
191
191
  examples: [
192
- "solve 1000",
193
- "solve # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C \uBC88\uD638 \uC790\uB3D9 \uAC10\uC9C0"
192
+ "archive 1000",
193
+ "archive # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C \uBC88\uD638 \uC790\uB3D9 \uAC10\uC9C0"
194
194
  ]
195
195
  })
196
- ], SolveCommand);
197
- var solve_default = CommandBuilder.fromClass(SolveCommand);
196
+ ], ArchiveCommand);
197
+ var archive_default = CommandBuilder.fromClass(ArchiveCommand);
198
198
  export {
199
- SolveCommand,
200
- solve_default as default
199
+ ArchiveCommand,
200
+ archive_default as default
201
201
  };
@@ -3,13 +3,13 @@ import {
3
3
  Command,
4
4
  CommandBuilder,
5
5
  CommandDef,
6
+ getArchiveDir,
6
7
  getArchiveStrategy,
7
8
  getAutoOpenEditor,
8
9
  getDefaultLanguage,
9
10
  getEditor,
10
- getProblemDir,
11
11
  getSolvedAcHandle
12
- } from "../chunk-TNGUME4H.js";
12
+ } from "../chunk-F4LZ6ENP.js";
13
13
  import {
14
14
  __decorateClass,
15
15
  getSupportedLanguages,
@@ -102,8 +102,8 @@ function useConfig({
102
102
  case "solved-ac-handle":
103
103
  updatedConfig.solvedAcHandle = value;
104
104
  break;
105
- case "problem-dir":
106
- updatedConfig.problemDir = value;
105
+ case "archive-dir":
106
+ updatedConfig.archiveDir = value;
107
107
  break;
108
108
  case "archive-strategy": {
109
109
  const validStrategies = ["flat", "by-range", "by-tier", "by-tag"];
@@ -159,7 +159,7 @@ function getConfigHelp() {
159
159
  editor \uC5D0\uB514\uD130 \uBA85\uB839\uC5B4 (\uC608: code, vim, nano)
160
160
  auto-open-editor fetch \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC5D0\uB514\uD130 \uC5F4\uAE30 (true/false)
161
161
  solved-ac-handle Solved.ac \uD578\uB4E4 (stats \uBA85\uB839\uC5B4\uC6A9)
162
- problem-dir \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C (\uAE30\uBCF8\uAC12: problems, "." \uB610\uB294 ""\uB294 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8)
162
+ 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
163
  archive-strategy \uC544\uCE74\uC774\uBE59 \uC804\uB7B5 (flat, by-range, by-tier, by-tag)
164
164
 
165
165
  \uC635\uC158:
@@ -180,7 +180,7 @@ var CONFIG_KEYS = [
180
180
  { label: "editor", value: "editor" },
181
181
  { label: "auto-open-editor", value: "auto-open-editor" },
182
182
  { label: "solved-ac-handle", value: "solved-ac-handle" },
183
- { label: "problem-dir", value: "problem-dir" },
183
+ { label: "archive-dir", value: "archive-dir" },
184
184
  { label: "archive-strategy", value: "archive-strategy" }
185
185
  ];
186
186
  function ConfigView({
@@ -213,7 +213,7 @@ function ConfigView({
213
213
  const editor = config?.editor ?? getEditor();
214
214
  const autoOpen = config?.autoOpenEditor ?? getAutoOpenEditor();
215
215
  const handle = config?.solvedAcHandle ?? getSolvedAcHandle();
216
- const problemDir = config?.problemDir ?? getProblemDir();
216
+ const archiveDir = config?.archiveDir ?? getArchiveDir();
217
217
  const archiveStrategy = config?.archiveStrategy ?? getArchiveStrategy();
218
218
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
219
219
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2699\uFE0F \uD604\uC7AC \uC124\uC815 (.ps-cli.json)" }) }),
@@ -239,9 +239,9 @@ function ConfigView({
239
239
  /* @__PURE__ */ jsx(Text, { bold: true, color: handle ? "cyan" : "gray", children: handle || "\uC124\uC815 \uC548 \uB428" })
240
240
  ] }),
241
241
  /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
242
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "problem-dir:" }),
242
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "archive-dir:" }),
243
243
  /* @__PURE__ */ jsx(Text, { children: " " }),
244
- /* @__PURE__ */ jsx(Text, { bold: true, children: problemDir })
244
+ /* @__PURE__ */ jsx(Text, { bold: true, children: archiveDir })
245
245
  ] }),
246
246
  /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
247
247
  /* @__PURE__ */ jsx(Text, { color: "gray", children: "archive-strategy:" }),
@@ -266,8 +266,8 @@ function ConfigView({
266
266
  case "solved-ac-handle":
267
267
  configValue = config?.solvedAcHandle ?? getSolvedAcHandle();
268
268
  break;
269
- case "problem-dir":
270
- configValue = config?.problemDir ?? getProblemDir();
269
+ case "archive-dir":
270
+ configValue = config?.archiveDir ?? getArchiveDir();
271
271
  break;
272
272
  case "archive-strategy":
273
273
  configValue = config?.archiveStrategy ?? getArchiveStrategy();
@@ -440,8 +440,8 @@ var ConfigCommand = class extends Command {
440
440
  return "true \uB610\uB294 false \uC785\uB825";
441
441
  case "solved-ac-handle":
442
442
  return "Solved.ac \uD578\uB4E4 \uC785\uB825";
443
- case "problem-dir":
444
- return "\uBB38\uC81C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C \uC785\uB825";
443
+ case "archive-dir":
444
+ return "\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C \uC785\uB825";
445
445
  case "archive-strategy":
446
446
  return "\uC544\uCE74\uC774\uBE59 \uC804\uB7B5 \uC785\uB825 (flat, by-range, by-tier, by-tag)";
447
447
  default:
@@ -458,8 +458,8 @@ var ConfigCommand = class extends Command {
458
458
  return "fetch \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC5D0\uB514\uD130\uB97C \uC5F4\uC9C0 \uC5EC\uBD80";
459
459
  case "solved-ac-handle":
460
460
  return "Solved.ac \uC0AC\uC6A9\uC790 \uD578\uB4E4";
461
- case "problem-dir":
462
- return '\uBB38\uC81C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C (\uAE30\uBCF8\uAC12: "problems", \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8: ".")';
461
+ case "archive-dir":
462
+ return '\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C (\uAE30\uBCF8\uAC12: "problems", \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8: ".")';
463
463
  case "archive-strategy":
464
464
  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)";
465
465
  default:
@@ -474,7 +474,7 @@ var ConfigCommand = class extends Command {
474
474
  return ["code", "cursor", "vim", "nano"];
475
475
  case "auto-open-editor":
476
476
  return ["true", "false"];
477
- case "problem-dir":
477
+ case "archive-dir":
478
478
  return ["problems", ".", ""];
479
479
  case "archive-strategy":
480
480
  return ["flat", "by-range", "by-tier", "by-tag"];
@@ -11,12 +11,12 @@ import {
11
11
  CommandDef,
12
12
  getAutoOpenEditor,
13
13
  getEditor,
14
- getProblemId,
15
14
  getSolvingDirPath,
16
15
  getTierColor,
17
16
  getTierImageUrl,
18
- getTierName
19
- } from "../chunk-TNGUME4H.js";
17
+ getTierName,
18
+ resolveProblemContext
19
+ } from "../chunk-F4LZ6ENP.js";
20
20
  import {
21
21
  __decorateClass,
22
22
  getLanguageConfig,
@@ -75,6 +75,27 @@ function parseTimeLimitToMs(timeLimit) {
75
75
  if (Number.isNaN(seconds)) return void 0;
76
76
  return Math.round(seconds * 1e3);
77
77
  }
78
+ function ensureTrailingNewline(content) {
79
+ if (!content || content.trim().length === 0) {
80
+ return content;
81
+ }
82
+ const trimmed = content.trimEnd();
83
+ if (trimmed.length === 0) {
84
+ return content;
85
+ }
86
+ const lines = trimmed.split("\n");
87
+ const lastLine = lines[lines.length - 1];
88
+ const isListItem = /^[\s]*[-*]\s/.test(lastLine) || /^[\s]*\d+[.)]\s/.test(lastLine);
89
+ const isTableRow = /^\s*\|.+\|\s*$/.test(lastLine);
90
+ const isCodeBlock = trimmed.endsWith("```");
91
+ if (isListItem || isTableRow || isCodeBlock) {
92
+ return trimmed + "\n";
93
+ }
94
+ if (!content.endsWith("\n")) {
95
+ return content + "\n";
96
+ }
97
+ return content;
98
+ }
78
99
  function getProjectRoot() {
79
100
  const __filename = fileURLToPath(import.meta.url);
80
101
  const __dirname = dirname(__filename);
@@ -150,16 +171,19 @@ ${separatorRow}
150
171
  ${valueRow}
151
172
  `;
152
173
  }
174
+ const description = ensureTrailingNewline(problem.description || "\uC124\uBA85 \uC5C6\uC74C");
175
+ const inputFormat = ensureTrailingNewline(
176
+ problem.inputFormat || "\uC785\uB825 \uD615\uC2DD \uC5C6\uC74C"
177
+ );
178
+ const outputFormat = ensureTrailingNewline(
179
+ problem.outputFormat || "\uCD9C\uB825 \uD615\uC2DD \uC5C6\uC74C"
180
+ );
153
181
  const readmeContent = `# [${problem.id}: ${problem.title}](https://www.acmicpc.net/problem/${problem.id})
154
182
 
155
183
  ${infoTable}## \uBB38\uC81C \uC124\uBA85
156
- ${problem.description || "\uC124\uBA85 \uC5C6\uC74C"}
157
-
158
- ## \uC785\uB825
159
- ${problem.inputFormat || "\uC785\uB825 \uD615\uC2DD \uC5C6\uC74C"}
160
-
161
- ## \uCD9C\uB825
162
- ${problem.outputFormat || "\uCD9C\uB825 \uD615\uC2DD \uC5C6\uC74C"}
184
+ ${description}## \uC785\uB825
185
+ ${inputFormat}## \uCD9C\uB825
186
+ ${outputFormat}
163
187
 
164
188
  ## \uC608\uC81C
165
189
  ${problem.testCases.map(
@@ -317,8 +341,8 @@ function FetchView({
317
341
  }
318
342
  var FetchCommand = class extends Command {
319
343
  async execute(args, flags) {
320
- const problemId = getProblemId(args);
321
- if (problemId === null) {
344
+ const context = await resolveProblemContext(args, { requireId: true });
345
+ if (context.problemId === null) {
322
346
  console.error("\uC624\uB958: \uBB38\uC81C \uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
323
347
  console.error(`\uC0AC\uC6A9\uBC95: ps fetch <\uBB38\uC81C\uBC88\uD638> [\uC635\uC158]`);
324
348
  console.error(`\uB3C4\uC6C0\uB9D0: ps fetch --help`);
@@ -338,7 +362,7 @@ var FetchCommand = class extends Command {
338
362
  return;
339
363
  }
340
364
  await this.renderView(FetchView, {
341
- problemId,
365
+ problemId: context.problemId,
342
366
  language: language || "python"
343
367
  });
344
368
  }
@@ -3,14 +3,14 @@ import {
3
3
  Command,
4
4
  CommandBuilder,
5
5
  CommandDef,
6
+ getArchiveDir,
6
7
  getArchiveStrategy,
7
8
  getAutoOpenEditor,
8
9
  getDefaultLanguage,
9
10
  getEditor,
10
- getProblemDir,
11
11
  getSolvedAcHandle,
12
12
  getSolvingDir
13
- } from "../chunk-TNGUME4H.js";
13
+ } from "../chunk-F4LZ6ENP.js";
14
14
  import {
15
15
  __decorateClass,
16
16
  getSupportedLanguages
@@ -32,11 +32,11 @@ import { join } from "path";
32
32
  import { execaCommand, execa } from "execa";
33
33
  import { useEffect, useState, useCallback } from "react";
34
34
  function useInit({ onComplete }) {
35
- const [currentStep, setCurrentStep] = useState("problem-dir");
35
+ const [currentStep, setCurrentStep] = useState("archive-dir");
36
36
  const [completedSteps, setCompletedSteps] = useState([]);
37
37
  const [confirmExit, setConfirmExit] = useState(false);
38
38
  const [initialized, setInitialized] = useState(false);
39
- const [problemDir, setProblemDirValue] = useState(getProblemDir());
39
+ const [archiveDir, setArchiveDirValue] = useState(getArchiveDir());
40
40
  const [solvingDir, setSolvingDirValue] = useState(getSolvingDir());
41
41
  const [archiveStrategy, setArchiveStrategy] = useState(getArchiveStrategy());
42
42
  const [language, setLanguage] = useState(getDefaultLanguage());
@@ -71,8 +71,8 @@ function useInit({ onComplete }) {
71
71
  await access(projectConfigPath);
72
72
  const configContent = await readFile(projectConfigPath, "utf-8");
73
73
  const projectConfig = JSON.parse(configContent);
74
- if (projectConfig.problemDir)
75
- setProblemDirValue(projectConfig.problemDir);
74
+ if (projectConfig.archiveDir)
75
+ setArchiveDirValue(projectConfig.archiveDir);
76
76
  if (projectConfig.solvingDir)
77
77
  setSolvingDirValue(projectConfig.solvingDir);
78
78
  if (projectConfig.archiveStrategy)
@@ -93,8 +93,8 @@ function useInit({ onComplete }) {
93
93
  }, []);
94
94
  const getStepLabel = useCallback((step) => {
95
95
  switch (step) {
96
- case "problem-dir":
97
- return "\uBB38\uC81C \uB514\uB809\uD1A0\uB9AC \uC124\uC815 (\uC544\uCE74\uC774\uBE0C\uB41C \uBB38\uC81C)";
96
+ case "archive-dir":
97
+ return "\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC \uC124\uC815 (\uC544\uCE74\uC774\uBE0C\uB41C \uBB38\uC81C)";
98
98
  case "solving-dir":
99
99
  return "Solving \uB514\uB809\uD1A0\uB9AC \uC124\uC815 (\uD478\uB294 \uC911\uC778 \uBB38\uC81C)";
100
100
  case "archive-strategy":
@@ -114,8 +114,8 @@ function useInit({ onComplete }) {
114
114
  const getStepValue = useCallback(
115
115
  (step) => {
116
116
  switch (step) {
117
- case "problem-dir":
118
- return problemDir === "." ? "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8" : problemDir;
117
+ case "archive-dir":
118
+ return archiveDir === "." ? "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8" : archiveDir;
119
119
  case "solving-dir":
120
120
  return solvingDir === "." ? "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8" : solvingDir;
121
121
  case "archive-strategy": {
@@ -140,13 +140,13 @@ function useInit({ onComplete }) {
140
140
  }
141
141
  },
142
142
  [
143
- problemDir,
143
+ archiveDir,
144
144
  solvingDir,
145
- archiveStrategy,
146
145
  language,
147
146
  editor,
148
147
  autoOpen,
149
- handle
148
+ handle,
149
+ archiveStrategy
150
150
  ]
151
151
  );
152
152
  const executeInit = useCallback(
@@ -155,7 +155,7 @@ function useInit({ onComplete }) {
155
155
  const cwd = process.cwd();
156
156
  const projectConfigPath = join(cwd, ".ps-cli.json");
157
157
  const projectConfig = {
158
- problemDir,
158
+ archiveDir,
159
159
  solvingDir,
160
160
  archiveStrategy,
161
161
  defaultLanguage: language,
@@ -172,11 +172,11 @@ function useInit({ onComplete }) {
172
172
  "utf-8"
173
173
  );
174
174
  setCreated((prev) => [...prev, ".ps-cli.json"]);
175
- if (problemDir !== "." && problemDir !== "") {
176
- const problemDirPath = join(cwd, problemDir);
175
+ if (archiveDir !== "." && archiveDir !== "") {
176
+ const archiveDirPath = join(cwd, archiveDir);
177
177
  try {
178
- await mkdir(problemDirPath, { recursive: true });
179
- setCreated((prev) => [...prev, `${problemDir}/`]);
178
+ await mkdir(archiveDirPath, { recursive: true });
179
+ setCreated((prev) => [...prev, `${archiveDir}/`]);
180
180
  } catch (err) {
181
181
  const error = err;
182
182
  if (error.code !== "EEXIST") {
@@ -280,7 +280,7 @@ ${gitignorePatterns.join("\n")}
280
280
  }
281
281
  },
282
282
  [
283
- problemDir,
283
+ archiveDir,
284
284
  solvingDir,
285
285
  archiveStrategy,
286
286
  language,
@@ -300,7 +300,7 @@ ${gitignorePatterns.join("\n")}
300
300
  setHandle(handleValue);
301
301
  }
302
302
  const stepOrder = [
303
- "problem-dir",
303
+ "archive-dir",
304
304
  "solving-dir",
305
305
  "archive-strategy",
306
306
  "language",
@@ -322,24 +322,14 @@ ${gitignorePatterns.join("\n")}
322
322
  }
323
323
  }
324
324
  },
325
- [
326
- currentStep,
327
- executeInit,
328
- problemDir,
329
- solvingDir,
330
- archiveStrategy,
331
- language,
332
- editor,
333
- autoOpen,
334
- onComplete
335
- ]
325
+ [currentStep, executeInit]
336
326
  );
337
327
  return {
338
328
  currentStep,
339
329
  completedSteps,
340
330
  confirmExit,
341
331
  initialized,
342
- problemDir,
332
+ archiveDir,
343
333
  solvingDir,
344
334
  archiveStrategy,
345
335
  language,
@@ -349,7 +339,7 @@ ${gitignorePatterns.join("\n")}
349
339
  handleInputMode,
350
340
  created,
351
341
  cancelled,
352
- setProblemDirValue,
342
+ setArchiveDirValue,
353
343
  setSolvingDirValue,
354
344
  setArchiveStrategy,
355
345
  setLanguage,
@@ -366,6 +356,22 @@ ${gitignorePatterns.join("\n")}
366
356
  };
367
357
  }
368
358
 
359
+ // src/utils/version.ts
360
+ import { readFileSync } from "fs";
361
+ import { join as join2, dirname } from "path";
362
+ import { fileURLToPath } from "url";
363
+ function getVersion() {
364
+ try {
365
+ const __filename = fileURLToPath(import.meta.url);
366
+ const __dirname = dirname(__filename);
367
+ const packageJsonPath = join2(__dirname, "../../package.json");
368
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
369
+ return packageJson.version;
370
+ } catch {
371
+ return "";
372
+ }
373
+ }
374
+
369
375
  // src/commands/init.tsx
370
376
  import { jsx, jsxs } from "react/jsx-runtime";
371
377
  function InitView({ onComplete }) {
@@ -377,7 +383,7 @@ function InitView({ onComplete }) {
377
383
  handleInputMode,
378
384
  created,
379
385
  cancelled,
380
- setProblemDirValue,
386
+ setArchiveDirValue,
381
387
  setSolvingDirValue,
382
388
  setArchiveStrategy,
383
389
  setLanguage,
@@ -424,7 +430,7 @@ function InitView({ onComplete }) {
424
430
  ] });
425
431
  }
426
432
  switch (currentStep) {
427
- case "problem-dir": {
433
+ case "archive-dir": {
428
434
  const options = [
429
435
  { label: "problems", value: "problems" },
430
436
  { label: ". (\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8)", value: "." }
@@ -436,7 +442,7 @@ function InitView({ onComplete }) {
436
442
  {
437
443
  options,
438
444
  onChange: (value) => {
439
- setProblemDirValue(value);
445
+ setArchiveDirValue(value);
440
446
  const displayValue = value === "." ? "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8" : value;
441
447
  moveToNextStep(displayValue, getStepLabel(currentStep));
442
448
  }
@@ -626,8 +632,16 @@ ${created.map((item) => `\u2022 ${item}`).join("\n")}` : "";
626
632
  if (!initialized) {
627
633
  return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "\uB85C\uB529 \uC911..." }) });
628
634
  }
635
+ const version = getVersion();
629
636
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
630
- /* @__PURE__ */ jsx(Box, { marginBottom: completedSteps.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u{1F680} ps-cli \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654" }) }),
637
+ /* @__PURE__ */ jsxs(Box, { marginBottom: completedSteps.length > 0 ? 1 : 0, children: [
638
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u{1F680} ps-cli \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654" }),
639
+ version && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
640
+ " ",
641
+ "v",
642
+ version
643
+ ] })
644
+ ] }),
631
645
  completedSteps.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: completedSteps.map((step, idx) => /* @__PURE__ */ jsxs(StatusMessage, { variant: "success", children: [
632
646
  step.label,
633
647
  ": ",
@@ -7,9 +7,8 @@ import {
7
7
  Command,
8
8
  CommandBuilder,
9
9
  CommandDef,
10
- getProblemId,
11
10
  resolveProblemContext
12
- } from "../chunk-TNGUME4H.js";
11
+ } from "../chunk-F4LZ6ENP.js";
13
12
  import {
14
13
  __decorateClass
15
14
  } from "../chunk-7MQMPJ3X.js";
@@ -63,11 +62,7 @@ function OpenView({ problemId, onComplete }) {
63
62
  }
64
63
  var OpenCommand = class extends Command {
65
64
  async execute(args, _flags) {
66
- const problemId = getProblemId(args);
67
- const context = await resolveProblemContext(
68
- problemId !== null ? [problemId.toString()] : [],
69
- { requireId: true }
70
- );
65
+ const context = await resolveProblemContext(args, { requireId: true });
71
66
  if (context.problemId === null) {
72
67
  console.error("\uC624\uB958: \uBB38\uC81C \uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
73
68
  console.error(`\uC0AC\uC6A9\uBC95: ps open <\uBB38\uC81C\uBC88\uD638>`);
@@ -6,10 +6,9 @@ import {
6
6
  Command,
7
7
  CommandBuilder,
8
8
  CommandDef,
9
- getProblemId,
10
9
  resolveLanguage,
11
10
  resolveProblemContext
12
- } from "../chunk-TNGUME4H.js";
11
+ } from "../chunk-F4LZ6ENP.js";
13
12
  import {
14
13
  __decorateClass,
15
14
  getSupportedLanguagesString
@@ -123,17 +122,14 @@ function RunView({
123
122
  }
124
123
  var RunCommand = class extends Command {
125
124
  async execute(args, flags) {
126
- const problemId = getProblemId(args);
127
- const context = await resolveProblemContext(
128
- problemId !== null ? [problemId.toString()] : []
129
- );
130
- const inputPath = flags.input ? join(context.problemDir, flags.input) : await this.findInputFile(context.problemDir);
125
+ const context = await resolveProblemContext(args);
126
+ const inputPath = flags.input ? join(context.archiveDir, flags.input) : await this.findInputFile(context.archiveDir);
131
127
  const detectedLanguage = await resolveLanguage(
132
- context.problemDir,
128
+ context.archiveDir,
133
129
  flags.language
134
130
  );
135
131
  await this.renderView(RunView, {
136
- problemDir: context.problemDir,
132
+ problemDir: context.archiveDir,
137
133
  language: detectedLanguage,
138
134
  inputFile: inputPath
139
135
  });
@@ -16,10 +16,10 @@ import {
16
16
  Command,
17
17
  CommandBuilder,
18
18
  CommandDef,
19
- getProblemDirPath,
19
+ getArchiveDirPath,
20
20
  getTierColor,
21
21
  getTierName
22
- } from "../chunk-TNGUME4H.js";
22
+ } from "../chunk-F4LZ6ENP.js";
23
23
  import {
24
24
  __decorateClass
25
25
  } from "../chunk-7MQMPJ3X.js";
@@ -270,7 +270,7 @@ function WorkbookSearchView({
270
270
  ] });
271
271
  }
272
272
  const problemsWithSolvedStatus = problems.map((problem) => {
273
- const problemDirPath = getProblemDirPath(problem.problemId);
273
+ const problemDirPath = getArchiveDirPath(problem.problemId);
274
274
  const isSolved = existsSync(problemDirPath);
275
275
  return {
276
276
  problemId: problem.problemId,
@@ -362,7 +362,7 @@ function SearchView({ query, onComplete }) {
362
362
  const searchResults = await searchProblems(query, currentPage);
363
363
  const resultsWithSolvedStatus = searchResults.problems.map(
364
364
  (problem) => {
365
- const problemDirPath = getProblemDirPath(problem.problemId);
365
+ const problemDirPath = getArchiveDirPath(problem.problemId);
366
366
  const isSolved = existsSync(problemDirPath);
367
367
  return {
368
368
  ...problem,
@@ -14,7 +14,7 @@ import {
14
14
  getSolvedAcHandle,
15
15
  getTierColor,
16
16
  getTierName
17
- } from "../chunk-TNGUME4H.js";
17
+ } from "../chunk-F4LZ6ENP.js";
18
18
  import {
19
19
  __decorateClass
20
20
  } from "../chunk-7MQMPJ3X.js";
@@ -8,10 +8,9 @@ import {
8
8
  CommandDef,
9
9
  detectProblemIdFromPath,
10
10
  findSolutionFile,
11
- getProblemId,
12
11
  resolveLanguage,
13
12
  resolveProblemContext
14
- } from "../chunk-TNGUME4H.js";
13
+ } from "../chunk-F4LZ6ENP.js";
15
14
  import {
16
15
  __decorateClass,
17
16
  getSupportedLanguagesString
@@ -195,19 +194,15 @@ function SubmitView({
195
194
  }
196
195
  var SubmitCommand = class extends Command {
197
196
  async execute(args, flags) {
198
- const problemId = getProblemId(args);
199
- const context = await resolveProblemContext(
200
- problemId !== null ? [problemId.toString()] : [],
201
- { requireId: true }
202
- );
203
- const sourcePath = await findSolutionFile(context.problemDir);
197
+ const context = await resolveProblemContext(args, { requireId: true });
198
+ const sourcePath = await findSolutionFile(context.archiveDir);
204
199
  const detectedLanguage = await resolveLanguage(
205
- context.problemDir,
200
+ context.archiveDir,
206
201
  flags.language
207
202
  );
208
203
  let finalProblemId = context.problemId;
209
204
  if (finalProblemId === null) {
210
- finalProblemId = detectProblemIdFromPath(context.problemDir);
205
+ finalProblemId = detectProblemIdFromPath(context.archiveDir);
211
206
  }
212
207
  if (finalProblemId === null) {
213
208
  throw new Error(
@@ -6,10 +6,9 @@ import {
6
6
  Command,
7
7
  CommandBuilder,
8
8
  CommandDef,
9
- getProblemId,
10
9
  resolveLanguage,
11
10
  resolveProblemContext
12
- } from "../chunk-TNGUME4H.js";
11
+ } from "../chunk-F4LZ6ENP.js";
13
12
  import {
14
13
  __decorateClass,
15
14
  getSupportedLanguagesString
@@ -317,16 +316,13 @@ function TestView({
317
316
  }
318
317
  var TestCommand = class extends Command {
319
318
  async execute(args, flags) {
320
- const problemId = getProblemId(args);
321
- const context = await resolveProblemContext(
322
- problemId !== null && problemId !== void 0 ? [problemId.toString()] : []
323
- );
319
+ const context = await resolveProblemContext(args);
324
320
  const language = await resolveLanguage(
325
- context.problemDir,
321
+ context.archiveDir,
326
322
  flags.language
327
323
  );
328
324
  await this.renderView(TestView, {
329
- problemDir: context.problemDir,
325
+ problemDir: context.archiveDir,
330
326
  language,
331
327
  watch: Boolean(flags.watch),
332
328
  timeoutMs: flags.timeoutMs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhseung/ps-cli",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "백준(BOJ) 문제 해결을 위한 통합 CLI 도구",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,11 +8,14 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "templates"
11
+ "templates",
12
+ "LICENSE"
12
13
  ],
13
14
  "scripts": {
14
15
  "build": "tsup && node scripts/add-shebang.js",
16
+ "prepublishOnly": "npm run build",
15
17
  "dev": "tsup --watch",
18
+ "ps": "node dist/index.js",
16
19
  "typecheck": "tsc --noEmit",
17
20
  "lint": "eslint",
18
21
  "format": "prettier --check .",