edcopy 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.
- edcopy-0.1.0/.gitignore +33 -0
- edcopy-0.1.0/PKG-INFO +76 -0
- edcopy-0.1.0/README.md +54 -0
- edcopy-0.1.0/pyproject.toml +44 -0
- edcopy-0.1.0/src/edcopy/__init__.py +2 -0
- edcopy-0.1.0/src/edcopy/cli.py +359 -0
- edcopy-0.1.0/uv.lock +35 -0
edcopy-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python cache/bytecode
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Virtual environments
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
|
|
10
|
+
# Build artifacts
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.eggs/
|
|
15
|
+
|
|
16
|
+
# Tooling caches
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# Editor/OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
|
|
29
|
+
# Generated by edcopy
|
|
30
|
+
prompt.md
|
|
31
|
+
|
|
32
|
+
# Dossier des exemples
|
|
33
|
+
examples/
|
edcopy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edcopy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interactive CLI to collect files and build a numbered prompt.md for LLM workflows.
|
|
5
|
+
Author: Edmond La Chance
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: cli,developer-tools,gemini,llm,prompt,tui
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
## edcopy
|
|
24
|
+
|
|
25
|
+
`edcopy` est un petit outil CLI pour préparer un `prompt.md` à partir de fichiers sélectionnés dans un TUI.
|
|
26
|
+
|
|
27
|
+
### Installation (global)
|
|
28
|
+
|
|
29
|
+
Depuis ce dossier:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Puis:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
edcopy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Usage rapide
|
|
42
|
+
|
|
43
|
+
Dans le TUI:
|
|
44
|
+
|
|
45
|
+
- Tape une ligne contenant un ou plusieurs chemins de fichiers (ex: `src/main.py README.md`)
|
|
46
|
+
- Utilise `Tab` pour autocompléter les fichiers (scan récursif depuis le dossier courant)
|
|
47
|
+
- Si tu tapes un chemin partiel (ex: `test1`) puis `Entrée`, `edcopy` prend automatiquement le meilleur match
|
|
48
|
+
- `Ctrl+C` termine la sélection et génère `prompt.md`
|
|
49
|
+
- `Entrée` sur une ligne vide génère aussi `prompt.md`
|
|
50
|
+
- Au lancement, un `prompt.md` existant est supprimé
|
|
51
|
+
|
|
52
|
+
Commandes disponibles:
|
|
53
|
+
|
|
54
|
+
- `@list` pour voir la sélection actuelle
|
|
55
|
+
- `@undo` pour enlever le dernier fichier ajouté
|
|
56
|
+
- `@clear` pour vider la sélection
|
|
57
|
+
- `@rescan` pour rescanner les fichiers
|
|
58
|
+
- `@done` pour générer
|
|
59
|
+
- `@quit` pour quitter sans générer
|
|
60
|
+
|
|
61
|
+
### Exemple de sortie
|
|
62
|
+
|
|
63
|
+
````md
|
|
64
|
+
## Fichier 1: `file.py`
|
|
65
|
+
Chemin: `/abs/path/to/file.py`
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
1 | print("hello")
|
|
69
|
+
2 | print("world")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Fichier 2: `file2.py`
|
|
75
|
+
Chemin: `/abs/path/to/file2.py`
|
|
76
|
+
````
|
edcopy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
## edcopy
|
|
2
|
+
|
|
3
|
+
`edcopy` est un petit outil CLI pour préparer un `prompt.md` à partir de fichiers sélectionnés dans un TUI.
|
|
4
|
+
|
|
5
|
+
### Installation (global)
|
|
6
|
+
|
|
7
|
+
Depuis ce dossier:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Puis:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
edcopy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Usage rapide
|
|
20
|
+
|
|
21
|
+
Dans le TUI:
|
|
22
|
+
|
|
23
|
+
- Tape une ligne contenant un ou plusieurs chemins de fichiers (ex: `src/main.py README.md`)
|
|
24
|
+
- Utilise `Tab` pour autocompléter les fichiers (scan récursif depuis le dossier courant)
|
|
25
|
+
- Si tu tapes un chemin partiel (ex: `test1`) puis `Entrée`, `edcopy` prend automatiquement le meilleur match
|
|
26
|
+
- `Ctrl+C` termine la sélection et génère `prompt.md`
|
|
27
|
+
- `Entrée` sur une ligne vide génère aussi `prompt.md`
|
|
28
|
+
- Au lancement, un `prompt.md` existant est supprimé
|
|
29
|
+
|
|
30
|
+
Commandes disponibles:
|
|
31
|
+
|
|
32
|
+
- `@list` pour voir la sélection actuelle
|
|
33
|
+
- `@undo` pour enlever le dernier fichier ajouté
|
|
34
|
+
- `@clear` pour vider la sélection
|
|
35
|
+
- `@rescan` pour rescanner les fichiers
|
|
36
|
+
- `@done` pour générer
|
|
37
|
+
- `@quit` pour quitter sans générer
|
|
38
|
+
|
|
39
|
+
### Exemple de sortie
|
|
40
|
+
|
|
41
|
+
````md
|
|
42
|
+
## Fichier 1: `file.py`
|
|
43
|
+
Chemin: `/abs/path/to/file.py`
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
1 | print("hello")
|
|
47
|
+
2 | print("world")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Fichier 2: `file2.py`
|
|
53
|
+
Chemin: `/abs/path/to/file2.py`
|
|
54
|
+
````
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "edcopy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Interactive CLI to collect files and build a numbered prompt.md for LLM workflows."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Edmond La Chance" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"cli",
|
|
17
|
+
"tui",
|
|
18
|
+
"prompt",
|
|
19
|
+
"llm",
|
|
20
|
+
"gemini",
|
|
21
|
+
"developer-tools",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Topic :: Software Development :: Build Tools",
|
|
34
|
+
"Topic :: Utilities",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"prompt-toolkit>=3.0.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
edcopy = "edcopy.cli:main"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/edcopy"]
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
from prompt_toolkit import PromptSession
|
|
12
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
13
|
+
|
|
14
|
+
IGNORED_DIRS = {
|
|
15
|
+
".git",
|
|
16
|
+
".hg",
|
|
17
|
+
".svn",
|
|
18
|
+
".venv",
|
|
19
|
+
"venv",
|
|
20
|
+
"node_modules",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
".mypy_cache",
|
|
23
|
+
".pytest_cache",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
TOKEN_PATTERN = re.compile(r"[^\s,;]+")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MentionCompleter(Completer):
|
|
30
|
+
def __init__(self, files: list[str]) -> None:
|
|
31
|
+
self.files = files
|
|
32
|
+
|
|
33
|
+
def set_files(self, files: list[str]) -> None:
|
|
34
|
+
self.files = files
|
|
35
|
+
|
|
36
|
+
def _match_rank(self, query: str, rel_path: str) -> int | None:
|
|
37
|
+
rel_lower = rel_path.lower()
|
|
38
|
+
basename_lower = Path(rel_path).name.lower()
|
|
39
|
+
|
|
40
|
+
if rel_lower.startswith(query):
|
|
41
|
+
return 0
|
|
42
|
+
if basename_lower.startswith(query):
|
|
43
|
+
return 1
|
|
44
|
+
if f"/{query}" in rel_lower:
|
|
45
|
+
return 2
|
|
46
|
+
if query in rel_lower:
|
|
47
|
+
return 3
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def ranked_matches(self, query: str) -> list[str]:
|
|
51
|
+
q = query.lower().strip()
|
|
52
|
+
ranked_matches: list[tuple[int, str]] = []
|
|
53
|
+
for rel_path in self.files:
|
|
54
|
+
if not q:
|
|
55
|
+
ranked_matches.append((0, rel_path))
|
|
56
|
+
continue
|
|
57
|
+
rank = self._match_rank(q, rel_path)
|
|
58
|
+
if rank is None:
|
|
59
|
+
continue
|
|
60
|
+
ranked_matches.append((rank, rel_path))
|
|
61
|
+
|
|
62
|
+
ranked_matches.sort(key=lambda item: (item[0], len(item[1]), item[1]))
|
|
63
|
+
return [path for _, path in ranked_matches]
|
|
64
|
+
|
|
65
|
+
def get_completions(self, document, complete_event):
|
|
66
|
+
text = document.text_before_cursor
|
|
67
|
+
match = re.search(r"(^|[\s,;])([^\s,;]*)$", text)
|
|
68
|
+
if not match:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
raw_prefix = match.group(2)
|
|
72
|
+
query = raw_prefix[1:] if raw_prefix.startswith("@") else raw_prefix
|
|
73
|
+
start_position = -len(raw_prefix)
|
|
74
|
+
|
|
75
|
+
for count, rel_path in enumerate(self.ranked_matches(query)):
|
|
76
|
+
yield Completion(
|
|
77
|
+
text=rel_path,
|
|
78
|
+
start_position=start_position,
|
|
79
|
+
display=rel_path,
|
|
80
|
+
)
|
|
81
|
+
if count >= 199:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def discover_files(root: Path) -> list[str]:
|
|
86
|
+
discovered: list[str] = []
|
|
87
|
+
for current, dirs, files in os.walk(root, topdown=True):
|
|
88
|
+
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
|
|
89
|
+
current_path = Path(current)
|
|
90
|
+
for filename in files:
|
|
91
|
+
path = current_path / filename
|
|
92
|
+
try:
|
|
93
|
+
rel = path.relative_to(root).as_posix()
|
|
94
|
+
except ValueError:
|
|
95
|
+
continue
|
|
96
|
+
discovered.append(rel)
|
|
97
|
+
discovered.sort()
|
|
98
|
+
return discovered
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def extract_candidates(line: str) -> list[str]:
|
|
102
|
+
candidates: list[str] = []
|
|
103
|
+
for token in TOKEN_PATTERN.findall(line):
|
|
104
|
+
value = token[1:] if token.startswith("@") else token
|
|
105
|
+
if value:
|
|
106
|
+
candidates.append(value)
|
|
107
|
+
return candidates
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def resolve_mentions(root: Path, mentions: Iterable[str]) -> list[Path]:
|
|
111
|
+
resolved: list[Path] = []
|
|
112
|
+
for mention in mentions:
|
|
113
|
+
path = Path(mention)
|
|
114
|
+
if not path.is_absolute():
|
|
115
|
+
path = (root / path).resolve()
|
|
116
|
+
else:
|
|
117
|
+
path = path.resolve()
|
|
118
|
+
resolved.append(path)
|
|
119
|
+
return resolved
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_numbered_lines(content: str) -> str:
|
|
123
|
+
lines = content.splitlines()
|
|
124
|
+
if not lines:
|
|
125
|
+
return "1 | "
|
|
126
|
+
return "\n".join(f"{idx} | {line}" for idx, line in enumerate(lines, start=1))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_file_content(path: Path) -> str:
|
|
130
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def language_from_suffix(path: Path) -> str:
|
|
134
|
+
mapping = {
|
|
135
|
+
".py": "python",
|
|
136
|
+
".js": "javascript",
|
|
137
|
+
".ts": "typescript",
|
|
138
|
+
".tsx": "tsx",
|
|
139
|
+
".jsx": "jsx",
|
|
140
|
+
".json": "json",
|
|
141
|
+
".md": "markdown",
|
|
142
|
+
".yml": "yaml",
|
|
143
|
+
".yaml": "yaml",
|
|
144
|
+
".sh": "bash",
|
|
145
|
+
".toml": "toml",
|
|
146
|
+
".html": "html",
|
|
147
|
+
".css": "css",
|
|
148
|
+
".sql": "sql",
|
|
149
|
+
".go": "go",
|
|
150
|
+
".rs": "rust",
|
|
151
|
+
".java": "java",
|
|
152
|
+
".c": "c",
|
|
153
|
+
".cpp": "cpp",
|
|
154
|
+
}
|
|
155
|
+
return mapping.get(path.suffix.lower(), "text")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_file_section(index: int, file_path: Path, content: str) -> str:
|
|
159
|
+
language = language_from_suffix(file_path)
|
|
160
|
+
numbered = format_numbered_lines(content)
|
|
161
|
+
return "\n".join(
|
|
162
|
+
[
|
|
163
|
+
f"## Fichier {index}: `{file_path.name}`",
|
|
164
|
+
f"Chemin: `{file_path}`",
|
|
165
|
+
"",
|
|
166
|
+
f"```{language}",
|
|
167
|
+
numbered,
|
|
168
|
+
"```",
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def write_prompt(selected_files: list[Path], output: Path) -> None:
|
|
174
|
+
if not selected_files:
|
|
175
|
+
output.write_text("", encoding="utf-8")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
sections: list[str] = []
|
|
179
|
+
for index, file_path in enumerate(selected_files, start=1):
|
|
180
|
+
try:
|
|
181
|
+
content = read_file_content(file_path)
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
sections.append(
|
|
184
|
+
"\n".join(
|
|
185
|
+
[
|
|
186
|
+
f"## Fichier {index}: `{file_path.name}`",
|
|
187
|
+
f"Chemin: `{file_path}`",
|
|
188
|
+
"",
|
|
189
|
+
f"[ERROR] Unable to read file: {exc}",
|
|
190
|
+
]
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
sections.append(format_file_section(index, file_path, content))
|
|
196
|
+
|
|
197
|
+
output.write_text("\n\n---\n\n".join(sections) + "\n", encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_tui(root: Path, output: Path) -> int:
|
|
201
|
+
if output.exists():
|
|
202
|
+
try:
|
|
203
|
+
output.unlink()
|
|
204
|
+
print(f"Sortie précédente supprimée: {output}")
|
|
205
|
+
except OSError as exc:
|
|
206
|
+
print(f"[WARN] Impossible de supprimer {output}: {exc}")
|
|
207
|
+
|
|
208
|
+
file_index = discover_files(root)
|
|
209
|
+
completer = MentionCompleter(file_index)
|
|
210
|
+
session = PromptSession(completer=completer, complete_while_typing=True)
|
|
211
|
+
|
|
212
|
+
selected: OrderedDict[Path, None] = OrderedDict()
|
|
213
|
+
|
|
214
|
+
print(f"edcopy - root: {root}")
|
|
215
|
+
print(f"Sortie: {output}")
|
|
216
|
+
print("Ajoute des fichiers en tapant leur chemin puis Entrée.")
|
|
217
|
+
print("Commandes: @list, @undo, @clear, @rescan, @done, @quit")
|
|
218
|
+
print("Ligne vide => @done | Ctrl+C => terminer et générer")
|
|
219
|
+
|
|
220
|
+
while True:
|
|
221
|
+
try:
|
|
222
|
+
line = session.prompt("edcopy> ")
|
|
223
|
+
except KeyboardInterrupt:
|
|
224
|
+
print("\nFin de sélection (Ctrl+C).")
|
|
225
|
+
break
|
|
226
|
+
except EOFError:
|
|
227
|
+
print()
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
stripped = line.strip()
|
|
231
|
+
if stripped == "":
|
|
232
|
+
break
|
|
233
|
+
if stripped == "@quit":
|
|
234
|
+
print("Aucun fichier généré.")
|
|
235
|
+
return 0
|
|
236
|
+
if stripped == "@done":
|
|
237
|
+
break
|
|
238
|
+
if stripped == "@clear":
|
|
239
|
+
selected.clear()
|
|
240
|
+
print("Sélection vidée.")
|
|
241
|
+
continue
|
|
242
|
+
if stripped == "@list":
|
|
243
|
+
if not selected:
|
|
244
|
+
print("Aucun fichier sélectionné.")
|
|
245
|
+
else:
|
|
246
|
+
print("Fichiers sélectionnés:")
|
|
247
|
+
for item in selected:
|
|
248
|
+
print(f"- {item}")
|
|
249
|
+
continue
|
|
250
|
+
if stripped == "@undo":
|
|
251
|
+
if not selected:
|
|
252
|
+
print("Aucun fichier à retirer.")
|
|
253
|
+
continue
|
|
254
|
+
removed, _ = selected.popitem(last=True)
|
|
255
|
+
print(f"Retiré: {removed}")
|
|
256
|
+
continue
|
|
257
|
+
if stripped == "@rescan":
|
|
258
|
+
file_index = discover_files(root)
|
|
259
|
+
completer.set_files(file_index)
|
|
260
|
+
print(f"Rescan terminé: {len(file_index)} fichiers indexés.")
|
|
261
|
+
continue
|
|
262
|
+
if stripped.startswith("/"):
|
|
263
|
+
print("Commande invalide. Utilise le préfixe @ (ex: @list).")
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
candidates = extract_candidates(line)
|
|
267
|
+
if not candidates:
|
|
268
|
+
print("Aucun chemin détecté.")
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
for raw_candidate in candidates:
|
|
272
|
+
candidate = resolve_mentions(root, [raw_candidate])[0]
|
|
273
|
+
auto_label = ""
|
|
274
|
+
if not candidate.exists() and not Path(raw_candidate).is_absolute():
|
|
275
|
+
matches = completer.ranked_matches(raw_candidate)
|
|
276
|
+
if matches:
|
|
277
|
+
matched_rel = matches[0]
|
|
278
|
+
candidate = (root / matched_rel).resolve()
|
|
279
|
+
auto_label = f"[AUTO] {raw_candidate} -> {matched_rel} | "
|
|
280
|
+
|
|
281
|
+
if not candidate.exists():
|
|
282
|
+
print(f"[WARN] Introuvable: {raw_candidate}")
|
|
283
|
+
continue
|
|
284
|
+
if not candidate.is_file():
|
|
285
|
+
print(f"[WARN] Pas un fichier: {candidate}")
|
|
286
|
+
continue
|
|
287
|
+
if candidate in selected:
|
|
288
|
+
print(f"[SKIP] Déjà ajouté: {candidate}")
|
|
289
|
+
continue
|
|
290
|
+
selected[candidate] = None
|
|
291
|
+
print(f"{auto_label}[OK] Ajouté: {candidate}")
|
|
292
|
+
|
|
293
|
+
write_prompt(list(selected.keys()), output)
|
|
294
|
+
print(f"prompt généré: {output} ({len(selected)} fichiers)")
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
299
|
+
parser = argparse.ArgumentParser(
|
|
300
|
+
prog="edcopy",
|
|
301
|
+
description="Collect files from a TUI and build a numbered prompt.md",
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"-o",
|
|
305
|
+
"--output",
|
|
306
|
+
default="prompt.md",
|
|
307
|
+
help="Chemin de sortie (défaut: prompt.md)",
|
|
308
|
+
)
|
|
309
|
+
parser.add_argument(
|
|
310
|
+
"-f",
|
|
311
|
+
"--file",
|
|
312
|
+
action="append",
|
|
313
|
+
default=[],
|
|
314
|
+
help="Ajoute un fichier directement (peut être répété).",
|
|
315
|
+
)
|
|
316
|
+
return parser.parse_args(argv)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def run_non_interactive(root: Path, files: list[str], output: Path) -> int:
|
|
320
|
+
selected: OrderedDict[Path, None] = OrderedDict()
|
|
321
|
+
for item in files:
|
|
322
|
+
mention = item[1:] if item.startswith("@") else item
|
|
323
|
+
candidate = Path(mention)
|
|
324
|
+
if not candidate.is_absolute():
|
|
325
|
+
candidate = (root / candidate).resolve()
|
|
326
|
+
else:
|
|
327
|
+
candidate = candidate.resolve()
|
|
328
|
+
|
|
329
|
+
if not candidate.exists():
|
|
330
|
+
print(f"[WARN] Introuvable: {candidate}")
|
|
331
|
+
continue
|
|
332
|
+
if not candidate.is_file():
|
|
333
|
+
print(f"[WARN] Pas un fichier: {candidate}")
|
|
334
|
+
continue
|
|
335
|
+
selected[candidate] = None
|
|
336
|
+
|
|
337
|
+
if not selected:
|
|
338
|
+
print("Aucun fichier valide fourni.")
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
write_prompt(list(selected.keys()), output)
|
|
342
|
+
print(f"prompt généré: {output} ({len(selected)} fichiers)")
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def main() -> int:
|
|
347
|
+
args = parse_args(sys.argv[1:])
|
|
348
|
+
root = Path.cwd()
|
|
349
|
+
output = Path(args.output)
|
|
350
|
+
if not output.is_absolute():
|
|
351
|
+
output = (root / output).resolve()
|
|
352
|
+
|
|
353
|
+
if args.file:
|
|
354
|
+
return run_non_interactive(root, args.file, output)
|
|
355
|
+
return run_tui(root, output)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
if __name__ == "__main__":
|
|
359
|
+
raise SystemExit(main())
|
edcopy-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "edcopy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
source = { editable = "." }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "prompt-toolkit" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[package.metadata]
|
|
14
|
+
requires-dist = [{ name = "prompt-toolkit", specifier = ">=3.0.0" }]
|
|
15
|
+
|
|
16
|
+
[[package]]
|
|
17
|
+
name = "prompt-toolkit"
|
|
18
|
+
version = "3.0.52"
|
|
19
|
+
source = { registry = "https://pypi.org/simple" }
|
|
20
|
+
dependencies = [
|
|
21
|
+
{ name = "wcwidth" },
|
|
22
|
+
]
|
|
23
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
|
24
|
+
wheels = [
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "wcwidth"
|
|
30
|
+
version = "0.6.0"
|
|
31
|
+
source = { registry = "https://pypi.org/simple" }
|
|
32
|
+
sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
|
|
33
|
+
wheels = [
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
|
|
35
|
+
]
|