getbased-agent-stack 0.4.1__tar.gz → 0.5.1__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.
Files changed (24) hide show
  1. {getbased_agent_stack-0.4.1/src/getbased_agent_stack.egg-info → getbased_agent_stack-0.5.1}/PKG-INFO +13 -2
  2. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/README.md +12 -1
  3. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/pyproject.toml +1 -1
  4. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/cli.py +35 -7
  5. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/units.py +19 -1
  6. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1/src/getbased_agent_stack.egg-info}/PKG-INFO +13 -2
  7. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/tests/test_cli.py +70 -0
  8. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/tests/test_units.py +34 -0
  9. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/LICENSE +0 -0
  10. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/setup.cfg +0 -0
  11. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/__init__.py +0 -0
  12. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/env_file.py +0 -0
  13. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/mcp_configs.py +0 -0
  14. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/systemd/getbased-dashboard.service +0 -0
  15. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack/systemd/getbased-rag.service +0 -0
  16. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack.egg-info/SOURCES.txt +0 -0
  17. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack.egg-info/dependency_links.txt +0 -0
  18. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack.egg-info/entry_points.txt +0 -0
  19. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack.egg-info/requires.txt +0 -0
  20. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/src/getbased_agent_stack.egg-info/top_level.txt +0 -0
  21. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/tests/test_env_file.py +0 -0
  22. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/tests/test_integration.py +0 -0
  23. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/tests/test_mcp_configs.py +0 -0
  24. {getbased_agent_stack-0.4.1 → getbased_agent_stack-0.5.1}/tests/test_systemd_units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
5
5
  License-Expression: GPL-3.0-only
6
6
  Requires-Python: >=3.10
@@ -26,7 +26,18 @@ Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agent
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- pipx install "getbased-agent-stack[full]"
29
+ pipx install --include-deps "getbased-agent-stack[full]"
30
+ ```
31
+
32
+ The `--include-deps` flag is required — it exposes `getbased-mcp`, `lens`, and `getbased-dashboard` alongside `getbased-stack` on your PATH. Without it, pipx only links the stack's own entry point and the MCP/rag/dashboard binaries stay hidden inside the venv.
33
+
34
+ `uv` users: install each package as its own tool instead, since `uv tool` has no `--include-deps` equivalent yet:
35
+
36
+ ```bash
37
+ uv tool install getbased-mcp
38
+ uv tool install "getbased-rag[full]"
39
+ uv tool install getbased-dashboard
40
+ uv tool install "getbased-agent-stack[full]"
30
41
  ```
31
42
 
32
43
  Pulls:
@@ -7,7 +7,18 @@ Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agent
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- pipx install "getbased-agent-stack[full]"
10
+ pipx install --include-deps "getbased-agent-stack[full]"
11
+ ```
12
+
13
+ The `--include-deps` flag is required — it exposes `getbased-mcp`, `lens`, and `getbased-dashboard` alongside `getbased-stack` on your PATH. Without it, pipx only links the stack's own entry point and the MCP/rag/dashboard binaries stay hidden inside the venv.
14
+
15
+ `uv` users: install each package as its own tool instead, since `uv tool` has no `--include-deps` equivalent yet:
16
+
17
+ ```bash
18
+ uv tool install getbased-mcp
19
+ uv tool install "getbased-rag[full]"
20
+ uv tool install getbased-dashboard
21
+ uv tool install "getbased-agent-stack[full]"
11
22
  ```
12
23
 
13
24
  Pulls:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "getbased-agent-stack"
7
- version = "0.4.1"
7
+ version = "0.5.1"
8
8
  description = "One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
@@ -86,7 +86,14 @@ def _yesno(msg: str, default: bool = True) -> bool:
86
86
 
87
87
 
88
88
  def cmd_init(args: argparse.Namespace) -> int:
89
+ # --yes / -y runs the full flow with defaults and no prompts. Intended
90
+ # for scripted installs (curl | bash) where stdin is unavailable and
91
+ # the EOF-returns-default path still triggers a getpass echo warning.
92
+ non_interactive = getattr(args, "yes", False)
93
+
89
94
  print("getbased-stack init — one-time setup")
