@rhseung/ps-cli 1.7.3 → 1.7.5

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,316 @@ 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 [문제번호] [옵션]
56
91
  ```
57
92
 
93
+ **옵션:**
94
+
95
+ - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
96
+ - 지원 언어: python, javascript, typescript, cpp
97
+ - `--watch`, `-w`: watch 모드 (파일 변경 시 자동 재테스트)
98
+ - solution._, testcases/\*\*/_.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으로 테스트
108
+ ```
109
+
110
+ **설명:**
111
+
112
+ - 현재 디렉토리 또는 지정한 문제 번호의 테스트 실행
113
+ - solution.\* 파일을 자동으로 찾아 언어 감지
114
+ - testcases/{번호}/input.txt와 testcases/{번호}/output.txt 파일을 기반으로 테스트
115
+ - 문제의 시간 제한을 자동으로 적용
116
+
117
+ ---
118
+
58
119
  ### `run` - 코드 실행
59
120
 
60
121
  테스트 없이 코드를 실행합니다.
61
122
 
123
+ **사용법:**
124
+
62
125
  ```bash
63
- ps run
64
- ps run 1000
126
+ ps run [문제번호] [옵션]
65
127
  ```
66
128
 
129
+ **옵션:**
130
+
131
+ - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
132
+ - 지원 언어: python, javascript, typescript, cpp
133
+ - `--input`, `-i`: 입력 파일 지정
134
+ - 숫자만 입력 시 testcases/{숫자}/input.txt로 자동 변환 (예: `--input 1`)
135
+ - 전체 경로도 지원 (예: testcases/1/input.txt)
136
+
137
+ **예제:**
138
+
139
+ ```bash
140
+ ps run # 현재 디렉토리에서 표준 입력으로 실행
141
+ ps run 1000 # 1000번 문제 표준 입력으로 실행
142
+ ps run --language python # Python으로 표준 입력으로 실행
143
+ ps run --input 1 # 테스트 케이스 1번 사용
144
+ ps run --input testcases/1/input.txt # 전체 경로로 입력 파일 지정
145
+ ```
146
+
147
+ **설명:**
148
+
149
+ - 현재 디렉토리 또는 지정한 문제 번호의 코드 실행
150
+ - solution.\* 파일을 자동으로 찾아 언어 감지
151
+ - --input 옵션으로 입력 파일 지정 가능 (예: `--input 1` 또는 `--input testcases/1/input.txt`)
152
+ - 옵션 없이 실행 시 표준 입력으로 입력 받기
153
+ - 테스트 케이스 검증 없이 단순 실행
154
+
155
+ ---
156
+
67
157
  ### `submit` - 제출
68
158
 
69
159
  백준 제출 페이지를 열고 소스 코드를 클립보드에 복사합니다.
70
160
 
161
+ **사용법:**
162
+
71
163
  ```bash
72
- ps submit
73
- ps submit 1000
164
+ ps submit [문제번호] [옵션]
74
165
  ```
75
166
 
76
- ### `solve` - 아카이빙
167
+ **옵션:**
77
168
 
78
- solving 디렉토리의 문제를 problem 디렉토리로 이동하고 Git 커밋을 생성합니다.
169
+ - `--language`, `-l`: 언어 선택 (지정 자동 감지 무시)
170
+ - 지원 언어: python, javascript, typescript, cpp
171
+
172
+ **예제:**
79
173
 
80
174
  ```bash
81
- ps solve 1000
175
+ ps submit # 현재 디렉토리에서 제출
176
+ ps submit 1000 # 1000번 문제 제출
177
+ ps submit --language python # Python으로 제출
82
178
  ```
83
179
 
180
+ **설명:**
181
+
182
+ - 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
183
+ - solution.\* 파일을 자동으로 찾아 언어 감지
184
+ - 소스 코드를 클립보드에 자동 복사
185
+ - 제출 페이지를 브라우저로 자동 열기
186
+
187
+ ---
188
+
189
+ ### `archive` - 아카이빙
190
+
191
+ solving 디렉토리의 문제를 archive 디렉토리로 이동하고 Git 커밋을 생성합니다.
192
+
193
+ **사용법:**
194
+
195
+ ```bash
196
+ ps archive [문제번호]
197
+ ```
198
+
199
+ **예제:**
200
+
201
+ ```bash
202
+ ps archive 1000 # 1000번 문제 아카이빙
203
+ ps archive # 현재 디렉토리에서 문제 번호 자동 감지
204
+ ```
205
+
206
+ **설명:**
207
+
208
+ - solving 디렉토리에서 문제를 찾아 archive 디렉토리로 이동
209
+ - Git add 및 commit 실행
210
+ - 커밋 메시지: "solve: {문제번호} - {문제이름}"
211
+
212
+ ---
213
+
84
214
  ### `open` - 문제 페이지 열기
85
215
 
86
216
  백준 문제 페이지를 브라우저로 엽니다.
87
217
 
218
+ **사용법:**
219
+
88
220
  ```bash
89
- ps open 1000
221
+ ps open [문제번호]
90
222
  ```
