podojo-cli 1.25.0__tar.gz → 1.25.2__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 (37) hide show
  1. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/CHANGELOG.md +26 -0
  2. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/PKG-INFO +2 -2
  3. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/README.md +1 -1
  4. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/pyproject.toml +1 -1
  5. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/aiinterviews.py +9 -6
  6. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/usertests.py +40 -20
  7. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_aiinterviews.py +15 -0
  8. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_usertests.py +25 -0
  9. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/uv.lock +1 -1
  10. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/.github/workflows/publish.yml +0 -0
  11. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/.gitignore +0 -0
  12. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/CLAUDE.md +0 -0
  13. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/LICENSE +0 -0
  14. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/__init__.py +0 -0
  15. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/client.py +0 -0
  16. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/__init__.py +0 -0
  17. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/auth.py +0 -0
  18. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/interviews.py +0 -0
  19. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/projects.py +0 -0
  20. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/showreel.py +0 -0
  21. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/synth.py +0 -0
  22. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/transcripts.py +0 -0
  23. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/videos.py +0 -0
  24. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/config.py +0 -0
  25. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/main.py +0 -0
  26. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/synth/__init__.py +0 -0
  27. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/synth/driver.py +0 -0
  28. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/synth/session.py +0 -0
  29. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/version_check.py +0 -0
  30. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/video/__init__.py +0 -0
  31. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/video/showreel.py +0 -0
  32. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/conftest.py +0 -0
  33. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_auth.py +0 -0
  34. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_interviews.py +0 -0
  35. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_projects.py +0 -0
  36. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_showreel.py +0 -0
  37. {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_transcripts.py +0 -0
@@ -5,6 +5,32 @@ All notable changes to the Podojo CLI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org).
7
7
 
8
+ ## [1.25.2] - 2026-10-01
9
+
10
+ ### Changed
11
+ - `usertests snippet` prints the recording snippet as two script tags plus a
12
+ `recorder.js` file to save next to the pages. The `rrweb.record()` call used
13
+ to be an inline `<script>`, which Boltable's content security policy
14
+ refuses, so recording never started on Boltable prototypes. The hints after
15
+ `usertests create` and on a failed go-live check now say to add the snippet
16
+ to every page instead of the `<head>`.
17
+
18
+ ### Fixed
19
+ - `usertests get` and `aiinterviews get` print plain YAML, so
20
+ `get > file.yaml` can be edited and pushed back with `update -f`. The
21
+ output used to be hard-wrapped at the terminal width (or 80 columns when
22
+ redirected), which split long links and broke the YAML, and `[...]` text
23
+ was treated as console markup. The response count and preview/live links
24
+ now go to stderr, so they no longer end up in the saved file.
25
+
26
+ ## [1.25.1] - 2026-09-24
27
+
28
+ ### Fixed
29
+ - YAML files for AI interviews and usertests are now read as UTF-8
30
+ explicitly. Reading them with the machine's locale encoding double-
31
+ encoded multi-byte characters (em-dashes, arrows) into mojibake when
32
+ uploading from a Windows machine.
33
+
8
34
  ## [1.25.0] - 2026-09-20
9
35
 
10
36
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: podojo-cli
3
- Version: 1.25.0
3
+ Version: 1.25.2
4
4
  Summary: CLI for the Podojo user research platform
5
5
  Project-URL: Homepage, https://github.com/podojo/cli-podojo
6
6
  Project-URL: Source, https://github.com/podojo/cli-podojo
@@ -116,7 +116,7 @@ podojo usertests list
116
116
  podojo usertests get checkout-usability-v1
117
117
  podojo usertests update checkout-usability-v1 -f changes.yaml
118
118
  podojo usertests delete checkout-usability-v1
119
- podojo usertests snippet # recorder script for self-hosted prototypes
119
+ podojo usertests snippet # recording snippet: two script tags + recorder.js
120
120
  ```
121
121
 
122
122
  A user test can open with an optional participant screener: on-screen
@@ -84,7 +84,7 @@ podojo usertests list
84
84
  podojo usertests get checkout-usability-v1
85
85
  podojo usertests update checkout-usability-v1 -f changes.yaml
86
86
  podojo usertests delete checkout-usability-v1
87
- podojo usertests snippet # recorder script for self-hosted prototypes
87
+ podojo usertests snippet # recording snippet: two script tags + recorder.js
88
88
  ```
