rclnodejs 1.5.2 → 1.7.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.
Files changed (59) hide show
  1. package/index.js +79 -3
  2. package/lib/action/client.js +55 -9
  3. package/lib/action/deferred.js +8 -2
  4. package/lib/action/server.js +10 -1
  5. package/lib/action/uuid.js +4 -1
  6. package/lib/client.js +152 -3
  7. package/lib/clock.js +4 -1
  8. package/lib/context.js +12 -2
  9. package/lib/duration.js +37 -12
  10. package/lib/errors.js +571 -0
  11. package/lib/event_handler.js +21 -4
  12. package/lib/interface_loader.js +52 -12
  13. package/lib/lifecycle.js +8 -2
  14. package/lib/logging.js +12 -3
  15. package/lib/message_serialization.js +179 -0
  16. package/lib/native_loader.js +9 -4
  17. package/lib/node.js +283 -47
  18. package/lib/parameter.js +176 -45
  19. package/lib/parameter_client.js +506 -0
  20. package/lib/parameter_watcher.js +309 -0
  21. package/lib/qos.js +22 -5
  22. package/lib/rate.js +6 -1
  23. package/lib/serialization.js +7 -2
  24. package/lib/subscription.js +16 -1
  25. package/lib/time.js +136 -21
  26. package/lib/time_source.js +13 -4
  27. package/lib/utils.js +313 -0
  28. package/lib/validator.js +11 -12
  29. package/package.json +2 -7
  30. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  31. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  32. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  33. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  34. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  35. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  36. package/rosidl_convertor/idl_convertor.js +3 -2
  37. package/rosidl_gen/generate_worker.js +1 -1
  38. package/rosidl_gen/idl_generator.js +11 -24
  39. package/rosidl_gen/index.js +1 -1
  40. package/rosidl_gen/templates/action-template.js +68 -0
  41. package/rosidl_gen/templates/message-template.js +1113 -0
  42. package/rosidl_gen/templates/service-event-template.js +31 -0
  43. package/rosidl_gen/templates/service-template.js +44 -0
  44. package/rosidl_parser/rosidl_parser.js +2 -2
  45. package/third_party/ref-napi/lib/ref.js +0 -45
  46. package/types/base.d.ts +3 -0
  47. package/types/client.d.ts +36 -0
  48. package/types/errors.d.ts +447 -0
  49. package/types/index.d.ts +17 -0
  50. package/types/interfaces.d.ts +1910 -1
  51. package/types/node.d.ts +56 -1
  52. package/types/parameter_client.d.ts +252 -0
  53. package/types/parameter_watcher.d.ts +104 -0
  54. package/rosidl_gen/templates/CMakeLists.dot +0 -40
  55. package/rosidl_gen/templates/action.dot +0 -50
  56. package/rosidl_gen/templates/message.dot +0 -851
  57. package/rosidl_gen/templates/package.dot +0 -16
  58. package/rosidl_gen/templates/service.dot +0 -26
  59. package/rosidl_gen/templates/service_event.dot +0 -10
package/lib/time.js CHANGED
@@ -17,6 +17,7 @@
17
17
  const rclnodejs = require('./native_loader.js');
18
18
  const Duration = require('./duration.js');
19
19
  const ClockType = require('./clock_type.js');
20
+ const { TypeValidationError, RangeValidationError } = require('./errors.js');
20
21
  const S_TO_NS = 10n ** 9n;
21
22
 
