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