sediment-cli 0.4.0__tar.gz → 0.5.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.
Files changed (88) hide show
  1. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/PKG-INFO +5 -5
  2. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/pyproject.toml +5 -5
  3. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/attribution.py +26 -10
  4. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/cli.py +4 -4
  5. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/client.py +5 -1
  6. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/ui.py +5 -0
  7. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_remote.py +90 -0
  8. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_delivery_transport.py +7 -2
  9. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/sediment.txt +1 -1
  10. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/.gitignore +0 -0
  11. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/LICENSE +0 -0
  12. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/hatch_build.py +0 -0
  13. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/__init__.py +0 -0
  14. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/LICENSE +0 -0
  15. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/README.md +0 -0
  16. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/index.ts +0 -0
  17. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/lib/contract.ts +0 -0
  18. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/lib/process.ts +0 -0
  19. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/lib/provider.ts +0 -0
  20. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/lib/register.ts +0 -0
  21. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/lib/retrieval.ts +0 -0
  22. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/_pi/package.json +0 -0
  23. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/agent_guide.txt +0 -0
  24. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/delivery.py +0 -0
  25. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/evidence.py +0 -0
  26. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/local_postgres.py +0 -0
  27. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/sediment_cli/transcript.py +0 -0
  28. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/conftest.py +0 -0
  29. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_agent_guide.py +0 -0
  30. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli.py +0 -0
  31. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_capture_authority.py +0 -0
  32. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_db.py +0 -0
  33. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_evidence.py +0 -0
  34. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_evidence_integration.py +0 -0
  35. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_help.py +0 -0
  36. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_libpq_unavailable.py +0 -0
  37. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_cli_ui.py +0 -0
  38. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_consumer_profile_cli.py +0 -0
  39. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_demo_verb.py +0 -0
  40. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_e2e_quickstart.py +0 -0
  41. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_installed_wheel.py +0 -0
  42. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_local_postgres.py +0 -0
  43. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/test_server_migration.py +0 -0
  44. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/commit.txt +0 -0
  45. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/db-check.txt +0 -0
  46. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/db-provision.txt +0 -0
  47. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/db-status.txt +0 -0
  48. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/db-upgrade.txt +0 -0
  49. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/db.txt +0 -0
  50. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/delivery-enqueue.txt +0 -0
  51. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/delivery-replay.txt +0 -0
  52. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/delivery-status.txt +0 -0
  53. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/delivery.txt +0 -0
  54. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/demo.txt +0 -0
  55. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/derive.txt +0 -0
  56. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/doctor.txt +0 -0
  57. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/evidence-fetch.txt +0 -0
  58. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/evidence-inspect.txt +0 -0
  59. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/evidence-inventory.txt +0 -0
  60. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/evidence.txt +0 -0
  61. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/export-diff-sft.txt +0 -0
  62. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/export-dpo.txt +0 -0
  63. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/export-recovery.txt +0 -0
  64. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/export-rlvr.txt +0 -0
  65. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/export-sft.txt +0 -0
  66. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/export.txt +0 -0
  67. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/facts.txt +0 -0
  68. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/guide.txt +0 -0
  69. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/install.txt +0 -0
  70. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/login.txt +0 -0
  71. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/logout.txt +0 -0
  72. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/mirror-gc.txt +0 -0
  73. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/quarantine-inference-calls.txt +0 -0
  74. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/quarantine-log.txt +0 -0
  75. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/quarantine.txt +0 -0
  76. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/release.txt +0 -0
  77. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-abandonment.txt +0 -0
  78. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-attribution-share.txt +0 -0
  79. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-dataset-diagnostics.txt +0 -0
  80. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-label-confidence-inspection.txt +0 -0
  81. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-lifecycle.txt +0 -0
  82. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-merge-retention.txt +0 -0
  83. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-model.txt +0 -0
  84. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-precision.txt +0 -0
  85. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report-recovery-yield.txt +0 -0
  86. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/report.txt +0 -0
  87. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/server.txt +0 -0
  88. {sediment_cli-0.4.0 → sediment_cli-0.5.0}/tests/testdata/help/uninstall.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sediment-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: The sediment command: login, install, server, facts, exports
5
5
  Project-URL: Source, https://github.com/sediment-ai/sediment
6
6
  Project-URL: Documentation, https://github.com/sediment-ai/sediment/tree/main/docs