22
23
  /**
@@ -36,29 +37,49 @@ class Time {
36
37
  clockType = ClockType.SYSTEM_TIME
37
38
  ) {
38
39
  if (typeof seconds !== 'bigint') {
39
- throw new TypeError('Invalid argument of seconds');
40
+ throw new TypeValidationError('seconds', seconds, 'bigint', {
41
+ entityType: 'time',
42
+ });
40
43
  }
41
44
 
42
45
  if (typeof nanoseconds !== 'bigint') {
43
- throw new TypeError('Invalid argument of nanoseconds');
46
+ throw new TypeValidationError('nanoseconds', nanoseconds, 'bigint', {
47
+ entityType: 'time',
48
+ });
44
49
  }
45
50
 
46
51
  if (typeof clockType !== 'number') {
47
- throw new TypeError('Invalid argument of clockType');
52
+ throw new TypeValidationError('clockType', clockType, 'number', {
53
+ entityType: 'time',
54
+ });
48
55
  }
49
56
 
50
57
  if (seconds < 0n) {
51
- throw new RangeError('seconds value must not be negative');
58
+ throw new RangeValidationError('seconds', seconds, '>= 0', {
59
+ entityType: 'time',
60
+ });
52
61
  }
53
62
 
54
63
  if (nanoseconds < 0n) {
55
- throw new RangeError('nanoseconds value must not be negative');
64
+ throw new RangeValidationError('nanoseconds', nanoseconds, '>= 0', {
65
+ entityType: 'time',
66
+ });
56
67
  }
57
68
 
58
69
  const total = seconds * S_TO_NS + nanoseconds;
59
70
  if (total >= 2n ** 63n) {
60
- throw new RangeError(
61
- 'Total nanoseconds value is too large to store in C time point.'
71
+ throw new RangeValidationError(
72
+ 'total nanoseconds',
73
+ total,
74
+ '< 2^63 (max C time point)',
75
+ {
76
+ entityType: 'time',
77
+ details: {
78
+ seconds: seconds,
79
+ nanoseconds: nanoseconds,
80
+ total: total,
81
+ },
82
+ }
62
83
  );
63
84
  }
64
85
  this._nanoseconds = total;
@@ -116,7 +137,9 @@ class Time {
116
137
  this._clockType
117
138
  );
118
139
  }
119
- throw new TypeError('Invalid argument');
140
+ throw new TypeValidationError('other', other, 'Duration', {
141
+ entityType: 'time',
142
+ });
120
143
  }
121
144
 
122
145
  /**
@@ -127,7 +150,18 @@ class Time {
127
150
  sub(other) {
128
151
  if (other instanceof Time) {
129
152
  if (other._clockType !== this._clockType) {
130
- throw new TypeError("Can't subtract times with different clock types");
153
+ throw new TypeValidationError(
154
+ 'other',
155
+ other,
156
+ `Time with clock type ${this._clockType}`,
157
+ {
158
+ entityType: 'time',
159
+ details: {
160
+ expectedClockType: this._clockType,
161
+ providedClockType: other._clockType,
162
+ },
163
+ }
164
+ );
131
165
  }
132
166
  return new Duration(0n, this._nanoseconds - other._nanoseconds);
133
167
  } else if (other instanceof Duration) {
@@ -137,7 +171,9 @@ class Time {
137
171
  this._clockType
138
172
  );
139
173
  }
140
- throw new TypeError('Invalid argument');
174
+ throw new TypeValidationError('other', other, 'Time or Duration', {
175
+ entityType: 'time',
176
+ });
141
177
  }
142
178
 
143
179
  /**
@@ -148,11 +184,24 @@ class Time {
148
184
  eq(other) {
149
185
  if (other instanceof Time) {
150
186
  if (other._clockType !== this._clockType) {
151
- throw new TypeError("Can't compare times with different clock types");
187
+ throw new TypeValidationError(
188
+ 'other',
189
+ other,
190
+ `Time with clock type ${this._clockType}`,
191
+ {
192
+ entityType: 'time',
193
+ details: {
194
+ expectedClockType: this._clockType,
195
+ providedClockType: other._clockType,
196
+ },
197
+ }
198
+ );
152
199
  }
153
200
  return this._nanoseconds === other.nanoseconds;
154
201
  }
155
- throw new TypeError('Invalid argument');
202
+ throw new TypeValidationError('other', other, 'Time', {
203
+ entityType: 'time',
204
+ });
156
205
  }
157
206
 
158
207
  /**
@@ -163,10 +212,24 @@ class Time {
163
212
  ne(other) {
164
213
  if (other instanceof Time) {
165
214
  if (other._clockType !== this._clockType) {
166
- throw new TypeError("Can't compare times with different clock types");
215
+ throw new TypeValidationError(
216
+ 'other',
217
+ other,
218
+ `Time with clock type ${this._clockType}`,
219
+ {
220
+ entityType: 'time',
221
+ details: {
222
+ expectedClockType: this._clockType,
223
+ providedClockType: other._clockType,
224
+ },
225
+ }
226
+ );
167
227
  }
168
228
  return this._nanoseconds !== other.nanoseconds;
169
229
  }
230
+ throw new TypeValidationError('other', other, 'Time', {
231
+ entityType: 'time',
232
+ });
170
233
  }
171
234
 
172
235
  /**
@@ -177,11 +240,24 @@ class Time {
177
240
  lt(other) {
178
241
  if (other instanceof Time) {
179
242
  if (other._clockType !== this._clockType) {
180
- throw new TypeError("Can't compare times with different clock types");
243
+ throw new TypeValidationError(
244
+ 'other',
245
+ other,
246
+ `Time with clock type ${this._clockType}`,
247
+ {
248
+ entityType: 'time',
249
+ details: {
250
+ expectedClockType: this._clockType,
251
+ providedClockType: other._clockType,
252
+ },
253
+ }
254
+ );
181
255
  }
182
256
  return this._nanoseconds < other.nanoseconds;
183
257
  }
184
- throw new TypeError('Invalid argument');
258
+ throw new TypeValidationError('other', other, 'Time', {
259
+ entityType: 'time',
260
+ });
185
261
  }
186
262
 
187
263
  /**
@@ -192,11 +268,24 @@ class Time {
192
268
  lte(other) {
193
269
  if (other instanceof Time) {
194
270
  if (other._clockType !== this._clockType) {
195
- throw new TypeError("Can't compare times with different clock types");
271
+ throw new TypeValidationError(
272
+ 'other',
273
+ other,
274
+ `Time with clock type ${this._clockType}`,
275
+ {
276
+ entityType: 'time',
277
+ details: {
278
+ expectedClockType: this._clockType,
279
+ providedClockType: other._clockType,
280
+ },
281
+ }
282
+ );
196
283
  }
197
284
  return this._nanoseconds <= other.nanoseconds;
198
285
  }
199
- throw new TypeError('Invalid argument');
286
+ throw new TypeValidationError('other', other, 'Time', {
287
+ entityType: 'time',
288
+ });
200
289
  }
201
290
 
202
291
  /**
@@ -207,11 +296,24 @@ class Time {
207
296
  gt(other) {
208
297
  if (other instanceof Time) {
209
298
  if (other._clockType !== this._clockType) {
210
- throw new TypeError("Can't compare times with different clock types");
299
+ throw new TypeValidationError(
300
+ 'other',
301
+ other,
302
+ `Time with clock type ${this._clockType}`,
303
+ {
304
+ entityType: 'time',
305
+ details: {
306
+ expectedClockType: this._clockType,
307
+ providedClockType: other._clockType,
308
+ },
309
+ }
310
+ );
211
311
  }
212
312
  return this._nanoseconds > other.nanoseconds;
213
313
  }
214
- throw new TypeError('Invalid argument');
314
+ throw new TypeValidationError('other', other, 'Time', {
315
+ entityType: 'time',
316
+ });
215
317
  }
216
318
 
217
319
  /**
@@ -222,11 +324,24 @@ class Time {
222
324
  gte(other) {
223
325
  if (other instanceof Time) {
224
326
  if (other._clockType !== this._clockType) {
225
- throw new TypeError("Can't compare times with different clock types");
327
+ throw new TypeValidationError(
328
+ 'other',
329
+ other,
330
+ `Time with clock type ${this._clockType}`,
331
+ {
332
+ entityType: 'time',
333
+ details: {
334
+ expectedClockType: this._clockType,
335
+ providedClockType: other._clockType,
336
+ },
337
+ }
338
+ );
226
339
  }
227
340
  return this._nanoseconds >= other.nanoseconds;
228
341
  }
229
- throw new TypeError('Invalid argument');
342
+ throw new TypeValidationError('other', other, 'Time', {
343
+ entityType: 'time',
344
+ });
230
345
  }
231
346
 
232
347
  /**
@@ -19,6 +19,7 @@ const { Clock, ROSClock } = require('./clock.js');
19
19
  const { ClockType } = Clock;
20
20
  const { Parameter, ParameterType } = require('./parameter.js');
21
21
  const Time = require('./time.js');
22
+ const { TypeValidationError, OperationError } = require('./errors.js');
22
23
 
23
24
  const USE_SIM_TIME_PARAM = 'use_sim_time';
24
25
  const CLOCK_TOPIC = '/clock';
@@ -102,7 +103,9 @@ class TimeSource {
102
103
  */
