Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,17 @@ jobs:
run: rustup toolchain install stable --profile minimal
- name: Check
run: make check
python-compat:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v5
- name: Test Python SDK
run: uv run --directory python --python ${{ matrix.python-version }} pytest tests -q
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ permissions:
jobs:
release:
runs-on: ubuntu-latest
environment: pypi
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -48,7 +49,7 @@ print(re.search(r'^version = "([^"]+)"$', text, re.M).group(1))
PY
)"
test "$python_version" = "$version"
go_cobra_core_version="$(awk '/github.com\/samzong\/kitup\/go / { print $2; exit }' go-cobra/go.mod)"
go_cobra_core_version="$(awk '/github.com\/lathe-cli\/kitup\/go / { print $2; exit }' go-cobra/go.mod)"
test "$go_cobra_core_version" = "v$version"
- name: Check published versions
id: published
Expand All @@ -64,7 +65,7 @@ PY
else
echo "crate=false" >> "$GITHUB_OUTPUT"
fi
if curl -fsS "https://pypi.org/pypi/kitup/${version}/json" >/dev/null 2>&1; then
if curl -fsS "https://pypi.org/pypi/kitup-sdk/${version}/json" >/dev/null 2>&1; then
echo "pypi=true" >> "$GITHUB_OUTPUT"
else
echo "pypi=false" >> "$GITHUB_OUTPUT"
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Do not publish packages from a pull request.
Use `make release-patch`, `make release-minor`, or `make release-major` from a clean, up-to-date `main` branch to create the release branch and version commit. Open and merge the release PR manually, then tag `main` manually. The release workflow publishes:

- `@kitup/sdk`
- `kitup` on PyPI
- `kitup-sdk` on PyPI
- `kitup` on crates.io
- `github.com/lathe-cli/kitup/go` through the `go/vX.Y.Z` tag
- `github.com/lathe-cli/kitup/go-cobra` through the `go-cobra/vX.Y.Z` tag
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ The final install report contains `installed`, `updated`, `skipped`, `conflicts`
Install:

```bash
pip install kitup
pip install kitup-sdk
```

Use the workflow API for user-facing install commands:
Expand Down
2 changes: 1 addition & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Implemented functions:

## Python

Package: `kitup`
Package: `kitup-sdk`

```python
from kitup import (
Expand Down
4 changes: 2 additions & 2 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`kitup` publishes one version across five package surfaces:

- npm: `@kitup/sdk`
- PyPI: `kitup`
- PyPI: `kitup-sdk`
- crates.io: `kitup`
- Go module: `github.com/lathe-cli/kitup/go`
- Go Cobra adapter: `github.com/lathe-cli/kitup/go-cobra`
Expand Down Expand Up @@ -87,4 +87,4 @@ Run the public install smoke check manually with:
scripts/smoke-release.sh X.Y.Z
```

The smoke check installs from npm, PyPI, crates.io, the public Go module, and the public Go Cobra adapter, then verifies that each SDK can load the default host spec or instantiate its adapter.
The smoke check installs from npm, PyPI (`kitup-sdk`), crates.io, the public Go module, and the public Go Cobra adapter, then verifies that each SDK can load the default host spec or instantiate its adapter.
6 changes: 3 additions & 3 deletions examples/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[project]
name = "kitup-example-python"
version = "0.0.0"
requires-python = ">=3.14"
dependencies = ["kitup"]
requires-python = ">=3.10"
dependencies = ["kitup-sdk"]

[tool.uv.sources]
kitup = { path = "../../python" }
kitup-sdk = { path = "../../python" }
2 changes: 1 addition & 1 deletion python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Shared installer SDK for bundled Agent Skills.
## Install

```bash
pip install kitup
pip install kitup-sdk
```

## Use
Expand Down
11 changes: 7 additions & 4 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ requires = ["hatchling>=1.27.0"]
build-backend = "hatchling.build"

