ccdock 0.1.0__tar.gz
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.
- ccdock-0.1.0/LICENSE +21 -0
- ccdock-0.1.0/PKG-INFO +131 -0
- ccdock-0.1.0/README.md +104 -0
- ccdock-0.1.0/ccdock/__init__.py +3 -0
- ccdock-0.1.0/ccdock/__main__.py +61 -0
- ccdock-0.1.0/ccdock/envs.py +188 -0
- ccdock-0.1.0/ccdock/launcher.py +271 -0
- ccdock-0.1.0/ccdock/scanner.py +147 -0
- ccdock-0.1.0/ccdock/server.py +210 -0
- ccdock-0.1.0/ccdock/sizes.py +151 -0
- ccdock-0.1.0/ccdock/store.py +71 -0
- ccdock-0.1.0/ccdock/web/index.html +607 -0
- ccdock-0.1.0/ccdock.egg-info/PKG-INFO +131 -0
- ccdock-0.1.0/ccdock.egg-info/SOURCES.txt +17 -0
- ccdock-0.1.0/ccdock.egg-info/dependency_links.txt +1 -0
- ccdock-0.1.0/ccdock.egg-info/entry_points.txt +2 -0
- ccdock-0.1.0/ccdock.egg-info/top_level.txt +1 -0
- ccdock-0.1.0/pyproject.toml +44 -0
- ccdock-0.1.0/setup.cfg +4 -0
ccdock-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pashidl-lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ccdock-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ccdock
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pick a Claude Code project and its conda/venv environment from a local dashboard, and launch it in a new Windows Terminal tab or window
|
|
5
|
+
Author-email: pashidl-lab <pashidl.lab@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pashidl-lab/ccdock
|
|
8
|
+
Project-URL: Issues, https://github.com/pashidl-lab/ccdock/issues
|
|
9
|
+
Keywords: claude-code,conda,venv,launcher,dashboard,windows-terminal
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
# ccdock
|
|
29
|
+
|
|
30
|
+
Claude Code로 작업하던 프로젝트와 conda 가상환경을 **한 화면에서 골라 바로 띄우는** 로컬 대시보드입니다.
|
|
31
|
+
PC를 재부팅할 때마다 폴더 경로를 더듬고 가상환경 이름을 기억해내는 과정을 없애기 위해 만들었습니다.
|
|
32
|
+
|
|
33
|
+
- 최근 작업순 프로젝트 목록 — Claude Code 사용 기록에서 자동 수집
|
|
34
|
+
- **conda 환경과 프로젝트 폴더의 venv를 모두 인식** (`.venv`, `mvkit-env` 같은 폴더)
|
|
35
|
+
- 각 가상환경의 **디스크 사용량 표시** — 배경에서 재고 캐시해 다음부터는 즉시 표시
|
|
36
|
+
- 프로젝트별 가상환경을 한 번 지정하면 계속 기억
|
|
37
|
+
- 선택 즉시 **Anaconda 프롬프트 새 탭 또는 새 창**이 열리고 환경이 활성화된 상태로 대기
|
|
38
|
+
- 창이 여러 개 열려 있으면 **어느 창에 탭을 추가할지 선택**
|
|
39
|
+
- 워크스페이스 — 자주 쓰는 3~5개 조합을 저장해두고 부팅 후 한 번에 복원
|
|
40
|
+
- 외부 의존성 0 (표준 라이브러리만), 브라우저에서 뜨는 가벼운 대시보드
|
|
41
|
+
|
|
42
|
+
## 설치
|
|
43
|
+
|
|
44
|
+
```bat
|
|
45
|
+
pip install ccdock
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
로컬 소스에서 바로 쓰려면:
|
|
49
|
+
|
|
50
|
+
```bat
|
|
51
|
+
pip install -e G:\path\to\ccdock
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> conda `base` 환경에 설치하면 어떤 Anaconda 프롬프트에서도 `ccdock`를 바로 호출할 수 있습니다.
|
|
55
|
+
|
|
56
|
+
## 사용
|
|
57
|
+
|
|
58
|
+
```bat
|
|
59
|
+
ccdock :: 대시보드를 열고 브라우저 실행
|
|
60
|
+
ccdock -p 9000 :: 다른 포트로
|
|
61
|
+
ccdock -n :: 브라우저를 열지 않음
|
|
62
|
+
ccdock list :: 프로젝트 목록을 터미널에 출력
|
|
63
|
+
ccdock workspace 오전작업 :: 저장된 워크스페이스를 한 번에 실행
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 대시보드
|
|
67
|
+
|
|
68
|
+
| 조작 | 설명 |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `/` | 검색창으로 이동 |
|
|
71
|
+
| 검색 후 `Enter` | 첫 번째 결과 실행 |
|
|
72
|
+
| ☆ | 프로젝트 고정 (목록 맨 위로) |
|
|
73
|
+
| 환경 드롭다운 | 가상환경 지정 — 즉시 저장됨 |
|
|
74
|
+
| `+ 담기` → `워크스페이스 저장` | 여러 프로젝트를 하나의 조합으로 저장 |
|
|
75
|
+
| `실행` | 터미널(새 탭/새 창)과 실행할 명령을 고르고 실행 |
|
|
76
|
+
|
|
77
|
+
실행 모달에서 고르는 항목:
|
|
78
|
+
|
|
79
|
+
- **가상환경** — conda 환경과 venv가 그룹으로 나뉘어 표시됩니다. 프로젝트 폴더 안에 venv가
|
|
80
|
+
있거나 `environment.yml` / `.ccdock-env`가 있으면 후보로 먼저 제안합니다
|
|
81
|
+
- **터미널** — *기존 창에 새 탭* (열린 Windows Terminal 창이 있을 때) / *새 창으로 열기*
|
|
82
|
+
- **어느 창에 탭을 추가할까요?** — 창이 2개 이상이면 창 제목으로 골라서 지정합니다
|
|
83
|
+
- **실행할 명령** — `claude` / `claude --continue` / `claude --resume` / 프롬프트만
|
|
84
|
+
|
|
85
|
+
### 가상환경 인식 범위
|
|
86
|
+
|
|
87
|
+
| 종류 | 인식 방법 | 활성화 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| conda | `~/.conda/environments.txt` | `activate.bat <이름>` |
|
|
90
|
+
| venv | 프로젝트 폴더와 그 상위 폴더 바로 아래의 `pyvenv.cfg` 보유 폴더 | `<경로>\Scripts\activate.bat` |
|
|
91
|
+
|
|
92
|
+
프로젝트 폴더 안에 만들어 둔 `.venv`, `mvkit-env` 같은 가상환경이 자동으로 목록에 올라옵니다.
|
|
93
|
+
|
|
94
|
+
### 디스크 사용량
|
|
95
|
+
|
|
96
|
+
목록의 각 환경에 `claude312 — 792.4 MB` 처럼 크기가 함께 표시됩니다. 환경 하나를 훑는 데
|
|
97
|
+
0.4~1초가 걸리므로 화면을 막지 않고 배경 스레드가 순서대로 재며, 결과는 `~/.ccdock/sizes.json`에
|
|
98
|
+
남아 다음 실행부터는 즉시 나옵니다(7일 후 자동 재측정). 헤더의 합계 배지를 누르면 다시 잽니다.
|
|
99
|
+
|
|
100
|
+
`base`는 `envs/`와 `pkgs/`를 빼고 계산합니다. 그러지 않으면 다른 환경과 패키지 캐시가
|
|
101
|
+
전부 포함돼 실제보다 훨씬 크게 나옵니다.
|
|
102
|
+
|
|
103
|
+
## 동작 방식
|
|
104
|
+
|
|
105
|
+
| 정보 | 출처 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| 프로젝트 경로 | `~/.claude.json` 의 `projects` 키 |
|
|
108
|
+
| 마지막 작업 시각 | `~/.claude/projects/<슬러그>/*.jsonl` 파일 mtime |
|
|
109
|
+
| 가상환경 목록 | `~/.conda/environments.txt` (없으면 conda 설치 폴더 스캔) |
|
|
110
|
+
| 사용자 설정 | `~/.ccdock/config.json` |
|
|
111
|
+
|
|
112
|
+
실행 시 임시 배치 파일을 만들어 `wt -w 0 new-tab`(새 탭) 또는 `wt -w new`(새 창)로 띄웁니다.
|
|
113
|
+
`wt -w 0`은 "가장 최근에 사용한 창"을 뜻하므로, 특정 창을 지정하면 Win32 API로 그 창을 먼저
|
|
114
|
+
앞으로 가져온 뒤 탭을 붙입니다. Windows Terminal이 없으면 `start cmd`로 대체합니다. 새 창에는 부모 프로세스의
|
|
115
|
+
`VIRTUAL_ENV` / `CONDA_PREFIX` 흔적을 제거한 깨끗한 환경이 전달되므로, ccdock 자체를
|
|
116
|
+
어떤 가상환경에서 실행하든 선택한 환경이 정확히 활성화됩니다.
|
|
117
|
+
|
|
118
|
+
## 보안
|
|
119
|
+
|
|
120
|
+
서버는 `127.0.0.1`에만 바인딩되며, 모든 API는 실행할 때마다 새로 만들어지는 토큰을 요구합니다.
|
|
121
|
+
브라우저는 토큰이 포함된 URL로 자동으로 열립니다.
|
|
122
|
+
|
|
123
|
+
## 요구 사항
|
|
124
|
+
|
|
125
|
+
- Windows (Windows Terminal이 있으면 탭 기능 사용 가능)
|
|
126
|
+
- Python 3.8+
|
|
127
|
+
- Anaconda 또는 Miniconda
|
|
128
|
+
|
|
129
|
+
## 라이선스
|
|
130
|
+
|
|
131
|
+
MIT
|
ccdock-0.1.0/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ccdock
|
|
2
|
+
|
|
3
|
+
Claude Code로 작업하던 프로젝트와 conda 가상환경을 **한 화면에서 골라 바로 띄우는** 로컬 대시보드입니다.
|
|
4
|
+
PC를 재부팅할 때마다 폴더 경로를 더듬고 가상환경 이름을 기억해내는 과정을 없애기 위해 만들었습니다.
|
|
5
|
+
|
|
6
|
+
- 최근 작업순 프로젝트 목록 — Claude Code 사용 기록에서 자동 수집
|
|
7
|
+
- **conda 환경과 프로젝트 폴더의 venv를 모두 인식** (`.venv`, `mvkit-env` 같은 폴더)
|
|
8
|
+
- 각 가상환경의 **디스크 사용량 표시** — 배경에서 재고 캐시해 다음부터는 즉시 표시
|
|
9
|
+
- 프로젝트별 가상환경을 한 번 지정하면 계속 기억
|
|
10
|
+
- 선택 즉시 **Anaconda 프롬프트 새 탭 또는 새 창**이 열리고 환경이 활성화된 상태로 대기
|
|
11
|
+
- 창이 여러 개 열려 있으면 **어느 창에 탭을 추가할지 선택**
|
|
12
|
+
- 워크스페이스 — 자주 쓰는 3~5개 조합을 저장해두고 부팅 후 한 번에 복원
|
|
13
|
+
- 외부 의존성 0 (표준 라이브러리만), 브라우저에서 뜨는 가벼운 대시보드
|
|
14
|
+
|
|
15
|
+
## 설치
|
|
16
|
+
|
|
17
|
+
```bat
|
|
18
|
+
pip install ccdock
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
로컬 소스에서 바로 쓰려면:
|
|
22
|
+
|
|
23
|
+
```bat
|
|
24
|
+
pip install -e G:\path\to\ccdock
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> conda `base` 환경에 설치하면 어떤 Anaconda 프롬프트에서도 `ccdock`를 바로 호출할 수 있습니다.
|
|
28
|
+
|
|
29
|
+
## 사용
|
|
30
|
+
|
|
31
|
+
```bat
|
|
32
|
+
ccdock :: 대시보드를 열고 브라우저 실행
|
|
33
|
+
ccdock -p 9000 :: 다른 포트로
|
|
34
|
+
ccdock -n :: 브라우저를 열지 않음
|
|
35
|
+
ccdock list :: 프로젝트 목록을 터미널에 출력
|
|
36
|
+
ccdock workspace 오전작업 :: 저장된 워크스페이스를 한 번에 실행
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 대시보드
|
|
40
|
+
|
|
41
|
+
| 조작 | 설명 |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `/` | 검색창으로 이동 |
|
|
44
|
+
| 검색 후 `Enter` | 첫 번째 결과 실행 |
|
|
45
|
+
| ☆ | 프로젝트 고정 (목록 맨 위로) |
|
|
46
|
+
| 환경 드롭다운 | 가상환경 지정 — 즉시 저장됨 |
|
|
47
|
+
| `+ 담기` → `워크스페이스 저장` | 여러 프로젝트를 하나의 조합으로 저장 |
|
|
48
|
+
| `실행` | 터미널(새 탭/새 창)과 실행할 명령을 고르고 실행 |
|
|
49
|
+
|
|
50
|
+
실행 모달에서 고르는 항목:
|
|
51
|
+
|
|
52
|
+
- **가상환경** — conda 환경과 venv가 그룹으로 나뉘어 표시됩니다. 프로젝트 폴더 안에 venv가
|
|
53
|
+
있거나 `environment.yml` / `.ccdock-env`가 있으면 후보로 먼저 제안합니다
|
|
54
|
+
- **터미널** — *기존 창에 새 탭* (열린 Windows Terminal 창이 있을 때) / *새 창으로 열기*
|
|
55
|
+
- **어느 창에 탭을 추가할까요?** — 창이 2개 이상이면 창 제목으로 골라서 지정합니다
|
|
56
|
+
- **실행할 명령** — `claude` / `claude --continue` / `claude --resume` / 프롬프트만
|
|
57
|
+
|
|
58
|
+
### 가상환경 인식 범위
|
|
59
|
+
|
|
60
|
+
| 종류 | 인식 방법 | 활성화 |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| conda | `~/.conda/environments.txt` | `activate.bat <이름>` |
|
|
63
|
+
| venv | 프로젝트 폴더와 그 상위 폴더 바로 아래의 `pyvenv.cfg` 보유 폴더 | `<경로>\Scripts\activate.bat` |
|
|
64
|
+
|
|
65
|
+
프로젝트 폴더 안에 만들어 둔 `.venv`, `mvkit-env` 같은 가상환경이 자동으로 목록에 올라옵니다.
|
|
66
|
+
|
|
67
|
+
### 디스크 사용량
|
|
68
|
+
|
|
69
|
+
목록의 각 환경에 `claude312 — 792.4 MB` 처럼 크기가 함께 표시됩니다. 환경 하나를 훑는 데
|
|
70
|
+
0.4~1초가 걸리므로 화면을 막지 않고 배경 스레드가 순서대로 재며, 결과는 `~/.ccdock/sizes.json`에
|
|
71
|
+
남아 다음 실행부터는 즉시 나옵니다(7일 후 자동 재측정). 헤더의 합계 배지를 누르면 다시 잽니다.
|
|
72
|
+
|
|
73
|
+
`base`는 `envs/`와 `pkgs/`를 빼고 계산합니다. 그러지 않으면 다른 환경과 패키지 캐시가
|
|
74
|
+
전부 포함돼 실제보다 훨씬 크게 나옵니다.
|
|
75
|
+
|
|
76
|
+
## 동작 방식
|
|
77
|
+
|
|
78
|
+
| 정보 | 출처 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| 프로젝트 경로 | `~/.claude.json` 의 `projects` 키 |
|
|
81
|
+
| 마지막 작업 시각 | `~/.claude/projects/<슬러그>/*.jsonl` 파일 mtime |
|
|
82
|
+
| 가상환경 목록 | `~/.conda/environments.txt` (없으면 conda 설치 폴더 스캔) |
|
|
83
|
+
| 사용자 설정 | `~/.ccdock/config.json` |
|
|
84
|
+
|
|
85
|
+
실행 시 임시 배치 파일을 만들어 `wt -w 0 new-tab`(새 탭) 또는 `wt -w new`(새 창)로 띄웁니다.
|
|
86
|
+
`wt -w 0`은 "가장 최근에 사용한 창"을 뜻하므로, 특정 창을 지정하면 Win32 API로 그 창을 먼저
|
|
87
|
+
앞으로 가져온 뒤 탭을 붙입니다. Windows Terminal이 없으면 `start cmd`로 대체합니다. 새 창에는 부모 프로세스의
|
|
88
|
+
`VIRTUAL_ENV` / `CONDA_PREFIX` 흔적을 제거한 깨끗한 환경이 전달되므로, ccdock 자체를
|
|
89
|
+
어떤 가상환경에서 실행하든 선택한 환경이 정확히 활성화됩니다.
|
|
90
|
+
|
|
91
|
+
## 보안
|
|
92
|
+
|
|
93
|
+
서버는 `127.0.0.1`에만 바인딩되며, 모든 API는 실행할 때마다 새로 만들어지는 토큰을 요구합니다.
|
|
94
|
+
브라우저는 토큰이 포함된 URL로 자동으로 열립니다.
|
|
95
|
+
|
|
96
|
+
## 요구 사항
|
|
97
|
+
|
|
98
|
+
- Windows (Windows Terminal이 있으면 탭 기능 사용 가능)
|
|
99
|
+
- Python 3.8+
|
|
100
|
+
- Anaconda 또는 Miniconda
|
|
101
|
+
|
|
102
|
+
## 라이선스
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""ccdock 진입점."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import __version__, launcher, scanner, server, store
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _safe_console():
|
|
10
|
+
"""cp949 콘솔에서도 한글 출력이 예외로 죽지 않게 한다."""
|
|
11
|
+
for stream in (sys.stdout, sys.stderr):
|
|
12
|
+
try:
|
|
13
|
+
stream.reconfigure(errors="replace")
|
|
14
|
+
except (AttributeError, ValueError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cmd_list(args):
|
|
19
|
+
for i, p in enumerate(scanner.collect(), 1):
|
|
20
|
+
print("%3d %-38s %-10s %s" % (
|
|
21
|
+
i, p["name"], scanner.relative_time(p["mtime"]), p["env"] or "-"))
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _cmd_workspace(args):
|
|
26
|
+
cfg = store.load()
|
|
27
|
+
items = cfg.get("workspaces", {}).get(args.name)
|
|
28
|
+
if not items:
|
|
29
|
+
names = ", ".join(cfg.get("workspaces", {})) or "(없음)"
|
|
30
|
+
print("워크스페이스를 찾을 수 없습니다: %s" % args.name, file=sys.stderr)
|
|
31
|
+
print("저장된 워크스페이스: %s" % names, file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
for r in launcher.launch_many(items, args.mode):
|
|
34
|
+
print((" OK " if r.get("ok") else " 실패 ") + str(r.get("path")))
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv=None):
|
|
39
|
+
ap = argparse.ArgumentParser(
|
|
40
|
+
prog="ccdock", description="Claude Code 프로젝트/가상환경 런처 대시보드")
|
|
41
|
+
ap.add_argument("-V", "--version", action="version", version="ccdock " + __version__)
|
|
42
|
+
ap.add_argument("-p", "--port", type=int, default=None, help="대시보드 포트")
|
|
43
|
+
ap.add_argument("-n", "--no-browser", action="store_true", help="브라우저를 열지 않음")
|
|
44
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
45
|
+
|
|
46
|
+
sub.add_parser("list", help="프로젝트 목록을 터미널에 출력")
|
|
47
|
+
w = sub.add_parser("workspace", help="저장된 워크스페이스 실행")
|
|
48
|
+
w.add_argument("name")
|
|
49
|
+
w.add_argument("-m", "--mode", choices=("window", "tab"), default="window")
|
|
50
|
+
|
|
51
|
+
args = ap.parse_args(argv)
|
|
52
|
+
_safe_console()
|
|
53
|
+
if args.cmd == "list":
|
|
54
|
+
return _cmd_list(args)
|
|
55
|
+
if args.cmd == "workspace":
|
|
56
|
+
return _cmd_workspace(args)
|
|
57
|
+
return server.serve(port=args.port, open_browser=not args.no_browser)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
sys.exit(main())
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""conda 가상환경 목록과 activate 스크립트 위치를 찾는다."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
HOME = Path.home()
|
|
10
|
+
_cache = {"at": 0.0, "envs": None, "root": None}
|
|
11
|
+
_TTL = 60.0
|
|
12
|
+
|
|
13
|
+
_ROOT_CANDIDATES = (
|
|
14
|
+
"anaconda3", "miniconda3", "Anaconda3", "Miniconda3",
|
|
15
|
+
"AppData/Local/anaconda3", "AppData/Local/miniconda3",
|
|
16
|
+
"mambaforge", "miniforge3",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def conda_root():
|
|
21
|
+
"""conda 설치 루트(base 경로)를 찾는다."""
|
|
22
|
+
if _cache["root"]:
|
|
23
|
+
return _cache["root"]
|
|
24
|
+
cands = []
|
|
25
|
+
for var in ("CONDA_ROOT", "CONDA_PREFIX_1", "CONDA_EXE"):
|
|
26
|
+
v = os.environ.get(var)
|
|
27
|
+
if not v:
|
|
28
|
+
continue
|
|
29
|
+
p = Path(v)
|
|
30
|
+
cands.append(p.parents[2] if var == "CONDA_EXE" and len(p.parents) > 2 else p)
|
|
31
|
+
for name in _ROOT_CANDIDATES:
|
|
32
|
+
cands.append(HOME / name)
|
|
33
|
+
cands.append(Path("C:/ProgramData/anaconda3"))
|
|
34
|
+
for c in cands:
|
|
35
|
+
try:
|
|
36
|
+
if (c / "Scripts" / "activate.bat").is_file():
|
|
37
|
+
_cache["root"] = c
|
|
38
|
+
return c
|
|
39
|
+
except OSError:
|
|
40
|
+
continue
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def activate_bat():
|
|
45
|
+
root = conda_root()
|
|
46
|
+
return str(root / "Scripts" / "activate.bat") if root else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _from_environments_txt():
|
|
50
|
+
"""~/.conda/environments.txt - conda 를 실행하지 않고 즉시 읽는 빠른 경로."""
|
|
51
|
+
f = HOME / ".conda" / "environments.txt"
|
|
52
|
+
if not f.is_file():
|
|
53
|
+
return []
|
|
54
|
+
out = []
|
|
55
|
+
try:
|
|
56
|
+
for line in f.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
57
|
+
line = line.strip()
|
|
58
|
+
if line and os.path.isdir(line):
|
|
59
|
+
out.append(line)
|
|
60
|
+
except OSError:
|
|
61
|
+
return []
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _from_envs_dir():
|
|
66
|
+
root = conda_root()
|
|
67
|
+
if not root:
|
|
68
|
+
return []
|
|
69
|
+
out = [str(root)]
|
|
70
|
+
envs = root / "envs"
|
|
71
|
+
if envs.is_dir():
|
|
72
|
+
try:
|
|
73
|
+
out += [str(d) for d in envs.iterdir() if (d / "python.exe").is_file() or d.is_dir()]
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _name_of(prefix, root):
|
|
80
|
+
p = Path(prefix)
|
|
81
|
+
if root and p == Path(root):
|
|
82
|
+
return "base"
|
|
83
|
+
return p.name
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_venv_id(env_id):
|
|
87
|
+
"""환경 식별자가 conda 이름인지 venv 경로인지 구분한다."""
|
|
88
|
+
s = str(env_id or "")
|
|
89
|
+
return ("\\" in s) or ("/" in s)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def venv_activate(prefix):
|
|
93
|
+
return str(Path(prefix) / "Scripts" / "activate.bat")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_venv(path):
|
|
97
|
+
p = Path(path)
|
|
98
|
+
try:
|
|
99
|
+
return (p / "pyvenv.cfg").is_file() and (p / "Scripts" / "activate.bat").is_file()
|
|
100
|
+
except OSError:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def scan_venvs(search_roots):
|
|
105
|
+
"""프로젝트 폴더 바로 아래에 있는 venv 를 찾는다 (.venv, venv, *-env 등)."""
|
|
106
|
+
out, seen = [], set()
|
|
107
|
+
for root in search_roots or ():
|
|
108
|
+
try:
|
|
109
|
+
if not os.path.isdir(root):
|
|
110
|
+
continue
|
|
111
|
+
for d in os.scandir(root):
|
|
112
|
+
if not d.is_dir():
|
|
113
|
+
continue
|
|
114
|
+
k = d.path.lower().rstrip("\\/")
|
|
115
|
+
if k in seen or not _is_venv(d.path):
|
|
116
|
+
continue
|
|
117
|
+
seen.add(k)
|
|
118
|
+
out.append({
|
|
119
|
+
"kind": "venv",
|
|
120
|
+
"id": d.path,
|
|
121
|
+
"name": d.name,
|
|
122
|
+
"prefix": d.path,
|
|
123
|
+
"where": os.path.basename(root) or root,
|
|
124
|
+
"python": (Path(d.path) / "Scripts" / "python.exe").is_file(),
|
|
125
|
+
})
|
|
126
|
+
except OSError:
|
|
127
|
+
continue
|
|
128
|
+
out.sort(key=lambda e: (e["name"].lower(), e["where"].lower()))
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def list_conda_envs(force=False):
|
|
133
|
+
"""conda 환경 목록. [{'kind':'conda','id':이름,'name':이름,'prefix':...}]"""
|
|
134
|
+
now = time.time()
|
|
135
|
+
if not force and _cache["envs"] is not None and now - _cache["at"] < _TTL:
|
|
136
|
+
return _cache["envs"]
|
|
137
|
+
|
|
138
|
+
root = conda_root()
|
|
139
|
+
prefixes = _from_environments_txt() or _from_envs_dir()
|
|
140
|
+
seen, out = set(), []
|
|
141
|
+
for pre in prefixes:
|
|
142
|
+
k = str(pre).lower().rstrip("\\/")
|
|
143
|
+
if k in seen:
|
|
144
|
+
continue
|
|
145
|
+
seen.add(k)
|
|
146
|
+
name = _name_of(pre, root)
|
|
147
|
+
py = Path(pre) / "python.exe"
|
|
148
|
+
out.append({
|
|
149
|
+
"kind": "conda",
|
|
150
|
+
"id": name,
|
|
151
|
+
"name": name,
|
|
152
|
+
"prefix": str(pre),
|
|
153
|
+
"where": "conda",
|
|
154
|
+
"python": py.is_file(),
|
|
155
|
+
})
|
|
156
|
+
out.sort(key=lambda e: (e["name"] != "base", e["name"].lower()))
|
|
157
|
+
_cache["envs"], _cache["at"] = out, now
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _size_exclusions(entry):
|
|
162
|
+
"""base 환경은 envs/ 와 pkgs/ 를 빼야 자기 자신의 크기가 나온다."""
|
|
163
|
+
if entry["kind"] == "conda" and entry["name"] == "base":
|
|
164
|
+
root = Path(entry["prefix"])
|
|
165
|
+
return (str(root / "envs"), str(root / "pkgs"))
|
|
166
|
+
return ()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def list_envs(search_roots=None, force=False, with_size=True):
|
|
170
|
+
"""conda 환경과 프로젝트 폴더의 venv 를 합쳐 돌려준다."""
|
|
171
|
+
from . import sizes
|
|
172
|
+
|
|
173
|
+
out = [dict(e) for e in list_conda_envs(force=force)]
|
|
174
|
+
known = {e["id"].lower() for e in out}
|
|
175
|
+
for v in scan_venvs(search_roots or []):
|
|
176
|
+
if v["id"].lower() not in known:
|
|
177
|
+
out.append(v)
|
|
178
|
+
|
|
179
|
+
if with_size:
|
|
180
|
+
for e in out:
|
|
181
|
+
s = sizes.get(e["prefix"], _size_exclusions(e))
|
|
182
|
+
e["size"] = s["bytes"] if s else None
|
|
183
|
+
e["files"] = s["files"] if s else None
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def env_names():
|
|
188
|
+
return [e["name"] for e in list_conda_envs()]
|