@opencode-cloud/core 8.0.0 → 10.1.0

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.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "opencode-cloud-core"
3
- version = "8.0.0"
3
+ version = "10.1.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  license = "MIT"
package/README.md CHANGED
@@ -196,6 +196,9 @@ occ stop
196
196
  # Restart the service
197
197
  occ restart
198
198
 
199
+ # Check for updates and choose what to update
200
+ occ update
201
+
199
202
  # Update the opencode-cloud CLI binary
200
203
  occ update cli
201
204
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cloud/core",
3
- "version": "8.0.0",
3
+ "version": "10.1.0",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -30,9 +30,9 @@
30
30
  # -----------------------------------------------------------------------------
31
31
 
32
32
  # -----------------------------------------------------------------------------
33
- # Stage 1: Runtime
33
+ # Stage 1: Base
34
34
  # -----------------------------------------------------------------------------
35
- FROM ubuntu:24.04 AS runtime
35
+ FROM ubuntu:24.04 AS base
36
36
 
37
37
  # OCI Labels for image metadata
38
38
  LABEL org.opencontainers.image.title="opencode-cloud"
@@ -236,6 +236,11 @@ ENV PNPM_HOME="/home/opencode/.local/share/pnpm"
236
236
  ENV PATH="${PNPM_HOME}:${PATH}"
237
237
  RUN mkdir -p "${PNPM_HOME}"
238
238
 
239
+ # bun - self-managing installer, trusted to handle versions
240
+ RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
241
+ && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun
242
+
243
+ ENV PATH="/home/opencode/.bun/bin:${PATH}"
239
244
 
240
245
  # uv - self-managing installer, trusted to handle versions (fast Python package manager)
241
246
  RUN curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -495,6 +500,11 @@ RUN printf '%s\n' \
495
500
  # Set up pipx path
496
501
  RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshrc
497
502
 
503
+ # -----------------------------------------------------------------------------
504
+ # Stage 2: opencode build
505
+ # -----------------------------------------------------------------------------
506
+ FROM base AS opencode-build
507
+
498
508
  # -----------------------------------------------------------------------------
499
509
  # opencode Setup (Fork + Broker + Proxy)
500
510
  # -----------------------------------------------------------------------------
@@ -504,11 +514,9 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
504
514
  # This block includes:
505
515
  # - opencode build (backend binary) + app build (frontend dist)
506
516
  # - opencode-broker build
507
- # - opencode web build + runtime
508
- # - PAM configuration + systemd services
509
- # - opencode config file
510
517
  #
511
- # NOTE: This section switches between opencode and root users as needed.
518
+ # NOTE: This stage uses opencode user + sudo for privileged installs.
519
+ USER opencode
512
520
 
513
521
  # -----------------------------------------------------------------------------
514
522
  # opencode Installation (Fork from pRizz/opencode)
@@ -516,18 +524,15 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
516
524
  # Clone the fork and build opencode from source (as non-root user)
517
525
  # Pin to specific commit for reproducibility
518
526
  # Build opencode from source (BuildKit cache mounts disabled for now)
519
- RUN OPENCODE_COMMIT="ecaebed0934eeda267a66226eb8c2106beaf68c8" \
527
+ RUN OPENCODE_COMMIT="8a63844a3d3166273a83d5e94d8820d62ad82510" \
520
528
  && rm -rf /tmp/opencode-repo \
521
529
  && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
522
530
  && cd /tmp/opencode-repo \
523
531
  && git fetch --depth 1 origin "${OPENCODE_COMMIT}" \
524
532
  && git checkout "${OPENCODE_COMMIT}" \
525
- && curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
526
- && export PATH="/home/opencode/.bun/bin:${PATH}" \
527
533
  && bun install --frozen-lockfile \
528
534
  && cd packages/opencode \
529
535
  && bun run build-single-ui \
530
- && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \
531
536
  && cd /tmp/opencode-repo \
532
537
  && sudo mkdir -p /opt/opencode/bin /opt/opencode/ui \
533
538
  && echo "${OPENCODE_COMMIT}" | sudo tee /opt/opencode/COMMIT >/dev/null \
@@ -536,30 +541,21 @@ RUN OPENCODE_COMMIT="ecaebed0934eeda267a66226eb8c2106beaf68c8" \
536
541
  && sudo cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /opt/opencode/ui/ \
537
542
  && sudo chown -R opencode:opencode /opt/opencode \
538
543
  && sudo chmod +x /opt/opencode/bin/opencode \