@@ -10,7 +10,7 @@ License-Expression: AGPL-3.0-or-later
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.12
12
12
  Requires-Dist: httpx>=0.27
13
- Requires-Dist: sediment-api==0.4.0
14
- Requires-Dist: sediment-core==0.4.0
15
- Requires-Dist: sediment-derive==0.4.0
16
- Requires-Dist: sediment-export==0.4.0
13
+ Requires-Dist: sediment-api==0.5.0
14
+ Requires-Dist: sediment-core==0.5.0
15
+ Requires-Dist: sediment-derive==0.5.0
16
+ Requires-Dist: sediment-export==0.5.0
@@ -5,7 +5,7 @@
5
5
  # Depends on sediment-api deliberately: the operator verbs are DB-local by
6
6
  # design (ADR 0001) and `server`/reports/mirror-gc forward into it.
7
7
  name = "sediment-cli"
8
- version = "0.4.0"
8
+ version = "0.5.0"
9
9
  description = "The sediment command: login, install, server, facts, exports"
10
10
  requires-python = ">=3.12"
11
11
  license = "AGPL-3.0-or-later"
@@ -14,10 +14,10 @@ license-files = ["LICENSE"]
14
14
  # so an install never mixes member versions (the release workflow asserts
15
15
  # the versions agree before publishing).
16
16
  dependencies = [
17
- "sediment-core==0.4.0",
18
- "sediment-derive==0.4.0",
19
- "sediment-export==0.4.0",
20
- "sediment-api==0.4.0",
17
+ "sediment-core==0.5.0",
18
+ "sediment-derive==0.5.0",
19
+ "sediment-export==0.5.0",
20
+ "sediment-api==0.5.0",
21
21
  "httpx>=0.27",
22
22
  ]
23
23
 
@@ -105,9 +105,12 @@ try:
105
105
  except ImportError: # pragma: no cover — run by path, not as a package
106
106
  # The checkout shim (scripts/sediment_attribution.py) and the fleet
107
107
  # bundle execute this file standalone, where no sibling ui module
108
- # exists. Those are hook/MDM piped contexts: plain text is the correct
109
- # output there, so a passthrough stand-in keeps the bytes identical.
108
+ # exists. Keep styling plain and match ui's server-text display filter.
110
109
  class ui: # type: ignore[no-redef] # ponytail: plain-text stand-in
110
+ @staticmethod
111
+ def printable_text(value: object) -> str:
112
+ return "".join(char for char in str(value) if char.isprintable())
113
+
111
114
  @staticmethod
112
115
  def style(text: str, *names: str, stream=None) -> str:
113
116
  return text
@@ -2876,13 +2879,23 @@ def _env_pairs(
2876
2879
 
2877
2880
 
2878
2881
  def _write_0600(path: Path, content: str) -> None:
2879
- """0600 on create AND on rewrite — O_CREAT's mode applies only at
2880
- creation, and these files carry the bearer token."""
2882
+ """Publish a complete private file without truncating the previous version."""
2883
+ if path.is_symlink():
2884
+ raise ValueError(
2885
+ "refusing to replace a symbolic link with private configuration"
2886
+ )
2881
2887
  path.parent.mkdir(parents=True, exist_ok=True)
2882
- fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
2883
- os.fchmod(fd, 0o600)
2884
- with os.fdopen(fd, "w", encoding="utf-8") as fh:
2885
- fh.write(content)
2888
+ temporary = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
2889
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
2890
+ try:
2891
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
2892
+ os.fchmod(fh.fileno(), 0o600)
2893
+ fh.write(content)
2894
+ fh.flush()
2895
+ os.fsync(fh.fileno())
2896
+ os.replace(temporary, path)
2897
+ finally:
2898
+ temporary.unlink(missing_ok=True)
2886
2899
 
2887
2900
 
2888
2901
  def _write_env_files(pairs: list[tuple[str, str]]) -> None:
@@ -3209,11 +3222,13 @@ def _uninstall_codex_profiles() -> bool:
3209
3222
  complete = True
3210
3223
  for path in profiles:
3211
3224
  try:
3212
- if path.is_symlink() or not path.is_file():
3225
+ if not path.is_file():
3213
3226
  raise ValueError
3214
3227
  original = path.read_text(encoding="utf-8")
3215
3228
  if _CODEX_OTEL_BEGIN not in original and _CODEX_OTEL_END not in original:
3216
3229
  continue
3230
+ if path.is_symlink():
3231
+ raise ValueError
3217
3232
  remaining = _without_codex_telemetry(original)
3218
3233
  if remaining.strip():
3219
3234
  _write_0600(path, remaining)
@@ -3423,7 +3438,8 @@ def _doctor_server(findings: list[Finding]) -> None:
3423
3438
  )
