@opencode-cloud/core 3.0.15 → 3.1.3
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 +2 -2
- package/README.md +33 -0
- package/package.json +1 -1
- package/src/config/schema.rs +14 -4
- package/src/docker/Dockerfile +316 -63
- package/src/docker/image.rs +415 -66
- package/src/docker/progress.rs +67 -67
package/src/docker/progress.rs
CHANGED
|
@@ -40,20 +40,6 @@ fn strip_ansi_codes(s: &str) -> String {
|
|
|
40
40
|
result
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/// Format duration as MM:SS, or HH:MM:SS if over an hour
|
|
44
|
-
fn format_elapsed(duration: Duration) -> String {
|
|
45
|
-
let total_secs = duration.as_secs();
|
|
46
|
-
let hours = total_secs / 3600;
|
|
47
|
-
let minutes = (total_secs % 3600) / 60;
|
|
48
|
-
let seconds = total_secs % 60;
|
|
49
|
-
|
|
50
|
-
if hours > 0 {
|
|
51
|
-
format!("{hours:02}:{minutes:02}:{seconds:02}")
|
|
52
|
-
} else {
|
|
53
|
-
format!("{minutes:02}:{seconds:02}")
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
43
|
/// Progress reporter for Docker operations
|
|
58
44
|
///
|
|
59
45
|
/// Manages multiple progress bars for concurrent operations like
|
|
@@ -61,11 +47,12 @@ fn format_elapsed(duration: Duration) -> String {
|
|
|
61
47
|
pub struct ProgressReporter {
|
|
62
48
|
multi: MultiProgress,
|
|
63
49
|
bars: HashMap<String, ProgressBar>,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
start_time: Instant,
|
|
50
|
+
last_update_by_id: HashMap<String, Instant>,
|
|
51
|
+
last_message_by_id: HashMap<String, String>,
|
|
67
52
|
/// Optional context prefix shown before step messages (e.g., "Building Docker image")
|
|
68
53
|
context: Option<String>,
|
|
54
|
+
/// When true, print build output lines directly instead of spinners
|
|
55
|
+
plain_output: bool,
|
|
69
56
|
}
|
|
70
57
|
|
|
71
58
|
impl Default for ProgressReporter {
|
|
@@ -80,10 +67,10 @@ impl ProgressReporter {
|
|
|
80
67
|
Self {
|
|
81
68
|
multi: MultiProgress::new(),
|
|
82
69
|
bars: HashMap::new(),
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
start_time: Instant::now(),
|
|
70
|
+
last_update_by_id: HashMap::new(),
|
|
71
|
+
last_message_by_id: HashMap::new(),
|
|
86
72
|
context: None,
|
|
73
|
+
plain_output: false,
|
|
87
74
|
}
|
|
88
75
|
}
|
|
89
76
|
|
|
@@ -94,17 +81,32 @@ impl ProgressReporter {
|
|
|
94
81
|
Self {
|
|
95
82
|
multi: MultiProgress::new(),
|
|
96
83
|
bars: HashMap::new(),
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
start_time: Instant::now(),
|
|
84
|
+
last_update_by_id: HashMap::new(),
|
|
85
|
+
last_message_by_id: HashMap::new(),
|
|
100
86
|
context: Some(context.to_string()),
|
|
87
|
+
plain_output: false,
|
|
101
88
|
}
|
|
102
89
|
}
|
|
103
90
|
|
|
91
|
+
/// Create a progress reporter that prints build output directly
|
|
92
|
+
pub fn with_context_plain(context: &str) -> Self {
|
|
93
|
+
Self {
|
|
94
|
+
multi: MultiProgress::new(),
|
|
95
|
+
bars: HashMap::new(),
|
|
96
|
+
last_update_by_id: HashMap::new(),
|
|
97
|
+
last_message_by_id: HashMap::new(),
|
|
98
|
+
context: Some(context.to_string()),
|
|
99
|
+
plain_output: true,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Check if plain output mode is enabled
|
|
104
|
+
pub fn is_plain_output(&self) -> bool {
|
|
105
|
+
self.plain_output
|
|
106
|
+
}
|
|
107
|
+
|
|
104
108
|
/// Format a message with context prefix if set
|
|
105
109
|
fn format_message(&self, message: &str) -> String {
|
|
106
|
-
let elapsed = format_elapsed(self.start_time.elapsed());
|
|
107
|
-
|
|
108
110
|
// Strip ANSI escape codes that Docker may include in its output
|
|
109
111
|
let stripped = strip_ansi_codes(message);
|
|
110
112
|
|
|
@@ -113,20 +115,25 @@ impl ProgressReporter {
|
|
|
113
115
|
// - Trim leading/trailing whitespace
|
|
114
116
|
let clean_msg = stripped.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
115
117
|
|
|
116
|
-
// Format: "
|
|
117
|
-
// Timer at the beginning for easy scanning
|
|
118
|
+
// Format: "Context · message" or "message"
|
|
118
119
|
match &self.context {
|
|
119
|
-
Some(ctx) => format!("
|
|
120
|
-
None =>
|
|
120
|
+
Some(ctx) => format!("{ctx} · {clean_msg}"),
|
|
121
|
+
None => clean_msg,
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/// Create a spinner for indeterminate progress (e.g., build steps)
|
|
125
126
|
pub fn add_spinner(&mut self, id: &str, message: &str) -> &ProgressBar {
|
|
127
|
+
if self.plain_output {
|
|
128
|
+
let spinner = ProgressBar::hidden();
|
|
129
|
+
self.bars.insert(id.to_string(), spinner);
|
|
130
|
+
return self.bars.get(id).expect("just inserted");
|
|
131
|
+
}
|
|
132
|
+
|
|
126
133
|
let spinner = self.multi.add(ProgressBar::new_spinner());
|
|
127
134
|
spinner.set_style(
|
|
128
135
|
ProgressStyle::default_spinner()
|
|
129
|
-
.template("{spinner:.green} {msg}")
|
|
136
|
+
.template("{spinner:.green} [{elapsed}] {msg}")
|
|
130
137
|
.expect("valid template")
|
|
131
138
|
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
|
|
132
139
|
);
|
|
@@ -140,6 +147,12 @@ impl ProgressReporter {
|
|
|
140
147
|
///
|
|
141
148
|
/// `total` is in bytes
|
|
142
149
|
pub fn add_bar(&mut self, id: &str, total: u64) -> &ProgressBar {
|
|
150
|
+
if self.plain_output {
|
|
151
|
+
let bar = ProgressBar::hidden();
|
|
152
|
+
self.bars.insert(id.to_string(), bar);
|
|
153
|
+
return self.bars.get(id).expect("just inserted");
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
let bar = self.multi.add(ProgressBar::new(total));
|
|
144
157
|
bar.set_style(
|
|
145
158
|
ProgressStyle::default_bar()
|
|
@@ -158,6 +171,10 @@ impl ProgressReporter {
|
|
|
158
171
|
///
|
|
159
172
|
/// `current` and `total` are in bytes, `status` is the Docker status message
|
|
160
173
|
pub fn update_layer(&mut self, layer_id: &str, current: u64, total: u64, status: &str) {
|
|
174
|
+
if self.plain_output {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
161
178
|
if let Some(bar) = self.bars.get(layer_id) {
|
|
162
179
|
// Update total if it changed (Docker sometimes updates this)
|
|
163
180
|
if bar.length() != Some(total) && total > 0 {
|
|
@@ -178,19 +195,29 @@ impl ProgressReporter {
|
|
|
178
195
|
/// Updates are throttled to prevent flickering from rapid message changes.
|
|
179
196
|
/// "Step X/Y" messages always update immediately as they indicate significant progress.
|
|
180
197
|
pub fn update_spinner(&mut self, id: &str, message: &str) {
|
|
198
|
+
if self.plain_output {
|
|
199
|
+
let clean = strip_ansi_codes(message);
|
|
200
|
+
let formatted = match &self.context {
|
|
201
|
+
Some(ctx) => format!("{ctx} · {clean}"),
|
|
202
|
+
None => clean,
|
|
203
|
+
};
|
|
204
|
+
eprintln!("{formatted}");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
let now = Instant::now();
|
|
182
209
|
let is_step_message = message.starts_with("Step ");
|
|
183
210
|
|
|
184
211
|
// Check if we should throttle this update
|
|
185
212
|
if !is_step_message {
|
|
186
|
-
if let Some(last) = self.
|
|
213
|
+
if let Some(last) = self.last_update_by_id.get(id) {
|
|
187
214
|
if now.duration_since(*last) < SPINNER_UPDATE_THROTTLE {
|
|
188
215
|
return; // Throttle: too soon since last update
|
|
189
216
|
}
|
|
190
217
|
}
|
|
191
218
|
|
|
192
219
|
// Skip if message is identical to last one
|
|
193
|
-
if let Some(last_msg) = self.
|
|
220
|
+
if let Some(last_msg) = self.last_message_by_id.get(id) {
|
|
194
221
|
if last_msg == message {
|
|
195
222
|
return;
|
|
196
223
|
}
|
|
@@ -208,8 +235,8 @@ impl ProgressReporter {
|
|
|
208
235
|
}
|
|
209
236
|
|
|
210
237
|
// Track update time and message
|
|
211
|
-
self.
|
|
212
|
-
self.
|
|
238
|
+
self.last_update_by_id.insert(id.to_string(), now);
|
|
239
|
+
self.last_message_by_id
|
|
213
240
|
.insert(id.to_string(), message.to_string());
|
|
214
241
|
}
|
|
215
242
|
|
|
@@ -300,30 +327,6 @@ mod tests {
|
|
|
300
327
|
reporter.abandon_all("Failed");
|
|
301
328
|
}
|
|
302
329
|
|
|
303
|
-
#[test]
|
|
304
|
-
fn format_elapsed_shows_seconds_only() {
|
|
305
|
-
let duration = Duration::from_secs(45);
|
|
306
|
-
assert_eq!(format_elapsed(duration), "00:45");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
#[test]
|
|
310
|
-
fn format_elapsed_shows_minutes_and_seconds() {
|
|
311
|
-
let duration = Duration::from_secs(90); // 1m 30s
|
|
312
|
-
assert_eq!(format_elapsed(duration), "01:30");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
#[test]
|
|
316
|
-
fn format_elapsed_shows_hours_when_needed() {
|
|
317
|
-
let duration = Duration::from_secs(3661); // 1h 1m 1s
|
|
318
|
-
assert_eq!(format_elapsed(duration), "01:01:01");
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
#[test]
|
|
322
|
-
fn format_elapsed_zero() {
|
|
323
|
-
let duration = Duration::from_secs(0);
|
|
324
|
-
assert_eq!(format_elapsed(duration), "00:00");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
330
|
#[test]
|
|
328
331
|
fn with_context_sets_context() {
|
|
329
332
|
let reporter = ProgressReporter::with_context("Building Docker image");
|
|
@@ -335,27 +338,24 @@ mod tests {
|
|
|
335
338
|
fn format_message_includes_context_for_steps() {
|
|
336
339
|
let reporter = ProgressReporter::with_context("Building Docker image");
|
|
337
340
|
let msg = reporter.format_message("Step 1/10 : FROM ubuntu");
|
|
338
|
-
// Format:
|
|
339
|
-
assert!(msg.
|
|
340
|
-
assert!(msg.starts_with("[00:00]"));
|
|
341
|
+
// Format: Context · message
|
|
342
|
+
assert!(msg.starts_with("Building Docker image · Step 1/10"));
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
#[test]
|
|
344
346
|
fn format_message_includes_context_for_all_messages() {
|
|
345
347
|
let reporter = ProgressReporter::with_context("Building Docker image");
|
|
346
348
|
let msg = reporter.format_message("Compiling foo v1.0");
|
|
347
|
-
// Format:
|
|
348
|
-
assert!(msg.
|
|
349
|
-
assert!(msg.starts_with("[00:00]"));
|
|
349
|
+
// Format: Context · message
|
|
350
|
+
assert!(msg.starts_with("Building Docker image · Compiling foo"));
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
#[test]
|
|
353
354
|
fn format_message_without_context() {
|
|
354
355
|
let reporter = ProgressReporter::new();
|
|
355
356
|
let msg = reporter.format_message("Step 1/10 : FROM ubuntu");
|
|
356
|
-
// Format:
|
|
357
|
-
assert!(msg.
|
|
358
|
-
assert!(msg.starts_with("[00:00]"));
|
|
357
|
+
// Format: message (no context, no dot)
|
|
358
|
+
assert!(msg.starts_with("Step 1/10"));
|
|
359
359
|
assert!(!msg.contains("·"));
|
|
360
360
|
}
|
|
361
361
|
|