applescript-node 1.0.1

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/dist/index.js ADDED
@@ -0,0 +1,3220 @@
1
+ 'use strict';
2
+
3
+ var child_process = require('child_process');
4
+ var util = require('util');
5
+ var crypto = require('crypto');
6
+ var promises = require('fs/promises');
7
+ var os = require('os');
8
+ var path = require('path');
9
+ var fastXmlParser = require('fast-xml-parser');
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/expressions.ts
18
+ var ExprBuilder = class {
19
+ /**
20
+ * Greater than comparison: left > right
21
+ * @param left - Variable or expression. Scoped variables (from loops) get autocomplete.
22
+ * The type `TScope | (string & Record<never, never>)` provides autocomplete for scoped
23
+ * variables while still accepting any string expression.
24
+ * @param right - Value to compare against (string or number)
25
+ * @returns AppleScript expression: "left > right"
26
+ * @example
27
+ * e.gt('counter', 10) // => "counter > 10"
28
+ * e.gt('aPerson', 5) // => "aPerson > 5" (with autocomplete for 'aPerson' if in scope)
29
+ */
30
+ gt(left, right) {
31
+ const rightValue = typeof right === "number" ? right.toString() : `"${right}"`;
32
+ return `${left} > ${rightValue}`;
33
+ }
34
+ /**
35
+ * Less than comparison: left < right
36
+ * @param left - Variable or expression (scoped variables get autocomplete)
37
+ */
38
+ lt(left, right) {
39
+ const rightValue = typeof right === "number" ? right.toString() : `"${right}"`;
40
+ return `${left} < ${rightValue}`;
41
+ }
42
+ /**
43
+ * Greater than or equal: left >= right
44
+ * @param left - Variable or expression (scoped variables get autocomplete)
45
+ */
46
+ gte(left, right) {
47
+ const rightValue = typeof right === "number" ? right.toString() : `"${right}"`;
48
+ return `${left} >= ${rightValue}`;
49
+ }
50
+ /**
51
+ * Less than or equal: left <= right
52
+ * @param left - Variable or expression (scoped variables get autocomplete)
53
+ */
54
+ lte(left, right) {
55
+ const rightValue = typeof right === "number" ? right.toString() : `"${right}"`;
56
+ return `${left} <= ${rightValue}`;
57
+ }
58
+ /**
59
+ * Equality comparison: left = right
60
+ * @param left - Variable or expression (scoped variables get autocomplete)
61
+ */
62
+ eq(left, right) {
63
+ let rightValue;
64
+ if (typeof right === "string") {
65
+ rightValue = `"${right}"`;
66
+ } else if (typeof right === "boolean") {
67
+ rightValue = right.toString();
68
+ } else {
69
+ rightValue = right.toString();
70
+ }
71
+ return `${left} = ${rightValue}`;
72
+ }
73
+ /**
74
+ * Inequality comparison: left is not equal to right
75
+ * @param left - Variable or expression (scoped variables get autocomplete)
76
+ */
77
+ ne(left, right) {
78
+ let rightValue;
79
+ if (typeof right === "string") {
80
+ rightValue = `"${right}"`;
81
+ } else if (typeof right === "boolean") {
82
+ rightValue = right.toString();
83
+ } else {
84
+ rightValue = right.toString();
85
+ }
86
+ return `${left} is not equal to ${rightValue}`;
87
+ }
88
+ /**
89
+ * Logical AND: combines multiple conditions
90
+ * Example: expr.and(expr.gt('x', 5), expr.lt('x', 10))
91
+ */
92
+ and(...conditions) {
93
+ return conditions.join(" and ");
94
+ }
95
+ /**
96
+ * Logical OR: combines multiple conditions
97
+ * Example: expr.or(expr.eq('status', 'done'), expr.eq('status', 'skipped'))
98
+ */
99
+ or(...conditions) {
100
+ return conditions.join(" or ");
101
+ }
102
+ /**
103
+ * Logical NOT: negates a condition
104
+ * Example: expr.not(expr.eq('status', 'pending'))
105
+ */
106
+ not(condition) {
107
+ return `not ${condition}`;
108
+ }
109
+ /**
110
+ * String length: length of str
111
+ * Often used in conditions like: expr.gt(expr.length('name'), 5)
112
+ */
113
+ length(str) {
114
+ return `length of ${str}`;
115
+ }
116
+ /**
117
+ * Property access: prop of obj
118
+ * @param obj - Variable name. Scoped variables (from loops) get autocomplete.
119
+ * The type allows both scoped variables and arbitrary string expressions.
120
+ * @param prop - Property name to access
121
+ * @returns AppleScript expression: "prop of obj"
122
+ * @example
123
+ * e.property('aNote', 'name') // => "name of aNote"
124
+ * e.property('aPerson', 'emails') // => "emails of aPerson" (with autocomplete for 'aPerson')
125
+ */
126
+ property(obj, prop) {
127
+ return `${prop} of ${obj}`;
128
+ }
129
+ /**
130
+ * Count: count of items
131
+ * Example: expr.gt(expr.count('notes'), 10)
132
+ */
133
+ count(items) {
134
+ return `count of ${items}`;
135
+ }
136
+ /**
137
+ * Existence check: exists item
138
+ * Example: expr.exists('window "Settings"')
139
+ */
140
+ exists(item) {
141
+ return `exists ${item}`;
142
+ }
143
+ /**
144
+ * String contains: haystack contains needle
145
+ * Example: expr.contains('name', '"test"')
146
+ */
147
+ contains(haystack, needle) {
148
+ const needleValue = typeof needle === "number" ? needle.toString() : needle;
149
+ return `${haystack} contains ${needleValue}`;
150
+ }
151
+ /**
152
+ * String starts with: str begins with prefix
153
+ * Example: expr.startsWith('name', '"John"')
154
+ */
155
+ startsWith(str, prefix) {
156
+ return `${str} begins with ${prefix}`;
157
+ }
158
+ /**
159
+ * String ends with: str ends with suffix
160
+ * Example: expr.endsWith('name', '"son"')
161
+ */
162
+ endsWith(str, suffix) {
163
+ return `${str} ends with ${suffix}`;
164
+ }
165
+ /**
166
+ * Type checking: the type of item is typeName
167
+ * Common types: 'text', 'number', 'list', 'record', 'boolean'
168
+ */
169
+ typeEquals(item, type) {
170
+ return `the type of ${item} is ${type}`;
171
+ }
172
+ /**
173
+ * Matches wildcard pattern
174
+ * Example: expr.matches('name', '"*Smith*"')
175
+ */
176
+ matches(str, pattern) {
177
+ return `${str} contains ${pattern}`;
178
+ }
179
+ /**
180
+ * Parentheses for explicit grouping
181
+ * Useful when combining complex boolean expressions
182
+ * Example: expr.or(expr.paren(expr.and(...)), expr.eq(...))
183
+ */
184
+ paren(condition) {
185
+ return `(${condition})`;
186
+ }
187
+ /**
188
+ * Create a comparison between two expressions
189
+ * Useful for comparing two computed properties
190
+ * Example: expr.compare('length of name1', '>', 'length of name2')
191
+ */
192
+ compare(left, operator, right) {
193
+ return `${left} ${operator} ${right}`;
194
+ }
195
+ /**
196
+ * Collection accessor: every element of container
197
+ * Example: expr.every('participant', 'aChat') => "every participant of aChat"
198
+ * Example: expr.every('note', 'folder "Notes"') => "every note of folder \"Notes\""
199
+ */
200
+ every(element, container) {
201
+ return `every ${element} of ${container}`;
202
+ }
203
+ /**
204
+ * Nested property accessor: chains multiple properties
205
+ * Example: expr.nestedProperty('aChat', 'account', 'id') => "id of account of aChat"
206
+ * Example: expr.nestedProperty('note', 'folder', 'name') => "name of folder of note"
207
+ * @param obj - Variable name (scoped variables get autocomplete)
208
+ */
209
+ nestedProperty(obj, ...properties) {
210
+ if (properties.length === 0) {
211
+ return obj;
212
+ }
213
+ return properties.reduce((acc, prop) => `${prop} of ${acc}`, obj);
214
+ }
215
+ /**
216
+ * Type casting: expression as type
217
+ * Example: expr.asType('creation date of aNote', 'string') => "creation date of aNote as string"
218
+ * Example: expr.asType(expr.property('aNote', 'id'), 'text') => "id of aNote as text"
219
+ */
220
+ asType(expression, type) {
221
+ return `${expression} as ${type}`;
222
+ }
223
+ /**
224
+ * Substring/range accessor: text start thru end of source
225
+ * Example: expr.text(1, 100, 'notePlaintext') => "text 1 thru 100 of notePlaintext"
226
+ * Example: expr.text(5, 10, 'myString') => "text 5 thru 10 of myString"
227
+ */
228
+ text(start, end, source) {
229
+ return `text ${start} thru ${end} of ${source}`;
230
+ }
231
+ /**
232
+ * Character accessor: character n of source
233
+ * Example: expr.character(1, 'myString') => "character 1 of myString"
234
+ */
235
+ character(index, source) {
236
+ return `character ${index} of ${source}`;
237
+ }
238
+ /**
239
+ * Item accessor: item n of collection
240
+ * Example: expr.item(1, 'myList') => "item 1 of myList"
241
+ * Example: expr.item('i', 'notes') => "item i of notes"
242
+ */
243
+ item(index, collection) {
244
+ return `item ${index} of ${collection}`;
245
+ }
246
+ /**
247
+ * Items range: items start thru end of collection
248
+ * Example: expr.items(1, 5, 'myList') => "items 1 thru 5 of myList"
249
+ */
250
+ items(start, end, collection) {
251
+ return `items ${start} thru ${end} of ${collection}`;
252
+ }
253
+ /**
254
+ * First item: first element of collection
255
+ * Example: expr.first('note', 'notes') => "first note of notes"
256
+ */
257
+ first(element, collection) {
258
+ return `first ${element} of ${collection}`;
259
+ }
260
+ /**
261
+ * Last item: last element of collection
262
+ * Example: expr.last('note', 'notes') => "last note of notes"
263
+ */
264
+ last(element, collection) {
265
+ return `last ${element} of ${collection}`;
266
+ }
267
+ /**
268
+ * Some: some element where condition
269
+ * Example: expr.some('note', 'notes', 'name contains "test"')
270
+ */
271
+ some(element, collection, condition) {
272
+ return `some ${element} of ${collection} where ${condition}`;
273
+ }
274
+ /**
275
+ * Filter: every element where condition
276
+ * Example: expr.filter('note', 'notes', 'shared = true')
277
+ */
278
+ filter(element, collection, condition) {
279
+ return `every ${element} of ${collection} where ${condition}`;
280
+ }
281
+ /**
282
+ * Concatenation: left & right
283
+ * Example: expr.concat('text 1 thru 50 of body', '"..."') => 'text 1 thru 50 of body & "..."'
284
+ */
285
+ concat(...parts) {
286
+ return parts.join(" & ");
287
+ }
288
+ /**
289
+ * Property of item accessor: property of item N of collection
290
+ * Example: expr.propertyOfItem('value', 1, 'emails of aPerson') => "value of item 1 of emails of aPerson"
291
+ * Example: expr.propertyOfItem('name', 1, 'contacts') => "name of item 1 of contacts"
292
+ */
293
+ propertyOfItem(property, index, collection) {
294
+ return `${property} of item ${index} of ${collection}`;
295
+ }
296
+ /**
297
+ * Value of item accessor (common shorthand for propertyOfItem('value', ...))
298
+ * Example: expr.valueOfItem(1, 'emails of aPerson') => "value of item 1 of emails of aPerson"
299
+ * Example: expr.valueOfItem(1, 'phones of contact') => "value of item 1 of phones of contact"
300
+ */
301
+ valueOfItem(index, collection) {
302
+ return this.propertyOfItem("value", index, collection);
303
+ }
304
+ };
305
+ function expr() {
306
+ return new ExprBuilder();
307
+ }
308
+
309
+ // src/builder-utils.ts
310
+ function resolveExpression(expression) {
311
+ return typeof expression === "function" ? expression(new ExprBuilder()) : expression;
312
+ }
313
+
314
+ // src/builder.ts
315
+ var ScriptBuilderError = class extends Error {
316
+ constructor(message) {
317
+ super(message);
318
+ this.name = "ScriptBuilderError";
319
+ }
320
+ };
321
+ var AppleScriptBuilder = class {
322
+ script = [];
323
+ indentLevel = 0;
324
+ INDENT = " ";
325
+ blockStack = [];
326
+ getIndentation() {
327
+ return this.INDENT.repeat(this.indentLevel);
328
+ }
329
+ escapeString(str) {
330
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
331
+ }
332
+ formatValue(value) {
333
+ if (value === null) return "missing value";
334
+ if (typeof value === "string") return `"${this.escapeString(value)}"`;
335
+ if (typeof value === "number") return value.toString();
336
+ if (typeof value === "boolean") return value.toString();
337
+ if (Array.isArray(value)) {
338
+ return `{${value.map((v) => this.formatValue(v)).join(", ")}}`;
339
+ }
340
+ const entries = Object.entries(value).map(([k, v]) => `${k}:${this.formatValue(v)}`).join(", ");
341
+ return `{${entries}}`;
342
+ }
343
+ makeRecord(properties) {
344
+ const entries = Object.entries(properties).map(([k, v]) => `${k}:${this.formatValue(v)}`).join(", ");
345
+ return `{${entries}}`;
346
+ }
347
+ validateBlockStack() {
348
+ if (this.blockStack.length > 0) {
349
+ const unclosedBlocks = this.blockStack.map((b) => b.type).join(", ");
350
+ throw new ScriptBuilderError(`Unclosed blocks remain: ${unclosedBlocks}`);
351
+ }
352
+ }
353
+ pushBlock(type, target) {
354
+ this.blockStack.push({ type, target });
355
+ this.indentLevel++;
356
+ }
357
+ popBlock() {
358
+ if (this.blockStack.length === 0) {
359
+ throw new ScriptBuilderError("Cannot end block: no blocks are currently open");
360
+ }
361
+ this.blockStack.pop();
362
+ this.indentLevel--;
363
+ }
364
+ validateBlockType(expectedType) {
365
+ if (this.blockStack.length === 0) {
366
+ throw new ScriptBuilderError(
367
+ `Cannot end ${expectedType} block: no blocks are currently open`
368
+ );
369
+ }
370
+ const currentBlock = this.blockStack[this.blockStack.length - 1];
371
+ if (currentBlock.type !== expectedType) {
372
+ throw new ScriptBuilderError(
373
+ `Cannot end ${expectedType} block: currently inside ${currentBlock.type} block`
374
+ );
375
+ }
376
+ }
377
+ addLine(line) {
378
+ this.script.push(`${this.getIndentation()}${line}`);
379
+ }
380
+ // Core language constructs
381
+ tell(target) {
382
+ this.script.push(`${this.getIndentation()}tell application "${this.escapeString(target)}"`);
383
+ this.pushBlock("tell", target);
384
+ return this;
385
+ }
386
+ tellTarget(target) {
387
+ this.script.push(`${this.getIndentation()}tell ${target}`);
388
+ this.pushBlock("tell", target);
389
+ return this;
390
+ }
391
+ tellProcess(processName) {
392
+ this.script.push(
393
+ `${this.getIndentation()}tell application "System Events" to tell process "${this.escapeString(processName)}"`
394
+ );
395
+ this.pushBlock("tell", processName);
396
+ return this;
397
+ }
398
+ on(handlerName, parameters) {
399
+ const params = parameters?.length ? ` ${parameters.join(", ")}` : "";
400
+ this.script.push(`${this.getIndentation()}on ${handlerName}${params}`);
401
+ this.pushBlock("on", handlerName);
402
+ return this;
403
+ }
404
+ /**
405
+ * Define a handler using the 'to' syntax (alternative to 'on').
406
+ * Both 'on' and 'to' are equivalent in AppleScript.
407
+ * @param handlerName The name of the handler
408
+ * @param parameters Optional array of parameter names
409
+ * @returns This builder instance for method chaining
410
+ * @example
411
+ * .to('sayHello', ['name'])
412
+ * .displayDialog('Hello ' & name)
413
+ * .endto()
414
+ */
415
+ to(handlerName, parameters) {
416
+ const params = parameters?.length ? ` ${parameters.join(", ")}` : "";
417
+ this.script.push(`${this.getIndentation()}to ${handlerName}${params}`);
418
+ this.pushBlock("on", handlerName);
419
+ return this;
420
+ }
421
+ /**
422
+ * Call/invoke a handler with parameters.
423
+ * @param handlerName The name of the handler to call
424
+ * @param parameters Optional array of parameter values
425
+ * @returns This builder instance for method chaining
426
+ * @example
427
+ * .callHandler('sayHello', ['"John"'])
428
+ * .callHandler('processFile', ['theFile', 'true'])
429
+ */
430
+ callHandler(handlerName, parameters) {
431
+ const params = parameters?.length ? ` ${parameters.join(", ")}` : "";
432
+ this.script.push(`${this.getIndentation()}${handlerName}${params}`);
433
+ return this;
434
+ }
435
+ /**
436
+ * Call a handler from within a tell statement using 'my' keyword.
437
+ * Required when calling handlers from within tell blocks.
438
+ * @param handlerName The name of the handler to call
439
+ * @param parameters Optional array of parameter values
440
+ * @returns This builder instance for method chaining
441
+ * @example
442
+ * .tell('Finder')
443
+ * .my('processFile', ['theFile'])
444
+ * .endtell()
445
+ */
446
+ my(handlerName, parameters) {
447
+ const params = parameters?.length ? ` ${parameters.join(", ")}` : "";
448
+ this.script.push(`${this.getIndentation()}my ${handlerName}${params}`);
449
+ return this;
450
+ }
451
+ /**
452
+ * Call a handler from within a tell statement using 'of me' syntax.
453
+ * Alternative to 'my' keyword for calling handlers from within tell blocks.
454
+ * @param handlerName The name of the handler to call
455
+ * @param parameters Optional array of parameter values
456
+ * @returns This builder instance for method chaining
457
+ * @example
458
+ * .tell('Finder')
459
+ * .ofMe('processFile', ['theFile'])
460
+ * .endtell()
461
+ */
462
+ ofMe(handlerName, parameters) {
463
+ const params = parameters?.length ? ` ${parameters.join(", ")}` : "";
464
+ this.script.push(`${this.getIndentation()}${handlerName}${params} of me`);
465
+ return this;
466
+ }
467
+ /**
468
+ * Define a handler with labeled parameters (interleaved syntax).
469
+ * AppleScript supports splitting parameter names with colons and spaces.
470
+ * @param handlerName The name of the handler
471
+ * @param labeledParams Object with parameter labels and values
472
+ * @returns This builder instance for method chaining
473
+ * @example
474
+ * .onLabeled('displayError', { message: 'theErrorMessage', buttons: 'theButtons' })
475
+ * .displayDialog(theErrorMessage, { buttons: theButtons })
476
+ * .endon()
477
+ */
478
+ onLabeled(handlerName, labeledParams) {
479
+ const paramPairs = Object.entries(labeledParams);
480
+ const params = paramPairs.map(([label, value]) => `${label} ${value}`).join(", ");
481
+ this.script.push(`${this.getIndentation()}on ${handlerName} ${params}`);
482
+ this.pushBlock("on", handlerName);
483
+ return this;
484
+ }
485
+ /**
486
+ * Define a handler with labeled parameters using 'to' syntax.
487
+ * @param handlerName The name of the handler
488
+ * @param labeledParams Object with parameter labels and values
489
+ * @returns This builder instance for method chaining
490
+ */
491
+ toLabeled(handlerName, labeledParams) {
492
+ const paramPairs = Object.entries(labeledParams);
493
+ const params = paramPairs.map(([label, value]) => `${label} ${value}`).join(", ");
494
+ this.script.push(`${this.getIndentation()}to ${handlerName} ${params}`);
495
+ this.pushBlock("on", handlerName);
496
+ return this;
497
+ }
498
+ // Event Handlers
499
+ /**
500
+ * Define a 'run' event handler (implicit or explicit).
501
+ * The run handler is called when a script executes.
502
+ * @param explicit If true, explicitly define the run handler; if false, use implicit
503
+ * @returns This builder instance for method chaining
504
+ * @example
505
+ * .runHandler(true) // Explicit: on run ... end run
506
+ * .displayDialog('Script is running')
507
+ * .endrun()
508
+ */
509
+ runHandler(explicit = false) {
510
+ if (explicit) {
511
+ this.script.push(`${this.getIndentation()}on run`);
512
+ this.pushBlock("on", "run");
513
+ }
514
+ return this;
515
+ }
516
+ /**
517
+ * Define a 'quit' event handler.
518
+ * Called when a script app quits.
519
+ * @returns This builder instance for method chaining
520
+ * @example
521
+ * .quitHandler()
522
+ * .displayDialog('Script is quitting')
523
+ * .endquit()
524
+ */
525
+ quitHandler() {
526
+ this.script.push(`${this.getIndentation()}on quit`);
527
+ this.pushBlock("on", "quit");
528
+ return this;
529
+ }
530
+ /**
531
+ * Define an 'open' event handler for drag-and-drop support.
532
+ * Makes the script app drag-and-droppable.
533
+ * @param parameterName Name for the dropped items parameter (default: 'theDroppedItems')
534
+ * @returns This builder instance for method chaining
535
+ * @example
536
+ * .openHandler('theFiles')
537
+ * .repeatWith('aFile', 'theFiles')
538
+ * .displayDialog('Processing: ' & aFile)
539
+ * .endrepeat()
540
+ * .endopen()
541
+ */
542
+ openHandler(parameterName = "theDroppedItems") {
543
+ this.script.push(`${this.getIndentation()}on open ${parameterName}`);
544
+ this.pushBlock("on", "open");
545
+ return this;
546
+ }
547
+ /**
548
+ * Define an 'idle' event handler for stay-open applications.
549
+ * Called periodically in stay-open script apps.
550
+ * @param returnSeconds Number of seconds to wait before next idle call (default: 30)
551
+ * @returns This builder instance for method chaining
552
+ * @example
553
+ * .idleHandler(5) // Check every 5 seconds
554
+ * .displayDialog('Idle processing')
555
+ * .return(5) // Return 5 seconds for next idle
556
+ * .endidle()
557
+ */
558
+ idleHandler(_returnSeconds = 30) {
559
+ this.script.push(`${this.getIndentation()}on idle`);
560
+ this.pushBlock("on", "idle");
561
+ return this;
562
+ }
563
+ end() {
564
+ if (this.blockStack.length === 0) {
565
+ throw new ScriptBuilderError("Cannot call end(): no blocks are currently open");
566
+ }
567
+ const block = this.blockStack[this.blockStack.length - 1];
568
+ this.popBlock();
569
+ const endText = block.type === "on" && block.target ? block.target : block.type;
570
+ this.script.push(`${this.getIndentation()}end ${endText}`);
571
+ return this;
572
+ }
573
+ /**
574
+ * Explicitly end an if block.
575
+ * Preferred over end() for clarity when working with multiple nested blocks.
576
+ */
577
+ endif() {
578
+ this.validateBlockType("if");
579
+ return this.end();
580
+ }
581
+ /**
582
+ * Explicitly end a repeat block.
583
+ * Preferred over end() for clarity when working with multiple nested blocks.
584
+ */
585
+ endrepeat() {
586
+ this.validateBlockType("repeat");
587
+ return this.end();
588
+ }
589
+ /**
590
+ * Explicitly end a try block.
591
+ * Preferred over end() for clarity when working with multiple nested blocks.
592
+ */
593
+ endtry() {
594
+ this.validateBlockType("try");
595
+ return this.end();
596
+ }
597
+ /**
598
+ * Explicitly end a tell block.
599
+ * Preferred over end() for clarity when working with multiple nested blocks.
600
+ */
601
+ endtell() {
602
+ this.validateBlockType("tell");
603
+ return this.end();
604
+ }
605
+ /**
606
+ * Explicitly end an on handler block.
607
+ * Preferred over end() for clarity when working with multiple nested blocks.
608
+ */
609
+ endon() {
610
+ this.validateBlockType("on");
611
+ return this.end();
612
+ }
613
+ /**
614
+ * Explicitly end a considering block.
615
+ * Preferred over end() for clarity when working with multiple nested blocks.
616
+ */
617
+ endconsidering() {
618
+ this.validateBlockType("considering");
619
+ return this.end();
620
+ }
621
+ /**
622
+ * Explicitly end an ignoring block.
623
+ * Preferred over end() for clarity when working with multiple nested blocks.
624
+ */
625
+ endignoring() {
626
+ this.validateBlockType("ignoring");
627
+ return this.end();
628
+ }
629
+ /**
630
+ * Explicitly end a using block.
631
+ * Preferred over end() for clarity when working with multiple nested blocks.
632
+ */
633
+ endusing() {
634
+ this.validateBlockType("using");
635
+ return this.end();
636
+ }
637
+ /**
638
+ * Explicitly end a with block.
639
+ * Preferred over end() for clarity when working with multiple nested blocks.
640
+ */
641
+ endwith() {
642
+ this.validateBlockType("with");
643
+ return this.end();
644
+ }
645
+ /**
646
+ * Explicitly end a 'to' handler block.
647
+ * Preferred over end() for clarity when working with handlers.
648
+ */
649
+ endto() {
650
+ this.validateBlockType("on");
651
+ return this.end();
652
+ }
653
+ /**
654
+ * Explicitly end a 'run' handler block.
655
+ * Preferred over end() for clarity when working with run handlers.
656
+ */
657
+ endrun() {
658
+ this.validateBlockType("on");
659
+ return this.end();
660
+ }
661
+ /**
662
+ * Explicitly end a 'quit' handler block.
663
+ * Preferred over end() for clarity when working with quit handlers.
664
+ */
665
+ endquit() {
666
+ this.validateBlockType("on");
667
+ return this.end();
668
+ }
669
+ /**
670
+ * Explicitly end an 'open' handler block.
671
+ * Preferred over end() for clarity when working with open handlers.
672
+ */
673
+ endopen() {
674
+ this.validateBlockType("on");
675
+ return this.end();
676
+ }
677
+ /**
678
+ * Explicitly end an 'idle' handler block.
679
+ * Preferred over end() for clarity when working with idle handlers.
680
+ */
681
+ endidle() {
682
+ this.validateBlockType("on");
683
+ return this.end();
684
+ }
685
+ if(condition) {
686
+ const conditionStr = resolveExpression(condition);
687
+ this.script.push(`${this.getIndentation()}if ${conditionStr}`);
688
+ this.pushBlock("if");
689
+ return this;
690
+ }
691
+ thenBlock() {
692
+ if (this.blockStack.length === 0 || this.blockStack[this.blockStack.length - 1].type !== "if") {
693
+ throw new ScriptBuilderError("Cannot call thenBlock(): no if block is currently open");
694
+ }
695
+ const lastLine = this.script[this.script.length - 1];
696
+ this.script[this.script.length - 1] = `${lastLine} then`;
697
+ return this;
698
+ }
699
+ else() {
700
+ if (this.blockStack.length === 0 || this.blockStack[this.blockStack.length - 1].type !== "if") {
701
+ throw new ScriptBuilderError("Cannot call else(): no if block is currently open");
702
+ }
703
+ this.indentLevel--;
704
+ this.script.push(`${this.getIndentation()}else`);
705
+ this.indentLevel++;
706
+ return this;
707
+ }
708
+ elseIf(condition) {
709
+ if (this.blockStack.length === 0 || this.blockStack[this.blockStack.length - 1].type !== "if") {
710
+ throw new ScriptBuilderError("Cannot call elseIf(): no if block is currently open");
711
+ }
712
+ this.indentLevel--;
713
+ this.script.push(`${this.getIndentation()}else if ${condition}`);
714
+ this.indentLevel++;
715
+ return this;
716
+ }
717
+ repeat(times) {
718
+ if (times !== void 0 && (!Number.isInteger(times) || times < 1)) {
719
+ throw new ScriptBuilderError("Repeat times must be a positive integer");
720
+ }
721
+ if (times !== void 0) {
722
+ this.script.push(`${this.getIndentation()}repeat ${times} times`);
723
+ } else {
724
+ this.script.push(`${this.getIndentation()}repeat`);
725
+ }
726
+ this.pushBlock("repeat");
727
+ return this;
728
+ }
729
+ repeatWith(variable, list) {
730
+ if (!(variable && list)) {
731
+ throw new ScriptBuilderError("Both variable and list must be provided for repeatWith");
732
+ }
733
+ this.script.push(`${this.getIndentation()}repeat with ${variable} in ${list}`);
734
+ this.pushBlock("repeat");
735
+ return this;
736
+ }
737
+ repeatUntil(condition) {
738
+ if (!condition) {
739
+ throw new ScriptBuilderError("Condition must be provided for repeatUntil");
740
+ }
741
+ this.script.push(`${this.getIndentation()}repeat until ${condition}`);
742
+ this.pushBlock("repeat");
743
+ return this;
744
+ }
745
+ repeatWhile(condition) {
746
+ if (!condition) {
747
+ throw new ScriptBuilderError("Condition must be provided for repeatWhile");
748
+ }
749
+ this.script.push(`${this.getIndentation()}repeat while ${condition}`);
750
+ this.pushBlock("repeat");
751
+ return this;
752
+ }
753
+ repeatWithRange(variable, start, end) {
754
+ if (!(variable && start && end)) {
755
+ throw new ScriptBuilderError("Variable, start, and end must be provided for repeatWithRange");
756
+ }
757
+ const startExpr = typeof start === "number" ? start.toString() : start;
758
+ const endExpr = typeof end === "number" ? end.toString() : end;
759
+ this.script.push(
760
+ `${this.getIndentation()}repeat with ${variable} from ${startExpr} to ${endExpr}`
761
+ );
762
+ this.pushBlock("repeat");
763
+ return this;
764
+ }
765
+ exitRepeat() {
766
+ const hasRepeatBlock = this.blockStack.some((block) => block.type === "repeat");
767
+ if (!hasRepeatBlock) {
768
+ throw new ScriptBuilderError("Cannot call exitRepeat(): no repeat block is currently open");
769
+ }
770
+ this.script.push(`${this.getIndentation()}exit repeat`);
771
+ return this;
772
+ }
773
+ exitRepeatIf(condition) {
774
+ const hasRepeatBlock = this.blockStack.some((block) => block.type === "repeat");
775
+ if (!hasRepeatBlock) {
776
+ throw new ScriptBuilderError("Cannot call exitRepeatIf(): no repeat block is currently open");
777
+ }
778
+ const cond = resolveExpression(condition);
779
+ this.script.push(`${this.getIndentation()}if ${cond} then`);
780
+ this.indentLevel++;
781
+ this.script.push(`${this.getIndentation()}exit repeat`);
782
+ this.indentLevel--;
783
+ this.script.push(`${this.getIndentation()}end if`);
784
+ return this;
785
+ }
786
+ continueRepeat() {
787
+ const hasRepeatBlock = this.blockStack.some((block) => block.type === "repeat");
788
+ if (!hasRepeatBlock) {
789
+ throw new ScriptBuilderError(
790
+ "Cannot call continueRepeat(): no repeat block is currently open"
791
+ );
792
+ }
793
+ this.script.push(`${this.getIndentation()}continue repeat`);
794
+ return this;
795
+ }
796
+ considering(attributes) {
797
+ if (!attributes.length) {
798
+ throw new ScriptBuilderError("At least one attribute must be provided for considering");
799
+ }
800
+ this.script.push(`${this.getIndentation()}considering ${attributes.join(", ")}`);
801
+ this.pushBlock("considering");
802
+ return this;
803
+ }
804
+ ignoring(attributes) {
805
+ if (!attributes.length) {
806
+ throw new ScriptBuilderError("At least one attribute must be provided for ignoring");
807
+ }
808
+ this.script.push(`${this.getIndentation()}ignoring ${attributes.join(", ")}`);
809
+ this.pushBlock("ignoring");
810
+ return this;
811
+ }
812
+ using(terms) {
813
+ this.script.push(`${this.getIndentation()}using terms from ${terms.join(", ")}`);
814
+ this.pushBlock("using");
815
+ return this;
816
+ }
817
+ with(timeout, transaction) {
818
+ let command = `${this.getIndentation()}with`;
819
+ if (timeout !== void 0) command += ` timeout of ${timeout}`;
820
+ if (transaction) command += " transaction";
821
+ this.script.push(command);
822
+ this.pushBlock("with");
823
+ return this;
824
+ }
825
+ try() {
826
+ this.script.push(`${this.getIndentation()}try`);
827
+ this.pushBlock("try");
828
+ return this;
829
+ }
830
+ onError(variableName) {
831
+ if (this.blockStack.length === 0 || this.blockStack[this.blockStack.length - 1].type !== "try") {
832
+ throw new ScriptBuilderError("Cannot call onError(): no try block is currently open");
833
+ }
834
+ this.indentLevel--;
835
+ const varPart = variableName ? ` ${variableName}` : "";
836
+ this.script.push(`${this.getIndentation()}on error${varPart}`);
837
+ this.indentLevel++;
838
+ return this;
839
+ }
840
+ error(message, number) {
841
+ let command = `${this.getIndentation()}error "${this.escapeString(message)}"`;
842
+ if (number !== void 0) command += ` number ${number}`;
843
+ this.script.push(command);
844
+ return this;
845
+ }
846
+ return(value) {
847
+ this.script.push(`${this.getIndentation()}return ${this.formatValue(value)}`);
848
+ return this;
849
+ }
850
+ returnRaw(expression) {
851
+ this.script.push(`${this.getIndentation()}return ${expression}`);
852
+ return this;
853
+ }
854
+ /**
855
+ * Build a JSON object string from AppleScript variables.
856
+ * Generates clean, readable JSON without manual string concatenation.
857
+ *
858
+ * @param variableMap Mapping of JSON keys to AppleScript variable names
859
+ * @returns AppleScript expression that evaluates to a JSON string
860
+ *
861
+ * @example
862
+ * // Instead of manual string building:
863
+ * // '"{" & "\\"name\\":\\"" & winName & "\\"}" '
864
+ *
865
+ * // Use:
866
+ * const jsonExpr = builder.buildJsonObject({
867
+ * name: 'winName',
868
+ * position: 'winPosition',
869
+ * size: 'winSize'
870
+ * });
871
+ * builder.returnRaw(jsonExpr);
872
+ *
873
+ * // Generates: '{"name":"Calculator","position":"100,200","size":"800x600"}'
874
+ */
875
+ buildJsonObject(variableMap) {
876
+ const entries = Object.entries(variableMap);
877
+ const parts = ['"{"'];
878
+ entries.forEach(([jsonKey, varName], index) => {
879
+ const comma = index > 0 ? "," : "";
880
+ parts.push(`"${comma}\\"${jsonKey}\\":\\""`);
881
+ parts.push(varName);
882
+ parts.push('"\\""');
883
+ });
884
+ parts.push('"}"');
885
+ return parts.join(" & ");
886
+ }
887
+ /**
888
+ * Build and return a JSON object from AppleScript variables.
889
+ * Convenience method that combines buildJsonObject() with returnRaw().
890
+ *
891
+ * @param variableMap Mapping of JSON keys to AppleScript variable names
892
+ *
893
+ * @example
894
+ * .setExpression('winName', 'name of window 1')
895
+ * .setExpression('winPosition', 'position of window 1 as text')
896
+ * .returnJsonObject({
897
+ * name: 'winName',
898
+ * position: 'winPosition'
899
+ * })
900
+ */
901
+ returnJsonObject(variableMap) {
902
+ const jsonExpr = this.buildJsonObject(variableMap);
903
+ this.returnRaw(jsonExpr);
904
+ return this;
905
+ }
906
+ /**
907
+ * Ultra-convenient shorthand for the common "map collection to JSON" pattern.
908
+ * Replaces verbose manual iteration, property extraction, and JSON conversion.
909
+ *
910
+ * This single method handles:
911
+ * - Creating temporary collection list
912
+ * - Iterating through items (with optional limit/condition)
913
+ * - Extracting properties with smart detection (simple vs complex expressions)
914
+ * - Field-level transformations (firstOf, ifExists, type conversion)
915
+ * - Error handling (skip failed items)
916
+ * - JSON serialization and return
917
+ *
918
+ * @param itemVariable Loop variable name (e.g., 'aNote')
919
+ * @param collection Collection to iterate (e.g., 'every note')
920
+ * @param properties Mapping of JSON keys to AppleScript properties or PropertyExtractor objects
921
+ * @param options Optional: limit, until/while conditions, error handling
922
+ *
923
+ * @example
924
+ * // Simple properties
925
+ * .tell('Notes')
926
+ * .mapToJson('aNote', 'every note', {
927
+ * id: 'id',
928
+ * name: 'name',
929
+ * content: 'plaintext',
930
+ * created: 'creation date of aNote as string',
931
+ * }, { limit: 10, skipErrors: true })
932
+ * .endtell()
933
+ *
934
+ * @example
935
+ * // Advanced field extractors (NEW!)
936
+ * .tell('Contacts')
937
+ * .mapToJson('aPerson', 'every person', {
938
+ * id: 'id',
939
+ * name: 'name',
940
+ * email: { property: (e) => e.property('aPerson', 'emails'), firstOf: true },
941
+ * phone: { property: 'phones', firstOf: true },
942
+ * birthday: { property: 'birth date', ifExists: true, asType: 'string' },
943
+ * }, { limit: 50, skipErrors: true })
944
+ * .endtell()
945
+ */
946
+ mapToJson(itemVariable, collection, properties, options = {}) {
947
+ const listVar = "__collected_items";
948
+ this.set(listVar, []);
949
+ const buildBody = (b) => {
950
+ const finalPropertyMap = {};
951
+ for (const [jsonKey, propDef] of Object.entries(properties)) {
952
+ if (typeof propDef === "string") {
953
+ finalPropertyMap[jsonKey] = propDef;
954
+ } else {
955
+ if (!propDef.property) {
956
+ throw new ScriptBuilderError(
957
+ `PropertyExtractor for "${jsonKey}" must have a "property" field`
958
+ );
959
+ }
960
+ const tempVarName = `__temp_${jsonKey}`;
961
+ const propExpr = typeof propDef.property === "function" ? propDef.property(new ExprBuilder()) : `${propDef.property} of ${itemVariable}`;
962
+ const defaultVal = propDef.default ? typeof propDef.default === "function" ? propDef.default(new ExprBuilder()) : propDef.default : "missing value";
963
+ if (propDef.firstOf) {
964
+ b.setFirstOf(tempVarName, propExpr, defaultVal);
965
+ } else if (propDef.ifExists) {
966
+ b.setIfExists(tempVarName, propExpr, defaultVal, propDef.asType);
967
+ } else {
968
+ b.setExpression(tempVarName, propExpr);
969
+ }
970
+ finalPropertyMap[jsonKey] = tempVarName;
971
+ }
972
+ }
973
+ if (options.skipErrors) {
974
+ b.tryCatch(
975
+ (tryBlock) => tryBlock.pickEndRecord(listVar, itemVariable, finalPropertyMap),
976
+ (catchBlock) => catchBlock.comment("Skip items with errors")
977
+ );
978
+ } else {
979
+ b.pickEndRecord(listVar, itemVariable, finalPropertyMap);
980
+ }
981
+ };
982
+ if (options.limit !== void 0) {
983
+ const limit = options.limit;
984
+ this.set("__counter", 0);
985
+ this.forEachUntil(
986
+ itemVariable,
987
+ collection,
988
+ (e) => e.gte("__counter", limit),
989
+ (b) => {
990
+ b.increment("__counter");
991
+ buildBody(b);
992
+ }
993
+ );
994
+ } else if (options.until !== void 0) {
995
+ this.forEachUntil(itemVariable, collection, options.until, buildBody);
996
+ } else if (options.while !== void 0) {
997
+ this.forEachWhile(itemVariable, collection, options.while, buildBody);
998
+ } else {
999
+ this.forEach(itemVariable, collection, buildBody);
1000
+ }
1001
+ const recordPropertyMap = Object.keys(properties).reduce((acc, key) => {
1002
+ acc[key] = key;
1003
+ return acc;
1004
+ }, {});
1005
+ return this.returnAsJson(listVar, recordPropertyMap);
1006
+ }
1007
+ /**
1008
+ * Return a list of records as a JSON string.
1009
+ * Converts AppleScript records to JSON format by manually building the JSON string.
1010
+ * Handles proper escaping of strings, booleans, numbers, and null values.
1011
+ * @param listVariable Name of the variable containing a list of records
1012
+ * @param propertyMap Mapping of JSON keys to AppleScript property names (e.g., {id: 'noteId', name: 'noteName'})
1013
+ */
1014
+ returnAsJson(listVariable, propertyMap) {
1015
+ const handlers = [
1016
+ "",
1017
+ "on escapeJsonString(str)",
1018
+ " set escapedStr to str",
1019
+ ` set escapedStr to my replaceText(escapedStr, "\\\\", "\\\\\\\\")`,
1020
+ ` set escapedStr to my replaceText(escapedStr, "\\"", "\\\\\\"") `,
1021
+ ` set escapedStr to my replaceText(escapedStr, return, "\\\\n")`,
1022
+ ` set escapedStr to my replaceText(escapedStr, linefeed, "\\\\n")`,
1023
+ ` set escapedStr to my replaceText(escapedStr, tab, "\\\\t")`,
1024
+ " return escapedStr",
1025
+ "end escapeJsonString",
1026
+ "",
1027
+ "on replaceText(theText, searchStr, replaceStr)",
1028
+ ` set AppleScript's text item delimiters to searchStr`,
1029
+ " set textItems to text items of theText",
1030
+ ` set AppleScript's text item delimiters to replaceStr`,
1031
+ " set newText to textItems as text",
1032
+ ` set AppleScript's text item delimiters to ""`,
1033
+ " return newText",
1034
+ "end replaceText",
1035
+ "",
1036
+ "on valueToJson(val)",
1037
+ " if val is missing value then",
1038
+ ` return "null"`,
1039
+ " else if class of val is boolean then",
1040
+ " if val then",
1041
+ ` return "true"`,
1042
+ " else",
1043
+ ` return "false"`,
1044
+ " end if",
1045
+ " else if class of val is integer or class of val is real then",
1046
+ " return val as text",
1047
+ " else",
1048
+ ` return "\\"" & my escapeJsonString(val as text) & "\\""`,
1049
+ " end if",
1050
+ "end valueToJson",
1051
+ ""
1052
+ ];
1053
+ this.script.unshift(...handlers);
1054
+ this.raw("set jsonParts to {}");
1055
+ this.raw(`repeat with rec in ${listVariable}`);
1056
+ this.raw(" try");
1057
+ this.raw(` set itemJson to "{"`);
1058
+ const entries = Object.entries(propertyMap);
1059
+ entries.forEach(([jsonKey, appleScriptProp], index) => {
1060
+ const comma = index > 0 ? "," : "";
1061
+ this.raw(
1062
+ ` set itemJson to itemJson & "${comma}\\"${jsonKey}\\":" & my valueToJson(${appleScriptProp} of rec)`
1063
+ );
1064
+ });
1065
+ this.raw(` set itemJson to itemJson & "}"`);
1066
+ this.raw(" set end of jsonParts to itemJson");
1067
+ this.raw(" end try");
1068
+ this.raw("end repeat");
1069
+ this.raw("");
1070
+ this.raw(`set AppleScript's text item delimiters to ","`);
1071
+ this.raw(`set jsonArray to "[" & (jsonParts as text) & "]"`);
1072
+ this.raw(`set AppleScript's text item delimiters to ""`);
1073
+ this.raw("return jsonArray");
1074
+ return this;
1075
+ }
1076
+ log(message) {
1077
+ this.script.push(`${this.getIndentation()}log "${this.escapeString(message)}"`);
1078
+ return this;
1079
+ }
1080
+ comment(text) {
1081
+ this.script.push(`${this.getIndentation()}-- ${text}`);
1082
+ return this;
1083
+ }
1084
+ // Application control
1085
+ activate() {
1086
+ this.script.push(`${this.getIndentation()}activate`);
1087
+ return this;
1088
+ }
1089
+ quit() {
1090
+ this.script.push(`${this.getIndentation()}quit`);
1091
+ return this;
1092
+ }
1093
+ reopen() {
1094
+ this.script.push(`${this.getIndentation()}reopen`);
1095
+ return this;
1096
+ }
1097
+ launch() {
1098
+ this.script.push(`${this.getIndentation()}launch`);
1099
+ return this;
1100
+ }
1101
+ running() {
1102
+ this.script.push(`${this.getIndentation()}running`);
1103
+ return this;
1104
+ }
1105
+ // Window management
1106
+ closeWindow(window) {
1107
+ if (window) {
1108
+ this.script.push(`${this.getIndentation()}close window "${this.escapeString(window)}"`);
1109
+ } else {
1110
+ this.script.push(`${this.getIndentation()}close front window`);
1111
+ }
1112
+ return this;
1113
+ }
1114
+ closeAllWindows() {
1115
+ this.script.push(`${this.getIndentation()}close every window`);
1116
+ return this;
1117
+ }
1118
+ minimizeWindow(window) {
1119
+ if (window) {
1120
+ this.script.push(
1121
+ `${this.getIndentation()}set miniaturized of window "${this.escapeString(window)}" to true`
1122
+ );
1123
+ } else {
1124
+ this.script.push(`${this.getIndentation()}set miniaturized of front window to true`);
1125
+ }
1126
+ return this;
1127
+ }
1128
+ zoomWindow(window) {
1129
+ if (window) {
1130
+ this.script.push(
1131
+ `${this.getIndentation()}set zoomed of window "${this.escapeString(window)}" to true`
1132
+ );
1133
+ } else {
1134
+ this.script.push(`${this.getIndentation()}set zoomed of front window to true`);
1135
+ }
1136
+ return this;
1137
+ }
1138
+ // UI interaction
1139
+ click(target) {
1140
+ this.script.push(`${this.getIndentation()}click ${target}`);
1141
+ return this;
1142
+ }
1143
+ select(target) {
1144
+ this.script.push(`${this.getIndentation()}select ${target}`);
1145
+ return this;
1146
+ }
1147
+ keystroke(text, modifiers) {
1148
+ const modString = modifiers?.length ? ` using {${modifiers.join(", ")}}` : "";
1149
+ this.script.push(`${this.getIndentation()}keystroke "${this.escapeString(text)}"${modString}`);
1150
+ return this;
1151
+ }
1152
+ /**
1153
+ * Type multiple characters with automatic delays between each keystroke.
1154
+ * Convenient shorthand for typing sequences like numbers or text.
1155
+ * @param text String of characters to type (each character gets a separate keystroke)
1156
+ * @param delayBetween Delay in seconds between each keystroke (default: 0.1)
1157
+ * @example
1158
+ * // Instead of:
1159
+ * // .keystroke('1').delay(0.1).keystroke('2').delay(0.1).keystroke('3')
1160
+ * // Use:
1161
+ * // .keystrokes('123')
1162
+ */
1163
+ keystrokes(text, delayBetween = 0.1) {
1164
+ const chars = text.split("");
1165
+ chars.forEach((char, index) => {
1166
+ this.keystroke(char);
1167
+ if (index < chars.length - 1) {
1168
+ this.delay(delayBetween);
1169
+ }
1170
+ });
1171
+ return this;
1172
+ }
1173
+ delay(seconds) {
1174
+ this.script.push(`${this.getIndentation()}delay ${seconds}`);
1175
+ return this;
1176
+ }
1177
+ // Dialog and alerts
1178
+ displayDialog(text, options = {}) {
1179
+ let command = `${this.getIndentation()}display dialog "${this.escapeString(text)}"`;
1180
+ if (options.buttons?.length) {
1181
+ const escapedButtons = options.buttons.map((b) => this.escapeString(b));
1182
+ command += ` buttons {"${escapedButtons.join('", "')}"}`;
1183
+ }
1184
+ if (options.defaultButton) {
1185
+ command += ` default button "${this.escapeString(options.defaultButton)}"`;
1186
+ }
1187
+ if (options.withIcon) {
1188
+ command += ` with icon ${options.withIcon}`;
1189
+ }
1190
+ if (options.givingUpAfter) {
1191
+ command += ` giving up after ${options.givingUpAfter}`;
1192
+ }
1193
+ this.script.push(command);
1194
+ return this;
1195
+ }
1196
+ displayNotification(text, options = {}) {
1197
+ let command = `${this.getIndentation()}display notification "${this.escapeString(text)}"`;
1198
+ if (options.title) {
1199
+ command += ` with title "${this.escapeString(options.title)}"`;
1200
+ }
1201
+ if (options.subtitle) {
1202
+ command += ` subtitle "${this.escapeString(options.subtitle)}"`;
1203
+ }
1204
+ if (options.sound) {
1205
+ command += ` sound name "${this.escapeString(options.sound)}"`;
1206
+ }
1207
+ this.script.push(command);
1208
+ return this;
1209
+ }
1210
+ // Variables and properties
1211
+ set(variable, value) {
1212
+ this.script.push(`${this.getIndentation()}set ${variable} to ${this.formatValue(value)}`);
1213
+ return this;
1214
+ }
1215
+ setExpression(variable, expression) {
1216
+ let expr2;
1217
+ if (typeof expression === "function") {
1218
+ expr2 = expression(new ExprBuilder());
1219
+ } else if (typeof expression === "string") {
1220
+ expr2 = expression;
1221
+ } else {
1222
+ expr2 = this.makeRecordFrom(expression);
1223
+ }
1224
+ this.script.push(`${this.getIndentation()}set ${variable} to ${expr2}`);
1225
+ return this;
1226
+ }
1227
+ setExpressions(expressions) {
1228
+ for (const [variable, expression] of Object.entries(expressions)) {
1229
+ const expr2 = resolveExpression(expression);
1230
+ this.script.push(`${this.getIndentation()}set ${variable} to ${expr2}`);
1231
+ }
1232
+ return this;
1233
+ }
1234
+ appendTo(variable, expression, options) {
1235
+ let expr2 = resolveExpression(expression);
1236
+ if (options?.prependLinefeed) {
1237
+ expr2 = `linefeed & ${expr2}`;
1238
+ }
1239
+ if (options?.appendLinefeed) {
1240
+ expr2 = `${expr2} & linefeed`;
1241
+ }
1242
+ this.script.push(`${this.getIndentation()}set ${variable} to ${variable} & ${expr2}`);
1243
+ return this;
1244
+ }
1245
+ increment(variable, by = 1) {
1246
+ this.script.push(`${this.getIndentation()}set ${variable} to ${variable} + ${by}`);
1247
+ return this;
1248
+ }
1249
+ decrement(variable, by = 1) {
1250
+ this.script.push(`${this.getIndentation()}set ${variable} to ${variable} - ${by}`);
1251
+ return this;
1252
+ }
1253
+ get(property) {
1254
+ this.script.push(`${this.getIndentation()}get ${property}`);
1255
+ return this;
1256
+ }
1257
+ copy(value, to) {
1258
+ this.script.push(`${this.getIndentation()}copy ${this.formatValue(value)} to ${to}`);
1259
+ return this;
1260
+ }
1261
+ count(items) {
1262
+ this.script.push(`${this.getIndentation()}count ${items}`);
1263
+ return this;
1264
+ }
1265
+ setCountOf(variable, items) {
1266
+ this.script.push(`${this.getIndentation()}set ${variable} to count of (${items})`);
1267
+ return this;
1268
+ }
1269
+ exists(item) {
1270
+ this.script.push(`${this.getIndentation()}exists ${item}`);
1271
+ return this;
1272
+ }
1273
+ setEnd(variable, value) {
1274
+ this.script.push(
1275
+ `${this.getIndentation()}set end of ${variable} to ${this.formatValue(value)}`
1276
+ );
1277
+ return this;
1278
+ }
1279
+ setEndRaw(variable, expression) {
1280
+ let expr2;
1281
+ if (typeof expression === "function") {
1282
+ expr2 = expression(new ExprBuilder());
1283
+ } else if (typeof expression === "string") {
1284
+ expr2 = expression;
1285
+ } else {
1286
+ expr2 = this.makeRecordFrom(expression);
1287
+ }
1288
+ this.script.push(`${this.getIndentation()}set end of ${variable} to ${expr2}`);
1289
+ return this;
1290
+ }
1291
+ setEndRecord(listVariable, sourceOrExpressions, propertyMap) {
1292
+ let recordExpressions;
1293
+ if (typeof sourceOrExpressions === "string") {
1294
+ if (!propertyMap) {
1295
+ throw new ScriptBuilderError(
1296
+ "propertyMap is required when sourceOrExpressions is a source object name"
1297
+ );
1298
+ }
1299
+ const sourceObj = sourceOrExpressions;
1300
+ recordExpressions = Object.entries(propertyMap).reduce(
1301
+ (acc, [key, prop]) => {
1302
+ acc[key] = `${prop} of ${sourceObj}`;
1303
+ return acc;
1304
+ },
1305
+ {}
1306
+ );
1307
+ } else {
1308
+ recordExpressions = sourceOrExpressions;
1309
+ }
1310
+ const recordStr = this.makeRecordFrom(recordExpressions);
1311
+ this.script.push(`${this.getIndentation()}set end of ${listVariable} to ${recordStr}`);
1312
+ return this;
1313
+ }
1314
+ /**
1315
+ * Intuitive shorthand for picking properties from a source object and building a record.
1316
+ * Automatically detects full expressions vs simple property names:
1317
+ * - Simple properties (no special keywords) get "of source" appended
1318
+ * - Complex expressions (with 'of', 'as', 'where', etc.) are used as-is
1319
+ *
1320
+ * @param listVariable Name of the list to append the record to
1321
+ * @param sourceObject Name of the source object to extract properties from
1322
+ * @param propertyMap Mapping of record keys to property names/expressions
1323
+ *
1324
+ * @example
1325
+ * // Mix simple properties and complex expressions
1326
+ * .pickEndRecord('notesList', 'aNote', {
1327
+ * noteId: 'id', // => id of aNote
1328
+ * noteName: 'name', // => name of aNote
1329
+ * noteCreated: 'creation date of aNote as string', // used as-is (has 'as')
1330
+ * noteModified: 'modification date as string', // used as-is (has 'as')
1331
+ * })
1332
+ */
1333
+ pickEndRecord(listVariable, sourceObject, propertyMap) {
1334
+ const recordExpressions = Object.entries(propertyMap).reduce(
1335
+ (acc, [key, prop]) => {
1336
+ const isFullExpression = prop.startsWith("__temp_") || prop.includes(" of ") || prop.includes(" where ") || prop.includes(" as ") || prop.includes(" whose ") || prop.includes(" thru ") || prop.includes("every ") || prop.includes("some ") || prop.includes("first ") || prop.includes("last ") || prop.includes("count ") || prop.includes("length ") || prop.includes(" contains ") || prop.includes(" begins with ") || prop.includes(" ends with ");
1337
+ acc[key] = isFullExpression ? prop : `${prop} of ${sourceObject}`;
1338
+ return acc;
1339
+ },
1340
+ {}
1341
+ );
1342
+ const recordStr = this.makeRecordFrom(recordExpressions);
1343
+ this.script.push(`${this.getIndentation()}set end of ${listVariable} to ${recordStr}`);
1344
+ return this;
1345
+ }
1346
+ /**
1347
+ * Set variable using ternary operator pattern with if-then-else block.
1348
+ * Much more concise than manually calling ifThenElse for simple conditional assignments.
1349
+ *
1350
+ * Generates an if-then-else block that sets the variable conditionally.
1351
+ *
1352
+ * @param variable - Variable name to set
1353
+ * @param condition - Condition to evaluate (string or ExprBuilder callback)
1354
+ * @param trueExpression - Expression to use if condition is true
1355
+ * @param falseExpression - Expression to use if condition is false
1356
+ *
1357
+ * @example
1358
+ * // With ExprBuilder for type-safe conditions
1359
+ * .setTernary('personEmail',
1360
+ * (e) => e.gt(e.count(e.property('aPerson', 'emails')), 0),
1361
+ * (e) => e.valueOfItem(1, e.property('aPerson', 'emails')),
1362
+ * 'missing value'
1363
+ * )
1364
+ *
1365
+ * @example
1366
+ * // With strings
1367
+ * .setTernary('status',
1368
+ * 'count of items > 0',
1369
+ * '"active"',
1370
+ * '"empty"'
1371
+ * )
1372
+ *
1373
+ * @example
1374
+ * // Replaces verbose ifThenElse:
1375
+ * // .ifThenElse(
1376
+ * // (e) => e.gt('x', 10),
1377
+ * // (then_) => then_.set('result', 'high'),
1378
+ * // (else_) => else_.set('result', 'low')
1379
+ * // )
1380
+ * // With concise ternary:
1381
+ * .setTernary('result', (e) => e.gt('x', 10), '"high"', '"low"')
1382
+ */
1383
+ setTernary(variable, condition, trueExpression, falseExpression) {
1384
+ const cond = resolveExpression(condition);
1385
+ const trueExpr = resolveExpression(trueExpression);
1386
+ const falseExpr = resolveExpression(falseExpression);
1387
+ return this.ifThenElse(
1388
+ cond,
1389
+ (then_) => then_.setExpression(variable, trueExpr),
1390
+ (else_) => else_.setExpression(variable, falseExpr)
1391
+ );
1392
+ }
1393
+ /**
1394
+ * Append to list using ternary operator pattern with if-then-else block.
1395
+ * Combines setEndRaw with conditional logic for ultra-concise syntax.
1396
+ *
1397
+ * Generates an if-then-else block that conditionally appends to the list.
1398
+ *
1399
+ * @param listVariable - Name of the list to append to
1400
+ * @param condition - Condition to evaluate (string or ExprBuilder callback)
1401
+ * @param trueExpression - Expression to append if condition is true
1402
+ * @param falseExpression - Expression to append if condition is false
1403
+ *
1404
+ * @example
1405
+ * // Conditionally append different values
1406
+ * .set('results', [])
1407
+ * .forEach('item', 'every file of desktop', (loop) =>
1408
+ * loop.setEndTernary('results',
1409
+ * (e) => e.gt(e.property('item', 'size'), 1000000),
1410
+ * '"large"',
1411
+ * '"small"'
1412
+ * )
1413
+ * )
1414
+ *
1415
+ * @example
1416
+ * // With complex expressions
1417
+ * .setEndTernary('emails',
1418
+ * 'count of email addresses of person > 0',
1419
+ * 'value of item 1 of email addresses of person',
1420
+ * 'missing value'
1421
+ * )
1422
+ */
1423
+ setEndTernary(listVariable, condition, trueExpression, falseExpression) {
1424
+ const cond = resolveExpression(condition);
1425
+ const trueExpr = resolveExpression(trueExpression);
1426
+ const falseExpr = resolveExpression(falseExpression);
1427
+ return this.ifThenElse(
1428
+ cond,
1429
+ (then_) => then_.setEndRaw(listVariable, trueExpr),
1430
+ (else_) => else_.setEndRaw(listVariable, falseExpr)
1431
+ );
1432
+ }
1433
+ /**
1434
+ * Set variable to first item of collection or default value if collection is empty.
1435
+ * Ultra-shorthand for the common "first or default" pattern.
1436
+ *
1437
+ * Automatically generates an if-then-else block that checks the collection count and sets the variable accordingly.
1438
+ *
1439
+ * @template TNewVar - The variable name (inferred from first parameter)
1440
+ * @param variable - Variable name to set (will be added to scope)
1441
+ * @param collection - Collection expression to get first item from
1442
+ * @param defaultValue - Value to use if collection is empty (default: 'missing value')
1443
+ *
1444
+ * @example
1445
+ * // Get first email or missing value
1446
+ * .setFirstOf('personEmail', (e) => e.property('aPerson', 'emails'))
1447
+ *
1448
+ * @example
1449
+ * // Get first email or custom default
1450
+ * .setFirstOf('personEmail', (e) => e.property('aPerson', 'emails'), '"no-email@example.com"')
1451
+ *
1452
+ * @example
1453
+ * // With string expression
1454
+ * .setFirstOf('personEmail', 'emails of aPerson', 'missing value')
1455
+ *
1456
+ * @example
1457
+ * // Replaces verbose pattern:
1458
+ * // .setTernary('personEmail',
1459
+ * // (e) => e.gt(e.count(e.property('aPerson', 'emails')), 0),
1460
+ * // (e) => e.valueOfItem(1, e.property('aPerson', 'emails')),
1461
+ * // 'missing value'
1462
+ * // )
1463
+ */
1464
+ setFirstOf(variable, collection, defaultValue = "missing value") {
1465
+ const coll = resolveExpression(collection);
1466
+ const defaultVal = resolveExpression(defaultValue);
1467
+ return this.ifThenElse(
1468
+ `count of ${coll} > 0`,
1469
+ (then_) => then_.setExpression(variable, `value of item 1 of ${coll}`),
1470
+ (else_) => else_.setExpression(variable, defaultVal)
1471
+ );
1472
+ }
1473
+ /**
1474
+ * Append first item of collection or default value to a list.
1475
+ * Ultra-shorthand for the common "first or default" pattern in list building.
1476
+ *
1477
+ * Automatically generates an if-then-else block that checks the collection count and appends to the list accordingly.
1478
+ *
1479
+ * @param listVariable - Name of the list to append to
1480
+ * @param collection - Collection expression to get first item from
1481
+ * @param defaultValue - Value to use if collection is empty (default: 'missing value')
1482
+ *
1483
+ * @example
1484
+ * // Build list of first emails
1485
+ * .forEach('person', 'every person', (loop) =>
1486
+ * loop.setEndFirstOf('emails', (e) => e.property('person', 'email addresses'))
1487
+ * )
1488
+ *
1489
+ * @example
1490
+ * // With custom default
1491
+ * .setEndFirstOf('results', 'items of record', '""')
1492
+ */
1493
+ setEndFirstOf(listVariable, collection, defaultValue = "missing value") {
1494
+ const coll = resolveExpression(collection);
1495
+ const defaultVal = resolveExpression(defaultValue);
1496
+ return this.ifThenElse(
1497
+ `count of ${coll} > 0`,
1498
+ (then_) => then_.setEndRaw(listVariable, `value of item 1 of ${coll}`),
1499
+ (else_) => else_.setEndRaw(listVariable, defaultVal)
1500
+ );
1501
+ }
1502
+ /**
1503
+ * Set variable to property value if it exists, otherwise use default value.
1504
+ * Ultra-shorthand for the common "take if exists or default" pattern.
1505
+ *
1506
+ * Automatically generates an if-then-else block that checks if the property exists
1507
+ * and sets the variable accordingly, with optional type conversion.
1508
+ *
1509
+ * @template TNewVar - The variable name (inferred from first parameter)
1510
+ * @param variable - Variable name to set (will be added to scope)
1511
+ * @param property - Property expression to check and retrieve
1512
+ * @param defaultValue - Value to use if property doesn't exist (default: 'missing value')
1513
+ * @param asType - Optional type to convert property to (e.g., 'string', 'integer')
1514
+ *
1515
+ * @example
1516
+ * // Get birth date as string or missing value
1517
+ * .setIfExists('personBirthday',
1518
+ * (e) => e.property('aPerson', 'birth date'),
1519
+ * 'missing value',
1520
+ * 'string'
1521
+ * )
1522
+ *
1523
+ * @example
1524
+ * // Simpler syntax with string expression
1525
+ * .setIfExists('personBirthday', 'birth date of aPerson', 'missing value', 'string')
1526
+ *
1527
+ * @example
1528
+ * // Without type conversion
1529
+ * .setIfExists('personNote', 'note of aPerson', '""')
1530
+ *
1531
+ * @example
1532
+ * // Replaces verbose pattern:
1533
+ * // .setTernary('personBirthday',
1534
+ * // (e) => e.exists(e.property('aPerson', 'birth date')),
1535
+ * // (e) => e.asType(e.property('aPerson', 'birth date'), 'string'),
1536
+ * // 'missing value'
1537
+ * // )
1538
+ */
1539
+ setIfExists(variable, property, defaultValue = "missing value", asType) {
1540
+ const prop = resolveExpression(property);
1541
+ const defaultVal = resolveExpression(defaultValue);
1542
+ const valueExpr = asType ? `${prop} as ${asType}` : prop;
1543
+ return this.ifThenElse(
1544
+ `exists ${prop}`,
1545
+ (then_) => then_.setExpression(variable, valueExpr),
1546
+ (else_) => else_.setExpression(variable, defaultVal)
1547
+ );
1548
+ }
1549
+ /**
1550
+ * Append property value to list if it exists, otherwise append default value.
1551
+ * Ultra-shorthand for the common "take if exists or default" pattern in list building.
1552
+ *
1553
+ * Automatically generates an if-then-else block that checks if the property exists
1554
+ * and appends to the list accordingly, with optional type conversion.
1555
+ *
1556
+ * @param listVariable - Name of the list to append to
1557
+ * @param property - Property expression to check and retrieve
1558
+ * @param defaultValue - Value to use if property doesn't exist (default: 'missing value')
1559
+ * @param asType - Optional type to convert property to (e.g., 'string', 'integer')
1560
+ *
1561
+ * @example
1562
+ * // Build list of birth dates
1563
+ * .forEach('person', 'every person', (loop) =>
1564
+ * loop.setEndIfExists('dates', 'birth date of person', '""', 'string')
1565
+ * )
1566
+ *
1567
+ * @example
1568
+ * // With ExprBuilder
1569
+ * .setEndIfExists('notes',
1570
+ * (e) => e.property('contact', 'note'),
1571
+ * 'missing value'
1572
+ * )
1573
+ */
1574
+ setEndIfExists(listVariable, property, defaultValue = "missing value", asType) {
1575
+ const prop = resolveExpression(property);
1576
+ const defaultVal = resolveExpression(defaultValue);
1577
+ const valueExpr = asType ? `${prop} as ${asType}` : prop;
1578
+ return this.ifThenElse(
1579
+ `exists ${prop}`,
1580
+ (then_) => then_.setEndRaw(listVariable, valueExpr),
1581
+ (else_) => else_.setEndRaw(listVariable, defaultVal)
1582
+ );
1583
+ }
1584
+ setProperty(variable, property, value) {
1585
+ this.script.push(
1586
+ `${this.getIndentation()}set ${property} of ${variable} to ${this.formatValue(value)}`
1587
+ );
1588
+ return this;
1589
+ }
1590
+ makeRecordFrom(variableNames) {
1591
+ const entries = Object.entries(variableNames).map(([key, varName]) => `${key}:${varName}`).join(", ");
1592
+ return `{${entries}}`;
1593
+ }
1594
+ // List operations
1595
+ first(items) {
1596
+ this.script.push(`${this.getIndentation()}first item of ${items}`);
1597
+ return this;
1598
+ }
1599
+ last(items) {
1600
+ this.script.push(`${this.getIndentation()}last item of ${items}`);
1601
+ return this;
1602
+ }
1603
+ rest(items) {
1604
+ this.script.push(`${this.getIndentation()}rest of ${items}`);
1605
+ return this;
1606
+ }
1607
+ reverse(items) {
1608
+ this.script.push(`${this.getIndentation()}reverse of ${items}`);
1609
+ return this;
1610
+ }
1611
+ some(items, test) {
1612
+ this.script.push(`${this.getIndentation()}some item of ${items} where ${test}`);
1613
+ return this;
1614
+ }
1615
+ every(items, test) {
1616
+ this.script.push(`${this.getIndentation()}every item of ${items} where ${test}`);
1617
+ return this;
1618
+ }
1619
+ whose(items, condition) {
1620
+ return `${items} whose ${condition}`;
1621
+ }
1622
+ getEvery(itemType, location) {
1623
+ const loc = location ? ` of ${location}` : "";
1624
+ this.script.push(`${this.getIndentation()}get every ${itemType}${loc}`);
1625
+ return this;
1626
+ }
1627
+ getEveryWhere(itemType, condition, location) {
1628
+ const loc = location ? ` of ${location}` : "";
1629
+ this.script.push(`${this.getIndentation()}get every ${itemType}${loc} where ${condition}`);
1630
+ return this;
1631
+ }
1632
+ // Text operations
1633
+ offset(text, in_) {
1634
+ this.script.push(`${this.getIndentation()}offset of ${text} in ${in_}`);
1635
+ return this;
1636
+ }
1637
+ contains(text, in_) {
1638
+ this.script.push(`${this.getIndentation()}${in_} contains ${text}`);
1639
+ return this;
1640
+ }
1641
+ beginsWith(text, with_) {
1642
+ this.script.push(`${this.getIndentation()}${text} begins with ${with_}`);
1643
+ return this;
1644
+ }
1645
+ endsWith(text, with_) {
1646
+ this.script.push(`${this.getIndentation()}${text} ends with ${with_}`);
1647
+ return this;
1648
+ }
1649
+ // System operations
1650
+ path(to) {
1651
+ this.script.push(`${this.getIndentation()}path to ${to}`);
1652
+ return this;
1653
+ }
1654
+ info(for_) {
1655
+ this.script.push(`${this.getIndentation()}info for ${for_}`);
1656
+ return this;
1657
+ }
1658
+ do(script) {
1659
+ this.script.push(`${this.getIndentation()}do script "${this.escapeString(script)}"`);
1660
+ return this;
1661
+ }
1662
+ doShellScript(command, administrator) {
1663
+ let cmd = `${this.getIndentation()}do shell script "${this.escapeString(command)}"`;
1664
+ if (administrator) {
1665
+ cmd += " with administrator privileges";
1666
+ }
1667
+ this.script.push(cmd);
1668
+ return this;
1669
+ }
1670
+ // Raw script and building
1671
+ raw(script) {
1672
+ this.script.push(`${this.getIndentation()}${script}`);
1673
+ return this;
1674
+ }
1675
+ // Enhanced Application control
1676
+ getRunningApplications() {
1677
+ this.raw(
1678
+ 'tell application "System Events" to return {name, bundle identifier, visible, frontmost} of every process where background only is false'
1679
+ );
1680
+ return this;
1681
+ }
1682
+ getFrontmostApplication() {
1683
+ this.raw(
1684
+ 'tell application "System Events" to return name of first process where frontmost is true'
1685
+ );
1686
+ return this;
1687
+ }
1688
+ activateApplication(appName) {
1689
+ this.raw(`tell application "${this.escapeString(appName)}" to activate`);
1690
+ return this;
1691
+ }
1692
+ hideApplication(appName) {
1693
+ this.raw(
1694
+ `tell application "System Events" to tell process "${this.escapeString(appName)}" to set visible to false`
1695
+ );
1696
+ return this;
1697
+ }
1698
+ unhideApplication(appName) {
1699
+ this.raw(
1700
+ `tell application "System Events" to tell process "${this.escapeString(appName)}" to set visible to true`
1701
+ );
1702
+ return this;
1703
+ }
1704
+ quitApplication(appName) {
1705
+ this.raw(`tell application "${this.escapeString(appName)}" to quit`);
1706
+ return this;
1707
+ }
1708
+ isApplicationRunning(appName) {
1709
+ this.raw(
1710
+ `tell application "System Events" to return exists (processes where name is "${this.escapeString(appName)}")`
1711
+ );
1712
+ return this;
1713
+ }
1714
+ getApplicationInfo(appName) {
1715
+ this.raw(
1716
+ `tell application "System Events" to tell process "${this.escapeString(appName)}" to return properties`
1717
+ );
1718
+ return this;
1719
+ }
1720
+ // Enhanced Window management
1721
+ getWindowInfo(appName, windowName) {
1722
+ this.raw(
1723
+ windowName ? `tell application "${this.escapeString(appName)}" to tell window "${this.escapeString(windowName)}" to return {name, id, bounds, miniaturized, zoomed}` : `tell application "${this.escapeString(appName)}" to tell front window to return {name, id, bounds, miniaturized, zoomed}`
1724
+ );
1725
+ return this;
1726
+ }
1727
+ getAllWindows(appName) {
1728
+ this.raw(
1729
+ `tell application "${this.escapeString(appName)}" to return {name, id, bounds, miniaturized, zoomed} of every window`
1730
+ );
1731
+ return this;
1732
+ }
1733
+ getFrontmostWindow(appName) {
1734
+ this.raw(
1735
+ `tell application "${this.escapeString(appName)}" to tell front window to return {name, id, bounds, miniaturized, zoomed}`
1736
+ );
1737
+ return this;
1738
+ }
1739
+ setWindowBounds(appName, windowName, bounds) {
1740
+ this.raw(
1741
+ `tell application "${this.escapeString(appName)}" to tell window "${this.escapeString(windowName)}" to set bounds to {${bounds.x}, ${bounds.y}, ${bounds.x + bounds.width}, ${bounds.y + bounds.height}}`
1742
+ );
1743
+ return this;
1744
+ }
1745
+ moveWindow(appName, windowName, x, y) {
1746
+ this.raw(
1747
+ `tell application "${this.escapeString(appName)}" to tell window "${this.escapeString(windowName)}" to set position to {${x}, ${y}}`
1748
+ );
1749
+ return this;
1750
+ }
1751
+ resizeWindow(appName, windowName, width, height) {
1752
+ this.raw(
1753
+ `tell application "${this.escapeString(appName)}" to tell window "${this.escapeString(windowName)}" to set size to {${width}, ${height}}`
1754
+ );
1755
+ return this;
1756
+ }
1757
+ arrangeWindows(arrangement) {
1758
+ this.raw(`tell application "System Events" to tell process "Finder" to ${arrangement} windows`);
1759
+ return this;
1760
+ }
1761
+ focusWindow(appName, windowName) {
1762
+ this.raw(`tell application "${this.escapeString(appName)}" to activate`);
1763
+ this.raw(
1764
+ `tell application "${this.escapeString(appName)}" to tell window "${this.escapeString(windowName)}" to set index to 1`
1765
+ );
1766
+ return this;
1767
+ }
1768
+ switchToWindow(appName, windowName) {
1769
+ return this.focusWindow(appName, windowName);
1770
+ }
1771
+ // Enhanced UI interaction
1772
+ pressKey(key, modifiers) {
1773
+ const modString = modifiers?.length ? ` using {${modifiers.join(", ")}}` : "";
1774
+ this.raw(`tell application "System Events" to key code ${key}${modString}`);
1775
+ return this;
1776
+ }
1777
+ pressKeyCode(keyCode, modifiers) {
1778
+ const modString = modifiers?.length ? ` using {${modifiers.join(", ")}}` : "";
1779
+ this.raw(`tell application "System Events" to key code ${keyCode}${modString}`);
1780
+ return this;
1781
+ }
1782
+ typeText(text) {
1783
+ this.raw(`tell application "System Events" to keystroke "${this.escapeString(text)}"`);
1784
+ return this;
1785
+ }
1786
+ clickButton(buttonName) {
1787
+ this.raw(`tell application "System Events" to click button "${this.escapeString(buttonName)}"`);
1788
+ return this;
1789
+ }
1790
+ clickMenuItem(menuName, itemName) {
1791
+ this.raw(
1792
+ `tell application "System Events" to click menu item "${this.escapeString(itemName)}" of menu "${this.escapeString(menuName)}" of menu bar 1`
1793
+ );
1794
+ return this;
1795
+ }
1796
+ // Convenience helpers for cleaner API
1797
+ /**
1798
+ * Simplified tell application pattern with automatic block closing.
1799
+ * Cleaner than manually calling tell()...end().
1800
+ * @param appName Name of the application to tell
1801
+ * @param block Callback that builds commands for the application
1802
+ */
1803
+ tellApp(appName, block) {
1804
+ this.tell(appName);
1805
+ block(this);
1806
+ return this.end();
1807
+ }
1808
+ /**
1809
+ * Simplified if-then pattern that automatically closes the if block.
1810
+ * Cleaner than manually calling if()...thenBlock()...endif().
1811
+ * @param condition The condition to check (string or ExprBuilder callback)
1812
+ * @param thenBlock Callback that builds the then branch
1813
+ */
1814
+ ifThen(condition, thenBlock) {
1815
+ this.if(condition).thenBlock();
1816
+ thenBlock(this);
1817
+ return this.endif();
1818
+ }
1819
+ /**
1820
+ * Simplified if-then-else pattern that automatically closes the if block.
1821
+ * Cleaner than manually calling if()...thenBlock()...else()...endif().
1822
+ * @param condition The condition to check (string or ExprBuilder callback)
1823
+ * @param thenBlock Callback that builds the then branch
1824
+ * @param elseBlock Callback that builds the else branch
1825
+ */
1826
+ ifThenElse(condition, thenBlock, elseBlock) {
1827
+ this.if(condition).thenBlock();
1828
+ thenBlock(this);
1829
+ this.else();
1830
+ elseBlock(this);
1831
+ return this.endif();
1832
+ }
1833
+ /**
1834
+ * Simplified try-catch pattern that automatically closes the try block.
1835
+ * Cleaner than manually calling try()...onError()...endtry().
1836
+ * @param tryBlock Callback that builds the try branch
1837
+ * @param catchBlock Callback that builds the on error branch
1838
+ */
1839
+ tryCatch(tryBlock, catchBlock) {
1840
+ this.try();
1841
+ tryBlock(this);
1842
+ this.onError();
1843
+ catchBlock(this);
1844
+ return this.endtry();
1845
+ }
1846
+ /**
1847
+ * Simplified try-catch pattern with error variable capture.
1848
+ * @param tryBlock Callback that builds the try branch
1849
+ * @param errorVarName Name of the variable to capture the error
1850
+ * @param catchBlock Callback that builds the on error branch
1851
+ */
1852
+ tryCatchError(tryBlock, errorVarName, catchBlock) {
1853
+ this.try();
1854
+ tryBlock(this);
1855
+ this.onError(errorVarName);
1856
+ catchBlock(this);
1857
+ return this.endtry();
1858
+ }
1859
+ /**
1860
+ * Iterate over items with automatic block closing.
1861
+ * More intuitive name for repeatWith that uses callback pattern.
1862
+ * @param variable Loop variable name
1863
+ * @param list Expression for the list to iterate (e.g., 'every note')
1864
+ * @param block Callback that builds the loop body
1865
+ */
1866
+ forEach(variable, list, block) {
1867
+ this.repeatWith(variable, list);
1868
+ block(this);
1869
+ return this.endrepeat();
1870
+ }
1871
+ /**
1872
+ * Iterate over items while a condition is true.
1873
+ * Combines forEach with an early exit condition for cleaner syntax.
1874
+ * @param variable Loop variable name
1875
+ * @param list Expression for the list to iterate (e.g., 'every note')
1876
+ * @param condition Condition to check before each iteration (continues while true)
1877
+ * @param block Callback that builds the loop body
1878
+ */
1879
+ forEachWhile(variable, list, condition, block) {
1880
+ this.repeatWith(variable, list);
1881
+ this.ifThen(
1882
+ typeof condition === "function" ? (e) => e.not(condition(e)) : (e) => e.not(condition),
1883
+ (b) => b.exitRepeat()
1884
+ );
1885
+ block(this);
1886
+ return this.endrepeat();
1887
+ }
1888
+ /**
1889
+ * Iterate over items until a condition becomes true.
1890
+ * Combines forEach with an early exit condition for cleaner syntax.
1891
+ * @param variable Loop variable name
1892
+ * @param list Expression for the list to iterate (e.g., 'every note')
1893
+ * @param condition Condition to check before each iteration (exits when true)
1894
+ * @param block Callback that builds the loop body
1895
+ */
1896
+ forEachUntil(variable, list, condition, block) {
1897
+ this.repeatWith(variable, list);
1898
+ this.ifThen(condition, (b) => b.exitRepeat());
1899
+ block(this);
1900
+ return this.endrepeat();
1901
+ }
1902
+ /**
1903
+ * Repeat a fixed number of times with automatic block closing.
1904
+ * @param times Number of times to repeat
1905
+ * @param block Callback that builds the loop body
1906
+ */
1907
+ repeatTimes(times, block) {
1908
+ this.repeat(times);
1909
+ block(this);
1910
+ return this.endrepeat();
1911
+ }
1912
+ /**
1913
+ * Repeat while condition is true with automatic block closing.
1914
+ * @param condition Condition to check (continues while true)
1915
+ * @param block Callback that builds the loop body
1916
+ */
1917
+ repeatWhileBlock(condition, block) {
1918
+ this.repeatWhile(condition);
1919
+ block(this);
1920
+ return this.endrepeat();
1921
+ }
1922
+ /**
1923
+ * Repeat until condition is true with automatic block closing.
1924
+ * @param condition Condition to check (continues until true)
1925
+ * @param block Callback that builds the loop body
1926
+ */
1927
+ repeatUntilBlock(condition, block) {
1928
+ this.repeatUntil(condition);
1929
+ block(this);
1930
+ return this.endrepeat();
1931
+ }
1932
+ /**
1933
+ * Load an existing AppleScript into the builder, preserving its structure.
1934
+ * Parses the script to detect block structure (tell, if, repeat, etc.) and
1935
+ * maintains proper nesting so you can continue editing with the builder API.
1936
+ * @param script The AppleScript source code to load
1937
+ * @returns This builder instance for method chaining
1938
+ */
1939
+ loadFromScript(script) {
1940
+ const lines = script.split("\n");
1941
+ for (const line of lines) {
1942
+ this.script.push(line);
1943
+ const trimmed = line.trim().toLowerCase();
1944
+ if (trimmed.startsWith("tell ") || /^tell application/.exec(trimmed)) {
1945
+ const target = this.extractTarget(line);
1946
+ this.pushBlock("tell", target);
1947
+ } else if (trimmed.startsWith("if ")) {
1948
+ this.pushBlock("if");
1949
+ } else if (trimmed.startsWith("repeat ") || trimmed === "repeat" || trimmed.startsWith("repeat with ")) {
1950
+ this.pushBlock("repeat");
1951
+ } else if (trimmed === "try" || trimmed.startsWith("try ")) {
1952
+ this.pushBlock("try");
1953
+ } else if (trimmed.startsWith("on ") && !trimmed.startsWith("on error")) {
1954
+ this.pushBlock("on");
1955
+ } else if (trimmed.startsWith("considering ")) {
1956
+ this.pushBlock("considering");
1957
+ } else if (trimmed.startsWith("ignoring ")) {
1958
+ this.pushBlock("ignoring");
1959
+ } else if (trimmed.startsWith("using ")) {
1960
+ this.pushBlock("using");
1961
+ } else if (trimmed.startsWith("with ")) {
1962
+ this.pushBlock("with");
1963
+ } else if (trimmed === "end" || trimmed === "end tell" || trimmed === "end if" || trimmed === "end repeat" || trimmed === "end try" || trimmed === "end considering" || trimmed === "end ignoring" || trimmed === "end using" || trimmed === "end with" || trimmed.startsWith("end ")) {
1964
+ this.popBlock();
1965
+ }
1966
+ }
1967
+ return this;
1968
+ }
1969
+ /**
1970
+ * Helper method to extract the target from a tell statement.
1971
+ * @param line The tell statement line
1972
+ * @returns The extracted target or undefined
1973
+ */
1974
+ extractTarget(line) {
1975
+ const match = /tell\s+(?:application\s+)?"?([^"]+)"?/i.exec(line);
1976
+ return match ? match[1].trim() : void 0;
1977
+ }
1978
+ build() {
1979
+ this.validateBlockStack();
1980
+ return this.script.join("\n");
1981
+ }
1982
+ reset() {
1983
+ this.script = [];
1984
+ this.indentLevel = 0;
1985
+ this.blockStack = [];
1986
+ return this;
1987
+ }
1988
+ };
1989
+ var exec = util.promisify(child_process.exec);
1990
+ var ScriptExecutor = class _ScriptExecutor {
1991
+ static buildFlags(options = {}) {
1992
+ const flags = [];
1993
+ if (options.language) {
1994
+ flags.push(`-l ${options.language}`);
1995
+ }
1996
+ const outputFlags = [];
1997
+ if (options.humanReadable !== false) outputFlags.push("h");
1998
+ if (options.errorToStdout) outputFlags.push("o");
1999
+ if (outputFlags.length > 0) {
2000
+ flags.push(`-s ${outputFlags.join("")}`);
2001
+ }
2002
+ return flags.join(" ");
2003
+ }
2004
+ /**
2005
+ * Execute an AppleScript or JavaScript string
2006
+ *
2007
+ * **Error Handling:**
2008
+ * This method implements comprehensive error handling that safely extracts:
2009
+ * - Error messages from any error type (Error, ErrnoException, etc.)
2010
+ * - Exit codes with proper type checking and defaults
2011
+ *
2012
+ * **Important Notes:**
2013
+ * - Script strings are automatically escaped for shell safety
2014
+ * - Single quotes in scripts are escaped using the pattern: `'` -> `'"'"'`
2015
+ * - Always returns a result object, never throws (errors are captured in result)
2016
+ *
2017
+ * @param script - The AppleScript or JavaScript code to execute
2018
+ * @param options - Execution options (language, output format, etc.)
2019
+ * @returns Promise resolving to execution result with success flag, output, error, and exit code
2020
+ *
2021
+ * @example
2022
+ * ```typescript
2023
+ * const result = await ScriptExecutor.execute('tell app "Finder" to get name');
2024
+ * if (result.success) {
2025
+ * console.log(result.output); // "Finder"
2026
+ * } else {
2027
+ * console.error(`Failed: ${result.error} (exit code: ${result.exitCode})`);
2028
+ * }
2029
+ * ```
2030
+ */
2031
+ static async execute(script, options = {}) {
2032
+ try {
2033
+ const flags = _ScriptExecutor.buildFlags(options);
2034
+ const command = `osascript ${flags} -e '${script.replace(/'/g, `'"'"'`)}'`;
2035
+ const { stdout } = await exec(command);
2036
+ return {
2037
+ success: true,
2038
+ output: stdout.trim(),
2039
+ exitCode: 0
2040
+ };
2041
+ } catch (error) {
2042
+ const message = error instanceof Error ? error.message : String(error);
2043
+ const errnoException = error;
2044
+ const exitCode = typeof errnoException.code === "string" ? Number.parseInt(errnoException.code, 10) || 1 : typeof errnoException.code === "number" ? errnoException.code : 1;
2045
+ return {
2046
+ success: false,
2047
+ output: null,
2048
+ error: message,
2049
+ exitCode
2050
+ };
2051
+ }
2052
+ }
2053
+ /**
2054
+ * Execute an AppleScript or JavaScript file
2055
+ *
2056
+ * **Error Handling:**
2057
+ * Uses the same robust error handling as `execute()` method.
2058
+ * See {@link ScriptExecutor.execute} for details on error handling patterns.
2059
+ *
2060
+ * **Important Notes:**
2061
+ * - File paths are passed directly to osascript (no shell escaping needed)
2062
+ * - File must exist and be readable
2063
+ * - Always returns a result object, never throws
2064
+ *
2065
+ * @param filePath - Path to the script file (.applescript, .scpt, etc.)
2066
+ * @param options - Execution options (language, output format, etc.)
2067
+ * @returns Promise resolving to execution result with success flag, output, error, and exit code
2068
+ *
2069
+ * @example
2070
+ * ```typescript
2071
+ * const result = await ScriptExecutor.executeFile('./scripts/my-script.applescript');
2072
+ * if (result.success) {
2073
+ * console.log(result.output);
2074
+ * }
2075
+ * ```
2076
+ */
2077
+ static async executeFile(filePath, options = {}) {
2078
+ try {
2079
+ const flags = _ScriptExecutor.buildFlags(options);
2080
+ const command = `osascript ${flags} "${filePath}"`;
2081
+ const { stdout } = await exec(command);
2082
+ return {
2083
+ success: true,
2084
+ output: stdout.trim(),
2085
+ exitCode: 0
2086
+ };
2087
+ } catch (error) {
2088
+ const message = error instanceof Error ? error.message : String(error);
2089
+ const errnoException = error;
2090
+ const exitCode = typeof errnoException.code === "string" ? Number.parseInt(errnoException.code, 10) || 1 : typeof errnoException.code === "number" ? errnoException.code : 1;
2091
+ return {
2092
+ success: false,
2093
+ output: null,
2094
+ error: message,
2095
+ exitCode
2096
+ };
2097
+ }
2098
+ }
2099
+ };
2100
+ var exec2 = util.promisify(child_process.exec);
2101
+ function buildCompileFlags(options) {
2102
+ const flags = [];
2103
+ if (options.language) {
2104
+ flags.push(`-l ${options.language}`);
2105
+ }
2106
+ if (options.executeOnly) {
2107
+ flags.push("-x");
2108
+ }
2109
+ if (options.stayOpen) {
2110
+ flags.push("-s");
2111
+ }
2112
+ if (options.useStartupScreen) {
2113
+ flags.push("-u");
2114
+ }
2115
+ return flags;
2116
+ }
2117
+ async function compileScript(script, options = {}) {
2118
+ try {
2119
+ const tmpId = crypto.randomBytes(8).toString("hex");
2120
+ const sourcePath = path.join(os.tmpdir(), `script_${tmpId}.applescript`);
2121
+ await promises.writeFile(sourcePath, script, "utf8");
2122
+ const outputPath = options.outputPath ?? path.join(os.tmpdir(), `script_${tmpId}.scpt`);
2123
+ const outputExt = options.bundleScript ? ".scptd" : ".scpt";
2124
+ const finalOutputPath = outputPath.endsWith(outputExt) ? outputPath : `${outputPath}${outputExt}`;
2125
+ const flags = buildCompileFlags(options);
2126
+ const command = `osacompile ${flags.join(" ")} -o "${finalOutputPath}" "${sourcePath}"`;
2127
+ await exec2(command);
2128
+ return {
2129
+ success: true,
2130
+ outputPath: finalOutputPath
2131
+ };
2132
+ } catch (error) {
2133
+ const err = error;
2134
+ return {
2135
+ success: false,
2136
+ outputPath: "",
2137
+ error: err.message
2138
+ };
2139
+ }
2140
+ }
2141
+ async function compileScriptFile(filePath, options = {}) {
2142
+ try {
2143
+ const outputPath = options.outputPath ?? filePath.replace(/\.[^.]+$/, ".scpt");
2144
+ const outputExt = options.bundleScript ? ".scptd" : ".scpt";
2145
+ const finalOutputPath = outputPath.endsWith(outputExt) ? outputPath : `${outputPath}${outputExt}`;
2146
+ const flags = buildCompileFlags(options);
2147
+ const command = `osacompile ${flags.join(" ")} -o "${finalOutputPath}" "${filePath}"`;
2148
+ await exec2(command);
2149
+ return {
2150
+ success: true,
2151
+ outputPath: finalOutputPath
2152
+ };
2153
+ } catch (error) {
2154
+ const err = error;
2155
+ return {
2156
+ success: false,
2157
+ outputPath: "",
2158
+ error: err.message
2159
+ };
2160
+ }
2161
+ }
2162
+ var exec3 = util.promisify(child_process.exec);
2163
+ async function decompileScript(scriptPath) {
2164
+ try {
2165
+ const { stdout, stderr } = await exec3(`osadecompile "${scriptPath}"`);
2166
+ if (stderr) {
2167
+ return {
2168
+ success: false,
2169
+ error: stderr.trim()
2170
+ };
2171
+ }
2172
+ return {
2173
+ success: true,
2174
+ source: stdout.trim()
2175
+ };
2176
+ } catch (error) {
2177
+ const err = error;
2178
+ return {
2179
+ success: false,
2180
+ error: err.message
2181
+ };
2182
+ }
2183
+ }
2184
+ var exec4 = util.promisify(child_process.exec);
2185
+ function parseCapabilityFlags(flags) {
2186
+ return {
2187
+ compiling: flags.includes("c"),
2188
+ sourceData: flags.includes("g"),
2189
+ coercion: flags.includes("x"),
2190
+ eventHandling: flags.includes("e"),
2191
+ recording: flags.includes("r"),
2192
+ convenience: flags.includes("v"),
2193
+ dialects: flags.includes("d"),
2194
+ appleEvents: flags.includes("h")
2195
+ };
2196
+ }
2197
+ async function getInstalledLanguages() {
2198
+ const { stdout } = await exec4("osalang -L");
2199
+ const lines = stdout.trim().split("\n").filter((line) => line.trim().length > 0);
2200
+ return lines.map((line) => {
2201
+ const [name, manufacturer, flags = "", ...descParts] = line.split(/\s+/);
2202
+ const description = descParts.join(" ").replace(/\(([^)]+)\)/, "$1").trim();
2203
+ return {
2204
+ name,
2205
+ subtype: name,
2206
+ manufacturer,
2207
+ capabilities: parseCapabilityFlags(flags),
2208
+ description
2209
+ };
2210
+ });
2211
+ }
2212
+ async function getDefaultLanguage() {
2213
+ const { stdout } = await exec4("osalang -d");
2214
+ const defaultLang = stdout.trim();
2215
+ const allLangs = await getInstalledLanguages();
2216
+ return allLangs.find((lang) => lang.name === defaultLang) ?? allLangs[0];
2217
+ }
2218
+ async function loadScript(filePath) {
2219
+ try {
2220
+ const content = await promises.readFile(filePath, "utf-8");
2221
+ const builder = new AppleScriptBuilder();
2222
+ builder.loadFromScript(content);
2223
+ return builder;
2224
+ } catch (error) {
2225
+ if (error instanceof Error) {
2226
+ throw new Error(`Failed to load script from ${filePath}: ${error.message}`);
2227
+ }
2228
+ throw error;
2229
+ }
2230
+ }
2231
+ var exec5 = util.promisify(child_process.exec);
2232
+ var sdefCache = /* @__PURE__ */ new Map();
2233
+ async function getSdef(appPath) {
2234
+ try {
2235
+ const { stdout } = await exec5(`sdef "${appPath}"`);
2236
+ return stdout;
2237
+ } catch (error) {
2238
+ if (error instanceof Error) {
2239
+ throw new Error(`Failed to get sdef for ${appPath}: ${error.message}`);
2240
+ }
2241
+ throw error;
2242
+ }
2243
+ }
2244
+ function parseSdef(xml) {
2245
+ const parser = new fastXmlParser.XMLParser({
2246
+ ignoreAttributes: false,
2247
+ attributeNamePrefix: "@_",
2248
+ textNodeName: "#text",
2249
+ parseAttributeValue: false,
2250
+ trimValues: true,
2251
+ isArray: (name) => {
2252
+ return [
2253
+ "suite",
2254
+ "class",
2255
+ "command",
2256
+ "enumeration",
2257
+ "property",
2258
+ "element",
2259
+ "parameter",
2260
+ "enumerator",
2261
+ "type"
2262
+ ].includes(name);
2263
+ }
2264
+ });
2265
+ const parsed = parser.parse(xml);
2266
+ const dictionary = parsed.dictionary;
2267
+ if (!dictionary) {
2268
+ throw new Error("Invalid sdef XML: missing dictionary root element");
2269
+ }
2270
+ const suites = [];
2271
+ const suitesData = Array.isArray(dictionary.suite) ? (
2272
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2273
+ dictionary.suite
2274
+ ) : (
2275
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2276
+ dictionary.suite ? (
2277
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2278
+ [dictionary.suite]
2279
+ ) : []
2280
+ );
2281
+ for (const suiteData of suitesData) {
2282
+ const suite = {
2283
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
2284
+ name: suiteData["@_name"] ?? "",
2285
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
2286
+ code: suiteData["@_code"] ?? "",
2287
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
2288
+ description: suiteData["@_description"],
2289
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2290
+ classes: parseClasses(suiteData.class),
2291
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2292
+ commands: parseCommands(suiteData.command),
2293
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2294
+ enumerations: parseEnumerations(suiteData.enumeration)
2295
+ };
2296
+ suites.push(suite);
2297
+ }
2298
+ return { suites };
2299
+ }
2300
+ function parseClasses(classesData) {
2301
+ if (!classesData) return [];
2302
+ const classes = Array.isArray(classesData) ? classesData : [classesData];
2303
+ return classes.map((cls) => ({
2304
+ name: cls["@_name"] ?? "",
2305
+ code: cls["@_code"] ?? "",
2306
+ description: cls["@_description"],
2307
+ plural: cls["@_plural"],
2308
+ inherits: cls["@_inherits"],
2309
+ properties: parseProperties(cls.property),
2310
+ elements: parseElements(cls.element)
2311
+ }));
2312
+ }
2313
+ function parseProperties(propertiesData) {
2314
+ if (!propertiesData) return [];
2315
+ const properties = Array.isArray(propertiesData) ? propertiesData : [propertiesData];
2316
+ return properties.map((prop) => ({
2317
+ name: prop["@_name"] ?? "",
2318
+ code: prop["@_code"] ?? "",
2319
+ type: parseTypeInfo(prop["@_type"], prop.type),
2320
+ access: prop["@_access"] ?? "rw",
2321
+ description: prop["@_description"]
2322
+ }));
2323
+ }
2324
+ function parseElements(elementsData) {
2325
+ if (!elementsData) return [];
2326
+ const elements = Array.isArray(elementsData) ? elementsData : [elementsData];
2327
+ return elements.map((elem) => ({
2328
+ type: elem["@_type"] ?? "",
2329
+ access: elem["@_access"]
2330
+ }));
2331
+ }
2332
+ function parseCommands(commandsData) {
2333
+ if (!commandsData) return [];
2334
+ const commands = Array.isArray(commandsData) ? commandsData : [commandsData];
2335
+ return commands.map((cmd) => ({
2336
+ name: cmd["@_name"] ?? "",
2337
+ code: cmd["@_code"] ?? "",
2338
+ description: cmd["@_description"],
2339
+ directParameter: cmd["direct-parameter"] ? {
2340
+ type: cmd["direct-parameter"]["@_type"] ?? "",
2341
+ description: cmd["direct-parameter"]["@_description"]
2342
+ } : void 0,
2343
+ parameters: parseParameters(cmd.parameter),
2344
+ result: cmd.result ? {
2345
+ type: cmd.result["@_type"] ?? "",
2346
+ description: cmd.result["@_description"]
2347
+ } : void 0
2348
+ }));
2349
+ }
2350
+ function parseParameters(parametersData) {
2351
+ if (!parametersData) return [];
2352
+ const parameters = Array.isArray(parametersData) ? parametersData : [parametersData];
2353
+ return parameters.map((param) => ({
2354
+ name: param["@_name"] ?? "",
2355
+ code: param["@_code"] ?? "",
2356
+ type: parseTypeInfo(param["@_type"], param.type),
2357
+ optional: param["@_optional"] === "yes",
2358
+ description: param["@_description"]
2359
+ }));
2360
+ }
2361
+ function parseEnumerations(enumerationsData) {
2362
+ if (!enumerationsData) return [];
2363
+ const enumerations = Array.isArray(enumerationsData) ? enumerationsData : [enumerationsData];
2364
+ return enumerations.map((enumData) => ({
2365
+ name: enumData["@_name"] ?? "",
2366
+ code: enumData["@_code"] ?? "",
2367
+ enumerators: parseEnumerators(enumData.enumerator)
2368
+ }));
2369
+ }
2370
+ function parseEnumerators(enumeratorsData) {
2371
+ if (!enumeratorsData) return [];
2372
+ const enumerators = Array.isArray(enumeratorsData) ? enumeratorsData : [enumeratorsData];
2373
+ return enumerators.map((enumer) => ({
2374
+ name: enumer["@_name"] ?? "",
2375
+ code: enumer["@_code"] ?? "",
2376
+ description: enumer["@_description"]
2377
+ }));
2378
+ }
2379
+ function parseTypeInfo(typeAttr, typeElement) {
2380
+ if (typeAttr) {
2381
+ return { type: typeAttr };
2382
+ }
2383
+ if (typeElement) {
2384
+ const types = Array.isArray(typeElement) ? typeElement : [typeElement];
2385
+ const firstType = types[0];
2386
+ return {
2387
+ type: firstType["@_type"] ?? "any",
2388
+ list: firstType["@_list"] === "yes"
2389
+ };
2390
+ }
2391
+ return { type: "any" };
2392
+ }
2393
+ async function getApplicationDictionary(appPath, useCache = true) {
2394
+ if (useCache && sdefCache.has(appPath)) {
2395
+ const cached = sdefCache.get(appPath);
2396
+ if (cached) return cached;
2397
+ }
2398
+ const xml = await getSdef(appPath);
2399
+ const dictionary = parseSdef(xml);
2400
+ if (useCache) {
2401
+ sdefCache.set(appPath, dictionary);
2402
+ }
2403
+ return dictionary;
2404
+ }
2405
+ function clearSdefCache() {
2406
+ sdefCache.clear();
2407
+ }
2408
+ function getAllCommands(dictionary) {
2409
+ return dictionary.suites.flatMap((suite) => suite.commands);
2410
+ }
2411
+ function getAllClasses(dictionary) {
2412
+ return dictionary.suites.flatMap((suite) => suite.classes);
2413
+ }
2414
+ function findCommand(dictionary, commandName) {
2415
+ for (const suite of dictionary.suites) {
2416
+ const command = suite.commands.find((cmd) => cmd.name === commandName);
2417
+ if (command) return command;
2418
+ }
2419
+ return;
2420
+ }
2421
+ function findClass(dictionary, className) {
2422
+ for (const suite of dictionary.suites) {
2423
+ const cls = suite.classes.find((c) => c.name === className);
2424
+ if (cls) return cls;
2425
+ }
2426
+ return;
2427
+ }
2428
+
2429
+ // src/sources/index.ts
2430
+ var sources_exports = {};
2431
+ __export(sources_exports, {
2432
+ applications: () => applications_exports,
2433
+ system: () => system_exports,
2434
+ windows: () => windows_exports
2435
+ });
2436
+
2437
+ // src/sources/applications.ts
2438
+ var applications_exports = {};
2439
+ __export(applications_exports, {
2440
+ activate: () => activate,
2441
+ getAll: () => getAll,
2442
+ getByName: () => getByName,
2443
+ getFrontmost: () => getFrontmost,
2444
+ hide: () => hide,
2445
+ isRunning: () => isRunning,
2446
+ launch: () => launch,
2447
+ quit: () => quit,
2448
+ show: () => show
2449
+ });
2450
+ async function getAll(includeBackgroundApps = false) {
2451
+ const whereClause = includeBackgroundApps ? "" : " whose background only is false";
2452
+ const script = createScript().tell("System Events").set("appsList", []).forEach(
2453
+ "proc",
2454
+ `every process${whereClause}`,
2455
+ (b) => b.tryCatch(
2456
+ (tryBlock) => tryBlock.setExpression("procName", (e) => e.property("proc", "name")).setExpression("procBundleId", (e) => e.property("proc", "bundle identifier")).setExpression("procPid", (e) => e.property("proc", "unix id")).setExpression("procVisible", (e) => e.property("proc", "visible")).setExpression("procFrontmost", (e) => e.property("proc", "frontmost")).setExpression("procWindowCount", (e) => e.count(e.property("proc", "windows"))).pickEndRecord("appsList", "proc", {
2457
+ name: "procName",
2458
+ bundleId: "procBundleId",
2459
+ pid: "procPid",
2460
+ visible: "procVisible",
2461
+ frontmost: "procFrontmost",
2462
+ windowCount: "procWindowCount"
2463
+ }),
2464
+ (catchBlock) => catchBlock.comment("Skip processes with errors")
2465
+ )
2466
+ ).returnAsJson("appsList", {
2467
+ name: "name",
2468
+ bundleId: "bundleId",
2469
+ pid: "pid",
2470
+ visible: "visible",
2471
+ frontmost: "frontmost",
2472
+ windowCount: "windowCount"
2473
+ }).endtell();
2474
+ const result = await ScriptExecutor.execute(script.build());
2475
+ if (!result.success) {
2476
+ throw new Error(`Failed to get applications: ${result.error}`);
2477
+ }
2478
+ return JSON.parse(result.output ?? "[]");
2479
+ }
2480
+ async function getFrontmost() {
2481
+ const script = createScript().tell("System Events").setExpression("frontProc", "first process whose frontmost is true").returnJsonObject({
2482
+ name: "name of frontProc",
2483
+ bundleId: "bundle identifier of frontProc",
2484
+ pid: "unix id of frontProc",
2485
+ visible: "visible of frontProc",
2486
+ frontmost: "true",
2487
+ windowCount: "count of windows of frontProc"
2488
+ }).endtell();
2489
+ const result = await ScriptExecutor.execute(script.build());
2490
+ if (!result.success) {
2491
+ throw new Error(`Failed to get frontmost application: ${result.error}`);
2492
+ }
2493
+ return JSON.parse(result.output ?? "{}");
2494
+ }
2495
+ async function isRunning(appName) {
2496
+ const escapedAppName = appName.replace(/"/g, '\\"');
2497
+ const script = `
2498
+ tell application "System Events"
2499
+ return exists (processes where name is "${escapedAppName}")
2500
+ end tell
2501
+ `;
2502
+ const result = await ScriptExecutor.execute(script);
2503
+ return result.success && result.output === "true";
2504
+ }
2505
+ async function getByName(appName) {
2506
+ const allApps = await getAll(true);
2507
+ return allApps.find((app) => app.name === appName) ?? null;
2508
+ }
2509
+ async function activate(appName) {
2510
+ const script = createScript().tell(appName).activate().endtell();
2511
+ const result = await ScriptExecutor.execute(script.build());
2512
+ if (!result.success) {
2513
+ throw new Error(`Failed to activate ${appName}: ${result.error}`);
2514
+ }
2515
+ }
2516
+ async function launch(appName) {
2517
+ const script = createScript().tell(appName).launch().endtell();
2518
+ const result = await ScriptExecutor.execute(script.build());
2519
+ if (!result.success) {
2520
+ throw new Error(`Failed to launch ${appName}: ${result.error}`);
2521
+ }
2522
+ }
2523
+ async function quit(appName) {
2524
+ const script = createScript().tell(appName).quit().endtell();
2525
+ const result = await ScriptExecutor.execute(script.build());
2526
+ if (!result.success) {
2527
+ throw new Error(`Failed to quit ${appName}: ${result.error}`);
2528
+ }
2529
+ }
2530
+ async function hide(appName) {
2531
+ const script = createScript().tell("System Events").tell(`process "${appName.replace(/"/g, '\\"')}"`).raw("set visible to false").endtell().endtell();
2532
+ const result = await ScriptExecutor.execute(script.build());
2533
+ if (!result.success) {
2534
+ throw new Error(`Failed to hide ${appName}: ${result.error}`);
2535
+ }
2536
+ }
2537
+ async function show(appName) {
2538
+ const script = createScript().tell("System Events").tell(`process "${appName.replace(/"/g, '\\"')}"`).raw("set visible to true").endtell().endtell();
2539
+ const result = await ScriptExecutor.execute(script.build());
2540
+ if (!result.success) {
2541
+ throw new Error(`Failed to show ${appName}: ${result.error}`);
2542
+ }
2543
+ }
2544
+
2545
+ // src/sources/system.ts
2546
+ var system_exports = {};
2547
+ __export(system_exports, {
2548
+ getClipboard: () => getClipboard,
2549
+ getDisplays: () => getDisplays,
2550
+ getInfo: () => getInfo,
2551
+ getPath: () => getPath,
2552
+ getUptime: () => getUptime,
2553
+ getVolumes: () => getVolumes,
2554
+ isDarkMode: () => isDarkMode,
2555
+ setClipboard: () => setClipboard
2556
+ });
2557
+ async function getInfo() {
2558
+ const script = `
2559
+ set computerName to computer name of (system info)
2560
+ set userName to short user name of (system info)
2561
+ set osVersion to system version of (system info)
2562
+
2563
+ set homeDir to POSIX path of (path to home folder)
2564
+
2565
+ -- Get hostname via shell
2566
+ set hostname to do shell script "hostname"
2567
+
2568
+ -- Get architecture via shell
2569
+ set arch to do shell script "uname -m"
2570
+
2571
+ return "{" & \xAC
2572
+ "\\"computerName\\":\\"" & computerName & "\\"," & \xAC
2573
+ "\\"hostname\\":\\"" & hostname & "\\"," & \xAC
2574
+ "\\"osVersion\\":\\"" & osVersion & "\\"," & \xAC
2575
+ "\\"systemVersion\\":\\"" & osVersion & "\\"," & \xAC
2576
+ "\\"userName\\":\\"" & userName & "\\"," & \xAC
2577
+ "\\"homeDirectory\\":\\"" & homeDir & "\\"," & \xAC
2578
+ "\\"architecture\\":\\"" & arch & "\\"" & \xAC
2579
+ "}"
2580
+ `;
2581
+ const result = await ScriptExecutor.execute(script);
2582
+ if (!result.success) {
2583
+ throw new Error(`Failed to get system info: ${result.error}`);
2584
+ }
2585
+ return JSON.parse(result.output ?? "{}");
2586
+ }
2587
+ async function getVolumes() {
2588
+ const script = createScript().tell("Finder").set("volumesList", []).forEach(
2589
+ "aDisk",
2590
+ "every disk",
2591
+ (b) => b.tryCatch(
2592
+ (tryBlock) => tryBlock.setExpression("diskName", (e) => e.property("aDisk", "name")).setExpression("diskCapacity", (e) => e.property("aDisk", "capacity")).setExpression("diskFreeSpace", (e) => e.property("aDisk", "free space")).setExpression("diskFormat", (e) => e.property("aDisk", "format")).setExpression("diskEjectable", (e) => e.property("aDisk", "ejectable")).raw("set diskUsed to diskCapacity - diskFreeSpace").setEndRecord("volumesList", {
2593
+ name: "diskName",
2594
+ capacity: "diskCapacity",
2595
+ freeSpace: "diskFreeSpace",
2596
+ usedSpace: "diskUsed",
2597
+ format: "diskFormat",
2598
+ ejectable: "diskEjectable"
2599
+ }),
2600
+ (catchBlock) => catchBlock.comment("Skip disks with errors")
2601
+ )
2602
+ ).returnAsJson("volumesList", {
2603
+ name: "name",
2604
+ capacity: "capacity",
2605
+ freeSpace: "freeSpace",
2606
+ usedSpace: "usedSpace",
2607
+ format: "format",
2608
+ ejectable: "ejectable"
2609
+ }).endtell();
2610
+ const result = await ScriptExecutor.execute(script.build());
2611
+ if (!result.success) {
2612
+ throw new Error(`Failed to get volumes: ${result.error}`);
2613
+ }
2614
+ return JSON.parse(result.output ?? "[]");
2615
+ }
2616
+ async function getDisplays() {
2617
+ const script = `
2618
+ do shell script "system_profiler SPDisplaysDataType | grep Resolution"
2619
+ `;
2620
+ const result = await ScriptExecutor.execute(script);
2621
+ if (!result.success) {
2622
+ return [
2623
+ {
2624
+ id: 1,
2625
+ bounds: {
2626
+ x: 0,
2627
+ y: 0,
2628
+ width: 0,
2629
+ height: 0
2630
+ }
2631
+ }
2632
+ ];
2633
+ }
2634
+ const lines = (result.output ?? "").split("\n");
2635
+ const displays = lines.map((line, idx) => {
2636
+ const match = /Resolution:\s+(\d+)\s+x\s+(\d+)/.exec(line);
2637
+ if (match) {
2638
+ return {
2639
+ id: idx + 1,
2640
+ bounds: {
2641
+ x: 0,
2642
+ y: 0,
2643
+ width: Number.parseInt(match[1], 10),
2644
+ height: Number.parseInt(match[2], 10)
2645
+ }
2646
+ };
2647
+ }
2648
+ return null;
2649
+ }).filter((d) => d !== null);
2650
+ return displays.length > 0 ? displays : [
2651
+ {
2652
+ id: 1,
2653
+ bounds: {
2654
+ x: 0,
2655
+ y: 0,
2656
+ width: 0,
2657
+ height: 0
2658
+ }
2659
+ }
2660
+ ];
2661
+ }
2662
+ async function getClipboard() {
2663
+ const script = `
2664
+ try
2665
+ return the clipboard as text
2666
+ on error
2667
+ return ""
2668
+ end try
2669
+ `;
2670
+ const result = await ScriptExecutor.execute(script);
2671
+ if (!result.success) {
2672
+ return "";
2673
+ }
2674
+ return result.output ?? "";
2675
+ }
2676
+ async function setClipboard(text) {
2677
+ const script = createScript().raw(`set the clipboard to "${text.replace(/"/g, '\\"')}"`).build();
2678
+ const result = await ScriptExecutor.execute(script);
2679
+ if (!result.success) {
2680
+ throw new Error(`Failed to set clipboard: ${result.error}`);
2681
+ }
2682
+ }
2683
+ async function getPath(folder) {
2684
+ const folderMap = {
2685
+ home: "home folder",
2686
+ desktop: "desktop folder",
2687
+ documents: "documents folder",
2688
+ downloads: "downloads folder",
2689
+ applications: "applications folder",
2690
+ library: "library folder",
2691
+ "temporary items": "temporary items folder",
2692
+ music: "music folder",
2693
+ pictures: "pictures folder",
2694
+ movies: "movies folder",
2695
+ public: "public folder"
2696
+ };
2697
+ const folderName = folderMap[folder] || "home folder";
2698
+ const script = `POSIX path of (path to ${folderName})`;
2699
+ const result = await ScriptExecutor.execute(script);
2700
+ if (!result.success) {
2701
+ throw new Error(`Failed to get path for ${folder}: ${result.error}`);
2702
+ }
2703
+ return (result.output ?? "").trim();
2704
+ }
2705
+ async function isDarkMode() {
2706
+ const script = 'tell application "System Events" to tell appearance preferences to return dark mode';
2707
+ const result = await ScriptExecutor.execute(script);
2708
+ return result.success && (result.output ?? "").trim() === "true";
2709
+ }
2710
+ async function getUptime() {
2711
+ const script = `do shell script "sysctl -n kern.boottime | awk '{print $4}' | sed 's/,//'"`;
2712
+ const result = await ScriptExecutor.execute(script);
2713
+ if (!result.success) {
2714
+ throw new Error(`Failed to get uptime: ${result.error}`);
2715
+ }
2716
+ const bootTime = Number.parseInt((result.output ?? "0").trim(), 10);
2717
+ const now = Math.floor(Date.now() / 1e3);
2718
+ const validBootTime = Number.isNaN(bootTime) ? 0 : bootTime;
2719
+ return now - validBootTime;
2720
+ }
2721
+
2722
+ // src/sources/windows.ts
2723
+ var windows_exports = {};
2724
+ __export(windows_exports, {
2725
+ getAll: () => getAll2,
2726
+ getByApp: () => getByApp,
2727
+ getCountByApp: () => getCountByApp,
2728
+ getFrontmost: () => getFrontmost2
2729
+ });
2730
+ async function getAll2() {
2731
+ const script = createScript().tell("System Events").set("windowsList", []).forEach(
2732
+ "proc",
2733
+ "every process whose visible is true",
2734
+ (b) => b.setExpression("appName", (e) => e.property("proc", "name")).forEach(
2735
+ "win",
2736
+ "windows of proc",
2737
+ (inner) => inner.tryCatch(
2738
+ (tryBlock) => tryBlock.setExpression("winName", (e) => e.property("win", "name")).setExpression("winId", (e) => e.property("win", "id")).setExpression("winBounds", (e) => e.property("win", "position")).setExpression("winSize", (e) => e.property("win", "size")).setExpression(
2739
+ "winMinimized",
2740
+ (e) => e.property("win", 'value of attribute "AXMinimized"')
2741
+ ).setExpression(
2742
+ "winZoomed",
2743
+ (e) => e.property("win", 'value of attribute "AXFullScreen"')
2744
+ ).setEndRecord("windowsList", {
2745
+ name: "winName",
2746
+ app: "appName",
2747
+ id: "winId",
2748
+ x: "item 1 of winBounds",
2749
+ y: "item 2 of winBounds",
2750
+ width: "item 1 of winSize",
2751
+ height: "item 2 of winSize",
2752
+ minimized: "winMinimized",
2753
+ zoomed: "winZoomed",
2754
+ visible: "true"
2755
+ }),
2756
+ (catchBlock) => catchBlock.comment("Skip windows with errors")
2757
+ )
2758
+ )
2759
+ ).returnAsJson("windowsList", {
2760
+ name: "name",
2761
+ app: "app",
2762
+ id: "id",
2763
+ x: "x",
2764
+ y: "y",
2765
+ width: "width",
2766
+ height: "height",
2767
+ minimized: "minimized",
2768
+ zoomed: "zoomed",
2769
+ visible: "visible"
2770
+ }).endtell();
2771
+ const result = await ScriptExecutor.execute(script.build());
2772
+ if (!result.success) {
2773
+ throw new Error(`Failed to get windows: ${result.error}`);
2774
+ }
2775
+ const windows = JSON.parse(result.output ?? "[]");
2776
+ return windows.map((win) => {
2777
+ const w = win;
2778
+ return {
2779
+ name: w.name ?? "",
2780
+ app: w.app ?? "",
2781
+ id: Number(w.id ?? 0),
2782
+ bounds: {
2783
+ x: Number(w.x ?? 0),
2784
+ y: Number(w.y ?? 0),
2785
+ width: Number(w.width ?? 0),
2786
+ height: Number(w.height ?? 0)
2787
+ },
2788
+ minimized: Boolean(w.minimized),
2789
+ zoomed: Boolean(w.zoomed),
2790
+ visible: Boolean(w.visible)
2791
+ };
2792
+ });
2793
+ }
2794
+ async function getByApp(appName) {
2795
+ const allWindows = await getAll2();
2796
+ return allWindows.filter((win) => win.app === appName);
2797
+ }
2798
+ async function getFrontmost2() {
2799
+ const script = `
2800
+ tell application "System Events"
2801
+ set frontApp to name of first process whose frontmost is true
2802
+ tell process frontApp
2803
+ try
2804
+ set frontWin to front window
2805
+ set winName to name of frontWin
2806
+ set winBounds to position of frontWin
2807
+ set winSize to size of frontWin
2808
+ set winMinimized to value of attribute "AXMinimized" of frontWin
2809
+ set winZoomed to value of attribute "AXFullScreen" of frontWin
2810
+
2811
+ return "{" & \xAC
2812
+ "\\"name\\":\\"" & winName & "\\"," & \xAC
2813
+ "\\"app\\":\\"" & frontApp & "\\"," & \xAC
2814
+ "\\"x\\":" & (item 1 of winBounds) & "," & \xAC
2815
+ "\\"y\\":" & (item 2 of winBounds) & "," & \xAC
2816
+ "\\"width\\":" & (item 1 of winSize) & "," & \xAC
2817
+ "\\"height\\":" & (item 2 of winSize) & "," & \xAC
2818
+ "\\"minimized\\":" & winMinimized & "," & \xAC
2819
+ "\\"zoomed\\":" & winZoomed & "," & \xAC
2820
+ "\\"visible\\":true" & \xAC
2821
+ "}"
2822
+ on error
2823
+ return ""
2824
+ end try
2825
+ end tell
2826
+ end tell
2827
+ `;
2828
+ const result = await ScriptExecutor.execute(script);
2829
+ if (!(result.success && result.output)) {
2830
+ return null;
2831
+ }
2832
+ try {
2833
+ const win = JSON.parse(result.output);
2834
+ return {
2835
+ name: win.name ?? "",
2836
+ app: win.app ?? "",
2837
+ id: 0,
2838
+ // Frontmost window query doesn't include ID
2839
+ bounds: {
2840
+ x: Number(win.x ?? 0),
2841
+ y: Number(win.y ?? 0),
2842
+ width: Number(win.width ?? 0),
2843
+ height: Number(win.height ?? 0)
2844
+ },
2845
+ minimized: Boolean(win.minimized),
2846
+ zoomed: Boolean(win.zoomed),
2847
+ visible: Boolean(win.visible)
2848
+ };
2849
+ } catch {
2850
+ return null;
2851
+ }
2852
+ }
2853
+ async function getCountByApp() {
2854
+ const allWindows = await getAll2();
2855
+ const counts = {};
2856
+ for (const win of allWindows) {
2857
+ counts[win.app] = (counts[win.app] || 0) + 1;
2858
+ }
2859
+ return counts;
2860
+ }
2861
+
2862
+ // src/validator.ts
2863
+ var ScriptValidator = class _ScriptValidator {
2864
+ dictionary;
2865
+ appName;
2866
+ constructor(dictionary, appName) {
2867
+ this.dictionary = dictionary;
2868
+ this.appName = appName;
2869
+ }
2870
+ /**
2871
+ * Create a validator for a specific application
2872
+ */
2873
+ static async forApplication(appPath) {
2874
+ const dictionary = await getApplicationDictionary(appPath);
2875
+ const appName = appPath.split("/").pop()?.replace(/\.app$/, "") ?? "Unknown";
2876
+ return new _ScriptValidator(dictionary, appName);
2877
+ }
2878
+ /**
2879
+ * Validate a script string
2880
+ */
2881
+ validate(script, options = {}) {
2882
+ const { strictness = "normal", provideSuggestions = true } = options;
2883
+ const issues = [];
2884
+ const tellBlocks = this.extractTellBlocks(script);
2885
+ for (const block of tellBlocks) {
2886
+ this.validateCommands(block.content, issues, provideSuggestions);
2887
+ this.validateProperties(block.content, issues, provideSuggestions);
2888
+ }
2889
+ const errors = issues.filter((i) => i.severity === "error");
2890
+ const warnings = issues.filter((i) => i.severity === "warning");
2891
+ const info = issues.filter((i) => i.severity === "info");
2892
+ let valid = errors.length === 0;
2893
+ if (strictness === "strict") {
2894
+ valid = valid && warnings.length === 0;
2895
+ }
2896
+ return {
2897
+ valid,
2898
+ issues,
2899
+ errors,
2900
+ warnings,
2901
+ info
2902
+ };
2903
+ }
2904
+ /**
2905
+ * Extract tell application blocks from script
2906
+ */
2907
+ extractTellBlocks(script) {
2908
+ const blocks = [];
2909
+ const lines = script.split("\n");
2910
+ let currentBlock = null;
2911
+ let blockDepth = 0;
2912
+ for (const line of lines) {
2913
+ const trimmed = line.trim();
2914
+ const tellMatch = /^tell\s+application\s+"([^"]+)"/i.exec(trimmed);
2915
+ if (tellMatch) {
2916
+ const target = tellMatch[1];
2917
+ if (currentBlock) {
2918
+ blockDepth++;
2919
+ } else {
2920
+ currentBlock = { target, content: "", depth: 0 };
2921
+ }
2922
+ continue;
2923
+ }
2924
+ if (/^end\s+tell/i.test(trimmed)) {
2925
+ if (blockDepth > 0) {
2926
+ blockDepth--;
2927
+ } else if (currentBlock) {
2928
+ blocks.push(currentBlock);
2929
+ currentBlock = null;
2930
+ }
2931
+ continue;
2932
+ }
2933
+ if (currentBlock) {
2934
+ currentBlock.content += line + "\n";
2935
+ }
2936
+ }
2937
+ return blocks;
2938
+ }
2939
+ /**
2940
+ * Validate commands in script content
2941
+ */
2942
+ validateCommands(content, issues, provideSuggestions) {
2943
+ const lines = content.split("\n");
2944
+ const keywords = /* @__PURE__ */ new Set([
2945
+ "if",
2946
+ "then",
2947
+ "else",
2948
+ "end",
2949
+ "repeat",
2950
+ "tell",
2951
+ "to",
2952
+ "of",
2953
+ "on",
2954
+ "return",
2955
+ "try",
2956
+ "error",
2957
+ "set",
2958
+ "get",
2959
+ "copy",
2960
+ "count",
2961
+ "exit",
2962
+ "considering",
2963
+ "ignoring",
2964
+ "with",
2965
+ "without",
2966
+ "using",
2967
+ "my",
2968
+ "its",
2969
+ "a",
2970
+ "an",
2971
+ "the",
2972
+ "some",
2973
+ "every",
2974
+ "whose"
2975
+ ]);
2976
+ for (const line of lines) {
2977
+ const trimmed = line.trim();
2978
+ if (!trimmed || trimmed.startsWith("--")) continue;
2979
+ const words = trimmed.split(/\s+/);
2980
+ if (words.length === 0) continue;
2981
+ const potentialCommand = words[0].toLowerCase();
2982
+ if (keywords.has(potentialCommand)) continue;
2983
+ if (/^[a-z_][a-z0-9_]*$/i.test(potentialCommand)) {
2984
+ const command = findCommand(this.dictionary, potentialCommand);
2985
+ if (!command) {
2986
+ const issue = {
2987
+ severity: "warning",
2988
+ message: `Command '${potentialCommand}' might not exist in ${this.appName}`,
2989
+ code: "unknown-command"
2990
+ };
2991
+ if (provideSuggestions) {
2992
+ const suggestion = this.suggestSimilarCommand(potentialCommand);
2993
+ if (suggestion) {
2994
+ issue.suggestion = `Did you mean '${suggestion.name}'? ${suggestion.description ?? ""}`;
2995
+ }
2996
+ }
2997
+ issues.push(issue);
2998
+ }
2999
+ }
3000
+ }
3001
+ }
3002
+ /**
3003
+ * Validate property access in script content
3004
+ */
3005
+ validateProperties(content, issues, _provideSuggestions) {
3006
+ const setPattern = /set\s+([\w\s]+?)\s+(?:of\s+|to\s+)/gi;
3007
+ let match;
3008
+ while ((match = setPattern.exec(content)) !== null) {
3009
+ const propertyName = match[1].trim();
3010
+ if (propertyName && propertyName.length > 0) {
3011
+ const readOnlyProp = this.findReadOnlyProperty(propertyName);
3012
+ if (readOnlyProp) {
3013
+ issues.push({
3014
+ severity: "error",
3015
+ message: `Property '${propertyName}' is read-only and cannot be set`,
3016
+ code: "readonly-property",
3017
+ suggestion: `Property '${propertyName}' in class '${readOnlyProp.className}' has access level 'r' (read-only)`
3018
+ });
3019
+ }
3020
+ }
3021
+ }
3022
+ }
3023
+ /**
3024
+ * Find a read-only property by name across all classes
3025
+ */
3026
+ findReadOnlyProperty(propertyName) {
3027
+ const classes = getAllClasses(this.dictionary);
3028
+ for (const cls of classes) {
3029
+ const prop = cls.properties.find(
3030
+ (p) => p.name.toLowerCase() === propertyName.toLowerCase() && p.access === "r"
3031
+ );
3032
+ if (prop) {
3033
+ return { className: cls.name, property: prop };
3034
+ }
3035
+ }
3036
+ return null;
3037
+ }
3038
+ /**
3039
+ * Suggest a similar command name using string similarity
3040
+ */
3041
+ suggestSimilarCommand(commandName) {
3042
+ const allCommands = this.dictionary.suites.flatMap((suite) => suite.commands);
3043
+ let bestMatch;
3044
+ let bestScore = Number.POSITIVE_INFINITY;
3045
+ for (const cmd of allCommands) {
3046
+ const distance = this.levenshteinDistance(commandName.toLowerCase(), cmd.name.toLowerCase());
3047
+ if (distance < bestScore && distance <= 3) {
3048
+ bestScore = distance;
3049
+ bestMatch = cmd;
3050
+ }
3051
+ }
3052
+ return bestMatch;
3053
+ }
3054
+ /**
3055
+ * Calculate Levenshtein distance between two strings
3056
+ */
3057
+ levenshteinDistance(a, b) {
3058
+ const matrix = [];
3059
+ for (let i = 0; i <= b.length; i++) {
3060
+ matrix[i] = [i];
3061
+ }
3062
+ for (let j = 0; j <= a.length; j++) {
3063
+ matrix[0][j] = j;
3064
+ }
3065
+ for (let i = 1; i <= b.length; i++) {
3066
+ for (let j = 1; j <= a.length; j++) {
3067
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
3068
+ matrix[i][j] = matrix[i - 1][j - 1];
3069
+ } else {
3070
+ matrix[i][j] = Math.min(
3071
+ matrix[i - 1][j - 1] + 1,
3072
+ // substitution
3073
+ matrix[i][j - 1] + 1,
3074
+ // insertion
3075
+ matrix[i - 1][j] + 1
3076
+ // deletion
3077
+ );
3078
+ }
3079
+ }
3080
+ }
3081
+ return matrix[b.length][a.length];
3082
+ }
3083
+ /**
3084
+ * Validate a command and its parameters
3085
+ */
3086
+ validateCommand(commandName, parameters = {}) {
3087
+ const issues = [];
3088
+ const command = findCommand(this.dictionary, commandName);
3089
+ if (!command) {
3090
+ issues.push({
3091
+ severity: "error",
3092
+ message: `Command '${commandName}' does not exist in ${this.appName}`,
3093
+ code: "unknown-command"
3094
+ });
3095
+ return issues;
3096
+ }
3097
+ const requiredParams = command.parameters.filter((p) => !p.optional);
3098
+ for (const param of requiredParams) {
3099
+ if (!(param.name in parameters)) {
3100
+ issues.push({
3101
+ severity: "error",
3102
+ message: `Required parameter '${param.name}' is missing for command '${commandName}'`,
3103
+ code: "missing-parameter",
3104
+ suggestion: param.description
3105
+ });
3106
+ }
3107
+ }
3108
+ for (const paramName of Object.keys(parameters)) {
3109
+ const paramDef = command.parameters.find((p) => p.name === paramName);
3110
+ if (!paramDef) {
3111
+ issues.push({
3112
+ severity: "warning",
3113
+ message: `Unknown parameter '${paramName}' for command '${commandName}'`,
3114
+ code: "unknown-parameter"
3115
+ });
3116
+ }
3117
+ }
3118
+ return issues;
3119
+ }
3120
+ /**
3121
+ * Validate property access on a class
3122
+ */
3123
+ validatePropertyAccess(className, propertyName, accessType) {
3124
+ const issues = [];
3125
+ const cls = findClass(this.dictionary, className);
3126
+ if (!cls) {
3127
+ issues.push({
3128
+ severity: "error",
3129
+ message: `Class '${className}' does not exist in ${this.appName}`,
3130
+ code: "unknown-class"
3131
+ });
3132
+ return issues;
3133
+ }
3134
+ const property = cls.properties.find((p) => p.name === propertyName);
3135
+ if (!property) {
3136
+ issues.push({
3137
+ severity: "error",
3138
+ message: `Property '${propertyName}' does not exist on class '${className}'`,
3139
+ code: "unknown-property"
3140
+ });
3141
+ return issues;
3142
+ }
3143
+ if (accessType === "write" && property.access === "r") {
3144
+ issues.push({
3145
+ severity: "error",
3146
+ message: `Property '${propertyName}' is read-only on class '${className}'`,
3147
+ code: "readonly-property",
3148
+ suggestion: `Property has access level 'r'. Available properties with write access: ${cls.properties.filter((p) => p.access === "rw").map((p) => p.name).join(", ")}`
3149
+ });
3150
+ }
3151
+ return issues;
3152
+ }
3153
+ /**
3154
+ * Get all available commands in the application
3155
+ */
3156
+ getAvailableCommands() {
3157
+ return this.dictionary.suites.flatMap((suite) => suite.commands);
3158
+ }
3159
+ /**
3160
+ * Get all available classes in the application
3161
+ */
3162
+ getAvailableClasses() {
3163
+ return getAllClasses(this.dictionary);
3164
+ }
3165
+ };
3166
+ async function validateScript(script, appPath, options) {
3167
+ const validator = await ScriptValidator.forApplication(appPath);
3168
+ return validator.validate(script, options);
3169
+ }
3170
+
3171
+ // src/index.ts
3172
+ var createScript = () => new AppleScriptBuilder();
3173
+ async function runScript(script, options) {
3174
+ const scriptString = typeof script === "string" ? script : script.build();
3175
+ const result = await ScriptExecutor.execute(scriptString, options);
3176
+ if (result.success && result.output) {
3177
+ const trimmed = result.output.trim();
3178
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
3179
+ try {
3180
+ const parsed = JSON.parse(result.output);
3181
+ const parsedResult = {
3182
+ success: result.success,
3183
+ output: parsed,
3184
+ exitCode: result.exitCode
3185
+ };
3186
+ return parsedResult;
3187
+ } catch {
3188
+ }
3189
+ }
3190
+ }
3191
+ return result;
3192
+ }
3193
+ var runScriptFile = async (filePath, options) => ScriptExecutor.executeFile(filePath, options);
3194
+
3195
+ exports.AppleScriptBuilder = AppleScriptBuilder;
3196
+ exports.ExprBuilder = ExprBuilder;
3197
+ exports.ScriptExecutor = ScriptExecutor;
3198
+ exports.ScriptValidator = ScriptValidator;
3199
+ exports.clearSdefCache = clearSdefCache;
3200
+ exports.compileScript = compileScript;
3201
+ exports.compileScriptFile = compileScriptFile;
3202
+ exports.createScript = createScript;
3203
+ exports.decompileScript = decompileScript;
3204
+ exports.expr = expr;
3205
+ exports.findClass = findClass;
3206
+ exports.findCommand = findCommand;
3207
+ exports.getAllClasses = getAllClasses;
3208
+ exports.getAllCommands = getAllCommands;
3209
+ exports.getApplicationDictionary = getApplicationDictionary;
3210
+ exports.getDefaultLanguage = getDefaultLanguage;
3211
+ exports.getInstalledLanguages = getInstalledLanguages;
3212
+ exports.getSdef = getSdef;
3213
+ exports.loadScript = loadScript;
3214
+ exports.parseSdef = parseSdef;
3215
+ exports.runScript = runScript;
3216
+ exports.runScriptFile = runScriptFile;
3217
+ exports.sources = sources_exports;
3218
+ exports.validateScript = validateScript;
3219
+ //# sourceMappingURL=index.js.map
3220
+ //# sourceMappingURL=index.js.map