ruyi 0.41.0a20250911__tar.gz → 0.41.0b20250924__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 (102) hide show
  1. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/PKG-INFO +5 -4
  2. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/README.md +2 -2
  3. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/pyproject.toml +1 -1
  4. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/cmd.py +3 -1
  5. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/completion.py +3 -1
  6. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/main.py +6 -1
  7. ruyi-0.41.0b20250924/ruyi/cli/oobe.py +79 -0
  8. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/device/provision.py +26 -5
  9. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/news.py +19 -0
  10. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/news_cli.py +15 -1
  11. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/repo.py +7 -2
  12. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/update_cli.py +1 -6
  13. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/provider.py +4 -14
  14. ruyi-0.41.0b20250924/ruyi/utils/mounts.py +45 -0
  15. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/LICENSE-Apache.txt +0 -0
  16. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/__init__.py +0 -0
  17. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/__main__.py +0 -0
  18. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/__init__.py +0 -0
  19. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/builtin_commands.py +0 -0
  20. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/completer.py +0 -0
  21. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/config_cli.py +0 -0
  22. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/self_cli.py +0 -0
  23. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/user_input.py +0 -0
  24. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/cli/version_cli.py +0 -0
  25. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/config/__init__.py +0 -0
  26. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/config/editor.py +0 -0
  27. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/config/errors.py +0 -0
  28. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/config/news.py +0 -0
  29. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/config/schema.py +0 -0
  30. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/device/__init__.py +0 -0
  31. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/device/provision_cli.py +0 -0
  32. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/log/__init__.py +0 -0
  33. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/.gitignore +0 -0
  34. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/__init__.py +0 -0
  35. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/runtime.py +0 -0
  36. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/venv/__init__.py +0 -0
  37. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/venv/emulator_cfg.py +0 -0
  38. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/venv/maker.py +0 -0
  39. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/venv/venv_cli.py +0 -0
  40. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/mux/venv_cfg.py +0 -0
  41. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/pluginhost/__init__.py +0 -0
  42. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/pluginhost/api.py +0 -0
  43. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/pluginhost/ctx.py +0 -0
  44. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/pluginhost/paths.py +0 -0
  45. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/pluginhost/plugin_cli.py +0 -0
  46. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/pluginhost/unsandboxed.py +0 -0
  47. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/py.typed +0 -0
  48. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/resource_bundle/__init__.py +0 -0
  49. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/resource_bundle/__main__.py +0 -0
  50. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/resource_bundle/data.py +0 -0
  51. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/__init__.py +0 -0
  52. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/admin_checksum.py +0 -0
  53. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/admin_cli.py +0 -0
  54. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/atom.py +0 -0
  55. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/augmented_pkg.py +0 -0
  56. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/canonical_dump.py +0 -0
  57. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/checksum.py +0 -0
  58. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/cli_completion.py +0 -0
  59. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/distfile.py +0 -0
  60. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/entity.py +0 -0
  61. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/entity_cli.py +0 -0
  62. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/entity_provider.py +0 -0
  63. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/fetch.py +0 -0
  64. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/host.py +0 -0
  65. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/install.py +0 -0
  66. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/install_cli.py +0 -0
  67. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/list.py +0 -0
  68. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/list_cli.py +0 -0
  69. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/list_filter.py +0 -0
  70. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/msg.py +0 -0
  71. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/news_store.py +0 -0
  72. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/pkg_manifest.py +0 -0
  73. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/profile.py +0 -0
  74. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/profile_cli.py +0 -0
  75. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/protocols.py +0 -0
  76. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/state.py +0 -0
  77. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/unpack.py +0 -0
  78. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/ruyipkg/unpack_method.py +0 -0
  79. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/__init__.py +0 -0
  80. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/aggregate.py +0 -0
  81. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/event.py +0 -0
  82. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/node_info.py +0 -0
  83. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/scope.py +0 -0
  84. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/store.py +0 -0
  85. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/telemetry/telemetry_cli.py +0 -0
  86. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/__init__.py +0 -0
  87. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/ar.py +0 -0
  88. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/ci.py +0 -0
  89. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/frontmatter.py +0 -0
  90. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/git.py +0 -0
  91. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/global_mode.py +0 -0
  92. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/l10n.py +0 -0
  93. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/markdown.py +0 -0
  94. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/nuitka.py +0 -0
  95. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/porcelain.py +0 -0
  96. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/prereqs.py +0 -0
  97. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/ssl_patch.py +0 -0
  98. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/templating.py +0 -0
  99. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/toml.py +0 -0
  100. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/url.py +0 -0
  101. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/utils/xdg_basedir.py +0 -0
  102. {ruyi-0.41.0a20250911 → ruyi-0.41.0b20250924}/ruyi/version.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: ruyi
