Skip to content

Conversation

@rmitchellscott
Copy link
Contributor

@rmitchellscott rmitchellscott commented Dec 7, 2025

This PR fixes a couple bugs/regressions related to my recent PRs:

  • error with detecting the backup partition when running on-device
  • error with CPIO import when running on-device
  • firmware downgrades across the 3.20/3.22 boundary
    • moved from swupdate-from-image-file to sourcing the args file and running swupdate since swupdate-from-image-file contains the "old A/B downgrade" check. This still enables a single run of swupdate as sourcing the args file correctly identifies the backup partition.

I've conducted the following tests to ensure correct behavior:

rMPP
codexctl install 3.24.0.149
codexctl install 3.20.0.92 (cross-3.20 boundary)

rM2 (SSH)
codexctl status
codexctl download 3.22.4.2
codexctl install ~/Downloads/remarkable-production-memfault-image-3.22.4.2-rm2-public
codexctl install 3.24.0.149
codexctl restore

rM2 (on-device)
codexctl status
codexctl download 3.22.4.2
codexctl install ~/Downloads/remarkable-production-memfault-image-3.22.4.2-rm2-public
codexctl install 3.24.0.149
codexctl restore

Summary by CodeRabbit

  • Refactor
    • Improved device management to transparently support both local and remote access.
    • Unified file-read and existence checks for local/SSH contexts.
    • Added helpers to detect active/inactive partitions and determine next-boot partition; safer mount/read/cleanup flows.
    • Swupdate invocation hardened with safer argument quoting and consistent execution.
  • Bug Fixes
    • Corrected update-image import reference to use the consolidated image module.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

Walkthrough

Moves CPIOUpdateImage import to remarkable_update_image.image and refactors DeviceManager to unify local and SSH file access, add active-device and partition-parsing helpers, and adjust partition/version/read/mount and swupdate invocation paths.

Changes

Cohort / File(s) Summary
Import change
codexctl/__init__.py
Replaced CPIOUpdateImage import from remarkable_update_image.cpio to remarkable_update_image.image.
Device manager refactor
codexctl/device.py
Large refactor to support both SSH and local contexts: added _get_active_device() and _parse_partition_info(); changed _read_version_from_path(self, ftp=None, base_path=""); introduced unified read_file/file_exists abstractions; updated partition mount/read/cleanup flows and Paper Pro handling; changed swupdate invocation to run via bash -c wrapper with shlex.quote; strengthened error handling and ensured cleanup.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant CLI as codexctl (client)
participant SSH as Remote device (SSH/SFTP)
participant LOCAL as Local filesystem
participant SWU as swupdate (on device)
Note over CLI,SSH,LOCAL: Determine active partition & read version
CLI->>SSH: run swupdate -g or rootdev (if remote)
par remote vs local
SSH-->>CLI: active device path
and
CLI->>LOCAL: run swupdate -g or rootdev (if local)
LOCAL-->>CLI: active device path
end
CLI->>CLI: _parse_partition_info(active_device)
Note over CLI,SSH,LOCAL: Mount inactive partition and read version file
alt remote
CLI->>SSH: mount inactive partition (remote mount)
SSH-->>CLI: mount success
CLI->>SSH: read version file via SFTP (read_file)
SSH-->>CLI: file contents
CLI->>SSH: unmount
else local
CLI->>LOCAL: mount inactive partition (local)
LOCAL-->>CLI: mount success
CLI->>LOCAL: read version file (read_file)
LOCAL-->>CLI: file contents
CLI->>LOCAL: unmount
end
Note over CLI,SWU: Install update
CLI->>SSH: ssh [bash -c "source /usr/lib/swupdate/conf.d/09-swupdate-args; swupdate ..."]
SSH->>SWU: swupdate invoked
SWU-->>SSH: result
SSH-->>CLI: result

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • _get_active_device() branching (swupdate vs rootdev) and command selection.
    • _parse_partition_info() handling of varied device path formats.
    • Mount/unmount lifecycles and cleanup for both SSH and local paths.
    • read_file/file_exists abstractions (SFTP vs local) and error semantics.
    • Remote command assembly: quoting, sourcing, and bash -c wrapper correctness.

