-
Notifications
You must be signed in to change notification settings - Fork 1
add: solvent init — first-run scaffolding command #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+279
−1
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """ | ||
| init.py — first-run scaffolding for SOLVENT. | ||
|
|
||
| Creates the application home directory, initialises the treasury database, | ||
| seeds workspace files, and prints a clear onboarding summary so users know | ||
| exactly where everything lives and what to do next. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| from .paths import base_dir, data_dir, db_path, reports_dir | ||
| from .workspace import seed_workspace, BOOTSTRAP_FILES | ||
|
|
||
|
|
||
| _ENV_EXAMPLE = """\ | ||
| # SOLVENT environment variables (copy to .env and fill in) | ||
|
|
||
| # Override the application home directory (default: ~/.solvent when pip-installed) | ||
| # SOLVENT_HOME=~/.solvent | ||
|
|
||
| # NVIDIA Nemotron — live inference (optional; offline stub works without this) | ||
| # NVIDIA_API_KEY=nvapi-... | ||
|
|
||
| # Stripe — live test-mode payments (optional; simulate mode works without this) | ||
| # STRIPE_API_KEY=sk_test_... | ||
|
|
||
| # Telegram — bot notifications (optional) | ||
| # TELEGRAM_BOT_TOKEN=... | ||
| # TELEGRAM_CHAT_ID=... | ||
| """ | ||
|
|
||
|
|
||
| def run(*, force: bool = False) -> int: | ||
| """ | ||
| Scaffold the SOLVENT runtime environment. | ||
|
|
||
| Returns 0 on success, 1 on error. | ||
| """ | ||
| created: list[str] = [] | ||
| already: list[str] = [] | ||
| errors: list[str] = [] | ||
|
|
||
| def _track(path: Path, *, was_new: bool) -> None: | ||
| (created if was_new else already).append(str(path)) | ||
|
|
||
| # --- application home ------------------------------------------------- | ||
| home = base_dir() | ||
| _track(home, was_new=not home.exists()) | ||
|
|
||
| # --- data directory + treasury DB ------------------------------------ | ||
| ddir = data_dir() | ||
| _track(ddir, was_new=not ddir.exists()) | ||
|
|
||
| rdir = reports_dir() | ||
| _track(rdir, was_new=not rdir.exists()) | ||
|
|
||
| db = db_path() | ||
| db_new = not db.exists() | ||
| try: | ||
| from .treasury import Treasury | ||
| t = Treasury(path=str(db)) | ||
| # Touch the DB by running the lightest possible query | ||
| with t._conn() as conn: | ||
| conn.execute("SELECT 1") | ||
| _track(db, was_new=db_new) | ||
| except Exception as exc: | ||
| errors.append(f"treasury DB: {exc}") | ||
|
|
||
| # --- .env.example ---------------------------------------------------- | ||
| env_example = home / ".env.example" | ||
| if not env_example.exists(): | ||
| env_example.write_text(_ENV_EXAMPLE, encoding="utf-8") | ||
| created.append(str(env_example)) | ||
| else: | ||
| already.append(str(env_example)) | ||
|
|
||
| # --- workspace files -------------------------------------------------- | ||
| try: | ||
| workspace_root = seed_workspace(force=force) | ||
| for name in BOOTSTRAP_FILES: | ||
| p = workspace_root / name | ||
| _track(p, was_new=not p.exists() or force) | ||
| except Exception as exc: | ||
| errors.append(f"workspace: {exc}") | ||
|
|
||
| # --- report ----------------------------------------------------------- | ||
| print("SOLVENT init") | ||
| print("=" * 60) | ||
|
|
||
| print(f"\n Home: {home}") | ||
| print(f" Data: {ddir}") | ||
| print(f" DB: {db}") | ||
| print(f" Reports: {rdir}") | ||
|
|
||
| if created: | ||
| print(f"\n Created ({len(created)}):") | ||
| for p in created: | ||
| print(f" + {p}") | ||
|
|
||
| if already: | ||
| print(f"\n Already present ({len(already)}) — skipped.") | ||
|
|
||
| if errors: | ||
| print(f"\n Errors ({len(errors)}):") | ||
| for e in errors: | ||
| print(f" ! {e}") | ||
| return 1 | ||
|
|
||
| print(""" | ||
| Next steps: | ||
| solvent doctor verify the full stack | ||
| solvent run the demo (no keys needed) | ||
| solvent finance financial report | ||
|
|
||
| Optional: edit .env.example → .env, fill in API keys, then: | ||
| source .env | ||
| solvent doctor re-run to confirm live features | ||
|
|
||
| Documentation: https://github.com/ianalloway/solvent-agent | ||
| """) | ||
| return 0 | ||
|
|
||
|
|
||
| def main(): | ||
| import argparse | ||
| p = argparse.ArgumentParser( | ||
| description="Initialise SOLVENT — create data dir, treasury DB, and workspace files.", | ||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||
| ) | ||
| p.add_argument( | ||
| "--force", | ||
| action="store_true", | ||
| help="overwrite existing workspace template files", | ||
| ) | ||
| args = p.parse_args() | ||
| sys.exit(run(force=args.force)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| """tests/test_init.py — unit tests for the solvent init command.""" | ||
|
|
||
| import io | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| from contextlib import redirect_stdout | ||
| from pathlib import Path | ||
| from unittest import mock | ||
|
|
||
|
|
||
| class TestInit(unittest.TestCase): | ||
|
|
||
| def _run_init(self, home_dir: str, force: bool = False) -> tuple[int, str]: | ||
| """Run init.run() inside a temporary SOLVENT_HOME, return (exit_code, output).""" | ||
| buf = io.StringIO() | ||
| workspace_dir = str(Path(home_dir) / "workspace") | ||
| env = {"SOLVENT_HOME": home_dir, "SOLVENT_WORKSPACE": workspace_dir} | ||
| with mock.patch.dict(os.environ, env): | ||
| import importlib | ||
| import solvent.paths as _paths | ||
| import solvent.workspace as _ws | ||
| import solvent.init as _init | ||
| importlib.reload(_paths) | ||
| importlib.reload(_ws) | ||
| importlib.reload(_init) | ||
| with redirect_stdout(buf): | ||
| rc = _init.run(force=force) | ||
| return rc, buf.getvalue() | ||
|
|
||
| def test_init_succeeds(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| rc, out = self._run_init(d) | ||
| self.assertEqual(rc, 0) | ||
|
|
||
| def test_init_creates_data_dir(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| self._run_init(d) | ||
| self.assertTrue(Path(d, "data").is_dir()) | ||
|
|
||
| def test_init_creates_db(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| self._run_init(d) | ||
| self.assertTrue(Path(d, "data", "solvent.db").is_file()) | ||
|
|
||
| def test_init_creates_reports_dir(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| self._run_init(d) | ||
| self.assertTrue(Path(d, "data", "reports").is_dir()) | ||
|
|
||
| def test_init_creates_env_example(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| self._run_init(d) | ||
| env_ex = Path(d, ".env.example") | ||
| self.assertTrue(env_ex.is_file()) | ||
| content = env_ex.read_text() | ||
| self.assertIn("NVIDIA_API_KEY", content) | ||
| self.assertIn("STRIPE_API_KEY", content) | ||
|
|
||
| def test_init_seeds_workspace(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| # workspace lives under CONFIG_DIR which is inside base_dir | ||
| self._run_init(d) | ||
| # soul.md or agents.md should exist somewhere under d | ||
| md_files = list(Path(d).rglob("SOUL.md")) | ||
| self.assertTrue(len(md_files) > 0, "SOUL.md not found after init") | ||
|
|
||
| def test_init_idempotent(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| rc1, _ = self._run_init(d) | ||
| rc2, _ = self._run_init(d) | ||
| self.assertEqual(rc1, 0) | ||
| self.assertEqual(rc2, 0) | ||
|
|
||
| def test_init_prints_home_path(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| _, out = self._run_init(d) | ||
| self.assertIn(d, out) | ||
|
|
||
| def test_init_prints_next_steps(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| _, out = self._run_init(d) | ||
| self.assertIn("Next steps", out) | ||
| self.assertIn("solvent doctor", out) | ||
|
|
||
| def test_init_force_overwrites_workspace(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| self._run_init(d) | ||
| workspace_dir = str(Path(d) / "workspace") | ||
| env = {"SOLVENT_HOME": d, "SOLVENT_WORKSPACE": workspace_dir} | ||
| import importlib | ||
| import solvent.paths as _paths | ||
| import solvent.workspace as _ws | ||
| with mock.patch.dict(os.environ, env): | ||
| importlib.reload(_paths) | ||
| importlib.reload(_ws) | ||
| soul = _ws.workspace_path() / "SOUL.md" | ||
| if soul.is_file(): | ||
| soul.write_text("CORRUPTED", encoding="utf-8") | ||
| rc, _ = self._run_init(d, force=True) | ||
| self.assertEqual(rc, 0) | ||
| content = soul.read_text() | ||
| self.assertNotEqual(content.strip(), "CORRUPTED") | ||
|
|
||
|
|
||
| class TestInitCLI(unittest.TestCase): | ||
|
|
||
| def test_main_exits_zero(self): | ||
| with tempfile.TemporaryDirectory() as d: | ||
| workspace_dir = str(Path(d) / "workspace") | ||
| env = {"SOLVENT_HOME": d, "SOLVENT_WORKSPACE": workspace_dir} | ||
| with mock.patch.dict(os.environ, env): | ||
| import importlib | ||
| import solvent.paths as _paths | ||
| import solvent.workspace as _ws | ||
| import solvent.init as _init | ||
| importlib.reload(_paths) | ||
| importlib.reload(_ws) | ||
| importlib.reload(_init) | ||
| # __main__.py pops "init" before calling main(), so argv has no subcommand | ||
| with mock.patch.object(sys, "argv", ["solvent"]): | ||
| with self.assertRaises(SystemExit) as ctx: | ||
| with mock.patch("sys.stdout", new_callable=io.StringIO): | ||
| _init.main() | ||
| self.assertEqual(ctx.exception.code, 0) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
SOLVENT_HOMEpoints outside the current working directory, this call still usesworkspace_path()'s default.solvent/workspaceunder the process cwd. As a result,solvent initreports the selected home as initialized while placing SOUL/BRAIN/AGENTS elsewhere; a latersolventorsolvent doctorrun from a different cwd will not use the workspace that init just created. Set/pass the workspace path relative tohomeor make the chosen workspace path explicit before seeding.Useful? React with 👍 / 👎.