@rhseung/ps-cli 1.8.0 → 1.9.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 +39 -9
- package/dist/chunk-D5PTJ4WQ.js +210 -0
- package/dist/chunk-EGPBBIJ2.js +251 -0
- package/dist/{chunk-JPDN34C7.js → chunk-JZK2464U.js} +971 -116
- package/dist/chunk-SBBC4CBS.js +414 -0
- package/dist/{chunk-VIHXBCOZ.js → chunk-TTTS4CDS.js} +1 -1
- package/dist/chunk-XV6UUPQC.js +288 -0
- package/dist/{chunk-4ISG24GW.js → chunk-YH6VDCNG.js} +173 -15
- package/dist/chunk-YXLVFZZH.js +370 -0
- package/dist/commands/archive.js +6 -206
- package/dist/commands/config.js +74 -242
- package/dist/commands/fetch.js +7 -396
- package/dist/commands/init.js +50 -10
- package/dist/commands/open.js +6 -137
- package/dist/commands/run.js +45 -33
- package/dist/commands/search.js +269 -130
- package/dist/commands/stats.js +249 -82
- package/dist/commands/submit.js +7 -247
- package/dist/commands/test.js +7 -371
- package/dist/index.js +13 -50
- package/package.json +2 -1
- package/dist/chunk-3LR2NGRC.js +0 -55
- package/dist/chunk-7MQMPJ3X.js +0 -88
- package/dist/chunk-ASMT3CRD.js +0 -500
- package/dist/chunk-PY6GW22W.js +0 -13
- package/dist/chunk-QB2R47PW.js +0 -61
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# ps-cli
|
|
2
2
|
|
|
3
|
-
백준 문제
|
|
3
|
+
**대화형** 인터페이스와 **사용자 친화적인 디자인**을 갖춘 백준 문제 해결 CLI 도구입니다.
|
|
4
|
+
검색부터 제출까지, 모든 과정을 직관적이고 아름다운 터미널 환경에서 해결하세요.
|
|
5
|
+
|
|
6
|
+
Nerd Font 사용을 권장합니다.
|
|
4
7
|
|
|
5
8
|
## 설치
|
|
6
9
|
|
|
@@ -13,6 +16,9 @@ bun install -g @rhseung/ps-cli
|
|
|
13
16
|
## 빠른 시작
|
|
14
17
|
|
|
15
18
|
```bash
|
|
19
|
+
# 0. 도움말 보기
|
|
20
|
+
ps --help
|
|
21
|
+
|
|
16
22
|
# 1. 프로젝트 초기화
|
|
17
23
|
ps init
|
|
18
24
|
|
|
@@ -60,6 +66,7 @@ ps fetch <문제번호> [옵션]
|
|
|
60
66
|
|
|
61
67
|
**옵션:**
|
|
62
68
|
|
|
69
|
+
- `--help`, `-h`: 도움말 표시
|
|
63
70
|
- `--language`, `-l`: 언어 선택 (python, javascript, typescript, cpp)
|
|
64
71
|
- 기본값: python 또는 설정 파일의 `default-language`
|
|
65
72
|
|
|
@@ -92,6 +99,7 @@ ps test [문제번호] [옵션]
|
|
|
92
99
|
|
|
93
100
|
**옵션:**
|
|
94
101
|
|
|
102
|
+
- `--help`, `-h`: 도움말 표시
|
|
95
103
|
- `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
|
|
96
104
|
- 지원 언어: python, javascript, typescript, cpp
|
|
97
105
|
- `--watch`, `-w`: watch 모드 (파일 변경 시 자동 재테스트)
|
|
@@ -128,6 +136,7 @@ ps run [문제번호] [옵션]
|
|
|
128
136
|
|
|
129
137
|
**옵션:**
|
|
130
138
|
|
|
139
|
+
- `--help`, `-h`: 도움말 표시
|
|
131
140
|
- `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
|
|
132
141
|
- 지원 언어: python, javascript, typescript, cpp
|
|
133
142
|
- `--input`, `-i`: 입력 파일 지정
|
|
@@ -166,6 +175,7 @@ ps submit [문제번호] [옵션]
|
|
|
166
175
|
|
|
167
176
|
**옵션:**
|
|
168
177
|
|
|
178
|
+
- `--help`, `-h`: 도움말 표시
|
|
169
179
|
- `--language`, `-l`: 언어 선택 (지정 시 자동 감지 무시)
|
|
170
180
|
- 지원 언어: python, javascript, typescript, cpp
|
|
171
181
|
|
|
@@ -213,35 +223,40 @@ ps archive # 현재 디렉토리에서 문제 번호 자동
|
|
|
213
223
|
|
|
214
224
|
---
|
|
215
225
|
|
|
216
|
-
### `open` - 문제 페이지 열기
|
|
226
|
+
### `open` - 문제 페이지 또는 에디터 열기
|
|
217
227
|
|
|
218
|
-
백준 문제 페이지 또는
|
|
228
|
+
백준 문제 페이지 또는 에디터를 엽니다.
|
|
219
229
|
|
|
220
230
|
**사용법:**
|
|
221
231
|
|
|
222
232
|
```bash
|
|
223
|
-
ps open [문제번호]
|
|
233
|
+
ps open [문제번호] [옵션]
|
|
224
234
|
ps open --workbook <문제집ID>
|
|
225
235
|
ps open -w <문제집ID>
|
|
226
236
|
```
|
|
227
237
|
|
|
228
238
|
**옵션:**
|
|
229
239
|
|
|
240
|
+
- `--help`, `-h`: 도움말 표시
|
|
230
241
|
- `--workbook`, `-w`: 문제집 ID를 지정하여 해당 문제집 페이지를 엽니다
|
|
242
|
+
- `--editor`, `-e`: 문제 파일을 에디터로 엽니다
|
|
231
243
|
|
|
232
244
|
**예제:**
|
|
233
245
|
|
|
234
246
|
```bash
|
|
235
|
-
ps open 1000 # 1000번 문제 열기
|
|
236
|
-
ps open # 문제
|
|
247
|
+
ps open 1000 # 1000번 문제 브라우저로 열기
|
|
248
|
+
ps open # 현재 문제 브라우저로 열기
|
|
249
|
+
ps open -e # 현재 문제 에디터로 열기
|
|
250
|
+
ps open 1000 -e # 1000번 문제 에디터로 열기
|
|
237
251
|
ps open --workbook 25052 # 문제집 25052 열기
|
|
238
|
-
ps open -w 25052 # 문제집 25052 열기 (단축 옵션)
|
|
239
252
|
```
|
|
240
253
|
|
|
241
254
|
**설명:**
|
|
242
255
|
|
|
243
256
|
- 문제 번호를 인자로 전달하거나 문제 디렉토리에서 실행하면 자동으로 문제 번호를 추론
|
|
244
257
|
- `--workbook` 또는 `-w` 옵션으로 문제집 페이지를 열 수 있습니다
|
|
258
|
+
- `--editor` 또는 `-e` 옵션으로 문제의 솔루션 파일이나 디렉토리를 에디터로 열 수 있습니다
|
|
259
|
+
- 에디터는 `ps config set editor <명령어>`로 설정할 수 있습니다 (기본값: `code`)
|
|
245
260
|
|
|
246
261
|
---
|
|
247
262
|
|
|
@@ -258,6 +273,7 @@ ps search --workbook <문제집ID>
|
|
|
258
273
|
|
|
259
274
|
**옵션:**
|
|
260
275
|
|
|
276
|
+
- `--help`, `-h`: 도움말 표시
|
|
261
277
|
- `--workbook`: 문제집 ID를 지정하여 해당 문제집의 문제 목록을 표시
|
|
262
278
|
|
|
263
279
|
**예제:**
|
|
@@ -274,7 +290,15 @@ ps search --workbook 25052 # 문제집 25052의 문제 목록 표시
|
|
|
274
290
|
**설명:**
|
|
275
291
|
|
|
276
292
|
- solved.ac 검색어 문법을 지원합니다
|
|
277
|
-
- 문제 목록에서
|
|
293
|
+
- 문제 목록에서 해결 상태를 아이콘으로 확인할 수 있습니다
|
|
294
|
+
- **해결됨(``)**: 이미 해결되어 아카이브된 문제
|
|
295
|
+
- **해결 중(``)**: 현재 해결 중인 문제
|
|
296
|
+
- 문제 선택 시 인터랙티브 메뉴를 통해 다양한 작업을 수행할 수 있습니다
|
|
297
|
+
- ` 브라우저에서 열기 (open)`
|
|
298
|
+
- ` 문제 가져오기 (fetch)`
|
|
299
|
+
- ` 로컬 테스트 실행 (test)` (해결 중/완료인 경우)
|
|
300
|
+
- ` 코드 제출하기 (submit)` (해결 중/완료인 경우)
|
|
301
|
+
- ` 문제 아카이브 (archive)` (해결 중인 경우)
|
|
278
302
|
- 페이지네이션을 통해 여러 페이지의 결과를 탐색할 수 있습니다
|
|
279
303
|
- `--workbook` 옵션으로 백준 문제집의 문제 목록을 볼 수 있습니다
|
|
280
304
|
|
|
@@ -292,6 +316,7 @@ ps stats [핸들] [옵션]
|
|
|
292
316
|
|
|
293
317
|
**옵션:**
|
|
294
318
|
|
|
319
|
+
- `--help`, `-h`: 도움말 표시
|
|
295
320
|
- `--handle`, `-h`: Solved.ac 핸들
|
|
296
321
|
- 설정에 저장된 값 사용 가능
|
|
297
322
|
- 인자로 전달하거나 플래그로 지정 가능
|
|
@@ -319,7 +344,7 @@ ps stats # 설정에 저장된 핸들 사용
|
|
|
319
344
|
**사용법:**
|
|
320
345
|
|
|
321
346
|
```bash
|
|
322
|
-
ps config <명령어> [키] [값]
|
|
347
|
+
ps config <명령어> [키] [값] [옵션]
|
|
323
348
|
```
|
|
324
349
|
|
|
325
350
|
**명령어:**
|
|
@@ -329,6 +354,10 @@ ps config <명령어> [키] [값]
|
|
|
329
354
|
- `list`: 모든 설정 조회
|
|
330
355
|
- `clear`: .ps-cli.json 파일 삭제
|
|
331
356
|
|
|
357
|
+
**옵션:**
|
|
358
|
+
|
|
359
|
+
- `--help`, `-h`: 도움말 표시
|
|
360
|
+
|
|
332
361
|
**예제:**
|
|
333
362
|
|
|
334
363
|
```bash
|
|
@@ -354,6 +383,7 @@ ps config clear # .ps-cli.json 파일 삭제
|
|
|
354
383
|
- `default-language`: 기본 언어 (python, javascript, typescript, cpp)
|
|
355
384
|
- `editor`: 에디터 명령어 (code, cursor, vim 등)
|
|
356
385
|
- `auto-open-editor`: fetch 후 자동으로 에디터 열기 (true/false)
|
|
386
|
+
- `include-tag`: README에 알고리즘 분류(태그) 포함 여부 (true/false, 기본값: true)
|
|
357
387
|
- `solved-ac-handle`: Solved.ac 핸들
|
|
358
388
|
- `archive-dir`: 아카이브된 문제 디렉토리 (기본값: problems)
|
|
359
389
|
- `solving-dir`: 푸는 중인 문제 디렉토리 (기본값: solving)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Command,
|
|
4
|
+
CommandBuilder,
|
|
5
|
+
CommandDef,
|
|
6
|
+
__decorateClass,
|
|
7
|
+
findProjectRoot,
|
|
8
|
+
getArchiveAutoCommit,
|
|
9
|
+
getArchiveCommitMessage,
|
|
10
|
+
getArchiveDirPath,
|
|
11
|
+
getSolvingDir,
|
|
12
|
+
getSolvingDirPath,
|
|
13
|
+
logger,
|
|
14
|
+
resolveProblemContext
|
|
15
|
+
} from "./chunk-JZK2464U.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/archive.tsx
|
|
18
|
+
import { StatusMessage, Alert, Spinner } from "@inkjs/ui";
|
|
19
|
+
import { Box } from "ink";
|
|
20
|
+
|
|
21
|
+
// src/hooks/use-archive.ts
|
|
22
|
+
import { access, readFile, rename, mkdir, readdir, rmdir } from "fs/promises";
|
|
23
|
+
import { join, dirname } from "path";
|
|
24
|
+
import { execa } from "execa";
|
|
25
|
+
import { useEffect, useState } from "react";
|
|
26
|
+
function useArchive({
|
|
27
|
+
problemId,
|
|
28
|
+
onComplete
|
|
29
|
+
}) {
|
|
30
|
+
const [status, setStatus] = useState(
|
|
31
|
+
"loading"
|
|
32
|
+
);
|
|
33
|
+
const [message, setMessage] = useState("\uBB38\uC81C\uB97C \uC544\uCE74\uC774\uBE0C\uD558\uB294 \uC911...");
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
async function archive() {
|
|
37
|
+
try {
|
|
38
|
+
const projectRoot = findProjectRoot();
|
|
39
|
+
if (!projectRoot) {
|
|
40
|
+
throw new Error("\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
41
|
+
}
|
|
42
|
+
const solvingDir = getSolvingDirPath(problemId, projectRoot);
|
|
43
|
+
setMessage("solving \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C\uB97C \uD655\uC778\uD558\uB294 \uC911...");
|
|
44
|
+
try {
|
|
45
|
+
await access(solvingDir);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`solving \uB514\uB809\uD1A0\uB9AC\uC5D0 \uBB38\uC81C ${problemId}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
setMessage("\uBB38\uC81C \uC815\uBCF4\uB97C \uC77D\uB294 \uC911...");
|
|
52
|
+
const metaPath = join(solvingDir, "meta.json");
|
|
53
|
+
let problemTitle = `\uBB38\uC81C ${problemId}`;
|
|
54
|
+
let problem;
|
|
55
|
+
try {
|
|
56
|
+
const metaContent = await readFile(metaPath, "utf-8");
|
|
57
|
+
const meta = JSON.parse(metaContent);
|
|
58
|
+
if (meta.title) {
|
|
59
|
+
problemTitle = meta.title;
|
|
60
|
+
}
|
|
61
|
+
if (meta.id && meta.level !== void 0) {
|
|
62
|
+
problem = {
|
|
63
|
+
id: meta.id,
|
|
64
|
+
title: meta.title || `\uBB38\uC81C ${meta.id}`,
|
|
65
|
+
level: meta.level,
|
|
66
|
+
tier: "",
|
|
67
|
+
// tier는 level에서 계산되므로 빈 문자열로 충분
|
|
68
|
+
tags: meta.tags || [],
|
|
69
|
+
testCases: []
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
const archiveDir = getArchiveDirPath(problemId, projectRoot, problem);
|
|
75
|
+
try {
|
|
76
|
+
await access(archiveDir);
|
|
77
|
+
throw new Error(
|
|
78
|
+
`archive \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC774\uBBF8 \uBB38\uC81C ${problemId}\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4.`
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof Error && err.message.includes("\uC774\uBBF8")) {
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const autoCommit = getArchiveAutoCommit();
|
|
86
|
+
const template = getArchiveCommitMessage() ?? "solve: {id} - {title}";
|
|
87
|
+
const commitMessage = template.replace("{id}", String(problemId)).replace("{title}", problemTitle);
|
|
88
|
+
const archiveDirParent = dirname(archiveDir);
|
|
89
|
+
setMessage("\uC544\uCE74\uC774\uBE0C \uB514\uB809\uD1A0\uB9AC\uB97C \uC900\uBE44\uD558\uB294 \uC911...");
|
|
90
|
+
await mkdir(archiveDirParent, { recursive: true });
|
|
91
|
+
setMessage("\uBB38\uC81C\uB97C archive \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9\uD558\uB294 \uC911...");
|
|
92
|
+
await rename(solvingDir, archiveDir);
|
|
93
|
+
if (autoCommit) {
|
|
94
|
+
try {
|
|
95
|
+
setMessage("Git \uCEE4\uBC0B\uC744 \uC2E4\uD589\uD558\uB294 \uC911...");
|
|
96
|
+
await execa("git", ["add", archiveDir], { cwd: projectRoot });
|
|
97
|
+
await execa("git", ["commit", "-m", commitMessage], {
|
|
98
|
+
cwd: projectRoot
|
|
99
|
+
});
|
|
100
|
+
} catch (gitError) {
|
|
101
|
+
setMessage("\uCEE4\uBC0B \uC2E4\uD328\uB85C \uC778\uD574 \uB864\uBC31\uD558\uB294 \uC911...");
|
|
102
|
+
try {
|
|
103
|
+
await rename(archiveDir, solvingDir);
|
|
104
|
+
} catch (rollbackError) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`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.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
throw gitError;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
setMessage("\uBE48 \uB514\uB809\uD1A0\uB9AC \uC815\uB9AC \uC911...");
|
|
113
|
+
try {
|
|
114
|
+
const solvingDirConfig = getSolvingDir();
|
|
115
|
+
if (solvingDirConfig && solvingDirConfig !== "." && solvingDirConfig !== "") {
|
|
116
|
+
let currentParentDir = dirname(solvingDir);
|
|
117
|
+
const solvingBaseDir = join(projectRoot, solvingDirConfig);
|
|
118
|
+
while (currentParentDir !== solvingBaseDir && currentParentDir !== projectRoot) {
|
|
119
|
+
try {
|
|
120
|
+
const entries = await readdir(currentParentDir);
|
|
121
|
+
if (entries.length === 0) {
|
|
122
|
+
await rmdir(currentParentDir);
|
|
123
|
+
currentParentDir = dirname(currentParentDir);
|
|
124
|
+
} else {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
setStatus("success");
|
|
135
|
+
setMessage(`\uBB38\uC81C ${problemId}\uB97C \uC544\uCE74\uC774\uBE0C\uD588\uC2B5\uB2C8\uB2E4: ${archiveDir}`);
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
onComplete?.();
|
|
138
|
+
}, 2e3);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
setStatus("error");
|
|
141
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
onComplete?.();
|
|
144
|
+
}, 2e3);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
void archive();
|
|
148
|
+
}, [problemId, onComplete]);
|
|
149
|
+
return {
|
|
150
|
+
status,
|
|
151
|
+
message,
|
|
152
|
+
error
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/commands/archive.tsx
|
|
157
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
158
|
+
function ArchiveView({ problemId, onComplete }) {
|
|
159
|
+
const { status, message, error } = useArchive({
|
|
160
|
+
problemId,
|
|
161
|
+
onComplete
|
|
162
|
+
});
|
|
163
|
+
if (status === "loading") {
|
|
164
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Spinner, { label: message }) });
|
|
165
|
+
}
|
|
166
|
+
if (status === "error") {
|
|
167
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Alert, { variant: "error", children: [
|
|
168
|
+
"\uC624\uB958 \uBC1C\uC0DD: ",
|
|
169
|
+
error
|
|
170
|
+
] }) });
|
|
171
|
+
}
|
|
172
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: "100%", children: /* @__PURE__ */ jsx(StatusMessage, { variant: "success", children: message }) });
|
|
173
|
+
}
|
|
174
|
+
var ArchiveCommand = class extends Command {
|
|
175
|
+
async execute(args, _) {
|
|
176
|
+
const context = await resolveProblemContext(args, { requireId: false });
|
|
177
|
+
if (context.problemId === null) {
|
|
178
|
+
logger.error("\uBB38\uC81C \uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
|
|
179
|
+
console.log(`\uB3C4\uC6C0\uB9D0: ps archive --help`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await this.renderView(ArchiveView, {
|
|
184
|
+
problemId: context.problemId
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
ArchiveCommand = __decorateClass([
|
|
189
|
+
CommandDef({
|
|
190
|
+
name: "archive",
|
|
191
|
+
description: `\uBB38\uC81C\uB97C \uC544\uCE74\uC774\uBE0C\uD558\uACE0 Git \uCEE4\uBC0B\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4.
|
|
192
|
+
- solving \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C\uB97C \uCC3E\uC544 archive \uB514\uB809\uD1A0\uB9AC\uB85C \uC774\uB3D9
|
|
193
|
+
- Git add \uBC0F commit \uC2E4\uD589 (\uCEE4\uBC0B \uBA54\uC2DC\uC9C0: "solve: {\uBB38\uC81C\uBC88\uD638} - {\uBB38\uC81C\uC774\uB984}")
|
|
194
|
+
- \uC544\uCE74\uC774\uBE0C \uACBD\uB85C, \uC804\uB7B5, \uC790\uB3D9 \uCEE4\uBC0B \uC5EC\uBD80 \uB4F1\uC740 ps config\uC5D0\uC11C \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.`,
|
|
195
|
+
flags: [],
|
|
196
|
+
autoDetectProblemId: true,
|
|
197
|
+
requireProblemId: false,
|
|
198
|
+
examples: [
|
|
199
|
+
"archive 1000",
|
|
200
|
+
"archive # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uBB38\uC81C \uBC88\uD638 \uC790\uB3D9 \uAC10\uC9C0"
|
|
201
|
+
]
|
|
202
|
+
})
|
|
203
|
+
], ArchiveCommand);
|
|
204
|
+
var archive_default = CommandBuilder.fromClass(ArchiveCommand);
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
ArchiveView,
|
|
208
|
+
ArchiveCommand,
|
|
209
|
+
archive_default
|
|
210
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openBrowser
|
|
4
|
+
} from "./chunk-QGMWUOJ3.js";
|
|
5
|
+
import {
|
|
6
|
+
Command,
|
|
7
|
+
CommandBuilder,
|
|
8
|
+
CommandDef,
|
|
9
|
+
__decorateClass,
|
|
10
|
+
defineFlags,
|
|
11
|
+
detectProblemIdFromPath,
|
|
12
|
+
findSolutionFile,
|
|
13
|
+
getSupportedLanguagesString,
|
|
14
|
+
resolveLanguage,
|
|
15
|
+
resolveProblemContext
|
|
16
|
+
} from "./chunk-JZK2464U.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/submit.tsx
|
|
19
|
+
import { Badge, StatusMessage, Alert, Spinner } from "@inkjs/ui";
|
|
20
|
+
import { Text, Box } from "ink";
|
|
21
|
+
|
|
22
|
+
// src/hooks/use-submit.ts
|
|
23
|
+
import { readFile } from "fs/promises";
|
|
24
|
+
import { useEffect, useState } from "react";
|
|
25
|
+
|
|
26
|
+
// src/utils/clipboard.ts
|
|
27
|
+
import { execa, execaCommand } from "execa";
|
|
28
|
+
async function copyToClipboard(text) {
|
|
29
|
+
try {
|
|
30
|
+
if (process.platform === "win32") {
|
|
31
|
+
await execaCommand("clip", {
|
|
32
|
+
shell: true,
|
|
33
|
+
input: text
|
|
34
|
+
});
|
|
35
|
+
} else if (process.platform === "darwin") {
|
|
36
|
+
await execaCommand("pbcopy", {
|
|
37
|
+
shell: false,
|
|
38
|
+
input: text
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
await execa("xclip", ["-selection", "clipboard"], {
|
|
43
|
+
input: text
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
try {
|
|
47
|
+
await execa("xsel", ["--clipboard", "--input"], {
|
|
48
|
+
input: text
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/hooks/use-submit.ts
|
|
62
|
+
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
63
|
+
function useSubmit({
|
|
64
|
+
problemId,
|
|
65
|
+
language: _language,
|
|
66
|
+
sourcePath,
|
|
67
|
+
onComplete
|
|
68
|
+
}) {
|
|
69
|
+
const [status, setStatus] = useState(
|
|
70
|
+
"loading"
|
|
71
|
+
);
|
|
72
|
+
const [message, setMessage] = useState("\uC81C\uCD9C \uC900\uBE44 \uC911...");
|
|
73
|
+
const [error, setError] = useState(null);
|
|
74
|
+
const [submitUrl, setSubmitUrl] = useState("");
|
|
75
|
+
const [clipboardSuccess, setClipboardSuccess] = useState(false);
|
|
76
|
+
const [clipboardError, setClipboardError] = useState(null);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
async function submit() {
|
|
79
|
+
try {
|
|
80
|
+
setMessage("\uC18C\uC2A4 \uCF54\uB4DC\uB97C \uC77D\uB294 \uC911...");
|
|
81
|
+
const sourceCode = await readFile(sourcePath, "utf-8");
|
|
82
|
+
setMessage("\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC\uD558\uB294 \uC911...");
|
|
83
|
+
const clipboardResult = await copyToClipboard(sourceCode);
|
|
84
|
+
setClipboardSuccess(clipboardResult);
|
|
85
|
+
if (!clipboardResult) {
|
|
86
|
+
setClipboardError("\uD074\uB9BD\uBCF4\uB4DC \uBCF5\uC0AC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
87
|
+
}
|
|
88
|
+
const url = `${BOJ_BASE_URL}/submit/${problemId}`;
|
|
89
|
+
setSubmitUrl(url);
|
|
90
|
+
setMessage("\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5EC\uB294 \uC911...");
|
|
91
|
+
await openBrowser(url);
|
|
92
|
+
setStatus("success");
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
onComplete();
|
|
95
|
+
}, 2e3);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
setStatus("error");
|
|
98
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
onComplete();
|
|
101
|
+
}, 2e3);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
void submit();
|
|
105
|
+
}, [problemId, sourcePath, onComplete]);
|
|
106
|
+
return {
|
|
107
|
+
status,
|
|
108
|
+
message,
|
|
109
|
+
error,
|
|
110
|
+
submitUrl,
|
|
111
|
+
clipboardSuccess,
|
|
112
|
+
clipboardError
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/commands/submit.tsx
|
|
117
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
118
|
+
var submitFlagsSchema = {
|
|
119
|
+
language: {
|
|
120
|
+
type: "string",
|
|
121
|
+
shortFlag: "l",
|
|
122
|
+
description: `\uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
|
|
123
|
+
\uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}`
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function SubmitView({
|
|
127
|
+
problemId,
|
|
128
|
+
language,
|
|
129
|
+
sourcePath,
|
|
130
|
+
onComplete
|
|
131
|
+
}) {
|
|
132
|
+
const {
|
|
133
|
+
status,
|
|
134
|
+
message,
|
|
135
|
+
error,
|
|
136
|
+
submitUrl,
|
|
137
|
+
clipboardSuccess,
|
|
138
|
+
clipboardError
|
|
139
|
+
} = useSubmit({
|
|
140
|
+
problemId,
|
|
141
|
+
language,
|
|
142
|
+
sourcePath,
|
|
143
|
+
onComplete
|
|
144
|
+
});
|
|
145
|
+
if (status === "loading") {
|
|
146
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
147
|
+
/* @__PURE__ */ jsx(Spinner, { label: message }),
|
|
148
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
149
|
+
"\uBB38\uC81C #",
|
|
150
|
+
problemId
|
|
151
|
+
] }) })
|
|
152
|
+
] });
|
|
153
|
+
}
|
|
154
|
+
if (status === "error") {
|
|
155
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
156
|
+
/* @__PURE__ */ jsxs(Alert, { variant: "error", children: [
|
|
157
|
+
"\uC624\uB958 \uBC1C\uC0DD: ",
|
|
158
|
+
error
|
|
159
|
+
] }),
|
|
160
|
+
submitUrl && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
161
|
+
"URL: ",
|
|
162
|
+
submitUrl
|
|
163
|
+
] }) })
|
|
164
|
+
] });
|
|
165
|
+
}
|
|
166
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
|
|
167
|
+
/* @__PURE__ */ jsx(StatusMessage, { variant: "success", children: "\uC81C\uCD9C \uD398\uC774\uC9C0\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4!" }),
|
|
168
|
+
/* @__PURE__ */ jsxs(
|
|
169
|
+
Box,
|
|
170
|
+
{
|
|
171
|
+
flexDirection: "column",
|
|
172
|
+
borderStyle: "round",
|
|
173
|
+
borderColor: "gray",
|
|
174
|
+
marginTop: 1,
|
|
175
|
+
paddingX: 1,
|
|
176
|
+
paddingY: 0,
|
|
177
|
+
alignSelf: "flex-start",
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
180
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\uBB38\uC81C \uBC88\uD638:" }),
|
|
181
|
+
" ",
|
|
182
|
+
problemId
|
|
183
|
+
] }),
|
|
184
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
185
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\uC5B8\uC5B4:" }),
|
|
186
|
+
" ",
|
|
187
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: language })
|
|
188
|
+
] }),
|
|
189
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
190
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "URL:" }),
|
|
191
|
+
" ",
|
|
192
|
+
/* @__PURE__ */ jsx(Text, { color: "blue", underline: true, children: submitUrl })
|
|
193
|
+
] }),
|
|
194
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: clipboardSuccess ? /* @__PURE__ */ jsx(Badge, { color: "green", children: "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC\uB428" }) : /* @__PURE__ */ jsx(Badge, { color: "yellow", children: "\uD074\uB9BD\uBCF4\uB4DC \uBCF5\uC0AC \uC2E4\uD328" }) }),
|
|
195
|
+
clipboardError && !clipboardSuccess && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Alert, { variant: "warning", children: clipboardError }) })
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
] });
|
|
200
|
+
}
|
|
201
|
+
var SubmitCommand = class extends Command {
|
|
202
|
+
async execute(args, flags) {
|
|
203
|
+
const context = await resolveProblemContext(args, { requireId: true });
|
|
204
|
+
const sourcePath = await findSolutionFile(context.archiveDir);
|
|
205
|
+
const detectedLanguage = await resolveLanguage(
|
|
206
|
+
context.archiveDir,
|
|
207
|
+
flags.language
|
|
208
|
+
);
|
|
209
|
+
let finalProblemId = context.problemId;
|
|
210
|
+
if (finalProblemId === null) {
|
|
211
|
+
finalProblemId = detectProblemIdFromPath(context.archiveDir);
|
|
212
|
+
}
|
|
213
|
+
if (finalProblemId === null) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"\uBB38\uC81C \uBC88\uD638\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC81C \uBC88\uD638\uB97C \uC778\uC790\uB85C \uC804\uB2EC\uD558\uAC70\uB098 \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589\uD574\uC8FC\uC138\uC694."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
await this.renderView(SubmitView, {
|
|
219
|
+
problemId: finalProblemId,
|
|
220
|
+
language: detectedLanguage,
|
|
221
|
+
sourcePath
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
SubmitCommand = __decorateClass([
|
|
226
|
+
CommandDef({
|
|
227
|
+
name: "submit",
|
|
228
|
+
description: `\uBC31\uC900 \uC81C\uCD9C \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5F4\uACE0 \uC18C\uC2A4 \uCF54\uB4DC\uB97C \uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC\uD569\uB2C8\uB2E4.
|
|
229
|
+
- \uBB38\uC81C \uBC88\uD638\uB97C \uC778\uC790\uB85C \uC804\uB2EC\uD558\uAC70\uB098 \uBB38\uC81C \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC2E4\uD589\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uBB38\uC81C \uBC88\uD638\uB97C \uCD94\uB860
|
|
230
|
+
- solution.* \uD30C\uC77C\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uC5B8\uC5B4 \uAC10\uC9C0
|
|
231
|
+
- \uC18C\uC2A4 \uCF54\uB4DC\uB97C \uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uC790\uB3D9 \uBCF5\uC0AC
|
|
232
|
+
- \uC81C\uCD9C \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC790\uB3D9 \uC5F4\uAE30
|
|
233
|
+
- \uAE30\uBCF8 \uC5B8\uC5B4 \uB4F1\uC740 ps config\uC5D0\uC11C \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.`,
|
|
234
|
+
flags: defineFlags(submitFlagsSchema),
|
|
235
|
+
autoDetectProblemId: true,
|
|
236
|
+
autoDetectLanguage: true,
|
|
237
|
+
requireProblemId: true,
|
|
238
|
+
examples: [
|
|
239
|
+
"submit # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC81C\uCD9C",
|
|
240
|
+
"submit 1000 # 1000\uBC88 \uBB38\uC81C \uC81C\uCD9C",
|
|
241
|
+
"submit --language python # Python\uC73C\uB85C \uC81C\uCD9C"
|
|
242
|
+
]
|
|
243
|
+
})
|
|
244
|
+
], SubmitCommand);
|
|
245
|
+
var submit_default = CommandBuilder.fromClass(SubmitCommand);
|
|
246
|
+
|
|
247
|
+
export {
|
|
248
|
+
SubmitView,
|
|
249
|
+
SubmitCommand,
|
|
250
|
+
submit_default
|
|
251
|
+
};
|