@rhseung/ps-cli 1.7.4 → 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
@@ -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
  ---
@@ -425,17 +428,6 @@ npm unlink -g @rhseung/ps-cli
425
428
 
426
429
  **주의:** `npm link`를 사용하면 글로벌 설치된 버전이 링크된 버전으로 대체됩니다.
427
430
 
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
431
  ## 라이선스
440
432
 
441
433
  MIT
@@ -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 {
@@ -122,10 +122,13 @@ async function generateProblemFiles(problem, language = "python") {
122
122
  "utf-8"
123
123
  );
124
124
  }
125
+ const testcasesDir = join(problemDir, "testcases");
125
126
  for (let i = 0; i < problem.testCases.length; i++) {
126
127
  const testCase = problem.testCases[i];
127
- const inputPath = join(problemDir, `input${i + 1}.txt`);
128
- const outputPath = join(problemDir, `output${i + 1}.txt`);
128
+ const caseDir = join(testcasesDir, String(i + 1));
129
+ await mkdir(caseDir, { recursive: true });
130
+ const inputPath = join(caseDir, "input.txt");
131
+ const outputPath = join(caseDir, "output.txt");
129
132
  await writeFile(inputPath, testCase.input, "utf-8");
130
133
  await writeFile(outputPath, testCase.output, "utf-8");
131
134
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSolution
4
- } from "../chunk-OJZLQ6FK.js";
4
+ } from "../chunk-VIHXBCOZ.js";
5
5
  import {
6
6
  Command,
7
7
  CommandBuilder,
@@ -15,7 +15,6 @@ import {
15
15
  } from "../chunk-7MQMPJ3X.js";
16
16
 
17
17
  // src/commands/run.tsx
18
- import { readdir } from "fs/promises";
19
18
  import { join } from "path";
20
19
  import { StatusMessage, Alert } from "@inkjs/ui";
21
20
  import { Spinner } from "@inkjs/ui";
@@ -77,6 +76,17 @@ function RunView({
77
76
  onComplete
78
77
  });
79
78
  if (status === "loading") {
79
+ if (!inputFile) {
80
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
81
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\uD45C\uC900 \uC785\uB825 \uB300\uAE30 \uC911" }) }),
82
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
83
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\uC785\uB825\uC744 \uC785\uB825\uD55C \uD6C4, \uB9C8\uC9C0\uB9C9 \uC904\uC5D0\uC11C Enter\uB97C \uB204\uB974\uACE0:" }),
84
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2022 macOS/Linux: Ctrl+D" }),
85
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2022 Windows: Ctrl+Z (\uADF8\uB9AC\uACE0 Enter)" }),
86
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: '\uC608: "3 4" \uC785\uB825 \u2192 Enter \u2192 Ctrl+D' })
87
+ ] })
88
+ ] });
89
+ }
80
90
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Spinner, { label: "\uCF54\uB4DC \uC2E4\uD589 \uC911..." }) });
81
91
  }
82
92
  if (status === "error") {
@@ -87,7 +97,7 @@ function RunView({
87
97
  }
88
98
  if (result) {
89
99
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
90
- /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
100
+ /* @__PURE__ */ jsxs(Box, { children: [
91
101
  /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\uC2E4\uD589 \uACB0\uACFC" }),
92
102
  /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
93
103
  problemDir,
@@ -102,9 +112,13 @@ function RunView({
102
112
  result.exitCode,
103
113
  ")"
104
114
  ] }) : /* @__PURE__ */ jsx(StatusMessage, { variant: "success", children: "\uC2E4\uD589 \uC644\uB8CC" }),
115
+ result.input && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
116
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\uC785\uB825:" }),
117
+ /* @__PURE__ */ jsx(Text, { children: result.input.trim() })
118
+ ] }),
105
119
  result.stdout && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
106
120
  /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\uCD9C\uB825:" }),
107
- /* @__PURE__ */ jsx(Text, { children: result.stdout })
121
+ /* @__PURE__ */ jsx(Text, { children: result.stdout.trim() })
108
122
  ] }),
109
123
  result.stderr && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
110
124
  /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: "\uC5D0\uB7EC \uCD9C\uB825:" }),