539
- && /opt/opencode/bin/opencode --version
540
-
541
- # Add opencode to PATH
542
- ENV PATH="/opt/opencode/bin:${PATH}"
544
+ && cd /tmp/opencode-repo/packages/opencode-broker \
545
+ && cargo build --release \
546
+ && sudo mkdir -p /usr/local/bin \
547
+ && sudo cp target/release/opencode-broker /usr/local/bin/opencode-broker \
548
+ && sudo chmod 4755 /usr/local/bin/opencode-broker \
549
+ && rm -rf /tmp/opencode-repo \
550
+ && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun
543
551
 
544
552
  # -----------------------------------------------------------------------------
545
- # opencode-broker Installation
553
+ # Stage 3: Runtime
546
554
  # -----------------------------------------------------------------------------
547
- # Build opencode-broker from source (Rust service for PAM authentication)
548
- # The broker handles PAM authentication and user process spawning
549
- # NOTE: Requires root privileges for setuid bit (chmod 4755) to allow broker
550
- # to run with elevated privileges for PAM authentication
551
- USER root
552
- RUN cd /tmp/opencode-repo/packages/opencode-broker \
553
- && runuser -u opencode -- bash -c '. /home/opencode/.cargo/env && cargo build --release' \
554
- && mkdir -p /usr/local/bin \
555
- && cp target/release/opencode-broker /usr/local/bin/opencode-broker \
556
- && chmod 4755 /usr/local/bin/opencode-broker \
557
- && rm -rf /tmp/opencode-repo
555
+ FROM base AS runtime
558
556
 
559
- # Verify broker binary exists and is executable
560
- RUN ls -la /usr/local/bin/opencode-broker \
561
- && test -x /usr/local/bin/opencode-broker \
562
- && echo "Broker installed"
557
+ # Add opencode to PATH
558
+ ENV PATH="/opt/opencode/bin:${PATH}"
563
559
 
564
560
  # -----------------------------------------------------------------------------
565
561
  # PAM Configuration
@@ -567,6 +563,7 @@ RUN ls -la /usr/local/bin/opencode-broker \
567
563
  # Install PAM configuration for opencode authentication
568
564
  # This allows opencode to authenticate users via PAM (same users as Cockpit)
569
565
  # NOTE: Requires root privileges to write to /etc/pam.d/
566
+ USER root
570
567
  RUN printf '%s\n' \
571
568
  '# PAM configuration for OpenCode authentication' \
572
569
  '# Install to /etc/pam.d/opencode' \
@@ -625,24 +622,11 @@ RUN printf '%s\n' \
625
622
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
626
623
  && ln -sf /etc/systemd/system/opencode-broker.service /etc/systemd/system/multi-user.target.wants/opencode-broker.service
627
624
 
628
- USER opencode
629
-
630
- # -----------------------------------------------------------------------------
631
- # GSD Plugin Installation
632
- # -----------------------------------------------------------------------------
633
- # Install the GSD (Get Shit Done) plugin for opencode
634
- # Note: If this fails in container builds due to "~" path resolution, retry with
635
- # OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode set explicitly.
636
- RUN mkdir -p /home/opencode/.npm \
637
- && npx --yes get-shit-done-cc --opencode --global \
638
- && rm -rf /home/opencode/.npm/_cacache /home/opencode/.npm/_npx
639
-
640
625
  # -----------------------------------------------------------------------------
641
626
  # opencode systemd Service (2026-01-22)
642
627
  # -----------------------------------------------------------------------------
643
628
  # Create opencode as a systemd service for Cockpit integration
644
629
  # NOTE: Requires root privileges to write to /etc/systemd/system/
645
- USER root
646
630
  RUN printf '%s\n' \
647
631
  '[Unit]' \
648
632
  'Description=opencode Web Interface' \
@@ -655,7 +639,7 @@ RUN printf '%s\n' \
655
639
  'ExecStart=/opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
656
640
  'Restart=always' \
657
641
  'RestartSec=5' \
658
- 'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
642
+ 'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
659
643
  '' \
660
644
  '[Install]' \
661
645
  'WantedBy=multi-user.target' \
@@ -683,15 +667,12 @@ RUN mkdir -p /home/opencode/.config/opencode \
683
667
  # Verify config file exists
684
668
  RUN ls -la /home/opencode/.config/opencode/opencode.jsonc && cat /home/opencode/.config/opencode/opencode.jsonc
685
669
 
686
- USER opencode
687
-
688
670
  # -----------------------------------------------------------------------------
689
671
  # Entrypoint Script (Hybrid Init Support)
690
672
  # -----------------------------------------------------------------------------
691
673
  # Supports both tini (default, works everywhere) and systemd (for Cockpit on Linux)