103
104
  attachNode(node) {
104
105
  if ((!node) instanceof rclnodejs.ShadowNode) {
105
- throw new TypeError('Invalid argument, must be type of Node');
106
+ throw new TypeValidationError('node', node, 'Node', {
107
+ entityType: 'time source',
108
+ });
106
109
  }
107
110
 
108
111
  if (this._node) {
@@ -150,8 +153,12 @@ class TimeSource {
150
153
  detachNode() {
151
154
  if (this._clockSubscription) {
152
155
  if (!this._node) {
153
- throw new Error(
154
- 'Unable to destroy previously created clock subscription'
156
+ throw new OperationError(
157
+ 'Unable to destroy previously created clock subscription',
158
+ {
159
+ code: 'NO_NODE_ATTACHED',
160
+ entityType: 'time source',
161
+ }
155
162
  );
156
163
  }
157
164
  this._node.destroySubscription(this._clockSubscription);
@@ -167,7 +174,9 @@ class TimeSource {
167
174
  */
168
175
  attachClock(clock) {
169
176
  if (!(clock instanceof ROSClock)) {
170
- throw new TypeError('Only clocks with type ROS_TIME can be attached.');
177
+ throw new TypeValidationError('clock', clock, 'ROSClock', {
178
+ entityType: 'time source',
179
+ });
171
180
  }
172
181
  clock.rosTimeOverride = this._lastTimeSet;
173
182
  clock.isRosTimeActive = this._isRosTimeActive;
package/lib/utils.js CHANGED
@@ -13,6 +13,157 @@
13
13
  // limitations under the License.
14
14
 
15
15
  const fs = require('fs');
16
+ const fsPromises = require('fs/promises');
17
+ const path = require('path');
18
+ const { ValidationError } = require('./errors.js');
19
+
20
+ /**
21
+ * Ensure directory exists, create recursively if needed (async)
22
+ * Replaces: fse.ensureDir() / fse.mkdirs()
23
+ * @param {string} dirPath - Path to directory
24
+ * @returns {Promise<void>}
25
+ */
26
+ async function ensureDir(dirPath) {
27
+ try {
28
+ await fsPromises.mkdir(dirPath, { recursive: true });
29
+ } catch (err) {
30
+ // Ignore if directory already exists
31
+ if (err.code !== 'EEXIST') throw err;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Ensure directory exists, create recursively if needed (sync)
37
+ * Replaces: fse.mkdirSync()
38
+ * @param {string} dirPath - Path to directory
39
+ */
40
+ function ensureDirSync(dirPath) {
41
+ try {
42
+ fs.mkdirSync(dirPath, { recursive: true });
43
+ } catch (err) {
44
+ // Ignore if directory already exists
45
+ if (err.code !== 'EEXIST') throw err;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Check if path exists (async)
51
+ * Replaces: fse.exists()
52
+ * @param {string} filePath - Path to check
53
+ * @returns {Promise<boolean>}
54
+ */
55
+ async function pathExists(filePath) {
56
+ try {
57
+ await fsPromises.access(filePath);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Empty a directory (remove all contents but keep the directory)
66
+ * Replaces: fse.emptyDir()
67
+ * @param {string} dirPath - Path to directory
68
+ * @returns {Promise<void>}
69
+ */
70
+ async function emptyDir(dirPath) {
71
+ try {
72
+ const files = await fsPromises.readdir(dirPath);
73
+ await Promise.all(
74
+ files.map((file) =>
75
+ fsPromises.rm(path.join(dirPath, file), {
76
+ recursive: true,
77
+ force: true,
78
+ })
79
+ )
80
+ );
81
+ } catch (err) {
82
+ // Ignore if directory doesn't exist
83
+ if (err.code !== 'ENOENT') throw err;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Copy file or directory recursively
89
+ * Replaces: fse.copy()
90
+ * @param {string} src - Source path
91
+ * @param {string} dest - Destination path
92
+ * @param {object} options - Copy options
93
+ * @returns {Promise<void>}
94
+ */
95
+ async function copy(src, dest, options = {}) {
96
+ const opts = {
97
+ recursive: true,
98
+ force: options.overwrite !== false,
99
+ ...options,
100
+ };
101
+ await fsPromises.cp(src, dest, opts);
102
+ }
103
+
104
+ /**
105
+ * Read and parse JSON file synchronously
106
+ * Replaces: fse.readJsonSync()
107
+ * @param {string} filePath - Path to JSON file
108
+ * @param {object} options - Read options
109
+ * @returns {any} Parsed JSON data
110
+ */
111
+ function readJsonSync(filePath, options = {}) {
112
+ const content = fs.readFileSync(filePath, options.encoding || 'utf8');
113
+ return JSON.parse(content);
114
+ }
115
+
116
+ /**
117
+ * Remove file or directory (async)
118
+ * Replaces: fse.remove()
119
+ * @param {string} filePath - Path to remove
120
+ * @returns {Promise<void>}
121
+ */
122
+ async function remove(filePath) {
123
+ try {
124
+ await fsPromises.rm(filePath, { recursive: true, force: true });
125
+ } catch (err) {
126
+ // Ignore if path doesn't exist
127
+ if (err.code !== 'ENOENT') throw err;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Remove file or directory (sync)
133
+ * Replaces: fse.removeSync()
134
+ * @param {string} filePath - Path to remove
135
+ */
136
+ function removeSync(filePath) {
137
+ try {
138
+ fs.rmSync(filePath, { recursive: true, force: true });
139
+ } catch (err) {
140
+ // Ignore if path doesn't exist
141
+ if (err.code !== 'ENOENT') throw err;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Write file with content (async)
147
+ * Replaces: fse.writeFile()
148
+ * @param {string} filePath - Path to file
149
+ * @param {string|Buffer} data - Content to write
150
+ * @param {object} options - Write options
151
+ * @returns {Promise<void>}
152
+ */
153
+ async function writeFile(filePath, data, options = {}) {
154
+ await fsPromises.writeFile(filePath, data, options);
155
+ }
156
+
157
+ /**
158
+ * Create directory (async)
159
+ * Replaces: fse.mkdir()
160
+ * @param {string} dirPath - Path to directory
161
+ * @param {object} options - mkdir options
162
+ * @returns {Promise<void>}
163
+ */
164
+ async function mkdir(dirPath, options = {}) {
165
+ await fsPromises.mkdir(dirPath, options);
166
+ }
16
167
 
17
168
  /**
18
169
  * Detect Ubuntu codename from /etc/os-release
@@ -32,6 +183,168 @@ function detectUbuntuCodename() {
32
183
  }
33
184
  }
34
185
 
186
+ /**
187
+ * Normalize a ROS 2 node name by removing the leading slash if present.
188
+ *
189
+ * ROS 2 node names may be specified with or without a leading slash depending
190
+ * on the context. This utility ensures consistent representation without the
191
+ * leading slash, which is the standard format for most ROS 2 APIs.
192
+ *
193
+ * @param {string} nodeName - The node name to normalize
194
+ * @returns {string} The normalized node name without leading slash
195
+ *
196
+ * @example
197
+ * normalizeNodeName('my_node') // 'my_node'
198
+ * normalizeNodeName('/my_node') // 'my_node'
199
+ * normalizeNodeName('/ns/my_node') // 'ns/my_node'
200
+ */
201
+ function normalizeNodeName(nodeName) {
202
+ return nodeName.startsWith('/') ? nodeName.substring(1) : nodeName;
203
+ }
204
+
205
+ /**
206
+ * Check if two numbers are equal within a given tolerance.
207
+ *
208
+ * This function compares two numbers using both relative and absolute tolerance,
209
+ * matching the behavior of the 'is-close' npm package.
210
+ *
211
+ * The comparison uses the formula:
212
+ * abs(a - b) <= max(rtol * max(abs(a), abs(b)), atol)
213
+ *
214
+ * Implementation checks:
215
+ * 1. Absolute tolerance: abs(a - b) <= atol
216
+ * 2. Relative tolerance: abs(a - b) / max(abs(a), abs(b)) <= rtol
217
+ *
218
+ * @param {number} a - The first number to compare
219
+ * @param {number} b - The second number to compare
220
+ * @param {number} [rtol=1e-9] - The relative tolerance parameter (default: 1e-9)
221
+ * @param {number} [atol=0.0] - The absolute tolerance parameter (default: 0.0)
222
+ * @returns {boolean} True if the numbers are close within the tolerance
223
+ *
224
+ * @example
225
+ * isClose(1.0, 1.0) // true - exact equality
226
+ * isClose(1.0, 1.1, 0.01) // false - relative diff: 0.1/1.1 ≈ 0.091 > 0.01
227
+ * isClose(10, 10.00001, 1e-6) // true - relative diff: 0.00001/10 = 1e-6 <= 1e-6
228
+ * isClose(0, 0.05, 0, 0.1) // true - absolute diff: 0.05 <= 0.1 (atol)
229
+ */
230
+ function isClose(a, b, rtol = 1e-9, atol = 0.0) {
231
+ // Handle exact equality
232
+ if (a === b) {
233
+ return true;
234
+ }
235
+
236
+ // Handle non-finite numbers
237
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
238
+ return false;
239
+ }
240
+
241
+ const absDiff = Math.abs(a - b);
242
+
243
+ // Check absolute tolerance first (optimization)
244
+ if (atol >= absDiff) {
245
+ return true;
246
+ }
247
+
248
+ // Check relative tolerance
249
+ const relativeScaler = Math.max(Math.abs(a), Math.abs(b));
250
+
251
+ // Handle division by zero when both values are zero or very close to zero
252
+ if (relativeScaler === 0) {
253
+ return true; // Both are zero, already handled by absolute tolerance
254
+ }
255
+
256
+ const relativeDiff = absDiff / relativeScaler;
257
+
258
+ return rtol >= relativeDiff;
259
+ }
260
+
261
+ /**
262
+ * Compare two semantic version strings.
263
+ *
264
+ * Supports version strings in the format: x.y.z or x.y.z.w
265
+ * where x, y, z, w are integers.
266
+ *
267
+ * @param {string} version1 - First version string (e.g., '1.2.3')
268
+ * @param {string} version2 - Second version string (e.g., '1.2.4')
269
+ * @param {string} operator - Comparison operator: '<', '<=', '>', '>=', '==', '!='
270
+ * @returns {boolean} Result of the comparison
271
+ *
272
+ * @example
273
+ * compareVersions('1.2.3', '1.2.4', '<') // true
274
+ * compareVersions('2.0.0', '1.9.9', '>') // true
275
+ * compareVersions('1.2.3', '1.2.3', '==') // true
276
+ * compareVersions('1.2.3', '1.2.3', '>=') // true
277
+ */
278
+ function compareVersions(version1, version2, operator) {
279
+ // Parse version strings into arrays of integers
280
+ const v1Parts = version1.split('.').map((part) => parseInt(part, 10));
281
+ const v2Parts = version2.split('.').map((part) => parseInt(part, 10));
282
+
283
+ // Pad arrays to same length with zeros
284
+ const maxLength = Math.max(v1Parts.length, v2Parts.length);
285
+ while (v1Parts.length < maxLength) v1Parts.push(0);
286
+ while (v2Parts.length < maxLength) v2Parts.push(0);
287
+
288
+ // Compare each part
289
+ let cmp = 0;
290
+ for (let i = 0; i < maxLength; i++) {
291
+ if (v1Parts[i] > v2Parts[i]) {
292
+ cmp = 1;
293
+ break;
294
+ } else if (v1Parts[i] < v2Parts[i]) {
295
+ cmp = -1;
296
+ break;
297
+ }
298
+ }
299
+
300
+ // Apply operator
301
+ switch (operator) {
302
+ case '<':
303
+ return cmp < 0;
304
+ case '<=':
305
+ return cmp <= 0;
306
+ case '>':
307
+ return cmp > 0;
308
+ case '>=':
309
+ return cmp >= 0;
310
+ case '==':
311
+ case '===':
312
+ return cmp === 0;
313
+ case '!=':
314
+ case '!==':
315
+ return cmp !== 0;
316
+ default:
317
+ throw new ValidationError(`Invalid operator: ${operator}`, {
318
+ code: 'INVALID_OPERATOR',
319
+ argumentName: 'operator',
320
+ providedValue: operator,
321
+ expectedType: "'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte'",
322
+ });
323
+ }
324
+ }
325
+
35
326
  module.exports = {
327
+ // General utilities
36
328
  detectUbuntuCodename,
329
+ isClose,
330
+ normalizeNodeName,
331
+
332
+ // File system utilities (async)
333
+ ensureDir,
334
+ mkdirs: ensureDir, // Alias for fs-extra compatibility
335
+ exists: pathExists, // Renamed to avoid conflict with deprecated fs.exists
336
+ pathExists,
337
+ emptyDir,
338
+ copy,
339
+ remove,
340
+ writeFile,
341
+ mkdir,
342
+
343
+ // File system utilities (sync)
344
+ ensureDirSync,
345
+ mkdirSync: ensureDirSync, // Alias for fs-extra compatibility
346
+ removeSync,
347
+ readJsonSync,
348
+
349
+ compareVersions,
37
350
  };