Possibly related PRs

Suggested reviewers

  • Eeems

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main issue: fixing on-device installs and the rmpp 3.20 downgrade regression mentioned in the PR objectives and issue #158.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Eeems Eeems linked an issue Dec 7, 2025 that may be closed by this pull request
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
codexctl/device.py (1)

416-417: Cleanup subprocess calls don't check return values.

If umount fails, the rm -rf still runs which could attempt to delete a still-mounted filesystem. While in a finally block for best-effort cleanup, consider checking the umount result before removing the directory:

                 finally:
-                    subprocess.run(["umount", mount_point])
-                    subprocess.run(["rm", "-rf", mount_point])
+                    umount_result = subprocess.run(["umount", mount_point], capture_output=True)
+                    if umount_result.returncode == 0:
+                        subprocess.run(["rm", "-rf", mount_point])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6204258 and 79281d1.

📒 Files selected for processing (2)
  • codexctl/__init__.py (1 hunks)
  • codexctl/device.py (7 hunks)
🧰 Additional context used
🪛 Ruff (0.14.7)
codexctl/device.py

311-311: Consider moving this statement to an else block

(TRY300)


328-328: Avoid specifying long messages outside the exception class

(TRY003)


335-335: Avoid specifying long messages outside the exception class

(TRY003)


337-337: Avoid specifying long messages outside the exception class

(TRY003)


354-354: subprocess call: check for execution of untrusted input

(S603)


383-383: Probable insecure usage of temporary file or directory: "/tmp/mount_p"

(S108)


395-395: Abstract raise to an inner function

(TRY301)


395-395: Avoid specifying long messages outside the exception class

(TRY003)


405-405: subprocess call: check for execution of untrusted input

(S603)


406-406: Starting a process with a partial executable path

(S607)


410-410: Abstract raise to an inner function

(TRY301)


410-410: Avoid specifying long messages outside the exception class

(TRY003)


416-416: subprocess call: check for execution of untrusted input

(S603)


416-416: Starting a process with a partial executable path

(S607)


417-417: subprocess call: check for execution of untrusted input

(S603)


417-417: Starting a process with a partial executable path

(S607)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build for ubuntu-latest
  • GitHub Check: Build for macos-latest
  • GitHub Check: Build for windows-latest
  • GitHub Check: Build for remarkable
🔇 Additional comments (6)
codexctl/device.py (6)

294-337: Well-structured abstraction for unified file access.

The refactored _read_version_from_path cleanly handles both SFTP and local file access via the nested helper functions. The fallback from update.conf to os-release is correct for supporting both old and new update engines.


339-355: Good helper for active device detection.

The conditional logic for command selection based on hardware type is appropriate—Paper Pro variants use swupdate -g while RM1/RM2 use rootdev. The subprocess call is safe since the command string is hardcoded.


357-369: Partition parsing logic is correct for expected device paths.

The helper correctly extracts partition information from device paths like /dev/mmcblk2p2. The assumption that active/inactive partitions are 2 and 3 is valid for reMarkable's A/B partition scheme.


423-475: Good refactoring using new helper methods.

The method now uses _get_active_device and _parse_partition_info for cleaner code. The version-aware partition scheme detection (3.22+ boundary) and proper fallback for both SSH and local contexts is well implemented.


499-511: Consistent local file handling with explicit encoding.

The call to _read_version_from_path() without the ftp parameter correctly triggers local file access. Adding explicit encoding="utf-8" to file opens is good practice.


779-797: Implementation change correctly addresses the regression.

Switching from swupdate-from-image-file to sourcing the args file and running swupdate directly is a good approach. This bypasses the problematic "old A/B downgrade" check while maintaining proper swupdate configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
codexctl/device.py (2)