@@ -123,7 +137,20 @@ function RunView({
123
137
  var RunCommand = class extends Command {
124
138
  async execute(args, flags) {
125
139
  const context = await resolveProblemContext(args);
126
- const inputPath = flags.input ? join(context.archiveDir, flags.input) : await this.findInputFile(context.archiveDir);
140
+ let inputPath;
141
+ if (flags.input) {
142
+ const inputValue = flags.input;
143
+ if (/^\d+$/.test(inputValue)) {
144
+ inputPath = join(
145
+ context.archiveDir,
146
+ "testcases",
147
+ inputValue,
148
+ "input.txt"
149
+ );
150
+ } else {
151
+ inputPath = join(context.archiveDir, inputValue);
152
+ }
153
+ }
127
154
  const detectedLanguage = await resolveLanguage(
128
155
  context.archiveDir,
129
156
  flags.language
@@ -134,15 +161,6 @@ var RunCommand = class extends Command {
134
161
  inputFile: inputPath
135
162
  });
136
163
  }
137
- // 입력 파일 찾기: private 메서드
138
- async findInputFile(problemDir) {
139
- const files = await readdir(problemDir);
140
- const inputFile = files.find((f) => f === "input1.txt") || files.find((f) => f === "input.txt");
141
- if (!inputFile) {
142
- throw new Error("input.txt \uB610\uB294 input1.txt \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
143
- }
144
- return join(problemDir, inputFile);
145
- }
146
164
  };
147
165
  RunCommand = __decorateClass([
148
166
  CommandDef({
@@ -150,7 +168,8 @@ RunCommand = __decorateClass([
150
168
  description: `\uCF54\uB4DC\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4 (\uD14C\uC2A4\uD2B8 \uC5C6\uC774).
151
169
  - \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC \uB610\uB294 \uC9C0\uC815\uD55C \uBB38\uC81C \uBC88\uD638\uC758 \uCF54\uB4DC \uC2E4\uD589
152
170
  - solution.* \uD30C\uC77C\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uC5B8\uC5B4 \uAC10\uC9C0
153
- - input.txt \uB610\uB294 input1.txt\uB97C \uD45C\uC900 \uC785\uB825\uC73C\uB85C \uC0AC\uC6A9
171
+ - --input \uC635\uC158\uC73C\uB85C \uC785\uB825 \uD30C\uC77C \uC9C0\uC815 \uAC00\uB2A5 (\uC608: testcases/1/input.txt)
172
+ - \uC635\uC158 \uC5C6\uC774 \uC2E4\uD589 \uC2DC \uD45C\uC900 \uC785\uB825\uC73C\uB85C \uC785\uB825 \uBC1B\uAE30
154
173
  - \uD14C\uC2A4\uD2B8 \uCF00\uC774\uC2A4 \uAC80\uC99D \uC5C6\uC774 \uB2E8\uC21C \uC2E4\uD589`,
155
174
  flags: [
156
175
  {
@@ -165,17 +184,18 @@ RunCommand = __decorateClass([
165
184
  name: "input",
166
185
  options: {
167
186
  shortFlag: "i",
168
- description: "\uC785\uB825 \uD30C\uC77C \uC9C0\uC815 (\uAE30\uBCF8\uAC12: input.txt \uB610\uB294 input1.txt)"
187
+ description: "\uC785\uB825 \uD30C\uC77C \uC9C0\uC815 (\uC608: 1 \uB610\uB294 testcases/1/input.txt)"
169
188
  }
170
189
  }
171
190
  ],
172
191
  autoDetectProblemId: true,
173
192
  autoDetectLanguage: true,
174
193
  examples: [
175
- "run # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589",
176
- "run 1000 # 1000\uBC88 \uBB38\uC81C \uC2E4\uD589",
177
- "run --language python # Python\uC73C\uB85C \uC2E4\uD589",
178
- "run --input input2.txt # \uD2B9\uC815 \uC785\uB825 \uD30C\uC77C \uC0AC\uC6A9"
194
+ "run # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uD45C\uC900 \uC785\uB825\uC73C\uB85C \uC2E4\uD589",
195
+ "run 1000 # 1000\uBC88 \uBB38\uC81C \uD45C\uC900 \uC785\uB825\uC73C\uB85C \uC2E4\uD589",
196
+ "run --language python # Python\uC73C\uB85C \uD45C\uC900 \uC785\uB825\uC73C\uB85C \uC2E4\uD589",
197
+ "run --input 1 # \uD14C\uC2A4\uD2B8 \uCF00\uC774\uC2A4 1\uBC88 \uC0AC\uC6A9",
198
+ "run --input testcases/1/input.txt # \uC804\uCCB4 \uACBD\uB85C\uB85C \uC785\uB825 \uD30C\uC77C \uC9C0\uC815"
179
199
  ]
180
200
  })
181
201
  ], RunCommand);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSolution
4
- } from "../chunk-OJZLQ6FK.js";
4
+ } from "../chunk-VIHXBCOZ.js";
5
5
  import {
6
6
  Command,
7
7
  CommandBuilder,
@@ -122,8 +122,14 @@ async function runAllTests({
122
122
  language,
123
123
  timeoutMs
124
124
  }) {
125
- const entries = await readdir(problemDir);
126
- const inputFiles = entries.filter((f) => /^input\d+\.txt$/.test(f));
125
+ const testcasesDir = join(problemDir, "testcases");
126
+ let caseDirs = [];
127
+ try {
128
+ const entries = await readdir(testcasesDir);
129
+ caseDirs = entries.filter((entry) => /^\d+$/.test(entry)).sort((a, b) => Number(a) - Number(b)).map((entry) => join(testcasesDir, entry));
130
+ } catch {
131
+ return { results: [], summary: buildSummary([]) };
132
+ }
127
133
  const results = [];
128
134
  let effectiveTimeout = timeoutMs;
129
135
  if (effectiveTimeout == null) {
@@ -147,11 +153,10 @@ async function runAllTests({
147
153
  if (effectiveTimeout == null) {
148
154
  effectiveTimeout = 5e3;
149
155
  }
150
- for (const inputFile of inputFiles) {
151
- const match = inputFile.match(/input(\d+)\.txt$/);
152
- const caseId = match ? Number(match[1]) : results.length + 1;
153
- const inputPath = join(problemDir, inputFile);
154
- const outputPath = join(problemDir, `output${caseId}.txt`);
156
+ for (const caseDir of caseDirs) {
157
+ const caseId = Number(join(caseDir).split("/").pop() || "0");
158
+ const inputPath = join(caseDir, "input.txt");
159
+ const outputPath = join(caseDir, "output.txt");
155
160
  let expected;
156
161
  try {
157
162
  expected = await readFile(outputPath, "utf-8");
@@ -160,7 +165,7 @@ async function runAllTests({
160
165
  caseId,
161
166
  inputPath,
162
167
  status: "error",
163
- error: "\uAE30\uB300 \uCD9C\uB825(output*.txt)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
168
+ error: "\uAE30\uB300 \uCD9C\uB825(output.txt)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
164
169
  });
165
170
  continue;
166
171
  }
@@ -246,8 +251,7 @@ function useTestRunner({
246
251
  const watcher = chokidar.watch(
247
252
  [
248
253
  join2(problemDir, "solution.*"),
249
- join2(problemDir, "input*.txt"),
250
- join2(problemDir, "output*.txt")
254
+ join2(problemDir, "testcases", "**", "*.txt")
251
255
  ],
252
256
  {
253
257
  ignoreInitial: true
@@ -335,7 +339,7 @@ TestCommand = __decorateClass([
335
339
  description: `\uC608\uC81C \uC785\uCD9C\uB825 \uAE30\uBC18\uC73C\uB85C \uB85C\uCEEC \uD14C\uC2A4\uD2B8\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
336
340
  - \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC \uB610\uB294 \uC9C0\uC815\uD55C \uBB38\uC81C \uBC88\uD638\uC758 \uD14C\uC2A4\uD2B8 \uC2E4\uD589
337
341
  - solution.* \uD30C\uC77C\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uC5B8\uC5B4 \uAC10\uC9C0
338
- - input*.txt\uC640 output*.txt \uD30C\uC77C\uC744 \uAE30\uBC18\uC73C\uB85C \uD14C\uC2A4\uD2B8
342
+ - testcases/{\uBC88\uD638}/input.txt\uC640 testcases/{\uBC88\uD638}/output.txt \uD30C\uC77C\uC744 \uAE30\uBC18\uC73C\uB85C \uD14C\uC2A4\uD2B8
339
343
  - \uBB38\uC81C\uC758 \uC2DC\uAC04 \uC81C\uD55C\uC744 \uC790\uB3D9\uC73C\uB85C \uC801\uC6A9
340
344
  - --watch \uC635\uC158\uC73C\uB85C \uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8`,
341
345
  flags: [
@@ -352,7 +356,7 @@ TestCommand = __decorateClass([
352
356
  options: {
353
357
  shortFlag: "w",
354
358
  description: `watch \uBAA8\uB4DC (\uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8)
355
- solution.*, input*.txt, output*.txt \uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0`
359
+ solution.*, testcases/**/*.txt \uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0`
356
360
  }
357
361
  }
358
362
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhseung/ps-cli",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "백준(BOJ) 문제 해결을 위한 통합 CLI 도구",
5
5
  "type": "module",
6
6
  "bin": {