@rhseung/ps-cli 1.7.4 → 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
@@ -95,7 +95,7 @@ ps test [문제번호] [옵션]
95
95
  - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
96
96
  - 지원 언어: python, javascript, typescript, cpp
97
97
  - `--watch`, `-w`: watch 모드 (파일 변경 시 자동 재테스트)
98
- - solution._, input_.txt, output\*.txt 파일 변경 감지
98
+ - solution._, testcases/\*\*/_.txt 파일 변경 감지
99
99
 
100
100
  **예제:**
101
101
 
@@ -111,7 +111,7 @@ ps test --language python # Python으로 테스트
111
111
 
112
112
  - 현재 디렉토리 또는 지정한 문제 번호의 테스트 실행
113
113
  - solution.\* 파일을 자동으로 찾아 언어 감지
114
- - input*.txt와 output*.txt 파일을 기반으로 테스트
114
+ - testcases/{번호}/input.txt와 testcases/{번호}/output.txt 파일을 기반으로 테스트
115
115
  - 문제의 시간 제한을 자동으로 적용
116
116
 
117
117
  ---
@@ -131,22 +131,25 @@ ps run [문제번호] [옵션]
131
131
  - `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
132
132
  - 지원 언어: python, javascript, typescript, cpp
133
133
  - `--input`, `-i`: 입력 파일 지정
134
- - 기본값: input.txt 또는 input1.txt
134
+ - 숫자만 입력 시 testcases/{숫자}/input.txt 자동 변환 (예: `--input 1`)
135
+ - 전체 경로도 지원 (예: testcases/1/input.txt)
135
136
 
136
137
  **예제:**
137
138
 
138
139
  ```bash
139
- ps run # 현재 디렉토리에서 실행
140
- ps run 1000 # 1000번 문제 실행
141
- ps run --language python # Python으로 실행
142
- ps run --input input2.txt # 특정 입력 파일 사용
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 # 전체 경로로 입력 파일 지정
143
145
  ```
144
146
 
145
147
  **설명:**
146
148
 
147
149
  - 현재 디렉토리 또는 지정한 문제 번호의 코드 실행
148
150
  - solution.\* 파일을 자동으로 찾아 언어 감지
149
- - input.txt 또는 input1.txt를 표준 입력으로 사용
151
+ - --input 옵션으로 입력 파일 지정 가능 (예: `--input 1` 또는 `--input testcases/1/input.txt`)
152
+ - 옵션 없이 실행 시 표준 입력으로 입력 받기
150
153
  - 테스트 케이스 검증 없이 단순 실행
151
154
 
152
155
  ---
@@ -185,7 +188,7 @@ ps submit --language python # Python으로 제출
185
188
 
186
189
  ### `archive` - 아카이빙
187
190
 
188
- solving 디렉토리의 문제를 archive 디렉토리로 이동하고 Git 커밋을 생성합니다.
191
+ solving 디렉토리의 문제를 archive 디렉토리로 이동하고 (선택적으로) Git 커밋을 생성합니다.
189
192
 
190
193
  **사용법:**
191
194
 
@@ -203,31 +206,42 @@ ps archive # 현재 디렉토리에서 문제 번호 자동
203
206
  **설명:**
204
207
 
205
208
  - solving 디렉토리에서 문제를 찾아 archive 디렉토리로 이동
206
- - Git add 및 commit 실행
207
- - 커밋 메시지: "solve: {문제번호} - {문제이름}"
209
+ - 설정에 따라 Git add 및 commit 실행
210
+ - 기본 커밋 메시지: "solve: {id} - {title}"
211
+ - `archive-auto-commit` 이 `true` 인 경우, **archive 디렉토리로 이동 후** Git 커밋을 시도하며, 커밋 실패 시 다시 원래 위치로 되돌림 (롤백)
212
+ - `archive-auto-commit` 이 `false` 인 경우, Git 커밋 없이 디렉토리만 이동
208
213
 
209
214
  ---
210
215
 
211
216
  ### `open` - 문제 페이지 열기
212
217
 
213
- 백준 문제 페이지를 브라우저로 엽니다.
218
+ 백준 문제 페이지 또는 문제집 페이지를 브라우저로 엽니다.
214
219
 
215
220
  **사용법:**
216
221
 
217
222
  ```bash
218
223
  ps open [문제번호]
224
+ ps open --workbook <문제집ID>
225
+ ps open -w <문제집ID>
219
226
  ```
220
227
 
228
+ **옵션:**
229
+
230
+ - `--workbook`, `-w`: 문제집 ID를 지정하여 해당 문제집 페이지를 엽니다
231
+
221
232
  **예제:**
222
233
 
223
234
  ```bash