295-340: Harden SFTP file_exists handling in _read_version_from_path

The SFTP file_exists helper currently only catches FileNotFoundError. Paramiko’s SFTPClient.stat typically raises an OSError/IOError (with errno.ENOENT) for missing files rather than a concrete FileNotFoundError, so non‑existent paths may currently bubble up as unexpected exceptions instead of a clean False.

You can make this more robust by widening the exception:

-        if ftp:
-            def file_exists(path: str) -> bool:
-                try:
-                    ftp.stat(path)
-                    return True
-                except FileNotFoundError:
-                    return False
+        if ftp:
+            def file_exists(path: str) -> bool:
+                try:
+                    ftp.stat(path)
+                    return True
+                except OSError:
+                    # Paramiko uses OSError/IOError for missing paths.
+                    return False

Same comment applies to the SFTP helpers in _get_paper_pro_partition_info, for consistency.


374-425: Close SFTP handle when reading backup-partition version

In _get_backup_partition_version, the remote branch opens an SFTP session but never closes it, which can leak channels if this method is called multiple times or fails early:

ftp = self.client.open_sftp()
...
try:
    version, _ = self._read_version_from_path(ftp, mount_point)
    return version
finally:
    self.client.exec_command(f"umount {mount_point}")
    self.client.exec_command(f"rm -rf {mount_point}")
    # ftp is never closed

You can close the SFTP handle in the finally block with minimal change:

-                try:
-                    version, _ = self._read_version_from_path(ftp, mount_point)
-                    return version
-                finally:
-                    self.client.exec_command(f"umount {mount_point}")
-                    self.client.exec_command(f"rm -rf {mount_point}")
+                try:
+                    version, _ = self._read_version_from_path(ftp, mount_point)
+                    return version
+                finally:
+                    self.client.exec_command(f"umount {mount_point}")
+                    self.client.exec_command(f"rm -rf {mount_point}")
+                    ftp.close()

This keeps resource usage under control, especially if status queries end up invoking this helper frequently.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79281d1 and 04d24c5.

📒 Files selected for processing (1)
  • codexctl/device.py (8 hunks)
🧰 Additional context used
🪛 Ruff (0.14.7)
codexctl/device.py

312-312: Consider moving this statement to an else block

(TRY300)


331-331: Avoid specifying long messages outside the exception class

(TRY003)


338-338: Avoid specifying long messages outside the exception class

(TRY003)


340-340: Avoid specifying long messages outside the exception class

(TRY003)


357-357: subprocess call: check for execution of untrusted input

(S603)


386-386: Probable insecure usage of temporary file or directory: "/tmp/mount_p"

(S108)


398-398: Abstract raise to an inner function

(TRY301)


398-398: Avoid specifying long messages outside the exception class

(TRY003)


408-408: subprocess call: check for execution of untrusted input

(S603)


409-409: Starting a process with a partial executable path

(S607)


413-413: Abstract raise to an inner function

(TRY301)


413-413: Avoid specifying long messages outside the exception class

(TRY003)


419-419: subprocess call: check for execution of untrusted input

(S603)


419-419: Starting a process with a partial executable path

(S607)


420-420: subprocess call: check for execution of untrusted input

(S603)


420-420: Starting a process with a partial executable path

(S607)


452-452: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (4)
codexctl/device.py (4)

435-487: Paper Pro partition detection logic looks consistent

The refactor to use _get_active_device + _parse_partition_info and then derive next_boot_part via the boot_part/root_part sysfs files is clear and symmetric between SSH and local access (via the file_exists/read_file helpers). The 3.22 boundary check on current_version is straightforward, and the fallback from boot_part to root_part when sysfs entries are missing should handle mixed old/new layouts well.

I don’t see any correctness problems in this block as written.


488-532: Good reuse of _read_version_from_path and explicit encodings in get_device_status