91
223
 
224
+ **예제:**
225
+
226
+ ```bash
227
+ ps open 1000 # 1000번 문제 열기
228
+ ps open # 문제 디렉토리에서 실행 시 자동 추론
229
+ ```
230
+
231
+ **설명:**
232
+
233
+ - 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
234
+
235
+ ---
236
+
92
237
  ### `search` - 문제 검색
93
238
 
94
- solved.ac에서 문제를 검색합니다.
239
+ solved.ac에서 문제를 검색하거나 백준 문제집의 문제 목록을 표시합니다.
240
+
241
+ **사용법:**
95
242
 
96
243
  ```bash
97
- ps search "*g1...g5"
98
- ps search --workbook 12345
244
+ ps search <쿼리> [옵션]
245
+ ps search --workbook <문제집ID>
99
246
  ```
100
247
 
248
+ **옵션:**
249
+
250
+ - `--workbook`: 문제집 ID를 지정하여 해당 문제집의 문제 목록을 표시
251
+
252
+ **예제:**
253
+
254
+ ```bash
255
+ ps search "*g1...g5" # Gold 1-5 문제 검색
256
+ ps search "tier:g1...g5" # Gold 1-5 문제 검색 (tier: 문법)
257
+ ps search "#dp" # DP 태그 문제 검색
258
+ ps search "tag:dp" # DP 태그 문제 검색 (tag: 문법)
259
+ ps search "*g1...g5 #dp" # Gold 1-5 티어의 DP 태그 문제 검색
260
+ ps search --workbook 25052 # 문제집 25052의 문제 목록 표시
261
+ ```
262
+
263
+ **설명:**
264
+
265
+ - solved.ac 검색어 문법을 지원합니다
266
+ - 문제 목록에서 선택하면 자동으로 브라우저에서 문제 페이지를 엽니다
267
+ - 페이지네이션을 통해 여러 페이지의 결과를 탐색할 수 있습니다
268
+ - `--workbook` 옵션으로 백준 문제집의 문제 목록을 볼 수 있습니다
269
+
270
+ ---
271
+
101
272
  ### `stats` - 통계 조회
102
273
 
103
- Solved.ac 사용자 통계를 조회합니다.
274
+ Solved.ac에서 사용자 통계를 조회합니다.
275
+
276
+ **사용법:**
104
277
 
105
278
  ```bash
106
- ps stats
107
- ps stats myhandle
279
+ ps stats [핸들] [옵션]
108
280
  ```
109
281
 
282
+ **옵션:**
283
+
284
+ - `--handle`, `-h`: Solved.ac 핸들
285
+ - 설정에 저장된 값 사용 가능
286
+ - 인자로 전달하거나 플래그로 지정 가능
287
+
288
+ **예제:**
289
+
290
+ ```bash
291
+ ps stats myhandle # myhandle의 통계 조회
292
+ ps stats --handle myhandle # 플래그로 핸들 지정
293
+ ps stats # 설정에 저장된 핸들 사용
294
+ ```
295
+
296
+ **설명:**
297
+
298
+ - 티어, 레이팅, 해결한 문제 수 등 표시
299
+ - 그라데이션으로 시각적으로 표시
300
+ - 핸들 우선순위: 인자 > 플래그 > 설정 파일
301
+
302
+ ---
303
+
110
304
  ### `config` - 설정 관리
111
305
 
112
- 프로젝트 설정을 관리합니다.
306
+ 프로젝트 설정 파일(.ps-cli.json)을 관리합니다.
307
+
308
+ **사용법:**
113
309
 
114
310
  ```bash
115
- ps config list
116
- ps config set default-language python
117
- ps config get archive-strategy
311
+ ps config <명령어> [키] [값]
118
312
  ```
119
313
 
314
+ **명령어:**
315
+
316
+ - `get [키]`: 설정 값 조회 (키 없으면 대화형 선택)
317
+ - `set [키] [값]`: 설정 값 설정 (키/값 없으면 대화형 선택)
318
+ - `list`: 모든 설정 조회
319
+ - `clear`: .ps-cli.json 파일 삭제
320
+
321
+ **예제:**
322
+
323
+ ```bash
324
+ ps config get # 대화형으로 키 선택 후 값 조회
325
+ ps config get default-language # default-language 값 조회
326
+ ps config set # 대화형으로 키 선택 후 값 설정
327
+ ps config set editor cursor # editor를 cursor로 설정
328
+ ps config list # 모든 설정 조회
329
+ ps config clear # .ps-cli.json 파일 삭제
330
+ ```
331
+
332
+ **설명:**
333
+
334
+ - 설정은 현재 프로젝트의 .ps-cli.json 파일에 저장됩니다
335
+ - 대화형 모드로 키와 값을 선택할 수 있습니다
336
+
120
337
  ## 설정
121
338
 
122
339
  프로젝트 루트의 `.ps-cli.json` 파일에 저장됩니다.
