hypha-debugger 0.2.0__tar.gz → 0.2.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 (26) hide show
  1. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/PKG-INFO +1 -1
  2. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/__init__.py +1 -1
  3. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/cli.py +170 -33
  4. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/source.py +7 -0
  5. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger.egg-info/PKG-INFO +1 -1
  6. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/pyproject.toml +1 -1
  7. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/tests/test_cli.py +53 -0
  8. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/README.md +0 -0
  9. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/__main__.py +0 -0
  10. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/debugger.py +0 -0
  11. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/__init__.py +0 -0
  12. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/execute.py +0 -0
  13. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/filesystem.py +0 -0
  14. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/info.py +0 -0
  15. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/inspect_vars.py +0 -0
  16. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/services/shell.py +0 -0
  17. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/utils/__init__.py +0 -0
  18. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger/utils/env.py +0 -0
  19. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger.egg-info/SOURCES.txt +0 -0
  20. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger.egg-info/dependency_links.txt +0 -0
  21. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger.egg-info/entry_points.txt +0 -0
  22. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger.egg-info/requires.txt +0 -0
  23. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/hypha_debugger.egg-info/top_level.txt +0 -0
  24. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/setup.cfg +0 -0
  25. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/tests/test_services.py +0 -0
  26. {hypha_debugger-0.2.0 → hypha_debugger-0.2.1}/tests/test_shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypha-debugger
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
5
5
  Author: Amun AI AB
6
6
  License: MIT
@@ -12,5 +12,5 @@ Usage (sync):
12
12
 
13
13
  from hypha_debugger.debugger import start_debugger, start_debugger_sync, DebugSession
14
14
 
15
- __version__ = "0.2.0"
15
+ __version__ = "0.2.1"
16
16
  __all__ = ["start_debugger", "start_debugger_sync", "DebugSession", "__version__"]
@@ -114,6 +114,29 @@ def _normalize_url(url: str) -> str:
114
114
  return url.rstrip("/")
115
115
 
116
116
 
117
+ # ---- profile type (terminal vs browser) ----------------------------------
118
+ def _profile_type(profile: dict) -> str:
119
+ return profile.get("type", "terminal")
120
+
121
+
122
+ def _infer_type(url: str) -> str:
123
+ """Guess the profile type from the service id in the URL."""
124
+ u = url.lower()
125
+ if "web-navigator" in u or "web-debugger" in u or "navigator" in u:
126
+ return "browser"
127
+ return "terminal"
128
+
129
+
130
+ def _print_value(value) -> None:
131
+ """Print a service result: strings verbatim, everything else as JSON."""
132
+ if value is None:
133
+ return
134
+ if isinstance(value, str):
135
+ sys.stdout.write(value if value.endswith("\n") else value + "\n")
136
+ else:
137
+ print(json.dumps(value, indent=2, default=str))
138
+
139
+
117
140
  def _resolve_profile(explicit: str = "") -> tuple:
118
141
  """Return (name, profile_dict). Priority: -p flag > $HYD_PROFILE > sole profile."""
119
142
  profiles = _load_profiles()
@@ -175,17 +198,7 @@ def _extract_opts(args: list) -> tuple:
175
198
  return opts, rest
176
199
 
177
200
 