89
89
 
90
90
  A user test can open with an optional participant screener: on-screen
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "1.25.0"
3
+ version = "1.25.2"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -11,6 +11,7 @@ from ..config import load_config
11
11
 
12
12
  app = typer.Typer(help="Manage AI voice interviews")
13
13
  console = Console()
14
+ err_console = Console(stderr=True, soft_wrap=True)
14
15
 
15
16
  REQUIRED_FIELDS = ["interview_id", "title", "questions"]
16
17
  REQUIRED_QUESTION_FIELDS = ["text"]
@@ -324,7 +325,7 @@ def _load_yaml(path: Path) -> dict:
324
325
  console.print(f"[red]Error:[/red] File not found: {path}")
325
326
  raise typer.Exit(1)
326
327
  try:
327
- with open(path) as f:
328
+ with open(path, encoding="utf-8") as f:
328
329
  data = yaml.safe_load(f)
329
330
  except yaml.YAMLError as e:
330
331
  console.print(f"[red]Error:[/red] Invalid YAML syntax:\n{e}")
@@ -449,16 +450,18 @@ def get_ai_interview(
449
450
  for key in ("id", "created_at", "created_by", "last_updated"):
450
451
  interview.pop(key, None)
451
452
 
452
- console.print(yaml.dump(interview, default_flow_style=False, sort_keys=False, allow_unicode=True))
453
+ # Plain stdout, so `get > file.yaml` round-trips: rich would hard-wrap long
454
+ # lines and eat [markup]. The info lines go to stderr for the same reason.
455
+ typer.echo(yaml.dump(interview, default_flow_style=False, sort_keys=False, allow_unicode=True))
453
456
  max_responses = interview.get("max_responses")
454
457
  if max_responses:
455
- console.print(f"Responses: {response_count} / {max_responses}")
458
+ err_console.print(f"Responses: {response_count} / {max_responses}")
456
459
  elif response_count:
457
- console.print(f"Responses: {response_count}")
460
+ err_console.print(f"Responses: {response_count}")
458
461
  if group:
459
462
  base = _ai_interviews_url()
460
- console.print(f"Preview: {base}/preview/{group}/{interview_id}")
461
- console.print(f"Live: {base}/{group}/{interview_id}")
463
+ err_console.print(f"Preview: {base}/preview/{group}/{interview_id}")
464
+ err_console.print(f"Live: {base}/{group}/{interview_id}")
462
465
 
463
466
 
464
467
  @app.command("create")
@@ -10,26 +10,44 @@ from ..client import PodojoClient
10
10
 
11
11
  app = typer.Typer(help="Manage unmoderated user tests")
12
12
  console = Console()
13
+ err_console = Console(stderr=True, soft_wrap=True)
13
14
 
14
15
  REQUIRED_FIELDS = ["usertest_id", "title", "prototype_url", "steps"]
15
16
  VALID_STEP_TYPES = {"screen", "prototype"}
16
17
  VALID_STEP_VARIANTS = {"question", "task", "instruction"}
17
18
  REQUIRED_STEP_FIELDS = ["type", "title"]
18
19
 
19
- RECORDER_SNIPPET = """\
20
+ # The rrweb.record() call lives in recorder.js rather than an inline <script>:
21
+ # Boltable's CSP refuses inline scripts, so an inline call never runs there.
22
+ SNIPPET_TAGS = """\
20
23
  <script src="https://usertests.podojo.com/player-v1.js" integrity="sha384-UpKcjeLZtEcYxELLISURK3MAcN3KQdTx8iygewcEWVl8LqgVrftgiMtuMHod7D8e" crossorigin="anonymous"></script>
21
- <script>
24
+ <script src="./recorder.js"></script>"""
25
+
26
+ RECORDER_JS = """\
27
+ (function initRecorder() {
22
28
  if (typeof rrweb === "undefined") {
23
29
  console.error("[rrweb] failed to load — screen recording disabled");
24
- } else {
25
- rrweb.record({
26
- emit(event) {
27
- window.parent.postMessage({ type: "rrweb-event", event: event }, "*");
28
- },
29
- checkoutEveryNms: 30000,
30
- });
30
+ return;
31
31
  }
32
- </script>
32
+
33
+ rrweb.record({
34
+ emit(event) {
35
+ window.parent.postMessage({ type: "rrweb-event", event: event }, "*");
36
+ },
37
+ checkoutEveryNms: 30000,
38
+ });
39
+ })();"""
40
+
41
+ RECORDER_SNIPPET = f"""\
42
+ 1. Add these two lines to every HTML page of your prototype, just before </body>:
43
+
44
+ {SNIPPET_TAGS}
45
+
46
+ 2. Save this as recorder.js in the same folder as your pages:
47
+
48
+ {RECORDER_JS}
49
+
50
+ Copy both exactly – any change stops the recording.
33
51
  """
34
52
 
35
53
  EXAMPLE_YAML = """\
@@ -268,7 +286,7 @@ def _load_yaml(path: Path) -> dict:
268
286
  console.print(f"[red]Error:[/red] File not found: {path}")
269
287
  raise typer.Exit(1)
270
288
  try:
271
- with open(path) as f:
289
+ with open(path, encoding="utf-8") as f:
272
290
  data = yaml.safe_load(f)
273
291
  except yaml.YAMLError as e:
274
292
  console.print(f"[red]Error:[/red] Invalid YAML syntax:\n{e}")
@@ -320,7 +338,7 @@ def _print_snippet_gate_error(e: httpx.HTTPStatusError) -> None:
320
338
  reason = f"the recording snippet was not found at {url}"
321
339
  console.print(f"[red]Error:[/red] Not going live: {reason}.")
322
340
  console.print("Without the snippet, participant screens are not recorded.")
323
- console.print(" - Add the snippet to your prototype's <head>: [bold]podojo usertests snippet[/bold]")
341
+ console.print(" - Add the snippet to every page of your prototype: [bold]podojo usertests snippet[/bold]")
324
342
  console.print(" - or set [bold]live: false[/bold] to save a draft")
325
343
  console.print(" - or re-run with [bold]--allow-missing-snippet[/bold] to go live without screen recording")
326
344
 
@@ -404,15 +422,17 @@ def get_usertest(
404
422
  for key in ("id", "created_at", "created_by", "last_updated"):
405
423
  usertest.pop(key, None)
406
424
 
407
- console.print(yaml.dump(usertest, default_flow_style=False, sort_keys=False, allow_unicode=True))
425
+ # Plain stdout, so `get > file.yaml` round-trips: rich would hard-wrap long
426
+ # lines and eat [markup]. The info lines go to stderr for the same reason.
427
+ typer.echo(yaml.dump(usertest, default_flow_style=False, sort_keys=False, allow_unicode=True))
408
428
  max_responses = usertest.get("max_responses")
409
429
  if max_responses:
410
- console.print(f"Responses: {response_count} / {max_responses}")
430
+ err_console.print(f"Responses: {response_count} / {max_responses}")
411
431
  elif response_count:
412
- console.print(f"Responses: {response_count}")
432
+ err_console.print(f"Responses: {response_count}")
413
433
  if group:
414
- console.print(f"Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
415
- console.print(f"Live: https://usertests.podojo.com/{group}?test={usertest_id}")
434
+ err_console.print(f"Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
435
+ err_console.print(f"Live: https://usertests.podojo.com/{group}?test={usertest_id}")
416
436
 
417
437
 
418
438
  @app.command("create")
@@ -457,8 +477,8 @@ def create_usertest(
457
477
  console.print(f" Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
458
478
  console.print(f" Live: https://usertests.podojo.com/{group}?test={usertest_id}")
459
479
  console.print(
460
- "\n[dim]If your prototype is self-hosted, add the recorder script to its "
461
- "[bold]<head>[/bold] to enable screen recording.\n"
480
+ "\n[dim]To record participants' screens, every page of your prototype needs "
481
+ "the recording snippet.\n"
462
482
  "Run [bold]podojo usertests snippet[/bold] to print it.[/dim]"
463
483
  )
464
484
 
@@ -539,5 +559,5 @@ def example():
539
559
 
540
560
  @app.command("snippet")
541
561
  def snippet():
542
- """Print the recorder script to embed in self-hosted prototypes."""
562
+ """Print the recording snippet: two script tags for every page, plus recorder.js."""
543
563
  print(RECORDER_SNIPPET)
@@ -119,6 +119,21 @@ def test_get_ai_interview(runner, httpx_mock):
119
119
  assert "http://interviews.test.local/test-group/ai-1" in result.output
120
120
 
121
121
 
122
+ def test_get_ai_interview_stdout_round_trips(runner, httpx_mock):
123
+ overview = "Participants [drivers in Tallinn] talk about " + "their last week of rides " * 6
124
+ httpx_mock.add_response(
125
+ url="http://test.local/api/v1/ai-interviews/ai-1",
126
+ json={**GET_RESPONSE, "overview": overview},
127
+ )
128
+
129
+ result = runner.invoke(app, ["aiinterviews", "get", "ai-1"])
130
+
131
+ data = yaml.safe_load(result.stdout)
132
+ assert data["overview"] == overview
133
+ assert "Responses" not in data and "Preview" not in data and "Live" not in data
134
+ assert "Responses: 7 / 20" in result.stderr
135
+
136
+
122
137
  def test_get_ai_interview_not_found(runner, httpx_mock):
123
138
  httpx_mock.add_response(
124
139
  url="http://test.local/api/v1/ai-interviews/nonexistent",
@@ -1,3 +1,5 @@
1
+ import yaml
2
+
1
3
  from podojo_cli.main import app
2
4
 
3
5
 
@@ -107,6 +109,26 @@ def test_get_usertest(runner, httpx_mock):
107
109
  assert "https://usertests.podojo.com/test-group?test=ut-1" in result.output
108
110
 
109
111
 
112
+ def test_get_usertest_stdout_round_trips(runner, httpx_mock):
113
+ long_url = "https://uxr-prototypes.static.boltable.eu/_t/y2eb-2jft-ekzv-cn53/price-lock-test/1.html"
114
+ httpx_mock.add_response(
115
+ url="http://test.local/api/v1/usertests/ut-1",
116
+ json={
117
+ **GET_RESPONSE,
118
+ "prototype_url": long_url,
119
+ "welcome_text": "Tap [Continue] when you are ready",
120
+ },
121
+ )
122
+
123
+ result = runner.invoke(app, ["usertests", "get", "ut-1"])
124
+
125
+ data = yaml.safe_load(result.stdout)
126
+ assert data["prototype_url"] == long_url
127
+ assert data["welcome_text"] == "Tap [Continue] when you are ready"
128
+ assert "Responses" not in data and "Preview" not in data and "Live" not in data
129
+ assert "Preview: https://usertests.podojo.com/preview/test-group/ut-1" in result.stderr
130
+
131
+
110
132
  def test_get_usertest_not_found(runner, httpx_mock):
111
133
  httpx_mock.add_response(
112
134
  url="http://test.local/api/v1/usertests/nonexistent",
@@ -615,4 +637,7 @@ def test_snippet(runner):
615
637
  assert result.exit_code == 0
616
638
  assert "player-v1.js" in result.output
617
639
  assert 'integrity="sha384-' in result.output
640
+ assert '<script src="./recorder.js"></script>' in result.output
618
641
  assert "rrweb.record" in result.output
642
+ # the recording must start from recorder.js: Boltable refuses inline scripts
643
+ assert "<script>" not in result.output
@@ -438,7 +438,7 @@ wheels = [
438
438
 
439
439
  [[package]]
440
440
  name = "podojo-cli"
441
- version = "1.25.0"
441
+ version = "1.25.2"
442
442
  source = { editable = "." }
443
443
  dependencies = [
444
444
  { name = "httpx" },
File without changes
File without changes
File without changes