Routing both remote and local version detection through _read_version_from_path removes duplicated parsing logic and keeps the “old vs new update engine” detection in one place. The added encoding="utf-8" on the local /etc/version and xochitl.conf reads is also a nice touch for consistency with the SFTP reads.

No changes requested here.


810-886: Bootloader update helper looks solid overall

The _update_paper_pro_bootloader flow — creating temp files, uploading via SFTP, running preinst and postinst with exit‑status checks, and cleaning up both remote and local artifacts — is structured well and should be robust for the rM Pro downgrade case.

No blocking issues from my side; just ensure this path is exercised on real hardware since failures here are hard to simulate in tests.


5-5: Fix remote swupdate command construction to use shlex.quote safely

Importing shlex and using it for the local bash -c call is good, but the remote path currently mixes a manual bash -c '...'{shlex.quote(out_location)}' pattern with shlex.quote(out_location). That pattern is not what shlex.quote is designed for:

  • For image paths containing spaces or shell metacharacters, embedding the shlex.quote(...) result inside a single‑quoted bash -c '...' string can still break parsing, and it undermines shlex.quote's safety guarantees.
  • We already have a shell (/bin/sh -c) around the command on the remote side; the simplest and safest option is to build a single script string and quote it once.

You can keep using bash and 09-swupdate-args, but restructure the command like this:

         print("\nDone! Running swupdate (PLEASE BE PATIENT, ~5 MINUTES)")
 
-            command = f"bash -c 'source /usr/lib/swupdate/conf.d/09-swupdate-args && swupdate $SWUPDATE_ARGS -i {shlex.quote(out_location)}'"
-            self.logger.debug(command)
-            _stdin, stdout, _stderr = self.client.exec_command(command)
+            script = (
+                f"source /usr/lib/swupdate/conf.d/09-swupdate-args "
+                f"&& swupdate $SWUPDATE_ARGS -i {shlex.quote(out_location)}"
+            )
+            command = f"bash -c {shlex.quote(script)}"
+            self.logger.debug(command)
+            _stdin, stdout, _stderr = self.client.exec_command(command)

The local branch is already in good shape:

command = [
    "bash",
    "-c",
    f"source /usr/lib/swupdate/conf.d/09-swupdate-args && "
    f"swupdate $SWUPDATE_ARGS -i {shlex.quote(version_file)}",
]

With this change, both local and remote invocations:

  • Use shlex.quote exactly where it's intended (to build shell command strings), and
  • Correctly handle image paths with spaces or special characters without opening an injection surface.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
codexctl/device.py (2)

744-765: Remote swupdate command still fragile when filename needs quoting

This line tries to address the earlier quoting issue by wrapping out_location in shlex.quote, but because it’s interpolated inside a single-quoted bash -c '...' string, paths that actually need quoting (spaces, special chars) will still break the command:

command = (
    "bash -c 'source /usr/lib/swupdate/conf.d/09-swupdate-args "
    f"&& swupdate $SWUPDATE_ARGS -i {shlex.quote(out_location)}'"
)

Example: if out_location contained a space, shlex.quote would yield '/tmp/foo bar', which injects nested single quotes into the outer '...' and produces an invalid shell command.

A safer pattern is to build the script separately and quote it once as an argument to bash -c:

script = (
    "source /usr/lib/swupdate/conf.d/09-swupdate-args "
    f"&& swupdate $SWUPDATE_ARGS -i {shlex.quote(out_location)}"
)
command = f"bash -c {shlex.quote(script)}"

This keeps $SWUPDATE_ARGS expansion inside bash, while correctly handling any filename that requires quoting.

Given this was intended to fix a prior quoting issue, I’d treat updating this as important before relying on arbitrary image paths.


800-816: Local swupdate invocation and quoting look correct

For the local (on-device) case:

command = [
    "bash",
    "-c",
    "source /usr/lib/swupdate/conf.d/09-swupdate-args "
    f"&& swupdate $SWUPDATE_ARGS -i {shlex.quote(version_file)}",
]

