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