@opencode-cloud/core 4.0.0 → 4.0.2

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 = "4.0.0"
3
+ version = "4.0.2"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  license = "MIT"
package/README.md CHANGED
@@ -8,8 +8,13 @@
8
8
  [![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
 
11
+ > [!WARNING]
12
+ > This project is a work in progress and evolving rapidly. Use with caution.
13
+
11
14
  A production-ready toolkit for deploying and managing [opencode](https://github.com/anomalyco/opencode) as a persistent cloud service, **sandboxed inside a Docker container** for isolation and security.
12
15
 
16
+ This project uses the opencode fork at https://github.com/pRizz/opencode, which adds additional authentication and security features.
17
+
13
18
  ## Quick install (cargo)
14
19
 
15
20
  ```bash
@@ -19,12 +24,14 @@ opencode-cloud --version
19
24
 
20
25
  ## Deploy to AWS
21
26
 
22
- [![Deploy to AWS](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/create/review?templateURL=https://raw.githubusercontent.com/pRizz/opencode-cloud/main/infra/aws/cloudformation/opencode-cloud-quick.yaml)
27
+ [![Deploy to AWS](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/create/review?templateURL=https://opencode-cloud-templates.s3.us-east-2.amazonaws.com/cloudformation/opencode-cloud-quick.yaml)
23
28
 
24
29
  Quick deploy provisions a private EC2 instance behind a public ALB with HTTPS.
25
30
  **A domain name is required** for ACM certificate validation.
31
+ **A Route53 hosted zone ID is required** for automated DNS validation.
26
32
 
27
- Docs: `docs/deploy/aws.md` (includes teardown steps)
33
+ Docs: `docs/deploy/aws.md` (includes teardown steps and S3 hosting setup for forks)
34
+ Credentials: `docs/deploy/aws.md#retrieving-credentials`
28
35
 
29
36
  ## Features
30
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cloud/core",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -700,7 +700,7 @@ mod tests {
700
700
  fn test_serialize_deserialize_with_mounts() {
701
701
  let config = Config {
702
702
  mounts: vec![
703
- "/home/user/data:/workspace/data".to_string(),
703
+ "/home/user/data:/home/opencode/workspace/data".to_string(),
704
704
  "/home/user/config:/etc/app:ro".to_string(),
705
705
  ],
706
706
  ..Config::default()
@@ -708,7 +708,10 @@ mod tests {
708
708
  let json = serde_json::to_string(&config).unwrap();
709
709
  let parsed: Config = serde_json::from_str(&json).unwrap();
710
710
  assert_eq!(parsed.mounts.len(), 2);
711
- assert_eq!(parsed.mounts[0], "/home/user/data:/workspace/data");
711
+ assert_eq!(
712
+ parsed.mounts[0],
713
+ "/home/user/data:/home/opencode/workspace/data"
714
+ );
712
715
  assert_eq!(parsed.mounts[1], "/home/user/config:/etc/app:ro");
713
716
  }
714
717
 
@@ -104,8 +104,6 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
104
104
  netcat-openbsd=1.226-* \
105
105
  iputils-ping=3:20240117-* \
106
106
  dnsutils=1:9.18.* \
107
- # Reverse proxy for opencode UI + API
108
- nginx=1.24.* \
109
107
  # Compression
110
108
  zip=3.0-* \
111
109
  unzip=6.0-* \
@@ -505,7 +503,7 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
505
503
  # This block includes:
506
504
  # - opencode build (backend binary) + app build (frontend dist)
507
505
  # - opencode-broker build
508
- # - nginx config for single-endpoint UI + API proxy
506
+ # - opencode web build + runtime
509
507
  # - PAM configuration + systemd services
510
508
  # - opencode config file
511
509
  #
@@ -517,7 +515,7 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
517
515
  # Clone the fork and build opencode from source (as non-root user)
518
516
  # Pin to specific commit for reproducibility
519
517
  # Build opencode from source (BuildKit cache mounts disabled for now)
520
- RUN OPENCODE_COMMIT="3a4eccc7e883575e0d5a508f46036a9f243c06e8" \
518
+ RUN OPENCODE_COMMIT="9b91eb17f5ca1b0ee99cfaa0b4c87da6dbe9e784" \
521
519
  && rm -rf /tmp/opencode-repo \
522
520
  && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
523
521
  && cd /tmp/opencode-repo \
@@ -526,13 +524,15 @@ RUN OPENCODE_COMMIT="3a4eccc7e883575e0d5a508f46036a9f243c06e8" \
526
524
  && curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
527
525
  && export PATH="/home/opencode/.bun/bin:${PATH}" \
528
526
  && bun install --frozen-lockfile \
529
- && bun run packages/opencode/script/build.ts --single \
530
- && cd packages/app \
531
- && bun run build \
527
+ && cd packages/opencode \
528
+ && export VITE_OPENCODE_SERVER_URL="http://localhost:3000" \
529
+ && bun run build-single-ui \
532
530
  && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \
533
531
  && cd /tmp/opencode-repo \
534
532
  && mkdir -p /home/opencode/.local/share/opencode/bin \
533
+ && mkdir -p /home/opencode/.local/share/opencode/ui \
535
534
  && cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/bin/opencode /home/opencode/.local/share/opencode/bin/opencode \
535
+ && cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /home/opencode/.local/share/opencode/ui/ \
536
536
  && chown -R opencode:opencode /home/opencode/.local/share/opencode \
537
537
  && chmod +x /home/opencode/.local/share/opencode/bin/opencode \
538
538
  && /home/opencode/.local/share/opencode/bin/opencode --version
@@ -540,14 +540,6 @@ RUN OPENCODE_COMMIT="3a4eccc7e883575e0d5a508f46036a9f243c06e8" \
540
540
  # Add opencode to PATH
541
541
  ENV PATH="/home/opencode/.local/share/opencode/bin:${PATH}"
542
542
 
543
- # Copy UI assets to standard web root (requires root)
544
- USER root
545
- RUN mkdir -p /var/www/opencode \
546
- && cp -R /tmp/opencode-repo/packages/app/dist/. /var/www/opencode/ \
547
- && chown -R root:root /var/www/opencode \
548
- && chmod 755 /var/www /var/www/opencode \
549
- && chmod -R a+rX /var/www/opencode
550
-
551
543
  # -----------------------------------------------------------------------------
552
544
  # opencode-broker Installation
553
545
  # -----------------------------------------------------------------------------
@@ -568,49 +560,6 @@ RUN ls -la /usr/local/bin/opencode-broker \
568
560
  && test -x /usr/local/bin/opencode-broker \
569
561
  && echo "Broker installed"
570
562
 
571
- # -----------------------------------------------------------------------------
572
- # Nginx Reverse Proxy for UI + API
573
- # -----------------------------------------------------------------------------
574
- # Serve the built UI from /var/www/opencode and proxy all 3000 traffic to backend.
575
- RUN rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true \
576
- && printf '%s\n' \
577
- 'server {' \
578
- ' listen 3000;' \
579
- ' server_name _;' \
580
- '' \
581
- ' location / {' \
582
- ' proxy_pass http://127.0.0.1:3001;' \
583
- ' proxy_http_version 1.1;' \
584
- ' proxy_set_header Upgrade $http_upgrade;' \
585
- ' proxy_set_header Connection "upgrade";' \
586
- ' proxy_set_header Host $host;' \
587
- ' proxy_set_header X-Real-IP $remote_addr;' \
588
- ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
589
- ' proxy_set_header X-Forwarded-Proto $scheme;' \
590
- ' }' \
591
- '}' \
592
- '' \
593
- 'server {' \
594
- ' listen 3002;' \
595
- ' server_name _;' \
596
- '' \
597
- ' root /var/www/opencode;' \
598
- ' index index.html;' \
599
- '' \
600
- ' location /assets/ {' \
601
- ' try_files $uri =404;' \
602
- ' }' \
603
- '' \
604
- ' location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|map)$ {' \
605
- ' try_files $uri =404;' \
606
- ' }' \
607
- '' \
608
- ' location / {' \
609
- ' try_files $uri $uri/ /index.html;' \
610
- ' }' \
611
- '}' \
612
- > /etc/nginx/conf.d/opencode.conf
613
-
614
563
  # -----------------------------------------------------------------------------
615
564
  # PAM Configuration
616
565
  # -----------------------------------------------------------------------------
@@ -690,7 +639,7 @@ RUN mkdir -p /home/opencode/.npm \
690
639
  # -----------------------------------------------------------------------------
691
640
  # opencode systemd Service (2026-01-22)
692
641
  # -----------------------------------------------------------------------------
693
- # Create opencode as a systemd service for Cockpit integration (backend only)
642
+ # Create opencode as a systemd service for Cockpit integration
694
643
  # NOTE: Requires root privileges to write to /etc/systemd/system/
695
644
  USER root
696
645
  RUN printf '%s\n' \
@@ -702,7 +651,7 @@ RUN printf '%s\n' \
702
651
  'Type=simple' \
703
652
  'User=opencode' \
704
653
  'WorkingDirectory=/home/opencode/workspace' \
705
- 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode --port 3001 --hostname 0.0.0.0' \
654
+ 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
706
655
  'Restart=always' \
707
656
  'RestartSec=5' \
708
657
  'Environment=PATH=/home/opencode/.local/share/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' \
@@ -715,44 +664,15 @@ RUN printf '%s\n' \
715
664
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
716
665
  && ln -sf /etc/systemd/system/opencode.service /etc/systemd/system/multi-user.target.wants/opencode.service
717
666
 
718
- # Nginx service for serving UI + proxying API
719
- RUN printf '%s\n' \
720
- '[Unit]' \
721
- 'Description=Nginx reverse proxy for opencode UI' \
722
- 'After=network.target opencode.service' \
723
- '' \
724
- '[Service]' \
725
- 'Type=simple' \
726
- 'ExecStart=/usr/sbin/nginx -g "daemon off;"' \
727
- 'ExecReload=/usr/sbin/nginx -s reload' \
728
- 'Restart=always' \
729
- 'RestartSec=5' \
730
- '' \
731
- '[Install]' \
732
- 'WantedBy=multi-user.target' \
733
- > /etc/systemd/system/opencode-nginx.service
734
-
735
- # Enable nginx service
736
- RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
737
- && ln -sf /etc/systemd/system/opencode-nginx.service /etc/systemd/system/multi-user.target.wants/opencode-nginx.service
738
-
739
- # Prevent the distro nginx service from also starting (port 3000 conflict)
740
- RUN rm -f /etc/systemd/system/multi-user.target.wants/nginx.service \
741
- && ln -sf /dev/null /etc/systemd/system/nginx.service
742
-
743
667
  # -----------------------------------------------------------------------------
744
668
  # opencode Configuration
745
669
  # -----------------------------------------------------------------------------
746
- # Create opencode.jsonc config file with PAM authentication enabled and UI URL
670
+ # Create opencode.jsonc config file with PAM authentication enabled
747
671
  RUN mkdir -p /home/opencode/.config/opencode \
748
672
  && printf '%s\n' \
749
673
  '{' \
750
- ' // Container UI served via nginx on 3002' \
751
674
  ' "auth": {' \
752
675
  ' "enabled": true' \
753
- ' },' \
754
- ' "server": {' \
755
- ' "uiUrl": "http://localhost:3002"' \
756
676
  ' }' \
757
677
  '}' \
758
678
  > /home/opencode/.config/opencode/opencode.jsonc \
@@ -776,9 +696,11 @@ RUN printf '%s\n' \
776
696
  'if [ "${USE_SYSTEMD}" = "1" ]; then' \
777
697
  ' exec /sbin/init' \
778
698
  'else' \
699
+ ' # Ensure broker socket directory exists' \
700
+ ' install -d -m 0755 /run/opencode' \
701
+ ' /usr/local/bin/opencode-broker &' \
779
702
  ' # Use runuser to switch to opencode user without password prompt' \
780
- ' runuser -u opencode -- /home/opencode/.local/share/opencode/bin/opencode --port 3001 --hostname 0.0.0.0 &' \
781
- ' exec /usr/sbin/nginx -g "daemon off;"' \
703
+ ' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \
782
704
  'fi' \
783
705
  > /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
784
706
 
@@ -788,10 +710,10 @@ RUN printf '%s\n' \
788
710
  # -----------------------------------------------------------------------------
789
711
  # Health Check
790
712
  # -----------------------------------------------------------------------------
791
- # Check that opencode health endpoint responds
713
+ # Check that opencode main page responds
792
714
  # Works for both tini and systemd modes
793
715
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
794
- CMD curl -f http://localhost:3000/health || exit 1
716
+ CMD curl -f http://localhost:3000/ || exit 1
795
717
 
796
718
  # -----------------------------------------------------------------------------
797
719
  # Version File
@@ -812,8 +734,8 @@ RUN echo "${OPENCODE_CLOUD_VERSION}" > /etc/opencode-cloud-version
812
734
  # -----------------------------------------------------------------------------
813
735
  WORKDIR /home/opencode/workspace
814
736
 
815
- # Expose opencode ports (3000/3001/3002) and Cockpit (9090)
816
- EXPOSE 3000 3001 3002 9090
737
+ # Expose opencode web (3000) and Cockpit (9090)
738
+ EXPOSE 3000 9090
817
739
 
818
740
  # Hybrid init: entrypoint script chooses tini or systemd based on USE_SYSTEMD env
819
741
  ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -28,10 +28,18 @@ docker pull ghcr.io/prizz/opencode-cloud-sandbox:latest
28
28
  Run the container:
29
29
 
30
30
  ```
31
- docker run --rm -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest
31
+ docker run --rm -it -p 3000:3000 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest
32
32
  ```
33
33
 
34
- The opencode web UI is available at `http://localhost:3000`. The backend is reachable at `http://localhost:3001`, and the static UI is served at `http://localhost:3002`. Cockpit runs on `http://localhost:9090`.
34
+ The opencode web UI is available at `http://localhost:3000`. Cockpit runs on `http://localhost:9090`.
35
+
36
+ ## opencode build and serve flow
37
+
38
+ The Docker image builds opencode directly from the fork and runs the web server without nginx:
39
+
40
+ 1. `cd packages/opencode`
41
+ 2. `bun run build` to generate `packages/opencode/dist`
42
+ 3. Run the server with `./bin/opencode web`
35
43
 
36
44
  ## Source
37
45
 
@@ -20,7 +20,7 @@ use std::collections::HashMap;
20
20
  use tracing::debug;
21
21
 
22
22
  /// Default container name
23
- pub const CONTAINER_NAME: &str = "opencode-cloud";
23
+ pub const CONTAINER_NAME: &str = "opencode-cloud-sandbox";
24
24
 
25
25
  /// Default port for opencode web UI
26
26
  pub const OPENCODE_WEB_PORT: u16 = 3000;
@@ -38,7 +38,7 @@ pub const OPENCODE_WEB_PORT: u16 = 3000;
38
38
  /// * `env_vars` - Additional environment variables (optional)
39
39
  /// * `bind_address` - IP address to bind on host (defaults to "127.0.0.1")
40
40
  /// * `cockpit_port` - Port to bind on host for Cockpit (defaults to 9090)
41
- /// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to true)
41
+ /// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to false)
42
42
  /// * `bind_mounts` - User-defined bind mounts from config and CLI flags (optional)
43
43
  #[allow(clippy::too_many_arguments)]
44
44
  pub async fn create_container(
@@ -57,7 +57,7 @@ pub async fn create_container(
57
57
  let image_name = image.unwrap_or(&default_image);
58
58
  let port = opencode_web_port.unwrap_or(OPENCODE_WEB_PORT);
59
59
  let cockpit_port_val = cockpit_port.unwrap_or(9090);
60
- let cockpit_enabled_val = cockpit_enabled.unwrap_or(true);
60
+ let cockpit_enabled_val = cockpit_enabled.unwrap_or(false);
61
61
 
62
62
  debug!(
63
63
  "Creating container {} from image {} with port {} and cockpit_port {} (enabled: {})",
@@ -202,7 +202,7 @@ pub async fn create_container(
202
202
  let config = Config {
203
203
  image: Some(image_name.to_string()),
204
204
  hostname: Some(CONTAINER_NAME.to_string()),
205
- working_dir: Some("/workspace".to_string()),
205
+ working_dir: Some("/home/opencode/workspace".to_string()),
206
206
  exposed_ports: Some(exposed_ports),
207
207
  env: final_env,
208
208
  host_config: Some(host_config),
@@ -473,7 +473,7 @@ mod tests {
473
473
 
474
474
  #[test]
475
475
  fn container_constants_are_correct() {
476
- assert_eq!(CONTAINER_NAME, "opencode-cloud");
476
+ assert_eq!(CONTAINER_NAME, "opencode-cloud-sandbox");
477
477
  assert_eq!(OPENCODE_WEB_PORT, 3000);
478
478
  }
479
479
 
@@ -22,7 +22,7 @@ use super::{DockerClient, DockerError};
22
22
  ///
23
23
  /// # Example
24
24
  /// ```ignore
25
- /// let output = exec_command(&client, "opencode-cloud", vec!["whoami"]).await?;
25
+ /// let output = exec_command(&client, "opencode-cloud-sandbox", vec!["whoami"]).await?;
26
26
  /// ```
27
27
  pub async fn exec_command(
28
28
  client: &DockerClient,
@@ -104,7 +104,7 @@ pub async fn exec_command(
104
104
  /// // Set password via chpasswd (secure, non-interactive)
105
105
  /// exec_command_with_stdin(
106
106
  /// &client,
107
- /// "opencode-cloud",
107
+ /// "opencode-cloud-sandbox",
108
108
  /// vec!["chpasswd"],
109
109
  /// "username:password\n"
110
110
  /// ).await?;
@@ -53,12 +53,21 @@ pub enum HealthError {
53
53
  Timeout,
54
54
  }
55
55
 
56
+ fn format_host(bind_addr: &str) -> String {
57
+ if bind_addr.contains(':') && !bind_addr.starts_with('[') {
58
+ format!("[{bind_addr}]")
59
+ } else {
60
+ bind_addr.to_string()
61
+ }
62
+ }
63
+
56
64
  /// Check health by querying OpenCode's /global/health endpoint
57
65
  ///
58
66
  /// Returns the health response on success (HTTP 200).
59
67
  /// Returns an error for connection issues, timeouts, or non-200 responses.
60
- pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
61
- let url = format!("http://127.0.0.1:{port}/global/health");
68
+ pub async fn check_health(bind_addr: &str, port: u16) -> Result<HealthResponse, HealthError> {
69
+ let host = format_host(bind_addr);
70
+ let url = format!("http://{host}:{port}/global/health");
62
71
 
63
72
  let client = reqwest::Client::builder()
64
73
  .timeout(Duration::from_secs(5))
@@ -95,10 +104,11 @@ pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
95
104
  /// If container stats fail, still returns response with container_state = "unknown".
96
105
  pub async fn check_health_extended(
97
106
  client: &DockerClient,
107
+ bind_addr: &str,
98
108
  port: u16,
99
109
  ) -> Result<ExtendedHealthResponse, HealthError> {
100
110
  // Get basic health info
101
- let health = check_health(port).await?;
111
+ let health = check_health(bind_addr, port).await?;
102
112
 
103
113
  // Get container stats
104
114
  let container_name = super::CONTAINER_NAME;
@@ -155,11 +165,21 @@ mod tests {
155
165
  #[tokio::test]
156
166
  async fn test_health_check_connection_refused() {
157
167
  // Port 1 should always refuse connection
158
- let result = check_health(1).await;
168
+ let result = check_health("127.0.0.1", 1).await;
159
169
  assert!(result.is_err());
160
170
  match result.unwrap_err() {
161
171
  HealthError::ConnectionRefused => {}
162
172
  other => panic!("Expected ConnectionRefused, got: {other:?}"),
163
173
  }
164
174
  }
175
+
176
+ #[test]
177
+ fn format_host_wraps_ipv6() {
178
+ assert_eq!(format_host("::1"), "[::1]");
179
+ }
180
+
181
+ #[test]
182
+ fn format_host_preserves_ipv4() {
183
+ assert_eq!(format_host("127.0.0.1"), "127.0.0.1");
184
+ }
165
185
  }
@@ -177,6 +177,8 @@ struct BuildLogState {
177
177
  error_log_buffer_size: usize,
178
178
  last_buildkit_vertex: Option<String>,
179
179
  last_buildkit_vertex_id: Option<String>,
180
+ export_vertex_id: Option<String>,
181
+ export_vertex_name: Option<String>,
180
182
  buildkit_logs_by_vertex_id: HashMap<String, String>,
181
183
  vertex_name_by_vertex_id: HashMap<String, String>,
182
184
  }
@@ -199,6 +201,8 @@ impl BuildLogState {
199
201
  error_log_buffer_size,
200
202
  last_buildkit_vertex: None,
201
203
  last_buildkit_vertex_id: None,
204
+ export_vertex_id: None,
205
+ export_vertex_name: None,
202
206
  buildkit_logs_by_vertex_id: HashMap::new(),
203
207
  vertex_name_by_vertex_id: HashMap::new(),
204
208
  }
@@ -255,31 +259,45 @@ fn handle_buildkit_status(
255
259
  ) {
256
260
  let latest_logs = append_buildkit_logs(&mut state.buildkit_logs_by_vertex_id, status);
257
261
  update_buildkit_vertex_names(&mut state.vertex_name_by_vertex_id, status);
258
- let (vertex_id, vertex_name) =
259
- match select_latest_buildkit_vertex(status, &state.vertex_name_by_vertex_id) {
260
- Some((vertex_id, vertex_name)) => (vertex_id, vertex_name),
261
- None => {
262
- let Some(log_entry) = latest_logs.last() else {
263
- return;
264
- };
265
- let name = state
266
- .vertex_name_by_vertex_id
267
- .get(&log_entry.vertex_id)
268
- .cloned()
269
- .or_else(|| state.last_buildkit_vertex.clone())
270
- .unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
271
- (log_entry.vertex_id.clone(), name)
272
- }
273
- };
262
+ update_export_vertex_from_logs(
263
+ &latest_logs,
264
+ &state.vertex_name_by_vertex_id,
265
+ &mut state.export_vertex_id,
266
+ &mut state.export_vertex_name,
267
+ );
268
+ let (vertex_id, vertex_name) = match select_latest_buildkit_vertex(
269
+ status,
270
+ &state.vertex_name_by_vertex_id,
271
+ state.export_vertex_id.as_deref(),
272
+ state.export_vertex_name.as_deref(),
273
+ ) {
274
+ Some((vertex_id, vertex_name)) => (vertex_id, vertex_name),
275
+ None => {
276
+ let Some(log_entry) = latest_logs.last() else {
277
+ return;
278
+ };
279
+ let name = state
280
+ .vertex_name_by_vertex_id
281
+ .get(&log_entry.vertex_id)
282
+ .cloned()
283
+ .or_else(|| state.last_buildkit_vertex.clone())
284
+ .unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
285
+ (log_entry.vertex_id.clone(), name)
286
+ }
287
+ };
274
288
  record_buildkit_logs(state, &latest_logs, &vertex_id, &vertex_name);
275
- state.last_buildkit_vertex_id = Some(vertex_id);
289
+ state.last_buildkit_vertex_id = Some(vertex_id.clone());
276
290
  if state.last_buildkit_vertex.as_deref() != Some(&vertex_name) {
277
291
  state.last_buildkit_vertex = Some(vertex_name.clone());
278
292
  }
279
293
 
280
294
  let message = if progress.is_plain_output() {
281
295
  vertex_name
282
- } else if let Some(log_entry) = latest_logs.last() {
296
+ } else if let Some(log_entry) = latest_logs
297
+ .iter()
298
+ .rev()
299
+ .find(|entry| entry.vertex_id == vertex_id)
300
+ {
283
301
  format!("{vertex_name} · {}", log_entry.message)
284
302
  } else {
285
303
  vertex_name
@@ -360,7 +378,17 @@ fn update_buildkit_vertex_names(
360
378
  fn select_latest_buildkit_vertex(
361
379
  status: &BuildkitStatusResponse,
362
380
  vertex_name_by_vertex_id: &HashMap<String, String>,
381
+ export_vertex_id: Option<&str>,
382
+ export_vertex_name: Option<&str>,
363
383
  ) -> Option<(String, String)> {
384
+ if let Some(export_vertex_id) = export_vertex_id {
385
+ let name = export_vertex_name
386
+ .map(str::to_string)
387
+ .or_else(|| vertex_name_by_vertex_id.get(export_vertex_id).cloned())
388
+ .unwrap_or_else(|| format_vertex_fallback_label(export_vertex_id));
389
+ return Some((export_vertex_id.to_string(), name));
390
+ }
391
+
364
392
  let mut best_runtime: Option<(u32, String, String)> = None;
365
393
  let mut fallback: Option<(String, String)> = None;
366
394
 
@@ -423,6 +451,24 @@ fn format_vertex_fallback_label(vertex_id: &str) -> String {
423
451
  format!("vertex {short}")
424
452
  }
425
453
 
454
+ fn update_export_vertex_from_logs(
455
+ latest_logs: &[BuildkitLogEntry],
456
+ vertex_name_by_vertex_id: &HashMap<String, String>,
457
+ export_vertex_id: &mut Option<String>,
458
+ export_vertex_name: &mut Option<String>,
459
+ ) {
460
+ if let Some(entry) = latest_logs
461
+ .iter()
462
+ .rev()
463
+ .find(|log| log.message.trim_start().starts_with("exporting to image"))
464
+ {
465
+ *export_vertex_id = Some(entry.vertex_id.clone());
466
+ if let Some(name) = vertex_name_by_vertex_id.get(&entry.vertex_id) {
467
+ *export_vertex_name = Some(name.clone());
468
+ }
469
+ }
470
+ }
471
+
426
472
  fn record_buildkit_logs(
427
473
  state: &mut BuildLogState,
428
474
  latest_logs: &[BuildkitLogEntry],
@@ -64,9 +64,9 @@ impl ParsedMount {
64
64
  /// use opencode_cloud_core::docker::ParsedMount;
65
65
  ///
66
66
  /// // Read-write mount (default)
67
- /// let mount = ParsedMount::parse("/home/user/data:/workspace/data").unwrap();
67
+ /// let mount = ParsedMount::parse("/home/user/data:/home/opencode/workspace/data").unwrap();
68
68
  /// assert_eq!(mount.host_path.to_str().unwrap(), "/home/user/data");
69
- /// assert_eq!(mount.container_path, "/workspace/data");
69
+ /// assert_eq!(mount.container_path, "/home/opencode/workspace/data");
70
70
  /// assert!(!mount.read_only);
71
71
  ///
72
72
  /// // Read-only mount
@@ -285,7 +285,7 @@ mod tests {
285
285
 
286
286
  #[test]
287
287
  fn non_system_path_no_warning() {
288
- let warning = check_container_path_warning("/workspace/data");
288
+ let warning = check_container_path_warning("/home/opencode/workspace/data");
289
289
  assert!(warning.is_none());
290
290
  }
291
291
 
@@ -37,7 +37,7 @@ pub struct UserInfo {
37
37
  ///
38
38
  /// # Example
39
39
  /// ```ignore
40
- /// create_user(&client, "opencode-cloud", "admin").await?;
40
+ /// create_user(&client, "opencode-cloud-sandbox", "admin").await?;
41
41
  /// ```
42
42
  pub async fn create_user(
43
43
  client: &DockerClient,
@@ -80,7 +80,7 @@ pub async fn create_user(
80
80
  ///
81
81
  /// # Example
82
82
  /// ```ignore
83
- /// set_user_password(&client, "opencode-cloud", "admin", "secret123").await?;
83
+ /// set_user_password(&client, "opencode-cloud-sandbox", "admin", "secret123").await?;
84
84
  /// ```
85
85
  pub async fn set_user_password(
86
86
  client: &DockerClient,
@@ -8,23 +8,23 @@ use bollard::volume::CreateVolumeOptions;
8
8
  use std::collections::HashMap;
9
9
  use tracing::debug;
10
10
 
11
- /// Volume name for opencode session history
12
- pub const VOLUME_SESSION: &str = "opencode-cloud-session";
11
+ /// Volume name for opencode data
12
+ pub const VOLUME_SESSION: &str = "opencode-data";
13
13
 
14
14
  /// Volume name for project files
15
- pub const VOLUME_PROJECTS: &str = "opencode-cloud-projects";
15
+ pub const VOLUME_PROJECTS: &str = "opencode-workspace";
16
16
 
17
17
  /// Volume name for opencode configuration
18
- pub const VOLUME_CONFIG: &str = "opencode-cloud-config";
18
+ pub const VOLUME_CONFIG: &str = "opencode-config";
19
19
 
20
20
  /// All volume names as array for iteration
21
21
  pub const VOLUME_NAMES: [&str; 3] = [VOLUME_SESSION, VOLUME_PROJECTS, VOLUME_CONFIG];
22
22
 
23
- /// Mount point for session history inside container
24
- pub const MOUNT_SESSION: &str = "/home/opencode/.opencode";
23
+ /// Mount point for opencode data inside container
24
+ pub const MOUNT_SESSION: &str = "/home/opencode/.local/share";
25
25
 
26
26
  /// Mount point for project files inside container
27
- pub const MOUNT_PROJECTS: &str = "/workspace";
27
+ pub const MOUNT_PROJECTS: &str = "/home/opencode/workspace";
28
28
 
29
29
  /// Mount point for configuration inside container
30
30
  pub const MOUNT_CONFIG: &str = "/home/opencode/.config";
@@ -122,9 +122,9 @@ mod tests {
122
122
 
123
123
  #[test]
124
124
  fn volume_constants_are_correct() {
125
- assert_eq!(VOLUME_SESSION, "opencode-cloud-session");
126
- assert_eq!(VOLUME_PROJECTS, "opencode-cloud-projects");
127
- assert_eq!(VOLUME_CONFIG, "opencode-cloud-config");
125
+ assert_eq!(VOLUME_SESSION, "opencode-data");
126
+ assert_eq!(VOLUME_PROJECTS, "opencode-workspace");
127
+ assert_eq!(VOLUME_CONFIG, "opencode-config");
128
128
  }
129
129
 
130
130
  #[test]
@@ -137,8 +137,8 @@ mod tests {
137
137
 
138
138
  #[test]
139
139
  fn mount_points_are_correct() {
140
- assert_eq!(MOUNT_SESSION, "/home/opencode/.opencode");
141
- assert_eq!(MOUNT_PROJECTS, "/workspace");
140
+ assert_eq!(MOUNT_SESSION, "/home/opencode/.local/share");
141
+ assert_eq!(MOUNT_PROJECTS, "/home/opencode/workspace");
142
142
  assert_eq!(MOUNT_CONFIG, "/home/opencode/.config");
143
143
  }
144
144
  }