178
- def cmd_sh(args: list, bare: bool = False) -> int:
179
- if bare:
180
- opts, command_parts = {"profile": "", "timeout": None, "cwd": ""}, args
181
- else:
182
- opts, command_parts = _extract_opts(args)
183
- command = " ".join(command_parts).strip()
184
- if not command:
185
- raise CliError("nothing to run. Usage: hyd sh '<command>'")
186
- _name, profile = _resolve_profile(opts["profile"])
187
- cwd = opts["cwd"] or _env_cwd()
188
- timeout = opts["timeout"] if opts["timeout"] is not None else 30
201
+ def _run_shell(profile: dict, command: str, cwd: str, timeout: int) -> int:
189
202
  data = _call_remote(
190
203
  profile, "execute_bash",
191
204
  {"command": command, "cwd": cwd, "timeout": timeout},
@@ -200,12 +213,113 @@ def cmd_sh(args: list, bare: bool = False) -> int:
200
213
  return int(data.get("exit_code", 0) or 0)
201
214
 
202
215
 
216
+ def _run_js(profile: dict, code: str, timeout: int) -> int:
217
+ data = _call_remote(profile, "execute_script", {"code": code}, http_timeout=(timeout or 60) + 15)
218
+ if isinstance(data, dict):
219
+ if data.get("error"):
220
+ sys.stderr.write(f"hyd: {data['error']}\n")
221
+ return 1
222
+ # execute_script returns {result, type, ...}; print just the result.
223
+ _print_value(data["result"] if "result" in data else data)
224
+ else:
225
+ _print_value(data)
226
+ return 0
227
+
228
+
229
+ def cmd_run(args: list, bare: bool = False) -> int:
230
+ """Type-adaptive run: shell on a terminal profile, JavaScript on a browser one."""
231
+ if bare:
232
+ opts, parts = {"profile": "", "timeout": None, "cwd": ""}, args
233
+ else:
234
+ opts, parts = _extract_opts(args)
235
+ payload = " ".join(parts).strip()
236
+ if not payload:
237
+ raise CliError("nothing to run. Usage: hyd '<shell command or JS>'")
238
+ name, profile = _resolve_profile(opts["profile"])
239
+ timeout = opts["timeout"] if opts["timeout"] is not None else 30
240
+ if _profile_type(profile) == "browser":
241
+ return _run_js(profile, payload, timeout)
242
+ return _run_shell(profile, payload, opts["cwd"] or _env_cwd(), timeout)
243
+
244
+
245
+ def cmd_sh(args: list) -> int:
246
+ opts, parts = _extract_opts(args)
247
+ command = " ".join(parts).strip()
248
+ if not command:
249
+ raise CliError("nothing to run. Usage: hyd sh '<command>'")
250
+ name, profile = _resolve_profile(opts["profile"])
251
+ if _profile_type(profile) == "browser":
252
+ raise CliError(f"profile '{name}' is a browser profile — use `hyd js '<code>'` (or bare `hyd '<code>'`).")
253
+ timeout = opts["timeout"] if opts["timeout"] is not None else 30
254
+ return _run_shell(profile, command, opts["cwd"] or _env_cwd(), timeout)
255
+
256
+
257
+ def cmd_js(args: list) -> int:
258
+ opts, parts = _extract_opts(args)
259
+ code = " ".join(parts).strip()
260
+ if not code:
261
+ raise CliError("nothing to run. Usage: hyd js '<javascript>'")
262
+ name, profile = _resolve_profile(opts["profile"])
263
+ if _profile_type(profile) != "browser":
264
+ raise CliError(f"profile '{name}' is a terminal profile — use `hyd sh '<command>'`.")
265
+ timeout = opts["timeout"] if opts["timeout"] is not None else 30
266
+ return _run_js(profile, code, timeout)
267
+
268
+
269
+ def cmd_call(args: list) -> int:
270
+ """Generic passthrough: call any service function. `hyd call <fn> [--json '{...}'] [k=v ...]`."""
271
+ opts, rest = _extract_opts(args)
272
+ if not rest:
273
+ raise CliError("usage: hyd call <function> [--json '{...}'] [key=value ...]")
274
+ fn, params = rest[0], {}
275
+ i = 1
276
+ while i < len(rest):
277
+ a = rest[i]
278
+ if a == "--json" and i + 1 < len(rest):
279
+ params.update(json.loads(rest[i + 1])); i += 2; continue
280
+ if "=" in a:
281
+ k, v = a.split("=", 1)
282
+ params[k] = v; i += 1; continue
283
+ i += 1
284
+ _name, profile = _resolve_profile(opts["profile"])
285
+ _print_value(_call_remote(profile, fn, params))
286
+ return 0
287
+
288
+
289
+ def cmd_nav(args: list) -> int:
290
+ if not args:
291
+ raise CliError("usage: hyd nav <url>")
292
+ name, profile = _resolve_profile()
293
+ if _profile_type(profile) != "browser":
294
+ raise CliError(f"profile '{name}' is not a browser profile.")
295
+ _print_value(_call_remote(profile, "navigate", {"url": args[0]}))
296
+ return 0
297
+
298
+
299
+ def cmd_shot(args: list) -> int:
300
+ import base64
301
+ name, profile = _resolve_profile()
302
+ if _profile_type(profile) != "browser":
303
+ raise CliError(f"profile '{name}' is not a browser profile.")
304
+ data = _call_remote(profile, "take_screenshot", {})
305
+ b64 = data.get("base64") or (data.get("data_url", "").split(",", 1)[-1] if data.get("data_url") else "")
306
+ if not b64:
307
+ raise CliError(f"no screenshot data: {data.get('error', data)}")
308
+ path = args[0] if args else "screenshot.png"
309
+ with open(path, "wb") as f:
310
+ f.write(base64.b64decode(b64))
311
+ print(path)
312
+ return 0
313
+
314
+
203
315
  def cmd_py(args: list) -> int:
204
316
  opts, code_parts = _extract_opts(args)
205
317
  code = " ".join(code_parts).strip()
206
318
  if not code:
207
319
  raise CliError("nothing to run. Usage: hyd py '<python code>'")
208
- _name, profile = _resolve_profile(opts["profile"])
320
+ name, profile = _resolve_profile(opts["profile"])
321
+ if _profile_type(profile) == "browser":
322
+ raise CliError(f"profile '{name}' is a browser profile — use `hyd js '<code>'`.")
209
323
  data = _call_remote(profile, "execute_code", {"code": code})
210
324
  if isinstance(data, dict):
211
325
  if data.get("stdout"):
@@ -243,26 +357,30 @@ def cmd_profile_add(args: list) -> int:
243
357
  i = 0
244
358
  while i < len(args):
245
359
  a = args[i]
246
- if a in ("--token", "--workspace") and i + 1 < len(args):
360
+ if a in ("--token", "--workspace", "--type") and i + 1 < len(args):
247
361
  opts[a.lstrip("-")] = args[i + 1]; i += 2; continue
248
362
  pos.append(a); i += 1
249
363
  if len(pos) < 2:
250
- raise CliError("usage: hyd profile add <name> <service_url> [--token T] [--workspace W] [--use]")
364
+ raise CliError("usage: hyd profile add <name> <service_url> [--type terminal|browser] [--token T] [--use]")
251
365
  name, url = pos[0], _normalize_url(pos[1])
366
+ ptype = opts.get("type")
367
+ if ptype and ptype not in ("terminal", "browser"):
368
+ raise CliError("--type must be 'terminal' or 'browser'")
252
369
  profiles = _load_profiles()
253
370
  entry = profiles.get(name, {})
254
371
  entry["service_url"] = url
372
+ entry["type"] = ptype or entry.get("type") or _infer_type(url)
255
373
  if opts.get("token"):
256
374
  entry["token"] = opts["token"]
257
375
  if opts.get("workspace"):
258
376
  entry["workspace"] = opts["workspace"]
259
377
  profiles[name] = entry
260
378
  _save_profiles(profiles)
379
+ label = f"{url} [{entry['type']}]"
261
380
  if use:
262
- _emit_state({"HYD_PROFILE": name}, human=f"profile '{name}' -> {url} (now active)")
381
+ _emit_state({"HYD_PROFILE": name}, human=f"profile '{name}' -> {label} (now active)")
263
382
  else:
264
- # No current-state change just confirm (eval-safe comment).
265
- print(f"# profile '{name}' -> {url} (activate: export HYD_PROFILE={name})")
383
+ print(f"# profile '{name}' -> {label} (activate: export HYD_PROFILE={name})")
266
384
  return 0
267
385
 
268
386
 
@@ -275,7 +393,7 @@ def cmd_profile_list(_args: list) -> int:
275
393
  for name, p in profiles.items():
276
394
  mark = "*" if name == active else " "
277
395
  tok = " (token)" if p.get("token") else ""
278
- print(f"{mark} {name} {p.get('service_url', '')}{tok}")
396
+ print(f"{mark} {name} [{_profile_type(p)}] {p.get('service_url', '')}{tok}")
279
397
  return 0
280
398
 
281
399
 
@@ -331,11 +449,16 @@ def cmd_status(_args: list) -> int:
331
449
  name, profile = _resolve_profile()
332
450
  except CliError as e:
333
451
  print(e); return 1
334
- print(f"profile: {name} cwd: {_env_cwd() or '(remote default)'} (HYD_PROFILE/HYD_CWD)")
452
+ ptype = _profile_type(profile)
453
+ print(f"profile: {name} [{ptype}] cwd: {_env_cwd() or '(remote default)'} (HYD_PROFILE/HYD_CWD)")
335
454
  print(f"url: {_normalize_url(profile['service_url'])}")
336
455
  try:
337
- info = _call_remote(profile, "get_process_info", {})
338
- print(f"connected: pid={info.get('pid')} host={info.get('hostname')} py={info.get('python_version')}")
456
+ if ptype == "browser":
457
+ info = _call_remote(profile, "get_page_info", {})
458
+ print(f"connected: url={info.get('url')} title={info.get('title')}")
459
+ else:
460
+ info = _call_remote(profile, "get_process_info", {})
461
+ print(f"connected: pid={info.get('pid')} host={info.get('hostname')} py={info.get('python_version')}")
339
462
  except CliError as e:
340
463
  print(f"UNREACHABLE: {e}"); return 1
341
464
  return 0
@@ -359,17 +482,20 @@ def cmd_shell_init(_args: list) -> int:
359
482
 
360
483
  def _print_help() -> int:
361
484
  print(
362
- """hyd — Hypha remote debugger CLI (run remote shell commands with minimal overhead)
485
+ """hyd — Hypha remote CLI: drive a terminal (Python process) OR a browser with minimal overhead
363
486
 
364
- USAGE
365
- hyd sh '<command>' Run a shell command on the active profile (cwd via HYD_CWD)
366
- hyd '<command>' Same (bare form; anything not a subcommand runs as a command)
367
- hyd py '<python>' Run Python via the debugger's execute_code
368
- hyd -p <profile> sh '...' Run against a specific profile (overrides HYD_PROFILE)
487
+ USAGE (adapts to the active profile's type)
488
+ hyd '<x>' Bare: runs <x> as SHELL on a terminal profile, JAVASCRIPT on a browser one
489
+ hyd sh '<command>' Force a shell command (terminal profile; cwd via HYD_CWD)
490
+ hyd js '<javascript>' Force JavaScript (browser profile) — returns the result
491
+ hyd py '<python>' Run Python via execute_code (terminal profile)
492
+ hyd call <fn> [--json '{…}'] [k=v …] Call ANY service function (either type)
493
+ hyd nav <url> | shot [file] Browser: navigate / save a screenshot (PNG)
494
+ hyd -p <profile> … Run against a specific profile (overrides HYD_PROFILE)
369
495
 
370
- PROFILES (machines — stored on disk)
371
- hyd profile add <name> <service_url> [--token T] [--workspace W] [--use]
372
- hyd profile list | show <name> | rm <name>
496
+ PROFILES (machines — stored on disk; each has a type: terminal|browser)
497
+ hyd profile add <name> <service_url> [--type terminal|browser] [--token T] [--use]
498
+ hyd profile list | show <name> | rm <name> (type inferred from the URL if omitted)
373
499
 
374
500
  CURRENT STATE (env vars, per-terminal — NOT on disk)
375
501
  export HYD_PROFILE=<name> Select the active profile for this shell
@@ -397,10 +523,20 @@ def main(argv=None) -> int:
397
523
  from hypha_debugger import __version__
398
524
  print(f"hyd (hypha-debugger) {__version__}")
399
525
  return 0
400
- if cmd in ("sh", "run", "exec"):
526
+ if cmd in ("run", "exec"):
527
+ return cmd_run(argv[1:])
528
+ if cmd == "sh":
401
529
  return cmd_sh(argv[1:])
530
+ if cmd == "js":
531
+ return cmd_js(argv[1:])
402
532
  if cmd == "py":
403
533
  return cmd_py(argv[1:])
534
+ if cmd == "call":
535
+ return cmd_call(argv[1:])
536
+ if cmd == "nav":
537
+ return cmd_nav(argv[1:])
538
+ if cmd == "shot":
539
+ return cmd_shot(argv[1:])
404
540
  if cmd in ("profile", "profiles"):
405
541
  return cmd_profile(argv[1:] if cmd == "profile" else ["list"] + argv[1:])
406
542
  if cmd == "use":
@@ -413,8 +549,9 @@ def main(argv=None) -> int:
413
549
  return cmd_status(argv[1:])
414
550
  if cmd == "shell-init":
415
551
  return cmd_shell_init(argv[1:])
416
- # Bare fallback: the whole argv is a shell command.
417
- return cmd_sh(argv, bare=True)
552
+ # Bare fallback: run the whole argv, dispatching by profile type
553
+ # (shell on a terminal profile, JavaScript on a browser one).
554
+ return cmd_run(argv, bare=True)
418
555
  except CliError as e:
419
556
  sys.stderr.write(f"hyd: {e}\n")
420
557
  return 2
@@ -122,6 +122,13 @@ Key ideas:
122
122
  `hyd sh`.
123
123
  - **Multiple machines**: add more profiles (`hyd profile add other "<url>" --token …`)
124
124
  and switch with `export HYD_PROFILE=other` or a one-off `hyd -p other sh '…'`.
125
+ - **Terminal *or* browser**: each profile has a `type` (inferred from the URL). A
126
+ terminal profile (this Python debugger) runs shell/Python; a `browser` profile (a
127
+ Hypha Navigator web service) runs JavaScript. The SAME bare `hyd '<x>'` adapts —
128
+ shell on a terminal profile, JS on a browser one — or force it with `hyd sh` /
129
+ `hyd js`. `hyd call <fn> [--json '{…}']` calls any function; `hyd nav <url>` /
130
+ `hyd shot [file]` are browser shortcuts. Example:
131
+ `hyd profile add web "<navigator-url>" --type browser --use && hyd 'document.title'`.
125
132
  - `hyd sh` streams stdout/stderr and exits with the remote command's exit code, so
126
133
  it behaves like a local shell. Run `hyd` (no args) for full help.
127
134
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypha-debugger
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
5
5
  Author: Amun AI AB
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hypha-debugger"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Injectable debugger for Python processes and AI agents, powered by Hypha RPC"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -116,6 +116,59 @@ def test_bare_fallback_runs_as_command(cfg, monkeypatch):
116
116
  assert seen["command"] == "ls -la"
117
117
 
118
118
 
119
+ def test_type_inference_browser_and_terminal(cfg):
120
+ cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
121
+ cli.main(["profile", "add", "box", "https://h.io/services/py-debugger-xyz"])
122
+ profs = cli._load_profiles()
123
+ assert profs["web"]["type"] == "browser"
124
+ assert profs["box"]["type"] == "terminal"
125
+
126
+
127
+ def test_type_explicit_override(cfg):
128
+ cli.main(["profile", "add", "x", "https://h.io/services/anything", "--type", "browser"])
129
+ assert cli._load_profiles()["x"]["type"] == "browser"
130
+
131
+
132
+ def test_bare_dispatches_by_type(cfg, monkeypatch):
133
+ cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
134
+ monkeypatch.setenv("HYD_PROFILE", "web")
135
+ seen = {}
136
+ monkeypatch.setattr(cli, "_call_remote", lambda profile, fn, params, http_timeout=75: seen.update(fn=fn, params=params) or {"result": "My Page", "type": "string"})
137
+ rc = cli.main(["document.title"]) # bare, browser profile -> execute_script
138
+ assert rc == 0 and seen["fn"] == "execute_script" and seen["params"]["code"] == "document.title"
139
+
140
+
141
+ def test_js_result_printed(cfg, monkeypatch, capsys):
142
+ cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
143
+ monkeypatch.setenv("HYD_PROFILE", "web")
144
+ monkeypatch.setattr(cli, "_call_remote", lambda *a, **k: {"result": {"a": 1}, "type": "object"})
145
+ cli.main(["js", "({a:1})"])
146
+ assert '"a": 1' in capsys.readouterr().out
147
+
148
+
149
+ def test_sh_rejects_browser_profile(cfg, monkeypatch, capsys):
150
+ cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
151
+ monkeypatch.setenv("HYD_PROFILE", "web")
152
+ rc = cli.main(["sh", "ls"])
153
+ assert rc == 2 and "browser profile" in capsys.readouterr().err
154
+
155
+
156
+ def test_js_rejects_terminal_profile(cfg, monkeypatch, capsys):
157
+ cli.main(["profile", "add", "box", "https://h.io/services/py-debugger-xyz"])
158
+ monkeypatch.setenv("HYD_PROFILE", "box")
159
+ rc = cli.main(["js", "1+1"])
160
+ assert rc == 2 and "terminal profile" in capsys.readouterr().err
161
+
162
+
163
+ def test_call_generic_passthrough(cfg, monkeypatch):
164
+ cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
165
+ monkeypatch.setenv("HYD_PROFILE", "web")
166
+ seen = {}
167
+ monkeypatch.setattr(cli, "_call_remote", lambda profile, fn, params, http_timeout=75: seen.update(fn=fn, params=params) or {"ok": True})
168
+ cli.main(["call", "navigate", "--json", '{"url":"https://x.com"}'])
169
+ assert seen["fn"] == "navigate" and seen["params"]["url"] == "https://x.com"
170
+
171
+
119
172
  def test_p_flag_selects_profile(cfg, monkeypatch):
120
173
  cli.main(["profile", "add", "a", "https://a.io/s/1"])
121
174
  cli.main(["profile", "add", "b", "https://b.io/s/2"])
File without changes
File without changes