Here shlex.quote(version_file) is passed directly into the script run by bash -c, which is exactly what we want: filenames with spaces or shell metacharacters are safely quoted in the inner shell, and $SWUPDATE_ARGS is expanded after sourcing the args file.

This addresses the earlier local-path quoting concern cleanly.

🧹 Nitpick comments (7)
codexctl/device.py (7)

295-340: Unified _read_version_from_path looks good; small optional cleanups

The ftp/local abstraction and reuse via file_exists/read_file is a nice improvement and makes callers much simpler. Two minor, optional tweaks:

  • update_conf_path/os_release_path construction is duplicated in a few places; consider extracting a tiny helper if you end up needing similar logic elsewhere.
  • For readability and slightly better diagnostics, you could distinguish “file missing” vs “key missing inside file” in the SystemError messages (currently both are lumped as generic failures).

Functionally this looks sound and meets the PR goals.


342-369: Active-device detection and error handling are solid; minor subprocess nit

The new _get_active_device correctly:

  • Picks swupdate -g for Paper Pro(-Move) and rootdev elsewhere.
  • Checks remote/local exit status and stderr and raises a clear SystemError on failure.

One minor, non-blocking suggestion: instead of cmd.split(), you could pass the argv list explicitly, e.g.:

if self.hardware in (HardwareType.RMPP, HardwareType.RMPPM):
    argv = ["swupdate", "-g"]
else:
    argv = ["rootdev"]

result = subprocess.run(argv, capture_output=True, text=True)

This avoids any ambiguity if the command ever stops being a simple two-word string.


370-382: Partition parsing assumes 2/3 scheme; consider guarding unexpected values

_parse_partition_info is straightforward and matches the expected ...p2/...p3 scheme, but it will silently treat any non-2 value as “active=other, inactive=2”. If rootdev/swupdate -g ever returns something unexpected, that could mis-point the mount.

Consider explicitly validating the parsed active_part and raising if it’s not 2 or 3:

if active_part not in (2, 3):
    raise SystemError(f"Unexpected active partition {active_part} from {active_device}")
inactive_part = 3 if active_part == 2 else 2

That would fail fast with a clearer error if the platform ever changes.


384-435: Backup-partition version logic works; tighten mountpoint & cleanup handling

The new _get_backup_partition_version correctly:

  • Reuses _get_active_device/_parse_partition_info.
  • Handles SSH vs local mounting.
  • Treats Paper Pro failures as fatal while returning "" for RM1/RM2.

A few non-blocking hardening suggestions:

  • The fixed mountpoint /tmp/mount_p{inactive_part} is workable but does open the usual /tmp TOCTOU/symlink window. Using tempfile.TemporaryDirectory(dir="/tmp") (or similar) for the mountpoint would mitigate that and avoid the explicit rm -rf.
  • For the local branch, using shutil.rmtree(mount_point) instead of subprocess.run(["rm", "-rf", mount_point]) is safer and easier to reason about.
  • In the SSH branch, ftp is never closed; not critical here, but explicitly ftp.close() in the finally would keep the connection tidier if this is called repeatedly.

Functionally this is fine; these are just robustness improvements.


445-496: Paper Pro partition-info helper matches the new scheme; minor duplication

Using _get_active_device + _parse_partition_info and then probing boot_part / root_part via the same file_exists / read_file abstraction as _read_version_from_path is a nice cleanup and should fix the on-device vs SSH divergence.

Two small follow-ups you might consider:

  • file_exists/read_file are now duplicated between this method and _read_version_from_path; pulling them into a tiny shared helper (e.g., _make_file_ops(ftp) -> (file_exists, read_file)) would DRY up the code and keep behavior consistent.
  • ftp = self.client.open_sftp() isn’t closed; adding a try/finally: ftp.close() (or a context manager helper) would avoid leaking SFTP sessions if this is called more than a handful of times.

Otherwise the 3.22+ vs legacy boot-part logic looks consistent with the stated downgrade behavior.