95
+ if non_interactive:
96
+ print("Running non-interactive (--yes) — takes default answers on every prompt.")
90
97
  print(
91
98
  "Writes ~/.config/getbased/env, (optionally) installs + starts systemd\n"
92
99
  "user units for rag and dashboard. Idempotent: safe to re-run."
@@ -98,11 +105,15 @@ def cmd_init(args: argparse.Namespace) -> int:
98
105
  current_token = existing.get("GETBASED_TOKEN", "")
99
106
  masked = "****" + current_token[-4:] if current_token else "(unset)"
100
107
  print(f"[1/4] getbased sync token (current: {masked})")
101
- token = _prompt(
102
- "Paste GETBASED_TOKEN (press Enter to keep current / skip)",
103
- default=current_token,
104
- secret=True,
105
- )
108
+ if non_interactive:
109
+ token = current_token
110
+ print(" keeping current value (set with `getbased-stack set GETBASED_TOKEN=…` later).")
111
+ else:
112
+ token = _prompt(
113
+ "Paste GETBASED_TOKEN (press Enter to keep current / skip)",
114
+ default=current_token,
115
+ secret=True,
116
+ )
106
117
 
107
118
  # 2. API key
108
119
  key_path = Path(existing.get("LENS_API_KEY_FILE", str(_default_api_key_file())))
@@ -127,7 +138,10 @@ def cmd_init(args: argparse.Namespace) -> int:
127
138
 
128
139
  # 4. install units
129
140
  print("\n[4/4] install systemd user units?")
130
- if _yesno("install + start getbased-rag + getbased-dashboard?", default=True):
141
+ install_units = True if non_interactive else _yesno(
142
+ "install + start getbased-rag + getbased-dashboard?", default=True
143
+ )
144
+ if install_units:
131
145
  mgr = units.UnitManager()
132
146
  for line in mgr.install(enable=True, start=True):
133
147
  print(f" {line}")
@@ -147,6 +161,12 @@ def cmd_init(args: argparse.Namespace) -> int:
147
161
 
148
162
 
149
163
  def _print_linger_hint(strict: bool) -> None:
164
+ # If systemctl isn't even available, the linger question is moot —
165
+ # we've already printed a clearer "systemd not available, unit files
166
+ # written but not activated" message above. Adding "services will
167
+ # stop on logout" here would mislead (there aren't any services).
168
+ if shutil.which("systemctl") is None:
169
+ return
150
170
  user = os.environ.get("USER", "")
151
171
  if not user:
152
172
  return
@@ -304,7 +324,15 @@ def build_parser() -> argparse.ArgumentParser:
304
324
  )
305
325
  sub = p.add_subparsers(dest="command")
306
326
 
307
- sub.add_parser("init", help="Interactive one-time setup (token, API key, units).")
327
+ pinit = sub.add_parser(
328
+ "init", help="Interactive one-time setup (token, API key, units)."
329
+ )
330
+ pinit.add_argument(
331
+ "-y",
332
+ "--yes",
333
+ action="store_true",
334
+ help="Non-interactive: take defaults on every prompt. Use for scripted installs.",
335
+ )
308
336
 
309
337
  pi = sub.add_parser("install", help="Install + start the systemd user units.")
310
338
  pi.add_argument("--no-enable", action="store_true", help="Copy files only; don't enable.")
@@ -44,7 +44,14 @@ class CommandResult:
44
44
 
45
45
 
46
46
  def _real_shell(cmd: "list[str]") -> CommandResult:
47
- proc = subprocess.run(cmd, capture_output=True, text=True)
47
+ # FileNotFoundError when the binary isn't on PATH — happens on systems
48
+ # without systemd (Docker containers, macOS, WSL1). Return a shell-like
49
+ # 127 instead of propagating so callers can handle "not available" the
50
+ # same way they handle "failed" without an unhandled traceback.
51
+ try:
52
+ proc = subprocess.run(cmd, capture_output=True, text=True)
53
+ except FileNotFoundError:
54
+ return CommandResult(127, "", f"command not found: {cmd[0]}")
48
55
  return CommandResult(proc.returncode, proc.stdout, proc.stderr)
49
56
 