[project]
name = "kitup"
version = "0.1.1"
name = "kitup-sdk"
version = "0.1.2"
description = "Shared installer SDK for bundled Agent Skills."
readme = "README.md"
requires-python = ">=3.14"
requires-python = ">=3.10"
license = { text = "MIT" }
keywords = ["agent", "skills", "installer", "sdk"]
dependencies = []
Expand All @@ -27,5 +27,8 @@ dev = [
[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.hatch.build.targets.wheel]
packages = ["src/kitup"]

[tool.ruff]
target-version = "py314"
target-version = "py310"
2 changes: 1 addition & 1 deletion python/src/kitup/_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def read_install_metadata(target_dir: Path) -> dict[str, object] | None:
return None
try:
payload = json.loads(metadata_file.read_text(encoding="utf-8"))
except OSError, ValueError:
except (OSError, ValueError):
return None
if not isinstance(payload, dict):
return None
Expand Down
22 changes: 18 additions & 4 deletions python/src/kitup/workflow.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from dataclasses import replace
import io
import sys
from typing import Iterable

Expand Down Expand Up @@ -562,10 +561,27 @@ def _coerce_flag_values(value: object) -> list[str]:

class _LineReader:
def __init__(self, source: object | None) -> None:
self._lines: list[str] = list(self._iter_lines(source))
self._stream = (
source
if source is not None
and not isinstance(source, str | bytes)
and hasattr(source, "readline")
else None
)
self._lines: list[str] = (
[] if self._stream is not None else list(self._iter_lines(source))
)
self._index = 0

def read_line(self) -> str | None:
if self._stream is not None:
line = self._stream.readline()
if isinstance(line, bytes):
line = line.decode("utf-8")
line = str(line)
if line == "":
return None
return line.rstrip("\n").rstrip("\r")
if self._index >= len(self._lines):
return None
line = self._lines[self._index]
Expand All @@ -584,8 +600,6 @@ def _iter_lines(self, source: object | None) -> Iterable[str]:
if isinstance(contents, bytes):
return self._split_text(contents.decode("utf-8"))
return self._split_text(str(contents))
if isinstance(source, io.StringIO):
return self._split_text(source.getvalue())
if isinstance(source, Iterable):
chunks: list[str] = []
for item in source:
Expand Down
46 changes: 46 additions & 0 deletions python/tests/test_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,14 @@ def isatty(self) -> bool:
return False


class _ReadlineOnlyTTYInput(io.StringIO):
def isatty(self) -> bool:
return True

def read(self) -> str:
raise AssertionError("workflow input must read one line at a time")


def test_run_bundled_skill_install_uses_stdio_defaults_for_interactive_flow(
monkeypatch, tmp_path
):
Expand Down Expand Up @@ -358,6 +366,44 @@ def test_run_bundled_skill_install_uses_stdio_defaults_for_interactive_flow(
assert (workspace / ".agents" / "skills" / "basic" / "SKILL.md").exists()


def test_run_bundled_skill_install_reads_interactive_stream_line_by_line(
tmp_path,
):
home = tmp_path / "home"
workspace = tmp_path / "workspace"
home.mkdir()
workspace.mkdir()
skill = workspace / "skill"
skill.mkdir()
(skill / "SKILL.md").write_text(
"---\nname: basic\ndescription: demo\n---\n",
encoding="utf-8",
)

output = io.StringIO()
report = run_bundled_skill_install(
InstallWorkflowOptions(
install=InstallOptions(
base=BaseOptions(home=str(home), cwd=str(workspace)),
app_id="example-cli",
skill_bundle=directory_bundle(str(skill)),
scope="user",
agents=["codex"],
),
prompt_scope=True,
scope_set=False,
input=_ReadlineOnlyTTYInput("project\ny\n"),
output=output,
)
)

assert report.scope == "project"
assert report.canceled is False
assert "Select install scope:" in output.getvalue()
assert "Proceed? [y/N] " in output.getvalue()
assert (workspace / ".agents" / "skills" / "basic" / "SKILL.md").exists()


def test_run_bundled_skill_install_infers_tty_from_custom_input_stream(
monkeypatch, tmp_path
):
Expand Down
84 changes: 83 additions & 1 deletion scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ const root = new URL("../", import.meta.url);
const rootPath = fileURLToPath(root);

function readJson(path) {
return JSON.parse(readFileSync(new URL(path, root), "utf8"));
return JSON.parse(readText(path));
}

function readText(path) {
return readFileSync(new URL(path, root), "utf8");
}

function fail(message) {
Expand Down Expand Up @@ -186,6 +190,82 @@ function validateFixtures() {
readJson("testdata/skills/basic/assets/template.json");
}

function matchOne(path, pattern, label) {
const match = readText(path).match(pattern);
assert(match, `missing ${label}`);
return match[1];
}

function validateVersions() {
const version = readJson("ts/package.json").version;
assert(
matchOne("rust/Cargo.toml", /^version = "([^"]+)"$/m, "rust version") ===
version,
"rust version drifted",
);
assert(
matchOne(
"python/pyproject.toml",
/^version = "([^"]+)"$/m,
"python version",
) === version,
"python version drifted",
);
assert(
matchOne(
"go-cobra/go.mod",
/^\s*github\.com\/lathe-cli\/kitup\/go v([^\s]+)$/m,
"go-cobra core version",
) === version,
"go-cobra core version drifted",
);
assert(
matchOne(
"python/pyproject.toml",
/^name = "([^"]+)"$/m,
"python package name",
) === "kitup-sdk",
"python package name drifted",
);
assert(
matchOne(
"python/pyproject.toml",
/^requires-python = "([^"]+)"$/m,
"python requires-python",
) === ">=3.10",
"python requires-python drifted",
);
}

function validateReleaseWorkflow() {
const workflow = readText(".github/workflows/release.yml");
assert(
workflow.includes("github.com\\/lathe-cli\\/kitup\\/go"),
"release workflow must check the canonical Go module path",
);
assert(
!workflow.includes("github.com\\/samzong\\/kitup\\/go"),
"release workflow still checks the old Go module path",
);
assert(
/^\s*environment: pypi$/m.test(workflow),
"release workflow must use the pypi environment",
);
assert(
workflow.includes("https://pypi.org/pypi/kitup-sdk/"),
"release workflow must check kitup-sdk on PyPI",
);
assert(
!workflow.includes("https://pypi.org/pypi/kitup/"),
"release workflow still checks the old PyPI package name",
);
const smoke = readText("scripts/smoke-release.sh");
assert(
smoke.includes('"kitup-sdk==$version"'),
"release smoke must install kitup-sdk from PyPI",
);
}

const hostsSpec = readJson("spec/hosts.json");
const cases = readJson("testdata/cases/bundled-skill-install.json");
readJson("spec/hosts.schema.json");
Expand All @@ -194,6 +274,8 @@ readJson("testdata/cases.schema.json");
const { ids, aliases } = validateHosts(hostsSpec);
validateCases(cases, hostsSpec.hosts);
validateFixtures();
validateVersions();
validateReleaseWorkflow();

assert(ids.has("kimi-cli"), "kimi-cli must be canonical");
assert(!ids.has("kimi-code-cli"), "kimi-code-cli must not be canonical");
Expand Down
2 changes: 1 addition & 1 deletion scripts/smoke-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ GO
smoke_python() {
dir="$(mktemp -d "$tmp/python.XXXXXX")"
cd "$dir"
uv run --with "kitup==$version" python - <<'PY'
uv run --with "kitup-sdk==$version" python - <<'PY'
from kitup import load_host_spec

spec = load_host_spec()
Expand Down
Loading