@mediar-ai/terminator 0.20.6

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/src/locator.rs ADDED
@@ -0,0 +1,172 @@
1
+ use napi_derive::napi;
2
+ use terminator::locator::WaitCondition as TerminatorWaitCondition;
3
+ use terminator::Locator as TerminatorLocator;
4
+
5
+ use crate::map_error;
6
+ use crate::Element;
7
+ use crate::Selector;
8
+ use napi::bindgen_prelude::Either;
9
+
10
+ /// Locator for finding UI elements by selector.
11
+ #[napi(js_name = "Locator")]
12
+ pub struct Locator {
13
+ inner: TerminatorLocator,
14
+ }
15
+
16
+ impl std::fmt::Display for Locator {
17
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18
+ write!(f, "Locator({})", self.inner.selector_string())
19
+ }
20
+ }
21
+
22
+ impl From<TerminatorLocator> for Locator {
23
+ fn from(l: TerminatorLocator) -> Self {
24
+ Locator { inner: l }
25
+ }
26
+ }
27
+
28
+ #[napi]
29
+ impl Locator {
30
+ /// (async) Get the first matching element.
31
+ ///
32
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
33
+ /// @returns {Promise<Element>} The first matching element.
34
+ #[napi]
35
+ pub async fn first(&self, timeout_ms: f64) -> napi::Result<Element> {
36
+ use std::time::Duration;
37
+ let timeout = Duration::from_millis(timeout_ms as u64);
38
+ self.inner
39
+ .first(Some(timeout))
40
+ .await
41
+ .map(Element::from)
42
+ .map_err(map_error)
43
+ }
44
+
45
+ /// (async) Get all matching elements.
46
+ ///
47
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
48
+ /// @param {number} [depth] - Maximum depth to search.
49
+ /// @returns {Promise<Array<Element>>} List of matching elements.
50
+ #[napi]
51
+ pub async fn all(&self, timeout_ms: f64, depth: Option<u32>) -> napi::Result<Vec<Element>> {
52
+ use std::time::Duration;
53
+ let timeout = Duration::from_millis(timeout_ms as u64);
54
+ let depth = depth.map(|d| d as usize);
55
+ self.inner
56
+ .all(Some(timeout), depth)
57
+ .await
58
+ .map(|els| els.into_iter().map(Element::from).collect())
59
+ .map_err(map_error)
60
+ }
61
+
62
+ /// Set a default timeout for this locator.
63
+ ///
64
+ /// @param {number} timeoutMs - Timeout in milliseconds.
65
+ /// @returns {Locator} A new locator with the specified timeout.
66
+ #[napi]
67
+ pub fn timeout(&self, timeout_ms: f64) -> Locator {
68
+ let loc = self
69
+ .inner
70
+ .clone()
71
+ .set_default_timeout(std::time::Duration::from_millis(timeout_ms as u64));
72
+ Locator::from(loc)
73
+ }
74
+
75
+ /// Set the root element for this locator.
76
+ ///
77
+ /// @param {Element} element - The root element.
78
+ /// @returns {Locator} A new locator with the specified root element.
79
+ #[napi]
80
+ pub fn within(&self, element: &Element) -> Locator {
81
+ let loc = self.inner.clone().within(element.inner.clone());
82
+ Locator::from(loc)
83
+ }
84
+
85
+ /// Chain another selector.
86
+ /// Accepts either a selector string or a Selector object.
87
+ ///
88
+ /// @param {string | Selector} selector - The selector.
89
+ /// @returns {Locator} A new locator with the chained selector.
90
+ #[napi]
91
+ pub fn locator(
92
+ &self,
93
+ #[napi(ts_arg_type = "string | Selector")] selector: Either<String, &Selector>,
94
+ ) -> napi::Result<Locator> {
95
+ use napi::bindgen_prelude::Either::*;
96
+ let sel_rust: terminator::selector::Selector = match selector {
97
+ A(sel_str) => sel_str.as_str().into(),
98
+ B(sel_obj) => sel_obj.inner.clone(),
99
+ };
100
+ let loc = self.inner.clone().locator(sel_rust);
101
+ Ok(Locator::from(loc))
102
+ }
103
+
104
+ /// (async) Validate element existence without throwing an error.
105
+ ///
106
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
107
+ /// @returns {Promise<ValidationResult>} Validation result with exists flag and optional element.
108
+ #[napi]
109
+ pub async fn validate(&self, timeout_ms: f64) -> napi::Result<ValidationResult> {
110
+ use std::time::Duration;
111
+ let timeout = Duration::from_millis(timeout_ms as u64);
112
+ match self.inner.validate(Some(timeout)).await {
113
+ Ok(Some(element)) => Ok(ValidationResult {
114
+ exists: true,
115
+ element: Some(Element::from(element)),
116
+ error: None,
117
+ }),
118
+ Ok(None) => Ok(ValidationResult {
119
+ exists: false,
120
+ element: None,
121
+ error: None,
122
+ }),
123
+ Err(e) => Ok(ValidationResult {
124
+ exists: false,
125
+ element: None,
126
+ error: Some(e.to_string()),
127
+ }),
128
+ }
129
+ }
130
+
131
+ /// (async) Wait for an element to meet a specific condition.
132
+ ///
133
+ /// @param {string} condition - Condition to wait for: 'exists', 'visible', 'enabled', 'focused'
134
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
135
+ /// @returns {Promise<Element>} The element when condition is met.
136
+ #[napi]
137
+ pub async fn wait_for(&self, condition: String, timeout_ms: f64) -> napi::Result<Element> {
138
+ use std::time::Duration;
139
+ let wait_condition = parse_condition(&condition)?;
140
+ let timeout = Duration::from_millis(timeout_ms as u64);
141
+
142
+ self.inner
143
+ .wait_for(wait_condition, Some(timeout))
144
+ .await
145
+ .map(Element::from)
146
+ .map_err(map_error)
147
+ }
148
+ }
149
+
150
+ /// Result of element validation
151
+ #[napi(object)]
152
+ pub struct ValidationResult {
153
+ /// Whether the element exists
154
+ pub exists: bool,
155
+ /// The element if found
156
+ pub element: Option<Element>,
157
+ /// Error message if validation failed (not element not found, but actual error)
158
+ pub error: Option<String>,
159
+ }
160
+ /// Convert string condition to WaitCondition enum
161
+ fn parse_condition(condition: &str) -> napi::Result<TerminatorWaitCondition> {
162
+ match condition.to_lowercase().as_str() {
163
+ "exists" => Ok(TerminatorWaitCondition::Exists),
164
+ "visible" => Ok(TerminatorWaitCondition::Visible),
165
+ "enabled" => Ok(TerminatorWaitCondition::Enabled),
166
+ "focused" => Ok(TerminatorWaitCondition::Focused),
167
+ _ => Err(napi::Error::new(
168
+ napi::Status::InvalidArg,
169
+ format!("Invalid condition '{condition}'. Valid: exists, visible, enabled, focused"),
170
+ )),
171
+ }
172
+ }
@@ -0,0 +1,139 @@
1
+ use napi::bindgen_prelude::FromNapiValue;
2
+ use napi_derive::napi;
3
+ use std::collections::BTreeMap;
4
+ use terminator::selector::Selector as TerminatorSelector;
5
+
6
+ /// Selector for locating UI elements. Provides a typed alternative to the string based selector API.
7
+ #[napi(js_name = "Selector")]
8
+ pub struct Selector {
9
+ pub(crate) inner: TerminatorSelector,
10
+ }
11
+
12
+ impl std::fmt::Display for Selector {
13
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14
+ write!(f, "{:?}", self.inner)
15
+ }
16
+ }
17
+
18
+ impl From<TerminatorSelector> for Selector {
19
+ fn from(sel: TerminatorSelector) -> Self {
20
+ Selector { inner: sel }
21
+ }
22
+ }
23
+
24
+ impl From<&Selector> for TerminatorSelector {
25
+ fn from(sel: &Selector) -> Self {
26
+ sel.inner.clone()
27
+ }
28
+ }
29
+
30
+ impl FromNapiValue for Selector {
31
+ unsafe fn from_napi_value(
32
+ env: napi::sys::napi_env,
33
+ napi_val: napi::sys::napi_value,
34
+ ) -> napi::Result<Self> {
35
+ let mut result = std::ptr::null_mut();
36
+ let status = napi::sys::napi_get_value_external(env, napi_val, &mut result);
37
+ if status != napi::sys::Status::napi_ok {
38
+ return Err(napi::Error::new(
39
+ napi::Status::InvalidArg,
40
+ "Failed to get external value for Selector".to_string(),
41
+ ));
42
+ }
43
+ Ok(std::ptr::read(result as *const Selector))
44
+ }
45
+ }
46
+
47
+ #[napi]
48
+ impl Selector {
49
+ /// Create a selector that matches elements by their accessibility `name`.
50
+ #[napi(factory)]
51
+ pub fn name(name: String) -> Self {
52
+ Selector::from(TerminatorSelector::Name(name))
53
+ }
54
+
55
+ /// Create a selector that matches elements by role (and optionally name).
56
+ #[napi(factory)]
57
+ pub fn role(role: String, name: Option<String>) -> Self {
58
+ Selector::from(TerminatorSelector::Role { role, name })
59
+ }
60
+
61
+ /// Create a selector that matches elements by accessibility `id`.
62
+ #[napi(factory, js_name = "id")]
63
+ pub fn id_factory(id: String) -> Self {
64
+ Selector::from(TerminatorSelector::Id(id))
65
+ }
66
+
67
+ /// Create a selector that matches elements by the text they display.
68
+ #[napi(factory)]
69
+ pub fn text(text: String) -> Self {
70
+ Selector::from(TerminatorSelector::Text(text))
71
+ }
72
+
73
+ /// Create a selector from an XPath-like path string.
74
+ #[napi(factory)]
75
+ pub fn path(path: String) -> Self {
76
+ Selector::from(TerminatorSelector::Path(path))
77
+ }
78
+
79
+ /// Create a selector that matches elements by a native automation id (e.g., AutomationID on Windows).
80
+ #[napi(factory, js_name = "nativeId")]
81
+ pub fn native_id(id: String) -> Self {
82
+ Selector::from(TerminatorSelector::NativeId(id))
83
+ }
84
+
85
+ /// Create a selector that matches elements by their class name.
86
+ #[napi(factory, js_name = "className")]
87
+ pub fn class_name(name: String) -> Self {
88
+ Selector::from(TerminatorSelector::ClassName(name))
89
+ }
90
+
91
+ /// Create a selector from an arbitrary attribute map.
92
+ #[napi(factory)]
93
+ pub fn attributes(attributes: std::collections::HashMap<String, String>) -> Self {
94
+ let map: BTreeMap<String, String> = attributes.into_iter().collect();
95
+ Selector::from(TerminatorSelector::Attributes(map))
96
+ }
97
+
98
+ /// Chain another selector onto this selector.
99
+ #[napi]
100
+ pub fn chain(&self, other: &Selector) -> Selector {
101
+ Selector::from(TerminatorSelector::Chain(vec![
102
+ self.inner.clone(),
103
+ other.inner.clone(),
104
+ ]))
105
+ }
106
+
107
+ /// Filter by visibility.
108
+ #[napi]
109
+ pub fn visible(&self, is_visible: bool) -> Selector {
110
+ Selector::from(TerminatorSelector::Chain(vec![
111
+ self.inner.clone(),
112
+ TerminatorSelector::Visible(is_visible),
113
+ ]))
114
+ }
115
+
116
+ /// Create a selector that selects the nth element from matches.
117
+ /// Positive values are 0-based from the start (0 = first, 1 = second).
118
+ /// Negative values are from the end (-1 = last, -2 = second-to-last).
119
+ #[napi(factory)]
120
+ pub fn nth(index: i32) -> Self {
121
+ Selector::from(TerminatorSelector::Nth(index))
122
+ }
123
+
124
+ /// Create a selector that matches elements having at least one descendant matching the inner selector.
125
+ /// This is similar to Playwright's :has() pseudo-class.
126
+ #[napi(factory)]
127
+ pub fn has(inner_selector: &Selector) -> Self {
128
+ Selector::from(TerminatorSelector::Has(Box::new(
129
+ inner_selector.inner.clone(),
130
+ )))
131
+ }
132
+
133
+ /// Create a selector that navigates to the parent element.
134
+ /// This is similar to Playwright's .. syntax.
135
+ #[napi(factory)]
136
+ pub fn parent() -> Self {
137
+ Selector::from(TerminatorSelector::Parent)
138
+ }
139
+ }
package/src/types.rs ADDED
@@ -0,0 +1,308 @@
1
+ use napi_derive::napi;
2
+ use std::collections::HashMap;
3
+
4
+ #[napi(object, js_name = "Bounds")]
5
+ pub struct Bounds {
6
+ pub x: f64,
7
+ pub y: f64,
8
+ pub width: f64,
9
+ pub height: f64,
10
+ }
11
+
12
+ #[napi(object, js_name = "Coordinates")]
13
+ pub struct Coordinates {
14
+ pub x: f64,
15
+ pub y: f64,
16
+ }
17
+
18
+ #[napi(object, js_name = "ClickResult")]
19
+ pub struct ClickResult {
20
+ pub method: String,
21
+ pub coordinates: Option<Coordinates>,
22
+ pub details: String,
23
+ }
24
+
25
+ #[napi(object, js_name = "CommandOutput")]
26
+ pub struct CommandOutput {
27
+ pub exit_status: Option<i32>,
28
+ pub stdout: String,
29
+ pub stderr: String,
30
+ }
31
+
32
+ #[napi(object)]
33
+ pub struct Monitor {
34
+ pub id: String,
35
+ pub name: String,
36
+ pub is_primary: bool,
37
+ pub width: u32,
38
+ pub height: u32,
39
+ pub x: i32,
40
+ pub y: i32,
41
+ pub scale_factor: f64,
42
+ }
43
+
44
+ #[napi(object)]
45
+ pub struct MonitorScreenshotPair {
46
+ pub monitor: Monitor,
47
+ pub screenshot: ScreenshotResult,
48
+ }
49
+
50
+ #[napi(object)]
51
+ pub struct ScreenshotResult {
52
+ pub width: u32,
53
+ pub height: u32,
54
+ pub image_data: Vec<u8>,
55
+ pub monitor: Option<Monitor>,
56
+ }
57
+
58
+ #[napi(object, js_name = "UIElementAttributes")]
59
+ pub struct UIElementAttributes {
60
+ pub role: String,
61
+ pub name: Option<String>,
62
+ pub label: Option<String>,
63
+ pub value: Option<String>,
64
+ pub description: Option<String>,
65
+ pub properties: HashMap<String, Option<String>>,
66
+ pub is_keyboard_focusable: Option<bool>,
67
+ pub bounds: Option<Bounds>,
68
+ }
69
+
70
+ #[napi(object, js_name = "UINode")]
71
+ pub struct UINode {
72
+ pub id: Option<String>,
73
+ pub attributes: UIElementAttributes,
74
+ pub children: Vec<UINode>,
75
+ }
76
+
77
+ #[napi(string_enum)]
78
+ pub enum PropertyLoadingMode {
79
+ /// Only load essential properties (role + name) - fastest
80
+ Fast,
81
+ /// Load all properties for complete element data - slower but comprehensive
82
+ Complete,
83
+ /// Load specific properties based on element type - balanced approach
84
+ Smart,
85
+ }
86
+
87
+ #[napi(object, js_name = "TreeBuildConfig")]
88
+ pub struct TreeBuildConfig {
89
+ /// Property loading strategy
90
+ pub property_mode: PropertyLoadingMode,
91
+ /// Optional timeout per operation in milliseconds
92
+ pub timeout_per_operation_ms: Option<i64>,
93
+ /// Optional yield frequency for responsiveness
94
+ pub yield_every_n_elements: Option<i32>,
95
+ /// Optional batch size for processing elements
96
+ pub batch_size: Option<i32>,
97
+ }
98
+
99
+ impl From<(f64, f64, f64, f64)> for Bounds {
100
+ fn from(t: (f64, f64, f64, f64)) -> Self {
101
+ Bounds {
102
+ x: t.0,
103
+ y: t.1,
104
+ width: t.2,
105
+ height: t.3,
106
+ }
107
+ }
108
+ }
109
+
110
+ impl From<(f64, f64)> for Coordinates {
111
+ fn from(t: (f64, f64)) -> Self {
112
+ Coordinates { x: t.0, y: t.1 }
113
+ }
114
+ }
115
+
116
+ impl From<terminator::ClickResult> for ClickResult {
117
+ fn from(r: terminator::ClickResult) -> Self {
118
+ ClickResult {
119
+ method: r.method,
120
+ coordinates: r.coordinates.map(Coordinates::from),
121
+ details: r.details,
122
+ }
123
+ }
124
+ }
125
+
126
+ impl From<terminator::Monitor> for Monitor {
127
+ fn from(m: terminator::Monitor) -> Self {
128
+ Monitor {
129
+ id: m.id,
130
+ name: m.name,
131
+ is_primary: m.is_primary,
132
+ width: m.width,
133
+ height: m.height,
134
+ x: m.x,
135
+ y: m.y,
136
+ scale_factor: m.scale_factor,
137
+ }
138
+ }
139
+ }
140
+
141
+ impl From<terminator::UINode> for UINode {
142
+ fn from(node: terminator::UINode) -> Self {
143
+ UINode {
144
+ id: node.id,
145
+ attributes: UIElementAttributes::from(node.attributes),
146
+ children: node.children.into_iter().map(UINode::from).collect(),
147
+ }
148
+ }
149
+ }
150
+
151
+ impl From<terminator::UIElementAttributes> for UIElementAttributes {
152
+ fn from(attrs: terminator::UIElementAttributes) -> Self {
153
+ // Convert HashMap<String, Option<serde_json::Value>> to HashMap<String, Option<String>>
154
+ let properties = attrs
155
+ .properties
156
+ .into_iter()
157
+ .map(|(k, v)| (k, v.map(|val| val.to_string())))
158
+ .collect();
159
+
160
+ UIElementAttributes {
161
+ role: attrs.role,
162
+ name: attrs.name,
163
+ label: attrs.label,
164
+ value: attrs.value,
165
+ description: attrs.description,
166
+ properties,
167
+ is_keyboard_focusable: attrs.is_keyboard_focusable,
168
+ bounds: attrs.bounds.map(|(x, y, width, height)| Bounds {
169
+ x,
170
+ y,
171
+ width,
172
+ height,
173
+ }),
174
+ }
175
+ }
176
+ }
177
+
178
+ #[napi(string_enum)]
179
+ pub enum TextPosition {
180
+ Top,
181
+ TopRight,
182
+ Right,
183
+ BottomRight,
184
+ Bottom,
185
+ BottomLeft,
186
+ Left,
187
+ TopLeft,
188
+ Inside,
189
+ }
190
+
191
+ #[napi(object)]
192
+ pub struct FontStyle {
193
+ pub size: u32,
194
+ pub bold: bool,
195
+ pub color: u32,
196
+ }
197
+
198
+ #[napi]
199
+ pub struct HighlightHandle {
200
+ inner: Option<terminator::HighlightHandle>,
201
+ }
202
+
203
+ #[napi]
204
+ impl HighlightHandle {
205
+ #[napi]
206
+ pub fn close(&mut self) {
207
+ if let Some(handle) = self.inner.take() {
208
+ handle.close();
209
+ }
210
+ }
211
+ }
212
+
213
+ impl HighlightHandle {
214
+ pub fn new(handle: terminator::HighlightHandle) -> Self {
215
+ Self {
216
+ inner: Some(handle),
217
+ }
218
+ }
219
+
220
+ pub fn new_dummy() -> Self {
221
+ Self { inner: None }
222
+ }
223
+ }
224
+
225
+ impl From<TextPosition> for terminator::TextPosition {
226
+ fn from(pos: TextPosition) -> Self {
227
+ match pos {
228
+ TextPosition::Top => terminator::TextPosition::Top,
229
+ TextPosition::TopRight => terminator::TextPosition::TopRight,
230
+ TextPosition::Right => terminator::TextPosition::Right,
231
+ TextPosition::BottomRight => terminator::TextPosition::BottomRight,
232
+ TextPosition::Bottom => terminator::TextPosition::Bottom,
233
+ TextPosition::BottomLeft => terminator::TextPosition::BottomLeft,
234
+ TextPosition::Left => terminator::TextPosition::Left,
235
+ TextPosition::TopLeft => terminator::TextPosition::TopLeft,
236
+ TextPosition::Inside => terminator::TextPosition::Inside,
237
+ }
238
+ }
239
+ }
240
+
241
+ impl From<FontStyle> for terminator::FontStyle {
242
+ fn from(style: FontStyle) -> Self {
243
+ terminator::FontStyle {
244
+ size: style.size,
245
+ bold: style.bold,
246
+ color: style.color,
247
+ }
248
+ }
249
+ }
250
+
251
+ impl Default for FontStyle {
252
+ fn default() -> Self {
253
+ Self {
254
+ size: 12,
255
+ bold: false,
256
+ color: 0x000000,
257
+ }
258
+ }
259
+ }
260
+
261
+ impl From<TreeBuildConfig> for terminator::platforms::TreeBuildConfig {
262
+ fn from(config: TreeBuildConfig) -> Self {
263
+ terminator::platforms::TreeBuildConfig {
264
+ property_mode: match config.property_mode {
265
+ PropertyLoadingMode::Fast => terminator::platforms::PropertyLoadingMode::Fast,
266
+ PropertyLoadingMode::Complete => {
267
+ terminator::platforms::PropertyLoadingMode::Complete
268
+ }
269
+ PropertyLoadingMode::Smart => terminator::platforms::PropertyLoadingMode::Smart,
270
+ },
271
+ timeout_per_operation_ms: config.timeout_per_operation_ms.map(|x| x as u64),
272
+ yield_every_n_elements: config.yield_every_n_elements.map(|x| x as usize),
273
+ batch_size: config.batch_size.map(|x| x as usize),
274
+ max_depth: None, // Not exposed in nodejs bindings yet
275
+ }
276
+ }
277
+ }
278
+
279
+ /// Convert SerializableUIElement to UINode
280
+ pub(crate) fn serializable_to_ui_node(elem: &terminator::SerializableUIElement) -> UINode {
281
+ let attrs = UIElementAttributes {
282
+ role: elem.role.clone(),
283
+ name: elem.name.clone(),
284
+ label: elem.label.clone(),
285
+ value: elem.value.clone(),
286
+ description: elem.description.clone(),
287
+ properties: HashMap::new(), // SerializableUIElement doesn't have properties field
288
+ is_keyboard_focusable: elem.is_keyboard_focusable,
289
+ bounds: elem.bounds.map(|(x, y, w, h)| Bounds {
290
+ x,
291
+ y,
292
+ width: w,
293
+ height: h,
294
+ }),
295
+ };
296
+
297
+ let children = elem
298
+ .children
299
+ .as_ref()
300
+ .map(|children| children.iter().map(serializable_to_ui_node).collect())
301
+ .unwrap_or_default();
302
+
303
+ UINode {
304
+ id: elem.id.clone(),
305
+ attributes: attrs,
306
+ children,
307
+ }
308
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Read the workspace Cargo.toml
7
+ const workspaceCargoPath = path.join(__dirname, '../../Cargo.toml');
8
+ const cargoContent = fs.readFileSync(workspaceCargoPath, 'utf8');
9
+
10
+ // Extract version from Cargo.toml
11
+ const versionMatch = cargoContent.match(/^version\s*=\s*"([^"]+)"/m);
12
+ if (!versionMatch) {
13
+ console.error('Could not find version in workspace Cargo.toml');
14
+ process.exit(1);
15
+ }
16
+
17
+ const version = versionMatch[1];
18
+ console.log(`Found version: ${version}`);
19
+
20
+ // Read package.json
21
+ const packagePath = path.join(__dirname, 'package.json');
22
+ const packageContent = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
23
+
24
+ // Update version and optionalDependencies
25
+ packageContent.version = version;
26
+
27
+ // Update optionalDependencies to use the same version
28
+ if (packageContent.optionalDependencies) {
29
+ for (const dep in packageContent.optionalDependencies) {
30
+ if (dep.startsWith('@mediar-ai/terminator-')) {
31
+ packageContent.optionalDependencies[dep] = version;
32
+ }
33
+ }
34
+ }
35
+
36
+ // Write back to package.json
37
+ fs.writeFileSync(packagePath, JSON.stringify(packageContent, null, 2) + '\n');
38
+
39
+ console.log(`Updated package.json version to: ${version}`);
40
+
41
+ // Also update platform packages
42
+ const npmDir = path.join(__dirname, 'npm');
43
+ if (fs.existsSync(npmDir)) {
44
+ const platforms = fs.readdirSync(npmDir);
45
+ for (const platform of platforms) {
46
+ const platformPath = path.join(npmDir, platform);
47
+ const platformPackagePath = path.join(platformPath, 'package.json');
48
+
49
+ if (fs.existsSync(platformPackagePath) && fs.statSync(platformPath).isDirectory()) {
50
+ try {
51
+ const platformPackage = JSON.parse(fs.readFileSync(platformPackagePath, 'utf8'));
52
+ platformPackage.version = version;
53
+ fs.writeFileSync(platformPackagePath, JSON.stringify(platformPackage, null, 2) + '\n');
54
+ console.log(`Updated ${platform} package version to: ${version}`);
55
+ } catch (error) {
56
+ console.warn(`Failed to update ${platform} package:`, error.message);
57
+ }
58
+ }
59
+ }
60
+ }