50
57
 
@@ -125,6 +132,17 @@ class UnitManager:
125
132
  written = self.install_files()
126
133
  for p in written:
127
134
  log.append(f"wrote {p}")
135
+ # Unit files are written above regardless — they're a prerequisite
136
+ # for any system that CAN run systemd. But if systemctl is absent
137
+ # (Docker container, macOS, WSL1), skip the daemon-reload/enable
138
+ # phase with a clear message rather than stacking cryptic "command
139
+ # not found" errors on top of each other.
140
+ if shutil.which("systemctl") is None:
141
+ log.append(
142
+ "systemctl not available — unit files written but not activated. "
143
+ "On a systemd-enabled host, re-run `getbased-stack install` to enable + start."
144
+ )
145
+ return log
128
146
  r = self.daemon_reload()
129
147
  if r.returncode != 0:
130
148
  log.append(f"daemon-reload FAILED: {r.stderr.strip()}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
5
5
  License-Expression: GPL-3.0-only
6
6
  Requires-Python: >=3.10
@@ -26,7 +26,18 @@ Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agent
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- pipx install "getbased-agent-stack[full]"
29
+ pipx install --include-deps "getbased-agent-stack[full]"
30
+ ```
31
+
32
+ The `--include-deps` flag is required — it exposes `getbased-mcp`, `lens`, and `getbased-dashboard` alongside `getbased-stack` on your PATH. Without it, pipx only links the stack's own entry point and the MCP/rag/dashboard binaries stay hidden inside the venv.
33
+
34
+ `uv` users: install each package as its own tool instead, since `uv tool` has no `--include-deps` equivalent yet:
35
+
36
+ ```bash
37
+ uv tool install getbased-mcp
38
+ uv tool install "getbased-rag[full]"
39
+ uv tool install getbased-dashboard
40
+ uv tool install "getbased-agent-stack[full]"
30
41
  ```
31
42
 
32
43
  Pulls:
@@ -295,6 +295,76 @@ def test_init_reuses_existing_api_key(stack_home, fake_shell, monkeypatch):
295
295
  assert key_path.read_text().strip() == "preexisting_key"
296
296
 
297
297
 
298
+ def test_init_yes_flag_skips_all_prompts(stack_home, fake_shell, monkeypatch):
299
+ """`init --yes` must not call input() or getpass() at all. Scripted
300
+ installers (curl | bash) can't service prompts and the EOF fallback
301
+ triggers a Python getpass echo warning that pollutes output.
302
+ Strict assertion: any prompt call fails the test."""
303
+ def _forbid_input(*a, **kw):
304
+ raise AssertionError("input() called under --yes")
305
+
306
+ def _forbid_getpass(*a, **kw):
307
+ raise AssertionError("getpass() called under --yes")
308
+
309
+ monkeypatch.setattr("builtins.input", _forbid_input)
310
+ monkeypatch.setattr("getpass.getpass", _forbid_getpass)
311
+
312
+ rc, out, _ = _run(["init", "--yes"])
313
+ assert rc == 0
314
+ # Banner reflects the mode so the user sees what happened
315
+ assert "non-interactive" in out.lower()
316
+ # Env file + units still land
317
+ assert env_file.env_file_path().exists()
318
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-rag.service").exists()
319
+
320
+
321
+ def test_init_yes_installs_units_without_asking(stack_home, fake_shell, monkeypatch):
322
+ """Default for the install-units prompt is Yes, so --yes must also
323
+ install + start. If this regressed to skip, install.sh would
324
+ silently leave services off."""
325
+ monkeypatch.setattr("builtins.input", lambda *a, **kw: "")
326
+ monkeypatch.setattr("getpass.getpass", lambda *a, **kw: "")
327
+
328
+ _run(["init", "--yes"])
329
+ # UnitManager.install() writes service files under XDG_CONFIG_HOME
330
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-rag.service").exists()
331
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-dashboard.service").exists()
332
+
333
+
334
+ def test_init_yes_survives_missing_systemctl(stack_home, fake_shell, monkeypatch):
335
+ """--yes on a host without systemctl (Docker, macOS, WSL1) must NOT
336
+ crash with an unhandled FileNotFoundError. Unit files still land;
337
+ systemd ops are skipped with a clear message."""
338
+ import shutil as _shutil
339
+ monkeypatch.setattr(_shutil, "which", lambda name: None)
340
+ # Prompts still stubbed defensively — --yes shouldn't call them.
341
+ monkeypatch.setattr("builtins.input", lambda *a, **kw: "")
342
+ monkeypatch.setattr("getpass.getpass", lambda *a, **kw: "")
343
+
344
+ rc, out, _ = _run(["init", "--yes"])
345
+ assert rc == 0
346
+ # Unit files still written (for re-run on a systemd-enabled host)
347
+ assert (stack_home / "config" / "systemd" / "user" / "getbased-rag.service").exists()
348
+ # Graceful message, not a traceback
349
+ assert "systemctl not available" in out
350
+ assert "Traceback" not in out
351
+
352
+
353
+ def test_init_yes_preserves_existing_token(stack_home, fake_shell, monkeypatch):
354
+ """Non-interactive mode must not nuke a previously-saved token —
355
+ it takes the 'keep current' default, same as pressing Enter."""
356
+ env_file.write_env_file(
357
+ {"GETBASED_TOKEN": "keep_me", "GETBASED_STACK_MANAGED": "1"}
358
+ )
359
+ # No input/getpass expected, but stub defensively in case a future
360
+ # code path adds an unguarded prompt — test still catches it.
361
+ monkeypatch.setattr("builtins.input", lambda *a, **kw: (_ for _ in ()).throw(AssertionError("input under --yes")))
362
+ monkeypatch.setattr("getpass.getpass", lambda *a, **kw: (_ for _ in ()).throw(AssertionError("getpass under --yes")))
363
+
364
+ _run(["init", "-y"])
365
+ assert env_file.read_env_file()["GETBASED_TOKEN"] == "keep_me"
366
+
367
+
298
368
  def test_init_is_reentrant(stack_home, fake_shell, monkeypatch):
299
369
  """Running init twice in a row must not break anything — second call
300
370
  should be a cheap idempotent update, not a destructive rewrite."""
@@ -164,6 +164,40 @@ def test_install_daemon_reload_failure_short_circuits(tmp_path):
164
164
  assert any("daemon-reload FAILED" in line for line in log)
165
165
 
166
166
 
167
+ def test_real_shell_handles_missing_binary(monkeypatch):
168
+ """_real_shell must not raise FileNotFoundError when systemctl is
169
+ absent (Docker, macOS, WSL1). Before 0.5.1 this crashed `init` with
170
+ an unhandled traceback — check it returns a shell-like 127 instead."""
171
+ import subprocess as sp
172
+
173
+ def boom(*a, **kw):
174
+ raise FileNotFoundError(2, "No such file or directory", "systemctl")
175
+
176
+ monkeypatch.setattr(sp, "run", boom)
177
+ r = units._real_shell(["systemctl", "--user", "daemon-reload"])
178
+ assert r.returncode == 127
179
+ assert "command not found" in r.stderr
180
+ assert "systemctl" in r.stderr
181
+
182
+
183
+ def test_install_skips_when_systemctl_absent(tmp_path, monkeypatch):
184
+ """On a host without systemctl, `install()` must write unit files
185
+ (harmless, enables later re-run) but skip daemon-reload/enable with
186
+ a clear message — not stack FAILED errors on top of each other."""
187
+ monkeypatch.setattr(units.shutil, "which", lambda name: None)
188
+ shell = FakeShell()
189
+ mgr = UnitManager(unit_dir=tmp_path, shell=shell)
190
+ log = mgr.install()
191
+
192
+ # Files written (prereq for future systemd-enabled reinstall)
193
+ for name in SERVICE_NAMES:
194
+ assert (tmp_path / name).exists()
195
+ # No systemctl calls attempted
196
+ assert not any(c[:1] == ["systemctl"] for c in shell.calls)
197
+ # User-visible message present
198
+ assert any("systemctl not available" in line for line in log)
199
+
200
+
167
201
  def test_install_enable_failure_reported(tmp_path):
168
202
  shell = FakeShell(
169
203
  {