borescope 0.1.0.dev1__tar.gz → 1.0.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 (167) hide show
  1. borescope-1.0.0/.gitignore +38 -0
  2. {borescope-0.1.0.dev1 → borescope-1.0.0}/PKG-INFO +17 -3
  3. {borescope-0.1.0.dev1 → borescope-1.0.0}/README.md +14 -0
  4. {borescope-0.1.0.dev1 → borescope-1.0.0}/pyproject.toml +104 -17
  5. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/__init__.py +6 -3
  6. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/__main__.py +4 -1
  7. borescope-1.0.0/src/borescope/cli.py +164 -0
  8. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/discovery.py +66 -58
  9. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/errors.py +4 -7
  10. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/juju.py +13 -14
  11. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/__init__.py +4 -1
  12. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/commands/__init__.py +4 -1
  13. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/commands/_args.py +18 -7
  14. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/commands/base.py +25 -11
  15. borescope-1.0.0/src/borescope/shell/commands/basic.py +109 -0
  16. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/commands/execcmd.py +13 -12
  17. borescope-1.0.0/src/borescope/shell/commands/filesystem.py +503 -0
  18. borescope-1.0.0/src/borescope/shell/commands/pebble.py +489 -0
  19. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/completion.py +16 -10
  20. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/context.py +9 -4
  21. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/history.py +6 -4
  22. borescope-1.0.0/src/borescope/shell/output.py +35 -0
  23. borescope-1.0.0/src/borescope/shell/parser.py +223 -0
  24. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/pathutils.py +7 -4
  25. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/repl.py +19 -20
  26. borescope-1.0.0/src/borescope/shell/sanitise.py +37 -0
  27. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/shell/theme.py +11 -7
  28. borescope-1.0.0/src/borescope/snapshot.py +102 -0
  29. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/transport/__init__.py +10 -13
  30. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/transport/cli_transport.py +9 -6
  31. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/transport/relay.py +8 -5
  32. borescope-1.0.0/src/borescope/transport/runner.py +241 -0
  33. {borescope-0.1.0.dev1 → borescope-1.0.0}/src/borescope/transport/socket_transport.py +4 -1
  34. borescope-1.0.0/tests/charms/bareshell-test/.gitignore +5 -0
  35. borescope-1.0.0/tests/charms/bareshell-test/Makefile +33 -0
  36. borescope-1.0.0/tests/charms/bareshell-test/README.md +42 -0
  37. borescope-1.0.0/tests/charms/bareshell-test/charmcraft.yaml +30 -0
  38. borescope-1.0.0/tests/charms/bareshell-test/requirements.txt +1 -0
  39. borescope-1.0.0/tests/charms/bareshell-test/src/charm.py +123 -0
  40. borescope-1.0.0/tests/charms/bareshell-test/workload-src/go.mod +5 -0
  41. borescope-1.0.0/tests/charms/bareshell-test/workload-src/main.go +70 -0
  42. borescope-1.0.0/tests/cloud-config.yaml +10 -0
  43. borescope-1.0.0/tests/conftest.py +317 -0
  44. borescope-1.0.0/tests/fuzz/README.md +77 -0
  45. borescope-1.0.0/tests/fuzz/corpus/mixed_quoting +1 -0
  46. borescope-1.0.0/tests/fuzz/corpus/pipe_command +1 -0
  47. borescope-1.0.0/tests/fuzz/corpus/pipe_with_flags +1 -0
  48. borescope-1.0.0/tests/fuzz/corpus/simple_command +1 -0
  49. borescope-1.0.0/tests/fuzz/corpus/single_quoted_arg +1 -0
  50. borescope-1.0.0/tests/fuzz/corpus/tilde_expansion +1 -0
  51. borescope-1.0.0/tests/fuzz/corpus/variable_expansion +1 -0
  52. borescope-1.0.0/tests/fuzz/corpus/whitespace_only +1 -0
  53. borescope-1.0.0/tests/fuzz/fuzz_parser.py +60 -0
  54. borescope-1.0.0/tests/integration/__init__.py +3 -0
  55. {borescope-0.1.0.dev1 → borescope-1.0.0}/tests/integration/conftest.py +13 -10
  56. {borescope-0.1.0.dev1 → borescope-1.0.0}/tests/integration/test_socket.py +31 -30
  57. borescope-1.0.0/tests/spread/cat-missing-file-errors/task.yaml +23 -0
  58. borescope-1.0.0/tests/spread/cat-multiple-files-concatenated/task.yaml +28 -0
  59. borescope-1.0.0/tests/spread/cat-no-args-reads-stdin/task.yaml +18 -0
  60. borescope-1.0.0/tests/spread/cat-single-file/task.yaml +25 -0
  61. borescope-1.0.0/tests/spread/cd-absolute-path/task.yaml +16 -0
  62. borescope-1.0.0/tests/spread/cd-into-file-errors/task.yaml +25 -0
  63. borescope-1.0.0/tests/spread/cd-no-args-goes-home/task.yaml +20 -0
  64. borescope-1.0.0/tests/spread/cd-relative-path/task.yaml +18 -0
  65. borescope-1.0.0/tests/spread/cp-file-to-existing-dir/task.yaml +25 -0
  66. borescope-1.0.0/tests/spread/cp-file-to-file/task.yaml +23 -0
  67. borescope-1.0.0/tests/spread/cp-missing-source-errors/task.yaml +28 -0
  68. borescope-1.0.0/tests/spread/double-quoted-tilde-is-literal/task.yaml +21 -0
  69. borescope-1.0.0/tests/spread/double-quotes-expand-var/task.yaml +23 -0
  70. borescope-1.0.0/tests/spread/echo-empty-string-arg-divergence/task.yaml +22 -0
  71. borescope-1.0.0/tests/spread/echo-multiple-args/task.yaml +24 -0
  72. borescope-1.0.0/tests/spread/echo-no-args-divergence/task.yaml +25 -0
  73. borescope-1.0.0/tests/spread/env-lists-tracked-variables/task.yaml +23 -0
  74. borescope-1.0.0/tests/spread/exit-default-zero/task.yaml +27 -0
  75. borescope-1.0.0/tests/spread/exit-non-numeric-arg-divergence/task.yaml +25 -0
  76. borescope-1.0.0/tests/spread/exit-with-code/task.yaml +21 -0
  77. borescope-1.0.0/tests/spread/expand-no-expansion-in-single-quotes/task.yaml +20 -0
  78. borescope-1.0.0/tests/spread/expand-tilde-alone/task.yaml +19 -0
  79. borescope-1.0.0/tests/spread/expand-tilde-not-at-word-start/task.yaml +21 -0
  80. borescope-1.0.0/tests/spread/expand-tilde-prefix/task.yaml +17 -0
  81. borescope-1.0.0/tests/spread/expand-unknown-var-empty/task.yaml +23 -0
  82. borescope-1.0.0/tests/spread/expand-var-braces/task.yaml +19 -0
  83. borescope-1.0.0/tests/spread/expand-var-dollar/task.yaml +19 -0
  84. borescope-1.0.0/tests/spread/find-by-name-pattern/task.yaml +29 -0
  85. borescope-1.0.0/tests/spread/find-by-type-directory/task.yaml +25 -0
  86. borescope-1.0.0/tests/spread/find-by-type-file/task.yaml +25 -0
  87. borescope-1.0.0/tests/spread/grep-basic-match-exit-0/task.yaml +25 -0
  88. borescope-1.0.0/tests/spread/grep-case-insensitive-i/task.yaml +22 -0
  89. borescope-1.0.0/tests/spread/grep-count-c/task.yaml +22 -0
  90. borescope-1.0.0/tests/spread/grep-invalid-pattern-divergence/task.yaml +34 -0
  91. borescope-1.0.0/tests/spread/grep-invert-v/task.yaml +24 -0
  92. borescope-1.0.0/tests/spread/grep-line-numbers-n/task.yaml +24 -0
  93. borescope-1.0.0/tests/spread/grep-multiple-files-prefixed/task.yaml +26 -0
  94. borescope-1.0.0/tests/spread/grep-no-match-exit-1/task.yaml +27 -0
  95. borescope-1.0.0/tests/spread/head-default-10-lines/task.yaml +28 -0
  96. borescope-1.0.0/tests/spread/head-missing-file-errors/task.yaml +21 -0
  97. borescope-1.0.0/tests/spread/head-n-flag/task.yaml +25 -0
  98. borescope-1.0.0/tests/spread/lib.sh +81 -0
  99. borescope-1.0.0/tests/spread/ls-a-shows-hidden/task.yaml +25 -0
  100. borescope-1.0.0/tests/spread/ls-default-no-hidden/task.yaml +30 -0
  101. borescope-1.0.0/tests/spread/ls-multiple-operands-headered/task.yaml +30 -0
  102. borescope-1.0.0/tests/spread/mixed-quoting-within-word/task.yaml +23 -0
  103. borescope-1.0.0/tests/spread/mkdir-create/task.yaml +22 -0
  104. borescope-1.0.0/tests/spread/mkdir-existing-errors/task.yaml +30 -0
  105. borescope-1.0.0/tests/spread/mkdir-p-creates-parents/task.yaml +23 -0
  106. borescope-1.0.0/tests/spread/mkdir-p-existing-ok/task.yaml +24 -0
  107. borescope-1.0.0/tests/spread/mv-into-existing-dir/task.yaml +27 -0
  108. borescope-1.0.0/tests/spread/mv-rename/task.yaml +25 -0
  109. borescope-1.0.0/tests/spread/pipe-empty-stage-rejected/task.yaml +23 -0
  110. borescope-1.0.0/tests/spread/pipe-quoted-bar-is-literal/task.yaml +19 -0
  111. borescope-1.0.0/tests/spread/pipe-two-stage/task.yaml +24 -0
  112. borescope-1.0.0/tests/spread/pwd-default-root/task.yaml +20 -0
  113. borescope-1.0.0/tests/spread/quoted-operator-literal-arg/task.yaml +22 -0
  114. borescope-1.0.0/tests/spread/rm-existing-file/task.yaml +23 -0
  115. borescope-1.0.0/tests/spread/rm-missing-with-f-ok/task.yaml +36 -0
  116. borescope-1.0.0/tests/spread/rm-missing-without-f-errors/task.yaml +23 -0
  117. borescope-1.0.0/tests/spread/rm-non-empty-dir-without-r-errors/task.yaml +34 -0
  118. borescope-1.0.0/tests/spread/rm-recursive/task.yaml +23 -0
  119. borescope-1.0.0/tests/spread/sequencing-semicolon-divergence/task.yaml +26 -0
  120. borescope-1.0.0/tests/spread/tail-default-10-lines/task.yaml +24 -0
  121. borescope-1.0.0/tests/spread/tail-n-flag/task.yaml +26 -0
  122. borescope-1.0.0/tests/spread/tokenize-double-quotes/task.yaml +19 -0
  123. borescope-1.0.0/tests/spread/touch-creates-new-file/task.yaml +25 -0
  124. borescope-1.0.0/tests/spread/touch-existing-updates-mtime-divergence/task.yaml +36 -0
  125. borescope-1.0.0/tests/spread/unbalanced-quote-is-error/task.yaml +25 -0
  126. borescope-1.0.0/tests/spread/unsupported-and-divergence/task.yaml +25 -0
  127. borescope-1.0.0/tests/spread/unsupported-multi-pipe-divergence/task.yaml +24 -0
  128. borescope-1.0.0/tests/spread/unsupported-or-divergence/task.yaml +22 -0
  129. borescope-1.0.0/tests/spread/unsupported-redirect-append-divergence/task.yaml +23 -0
  130. borescope-1.0.0/tests/spread/unsupported-redirect-in-divergence/task.yaml +22 -0
  131. borescope-1.0.0/tests/spread/unsupported-redirect-out-divergence/task.yaml +26 -0
  132. borescope-1.0.0/tests/test_args.py +60 -0
  133. borescope-1.0.0/tests/test_cli.py +102 -0
  134. borescope-1.0.0/tests/test_commands.py +251 -0
  135. borescope-1.0.0/tests/test_completion.py +53 -0
  136. borescope-1.0.0/tests/test_discovery.py +196 -0
  137. borescope-1.0.0/tests/test_juju.py +95 -0
  138. borescope-1.0.0/tests/test_parser.py +104 -0
  139. borescope-1.0.0/tests/test_pathutils.py +33 -0
  140. borescope-1.0.0/tests/test_pebble_commands.py +243 -0
  141. borescope-1.0.0/tests/test_registry.py +68 -0
  142. borescope-1.0.0/tests/test_relay.py +31 -0
  143. borescope-1.0.0/tests/test_repl.py +67 -0
  144. borescope-1.0.0/tests/test_runner.py +227 -0
  145. borescope-1.0.0/tests/test_sanitise.py +26 -0
  146. borescope-1.0.0/tests/test_snapshot.py +75 -0
  147. borescope-0.1.0.dev1/.gitignore +0 -27
  148. borescope-0.1.0.dev1/src/borescope/cli.py +0 -144
  149. borescope-0.1.0.dev1/src/borescope/shell/commands/basic.py +0 -117
  150. borescope-0.1.0.dev1/src/borescope/shell/commands/filesystem.py +0 -494
  151. borescope-0.1.0.dev1/src/borescope/shell/commands/pebble.py +0 -388
  152. borescope-0.1.0.dev1/src/borescope/shell/parser.py +0 -91
  153. borescope-0.1.0.dev1/src/borescope/snapshot.py +0 -103
  154. borescope-0.1.0.dev1/src/borescope/transport/runner.py +0 -149
  155. borescope-0.1.0.dev1/tests/conftest.py +0 -166
  156. borescope-0.1.0.dev1/tests/integration/__init__.py +0 -0
  157. borescope-0.1.0.dev1/tests/test_args.py +0 -46
  158. borescope-0.1.0.dev1/tests/test_commands.py +0 -152
  159. borescope-0.1.0.dev1/tests/test_completion.py +0 -53
  160. borescope-0.1.0.dev1/tests/test_discovery.py +0 -186
  161. borescope-0.1.0.dev1/tests/test_parser.py +0 -58
  162. borescope-0.1.0.dev1/tests/test_pathutils.py +0 -30
  163. borescope-0.1.0.dev1/tests/test_registry.py +0 -65
  164. borescope-0.1.0.dev1/tests/test_relay.py +0 -28
  165. borescope-0.1.0.dev1/tests/test_repl.py +0 -46
  166. borescope-0.1.0.dev1/tests/test_runner.py +0 -92
  167. {borescope-0.1.0.dev1 → borescope-1.0.0}/LICENSE +0 -0