692
674
  # Set USE_SYSTEMD=1 environment variable to use systemd init
693
675
  # Note: Entrypoint runs as root to support both modes; tini mode drops to opencode user
694
- USER root
695
676
  RUN printf '%s\n' \
696
677
  '#!/bin/bash' \
697
678
  'if [ "${USE_SYSTEMD}" = "1" ]; then' \
@@ -716,6 +697,34 @@ RUN printf '%s\n' \
716
697
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
717
698
  CMD curl -f -H "Accept: text/html" http://localhost:3000/ || exit 1
718
699
 
700
+ # -----------------------------------------------------------------------------
701
+ # opencode Artifacts
702
+ # -----------------------------------------------------------------------------
703
+ COPY --from=opencode-build /opt/opencode /opt/opencode
704
+ COPY --from=opencode-build /usr/local/bin/opencode-broker /usr/local/bin/opencode-broker
705
+
706
+ RUN chown -R opencode:opencode /opt/opencode \
707
+ && chmod +x /opt/opencode/bin/opencode \
708
+ && chmod 4755 /usr/local/bin/opencode-broker
709
+
710
+ # Verify broker binary exists and is executable
711
+ RUN ls -la /usr/local/bin/opencode-broker \
712
+ && test -x /usr/local/bin/opencode-broker \
713
+ && echo "Broker installed"
714
+
715
+ USER opencode
716
+ RUN /opt/opencode/bin/opencode --version
717
+
718
+ # -----------------------------------------------------------------------------
719
+ # GSD Plugin Installation
720
+ # -----------------------------------------------------------------------------
721
+ # Install the GSD (Get Shit Done) plugin for opencode
722
+ # Note: If this fails in container builds due to "~" path resolution, retry with
723
+ # OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode set explicitly.
724
+ RUN mkdir -p /home/opencode/.npm \
725
+ && npx --yes get-shit-done-cc --opencode --global \
726
+ && rm -rf /home/opencode/.npm/_cacache /home/opencode/.npm/_npx
727
+
719
728
  # -----------------------------------------------------------------------------
720
729
  # Version File
721
730
  # -----------------------------------------------------------------------------
package/src/docker/mod.rs CHANGED
@@ -21,6 +21,7 @@ mod health;
21
21
  pub mod image;
22
22
  pub mod mount;
23
23
  pub mod progress;
24
+ mod registry;
24
25
  pub mod state;
25
26
  pub mod update;
26
27
  pub mod users;
@@ -47,7 +48,10 @@ pub use image::{build_image, image_exists, pull_image};
47
48
  pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
48
49
 
49
50
  // Version detection
50
- pub use version::{VERSION_LABEL, get_cli_version, get_image_version, versions_compatible};
51
+ pub use version::{
52
+ VERSION_LABEL, get_cli_version, get_image_version, get_registry_latest_version,
53
+ versions_compatible,
54
+ };
51
55
 
52
56
  // Container exec operations