638-681: Paper Pro restore logic for 3.22 boundary looks reasonable; document assumptions

The new Paper Pro branch in restore_previous_version:

  • Derives current_part/inactive_part via _get_paper_pro_partition_info.
  • Distinguishes <3.22 vs >=3.22 for both current and backup.
  • Uses root_part sysfs writes only for legacy, and mmc bootpart enable when either side is “new”.
  • Resets the appropriate root{a,b}_errcnt counter.

This matches the described goal of handling 3.20↔3.22+ transitions in a single script. Given how hardware-specific this is, I’d just suggest adding a short comment summarizing the expected partition layout (e.g., “2→A / 3→B; boot0/boot1 mapping”) and the rationale for inactive_part - 1 / inactive_part - 2 so future maintainers don’t have to reverse-engineer the arithmetic.

Behavior-wise, this looks fine and aligns with the test matrix in the PR description.


820-897: Bootloader update helper is sound; minor robustness tweaks possible

_update_paper_pro_bootloader looks careful and appropriate for something as sensitive as a bootloader update:

  • Uses tmpfiles locally, uploads via SFTP, then runs preinst and postinst with explicit exit-status checks.
  • Cleans up both remote and local temp artifacts in a finally block.
  • Surfaces stderr content in SystemError messages when the script fails.

A couple of small, optional robustness ideas:

  • If you ever expect significant output from the update script, consider draining stdout/stderr before or while waiting on recv_exit_status() to avoid any chance of channel-buffer deadlock.
  • In the finally, you may want to wrap os.unlink(...) calls in a small try/except FileNotFoundError so a missing temp file doesn’t mask a more interesting earlier exception.

Nothing blocking here; the overall structure looks good.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04d24c5 and 87ded47.

📒 Files selected for processing (1)
  • codexctl/device.py (8 hunks)
🧰 Additional context used
🪛 Ruff (0.14.7)
codexctl/device.py

312-312: Consider moving this statement to an else block

(TRY300)


331-331: Avoid specifying long messages outside the exception class

(TRY003)


338-338: Avoid specifying long messages outside the exception class

(TRY003)


340-340: Avoid specifying long messages outside the exception class

(TRY003)


362-362: Avoid specifying long messages outside the exception class

(TRY003)


365-365: subprocess call: check for execution of untrusted input

(S603)


367-367: Avoid specifying long messages outside the exception class

(TRY003)


396-396: Probable insecure usage of temporary file or directory: "/tmp/mount_p"

(S108)


408-408: Abstract raise to an inner function

(TRY301)


408-408: Avoid specifying long messages outside the exception class

(TRY003)


418-418: subprocess call: check for execution of untrusted input

(S603)


419-419: Starting a process with a partial executable path

(S607)


423-423: Abstract raise to an inner function

(TRY301)


423-423: Avoid specifying long messages outside the exception class

(TRY003)


429-429: subprocess call: check for execution of untrusted input

(S603)


429-429: Starting a process with a partial executable path

(S607)


430-430: subprocess call: check for execution of untrusted input

(S603)


430-430: Starting a process with a partial executable path

(S607)


462-462: Consider moving this statement to an else block

(TRY300)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build for macos-latest
  • GitHub Check: Build for windows-latest
  • GitHub Check: Build for remarkable
  • GitHub Check: Build for ubuntu-latest
🔇 Additional comments (1)
codexctl/device.py (1)

506-533: Device-status refactor to reuse _read_version_from_path looks good

Switching both SSH and local code paths in get_device_status to use _read_version_from_path is a good simplification and ensures version parsing stays consistent with the new helpers.

The added encoding="utf-8" on local file reads is also a nice touch for robustness.

No issues from my side here.

@rmitchellscott rmitchellscott requested a review from Eeems December 7, 2025 03:56
@Eeems Eeems merged commit be75774 into Jayy001:main Dec 7, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot get backup partition version: no SSH client connection

2 participants