@@ -0,0 +1,38 @@
1
+ __pycache__
2
+ /sandbox
3
+ .idea
4
+ *~
5
+ .venv
6
+ venv
7
+ .vscode
8
+ .coverage
9
+ coverage.xml
10
+ htmlcov
11
+ /.tox
12
+ .*.swp
13
+ .ruff_cache
14
+ .pytest_cache
15
+
16
+ # Tokens and settings for `act` to run GHA locally
17
+ .env
18
+ .envrc
19
+ .secrets
20
+
21
+ # Build artifacts
22
+ /cascade.egg-info
23
+ /dist
24
+ /build
25
+
26
+ # Agents
27
+ .claude/*local*
28
+
29
+ # Local docs-preview tooling writes this when viewing the page
30
+ /docs/localStorage.json
31
+
32
+ # Workshop tool's per-checkout runtime lock; not source.
33
+ /.workshop.lock
34
+
35
+ # libFuzzer-generated corpus entries (SHA1-named).
36
+ # The hand-written seeds use descriptive names and stay tracked; ignore the
37
+ # auto-discovered ones that accumulate during local fuzz runs.
38
+ tests/fuzz/corpus/[0-9a-f]*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: borescope
3
- Version: 0.1.0.dev1
3
+ Version: 1.0.0
4
4
  Summary: A natural shell for debugging Juju Kubernetes workload containers via Pebble
5
5
  Project-URL: Homepage, https://github.com/tonyandrewmeyer/borescope
6
6
  Project-URL: Repository, https://github.com/tonyandrewmeyer/borescope
@@ -9,7 +9,7 @@ Author-email: Tony Meyer <borescope@aotearoa.dev>
9
9
  License-Expression: Apache-2.0
10
10
  License-File: LICENSE
11
11
  Keywords: charm,debugging,juju,kubernetes,pebble,shell
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: System Administrators
@@ -24,7 +24,7 @@ Classifier: Topic :: System :: Systems Administration
24
24
  Classifier: Topic :: Utilities
25
25
  Requires-Python: >=3.11
26
26
  Requires-Dist: ops<4,>=2.0.0
27
- Requires-Dist: pebble-shimmer>=1.0.0b2
27
+ Requires-Dist: pebble-shimmer>=1.0.0
28
28
  Requires-Dist: prompt-toolkit>=3.0
29
29
  Requires-Dist: pyyaml>=6.0
30
30
  Description-Content-Type: text/markdown
@@ -70,6 +70,20 @@ borescope <unit> --command "services" # one-shot, no REPL (for scripts)
70
70
  borescope <unit> --snapshot # dump container state as JSON
71
71
  ```
72
72
 
73
+ ## Documentation
74
+
75
+ Full documentation — a tutorial, how-to guides, and CLI/command reference — is
76
+ at **<https://tonyandrewmeyer.github.io/borescope/>**.
77
+
78
+ The docs are plain Markdown under [`docs/src/`](docs/src/), built into static
79
+ HTML with a small script (no docs framework). To build them locally:
80
+
81
+ ```console
82
+ uv run python docs/src/_build.py # or: tox -e docs
83
+ ```
84
+
85
+ See [`docs/README.md`](docs/README.md) for the authoring rules.
86
+
73
87
  ## How it works
74
88
 
75
89
  borescope is three thin, independently-testable layers:
@@ -39,6 +39,20 @@ borescope <unit> --command "services" # one-shot, no REPL (for scripts)
39
39
  borescope <unit> --snapshot # dump container state as JSON
40
40
  ```
41
41
 
42
+ ## Documentation
43
+
44
+ Full documentation — a tutorial, how-to guides, and CLI/command reference — is
45
+ at **<https://tonyandrewmeyer.github.io/borescope/>**.
46
+
47
+ The docs are plain Markdown under [`docs/src/`](docs/src/), built into static
48
+ HTML with a small script (no docs framework). To build them locally:
49
+
50
+ ```console
51
+ uv run python docs/src/_build.py # or: tox -e docs
52
+ ```
53
+
54
+ See [`docs/README.md`](docs/README.md) for the authoring rules.
55
+
42
56
  ## How it works
43
57
 
44
58
  borescope is three thin, independently-testable layers:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "borescope"
3
- version = "0.1.0.dev1"
3
+ version = "1.0.0"
4
4
  description = "A natural shell for debugging Juju Kubernetes workload containers via Pebble"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -10,7 +10,7 @@ authors = [
10
10
  ]
11
11
  keywords = ["juju", "pebble", "kubernetes", "charm", "shell", "debugging"]
12
12
  classifiers = [
13
- "Development Status :: 3 - Alpha",
13
+ "Development Status :: 5 - Production/Stable",
14
14
  "Environment :: Console",
15
15
  "Intended Audience :: Developers",
16
16
  "Intended Audience :: System Administrators",
@@ -26,21 +26,27 @@ classifiers = [
26
26
  ]
27
27
  dependencies = [
28
28
  "ops>=2.0.0,<4",
29
- "pebble-shimmer>=1.0.0b2",
29
+ "pebble-shimmer>=1.0.0",
30
30
  "prompt_toolkit>=3.0",
31
31
  "PyYAML>=6.0",
32
32
  ]
33
33
 
34
34
  [dependency-groups]
35
35
  dev = [
36
+ "atheris>=2.3.0",
36
37
  "pytest>=7.0.0",
37
38
  "pytest-cov>=4.0.0",
38
39
  "pytest-mock>=3.10.0",
39
40
  "ruff==0.15.14",
40
41
  "ty==0.0.38",
41
- "tox>=4.22.0",
42
42
  "pre-commit>=3.0.0",
43
43
  ]
44
+ docs = [
45
+ "jinja2>=3.1",
46
+ "markdown-it-py>=3.0",
47
+ "mdit-py-plugins>=0.4",
48
+ "PyYAML>=6.0",
49
+ ]
44
50
 
45
51
  [project.scripts]
46
52
  borescope = "borescope.cli:main"
@@ -67,29 +73,110 @@ include = [
67
73
 
68
74
  # Ruff configuration
69
75
  [tool.ruff]
70
- line-length = 88
76
+ line-length = 99
71
77
  target-version = "py311"
72
78
  src = ["src", "tests"]
73
79
 
80
+ [tool.ruff.format]
81
+ quote-style = "single"
82
+
74
83
  [tool.ruff.lint]
84
+ # CPY (flake8-copyright) currently requires preview mode.
85
+ preview = true
86
+ explicit-preview-rules = true
75
87
  select = [
76
- "E", # pycodestyle errors
77
- "W", # pycodestyle warnings
78
- "F", # pyflakes
79
- "I", # isort
80
- "B", # flake8-bugbear
81
- "C4", # flake8-comprehensions
82
- "UP", # pyupgrade
83
- "N", # pep8-naming
88
+ # Pyflakes
89
+ "F",
90
+ # Pycodestyle
91
+ "E",
92
+ "W",
93
+ # isort
94
+ "I001",
95
+ # pep8-naming
96
+ "N",
97
+ # flake8-builtins
98
+ "A",
99
+ # flake8-copyright
100
+ "CPY001",
101
+ # pyupgrade
102
+ "UP",
103
+ # flake8-2020
104
+ "YTT",
105
+ # flake8-bandit
106
+ "S",
107
+ # flake8-bugbear
108
+ "B",
109
+ # flake8-simplify
110
+ "SIM",
111
+ # Ruff-specific
112
+ "RUF",
113
+ # Perflint
114
+ "PERF",
115
+ # pydocstyle
116
+ "D",
117
+ # flake8-future-annotations
118
+ "FA",
119
+ # flake8-type-checking
120
+ "TC",
84
121
  ]
85
122
  ignore = [
86
- "E501", # line too long, handled by formatter
87
- "B008", # do not perform function calls in argument defaults
88
- "C901", # too complex
123
+ # Line too long: handled by the formatter.
124
+ "E501",
125
+ # Don't force imports into TYPE_CHECKING blocks.
126
+ "TC001",
127
+ "TC002",
128
+ "TC003",
129
+ # assert is fine.
130
+ "S101",
131
+ # Magic methods and __init__ don't need docstrings.
132
+ "D105",
133
+ "D107",
134
+ # subprocess call: check for execution of untrusted input. False-positive heavy.
135
+ "S603",
89
136
  ]
90
137
 
138
+ [tool.ruff.lint.pydocstyle]
139
+ convention = "google"
140
+
141
+ [tool.ruff.lint.flake8-builtins]
142
+ builtins-ignorelist = ["id", "min", "map", "range", "type", "input", "format", "exit", "help"]
143
+
144
+ [tool.ruff.lint.flake8-copyright]
145
+ notice-rgx = "(?i)Copyright \\d{4} Tony Meyer"
146
+
91
147
  [tool.ruff.lint.per-file-ignores]
92
- "tests/*" = ["N802", "N803", "N806"]
148
+ # Command classes are self-documenting via their ``name``/``summary``/``usage``
149
+ # attributes; a docstring would only restate ``summary``.
150
+ "src/borescope/shell/commands/*.py" = [
151
+ "D101",
152
+ "D102",
153
+ ]
154
+ # Transport is a structural ``Protocol`` mirroring ``ops.pebble.Client``; the
155
+ # upstream API is the docstring.
156
+ "src/borescope/transport/__init__.py" = [
157
+ "D102",
158
+ ]
159
+ "tests/*" = [
160
+ # Docstrings aren't required in tests.
161
+ "D",
162
+ # Hard-coded "secrets" are fine in tests.
163
+ "S105",
164
+ "S106",
165
+ ]
166
+ "tests/**/*" = [
167
+ "D",
168
+ "S105",
169
+ "S106",
170
+ ]
171
+ # The docs build script intentionally keeps two things ruff would flag:
172
+ "docs/src/_build.py" = [
173
+ # The entity-rewrite table maps literal "ambiguous" Unicode characters
174
+ # (en dash, curly quotes, ...) to HTML entities — they must stay as-is.
175
+ "RUF001",
176
+ # Jinja2 autoescape is deliberately off: the body is trusted, already-
177
+ # rendered HTML, and escaping it would double-encode the docs.
178
+ "S701",
179
+ ]
93
180
 