3424
3439
  )
3425
3440
  return
3426
- detail = f"reachable, {authority} token valid (org {body.get('org_id')})"
3441
+ org_id = ui.printable_text(body.get("org_id"))
3442
+ detail = f"reachable, {authority} token valid (org {org_id})"
3427
3443
  try:
3428
3444
  from sediment_cli import __version__ as client_version
3429
3445
  from sediment_cli import version_skew_advice
@@ -820,7 +820,8 @@ def _store_login(
820
820
  cfg["servers"] = servers
821
821
  write_config(cfg)
822
822
  action = "capture credential enrolled for" if capture else "logged in to"
823
- print(f"{ui.glyph('✓', 'phosphor')}{action} {url} (org {me['org_id']})")
823
+ org_id = ui.printable_text(me["org_id"])
824
+ print(f"{ui.glyph('✓', 'phosphor')}{action} {url} (org {org_id})")
824
825
  return 0
825
826
 
826
827
 
@@ -908,9 +909,8 @@ def cmd_logout(args: argparse.Namespace) -> int:
908
909
  if cfg.get("current") == url:
909
910
  cfg["current"] = None
910
911
  write_config(cfg)
911
- print(
912
- f"{ui.glyph('✓', 'phosphor')}logged out of {url} (org {removed.get('org_id')})"
913
- )
912
+ org_id = ui.printable_text(removed.get("org_id"))
913
+ print(f"{ui.glyph('✓', 'phosphor')}logged out of {url} (org {org_id})")
914
914
  return 0
915
915
 
916
916
 
@@ -226,7 +226,11 @@ def _error_detail(resp: httpx.Response) -> str:
226
226
  }
227
227
  if isinstance(reason, str) and reason in messages:
228
228
  return messages[reason]
229
- return detail if isinstance(detail, str) else f"server error ({resp.status_code})"
229
+ return (
230
+ ui.printable_text(detail)
231
+ if isinstance(detail, str)
232
+ else f"server error ({resp.status_code})"
233
+ )
230
234
 
231
235
 
232
236
  def probe_me(base_url: str, token: str) -> dict[str, Any]:
@@ -54,6 +54,11 @@ _LOGO = "\n".join(
54
54
  )
55
55
 
56
56
 
57
+ def printable_text(value: object) -> str:
58
+ """Keep printable text without server-supplied terminal or line controls."""
59
+ return "".join(char for char in str(value) if char.isprintable())
60
+
61
+
57
62
  def on(stream: TextIO | None = None) -> bool:
58
63
  """Whether *stream* (default stdout) gets styled output."""
59
64
  stream = sys.stdout if stream is None else stream
@@ -862,6 +862,96 @@ def test_error_detail_uses_the_servers_detail_string() -> None:
862
862
  assert api_client._error_detail(resp) == "org_id is required"
863
863
 
864
864
 
