sufleur-cli 0.1.0__py3-none-win_arm64.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.
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from ._find_sufleur import SufleurNotFound, find_sufleur_bin
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["SufleurNotFound", "find_sufleur_bin"]
@@ -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
@@ -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=N56Vr-oMCLnAie0H6-b1xG74_NJCLR4nKwLjZC9a7lI,11681280
6
+ sufleur_cli-0.1.0.dist-info/METADATA,sha256=PTi9cnyoL9O7q6cWJrgbsFn0PzBRv4BAJPl3ftryTZU,8014
7
+ sufleur_cli-0.1.0.dist-info/WHEEL,sha256=w7VOCnMnx9r5-lu2GjBVd6BdGrPc0Fm_UFMvUozKtTg,94
8
+ sufleur_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_arm64