94
181
  [tool.ruff.lint.isort]
95
182
  known-first-party = ["borescope"]
@@ -1,10 +1,13 @@
1
+ # Copyright 2026 Tony Meyer
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  """borescope - a natural shell for debugging Juju Kubernetes workload containers."""
2
5
 
3
6
  from importlib.metadata import PackageNotFoundError, version
4
7
 
5
8
  try:
6
- __version__ = version("borescope")
9
+ __version__ = version('borescope')
7
10
  except PackageNotFoundError: # pragma: no cover - running from an uninstalled tree
8
- __version__ = "0.0.0+unknown"
11
+ __version__ = '0.0.0+unknown'
9
12
 
10
- __all__ = ["__version__"]
13
+ __all__ = ['__version__']
@@ -1,3 +1,6 @@
1
+ # Copyright 2026 Tony Meyer
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
1
4
  """``python -m borescope`` entry point."""
2
5
 
3
6
  from __future__ import annotations
@@ -6,5 +9,5 @@ import sys
6
9
 
7
10
  from .cli import main
8
11
 
9
- if __name__ == "__main__":
12
+ if __name__ == '__main__':
10
13
  sys.exit(main())
@@ -0,0 +1,164 @@
1
+ # Copyright 2026 Tony Meyer
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Command-line entry point for borescope.
5
+
6
+ Deliberately *out* of scope for v1, and worth marking explicitly: borescope has
7
+ no Canonical-spec verbosity ladder (`--quiet` / `--verbose` /
8
+ `--verbosity=debug|trace`). The tool's primary output is the REPL or a single
9
+ command's result, neither of which benefits from a five-level taxonomy, so we
10
+ stay minimal until a concrete need shows up.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+
18
+ from . import __version__
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ """Build the top-level ``argparse`` parser."""
23
+ parser = argparse.ArgumentParser(
24
+ prog='borescope',
25
+ description=('A natural shell for debugging Juju Kubernetes workload containers.'),
26
+ )
27
+ parser.add_argument('unit', nargs='?', help="unit reference, for example 'myapp/0'")
28
+ parser.add_argument('--container', help='workload container name (default: first declared)')
29
+ # Canonical CLI guidance is "don't offer both short and long" for the same
30
+ # flag — `-m/--model` deliberately diverges to match `juju`'s own convention
31
+ # (`juju ssh -m <model> …`), which is what users muscle-memory their way to
32
+ # when reaching for borescope.
33
+ parser.add_argument('-m', '--model', help='Juju model (default: current)')
34
+ parser.add_argument(
35
+ '--command',
36
+ help='run a single command and exit (no REPL)',
37
+ )
38
+ parser.add_argument(
39
+ '--snapshot',
40
+ action='store_true',
41
+ help='dump container state as JSON and exit',
42
+ )
43
+ parser.add_argument(
44
+ '--socket',
45
+ help='talk directly to a Pebble unix socket (skip juju)',
46
+ )
47
+ parser.add_argument(
48
+ '--here',
49
+ action='store_true',
50
+ help=(
51
+ "run inside the charm container: auto-detect a workload's mounted "
52
+ 'Pebble socket (use --container to pick when there are several)'
53
+ ),
54
+ )
55
+ parser.add_argument('--juju', default='juju', help='juju binary to invoke (default: juju)')
56
+ parser.add_argument(
57
+ '--via',
58
+ choices=('ssh', 'exec'),
59
+ default='ssh',
60
+ help=(
61
+ "Juju relay for Mode B: 'ssh' (default, streaming) or 'exec' "
62
+ '(request/response — for sites where ssh is disabled)'
63
+ ),
64
+ )
65
+ parser.add_argument('--version', action='version', version=f'borescope {__version__}')
66
+ return parser
67
+
68
+
69
+ def _build_target(args: argparse.Namespace):
70
+ from .discovery import Target, resolve_local_target, resolve_target, validate_container_name
71
+
72
+ if args.container is not None:
73
+ validate_container_name(args.container)
74
+ if args.here:
75
+ return resolve_local_target(container=args.container)
76
+ if args.socket:
77
+ unit = args.unit or 'local'
78
+ app = unit.split('/')[0]
79
+ return Target(
80
+ unit=unit,
81
+ app=app,
82
+ container=args.container,
83
+ model=args.model,
84
+ juju_binary=args.juju,
85
+ socket_path=args.socket,
86
+ )
87
+ return resolve_target(
88
+ args.unit,
89
+ container=args.container,
90
+ model=args.model,
91
+ juju_binary=args.juju,
92
+ via=args.via,
93
+ )
94
+
95
+
96
+ def main(argv: list[str] | None = None) -> int:
97
+ """Run borescope from the command line and return the exit code."""
98
+ # Accept Canonical-style `help` / `version` subcommands as aliases for the
99
+ # Python-default `--help` / `--version`. Awkward to support both, but it
100
+ # means the tool matches the standard *and* what `argparse` users expect.
101
+ cli_args = sys.argv[1:] if argv is None else argv
102
+ if cli_args[:1] == ['help']:
103
+ build_parser().print_help()
104
+ return 0
105
+ if cli_args[:1] == ['version']:
106
+ print(f'borescope {__version__}')
107
+ return 0
108
+
109
+ args = build_parser().parse_args(argv)
110
+ if not args.unit and not args.socket and not args.here:
111
+ print(
112
+ "borescope: A unit reference is required (for example 'borescope myapp/0'), "
113
+ 'or use --here when running inside a charm container.',
114
+ file=sys.stderr,
115
+ )
116
+ return 2
117
+
118
+ # Heavy imports happen only past argument parsing, keeping --help/--version fast.
119
+ from .errors import BorescopeError
120
+ from .shell import ShellContext
121
+ from .transport import open_transport
122
+
123
+ try:
124
+ target = _build_target(args)
125
+ transport = open_transport(
126
+ unit=target.unit,
127
+ container=target.container,
128
+ model=target.model,
129
+ juju_binary=target.juju_binary,
130
+ socket_path=target.socket_path,
131
+ via=target.via,
132
+ )
133
+ if args.snapshot:
134
+ from .snapshot import snapshot_json
135
+
136
+ print(snapshot_json(transport, target))
137
+ return 0
138
+
139
+ from .discovery import sanity_check
140
+
141
+ sanity_check(transport, target)
142
+ except BorescopeError as exc:
143
+ print(f'borescope: {exc}', file=sys.stderr)
144
+ return 1
145
+
146
+ from .shell import Shell
147
+
148
+ shell = Shell(ShellContext(transport=transport, target=target))
149
+
150
+ if args.command is not None:
151
+ return shell.execute_and_emit(args.command)
152
+
153
+ if not sys.stdin.isatty():
154
+ code = 0
155
+ for line in sys.stdin:
156
+ if line.strip():
157
+ code = shell.execute_and_emit(line.rstrip('\n'))
158
+ return code
159
+
160
+ return shell.loop()
161
+
162
+
163
+ if __name__ == '__main__': # pragma: no cover
164
+ sys.exit(main())