865
+ @pytest.mark.parametrize("command", ["login", "facts", "demo"])
866
+ @pytest.mark.parametrize(
867
+ ("detail", "display"),
868
+ [
869
+ ("org_id is required", "org_id is required"),
870
+ ("Équipe 東京 — Δ 🚀", "Équipe 東京 — Δ 🚀"),
871
+ ("bad\x1b[31m\x1b]0;title\x07\r\nforged\x9b\u2028", "bad[31m]0;titleforged"),
872
+ ],
873
+ ids=["ascii", "unicode", "controls"],
874
+ )
875
+ def test_remote_errors_print_only_display_text(
876
+ tmp_path, monkeypatch, capsys, command, detail, display
877
+ ):
878
+ import httpx
879
+
880
+ url = "http://127.0.0.1:8000"
881
+ monkeypatch.setattr(api_client, "CONFIG_PATH", tmp_path / "config.json")
882
+ monkeypatch.setattr(api_client, "_version_checked", True)
883
+ monkeypatch.delenv("SEDIMENT_DATABASE_URL", raising=False)
884
+ monkeypatch.delenv("SEDIMENT_SESSION_TOKEN", raising=False)
885
+ monkeypatch.setenv("SEDIMENT_URL", url)
886
+ api_client.write_config({"current": url, "servers": {url: {"token": "synthetic"}}})
887
+ monkeypatch.setattr(
888
+ api_client,
889
+ "_http",
890
+ lambda: httpx.Client(
891
+ transport=httpx.MockTransport(
892
+ lambda request: httpx.Response(400, json={"detail": detail})
893
+ )
894
+ ),
895
+ )
896
+ monkeypatch.setattr(sys, "stdin", io.StringIO("synthetic\n"))
897
+ arguments = [command, url, "--with-token"] if command == "login" else [command]
898
+
899
+ assert cli.main(arguments) == 1
900
+
901
+ output = capsys.readouterr()
902
+ # Compare escaped output so a regression can't execute controls in test logs.
903
+ assert ascii(output.err) == ascii(f"error: {display}\n")
904
+ assert output.out == (
905
+ f"posting demo session (synthetic) to {url}\n" if command == "demo" else ""
906
+ )
907
+
908
+
909
+ @pytest.mark.parametrize(
910
+ ("org_id", "display"),
911
+ [
912
+ ("ordinary-team", "ordinary-team"),
913
+ ("Équipe 東京 — Δ 🚀", "Équipe 東京 — Δ 🚀"),
914
+ ("team\x1b]0;title\x07\r\nforged", "team]0;titleforged"),
915
+ ],
916
+ ids=["ascii", "unicode", "controls"],
917
+ )
918
+ def test_login_and_logout_filter_identity_only_for_display(
919
+ tmp_path, monkeypatch, capsys, org_id, display
920
+ ):
921
+ import httpx
922
+
923
+ url = "https://sediment.example.com"
924
+ monkeypatch.delenv("SEDIMENT_SESSION_TOKEN", raising=False)
925
+ monkeypatch.setattr(api_client, "CONFIG_PATH", tmp_path / "config.json")
926
+ monkeypatch.setattr(sys, "stdin", io.StringIO("synthetic\n"))
927
+ monkeypatch.setattr(
928
+ api_client,
929
+ "_http",
930
+ lambda: httpx.Client(
931
+ transport=httpx.MockTransport(
932
+ lambda request: httpx.Response(
933
+ 200,
934
+ json={
935
+ "org_id": org_id,
936
+ "authority": "operator",
937
+ "client_id": "operator",
938
+ },
939
+ )
940
+ )
941
+ ),
942
+ )
943
+
944
+ assert cli.main(["login", url, "--with-token"]) == 0
945
+ assert api_client.read_config()["servers"][url]["org_id"] == org_id
946
+ assert cli.main(["logout"]) == 0
947
+
948
+ output = capsys.readouterr()
949
+ assert ascii(output.out) == ascii(
950
+ f"logged in to {url} (org {display})\nlogged out of {url} (org {display})\n"
951
+ )
952
+ assert output.err == ""
953
+
954
+
865
955
  def test_commit_shows_exact_ci_without_session_observation(remote, capsys, monkeypatch):
866
956
  from sediment_api.config import settings
867
957
  from sediment_api.main import app
@@ -1171,8 +1171,13 @@ for line in sys.stdin:
1171
1171
  process.stdin.write(line)
1172
1172
  process.stdin.flush()
1173
1173
  results = [process.stdout.readline() for process in publishers]
1174
- assert results == ["queued buffered\n"] * 8, f"round {round_}"
1175
- assert delivery.status(queue)["pending"] == 8
1174
+ assert set(results) <= {"queued buffered\n", "declined storage_busy\n"}, (
1175
+ f"round {round_}"
1176
+ )
1177
+ assert "queued buffered\n" in results, f"round {round_}"
1178
+ assert delivery.status(queue)["pending"] == results.count(
1179
+ "queued buffered\n"
1180
+ )
1176
1181
  finally:
1177
1182
  for process in publishers:
1178
1183
  process.kill()
@@ -1,4 +1,4 @@
1
- sediment v0.4.0 — Turn AI developer workflow traces into RL-ready training data.
1
+ sediment v0.5.0 — Turn AI developer workflow traces into RL-ready training data.
2
2
 
3
3
  USAGE:
4
4
  sediment <command> [options]
File without changes
File without changes