224
235
  ps open 1000 # 1000번 문제 열기
225
236
  ps open # 문제 디렉토리에서 실행 시 자동 추론
237
+ ps open --workbook 25052 # 문제집 25052 열기
238
+ ps open -w 25052 # 문제집 25052 열기 (단축 옵션)
226
239
  ```
227
240
 
228
241
  **설명:**
229
242
 
230
243
  - 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
244
+ - `--workbook` 또는 `-w` 옵션으로 문제집 페이지를 열 수 있습니다
231
245
 
232
246
  ---
233
247
 
@@ -344,6 +358,8 @@ ps config clear # .ps-cli.json 파일 삭제
344
358
  - `archive-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
345
359
  - `solving-dir`: 푸는 중인 문제 디렉토리 (기본값: solving)
346
360
  - `archive-strategy`: 아카이빙 전략
361
+ - `archive-auto-commit`: archive 실행 시 Git 커밋 자동 실행 여부 (true/false, 기본값: true)
362
+ - `archive-commit-message`: archive 시 사용할 Git 커밋 메시지 템플릿 (`{id}`, `{title}` 사용 가능, 기본값: `solve: {id} - {title}`)
347
363
 
348
364
  ### 아카이빙 전략
349
365
 
@@ -413,7 +429,7 @@ node dist/index.js init
413
429
  ```bash
414
430
  # 프로젝트 디렉토리에서
415
431
  bun run build
416
- npm link
432
+ bun link
417
433
 
418
434
  # 외부 폴더에서 테스트
419
435
  cd /path/to/test-project
@@ -425,17 +441,6 @@ npm unlink -g @rhseung/ps-cli
425
441
 
426
442
  **주의:** `npm link`를 사용하면 글로벌 설치된 버전이 링크된 버전으로 대체됩니다.
427
443
 
428
- ### 방법 3: 프로젝트 내에서만 테스트
429
-
430
- ```bash
431
- # 빌드
432
- bun run build
433
-
434
- # 프로젝트 디렉토리 내에서만
435
- bun run ps init
436
- bun run ps fetch 1000
437
- ```
438
-
439
444
  ## 라이선스
440
445
 
441
446
  MIT
@@ -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
  };
@@ -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 {
@@ -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,
@@ -122,10 +125,13 @@ async function generateProblemFiles(problem, language = "python") {
122
125
  "utf-8"
123
126
  );
124
127
  }
128
+ const testcasesDir = join(problemDir, "testcases");
125
129
  for (let i = 0; i < problem.testCases.length; i++) {
126
130
  const testCase = problem.testCases[i];
127
- const inputPath = join(problemDir, `input${i + 1}.txt`);
128
- const outputPath = join(problemDir, `output${i + 1}.txt`);
131
+ const caseDir = join(testcasesDir, String(i + 1));
132
+ await mkdir(caseDir, { recursive: true });
133
+ const inputPath = join(caseDir, "input.txt");
134
+ const outputPath = join(caseDir, "output.txt");
129
135
  await writeFile(inputPath, testCase.input, "utf-8");
130
136
  await writeFile(outputPath, testCase.output, "utf-8");
131
137
  }
@@ -312,6 +318,14 @@ ${editor}\uB85C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.`
312
318
 
313
319
  // src/commands/fetch.tsx
314
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
+ };
315
329
  function FetchView({
316
330
  problemId,
317
331
  language = "python",
@@ -375,16 +389,7 @@ FetchCommand = __decorateClass([
375
389
  - \uBB38\uC81C \uC124\uBA85, \uC785\uCD9C\uB825 \uD615\uC2DD, \uC608\uC81C \uC785\uCD9C\uB825 \uD30C\uC77C \uC790\uB3D9 \uC0DD\uC131
376
390
  - \uC120\uD0DD\uD55C \uC5B8\uC5B4\uC758 \uC194\uB8E8\uC158 \uD15C\uD50C\uB9BF \uD30C\uC77C \uC0DD\uC131
377
391
  - README.md\uC5D0 \uBB38\uC81C \uC815\uBCF4, \uD1B5\uACC4, \uD0DC\uADF8 \uB4F1 \uD3EC\uD568`,
378
- flags: [
379
- {
380
- name: "language",
381
- options: {
382
- shortFlag: "l",
383
- description: `\uC5B8\uC5B4 \uC120\uD0DD (${getSupportedLanguagesString()})
384
- \uAE30\uBCF8\uAC12: python`
385
- }
386
- }
387
- ],
392
+ flags: defineFlags(fetchFlagsSchema),
388
393
  autoDetectProblemId: false,
389
394
  requireProblemId: true,
390
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