sufleur-cli 0.1.0__py3-none-win_amd64.whl
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.
sufleur_cli/__init__.py
ADDED
sufleur_cli/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from sufleur_cli import find_sufleur_bin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
binary = find_sufleur_bin()
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
result = subprocess.run([binary, *sys.argv[1:]])
|
|
16
|
+
except KeyboardInterrupt:
|
|
17
|
+
sys.exit(2)
|
|
18
|
+
sys.exit(result.returncode)
|
|
19
|
+
else:
|
|
20
|
+
os.execvp(binary, [binary, *sys.argv[1:]])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import sysconfig
|
|
6
|
+
from fnmatch import fnmatch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SufleurNotFound(FileNotFoundError):
|
|
10
|
+
"""Raised when the sufleur binary cannot be located on disk."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_sufleur_bin() -> str:
|
|
14
|
+
"""Return the absolute path to the bundled `sufleur` binary.
|
|
15
|
+
|
|
16
|
+
Mirrors astral-sh/uv's `find_uv_bin` search order so the same set of
|
|
17
|
+
install layouts works: ordinary venv, system pip, `pip install --prefix`,
|
|
18
|
+
`pip install --target`, and `pip install --user`.
|
|
19
|
+
"""
|
|
20
|
+
binary_name = "sufleur" + (sysconfig.get_config_var("EXE") or "")
|
|
21
|
+
|
|
22
|
+
targets: list[str | None] = [
|
|
23
|
+
sysconfig.get_path("scripts"),
|
|
24
|
+
sysconfig.get_path("scripts", vars={"base": sys.base_prefix}),
|
|
25
|
+
_join(_matching_parents(_module_path(), _site_packages_match()), _scripts_subdir()),
|
|
26
|
+
_join(_matching_parents(_module_path(), "sufleur_cli"), "bin"),
|
|
27
|
+
sysconfig.get_path("scripts", scheme=_user_scheme()),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
seen: list[str] = []
|
|
31
|
+
for target in targets:
|
|
32
|
+
if not target or target in seen:
|
|
33
|
+
continue
|
|
34
|
+
seen.append(target)
|
|
35
|
+
candidate = os.path.join(target, binary_name)
|
|
36
|
+
if os.path.isfile(candidate):
|
|
37
|
+
return candidate
|
|
38
|
+
|
|
39
|
+
locations = "\n".join(f" - {t}" for t in seen)
|
|
40
|
+
raise SufleurNotFound(
|
|
41
|
+
f"Could not find the sufleur binary in any of the following locations:\n{locations}\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _site_packages_match() -> str:
|
|
46
|
+
if sys.platform == "win32":
|
|
47
|
+
return "Lib/site-packages/sufleur_cli"
|
|
48
|
+
return "lib/python*/site-packages/sufleur_cli"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _scripts_subdir() -> str:
|
|
52
|
+
return "Scripts" if sys.platform == "win32" else "bin"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _module_path() -> str:
|
|
56
|
+
return os.path.dirname(__file__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _matching_parents(path: str | None, match: str) -> str | None:
|
|
60
|
+
if not path:
|
|
61
|
+
return None
|
|
62
|
+
parts = path.split(os.sep)
|
|
63
|
+
match_parts = match.split("/")
|
|
64
|
+
if len(parts) < len(match_parts):
|
|
65
|
+
return None
|
|
66
|
+
if not all(
|
|
67
|
+
fnmatch(part, match_part)
|
|
68
|
+
for part, match_part in zip(reversed(parts), reversed(match_parts))
|
|
69
|
+
):
|
|
70
|
+
return None
|
|
71
|
+
return os.sep.join(parts[: -len(match_parts)])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _join(path: str | None, *parts: str) -> str | None:
|
|
75
|
+
if not path:
|
|
76
|
+
return None
|
|
77
|
+
return os.path.join(path, *parts)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _user_scheme() -> str:
|
|
81
|
+
if sys.version_info >= (3, 10):
|
|
82
|
+
return sysconfig.get_preferred_scheme("user")
|
|
83
|
+
if os.name == "nt":
|
|
84
|
+
return "nt_user"
|
|
85
|
+
if sys.platform == "darwin" and getattr(sys, "_framework", ""):
|
|
86
|
+
return "osx_framework_user"
|
|
87
|
+
return "posix_user"
|
sufleur_cli/py.typed
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sufleur-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for sufleur — type-safe codegen for versioned LLM prompts.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sufleur/cli
|
|
6
|
+
Project-URL: Issues, https://github.com/sufleur/cli/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/sufleur/cli
|
|
8
|
+
Author: Tamás Vajda
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: cli,codegen,llm,prompts,python,sufleur
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Go
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: generated
|
|
27
|
+
Requires-Dist: chevron; extra == 'generated'
|
|
28
|
+
Requires-Dist: pydantic; extra == 'generated'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# sufleur-cli
|
|
32
|
+
|
|
33
|
+
The CLI for [**Sufleur**](https://sufleur.com) — the registry where you author, version, and publish LLM prompts. This is the consumer side: it installs prompts from your Sufleur workspace into your project the way `pip` installs packages — declared in `sufleur.yaml`, locked to `sufleur-lock.yaml`, generated into one Python module with full types and runtime helpers.
|
|
34
|
+
|
|
35
|
+
Create a workspace and start authoring prompts at <https://sufleur.com>.
|
|
36
|
+
|
|
37
|
+
## What you call from your code
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from generated.prompts import get_prompt
|
|
41
|
+
|
|
42
|
+
review = get_prompt("@my-workspace/code-review")
|
|
43
|
+
|
|
44
|
+
rendered = review.render("en", {"diff": "...", "language": "go"})
|
|
45
|
+
prompt: str = rendered["prompt"] # ready-to-send prompt string
|
|
46
|
+
|
|
47
|
+
result = review.parse_output(llm_response_text)
|
|
48
|
+
if result["success"]:
|
|
49
|
+
result["data"] # Pydantic model, validated against the prompt's output schema
|
|
50
|
+
else:
|
|
51
|
+
result["error"]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`"@my-workspace/code-review"` is checked at type-check time (mypy / pyright): typos fail, the entrypoint name `"en"` is narrowed against the prompt's available entrypoints (via `@overload`), and the input is a `TypedDict` derived from the JSON Schema declared on that entrypoint. The version that resolves at codegen time is pinned in `sufleur-lock.yaml`.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install sufleur-cli
|
|
60
|
+
sufleur --help
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or with [pipx](https://pipx.pypa.io/) for an isolated install:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pipx install sufleur-cli
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The wrapper ships the prebuilt binary inside a per-platform wheel — pip selects the right one via PEP 425 platform tags. There's no Python interpreter in the invocation hot path; `sufleur` is the native binary on your `PATH`.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mkdir my-app && cd my-app
|
|
75
|
+
sufleur init # creates sufleur.yaml interactively
|
|
76
|
+
sufleur add @my-workspace/code-review ^1.0.0 # add + fetch + lock
|
|
77
|
+
sufleur generate # writes ./generated/prompts.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The generated module imports two runtime peers. Install them with the `[generated]` extra:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install 'sufleur-cli[generated]'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
…or add `chevron` (Mustache templating) and `pydantic` (output-schema validation, only needed when prompts have output schemas) directly to your project's dependencies. The CLI itself has no Python runtime deps; the `[generated]` extra exists so users who run `init`/`add`/`install` but never `generate` aren't forced to install code they don't use.
|
|
87
|
+
|
|
88
|
+
The generated code targets **Python 3.10+** (PEP 604 union syntax).
|
|
89
|
+
|
|
90
|
+
## What `sufleur generate` emits
|
|
91
|
+
|
|
92
|
+
A single `.py` module containing every prompt inlined (no runtime fetches). The public API is `get_prompt(name)`, which returns a result object with:
|
|
93
|
+
|
|
94
|
+
- **`render(entrypoint, input)` → `{"prompt": str}`** — Chevron renders the entrypoint template against `input`. The signature is narrowed via `@overload` per entrypoint, so type checkers reject the wrong input shape.
|
|
95
|
+
- **`metadata`** — a `TypedDict` containing `version`, your workspace's custom metadata, and (when applicable) `output_schema`.
|
|
96
|
+
- **`parse_output(raw)`** *(only present if the prompt has an output schema)* — strips ``` fences, JSON-parses, and validates with a Pydantic model generated from the prompt's JSON Schema. Returns `{"success": True, "data": <Model>}` or `{"success": False, "error": str}`.
|
|
97
|
+
|
|
98
|
+
Plus generated `TypedDict`s per entrypoint, with field docstrings for any schema property that has a `description`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
class CodeReview_EnInput(TypedDict):
|
|
102
|
+
diff: str
|
|
103
|
+
"""The unified diff to review."""
|
|
104
|
+
language: str
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Prompts published with `DRAFT` status emit a `warnings.warn(...)` when their `get_prompt` is called.
|
|
108
|
+
|
|
109
|
+
## sufleur.yaml
|
|
110
|
+
|
|
111
|
+
The manifest. Looks like:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
api_keys:
|
|
115
|
+
my-workspace: ${MY_WORKSPACE_API_KEY}
|
|
116
|
+
|
|
117
|
+
prompts:
|
|
118
|
+
'@my-workspace/greeting': '*'
|
|
119
|
+
'@my-workspace/code-review': '^2.0.0'
|
|
120
|
+
# alias: keep two pinned versions side-by-side under different names
|
|
121
|
+
'@my-workspace/code-review-strict': '@my-workspace/code-review@~1.4.0'
|
|
122
|
+
|
|
123
|
+
output:
|
|
124
|
+
language: python
|
|
125
|
+
file: ./generated/prompts.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Constraints are npm-style semver ranges (`^`, `~`, `>=`, exact, `*`). The resolution is recorded in `sufleur-lock.yaml`. **Commit both files** — `sufleur.yaml` is the source of truth, `sufleur-lock.yaml` is the receipt.
|
|
129
|
+
|
|
130
|
+
## CI usage
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
sufleur install --frozen # fail if lockfile is stale
|
|
134
|
+
sufleur generate
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`--frozen` is the `pip-compile`-equivalent: refuses to update the lockfile, hard-errors if the manifest and lockfile disagree.
|
|
138
|
+
|
|
139
|
+
## Commands
|
|
140
|
+
|
|
141
|
+
| Command | Description |
|
|
142
|
+
| ------- | ----------- |
|
|
143
|
+
| `sufleur init` | Interactive scaffolding for `sufleur.yaml`. |
|
|
144
|
+
| `sufleur add @ws/name [range]` | Add a prompt, fetch it, update the lockfile. `--alias <name>` keeps multiple versions; `--force` overwrites an existing entry. |
|
|
145
|
+
| `sufleur remove @ws/name` | Remove a prompt from the manifest and prune its cache (kept if another alias still resolves to the same version). |
|
|
146
|
+
| `sufleur install` | Resolve the manifest, fetch what's missing, refresh the lockfile. `--frozen` for CI. |
|
|
147
|
+
| `sufleur update [@ws/name]` | Re-resolve constraints — one prompt or all. |
|
|
148
|
+
| `sufleur generate` | Regenerate the output file from the lockfile + cache. |
|
|
149
|
+
|
|
150
|
+
`-v` / `--verbose` enables HTTP request/response logs on any command. Variables in `.env` are loaded automatically; per-workspace API keys can be referenced as `${ENV_VAR_NAME}` in `sufleur.yaml`.
|
|
151
|
+
|
|
152
|
+
## Invocation modes
|
|
153
|
+
|
|
154
|
+
The `sufleur` command on your `PATH` is the Go binary itself. For tools that prefer module-style invocation:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
python -m sufleur_cli --help
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This goes through a tiny Python wrapper that locates the binary and `os.execvp`s it (POSIX) or `subprocess.run`s it (Windows). Slightly slower because Python boots first, but useful when invoking the CLI programmatically from a Python tool that wants to be sure it's calling the binary in the active environment.
|
|
161
|
+
|
|
162
|
+
The `find_sufleur_bin()` helper is also importable:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from sufleur_cli import find_sufleur_bin
|
|
166
|
+
print(find_sufleur_bin()) # absolute path to the binary
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Supported platforms
|
|
170
|
+
|
|
171
|
+
| OS | Architectures |
|
|
172
|
+
| ------- | ---------------------------------------------- |
|
|
173
|
+
| macOS | x86_64, arm64 |
|
|
174
|
+
| Linux | x86_64, aarch64 (manylinux 2.17 / glibc 2.17+) |
|
|
175
|
+
| Windows | x86_64, arm64 |
|
|
176
|
+
|
|
177
|
+
Alpine / musl libc is currently unsupported (no musllinux wheel) — pip will refuse with "no matching distribution" rather than silently producing a broken install. There is no source distribution.
|
|
178
|
+
|
|
179
|
+
## Links
|
|
180
|
+
|
|
181
|
+
- **Sufleur platform** — author and manage prompts: <https://sufleur.com>
|
|
182
|
+
- **Source code, issues, release notes**: <https://github.com/sufleur/cli>
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sufleur_cli/__init__.py,sha256=uyZziI8rHkx3ekXq5X0wqdrIhzt94JQ8Mp2oroJmUbk,171
|
|
2
|
+
sufleur_cli/__main__.py,sha256=stdEvPSb2do6t0MwTn8BsRsaiPvZvCMTVWTrQfSl2bk,481
|
|
3
|
+
sufleur_cli/_find_sufleur.py,sha256=Ci2QiKRlgA_eUM15kpiFSTgXU0LQ-g6wwa07MJLgkyM,2605
|
|
4
|
+
sufleur_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
sufleur_cli-0.1.0.data/scripts/sufleur.exe,sha256=UILK9kRr7w372cpXmPFw0LgKJ2LqLfIR7wcPdHvV00E,12393472
|
|
6
|
+
sufleur_cli-0.1.0.dist-info/METADATA,sha256=PTi9cnyoL9O7q6cWJrgbsFn0PzBRv4BAJPl3ftryTZU,8014
|
|
7
|
+
sufleur_cli-0.1.0.dist-info/WHEEL,sha256=OKr2XcpSNWrtUe-CU6RMYrBnsfqGnZ3jZM4vKnozTRA,94
|
|
8
|
+
sufleur_cli-0.1.0.dist-info/RECORD,,
|