@@ -127,7 +344,7 @@ ps config get archive-strategy
127
344
  - `editor`: 에디터 명령어 (code, cursor, vim 등)
128
345
  - `auto-open-editor`: fetch 후 자동으로 에디터 열기 (true/false)
129
346
  - `solved-ac-handle`: Solved.ac 핸들
130
- - `problem-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
347
+ - `archive-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
131
348
  - `solving-dir`: 푸는 중인 문제 디렉토리 (기본값: solving)
132
349
  - `archive-strategy`: 아카이빙 전략
133
350
 
@@ -173,7 +390,43 @@ ps config get archive-strategy
173
390
  3. **작성**: `solving/1000/`에서 코드 작성
174
391
  4. **테스트**: `ps test`로 로컬 테스트
175
392
  5. **제출**: `ps submit`으로 제출
176
- 6. **아카이빙**: `ps solve`로 problem 디렉토리로 이동
393
+ 6. **아카이빙**: `ps archive`로 archive 디렉토리로 이동
394
+
395
+ ## 개발
396
+
397
+ 로컬에서 개발하거나 테스트할 때는 글로벌로 설치된 `ps` 명령어와 충돌을 피하기 위해 다음 방법을 사용할 수 있습니다:
398
+
399
+ ### 방법 1: 절대 경로로 직접 실행 (외부 폴더 테스트 가능)
400
+
401
+ ```bash
402
+ # 빌드
403
+ bun run build
404
+
405
+ # 프로젝트 디렉토리에서 절대 경로로 실행
406
+ /path/to/ps-cli/dist/index.js init
407
+ /path/to/ps-cli/dist/index.js fetch 1000
408
+
409
+ # 또는 프로젝트 디렉토리로 이동 후
410
+ cd /path/to/ps-cli
411
+ node dist/index.js init
412
+ ```
413
+
414
+ ### 방법 2: npm link 사용 (주의 필요)
415
+
416
+ ```bash
417
+ # 프로젝트 디렉토리에서
418
+ bun run build
419
+ npm link
420
+
421
+ # 외부 폴더에서 테스트
422
+ cd /path/to/test-project
423
+ ps init # 로컬 버전이 사용됨
424
+
425
+ # 테스트 후 링크 해제
426
+ npm unlink -g @rhseung/ps-cli
427
+ ```
428
+
429
+ **주의:** `npm link`를 사용하면 글로벌 설치된 버전이 링크된 버전으로 대체됩니다.
177
430
 
178
431
  ## 라이선스
179
432
 
@@ -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,
@@ -6,7 +6,23 @@ import {
6
6
  // src/services/runner.ts
7
7
  import { readFile } from "fs/promises";
8
8
  import { join } from "path";
9
+ import { createInterface } from "readline";
9
10
  import { execa, execaCommand } from "execa";
11
+ async function readStdin() {
12
+ return new Promise((resolve) => {
13
+ const lines = [];
14
+ const rl = createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout
17
+ });
18
+ rl.on("line", (line) => {
19
+ lines.push(line);
20
+ });
21
+ rl.on("close", () => {
22
+ resolve(lines.join("\n"));
23
+ });
24
+ });
25
+ }
10
26
  async function runSolution({
11
27
  problemDir,
12
28
  language,
@@ -16,7 +32,15 @@ async function runSolution({
16
32
  const langConfig = getLanguageConfig(language);
17
33
  const solutionFile = `solution.${langConfig.extension}`;
18
34
  const solutionPath = join(problemDir, solutionFile);
19
- const input = await readFile(inputPath, "utf-8");
35
+ let input;
36
+ let capturedInput;
37
+ if (inputPath) {
38
+ input = await readFile(inputPath, "utf-8");
39
+ capturedInput = input;
40
+ } else {
41
+ capturedInput = await readStdin();
42
+ input = capturedInput;
43
+ }
20
44
  const start = Date.now();
21
45
  try {
22
46
  if (langConfig.compileCommand) {
@@ -27,7 +51,7 @@ async function runSolution({
27
51
  }
28
52
  const child = execa(langConfig.runCommand, [solutionPath], {
29
53
  cwd: problemDir,
30
- input,
54
+ ...input !== void 0 ? { input } : { stdin: "inherit" },
31
55
  timeout: timeoutMs
32
56
  });
33
57
  const result = await child;
@@ -39,7 +63,8 @@ async function runSolution({
39
63
  stderr,
40
64
  exitCode,
41
65
  timedOut: false,
42
- durationMs
66
+ durationMs,
67
+ ...capturedInput !== void 0 && { input: capturedInput }
43
68
  };
44
69
  } catch (error) {
45
70
  const durationMs = Date.now() - start;
@@ -50,7 +75,8 @@ async function runSolution({
50
75
  stderr: err.stderr ?? err.shortMessage ?? err.message,
51
76
  exitCode: err.exitCode ?? null,
52
77
  timedOut: Boolean(err.timedOut),
53
- durationMs
78
+ durationMs,
79
+ ...capturedInput !== void 0 && { input: capturedInput }
54
80
  };
55
81
  }
56
82
  return {