krypton-app 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.
@@ -0,0 +1,41 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ .coverage.*
9
+ htmlcov/
10
+ *.egg-info/
11
+ /dist/
12
+ /build/
13
+ .claudeloop/
14
+ .playwright-mcp/
15
+ .vibey/plans/
16
+ .agyloop/
17
+ .codexloop/
18
+ .cursorloop/
19
+ .qwenloop/
20
+ .vibey-tmp/site/
21
+ /site/
22
+
23
+ # Workspace members do not carry their own lockfiles. uv resolves the whole workspace
24
+ # (`[tool.uv.workspace] members`, ADR-0021) into the single root uv.lock, and that is the
25
+ # only one `uv lock --check` validates. The per-runner locks that survived the sibling-repo
26
+ # absorption were installed from by nothing -- CI installs the runners with plain
27
+ # `pip install -e ".[dev]"` -- and served only to raise Dependabot advisories against
28
+ # versions no environment ever resolved. Keep them out.
29
+ src/vibey_runners/*/uv.lock
30
+ src/vibey_tools/*/uv.lock
31
+
32
+ # pre-commit writes here: `[env] PRE_COMMIT_HOME = ".cache/pre-commit"` in
33
+ # .claude/settings.json keeps its cache inside the sandbox-writable tree, which means
34
+ # every worktree grows a .cache/ that is tooling state, never repository content.
35
+ .cache/
36
+
37
+ # The npm workspace (root package.json, ADR-0067): its install tree and each package's build.
38
+ /node_modules/
39
+ packages/*/node_modules/
40
+ packages/*/dist/
41
+ packages/*/coverage/
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.5
2
+ Name: krypton-app
3
+ Version: 0.1.0
4
+ Summary: krypton, the apps of vibey: launches the local vibey hub and opens it.
5
+ Project-URL: Homepage, https://the-vibey-project.github.io/vibey/
6
+ Project-URL: Source, https://github.com/the-vibey-project/vibey/tree/main/clients/krypton-app
7
+ Project-URL: Issues, https://github.com/the-vibey-project/vibey/issues
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: vibey-engine
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.13; extra == 'dev'
13
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
14
+ Requires-Dist: pytest>=8.3; extra == 'dev'
15
+ Requires-Dist: ruff>=0.8; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # krypton-app
19
+
20
+ krypton is every app and interface of [vibey](https://the-vibey-project.github.io/vibey/).
21
+ This package carries them and installs one command, `krypton`.
22
+
23
+ ```bash
24
+ pip install krypton-app # brings vibey-engine with it
25
+ krypton # start the local vibey hub and open it in your browser
26
+ krypton --help
27
+ ```
28
+
29
+ ## What `krypton` does today
30
+
31
+ `krypton` is a launcher. It finds the `vibey` command, starts `vibey serve` on
32
+ `127.0.0.1:8765` (change it with `--host` and `--port`), waits until the hub answers, and
33
+ opens it in your browser. `--no-browser` only starts it.
34
+
35
+ When the installed vibey-engine has no `vibey serve` yet, `krypton` says so plainly, lists
36
+ what you can use instead, and exits with status 1. It never pretends a hub is running.
37
+
38
+ ## Releases
39
+
40
+ Published by `.github/workflows/krypton-app.yml` alone, by trusted publishing: a dev build
41
+ to TestPyPI (environment `testpypi`) on each push to `develop` that changes this tree or the
42
+ workflow, and a release to PyPI (environment `pypi`) on each such push to `main`. There is no
43
+ schedule. A push GitHub skips publishes nothing: a squash merge whose message quotes an old
44
+ `[skip ci]` subject skips every push workflow, which is how the 3.0.0 promotion first
45
+ published neither package. Merge a promotion by rebase, or edit the squash message. ADR-0069 records why vibey publishes exactly two packages, `vibey-engine` and
46
+ `krypton-app`.
@@ -0,0 +1,29 @@
1
+ # krypton-app
2
+
3
+ krypton is every app and interface of [vibey](https://the-vibey-project.github.io/vibey/).
4
+ This package carries them and installs one command, `krypton`.
5
+
6
+ ```bash
7
+ pip install krypton-app # brings vibey-engine with it
8
+ krypton # start the local vibey hub and open it in your browser
9
+ krypton --help
10
+ ```
11
+
12
+ ## What `krypton` does today
13
+
14
+ `krypton` is a launcher. It finds the `vibey` command, starts `vibey serve` on
15
+ `127.0.0.1:8765` (change it with `--host` and `--port`), waits until the hub answers, and
16
+ opens it in your browser. `--no-browser` only starts it.
17
+
18
+ When the installed vibey-engine has no `vibey serve` yet, `krypton` says so plainly, lists
19
+ what you can use instead, and exits with status 1. It never pretends a hub is running.
20
+
21
+ ## Releases
22
+
23
+ Published by `.github/workflows/krypton-app.yml` alone, by trusted publishing: a dev build
24
+ to TestPyPI (environment `testpypi`) on each push to `develop` that changes this tree or the
25
+ workflow, and a release to PyPI (environment `pypi`) on each such push to `main`. There is no
26
+ schedule. A push GitHub skips publishes nothing: a squash merge whose message quotes an old
27
+ `[skip ci]` subject skips every push workflow, which is how the 3.0.0 promotion first
28
+ published neither package. Merge a promotion by rebase, or edit the squash message. ADR-0069 records why vibey publishes exactly two packages, `vibey-engine` and
29
+ `krypton-app`.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ # krypton-app: the apps' package (sub-doctrine 9.e, ADR-0069). Every app and interface a
6
+ # person uses is krypton; this package carries them and the `krypton` command. It is
7
+ # published by its own workflow, .github/workflows/krypton-app.yml, and by no other.
8
+ [project]
9
+ name = "krypton-app"
10
+ version = "0.1.0"
11
+ description = "krypton, the apps of vibey: launches the local vibey hub and opens it."
12
+ readme = "README.md"
13
+ requires-python = ">=3.12"
14
+ license = { text = "MIT" }
15
+ # The launcher starts `vibey serve`, so the engine is what makes it useful. Unpinned: the
16
+ # two packages release on their own cadences, and the launcher checks what the installed
17
+ # engine can do at run time rather than trusting a version number to say it.
18
+ dependencies = ["vibey-engine"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://the-vibey-project.github.io/vibey/"
22
+ Source = "https://github.com/the-vibey-project/vibey/tree/main/clients/krypton-app"
23
+ Issues = "https://github.com/the-vibey-project/vibey/issues"
24
+
25
+ [project.scripts]
26
+ krypton = "krypton_app.cli:main"
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest>=8.3", "pytest-cov>=6.0", "ruff>=0.8", "mypy>=1.13"]
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/krypton_app"]
33
+
34
+ [tool.ruff]
35
+ line-length = 100
36
+ target-version = "py312"
37
+
38
+ [tool.mypy]
39
+ strict = true
40
+ mypy_path = "src"
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
@@ -0,0 +1,2 @@
1
+ # Made with ❤️ by [Vibey](https://the-vibey-project.github.io/vibey/), Developed by [Adam Matthew Steinberger](https://vibewithadam.matthewsteinberger.com/) ([@adammatthewsteinberger](https://github.com/adammatthewsteinberger/)).
2
+ """krypton-app: the apps of vibey, and the `krypton` command that launches them."""
@@ -0,0 +1,5 @@
1
+ """python -m krypton_app"""
2
+
3
+ from krypton_app.cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,61 @@
1
+ """The `krypton` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from collections.abc import Sequence
9
+ from importlib.metadata import PackageNotFoundError, version
10
+ from pathlib import Path
11
+
12
+ from krypton_app.launcher import KryptonLauncher
13
+
14
+
15
+ def _version() -> str:
16
+ # A module-level function because it is a one-line lookup with no state (ADR-0016).
17
+ try:
18
+ return version("krypton-app")
19
+ except PackageNotFoundError: # pragma: no cover - only in an uninstalled tree
20
+ return "0+unknown"
21
+
22
+
23
+ def _vibey_beside_me() -> str | None:
24
+ # A module-level function because it is a one-line lookup with no state (ADR-0016).
25
+ # The engine installed into the same environment as krypton-app comes first: a `vibey`
26
+ # earlier on PATH may be an older one from another environment.
27
+ candidate = Path(sys.executable).parent / "vibey"
28
+ return str(candidate) if os.access(candidate, os.X_OK) else None
29
+
30
+
31
+ def main(argv: Sequence[str] | None = None) -> int:
32
+ # A module-level function because a console-script entry point must be one (ADR-0016);
33
+ # everything it does is KryptonLauncher's.
34
+ parser = argparse.ArgumentParser(
35
+ prog="krypton",
36
+ description=(
37
+ "krypton, the apps of vibey. Starts the local vibey hub (`vibey serve`) and "
38
+ "opens it in your browser; if the installed vibey cannot serve it yet, says so "
39
+ "and lists what is available instead."
40
+ ),
41
+ )
42
+ parser.add_argument("--version", action="version", version=f"krypton {_version()}")
43
+ parser.add_argument(
44
+ "--host", default="127.0.0.1", help="address to serve on (default: 127.0.0.1)"
45
+ )
46
+ parser.add_argument("--port", type=int, default=8765, help="port to serve on (default: 8765)")
47
+ parser.add_argument(
48
+ "--no-browser", action="store_true", help="start the hub without opening it"
49
+ )
50
+ parser.add_argument(
51
+ "--vibey",
52
+ default=None,
53
+ help="the vibey program to use (default: the one beside krypton, else vibey on PATH)",
54
+ )
55
+ args = parser.parse_args(argv)
56
+ launcher = KryptonLauncher(out=sys.stdout, vibey=args.vibey or _vibey_beside_me())
57
+ return launcher.launch(args.host, args.port, open_browser=not args.no_browser)
58
+
59
+
60
+ if __name__ == "__main__": # pragma: no cover
61
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Interfaces of krypton-app (ADR-0016)."""
@@ -0,0 +1,13 @@
1
+ """The contract of the krypton launcher (ADR-0016: an interface beside every class)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+
8
+ class KryptonLauncherInterface(ABC):
9
+ """Starts the local vibey hub and opens it, or says plainly why it cannot."""
10
+
11
+ @abstractmethod
12
+ def launch(self, host: str, port: int, open_browser: bool) -> int:
13
+ """Run the hub until it exits; return the process exit status."""
@@ -0,0 +1,104 @@
1
+ """The krypton launcher: `vibey serve`, then the browser -- or an honest refusal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import time
7
+ import urllib.error
8
+ import urllib.request
9
+ import webbrowser
10
+ from collections.abc import Callable, Sequence
11
+ from dataclasses import dataclass, field
12
+ from shutil import which
13
+ from typing import IO, Protocol
14
+
15
+ from krypton_app.interfaces.krypton_launcher_interface import KryptonLauncherInterface
16
+
17
+ AVAILABLE_INSTEAD = (
18
+ "What you can use now:\n"
19
+ " vibey --help the vibey command line\n"
20
+ " vibey doctor what is installed and what is missing\n"
21
+ " the VS Code extension krypton inside your editor (clients/vscode)\n"
22
+ )
23
+
24
+
25
+ class _Process(Protocol):
26
+ def poll(self) -> int | None: ...
27
+ def wait(self) -> int: ...
28
+ def terminate(self) -> None: ...
29
+
30
+
31
+ def _probe(url: str) -> bool:
32
+ """Whether anything answers HTTP at `url`, whatever the status."""
33
+ try:
34
+ with urllib.request.urlopen(url, timeout=2): # nosec B310 - loopback URL we built
35
+ return True
36
+ except urllib.error.HTTPError:
37
+ return True
38
+ except (urllib.error.URLError, OSError):
39
+ return False
40
+
41
+
42
+ @dataclass
43
+ class KryptonLauncher(KryptonLauncherInterface):
44
+ """Launches the hub through the installed `vibey`, with every side effect injectable."""
45
+
46
+ out: IO[str]
47
+ vibey: str | None = None
48
+ ready_timeout: float = 60.0
49
+ find: Callable[[str], str | None] = which
50
+ run: Callable[[Sequence[str]], int] = field(
51
+ default=lambda argv: (
52
+ subprocess.run( # nosec B603 - argv, no shell
53
+ list(argv), capture_output=True, timeout=30, check=False
54
+ ).returncode
55
+ )
56
+ )
57
+ spawn: Callable[[Sequence[str]], _Process] = field(
58
+ default=lambda argv: subprocess.Popen(list(argv)) # nosec B603 - argv, no shell
59
+ )
60
+ probe: Callable[[str], bool] = _probe
61
+ browse: Callable[[str], object] = webbrowser.open
62
+ sleep: Callable[[float], None] = time.sleep
63
+ clock: Callable[[], float] = time.monotonic
64
+
65
+ def launch(self, host: str, port: int, open_browser: bool) -> int:
66
+ vibey = self.vibey or self.find("vibey")
67
+ if vibey is None:
68
+ self.out.write(
69
+ "krypton: the vibey command is not installed, so there is no hub to start.\n"
70
+ "Install the engine with: pip install vibey-engine\n"
71
+ )
72
+ return 1
73
+ if self.run([vibey, "serve", "--help"]) != 0:
74
+ self.out.write(
75
+ "krypton: the local web app is started by `vibey serve`, and the installed\n"
76
+ f"vibey ({vibey}) does not provide it yet. Nothing was started.\n"
77
+ + AVAILABLE_INSTEAD
78
+ )
79
+ return 1
80
+ url = f"http://{host}:{port}/"
81
+ process = self.spawn([vibey, "serve", "--host", host, "--port", str(port)])
82
+ deadline = self.clock() + self.ready_timeout
83
+ while not self.probe(url):
84
+ status = process.poll()
85
+ if status is not None:
86
+ self.out.write(
87
+ f"krypton: vibey serve exited with status {status} before it answered.\n"
88
+ )
89
+ return status or 1
90
+ if self.clock() >= deadline:
91
+ self.out.write(
92
+ f"krypton: nothing answered at {url} within {self.ready_timeout:g}s.\n"
93
+ )
94
+ process.terminate()
95
+ return 1
96
+ self.sleep(0.5)
97
+ self.out.write(f"krypton: the vibey hub is at {url}\n")
98
+ if open_browser:
99
+ self.browse(url)
100
+ try:
101
+ return process.wait()
102
+ except KeyboardInterrupt:
103
+ process.terminate()
104
+ return 130
File without changes
@@ -0,0 +1,141 @@
1
+ """The krypton launcher, with every side effect faked: no process, browser or network."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ from collections.abc import Sequence
7
+
8
+ import pytest
9
+
10
+ from krypton_app import cli
11
+ from krypton_app.launcher import KryptonLauncher
12
+
13
+
14
+ class FakeProcess:
15
+ def __init__(
16
+ self, polls: Sequence[int | None] = (None,), wait: int | BaseException = 0
17
+ ) -> None:
18
+ self._polls = list(polls)
19
+ self._wait = wait
20
+ self.terminated = False
21
+
22
+ def poll(self) -> int | None:
23
+ return self._polls.pop(0) if len(self._polls) > 1 else self._polls[0]
24
+
25
+ def wait(self) -> int:
26
+ if isinstance(self._wait, BaseException):
27
+ raise self._wait
28
+ return self._wait
29
+
30
+ def terminate(self) -> None:
31
+ self.terminated = True
32
+
33
+
34
+ def make(
35
+ *,
36
+ found: str | None = "/x/vibey",
37
+ serve_rc: int = 0,
38
+ process: FakeProcess | None = None,
39
+ probes: Sequence[bool] = (True,),
40
+ times: Sequence[float] = (0.0,),
41
+ ) -> tuple[KryptonLauncher, io.StringIO, list[list[str]], list[str]]:
42
+ out = io.StringIO()
43
+ spawned: list[list[str]] = []
44
+ browsed: list[str] = []
45
+ probe_values = list(probes)
46
+ clock_values = list(times)
47
+ proc = process or FakeProcess()
48
+
49
+ def spawn(argv: Sequence[str]) -> FakeProcess:
50
+ spawned.append(list(argv))
51
+ return proc
52
+
53
+ launcher = KryptonLauncher(
54
+ out=out,
55
+ find=lambda _name: found,
56
+ run=lambda _argv: serve_rc,
57
+ spawn=spawn,
58
+ probe=lambda _url: probe_values.pop(0) if len(probe_values) > 1 else probe_values[0],
59
+ browse=browsed.append,
60
+ sleep=lambda _s: None,
61
+ clock=lambda: clock_values.pop(0) if len(clock_values) > 1 else clock_values[0],
62
+ )
63
+ return launcher, out, spawned, browsed
64
+
65
+
66
+ def test_no_vibey_says_how_to_install_it() -> None:
67
+ launcher, out, spawned, _ = make(found=None)
68
+ assert launcher.launch("127.0.0.1", 8765, open_browser=True) == 1
69
+ assert "pip install vibey-engine" in out.getvalue()
70
+ assert spawned == []
71
+
72
+
73
+ def test_an_engine_without_serve_starts_nothing_and_says_what_is_available() -> None:
74
+ launcher, out, spawned, _ = make(serve_rc=2)
75
+ assert launcher.launch("127.0.0.1", 8765, open_browser=True) == 1
76
+ assert "does not provide it yet" in out.getvalue()
77
+ assert "vibey --help" in out.getvalue()
78
+ assert spawned == []
79
+
80
+
81
+ def test_the_hub_starts_and_opens() -> None:
82
+ launcher, out, spawned, browsed = make()
83
+ assert launcher.launch("127.0.0.1", 8765, open_browser=True) == 0
84
+ assert spawned == [["/x/vibey", "serve", "--host", "127.0.0.1", "--port", "8765"]]
85
+ assert browsed == ["http://127.0.0.1:8765/"]
86
+ assert "the vibey hub is at" in out.getvalue()
87
+
88
+
89
+ def test_no_browser_opens_nothing() -> None:
90
+ launcher, _, _, browsed = make()
91
+ assert launcher.launch("127.0.0.1", 8765, open_browser=False) == 0
92
+ assert browsed == []
93
+
94
+
95
+ def test_a_serve_that_dies_early_is_reported() -> None:
96
+ launcher, out, _, _ = make(process=FakeProcess(polls=[3]), probes=[False])
97
+ assert launcher.launch("127.0.0.1", 8765, open_browser=True) == 3
98
+ assert "exited with status 3" in out.getvalue()
99
+
100
+
101
+ def test_a_serve_that_never_answers_is_stopped() -> None:
102
+ proc = FakeProcess()
103
+ launcher, out, _, _ = make(process=proc, probes=[False], times=[0.0, 100.0])
104
+ assert launcher.launch("127.0.0.1", 8765, open_browser=True) == 1
105
+ assert proc.terminated
106
+ assert "nothing answered" in out.getvalue()
107
+
108
+
109
+ def test_a_poll_after_one_wait_then_ready() -> None:
110
+ launcher, _, _, browsed = make(probes=[False, True], times=[0.0, 1.0])
111
+ assert launcher.launch("h", 1, open_browser=True) == 0
112
+ assert browsed == ["http://h:1/"]
113
+
114
+
115
+ def test_interrupt_stops_the_hub() -> None:
116
+ proc = FakeProcess(wait=KeyboardInterrupt())
117
+ launcher, _, _, _ = make(process=proc)
118
+ assert launcher.launch("127.0.0.1", 8765, open_browser=False) == 130
119
+ assert proc.terminated
120
+
121
+
122
+ def test_cli_passes_its_options(monkeypatch: pytest.MonkeyPatch) -> None:
123
+ calls: list[tuple[str, int, bool]] = []
124
+
125
+ class Fake:
126
+ def __init__(self, out: object, vibey: str | None) -> None:
127
+ assert vibey == "/nonexistent/vibey"
128
+
129
+ def launch(self, host: str, port: int, open_browser: bool) -> int:
130
+ calls.append((host, port, open_browser))
131
+ return 7
132
+
133
+ monkeypatch.setattr(cli, "KryptonLauncher", Fake)
134
+ assert cli.main(["--no-browser", "--vibey", "/nonexistent/vibey"]) == 7
135
+ assert calls == [("127.0.0.1", 8765, False)]
136
+
137
+
138
+ def test_cli_help_exits_zero() -> None:
139
+ with pytest.raises(SystemExit) as raised:
140
+ cli.main(["--help"])
141
+ assert raised.value.code == 0