3
- Version: 0.41.0a20250911
3
+ Version: 0.41.0b20250924
4
4
  Summary: Package manager for RuyiSDK
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -203,6 +203,7 @@ License: Apache License
203
203
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
204
204
  See the License for the specific language governing permissions and
205
205
  limitations under the License.
206
+ License-File: LICENSE-Apache.txt
206
207
  Keywords: ruyi,ruyisdk
207
208
  Author: WANG Xuerui
208
209
  Author-email: wangxuerui@iscas.ac.cn
@@ -271,6 +272,8 @@ PyPI installation, but the one-file distribution is a bit easier to set up
271
272
  because one doesn't have to first configure a Python environment. Either way,
272
273
  the feature set should be the same.
273
274
 
275
+ Detailed installation instructions are also available [at our documentation site](https://ruyisdk.org/en/docs/Package-Manager/installation).
276
+
274
277
  ### ✅ Recommended: Install from PyPI
275
278
 
276
279
  This is the recommended way to install `ruyi` on your machine. In any Python
@@ -431,8 +434,6 @@ We collect the following information with `ruyi`:
431
434
  * without exposing any parameters
432
435
  * invocation time is recorded with a granularity of 1 minute
433
436
 
434
- You can see our Privacy Policy on the RuyiSDK website.
435
-
436
437
  You can see [our Privacy Policy][privacy-policy-en] ([中文][privacy-policy-zh])
437
438
  on the RuyiSDK website.
438
439
 
@@ -28,6 +28,8 @@ PyPI installation, but the one-file distribution is a bit easier to set up
28
28
  because one doesn't have to first configure a Python environment. Either way,
29
29
  the feature set should be the same.
30
30
 
31
+ Detailed installation instructions are also available [at our documentation site](https://ruyisdk.org/en/docs/Package-Manager/installation).
32
+
31
33
  ### ✅ Recommended: Install from PyPI
32
34
 
33
35
  This is the recommended way to install `ruyi` on your machine. In any Python
@@ -188,8 +190,6 @@ We collect the following information with `ruyi`:
188
190
  * without exposing any parameters
189
191
  * invocation time is recorded with a granularity of 1 minute
190
192
 
191
- You can see our Privacy Policy on the RuyiSDK website.
192
-
193
193
  You can see [our Privacy Policy][privacy-policy-en] ([中文][privacy-policy-zh])
194
194
  on the RuyiSDK website.
195
195
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "ruyi"
7
- version = "0.41.0-alpha.20250911"
7
+ version = "0.41.0-beta.20250924"
8
8
  description = "Package manager for RuyiSDK"
9
9
  keywords = ["ruyi", "ruyisdk"]
10
10
  license = { file = "LICENSE-Apache.txt" }
@@ -198,7 +198,9 @@ class RootCommand(
198
198
  return 0
199
199
  # the rest are implementation of "--output-completion-script"
200
200
 
201
- if sh not in {"bash", "zsh"}:
201
+ from .completion import SUPPORTED_SHELLS
202
+
203
+ if sh not in SUPPORTED_SHELLS:
202
204
  raise ValueError(f"Unsupported shell: {sh}")
203
205
 
204
206
  import sys
@@ -5,7 +5,9 @@ see https://github.com/kislyuk/argcomplete/issues/443 for why this is needed
5
5
  """
6
6
 
7
7
  import argparse
8
- from typing import Any, Callable, Optional, Sequence, cast
8
+ from typing import Any, Callable, Final, Optional, Sequence, cast
9
+
10
+ SUPPORTED_SHELLS: Final[set[str]] = {"bash", "zsh"}
9
11
 
10
12
 
11
13
  class ArgcompleteAction(argparse.Action):
@@ -7,6 +7,7 @@ from ..config import GlobalConfig
7
7
  from ..telemetry.scope import TelemetryScope
8
8
  from ..utils.global_mode import GlobalModeProvider
9
9
  from . import RUYI_ENTRYPOINT_NAME
10
+ from .oobe import OOBE
10
11
 
11
12
  ALLOWED_RUYI_ENTRYPOINT_NAMES: Final = (
12
13
  RUYI_ENTRYPOINT_NAME,
@@ -21,11 +22,15 @@ def is_called_as_ruyi(argv0: str) -> bool:
21
22
 
22
23
 
23
24
  def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
25
+ oobe = OOBE(gc)
26
+
24
27
  if tm := gc.telemetry:
25
28
  tm.check_first_run_status()
26
29
  tm.init_installation(False)
27
30
  atexit.register(tm.flush)
28
- tm.maybe_prompt_for_first_run_upload()
31
+ oobe.handlers.append(tm.oobe_prompt)
32
+
33
+ oobe.maybe_prompt()
29
34
 
30
35
  if not is_called_as_ruyi(gm.argv0):
31
36
  from ..mux.runtime import mux_main
@@ -0,0 +1,79 @@
1
+ """First-run (Out-of-the-box) experience for ``ruyi``."""
2
+
3
+ import os
4
+ import sys
5
+ from typing import Callable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from ..config import GlobalConfig
9
+
10
+
11
+ SHELL_AUTO_COMPLETION_TIP = """
12
+ [bold green]tip[/]: you can enable shell auto-completion for [yellow]ruyi[/] by adding the
13
+ following line to your [green]{shrc}[/], if you have not done so already:
14
+
15
+ [green]eval "$(ruyi --output-completion-script={shell})"[/]
16
+
17
+ You can do so by running the following command later:
18
+
19
+ [green]echo 'eval "$(ruyi --output-completion-script={shell})"' >> {shrc}[/]
20
+ """
21
+
22
+
23
+ class OOBE:
24
+ """Out-of-the-box experience (OOBE) handler for RuyiSDK CLI."""
25
+
26
+ def __init__(self, gc: "GlobalConfig") -> None:
27
+ self._gc = gc
28
+ self.handlers: list[Callable[[], None]] = [
29
+ self._builtin_shell_completion_tip,
30
+ ]
31
+
32
+ def is_first_run(self) -> bool:
33
+ if tm := self._gc.telemetry:
34
+ return tm.is_first_run
35
+ # cannot reliably determine first run status without telemetry
36
+ # we may revisit this later if it turns out users want OOBE tips even
37
+ # if they know how to disable telemetry (hence more likely to be power
38
+ # users)
39
+ return False
40
+
41
+ def should_prompt(self) -> bool:
42
+ from ..utils.global_mode import is_env_var_truthy
43
+
44
+ if is_env_var_truthy(os.environ, "RUYI_DEBUG_FORCE_FIRST_RUN"):
45
+ return True
46
+
47
+ return self.is_first_run() and sys.stdin.isatty()
48
+
49
+ def maybe_prompt(self) -> None:
50
+ if not self.should_prompt():
51
+ return
52
+
53
+ logger = self._gc.logger
54
+ logger.I(
55
+ "Welcome to RuyiSDK! This appears to be your first run of [yellow]ruyi[/].",
56
+ )
57
+
58
+ for handler in self.handlers:
59
+ handler()
60
+
61
+ def _builtin_shell_completion_tip(self) -> None:
62
+ from ..telemetry.node_info import probe_for_shell
63
+ from .completion import SUPPORTED_SHELLS
64
+
65
+ # Only show the tip if we're not externally managed by a package manager,
66
+ # because we expect proper shell integration to be done by distro packagers
67
+ if self._gc.is_installation_externally_managed:
68
+ return
69
+
70
+ shell = probe_for_shell(os.environ)
71
+ if shell not in SUPPORTED_SHELLS:
72
+ return
73
+
74
+ self._gc.logger.stdout(
75
+ SHELL_AUTO_COMPLETION_TIP.format(
76
+ shell=shell,
77
+ shrc=f"~/.{shell}rc",
78
+ )
79
+ )
@@ -15,7 +15,7 @@ from ..ruyipkg.pkg_manifest import (
15
15
  PartitionMapDecl,
16
16
  )
17
17
  from ..ruyipkg.repo import MetadataRepo
18
- from ..utils import prereqs
18
+ from ..utils import mounts, prereqs
19
19
 
20
20
  if TYPE_CHECKING:
21
21
  from ..ruyipkg.pkg_manifest import BoundPackageManifest
@@ -209,10 +209,31 @@ information you will need later.
209
209
  )
210
210
  for part in requested_host_blkdevs:
211
211
  part_desc = get_part_desc(part)
212
- path = user_input.ask_for_file(
213
- logger,
214
- f"Please give the path for the {part_desc}:",
215
- )
212
+
213
+ while True:
214
+ path = user_input.ask_for_file(
215
+ logger,
216
+ f"Please give the path for the {part_desc}:",
217
+ )
218
+
219
+ # Retrieve the latest mount info in case the user un-mounts
220
+ # on seeing the prompt
221
+ all_mounts = mounts.parse_mounts()
222
+ blkdev_mounts = [m for m in all_mounts if m.source_is_blkdev]
223
+ path_valid = True
224
+ for m in blkdev_mounts:
225
+ if m.source_path.samefile(path):
226
+ logger.W(
227
+ f"path [cyan]'{path}'[/] is currently mounted at [yellow]'{m.target}'[/]"
228
+ )
229
+ logger.I(
230
+ "rejecting the path for safety; please double-check and retry"
231
+ )
232
+ path_valid = False
233
+ break
234
+ if path_valid:
235
+ break
236
+
216
237
  host_blkdev_map[part] = path
217
238
 
218
239
  # final confirmation
@@ -32,6 +32,25 @@ def print_news_item_titles(
32
32
  logger.stdout(tbl)
33
33
 
34
34
 
35
+ def maybe_notify_unread_news(
36
+ gc: GlobalConfig,
37
+ prompt_no_unread: bool = True,
38
+ ) -> None:
39
+ """Check if there are new newsitems, notify the user if so."""
40
+
41
+ unread_newsitems = gc.repo.news_store().list(True)
42
+ if unread_newsitems:
43
+ gc.logger.stdout(f"\nThere are {len(unread_newsitems)} new news item(s):\n")
44
+ print_news_item_titles(gc.logger, unread_newsitems, gc.lang_code)
45
+ gc.logger.stdout("\nYou can read them with [yellow]ruyi news read[/].")
46
+ return
47
+
48
+ if prompt_no_unread:
49
+ gc.logger.stdout(
50
+ "\nAll news items have been read. To see a list of them, run [yellow]ruyi news list[/].\n"
51
+ )
52
+
53
+
35
54
  def do_news_list(
36
55
  cfg: GlobalConfig,
37
56
  only_unread: bool,
@@ -12,11 +12,25 @@ class NewsCommand(
12
12
  RootCommand,
13
13
  cmd="news",
14
14
  has_subcommands=True,
15
+ is_subcommand_required=False,
16
+ has_main=True,
15
17
  help="List and read news items from configured repository",
16
18
  ):
19
+ _my_parser: "ArgumentParser | None" = None
20
+
17
21
  @classmethod
18
22
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
19
- pass
23
+ cls._my_parser = p
24
+
25
+ @classmethod
26
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
27
+ from .news import maybe_notify_unread_news
28
+
29
+ assert cls._my_parser is not None
30
+ cls._my_parser.print_help()
31
+ maybe_notify_unread_news(cfg, True)
32
+
33
+ return 0
20
34
 
21
35
 
22
36
  class NewsListCommand(
@@ -284,7 +284,8 @@ class MetadataRepo(ProvidesPackageManifests):
284
284
  self.repo = Repository(self.root)
285
285
  return self.repo
286
286
 
287
- self.logger.D(f"{self.root} does not exist, cloning from {self.remote}")
287
+ self.logger.I(f"the package repository does not exist at [yellow]{self.root}[/]")
288
+ self.logger.I(f"cloning from [cyan link={self.remote}]{self.remote}[/]")
288
289
 
289
290
  with RemoteGitProgressIndicator() as pr:
290
291
  repo = clone_repository(
@@ -306,13 +307,15 @@ class MetadataRepo(ProvidesPackageManifests):
306
307
  return self.repo
307
308
 
308
309
  def sync(self) -> None:
310
+ self._gc.logger.I("updating the package repository")
311
+
309
312
  repo = self.ensure_git_repo()
310
313
 
311
314
  # only manage the repo settings on the user's behalf if the user
312
315
  # has not overridden the repo directory themselves
313
316
  allow_auto_management = self._gc.override_repo_dir is None
314
317
 
315
- return pull_ff_or_die(
318
+ pull_ff_or_die(
316
319
  self.logger,
317
320
  repo,
318
321
  "origin",
@@ -321,6 +324,8 @@ class MetadataRepo(ProvidesPackageManifests):
321
324
  allow_auto_management=allow_auto_management,
322
325
  )
323
326
 
327
+ self._gc.logger.I("package repository is updated")
328
+
324
329
  @property
325
330
  def global_config(self) -> "GlobalConfig":
326
331
  return self._gc
@@ -44,11 +44,6 @@ Re-run [yellow]ruyi install[/] to upgrade, and don't forget to re-create any aff
44
44
  virtual environments."""
45
45
  )
46
46
 
47
- # check if there are new newsitems
48
- unread_newsitems = mr.news_store().list(True)
49
- if unread_newsitems:
50
- logger.stdout(f"\nThere are {len(unread_newsitems)} new news item(s):\n")
51
- news.print_news_item_titles(logger, unread_newsitems, cfg.lang_code)
52
- logger.stdout("\nYou can read them with [yellow]ruyi news read[/].")
47
+ news.maybe_notify_unread_news(cfg, False)
53
48
 
54
49
  return 0
@@ -3,7 +3,6 @@ import datetime
3
3
  import functools
4
4
  import json
5
5
  import pathlib
6
- import sys
7
6
  import time
8
7
  from typing import Callable, TYPE_CHECKING, cast
9
8
  import uuid
@@ -19,9 +18,7 @@ if TYPE_CHECKING:
19
18
 
20
19
  FALLBACK_PM_TELEMETRY_ENDPOINT = "https://api.ruyisdk.cn/telemetry/pm/"
21
20
 
22
- FIRST_RUN_PROMPT = """\
23
- Welcome to RuyiSDK! This appears to be your first run of [yellow]ruyi[/].
24
-
21
+ TELEMETRY_INITIAL_UPLOAD_PROMPT = """
25
22
  By default, the RuyiSDK team collects anonymous usage data to help us improve
26
23
  the product. No personal information or detail about your project is ever
27
24
  collected. The data will be uploaded to RuyiSDK team-managed servers located
@@ -388,19 +385,12 @@ class TelemetryProvider:
388
385
 
389
386
  return store.upload_staged_payloads()
390
387
 
391
- def maybe_prompt_for_first_run_upload(self) -> None:
392
- """
393
- Ask whether the user consents to a first-run telemetry upload when
394
- running for the first time (OOBE) and with an interactive stdin.
395
- """
396
-
397
- # Only prompt if this is first run and stdin is a TTY
398
- if not (self.is_first_run and sys.stdin.isatty()):
399
- return
388
+ def oobe_prompt(self) -> None:
389
+ """Ask whether the user consents to a first-run telemetry upload."""
400
390
 
401
391
  from ..cli import user_input
402
392
 
403
- self.logger.I(FIRST_RUN_PROMPT)
393
+ self.logger.stdout(TELEMETRY_INITIAL_UPLOAD_PROMPT)
404
394
  if not user_input.ask_for_yesno_confirmation(
405
395
  self.logger,
406
396
  "Do you agree?",
@@ -0,0 +1,45 @@
1
+ """Utilities for parsing mount information from /proc/self/mounts."""
2
+
3
+ import pathlib
4
+ import re
5
+ from typing import NamedTuple
6
+
7
+
8
+ class MountInfo(NamedTuple):
9
+ source: str
10
+ target: str
11
+ fstype: str
12
+ options: list[str]
13
+
14
+ @property
15
+ def source_path(self) -> pathlib.Path:
16
+ return pathlib.Path(self.source)
17
+
18
+ @property
19
+ def source_is_blkdev(self) -> bool:
20
+ return self.source_path.is_block_device()
21
+
22
+
23
+ def parse_mounts(contents: str | None = None) -> list[MountInfo]:
24
+ if contents is None:
25
+ try:
26
+ with open("/proc/self/mounts", "r", encoding="utf-8") as f:
27
+ contents = f.read()
28
+ except OSError:
29
+ return []
30
+
31
+ mounts: list[MountInfo] = []
32
+ for line in contents.splitlines():
33
+ parts = line.split()
34
+ if len(parts) < 4:
35
+ continue
36
+ source, target, fstype, opts = parts[:4]
37
+ options = opts.split(",")
38
+ source = _unescape_octals(source)
39
+ target = _unescape_octals(target)
40
+ mounts.append(MountInfo(source, target, fstype, options))
41
+ return mounts
42
+
43
+
44
+ def _unescape_octals(s: str) -> str:
45
+ return re.sub(r"\\([0-3][0-7]{2})", lambda m: chr(int(m.group(1), 8)), s)