53
57
  pub use exec::{
@@ -0,0 +1,214 @@
1
+ //! Docker registry API helpers.
2
+ //!
3
+ //! Provides lightweight helpers for querying registry manifests and configs
4
+ //! without pulling images.
5
+
6
+ use super::DockerError;
7
+ use reqwest::header::ACCEPT;
8
+ use serde::Deserialize;
9
+ use std::collections::HashMap;
10
+
11
+ #[derive(Deserialize)]
12
+ struct TokenResponse {
13
+ token: Option<String>,
14
+ access_token: Option<String>,
15
+ }
16
+
17
+ #[derive(Deserialize)]
18
+ struct ManifestConfig {
19
+ digest: String,
20
+ }
21
+
22
+ #[derive(Deserialize)]
23
+ struct Manifest {
24
+ config: ManifestConfig,
25
+ }
26
+
27
+ #[derive(Deserialize)]
28
+ struct ManifestList {
29
+ manifests: Vec<ManifestDescriptor>,
30
+ }
31
+
32
+ #[derive(Deserialize)]
33
+ struct ManifestDescriptor {
34
+ digest: String,
35
+ platform: Option<ManifestPlatform>,
36
+ }
37
+
38
+ #[derive(Deserialize)]
39
+ struct ManifestPlatform {
40
+ architecture: Option<String>,
41
+ os: Option<String>,
42
+ }
43
+
44
+ #[derive(Deserialize)]
45
+ #[serde(untagged)]
46
+ enum ManifestResponse {
47
+ Single(Manifest),
48
+ List(ManifestList),
49
+ }
50
+
51
+ #[derive(Deserialize)]
52
+ struct ImageConfig {
53
+ config: Option<ImageConfigDetails>,
54
+ }
55
+
56
+ #[derive(Deserialize)]
57
+ struct ImageConfigDetails {
58
+ #[serde(rename = "Labels")]
59
+ labels: Option<HashMap<String, String>>,
60
+ }
61
+
62
+ pub async fn fetch_registry_version(
63
+ registry_base: &str,
64
+ token_url: &str,
65
+ repo: &str,
66
+ tag: &str,
67
+ maybe_manifest_digest: Option<&str>,
68
+ label_key: &str,
69
+ ) -> Result<Option<String>, DockerError> {
70
+ let client = reqwest::Client::new();
71
+ let token = fetch_registry_token(&client, token_url).await?;
72
+ let manifest = if let Some(digest) = maybe_manifest_digest {
73
+ match fetch_registry_manifest(&client, registry_base, repo, digest, &token).await {
74
+ Ok(manifest) => manifest,
75
+ Err(digest_err) => fetch_registry_manifest(&client, registry_base, repo, tag, &token)
76
+ .await
77
+ .map_err(|tag_err| {
78
+ DockerError::Connection(format!(
79
+ "Failed to fetch registry manifest. Digest error: {digest_err}. Tag error: {tag_err}"
80
+ ))
81
+ })?,
82
+ }
83
+ } else {
84
+ fetch_registry_manifest(&client, registry_base, repo, tag, &token).await?
85
+ };
86
+ let image_config = fetch_registry_image_config(
87
+ &client,
88
+ registry_base,
89
+ repo,
90
+ &manifest.config.digest,
91
+ &token,
92
+ )
93
+ .await?;
94
+
95
+ let maybe_version = image_config
96
+ .config
97
+ .and_then(|details| details.labels)
98
+ .and_then(|labels| labels.get(label_key).cloned());
99
+
100
+ Ok(maybe_version)
101
+ }
102
+
103
+ async fn fetch_registry_token(
104
+ client: &reqwest::Client,
105
+ token_url: &str,
106
+ ) -> Result<String, DockerError> {
107
+ let response = client
108
+ .get(token_url)
109
+ .send()
110
+ .await
111
+ .map_err(|e| DockerError::Connection(format!("Failed to fetch registry token: {e}")))?;
112
+
113
+ let token_response: TokenResponse = response
114
+ .json()
115
+ .await
116
+ .map_err(|e| DockerError::Connection(format!("Failed to decode registry token: {e}")))?;
117
+ let token = token_response
118
+ .token
119
+ .or(token_response.access_token)
120
+ .ok_or_else(|| DockerError::Connection("Registry token missing".to_string()))?;
121
+
122
+ Ok(token)
123
+ }
124
+
125
+ async fn fetch_registry_manifest(
126
+ client: &reqwest::Client,
127
+ registry_base: &str,
128
+ repo: &str,
129
+ reference: &str,
130
+ token: &str,
131
+ ) -> Result<Manifest, DockerError> {
132
+ let mut current_reference = reference.to_string();
133
+ loop {
134
+ let manifest_url = format!("{registry_base}/v2/{repo}/manifests/{current_reference}");
135
+ let response = client
136
+ .get(&manifest_url)
137
+ .header(
138
+ ACCEPT,
139
+ "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json",
140
+ )
141
+ .bearer_auth(token)
142
+ .send()
143
+ .await
144
+ .map_err(|e| {
145
+ DockerError::Connection(format!("Failed to fetch registry manifest: {e}"))
146
+ })?;
147
+
148
+ if response.status() == reqwest::StatusCode::NOT_FOUND {
149
+ return Err(DockerError::Connection(format!(
150
+ "Manifest not found for {repo}:{current_reference}"
151
+ )));
152
+ }
153
+
154
+ let manifest_response: ManifestResponse = response
155
+ .json()
156
+ .await
157
+ .map_err(|e| DockerError::Connection(format!("Failed to decode manifest: {e}")))?;
158
+
159
+ match manifest_response {
160
+ ManifestResponse::Single(manifest) => return Ok(manifest),
161
+ ManifestResponse::List(list) => {
162
+ let digest = select_manifest_digest(&list).ok_or_else(|| {
163
+ DockerError::Connection(format!(
164
+ "No manifests available for {repo}:{current_reference}"
165
+ ))
166
+ })?;
167
+ current_reference = digest;
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ fn select_manifest_digest(list: &ManifestList) -> Option<String> {
174
+ let preferred = list.manifests.iter().find(|manifest| {
175
+ let Some(platform) = &manifest.platform else {
176
+ return false;
177
+ };
178
+ platform.os.as_deref() == Some("linux") && platform.architecture.as_deref() == Some("amd64")
179
+ });
180
+
181
+ preferred
182
+ .or_else(|| list.manifests.first())
183
+ .map(|manifest| manifest.digest.clone())
184
+ }
185
+
186
+ async fn fetch_registry_image_config(
187
+ client: &reqwest::Client,
188
+ registry_base: &str,
189
+ repo: &str,
190
+ digest: &str,
191
+ token: &str,
192
+ ) -> Result<ImageConfig, DockerError> {
193
+ let config_url = format!("{registry_base}/v2/{repo}/blobs/{digest}");
194
+ let response = client
195
+ .get(&config_url)
196
+ .bearer_auth(token)
197
+ .send()
198
+ .await
199
+ .map_err(|e| DockerError::Connection(format!("Failed to fetch image config: {e}")))?;
200
+
201
+ if !response.status().is_success() {
202
+ return Err(DockerError::Connection(format!(
203
+ "Failed to fetch image config: HTTP {}",
204
+ response.status()
205
+ )));
206
+ }
207
+
208
+ let config = response
209
+ .json()
210
+ .await
211
+ .map_err(|e| DockerError::Connection(format!("Failed to decode image config: {e}")))?;
212
+
213
+ Ok(config)
214
+ }
@@ -2,7 +2,8 @@
2
2
  //!
3
3
  //! Reads version information from Docker image labels.
4
4
 
5
- use super::{DockerClient, DockerError};
5
+ use super::registry::fetch_registry_version;
6
+ use super::{DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
6
7
 
7
8
  /// Version label key in Docker image
8
9
  pub const VERSION_LABEL: &str = "org.opencode-cloud.version";
@@ -38,6 +39,64 @@ pub async fn get_image_version(
38
39
  Ok(version)
39
40
  }
40
41
 
42
+ pub async fn get_registry_latest_version(
43
+ client: &DockerClient,
44
+ ) -> Result<Option<String>, DockerError> {
45
+ match fetch_ghcr_registry_version(client).await {
46
+ Ok(version) => Ok(version),
47
+ Err(ghcr_err) => fetch_dockerhub_registry_version(client).await.map_err(|dockerhub_err| {
48
+ DockerError::Connection(format!(
49
+ "Failed to fetch registry version. GHCR: {ghcr_err}. Docker Hub: {dockerhub_err}"
50
+ ))
51
+ }),
52
+ }
53
+ }
54
+
55
+ async fn fetch_ghcr_registry_version(client: &DockerClient) -> Result<Option<String>, DockerError> {
56
+ let repo = IMAGE_NAME_GHCR
57
+ .strip_prefix("ghcr.io/")
58
+ .unwrap_or(IMAGE_NAME_GHCR);
59
+ let reference = format!("{IMAGE_NAME_GHCR}:{IMAGE_TAG_DEFAULT}");
60
+ let digest = fetch_registry_digest(client, &reference).await;
61
+ fetch_registry_version(
62
+ "https://ghcr.io",
63
+ &format!("https://ghcr.io/token?scope=repository:{repo}:pull"),
64
+ repo,
65
+ IMAGE_TAG_DEFAULT,
66
+ digest.as_deref(),
67
+ VERSION_LABEL,
68
+ )
69
+ .await
70
+ }
71
+
72
+ async fn fetch_dockerhub_registry_version(
73
+ client: &DockerClient,
74
+ ) -> Result<Option<String>, DockerError> {
75
+ let repo = IMAGE_NAME_DOCKERHUB;
76
+ let reference = format!("{IMAGE_NAME_DOCKERHUB}:{IMAGE_TAG_DEFAULT}");
77
+ let digest = fetch_registry_digest(client, &reference).await;
78
+ fetch_registry_version(
79
+ "https://registry-1.docker.io",
80
+ &format!(
81
+ "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{repo}:pull"
82
+ ),
83
+ repo,
84
+ IMAGE_TAG_DEFAULT,
85
+ digest.as_deref(),
86
+ VERSION_LABEL,
87
+ )
88
+ .await
89
+ }
90
+
91
+ async fn fetch_registry_digest(client: &DockerClient, reference: &str) -> Option<String> {
92
+ client
93
+ .inner()
94
+ .inspect_registry_image(reference, None)
95
+ .await
96
+ .ok()
97
+ .and_then(|info| info.descriptor.digest)
98
+ }
99
+
41
100
  /// CLI version from Cargo.toml
42
101
  pub fn get_cli_version() -> &'static str {
43
102
  env!("CARGO_PKG_VERSION")